Explorar o código

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

wangweimin %!s(int64=3) %!d(string=hai) anos
pai
achega
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>`
 
-**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**
 
 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**
 
-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
 * `remove(scope) <pywebio.output.remove>` : Remove 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``
 
+* type: scope
+
+  * dom_id: the DOM id need to be set to this widget
+  * contents list: list of output spec
+
 pin_value
 ^^^^^^^^^^^^^^^
 

+ 45 - 21
pywebio/output.py

@@ -17,17 +17,17 @@ Functions list
 +--------------------+---------------------------+------------------------------------------------------------+
 |                    | **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                                 |
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `remove`                  | Remove 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                                          |
 |                    +---------------------------+------------------------------------------------------------+
@@ -85,8 +85,6 @@ Functions list
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `style`:sup:`*`           | Customize the css style of output content                  |
 +--------------------+---------------------------+------------------------------------------------------------+
-| Placeholder        | `output`:sup:`*`          | Placeholder of output                                      |
-+--------------------+---------------------------+------------------------------------------------------------+
 
 Output Scope
 --------------
@@ -95,12 +93,12 @@ Output Scope
 
    * :ref:`Use Guide: Output Scope <output_scope>`
 
-.. autofunction:: set_scope
+.. autofunction:: put_scope
+.. autofunction:: use_scope
 .. autofunction:: get_scope
 .. autofunction:: clear
 .. autofunction:: remove
 .. autofunction:: scroll_to
-.. autofunction:: use_scope
 
 Content Outputting
 -----------------------
@@ -207,9 +205,6 @@ Layout and Style
 .. autofunction:: put_grid
 .. autofunction:: style
 
-Placeholder
---------------
-.. autofunction:: output
 
 """
 import html
@@ -232,7 +227,7 @@ except ImportError:
 
 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_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
            '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)
 
 
+@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')
 def output(*contents):
     """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()``,
      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):
         """
         与 `Output` 的不同在于, 不会在销毁时(__del__)自动输出
@@ -1687,17 +1707,16 @@ def toast(content, duration=2, position='center', color='info', onclick=None):
 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>`
 
     :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)
     :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:
 
@@ -1711,6 +1730,13 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
             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:
         name = random_str(10)
     else:
@@ -1718,10 +1744,8 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
 
     def before_enter():
         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)
 

+ 13 - 0
test/template.py

@@ -297,6 +297,19 @@ def basic_output():
     hobby.append(put_text('Music'), put_text('Drama'))
     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():

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

@@ -4,10 +4,27 @@ import {body_scroll_to} from "../utils";
 
 import {getWidgetElement} from "../models/output"
 import {CommandHandler} from "./base";
-import {AfterPinShow} from "../models/pin";
 
 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 {
     session: Session;
 
@@ -79,7 +96,7 @@ export class OutputHandler implements CommandHandler {
                 else if (state.AutoScrollBottom && output_to_root)
                     this.scroll_bottom();
             }
-            AfterPinShow();
+            trigger_output_widget_show_event();
         } else if (msg.command === 'output_ctl') {
             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 {CommandHandler} from "./base";
-import {AfterPinShow} from "../models/pin";
+import {trigger_output_widget_show_event} from "./output";
 
 
 export class PopupHandler implements CommandHandler {
@@ -32,7 +32,7 @@ export class PopupHandler implements CommandHandler {
 
             let elem = PopupHandler.get_element(msg.spec);
             this.body.append(elem);
-            AfterPinShow();
+            trigger_output_widget_show_event();
 
             // 弹窗关闭后就立即销毁
             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",
         "reset": "Reset",
         "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",
+        "duplicated_scope_name": "Error: The name of this scope is duplicated with the previous one!",
     },
     "zh": {
         "disconnected_with_server": "与服务器连接已断开,请刷新页面重新操作",
@@ -28,6 +29,7 @@ const translations: { [lang: string]: { [msgid: string]: string } } = {
         "cancel": "取消",
         "duplicated_pin_name": "该 Pin widget 已失效(由于输出了新的同名 pin widget)",
         "browse_file": "浏览文件",
+        "duplicated_scope_name": "错误: 此scope与已有scope重复!",
     },
     "ru": {
         "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 {pushData} from "../session";
 import {PinWidget} from "./pin";
+import {t} from "../i18n";
+import {AfterCurrentOutputWidgetShow} from "../handlers/output";
 
 export interface Widget {
     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 = {
     handle_type: 'custom_widget',
     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 } = {};
@@ -283,7 +310,7 @@ export function render_tpl(tpl: string, data: { [i: string]: any }) {
             let sub_elem = getWidgetElement(spec);
             elem.find(`#${dom_id}`).replaceWith(sub_elem);
         } 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;

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

@@ -1,19 +1,8 @@
 import {get_input_item_from_type} from "./input/index"
 import {InputItem} from "./input/base";
 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 } = {};
 
@@ -74,7 +63,7 @@ export let PinWidget = {
 
         name2input[input_spec.name] = input_item;
 
-        after_show_callbacks.push(() => {
+        AfterCurrentOutputWidgetShow(() => {
             input_item.after_add_to_dom();
             input_item.after_show(true);
         });