1
0
Эх сурвалжийг харах

feat: add popup() to show a popup

wangweimin 5 жил өмнө
parent
commit
2911878fe5

+ 12 - 0
docs/guide.rst

@@ -127,6 +127,9 @@ PyWebIO提供了一些便捷函数来输出表格、链接等格式::
     # 文件输出
     # 文件输出
     put_file('hello_word.txt', b'hello word!')
     put_file('hello_word.txt', b'hello word!')
 
 
+    # 显示一个弹窗
+    popup('popup title', 'popup html content')
+
 所有输出内容的函数名都以 ``put_`` 开始
 所有输出内容的函数名都以 ``put_`` 开始
 
 
 PyWebIO提供的全部输出函数请见 :doc:`pywebio.output </output>` 模块
 PyWebIO提供的全部输出函数请见 :doc:`pywebio.output </output>` 模块
@@ -150,6 +153,15 @@ PyWebIO提供的全部输出函数请见 :doc:`pywebio.output </output>` 模块
 
 
 .. image:: /assets/put_table.png
 .. image:: /assets/put_table.png
 
 
+类似的, `popup() <pywebio.output.popup>` 也可以将 ``put_xxx`` 作为弹窗内容::
+
+    popup('Popup title', [
+        '<h3>Popup Content</h3>',
+        put_text('html: <br/>'),
+        put_table([['A', 'B'], ['C', 'D']]),
+        put_buttons(['close_popup()'], onclick=lambda _: close_popup())
+    ])
+
 事件回调
 事件回调
 ^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^
 
 

+ 19 - 0
docs/spec.rst

@@ -202,6 +202,25 @@ output:
   * data: 二维数组,表示表格数据,第一行为表头
   * data: 二维数组,表示表格数据,第一行为表头
   * span: 跨行/跨列的单元格信息,格式: {"[行id],[列id]": {"row":跨行数, "col":跨列数 }}
   * span: 跨行/跨列的单元格信息,格式: {"[行id],[列id]": {"row":跨行数, "col":跨列数 }}
 
 
+popup
+^^^^^^^^^^^^^^^
+显示弹窗
+
+命令 spec 字段:
+
+* title: 弹窗标题
+* content: 数组,元素为字符串/对象,字符串表示html
+* size: 弹窗窗口大小,可选值: ``large`` 、 ``normal`` 、 ``small``
+* implicit_close: 是否可以通过点击弹窗外的内容或按下 `Esc` 键来关闭弹窗
+* closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
+  设置为 ``false`` 时弹窗仅能通过 ``popup_close`` command 关闭, ``implicit_close`` 参数被忽略.
+
+close_popup
+^^^^^^^^^^^^^^^
+关闭正在显示的弹窗
+
+该命令字段 ``spec`` 为 ``null``
+
 output_ctl:
 output_ctl:
 ^^^^^^^^^^^^^^^
 ^^^^^^^^^^^^^^^
 输入控制
 输入控制

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
pywebio/html/js/pywebio.min.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
pywebio/html/js/pywebio.min.js.map


+ 4 - 0
pywebio/io_ctrl.py

@@ -18,6 +18,10 @@ class OutputReturn:
     否则消息则作为其他消息的一部分
     否则消息则作为其他消息的一部分
     """
     """
 
 
+    @staticmethod
+    def jsonify(data):
+        return json.loads(json.dumps(data, default=output_json_encoder))
+
     @staticmethod
     @staticmethod
     def safely_destruct(obj):
     def safely_destruct(obj):
         """安全销毁 OutputReturn 对象, 使 OutputReturn.__del__ 不进行任何操作"""
         """安全销毁 OutputReturn 对象, 使 OutputReturn.__del__ 不进行任何操作"""

+ 42 - 2
pywebio/output.py

@@ -35,7 +35,6 @@ r"""输出内容到用户浏览器
 .. autofunction:: put_file
 .. autofunction:: put_file
 """
 """
 import io
 import io
-import json
 import logging
 import logging
 from base64 import b64encode
 from base64 import b64encode
 from collections.abc import Mapping
 from collections.abc import Mapping
@@ -51,7 +50,15 @@ logger = logging.getLogger(__name__)
 
 
 __all__ = ['Position', 'set_title', 'set_output_fixed_height', 'set_auto_scroll_bottom', 'set_anchor', 'clear_before',
 __all__ = ['Position', 'set_title', 'set_output_fixed_height', 'set_auto_scroll_bottom', 'set_anchor', 'clear_before',
            'clear_after', 'clear_range', 'remove', 'scroll_to', 'put_text', 'put_html', 'put_code', 'put_markdown',
            'clear_after', 'clear_range', 'remove', 'scroll_to', 'put_text', 'put_html', 'put_code', 'put_markdown',
-           'put_table', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file']
+           'put_table', 'table_cell_buttons', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
+           'close_popup']
+
+
+# popup尺寸
+class PopupSize:
+    LARGE = 'large'
+    NORMAL = 'normal'
+    SMALL = 'small'
 
 
 
 
 class Position:
 class Position:
@@ -479,3 +486,36 @@ def put_file(name, content, anchor=None, before=None, after=None) -> OutputRetur
     spec = _get_output_spec('file', name=name, content=content, anchor=anchor, before=before, after=after)
     spec = _get_output_spec('file', name=name, content=content, anchor=anchor, before=before, after=after)
     return OutputReturn(spec)
     return OutputReturn(spec)
 
 
+
+@safely_destruct_output_when_exp('content')
+def popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True):
+    """popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True)
+
+    显示弹窗
+
+    :param str title: 弹窗标题
+    :type content: list/str
+    :param content: 弹窗内容. 当 ``content`` 为字符串时,表示html;当 ``content`` 为列表是,列表项可以为字符串和 ``put_xxx`` 类输出函数的返回值.
+    :param str size: 弹窗窗口大小,可选值:
+
+         * ``LARGE`` : 大尺寸
+         * ``NORMAL`` : 普通尺寸
+         * ``SMALL`` : 小尺寸
+
+    :param bool implicit_close: 是否可以通过点击弹窗外的内容或按下 ``Esc`` 键来关闭弹窗
+    :param bool closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,
+       设置为 ``False`` 时弹窗仅能通过 :func:`popup_close()` 关闭, ``implicit_close`` 参数被忽略.
+    """
+    if isinstance(content, str):
+        content = [content]
+
+    for item in content:
+        assert isinstance(item, (str, OutputReturn)), "popup() content must be list of str/put_xxx()"
+
+    send_msg(cmd='popup', spec=dict(content=OutputReturn.jsonify(content), title=title, size=size,
+                                    implicit_close=implicit_close, closable=closable))
+
+
+def close_popup():
+    """关闭弹窗"""
+    send_msg(cmd='close_popup')

+ 1 - 1
test/1.basic.py

@@ -38,7 +38,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 
 def start_test_server():
 def start_test_server():
     pywebio.enable_debug()
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1', debug=True, auto_open_webbrowser=False)
+    start_server(target, port=8080, host='127.0.0.1', auto_open_webbrowser=False)
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':

+ 1 - 1
test/11.bokeh.py

@@ -228,7 +228,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 
 def start_test_server():
 def start_test_server():
     pywebio.enable_debug()
     pywebio.enable_debug()
-    start_server(target, port=8080, debug=True, auto_open_webbrowser=False)
+    start_server(target, port=8080, auto_open_webbrowser=False)
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':

+ 1 - 1
test/5.coroutine_based_session.py

@@ -35,7 +35,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 
 def start_test_server():
 def start_test_server():
     pywebio.enable_debug()
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1', debug=True)
+    start_server(target, port=8080, host='127.0.0.1')
 
 
 
 
 if __name__ == '__main__':
 if __name__ == '__main__':

+ 28 - 0
test/template.py

@@ -98,6 +98,24 @@ def basic_output():
         ('MIDDLE', Position.MIDDLE),
         ('MIDDLE', Position.MIDDLE),
     ], onclick=lambda pos: scroll_to('scroll_basis', pos), anchor='scroll_basis_btns')
     ], onclick=lambda pos: scroll_to('scroll_basis', pos), anchor='scroll_basis_btns')
 
 
+    def show_popup():
+        popup('Popup title', [
+            '<h3>Popup Content</h3>',
+            put_text('html: <br/>'),
+            put_table([
+                ['Type', 'Content'],
+                ['html', 'X<sup>2</sup>'],
+                ['text', put_text('<hr/>')],
+                ['buttons', put_buttons(['A', 'B'], onclick=...)],
+                ['markdown', put_markdown('`Awesome PyWebIO!`')],
+                ['file', put_file('hello.text', b'')],
+                ['table', put_table([['A', 'B'], ['C', 'D']])]
+            ]),
+            put_buttons(['close_popup()'], onclick=lambda _: close_popup())
+        ], size=PopupSize.NORMAL)
+
+    put_buttons(['popup()'], onclick=lambda _: show_popup(), anchor='popup_btn')
+
     def edit_row(choice, row):
     def edit_row(choice, row):
         put_text("You click %s button at row %s" % (choice, row), after='table_cell_buttons')
         put_text("You click %s button at row %s" % (choice, row), after='table_cell_buttons')
 
 
@@ -227,6 +245,7 @@ def test_output(browser: Chrome, enable_percy=False):
         time.sleep(0.5)
         time.sleep(0.5)
         browser.execute_script("arguments[0].click();", btn)
         browser.execute_script("arguments[0].click();", btn)
 
 
+    # 滚动窗口
     btns = browser.find_elements_by_css_selector('#pywebio-anchor-scroll_basis_btns button')
     btns = browser.find_elements_by_css_selector('#pywebio-anchor-scroll_basis_btns button')
     for btn in btns:
     for btn in btns:
         time.sleep(1)
         time.sleep(1)
@@ -235,6 +254,15 @@ def test_output(browser: Chrome, enable_percy=False):
     time.sleep(1)
     time.sleep(1)
     enable_percy and percySnapshot(browser=browser, name='basic output')
     enable_percy and percySnapshot(browser=browser, name='basic output')
 
 
+    # popup
+    btn = browser.find_element_by_css_selector('#pywebio-anchor-popup_btn button')
+    browser.execute_script("arguments[0].click();", btn)
+
+    time.sleep(1)
+    enable_percy and percySnapshot(browser=browser, name='popup')
+
+    browser.execute_script("$('.modal').modal('hide');")
+
 
 
 def basic_input():
 def basic_input():
     age = yield input("How old are you?", type=NUMBER)
     age = yield input("How old are you?", type=NUMBER)

+ 91 - 0
webiojs/src/handlers/popup.ts

@@ -0,0 +1,91 @@
+import {Command, Session} from "../session";
+import {randomid} from "../utils";
+
+import {getWidgetElement} from "../models/output"
+import {CommandHandler} from "./base";
+
+
+export class PopupHandler implements CommandHandler{
+    session: Session;
+
+    accept_command = ['popup', 'close_popup'];
+
+    private body = $('body');
+
+    constructor(session: Session) {
+        this.session = session;
+    }
+
+    handle_message(msg: Command) {
+        if (msg.command == 'popup') {
+            let ele = PopupHandler.get_element(msg.spec);
+            this.body.append(ele);
+            ele.on('hidden.bs.modal', function (e) {
+                ele.remove();
+            });
+            // @ts-ignore
+            ele.modal('show');
+        } else if (msg.command == 'close_popup') {
+            // @ts-ignore
+            $('.modal').modal('hide');
+        }
+
+    }
+
+    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">
+          <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">
+                <h5 class="modal-title" id="model-id-{{ mid }}">{{ title }}</h5>
+                {{#closable}}
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                  <span aria-hidden="true">&times;</span>
+                </button>
+                {{/closable}}
+              </div>
+              <div class="modal-body markdown-body">
+                {{& content }}
+              </div>
+              <!--  
+              <div class="modal-footer">
+                <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                <button type="button" class="btn btn-primary">Submit</button>
+              </div> 
+              -->
+            </div>
+          </div>
+        </div>`;
+        let mid = randomid(10);
+
+        let body_html = '';
+
+        for (let output_item of spec.content) {
+            if (typeof output_item === 'object') {
+                try {
+                    let nodes = getWidgetElement(output_item);
+                    for (let node of nodes)
+                        body_html += node.outerHTML || '';
+                } catch (e) {
+                    console.error('Get widget html error,', e, output_item);
+                }
+            } else {
+                body_html += output_item;
+            }
+        }
+
+        if (!spec.closable)
+            spec.implicit_close = false;
+
+        let html = Mustache.render(tpl, {
+            ...spec,  // 字段: content, title, size, implicit_close, closable
+            large: spec.size == 'large',
+            small: spec.size == 'small',
+            mid: mid,
+            content: body_html,
+        });
+        return $(html as string);
+    }
+
+}

+ 3 - 1
webiojs/src/main.ts

@@ -4,6 +4,7 @@ import {InputHandler} from "./handlers/input"
 import {OutputHandler} from "./handlers/output"
 import {OutputHandler} from "./handlers/output"
 import {DisplayAreaButtonOnClick} from "./models/output"
 import {DisplayAreaButtonOnClick} from "./models/output"
 import {CommandDispatcher} from "./handlers/base"
 import {CommandDispatcher} from "./handlers/base"
+import {PopupHandler} from "./handlers/popup";
 
 
 // 获取后端API地址
 // 获取后端API地址
 function get_backend_addr() {
 function get_backend_addr() {
@@ -22,8 +23,9 @@ function set_up_session(webio_session: Session, output_container_elem: JQuery, i
 
 
     let output_ctrl = new OutputHandler(webio_session, output_container_elem);
     let output_ctrl = new OutputHandler(webio_session, output_container_elem);
     let input_ctrl = new InputHandler(webio_session, input_container_elem);
     let input_ctrl = new InputHandler(webio_session, input_container_elem);
+    let popup_ctrl = new PopupHandler(webio_session);
 
 
-    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl);
+    let dispatcher = new CommandDispatcher(output_ctrl, input_ctrl, popup_ctrl);
 
 
     webio_session.on_server_message((msg: Command) => {
     webio_session.on_server_message((msg: Command) => {
         let ok = dispatcher.dispatch_message(msg);
         let ok = dispatcher.dispatch_message(msg);

+ 11 - 0
webiojs/src/utils.ts

@@ -113,3 +113,14 @@ export function box_scroll_to(target: JQuery, container: JQuery, position = 'top
     if (scrollTopOffset !== null)
     if (scrollTopOffset !== null)
         container.stop().animate({scrollTop: container.scrollTop() + scrollTopOffset + offset}, speed, complete);
         container.stop().animate({scrollTop: container.scrollTop() + scrollTopOffset + offset}, speed, complete);
 }
 }
+
+
+export function randomid(length: number) {
+    let result = '';
+    let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+    let charactersLength = characters.length;
+    for (let i = 0; i < length; i++) {
+        result += characters.charAt(Math.floor(Math.random() * charactersLength));
+    }
+    return result;
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно