From b1629a018f1eacb16d417aa7f77d0574593dc07f Mon Sep 17 00:00:00 2001 From: wangweimin Date: Fri, 29 Oct 2021 21:19:49 +0800 Subject: [PATCH] add `put_scope()` to replace `output()` --- docs/guide.rst | 62 ++++++++++++++++++++-------------- docs/spec.rst | 5 +++ pywebio/output.py | 60 +++++++++++++++++++++++--------- test/template.py | 13 +++++++ webiojs/src/handlers/output.ts | 21 ++++++++++-- webiojs/src/handlers/popup.ts | 4 +-- webiojs/src/i18n.ts | 4 ++- webiojs/src/models/output.ts | 31 +++++++++++++++-- webiojs/src/models/pin.ts | 15 ++------ 9 files changed, 154 insertions(+), 61 deletions(-) diff --git a/docs/guide.rst b/docs/guide.rst index 7fd2332e..ef815e6f 100644 --- a/docs/guide.rst +++ b/docs/guide.rst @@ -297,29 +297,6 @@ In addition, you can use `put_widget() ` to make your For a full list of functions that accept ``put_xxx()`` calls as content, see :ref:`Output functions 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() ` function. `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() ` 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 also provides the following scope control functions: +In addition to `use_scope() ` and `put_scope() `, +PyWebIO also provides the following scope control functions: -* `set_scope(name) ` : Create scope at current location(or specified location) * `clear(scope) ` : Clear the contents of the scope * `remove(scope) ` : Remove scope * `scroll_to(scope) ` : Scroll the page to the scope diff --git a/docs/spec.rst b/docs/spec.rst index b6945cea..d00ced64 100644 --- a/docs/spec.rst +++ b/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 ^^^^^^^^^^^^^^^ diff --git a/pywebio/output.py b/pywebio/output.py index fa13e2a5..5c8e12e6 100644 --- a/pywebio/output.py +++ b/pywebio/output.py @@ -17,17 +17,17 @@ +--------------------+---------------------------+------------------------------------------------------------+ | | **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 | | +---------------------------+------------------------------------------------------------+ @@ -95,12 +95,12 @@ * :ref:`Use Guide: 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 ----------------------- @@ -232,7 +232,7 @@ 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 +1390,30 @@ 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] + + 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 ` 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 +1451,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 +1711,16 @@ def show_msg(): 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() ` :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 +1734,13 @@ def app(): 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 +1748,8 @@ def app(): 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) diff --git a/test/template.py b/test/template.py index f2f9ffd3..40b43ef1 100644 --- a/test/template.py +++ b/test/template.py @@ -297,6 +297,19 @@ def edit_row(choice, row): 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(): diff --git a/webiojs/src/handlers/output.ts b/webiojs/src/handlers/output.ts index 2edc849b..c1e926f6 100644 --- a/webiojs/src/handlers/output.ts +++ b/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); } diff --git a/webiojs/src/handlers/popup.ts b/webiojs/src/handlers/popup.ts index 08cd6a0b..5165a8bb 100644 --- a/webiojs/src/handlers/popup.ts +++ b/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) { diff --git a/webiojs/src/i18n.ts b/webiojs/src/i18n.ts index 255ccaac..6899c04c 100644 --- a/webiojs/src/i18n.ts +++ b/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": "Соединение с сервером потеряно, пожалуйста перезагрузите страницу", diff --git a/webiojs/src/models/output.ts b/webiojs/src/models/output.ts index 87d6d439..5a76623a 100644 --- a/webiojs/src/models/output.ts +++ b/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 = `
+ {{#contents}} + {{& pywebio_output_parse}} + {{/contents}} +
`; +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 = `

${t("duplicated_scope_name")}

`; + 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; diff --git a/webiojs/src/models/pin.ts b/webiojs/src/models/pin.ts index bb1ae3e8..fe5acfe0 100644 --- a/webiojs/src/models/pin.ts +++ b/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); });