瀏覽代碼

feat: `popup()` support as context manager and decorator

wangweimin 4 年之前
父節點
當前提交
53d2f24d13
共有 4 個文件被更改,包括 102 次插入55 次删除
  1. 1 0
      docs/spec.rst
  2. 88 48
      pywebio/output.py
  3. 10 2
      test/13.misc.py
  4. 3 5
      webiojs/src/handlers/popup.ts

+ 1 - 0
docs/spec.rst

@@ -228,6 +228,7 @@ popup
 * implicit_close: 是否可以通过点击弹窗外的内容或按下 `Esc` 键来关闭弹窗
 * closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
   设置为 ``false`` 时弹窗仅能通过 ``popup_close`` command 关闭, ``implicit_close`` 参数被忽略.
+* dom_id: 弹窗内容区的dom id
 
 toast
 ^^^^^^^^^^^^^^^

+ 88 - 48
pywebio/output.py

@@ -59,11 +59,11 @@ r"""输出内容到用户浏览器
 """
 import io
 import logging
+import string
 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
@@ -1068,12 +1068,12 @@ def style(outputs, css_style) -> Union[Output, OutputList]:
 
 
 @safely_destruct_output_when_exp('content')
-def popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True):
+def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closable=True):
     """popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True)
 
     显示弹窗
 
-    PyWebIO不允许同时显示多个弹窗,在显示新弹窗前,会自动关闭页面上存在的弹窗
+    ⚠️: PyWebIO不允许同时显示多个弹窗,在显示新弹窗前,会自动关闭页面上存在的弹窗。可以使用 `close_popup()` 主动关闭弹窗
 
     :param str title: 弹窗标题
     :type content: list/str/put_xxx()
@@ -1088,9 +1088,11 @@ def popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=T
     :param bool closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
        设置为 ``False`` 时弹窗仅能通过 :func:`popup_close()` 关闭, ``implicit_close`` 参数被忽略.
 
-    Example::
+    支持直接传入内容、上下文管理器、装饰器三种形式的调用
 
-        popup('popup title', 'popup html content', size=PopupSize.SMALL)
+    * 直接传入内容::
+
+        popup('popup title', 'popup text content', size=PopupSize.SMALL)
 
         popup('Popup title', [
             put_html('<h3>Popup Content</h3>'),
@@ -1099,19 +1101,49 @@ def popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=T
             put_buttons(['close_popup()'], onclick=lambda _: close_popup())
         ])
 
+    * 作为上下文管理器使用::
+
+        with popup('Popup title') as s:
+            put_html('<h3>Popup Content</h3>')
+            put_text('html: <br/>')
+            put_buttons(['clear()'], onclick=lambda _: clear(scope=s))
+
+        put_text('Also work!', scope=s)
+
+
+    上下文管理器会开启一个新的输出域并返回Scope名,上下文管理器中的输出调用会显示到弹窗上。
+    上下文管理器退出后,弹窗并不会关闭,依然可以使用 ``scope`` 参数输出内容到弹窗。
+
+    * 作为装饰器使用::
+
+        @popup('Popup title')
+        def show_popup():
+            put_xxx()
+            ...
+
+        show_popup()
+
     """
+    if content is None:
+        content = []
+
     if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
 
     for item in content:
         assert isinstance(item, (str, Output)), "popup() content must be list of str/put_xxx()"
 
+    dom_id = random_str(10)
+
     send_msg(cmd='popup', spec=dict(content=Output.dump_dict(content), title=title, size=size,
-                                    implicit_close=implicit_close, closable=closable))
+                                    implicit_close=implicit_close, closable=closable,
+                                    dom_id=_parse_scope(dom_id, no_css_selector=True)))
+
+    return use_scope_(dom_id)
 
 
 def close_popup():
-    """关闭弹窗"""
+    """关闭当前页面上正在显示的弹窗"""
     send_msg(cmd='close_popup')
 
 
@@ -1174,44 +1206,52 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
     else:
         _check_scope_name(name)
 
-    class use_scope_:
-        def __enter__(self):
-            if create_scope:
-                set_scope(name, **scope_params)
-
-            if clear:
-                clear_scope(name)
-
-            get_current_session().push_scope(name)
-            return name
-
-        def __exit__(self, exc_type, exc_val, exc_tb):
-            """该方法如果返回True ,说明上下文管理器可以处理异常,使得 with 语句终止异常传播"""
-            get_current_session().pop_scope()
-            return False  # Propagate Exception
-
-        def __call__(self, func):
-            """装饰器"""
-
-            @wraps(func)
-            def wrapper(*args, **kwargs):
-                self.__enter__()
-                try:
-                    return func(*args, **kwargs)
-                finally:
-                    self.__exit__(None, None, None)
-
-            @wraps(func)
-            async def coro_wrapper(*args, **kwargs):
-                self.__enter__()
-                try:
-                    return await func(*args, **kwargs)
-                finally:
-                    self.__exit__(None, None, None)
-
-            if iscoroutinefunction(func):
-                return coro_wrapper
-            else:
-                return wrapper
-
-    return use_scope_()
+    def before_enter():
+        if create_scope:
+            set_scope(name, **scope_params)
+
+        if clear:
+            clear_scope(name)
+
+    return use_scope_(name=name, before_enter=before_enter)
+
+
+class use_scope_:
+    def __init__(self, name, before_enter=None):
+        self.before_enter = before_enter
+        self.name = name
+
+    def __enter__(self):
+        if self.before_enter:
+            self.before_enter()
+        get_current_session().push_scope(self.name)
+        return self.name
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """该方法如果返回True ,说明上下文管理器可以处理异常,使得 with 语句终止异常传播"""
+        get_current_session().pop_scope()
+        return False  # Propagate Exception
+
+    def __call__(self, func):
+        """装饰器"""
+
+        @wraps(func)
+        def wrapper(*args, **kwargs):
+            self.__enter__()
+            try:
+                return func(*args, **kwargs)
+            finally:
+                self.__exit__(None, None, None)
+
+        @wraps(func)
+        async def coro_wrapper(*args, **kwargs):
+            self.__enter__()
+            try:
+                return await func(*args, **kwargs)
+            finally:
+                self.__exit__(None, None, None)
+
+        if iscoroutinefunction(func):
+            return coro_wrapper
+        else:
+            return wrapper

+ 10 - 2
test/13.misc.py

@@ -54,8 +54,16 @@ def target():
         ['1', table_cell_buttons(['edit', 'delete'], onclick=lambda _: None)],
     ])
 
-    popup('title', 'html content')
-    popup('title2', 'html content')
+    popup('title', 'text content')
+    @popup('Popup title')
+    def show_popup():
+        put_html('<h3>Popup Content</h3>')
+        put_text('html: <br/>')
+    with popup('Popup title') as s:
+        put_html('<h3>Popup Content</h3>')
+        clear(s)
+        put_buttons(['clear()'], onclick=lambda _: clear(s))
+    popup('title2', 'text content')
     close_popup()
 
     with use_scope() as name:

+ 3 - 5
webiojs/src/handlers/popup.ts

@@ -58,7 +58,7 @@ export class PopupHandler implements CommandHandler {
 
     static get_element(spec: { title: string, content: any[], closable: boolean, implicit_close: boolean, size: string }) {
         // https://v4.bootcss.com/docs/components/modal/#options
-        const tpl = `<div class="modal fade" {{^implicit_close}}data-backdrop="static"{{/implicit_close}} aria-labelledby="model-id-{{ mid }}" tabindex="-1" role="dialog" aria-hidden="true">
+        const tpl = `<div class="modal fade" {{^implicit_close}}data-backdrop="static"{{/implicit_close}} aria-labelledby="model-id-{{ dom_id }}" tabindex="-1" role="dialog" aria-hidden="true">
           <div class="modal-dialog modal-dialog-scrollable {{#large}}modal-lg{{/large}} {{#small}}modal-sm{{/small}}" role="document">
             <div class="modal-content">
               <div class="modal-header">
@@ -69,7 +69,7 @@ export class PopupHandler implements CommandHandler {
                 </button>
                 {{/closable}}
               </div>
-              <div class="modal-body markdown-body">
+              <div class="modal-body markdown-body" id="{{ dom_id }}">
                 {{#content}}
                     {{& pywebio_output_parse}}
                 {{/content}}
@@ -83,7 +83,6 @@ export class PopupHandler implements CommandHandler {
             </div>
           </div>
         </div>`;
-        let mid = randomid(10);
 
         if (!spec.closable)
             spec.implicit_close = false;
@@ -96,10 +95,9 @@ export class PopupHandler implements CommandHandler {
         };
 
         let html = Mustache.render(tpl, {
-            ...spec,  // 字段: content, title, size, implicit_close, closable
+            ...spec,  // 字段: content, title, size, implicit_close, closable, dom_id
             large: spec.size == 'large',
             small: spec.size == 'small',
-            mid: mid,
             pywebio_output_parse: pywebio_output_parse
         });
         return $(html as string);