Sfoglia il codice sorgente

add `put_scope()` to replace `output()`

wangweimin 3 anni fa
parent
commit
3a433cda0a

+ 37 - 25
docs/guide.rst

@@ -297,29 +297,6 @@ In addition, you can use `put_widget() <pywebio.output.put_widget>` to make your
 
 
 For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions list <output_func_list>`
 For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions list <output_func_list>`
 
 
-**Placeholder**
-
-When using combination output, if you want to dynamically update the ``put_xxx()`` content after it has been output,
-you can use the `output() <pywebio.output.output>` function. `output() <pywebio.output.output>` is like a placeholder,
-it can be passed in anywhere that ``put_xxx()`` can passed in. And after being output, the content can also be modified:
-
-.. exportable-codeblock::
-    :name: output
-    :summary: Output placeholder——`output()`
-
-    hobby = output('Coding')  # equal to output(put_text('Coding'))
-    put_table([
-        ['Name', 'Hobbies'],
-        ['Wang', hobby]      # hobby is initialized to Coding
-    ])
-    ## ----
-
-    hobby.reset('Movie')  # hobby is reset to Movie
-    ## ----
-    hobby.append('Music', put_text('Drama'))   # append Music, Drama to hobby
-    ## ----
-    hobby.insert(0, put_markdown('**Coding**'))  # insert the Coding into the top of the hobby
-
 **Context Manager**
 **Context Manager**
 
 
 Some output functions that accept ``put_xxx()`` calls as content can be used as context manager:
 Some output functions that accept ``put_xxx()`` calls as content can be used as context manager:
@@ -525,11 +502,46 @@ The above code will generate the following scope layout::
    │ └─────────────────────┘ │
    │ └─────────────────────┘ │
    └─────────────────────────┘
    └─────────────────────────┘
 
 
+.. _put_scope:
+
+**put_scope()**
+
+We already know that the scope is a container of output content. So can we use this container as a sub-item
+of a output (like, set a cell in table as a container)? Yes, you can use `put_scope() <pywebio.output.put_scope>` to
+create a scope explicitly.
+The function name starts with ``put_``, which means it can be pass to the functions that accept ``put_xxx()`` calls.
+
+.. exportable-codeblock::
+    :name: put_scope
+    :summary: `put_scope()`
+
+    put_table([
+        ['Name', 'Hobbies'],
+        ['Tom', put_scope('hobby', content=put_text('Coding'))]  # hobby is initialized to coding
+    ])
+
+    ## ----
+    with use_scope('hobby', clear=True):
+        put_text('Movie')  # hobby is reset to Movie
+
+    ## ----
+    # append Music, Drama to hobby
+    with use_scope('hobby'):
+        put_text('Music')
+        put_text('Drama')
+
+    ## ----
+    # insert the Coding into the top of the hobby
+    put_markdown('**Coding**', scope='hobby', position=0)
+
+
+.. caution:: It is not allowed to have two scopes with the same name in the application.
+
 **Scope control**
 **Scope control**
 
 
-In addition to `use_scope() <pywebio.output.use_scope>`, PyWebIO also provides the following scope control functions:
+In addition to `use_scope() <pywebio.output.use_scope>` and `put_scope() <pywebio.output.put_scope>`,
+PyWebIO also provides the following scope control functions:
 
 
-* `set_scope(name) <pywebio.output.set_scope>` : Create scope at current location(or specified location)
 * `clear(scope) <pywebio.output.clear>` : Clear the contents of the scope
 * `clear(scope) <pywebio.output.clear>` : Clear the contents of the scope
 * `remove(scope) <pywebio.output.remove>` : Remove scope
 * `remove(scope) <pywebio.output.remove>` : Remove scope
 * `scroll_to(scope) <pywebio.output.scroll_to>` : Scroll the page to the scope
 * `scroll_to(scope) <pywebio.output.scroll_to>` : Scroll the page to the scope

+ 5 - 0
docs/spec.rst

@@ -248,6 +248,11 @@ Unique attributes of different types:
 
 
   * input: input spec, same as the item of ``input_group.inputs``
   * input: input spec, same as the item of ``input_group.inputs``
 
 
+* type: scope
+
+  * dom_id: the DOM id need to be set to this widget
+  * contents list: list of output spec
+
 pin_value
 pin_value
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^
 
 

+ 45 - 21
pywebio/output.py

@@ -17,17 +17,17 @@ Functions list
 +--------------------+---------------------------+------------------------------------------------------------+
 +--------------------+---------------------------+------------------------------------------------------------+
 |                    | **Name**                  | **Description**                                            |
 |                    | **Name**                  | **Description**                                            |
 +--------------------+---------------------------+------------------------------------------------------------+
 +--------------------+---------------------------+------------------------------------------------------------+
-| Output Scope       | `set_scope`               | Create a new scope                                         |
+| Output Scope       | `put_scope`               | Create a new scope                                         |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
-|                    | `get_scope`               | Get the scope name in the runtime scope stack              |
+|                    | `use_scope`:sup:`†`       | Enter a scope                                              |
+|                    +---------------------------+------------------------------------------------------------+
+|                    | `get_scope`               | Get the current scope name in the runtime scope stack      |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `clear`                   | Clear the content of scope                                 |
 |                    | `clear`                   | Clear the content of scope                                 |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `remove`                  | Remove the scope                                           |
 |                    | `remove`                  | Remove the scope                                           |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `scroll_to`               | Scroll the page to the scope                               |
 |                    | `scroll_to`               | Scroll the page to the scope                               |
-|                    +---------------------------+------------------------------------------------------------+
-|                    | `use_scope`:sup:`†`       | Open or enter a scope                                      |
 +--------------------+---------------------------+------------------------------------------------------------+
 +--------------------+---------------------------+------------------------------------------------------------+
 | Content Outputting | `put_text`                | Output plain text                                          |
 | Content Outputting | `put_text`                | Output plain text                                          |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
@@ -85,8 +85,6 @@ Functions list
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `style`:sup:`*`           | Customize the css style of output content                  |
 |                    | `style`:sup:`*`           | Customize the css style of output content                  |
 +--------------------+---------------------------+------------------------------------------------------------+
 +--------------------+---------------------------+------------------------------------------------------------+
-| Placeholder        | `output`:sup:`*`          | Placeholder of output                                      |
-+--------------------+---------------------------+------------------------------------------------------------+
 
 
 Output Scope
 Output Scope
 --------------
 --------------
@@ -95,12 +93,12 @@ Output Scope
 
 
    * :ref:`Use Guide: Output Scope <output_scope>`
    * :ref:`Use Guide: Output Scope <output_scope>`
 
 
-.. autofunction:: set_scope
+.. autofunction:: put_scope
+.. autofunction:: use_scope
 .. autofunction:: get_scope
 .. autofunction:: get_scope
 .. autofunction:: clear
 .. autofunction:: clear
 .. autofunction:: remove
 .. autofunction:: remove
 .. autofunction:: scroll_to
 .. autofunction:: scroll_to
-.. autofunction:: use_scope
 
 
 Content Outputting
 Content Outputting
 -----------------------
 -----------------------
@@ -207,9 +205,6 @@ Layout and Style
 .. autofunction:: put_grid
 .. autofunction:: put_grid
 .. autofunction:: style
 .. autofunction:: style
 
 
-Placeholder
---------------
-.. autofunction:: output
 
 
 """
 """
 import html
 import html
@@ -232,7 +227,7 @@ except ImportError:
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs',
+__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs', 'put_scope',
            'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
            'put_text', 'put_html', 'put_code', 'put_markdown', 'use_scope', 'set_scope', 'clear', 'remove',
            'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
            'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
            'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
            'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
@@ -1390,10 +1385,31 @@ def put_grid(content, cell_width='auto', cell_height='auto', cell_widths=None, c
     return put_widget(template=tpl, data=dict(contents=content), scope=scope, position=position)
     return put_widget(template=tpl, data=dict(contents=content), scope=scope, position=position)
 
 
 
 
+@safely_destruct_output_when_exp('content')
+def put_scope(name, content=[], scope=None, position=OutputPosition.BOTTOM) -> Output:
+    """Output a scope
+
+    :param str name:
+    :param list/put_xxx() content: The initial content of the scope, can be ``put_xxx()`` or a list of it.
+    :param int scope, position: Those arguments have the same meaning as for `put_text()`
+    """
+    if not isinstance(content, list):
+        content = [content]
+
+    assert is_html_safe_value(name), "Scope name only allow letter/digit/'_'/'-' char."
+    dom_id = scope2dom(name, no_css_selector=True)
+
+    spec = _get_output_spec('scope', dom_id=dom_id, contents=content, scope=scope, position=position)
+    return Output(spec)
+
+
 @safely_destruct_output_when_exp('contents')
 @safely_destruct_output_when_exp('contents')
 def output(*contents):
 def output(*contents):
     """Placeholder of output
     """Placeholder of output
 
 
+    .. deprecated:: 1.5
+        See :ref:`User Guide <put_scope>` for new way to set css style for output.
+
      ``output()`` can be passed in anywhere that ``put_xxx()`` can passed in. A handler it returned by ``output()``,
      ``output()`` can be passed in anywhere that ``put_xxx()`` can passed in. A handler it returned by ``output()``,
      and after being output, the content can also be modified by the handler (See code example below).
      and after being output, the content can also be modified by the handler (See code example below).
 
 
@@ -1431,6 +1447,10 @@ def output(*contents):
 
 
     """
     """
 
 
+    import warnings
+    warnings.warn("`pywebio.output.output()` is deprecated since v1.5 and will remove in the future version, "
+                  "use `pywebio.output.put_scope()` instead", DeprecationWarning, stacklevel=2)
+
     class OutputHandler(Output):
     class OutputHandler(Output):
         """
         """
         与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
         与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
@@ -1687,17 +1707,16 @@ def toast(content, duration=2, position='center', color='info', onclick=None):
 clear_scope = clear
 clear_scope = clear
 
 
 
 
-def use_scope(name=None, clear=False, create_scope=True, **scope_params):
-    """Open or enter a scope. Can be used as context manager and decorator.
+def use_scope(name=None, clear=False, **kwargs):
+    """use_scope(name=None, clear=False)
+
+    Open or enter a scope. Can be used as context manager and decorator.
 
 
     See :ref:`User manual - use_scope() <use_scope>`
     See :ref:`User manual - use_scope() <use_scope>`
 
 
     :param str name: Scope name. If it is None, a globally unique scope name is generated.
     :param str name: Scope name. If it is None, a globally unique scope name is generated.
         (When used as context manager, the context manager will return the scope name)
         (When used as context manager, the context manager will return the scope name)
     :param bool clear: Whether to clear the contents of the scope before entering the scope.
     :param bool clear: Whether to clear the contents of the scope before entering the scope.
-    :param bool create_scope: Whether to create scope when scope does not exist.
-    :param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
-        Only available when ``create_scope=True``.
 
 
     :Usage:
     :Usage:
 
 
@@ -1711,6 +1730,13 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
             put_xxx()
             put_xxx()
 
 
     """
     """
+    # For backward compatible
+    #     :param bool create_scope: Whether to create scope when scope does not exist.
+    #     :param scope_params: Extra parameters passed to `set_scope()` when need to create scope.
+    #         Only available when ``create_scope=True``.
+    create_scope = kwargs.pop('create_scope', True)
+    scope_params = kwargs
+
     if name is None:
     if name is None:
         name = random_str(10)
         name = random_str(10)
     else:
     else:
@@ -1718,10 +1744,8 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
 
 
     def before_enter():
     def before_enter():
         if create_scope:
         if create_scope:
-            set_scope(name, **scope_params)
-
-        if clear:
-            clear_scope(name)
+            if_exist = 'clear' if clear else None
+            set_scope(name, if_exist=if_exist, **scope_params)
 
 
     return use_scope_(name=name, before_enter=before_enter)
     return use_scope_(name=name, before_enter=before_enter)
 
 

+ 13 - 0
test/template.py

@@ -297,6 +297,19 @@ def basic_output():
     hobby.append(put_text('Music'), put_text('Drama'))
     hobby.append(put_text('Music'), put_text('Drama'))
     hobby.insert(0, put_markdown('**Coding**'))
     hobby.insert(0, put_markdown('**Coding**'))
 
 
+    put_table([
+        ['Name', 'Hobbies'],
+        ['Tom', put_scope('hobby', content=put_text('Coding'))]
+    ])
+
+    with use_scope('hobby', clear=True):
+        put_text('Movie')  # hobby is reset to Movie
+
+    with use_scope('hobby'):
+        put_text('Music')
+        put_text('Drama')
+
+    put_markdown('**Coding**', scope='hobby', position=0)
 
 
 
 
 def background_output():
 def background_output():

+ 19 - 2
webiojs/src/handlers/output.ts

@@ -4,10 +4,27 @@ import {body_scroll_to} from "../utils";
 
 
 import {getWidgetElement} from "../models/output"
 import {getWidgetElement} from "../models/output"
 import {CommandHandler} from "./base";
 import {CommandHandler} from "./base";
-import {AfterPinShow} from "../models/pin";
 
 
 const DISPLAY_NONE_TAGS = ['script', 'style'];
 const DISPLAY_NONE_TAGS = ['script', 'style'];
 
 
+let after_show_callbacks: (() => void) [] = [];
+
+// register a callback to execute after the current output widget showing
+export function AfterCurrentOutputWidgetShow(callback: () => void){
+    after_show_callbacks.push(callback);
+}
+
+export function trigger_output_widget_show_event() {
+    for (let cb of after_show_callbacks) {
+        try {
+            cb.call(this);
+        } catch (e) {
+            console.error('Error in callback of pin widget show event.');
+        }
+    }
+    after_show_callbacks = [];
+}
+
 export class OutputHandler implements CommandHandler {
 export class OutputHandler implements CommandHandler {
     session: Session;
     session: Session;
 
 
@@ -79,7 +96,7 @@ export class OutputHandler implements CommandHandler {
                 else if (state.AutoScrollBottom && output_to_root)
                 else if (state.AutoScrollBottom && output_to_root)
                     this.scroll_bottom();
                     this.scroll_bottom();
             }
             }
-            AfterPinShow();
+            trigger_output_widget_show_event();
         } else if (msg.command === 'output_ctl') {
         } else if (msg.command === 'output_ctl') {
             this.handle_output_ctl(msg);
             this.handle_output_ctl(msg);
         }
         }

+ 2 - 2
webiojs/src/handlers/popup.ts

@@ -2,7 +2,7 @@ import {Command, Session} from "../session";
 
 
 import {render_tpl} from "../models/output"
 import {render_tpl} from "../models/output"
 import {CommandHandler} from "./base";
 import {CommandHandler} from "./base";
-import {AfterPinShow} from "../models/pin";
+import {trigger_output_widget_show_event} from "./output";
 
 
 
 
 export class PopupHandler implements CommandHandler {
 export class PopupHandler implements CommandHandler {
@@ -32,7 +32,7 @@ export class PopupHandler implements CommandHandler {
 
 
             let elem = PopupHandler.get_element(msg.spec);
             let elem = PopupHandler.get_element(msg.spec);
             this.body.append(elem);
             this.body.append(elem);
-            AfterPinShow();
+            trigger_output_widget_show_event();
 
 
             // 弹窗关闭后就立即销毁
             // 弹窗关闭后就立即销毁
             elem.on('hidden.bs.modal', function (e) {
             elem.on('hidden.bs.modal', function (e) {

+ 3 - 1
webiojs/src/i18n.ts

@@ -14,8 +14,9 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
         "submit": "Submit",
         "submit": "Submit",
         "reset": "Reset",
         "reset": "Reset",
         "cancel": "Cancel",
         "cancel": "Cancel",
-        "duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name ).",
+        "duplicated_pin_name": "This pin widget has expired (due to the output of a new pin widget with the same name).",
         "browse_file": "Browse",
         "browse_file": "Browse",
+        "duplicated_scope_name": "Error: The name of this scope is duplicated with the previous one!",
     },
     },
     "zh": {
     "zh": {
         "disconnected_with_server": "与服务器连接已断开,请刷新页面重新操作",
         "disconnected_with_server": "与服务器连接已断开,请刷新页面重新操作",
@@ -28,6 +29,7 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
         "cancel": "取消",
         "cancel": "取消",
         "duplicated_pin_name": "该 Pin widget 已失效(由于输出了新的同名 pin widget)",
         "duplicated_pin_name": "该 Pin widget 已失效(由于输出了新的同名 pin widget)",
         "browse_file": "浏览文件",
         "browse_file": "浏览文件",
+        "duplicated_scope_name": "错误: 此scope与已有scope重复!",
     },
     },
     "ru": {
     "ru": {
         "disconnected_with_server": "Соединение с сервером потеряно, пожалуйста перезагрузите страницу",
         "disconnected_with_server": "Соединение с сервером потеряно, пожалуйста перезагрузите страницу",

+ 29 - 2
webiojs/src/models/output.ts

@@ -2,6 +2,8 @@ import {b64toBlob, randomid} from "../utils";
 import * as marked from 'marked';
 import * as marked from 'marked';
 import {pushData} from "../session";
 import {pushData} from "../session";
 import {PinWidget} from "./pin";
 import {PinWidget} from "./pin";
+import {t} from "../i18n";
+import {AfterCurrentOutputWidgetShow} from "../handlers/output";
 
 
 export interface Widget {
 export interface Widget {
     handle_type: string;
     handle_type: string;
@@ -199,6 +201,31 @@ let TabsWidget = {
     }
     }
 };
 };
 
 
+
+const SCOPE_TPL = `<div>
+    {{#contents}}
+        {{& pywebio_output_parse}}
+    {{/contents}}
+</div>`;
+let ScopeWidget = {
+    handle_type: 'scope',
+    get_element: function (spec: {dom_id:string, contents: any[]}) {
+        let elem = render_tpl(SCOPE_TPL, spec);
+        // need to check the duplicate id after current output widget shown.
+        // because the current widget may have multiple sub-widget which have same dom id.
+        AfterCurrentOutputWidgetShow(()=>{
+            if($(`#${spec.dom_id}`).length !== 0){
+                let tip = `<p style="color: grey; border:1px solid #ced4da; padding: .375rem .75rem;">${t("duplicated_scope_name")}</p>`;
+                elem.empty().html(tip);
+            }else{
+                elem.attr('id', spec.dom_id);
+            }
+        })
+        return elem;
+    }
+};
+
+
 let CustomWidget = {
 let CustomWidget = {
     handle_type: 'custom_widget',
     handle_type: 'custom_widget',
     get_element: function (spec: { template: string, data: { [i: string]: any } }) {
     get_element: function (spec: { template: string, data: { [i: string]: any } }) {
@@ -206,7 +233,7 @@ let CustomWidget = {
     }
     }
 };
 };
 
 
-let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget];
+let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget, PinWidget, ScopeWidget];
 
 
 
 
 let type2widget: { [i: string]: Widget } = {};
 let type2widget: { [i: string]: Widget } = {};
@@ -283,7 +310,7 @@ export function render_tpl(tpl: string, data: { [i: string]: any }) {
             let sub_elem = getWidgetElement(spec);
             let sub_elem = getWidgetElement(spec);
             elem.find(`#${dom_id}`).replaceWith(sub_elem);
             elem.find(`#${dom_id}`).replaceWith(sub_elem);
         } catch (e) {
         } catch (e) {
-            console.error('Error when render widget: \n%s', JSON.stringify(spec));
+            console.error('Error when render widget: \n%s\nSPEC:%s', e, JSON.stringify(spec));
         }
         }
     }
     }
     return elem;
     return elem;

+ 2 - 13
webiojs/src/models/pin.ts

@@ -1,19 +1,8 @@
 import {get_input_item_from_type} from "./input/index"
 import {get_input_item_from_type} from "./input/index"
 import {InputItem} from "./input/base";
 import {InputItem} from "./input/base";
 import {t} from "../i18n";
 import {t} from "../i18n";
+import {AfterCurrentOutputWidgetShow} from "../handlers/output";
 
 
-let after_show_callbacks: (() => void) [] = [];
-
-export function AfterPinShow() {
-    for (let cb of after_show_callbacks) {
-        try {
-            cb.call(this);
-        } catch (e) {
-            console.error('Error in callback of pin widget show event.');
-        }
-    }
-    after_show_callbacks = [];
-}
 
 
 let name2input: { [k: string]: InputItem } = {};
 let name2input: { [k: string]: InputItem } = {};
 
 
@@ -74,7 +63,7 @@ export let PinWidget = {
 
 
         name2input[input_spec.name] = input_item;
         name2input[input_spec.name] = input_item;
 
 
-        after_show_callbacks.push(() => {
+        AfterCurrentOutputWidgetShow(() => {
             input_item.after_add_to_dom();
             input_item.after_add_to_dom();
             input_item.after_show(true);
             input_item.after_show(true);
         });
         });