Pārlūkot izejas kodu

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

wangweimin 4 gadi atpakaļ
vecāks
revīzija
53d2f24d13
4 mainītis faili ar 102 papildinājumiem un 55 dzēšanām
  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` 键来关闭弹窗
 * implicit_close: 是否可以通过点击弹窗外的内容或按下 `Esc` 键来关闭弹窗
 * closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
 * closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
   设置为 ``false`` 时弹窗仅能通过 ``popup_close`` command 关闭, ``implicit_close`` 参数被忽略.
   设置为 ``false`` 时弹窗仅能通过 ``popup_close`` command 关闭, ``implicit_close`` 参数被忽略.
+* dom_id: 弹窗内容区的dom id
 
 
 toast
 toast
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^

+ 88 - 48
pywebio/output.py

@@ -59,11 +59,11 @@ r"""输出内容到用户浏览器
 """
 """
 import io
 import io
 import logging
 import logging
+import string
 from base64 import b64encode
 from base64 import b64encode
 from collections.abc import Mapping, Sequence
 from collections.abc import Mapping, Sequence
 from functools import wraps
 from functools import wraps
 from typing import Union
 from typing import Union
-import string
 
 
 from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList
 from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList
 from .session import get_current_session, download
 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')
 @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)
     """popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True)
 
 
     显示弹窗
     显示弹窗
 
 
-    PyWebIO不允许同时显示多个弹窗,在显示新弹窗前,会自动关闭页面上存在的弹窗
+    ⚠️: PyWebIO不允许同时显示多个弹窗,在显示新弹窗前,会自动关闭页面上存在的弹窗。可以使用 `close_popup()` 主动关闭弹窗
 
 
     :param str title: 弹窗标题
     :param str title: 弹窗标题
     :type content: list/str/put_xxx()
     :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: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
     :param bool closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
        设置为 ``False`` 时弹窗仅能通过 :func:`popup_close()` 关闭, ``implicit_close`` 参数被忽略.
        设置为 ``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', [
         popup('Popup title', [
             put_html('<h3>Popup Content</h3>'),
             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())
             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)):
     if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
         content = [content]
 
 
     for item in content:
     for item in content:
         assert isinstance(item, (str, Output)), "popup() content must be list of str/put_xxx()"
         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,
     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():
 def close_popup():
-    """关闭弹窗"""
+    """关闭当前页面上正在显示的弹窗"""
     send_msg(cmd='close_popup')
     send_msg(cmd='close_popup')
 
 
 
 
@@ -1174,44 +1206,52 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
     else:
     else:
         _check_scope_name(name)
         _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)],
         ['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()
     close_popup()
 
 
     with use_scope() as name:
     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 }) {
     static get_element(spec: { title: string, content: any[], closable: boolean, implicit_close: boolean, size: string }) {
         // https://v4.bootcss.com/docs/components/modal/#options
         // 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-dialog modal-dialog-scrollable {{#large}}modal-lg{{/large}} {{#small}}modal-sm{{/small}}" role="document">
             <div class="modal-content">
             <div class="modal-content">
               <div class="modal-header">
               <div class="modal-header">
@@ -69,7 +69,7 @@ export class PopupHandler implements CommandHandler {
                 </button>
                 </button>
                 {{/closable}}
                 {{/closable}}
               </div>
               </div>
-              <div class="modal-body markdown-body">
+              <div class="modal-body markdown-body" id="{{ dom_id }}">
                 {{#content}}
                 {{#content}}
                     {{& pywebio_output_parse}}
                     {{& pywebio_output_parse}}
                 {{/content}}
                 {{/content}}
@@ -83,7 +83,6 @@ export class PopupHandler implements CommandHandler {
             </div>
             </div>
           </div>
           </div>
         </div>`;
         </div>`;
-        let mid = randomid(10);
 
 
         if (!spec.closable)
         if (!spec.closable)
             spec.implicit_close = false;
             spec.implicit_close = false;
@@ -96,10 +95,9 @@ export class PopupHandler implements CommandHandler {
         };
         };
 
 
         let html = Mustache.render(tpl, {
         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',
             large: spec.size == 'large',
             small: spec.size == 'small',
             small: spec.size == 'small',
-            mid: mid,
             pywebio_output_parse: pywebio_output_parse
             pywebio_output_parse: pywebio_output_parse
         });
         });
         return $(html as string);
         return $(html as string);