瀏覽代碼

maint: remove `use_custom_selector` in output with scope

wangweimin 4 年之前
父節點
當前提交
aa2037ad86
共有 4 個文件被更改,包括 71 次插入59 次删除
  1. 7 12
      docs/spec.rst
  2. 42 19
      pywebio/output.py
  3. 5 4
      pywebio/utils.py
  4. 17 24
      webiojs/src/handlers/output.ts

+ 7 - 12
docs/spec.rst

@@ -3,7 +3,7 @@
 
 PyWebIO采用服务器-客户端架构,服务端运行任务代码,通过网络与客户端(也就是用户浏览器)交互
 
-服务器与客户端有两种国内通信方式:WebSocket 和 Http 通信。
+服务器与客户端有两种通信方式:WebSocket 和 Http 通信。
 
 使用 Tornado或aiohttp 后端时,服务器与客户端通过 WebSocket 通信,使用 Flask或Django 后端时,服务器与客户端通过 Http 通信。
 
@@ -183,8 +183,7 @@ output
 
 * type: 内容类型
 * style: str 自定义样式
-* scope: str 内容输出的域的名称
-* use_custom_selector: bool, 可选,表示是否将内容输出到自定义的CSS选择器指定的容器中. 默认为False, 若为真,则scope参数为自定义的CSS选择器,若CSS选择器匹配到页面上的多个容器,则内容会输出到每个匹配到的容器
+* scope: str 内容输出的域的css选择器。若CSS选择器匹配到页面上的多个容器,则内容会输出到每个匹配到的容器
 * position: int 在输出域中输出的位置, 见 :ref:`输出函数的scope相关参数 <scope_param>`
 * 不同type时的特有字段
 
@@ -258,26 +257,22 @@ output_ctl
 * title: 设定标题
 * output_fixed_height: 设置是否输出区固定高度
 * auto_scroll_bottom: 设置有新内容时是否自动滚动到底部
-* set_scope: 创建scope
+* set_scope: 创建scope的名字
 
-    * container: 新创建的scope的父scope
+    * container: 新创建的scope的父scope的css选择器
     * position: 在父scope中创建此scope的位置. int, position>=0表示在父scope的第position个(从0计数)子元素的前面创建;position<0表示在父scope的倒数第position个(从-1计数)元素之后创建新scope
     * if_exist: scope已经存在时如何操作:
 
-        - `'none'` 表示立即返回不进行任何操作
+        - null/不指定时表示立即返回不进行任何操作
         - `'remove'` 表示先移除旧scope再创建新scope
         - `'clear'` 表示将旧scope的内容清除,不创建新scope
 
-* clear: 清空scope的内容
-
-    * use_custom_selector: bool, 可选,指示clear的值是否为自定义的CSS选择器
-      默认为False, 为真时,若CSS选择器匹配到页面上的多个容器,则每个匹配到的容器都会被清空
-
+* clear: 需要清空的scope的css选择器
 * clear_before
 * clear_after
 * clear_range:[,]
 * scroll_to:
-* position: top/middle/bottom 与scroll_to一起出现, 表示滚动页面,让锚点位于屏幕可视区域顶部/中部/底部
+* position: top/middle/bottom 与scroll_to一起出现, 表示滚动页面,让scope位于屏幕可视区域顶部/中部/底部
 * remove: 将给定的scope连同scope处的内容移除
 
 run_script

+ 42 - 19
pywebio/output.py

@@ -63,6 +63,7 @@ from base64 import b64encode
 from collections.abc import Mapping, Sequence
 from functools import wraps
 from typing import Union
+import string
 
 from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList
 from .session import get_current_session, download
@@ -123,16 +124,34 @@ def set_auto_scroll_bottom(enabled=True):
     send_msg('output_ctl', dict(auto_scroll_bottom=enabled))
 
 
-def _parse_scope(name):
-    """获取实际用于前端html页面中的id属性
+_scope_name_allowed_chars = set(string.ascii_letters + string.digits + '_-')
 
+
+def _check_scope_name(name):
+    """
     :param str name:
     """
+    assert all(i in _scope_name_allowed_chars for i in name), "Scope name only allow letter/digit/'_'/'-' char."
+
+
+def _parse_scope(name, no_css_selector=False):
+    """获取实际用于前端html页面中的CSS选择器/元素名
+
+    name 为str/tuple,为str时,视作Dom ID名; tuple格式为(css选择器符号, 元素名),仅供内部实现使用
+    """
+    selector = '#'
+    if isinstance(name, tuple):
+        selector, name = name
+
     name = name.replace(' ', '-')
-    return 'pywebio-scope-%s' % name
+
+    if no_css_selector:
+        selector = ''
+
+    return '%spywebio-scope-%s' % (selector, name)
 
 
-def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTOM, if_exist='none'):
+def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTOM, if_exist=None):
     """创建一个新的scope.
 
     :param str name: scope名
@@ -141,16 +160,17 @@ def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTO
        `OutputPosition.TOP` : 在父scope的顶部创建, `OutputPosition.BOTTOM` : 在父scope的尾部创建
     :param str if_exist: 已经存在 ``name`` scope 时如何操作:
 
-        - `'none'` 表示不进行任何操作
+        - `None` 表示不进行任何操作
         - `'remove'` 表示先移除旧scope再创建新scope
         - `'clear'` 表示将旧scope的内容清除,不创建新scope
 
-       默认为 `'none'`
+       默认为 `None`
     """
     if isinstance(container_scope, int):
         container_scope = get_current_session().get_scope_name(container_scope)
 
-    send_msg('output_ctl', dict(set_scope=_parse_scope(name),
+    _check_scope_name(name)
+    send_msg('output_ctl', dict(set_scope=_parse_scope(name, no_css_selector=True),
                                 container=_parse_scope(container_scope),
                                 position=position, if_exist=if_exist))
 
@@ -187,7 +207,7 @@ def scroll_to(scope, position=Position.TOP):
 
 def _get_output_spec(type, scope, position, **other_spec):
     """
-    获取 ``output`` 指令的spec字段
+    获取输出类指令的spec字段
 
     :param str type: 输出类型
     :param int/str scope: 输出到的scope
@@ -197,6 +217,8 @@ def _get_output_spec(type, scope, position, **other_spec):
     :return dict:  ``output`` 指令的spec字段
     """
     spec = dict(type=type)
+
+    # 将非None的参数加入SPEC中
     spec.update({k: v for k, v in other_spec.items() if v is not None})
 
     if isinstance(scope, int):
@@ -959,20 +981,19 @@ def output(*contents):
         def __del__(self):
             pass
 
-        def __init__(self, spec, container_selector):
+        def __init__(self, spec, scope):
             super().__init__(spec)
-            self.container_selector = container_selector
+            self.scope = scope
 
         @safely_destruct_output_when_exp('outputs')
         def reset(self, *outputs):
-            send_msg('output_ctl', dict(clear=self.container_selector, use_custom_selector=True))
+            clear_scope(scope=self.scope)
             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['scope'] = _parse_scope(self.scope)
                 o.spec['position'] = OutputPosition.BOTTOM
                 o.send()
 
@@ -981,12 +1002,11 @@ def output(*contents):
             """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['scope'] = _parse_scope(self.scope)
                 o.spec['position'] = idx + direction * acc
                 o.send()
 
-    dom_id = _parse_scope(random_str(10))
+    dom_name = random_str(10)
     tpl = """<div class="{{dom_class_name}}">
             {{#contents}}
                 {{#.}}
@@ -994,8 +1014,9 @@ def output(*contents):
                 {{/.}}
             {{/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)
+    out_spec = put_widget(template=tpl,
+                          data=dict(contents=contents, dom_class_name=_parse_scope(dom_name, no_css_selector=True)))
+    return OutputHandler(Output.dump_dict(out_spec), ('.', dom_name))
 
 
 @safely_destruct_output_when_exp('outputs')
@@ -1121,7 +1142,7 @@ def toast(content, duration=2, position='center', color='info', onclick=None):
     color = colors.get(color, color)
     callback_id = output_register_callback(lambda _: onclick()) if onclick is not None else None
 
-    send_msg(cmd='toast', spec=dict(content=content, duration=int(duration*1000), position=position,
+    send_msg(cmd='toast', spec=dict(content=content, duration=int(duration * 1000), position=position,
                                     color=color, callback_id=callback_id))
 
 
@@ -1150,6 +1171,8 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
     """
     if name is None:
         name = random_str(10)
+    else:
+        _check_scope_name(name)
 
     class use_scope_:
         def __enter__(self):

+ 5 - 4
pywebio/utils.py

@@ -154,12 +154,13 @@ def get_free_port():
         return s.getsockname()[1]
 
 
-def random_str(len=16):
-    """生成小写字母和数组组成的随机字符串
+def random_str(length=16):
+    """生成字母和数组组成的随机字符串
 
-    :param int len: 字符串长度
+    :param int length: 字符串长度
     """
-    return ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(len))
+    candidates = string.ascii_letters + string.digits
+    return ''.join(random.SystemRandom().choice(candidates) for _ in range(length))
 
 
 def run_as_function(gen):

+ 17 - 24
webiojs/src/handlers/output.ts

@@ -38,11 +38,7 @@ export class OutputHandler implements CommandHandler {
                 return console.error(`Handle command error, command: ${msg}, error:${e}`);
             }
 
-            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);
+            let container_elem = $(msg.spec.scope);
 
             if (config.outputAnimation && elem[0].tagName.toLowerCase() != 'script' && container_elem.length == 1) elem.hide();
 
@@ -93,22 +89,22 @@ export class OutputHandler implements CommandHandler {
                 set_scope: string, // scope名
                 container: string, // 此scope的父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, `'clear'` 表示将旧scope的内容清除,不创建新scope,null/不指定时表示立即返回不进行任何操作
             };
 
-            let container_elem = $(`#${spec.container}`);
+            let container_elem = $(`${spec.container}`);
             if (container_elem.length === 0)
                 return console.error(`Scope '${msg.spec.scope}' not found`);
 
-            let old = this.container_elem.find(`#${spec.set_scope}`);
+            let old = $(`#${spec.set_scope}`);
             if (old.length) {
-                if (spec.if_exist == 'none')
-                    return;
-                else if (spec.if_exist == 'remove')
+                if (spec.if_exist == 'remove')
                     old.remove();
                 else if (spec.if_exist == 'clear') {
                     old.empty();
                     return;
+                }else{
+                    return
                 }
             }
 
@@ -119,23 +115,20 @@ export class OutputHandler implements CommandHandler {
                 container_elem.append(html);
             else {
                 if (spec.position >= 0)
-                    $(`#${spec.container}>*`).eq(spec.position).insertBefore(html);
+                    $(`${spec.container}>*`).eq(spec.position).insertBefore(html);
                 else
-                    $(`#${spec.container}>*`).eq(spec.position).insertAfter(html);
+                    $(`${spec.container}>*`).eq(spec.position).insertAfter(html);
             }
         }
         if (msg.spec.clear !== undefined) {
-            if (!msg.spec.use_custom_selector)
-                this.container_elem.find(`#${msg.spec.clear}`).empty();
-            else
-                $(msg.spec.clear).empty();
+            $(msg.spec.clear).empty();
         }
         if (msg.spec.clear_before !== undefined)
-            this.container_elem.find(`#${msg.spec.clear_before}`).prevAll().remove();
+            $(`${msg.spec.clear_before}`).prevAll().remove();
         if (msg.spec.clear_after !== undefined)
-            this.container_elem.find(`#${msg.spec.clear_after}~*`).remove();
+            $(`${msg.spec.clear_after}~*`).remove();
         if (msg.spec.scroll_to !== undefined) {
-            let target = $(`#${msg.spec.scroll_to}`);
+            let target = $(`${msg.spec.scroll_to}`);
             if (!target.length) {
                 console.error(`Scope ${msg.spec.scroll_to} not found`);
             } else if (state.OutputFixedHeight) {
@@ -145,11 +138,11 @@ export class OutputHandler implements CommandHandler {
             }
         }
         if (msg.spec.clear_range !== undefined) {
-            if (this.container_elem.find(`#${msg.spec.clear_range[0]}`).length &&
-                this.container_elem.find(`#${msg.spec.clear_range[1]}`).length) {
+            if ($(`${msg.spec.clear_range[0]}`).length &&
+                $(`${msg.spec.clear_range[1]}`).length) {
                 let removed: HTMLElement[] = [];
                 let valid = false;
-                this.container_elem.find(`#${msg.spec.clear_range[0]}~*`).each(function () {
+                $(`${msg.spec.clear_range[0]}~*`).each(function () {
                     if (this.id === msg.spec.clear_range[1]) {
                         valid = true;
                         return false;
@@ -164,7 +157,7 @@ export class OutputHandler implements CommandHandler {
             }
         }
         if (msg.spec.remove !== undefined)
-            this.container_elem.find(`#${msg.spec.remove}`).remove();
+            $(`${msg.spec.remove}`).remove();
     };
 
 }