瀏覽代碼

feat: add `pywebio.output.output()`

wangweimin 4 年之前
父節點
當前提交
2c45d432c9
共有 5 個文件被更改,包括 143 次插入15 次删除
  1. 16 0
      docs/guide.rst
  2. 8 1
      docs/spec.rst
  3. 83 1
      pywebio/output.py
  4. 14 2
      test/template.py
  5. 22 11
      webiojs/src/handlers/output.ts

+ 16 - 0
docs/guide.rst

@@ -133,6 +133,8 @@ PyWebIO提供了一些便捷函数来输出表格、链接等格式::
 
 
 PyWebIO提供的全部输出函数见 :doc:`pywebio.output </output>` 模块
 PyWebIO提供的全部输出函数见 :doc:`pywebio.output </output>` 模块
 
 
+.. _combine_output:
+
 组合输出
 组合输出
 ^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^
 函数名以 ``put_`` 开始的输出函数,可以与一些输出函数组合使用,作为最终输出的一部分:
 函数名以 ``put_`` 开始的输出函数,可以与一些输出函数组合使用,作为最终输出的一部分:
@@ -165,6 +167,20 @@ PyWebIO提供的全部输出函数见 :doc:`pywebio.output </output>` 模块
 其他接受 ``put_xxx()`` 调用作为参数的输出函数还有 `put_collapse() <pywebio.output.put_collapse>` 、 `put_scrollable() <pywebio.output.put_scrollable>` 、`put_widget() <pywebio.output.put_widget>` ,
 其他接受 ``put_xxx()`` 调用作为参数的输出函数还有 `put_collapse() <pywebio.output.put_collapse>` 、 `put_scrollable() <pywebio.output.put_scrollable>` 、`put_widget() <pywebio.output.put_widget>` ,
 此外,还可以通过 `put_widget() <pywebio.output.put_widget>` 自定义可接收 ``put_xxx()`` 调用的输出组件,具体用法请参考函数文档。
 此外,还可以通过 `put_widget() <pywebio.output.put_widget>` 自定义可接收 ``put_xxx()`` 调用的输出组件,具体用法请参考函数文档。
 
 
+使用组合输出时,如果想在内容输出后,对其中的 ``put_xxx()`` 子项进行动态修改,可以使用 `output() <pywebio.output.output>` 函数,
+`output() <pywebio.output.output>` 返回一个handler,handler本身可以像 ``put_xxx()`` 一样传入 `put_table` 、 `popup` 、 `put_widget` 等函数中组成组合输入,
+并且,在输出后,还可以通过handler对子项内容进行修改(比如重置或增加内容)::
+
+   hobby = output(put_text('Coding'))
+   put_table([
+      ['Name', 'Hobbies'],
+      ['Wang', hobby]      # hobby 初始为 Coding
+   ])
+
+   hobby.reset(put_text('Movie'))  # hobby 被重置为 Movie
+   hobby.append(put_text('Music'), put_text('Drama'))   # 向 hobby 追加 Music, Drama
+   hobby.insert(0, put_markdown('**Coding**'))  # 将 Coding 插入 hobby 顶端
+
 
 
 事件回调
 事件回调
 ^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^

+ 8 - 1
docs/spec.rst

@@ -179,6 +179,7 @@ output
 * type: 内容类型
 * type: 内容类型
 * style: str 自定义样式
 * style: str 自定义样式
 * scope: str 内容输出的域的名称
 * scope: str 内容输出的域的名称
+* use_custom_selector: bool, 可选,表示是否将内容输出到自定义的CSS选择器指定的容器中. 默认为False, 若为真,则scope参数为自定义的CSS选择器,若CSS选择器匹配到页面上的多个容器,则内容会输出到每个匹配到的容器
 * position: int 在输出域中输出的位置, 见 :ref:`输出函数的scope相关参数 <scope_param>`
 * position: int 在输出域中输出的位置, 见 :ref:`输出函数的scope相关参数 <scope_param>`
 * 不同type时的特有字段
 * 不同type时的特有字段
 
 
@@ -245,11 +246,15 @@ output_ctl
     * position: 在父scope中创建此scope的位置. int, position>=0表示在父scope的第position个(从0计数)子元素的前面创建;position<0表示在父scope的倒数第position个(从-1计数)元素之后创建新scope
     * position: 在父scope中创建此scope的位置. int, position>=0表示在父scope的第position个(从0计数)子元素的前面创建;position<0表示在父scope的倒数第position个(从-1计数)元素之后创建新scope
     * if_exist: scope已经存在时如何操作:
     * if_exist: scope已经存在时如何操作:
 
 
-        - `'none'` 表示不进行任何操作
+        - `'none'` 表示立即返回不进行任何操作
         - `'remove'` 表示先移除旧scope再创建新scope
         - `'remove'` 表示先移除旧scope再创建新scope
         - `'clear'` 表示将旧scope的内容清除,不创建新scope
         - `'clear'` 表示将旧scope的内容清除,不创建新scope
 
 
 * clear: 清空scope的内容
 * clear: 清空scope的内容
+
+    * use_custom_selector: bool, 可选,指示clear的值是否为自定义的CSS选择器
+      默认为False, 为真时,若CSS选择器匹配到页面上的多个容器,则每个匹配到的容器都会被清空
+
 * clear_before
 * clear_before
 * clear_after
 * clear_after
 * clear_range:[,]
 * clear_range:[,]
@@ -295,6 +300,8 @@ input_event
 
 
 注意: checkbox_radio 不产生blur事件
 注意: checkbox_radio 不产生blur事件
 
 
+.. _callback_event:
+
 callback
 callback
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^
 用户点击显示区的按钮时触发
 用户点击显示区的按钮时触发

+ 83 - 1
pywebio/output.py

@@ -46,6 +46,10 @@ r"""输出内容到用户浏览器
 .. autofunction:: put_grid
 .. autofunction:: put_grid
 .. autofunction:: style
 .. autofunction:: style
 
 
+其他
+--------------
+.. autofunction::  output
+
 """
 """
 import io
 import io
 import logging
 import logging
@@ -69,7 +73,8 @@ __all__ = ['Position', 'set_title', 'set_output_fixed_height', 'set_auto_scroll_
            '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', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
            'put_table', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
            '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',
-           'put_row', 'put_grid', 'column', 'row', 'grid', 'span', 'put_processbar', 'set_processbar', 'put_loading']
+           'put_row', 'put_grid', 'column', 'row', 'grid', 'span', 'put_processbar', 'set_processbar', 'put_loading',
+           'output']
 
 
 
 
 # popup尺寸
 # popup尺寸
@@ -913,6 +918,83 @@ row = put_row
 grid = put_grid
 grid = put_grid
 
 
 
 
+@safely_destruct_output_when_exp('contents')
+def output(*contents):
+    """返回一个handler,相当于 ``put_xxx()`` 的占位符,可以传入任何接收 ``put_xxx()`` 调用的地方,通过handler可对自身内容进行修改
+
+    output用于对 :ref:`组合输出 <combine_output>` 中的 ``put_xxx()`` 子项进行动态修改(见下方代码示例)
+
+    :param contents: 要输出的初始内容. 元素为 ``put_xxx()`` 形式的调用或字符串,字符串会被看成HTML.
+    :return: OutputHandler 实例, 实例支持的方法如下:
+
+    * ``reset(*contents)`` : 重置内容为 ``contents``
+    * ``append(*contents)`` : 在末尾追加内容
+    * ``insert(idx, *contents)`` : 插入内容. ``idx`` 表示内容插入位置:
+
+       | idx>=0 时表示输出内容到原内容的idx索引的元素的前面;
+       | idx<0 时表示输出内容到到原内容的idx索引元素之后.
+
+    :Example:
+
+    ::
+
+        hobby = output(put_text('Coding'))
+        put_table([
+            ['Name', 'Hobbies'],
+            ['Wang', hobby]
+        ])
+
+        hobby.reset(put_text('Movie'))
+        hobby.append(put_text('Music'), put_text('Drama'))
+        hobby.insert(0, put_markdown('**Coding**'))
+
+    """
+
+    class OutputHandler(Output):
+        """与 Output 的不同在于, 不会在销毁时(__del__)自动输出"""
+
+        def __del__(self):
+            pass
+
+        def __init__(self, spec, container_selector):
+            super().__init__(spec)
+            self.container_selector = container_selector
+
+        @safely_destruct_output_when_exp('outputs')
+        def reset(self, *outputs):
+            send_msg('output_ctl', dict(clear=self.container_selector, use_custom_selector=True))
+            self.append(*outputs)
+
+        @safely_destruct_output_when_exp('outputs')
+        def append(self, *outputs):
+            for o in outputs:
+                o.spec['scope'] = self.container_selector
+                o.spec['use_custom_selector'] = True
+                o.spec['position'] = OutputPosition.BOTTOM
+                o.send()
+
+        @safely_destruct_output_when_exp('outputs')
+        def insert(self, idx, *outputs):
+            """idx可为负,"""
+            direction = 1 if idx >= 0 else -1
+            for acc, o in enumerate(outputs):
+                o.spec['scope'] = self.container_selector
+                o.spec['use_custom_selector'] = True
+                o.spec['position'] = idx + direction * acc
+                o.send()
+
+    dom_id = _parse_scope(random_str(10))
+    tpl = """<div class="{{dom_class_name}}">
+            {{#contents}}
+                {{#.}}
+                    {{& pywebio_output_parse}}
+                {{/.}}
+            {{/contents}}
+        </div>"""
+    out_spec = put_widget(template=tpl, data=dict(contents=contents, dom_class_name=dom_id))
+    return OutputHandler(Output.dump_dict(out_spec), '.' + dom_id)
+
+
 @safely_destruct_output_when_exp('outputs')
 @safely_destruct_output_when_exp('outputs')
 def style(outputs, css_style) -> Union[Output, OutputList]:
 def style(outputs, css_style) -> Union[Output, OutputList]:
     """自定义输出内容的css样式
     """自定义输出内容的css样式

+ 14 - 2
test/template.py

@@ -261,7 +261,7 @@ def basic_output():
         for x in range(5)
         for x in range(5)
     ], direction='column')
     ], direction='column')
 
 
-    put_row([style(put_code(i), 'margin-right:10px;') for i in range(6)], 'repeat(auto-fill, 25%)')
+    put_row([style(put_code(i), 'margin-right:10px;') for i in range(4)], 'repeat(auto-fill, 25%)')
 
 
     put_markdown('### Span')
     put_markdown('### Span')
     cell = lambda text: style(put_code(text), 'margin-right:10px;')
     cell = lambda text: style(put_code(text), 'margin-right:10px;')
@@ -282,6 +282,17 @@ def basic_output():
 
 
     put_loading()
     put_loading()
 
 
+    # output
+    hobby = output(put_text('Coding'))
+    put_table([
+        ['Name', 'Hobbies'],
+        ['Wang', hobby]
+    ])
+
+    hobby.reset(put_text('Movie'))
+    hobby.append(put_text('Music'), put_text('Drama'))
+    hobby.insert(0, put_markdown('**Coding**'))
+
 
 
 def background_output():
 def background_output():
     put_text("Background output")
     put_text("Background output")
@@ -730,7 +741,8 @@ def save_output(browser: Chrome, filename=None, process_func=None):
     :return: 处理前后的html文本
     :return: 处理前后的html文本
     """
     """
     raw_html = browser.find_element_by_id('markdown-body').get_attribute('innerHTML')
     raw_html = browser.find_element_by_id('markdown-body').get_attribute('innerHTML')
-    html = re.sub(r"WebIO.DisplayAreaButtonOnClick\(.*?\)", '', raw_html)
+    html = re.sub(r'"pywebio-scope-.*?"', '', raw_html)
+    html = re.sub(r"WebIO.DisplayAreaButtonOnClick\(.*?\)", '', html)
     html = re.sub(r"</(.*?)>", r'</\g<1>>\n', html)  # 进行断行方便后续的diff判断
     html = re.sub(r"</(.*?)>", r'</\g<1>>\n', html)  # 进行断行方便后续的diff判断
     if process_func:
     if process_func:
         html = process_func(html)
         html = process_func(html)

+ 22 - 11
webiojs/src/handlers/output.ts

@@ -38,9 +38,14 @@ export class OutputHandler implements CommandHandler {
                 return console.error(`Handle command error, command: ${msg}, error:${e}`);
                 return console.error(`Handle command error, command: ${msg}, error:${e}`);
             }
             }
 
 
-            if (config.outputAnimation && elem[0].tagName.toLowerCase() != 'script') elem.hide();
+            let container_elem;
+            if (!msg.spec.use_custom_selector)
+                container_elem = this.container_elem.find(`#${msg.spec.scope || 'pywebio-scope-ROOT'}`);
+            else
+                container_elem = $(msg.spec.scope);
+
+            if (config.outputAnimation && elem[0].tagName.toLowerCase() != 'script' && container_elem.length == 1) elem.hide();
 
 
-            let container_elem = this.container_elem.find(`#${msg.spec.scope || 'pywebio-scope-ROOT'}`);
             if (container_elem.length === 0)
             if (container_elem.length === 0)
                 return console.error(`Scope '${msg.spec.scope}' not found`);
                 return console.error(`Scope '${msg.spec.scope}' not found`);
 
 
@@ -51,14 +56,16 @@ export class OutputHandler implements CommandHandler {
             else if (msg.spec.position === -1)
             else if (msg.spec.position === -1)
                 container_elem.append(elem);
                 container_elem.append(elem);
             else {
             else {
-                let pos = $(container_elem[0].children).eq(msg.spec.position);
-                if (msg.spec.position >= 0)
-                    elem.insertBefore(pos);
-                else
-                    elem.insertAfter(pos);
+                for (let con of container_elem) {
+                    let pos = $(con.children).eq(msg.spec.position);
+                    if (msg.spec.position >= 0)
+                        elem.insertBefore(pos);
+                    else
+                        elem.insertAfter(pos);
+                }
             }
             }
 
 
-            if (config.outputAnimation && elem[0].tagName.toLowerCase() != 'script') elem.fadeIn();
+            if (config.outputAnimation && elem[0].tagName.toLowerCase() != 'script' && container_elem.length == 1) elem.fadeIn();
         } else if (msg.command === 'output_ctl') {
         } else if (msg.command === 'output_ctl') {
             this.handle_output_ctl(msg);
             this.handle_output_ctl(msg);
         }
         }
@@ -86,7 +93,7 @@ export class OutputHandler implements CommandHandler {
                 set_scope: string, // scope名
                 set_scope: string, // scope名
                 container: string, // 此scope的父scope
                 container: string, // 此scope的父scope
                 position: number, // 在父scope中创建此scope的位置 0 -> 在父scope的顶部创建, -1 -> 在父scope的尾部创建
                 position: number, // 在父scope中创建此scope的位置 0 -> 在父scope的顶部创建, -1 -> 在父scope的尾部创建
-                if_exist: string // 已经存在 ``name`` scope 时如何操作:  `'remove'` 表示先移除旧scope再创建新scope, `'none'` 表示不进行任何操作, `'clear'` 表示将旧scope的内容清除,不创建新scope
+                if_exist: string // 已经存在 ``name`` scope 时如何操作:  `'remove'` 表示先移除旧scope再创建新scope, `'none'` 表示立即返回不进行任何操作, `'clear'` 表示将旧scope的内容清除,不创建新scope
             };
             };
 
 
             let container_elem = $(`#${spec.container}`);
             let container_elem = $(`#${spec.container}`);
@@ -117,8 +124,12 @@ export class OutputHandler implements CommandHandler {
                     $(`#${spec.container}>*`).eq(spec.position).insertAfter(html);
                     $(`#${spec.container}>*`).eq(spec.position).insertAfter(html);
             }
             }
         }
         }
-        if (msg.spec.clear !== undefined)
-            this.container_elem.find(`#${msg.spec.clear}`).empty();
+        if (msg.spec.clear !== undefined) {
+            if (!msg.spec.use_custom_selector)
+                this.container_elem.find(`#${msg.spec.clear}`).empty();
+            else
+                $(msg.spec.clear).empty();
+        }
         if (msg.spec.clear_before !== undefined)
         if (msg.spec.clear_before !== undefined)
             this.container_elem.find(`#${msg.spec.clear_before}`).prevAll().remove();
             this.container_elem.find(`#${msg.spec.clear_before}`).prevAll().remove();
         if (msg.spec.clear_after !== undefined)
         if (msg.spec.clear_after !== undefined)