浏览代码

feat: add 'put_tabs()' to output tabs

wangweimin 4 年之前
父节点
当前提交
b19dd62d58
共有 4 个文件被更改,包括 179 次插入16 次删除
  1. 55 1
      pywebio/html/css/app.css
  2. 39 2
      pywebio/output.py
  3. 13 0
      test/template.py
  4. 72 13
      webiojs/src/models/output.ts

+ 55 - 1
pywebio/html/css/app.css

@@ -215,4 +215,58 @@ details[open]>summary {
 
 
 .alert > a {
 .alert > a {
     font-weight: 700;
     font-weight: 700;
-}
+}
+
+/*Tabs widget style*/
+/*Credit: https://themes.gohugo.io/theme/hugo-book/docs/shortcodes/tabs/   Licensed by MIT */
+.webio-tabs {
+    margin-top: 1rem;
+    margin-bottom: 1rem;
+    border: 1px solid #e9ecef;
+    border-radius: .25rem;
+    overflow: hidden;
+    display: flex;
+    flex-wrap: wrap
+}
+
+.webio-tabs > label {
+    display: inline-block;
+    padding: .5rem 1rem;
+    border-bottom: 1px transparent;
+    cursor: pointer;
+    margin-bottom: 0!important;
+}
+
+.webio-tabs > label:hover {
+    background-color: #e9ecef;
+}
+
+.webio-tabs > .webio-tabs-content {
+    order: 999;
+    width: 100%;
+    border-top: 1px solid #f5f5f5;
+    padding: 1rem;
+    display: none
+}
+
+.webio-tabs > input[type=radio]:checked + label {
+    border-bottom: 2px solid #0055bb
+}
+
+.webio-tabs > input[type=radio]:checked + label + .webio-tabs-content {
+    display: block
+}
+
+.webio-tabs > input.toggle {
+    height: 0;
+    width: 0;
+    overflow: hidden;
+    opacity: 0;
+    position: absolute;
+}
+
+.webio-tabs > [type=radio] {
+    box-sizing: border-box;
+    padding: 0;
+}
+/*End of Tabs widget*/

+ 39 - 2
pywebio/output.py

@@ -58,6 +58,8 @@ Functions list
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `put_file`                | Output a link to download a file                           |
 |                    | `put_file`                | Output a link to download a file                           |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
+|                    | `put_tabs`:sup:`*`        | Output tabs                                                |
+|                    +---------------------------+------------------------------------------------------------+
 |                    | `put_collapse`:sup:`*†`   | Output collapsible content                                 |
 |                    | `put_collapse`:sup:`*†`   | Output collapsible content                                 |
 |                    +---------------------------+------------------------------------------------------------+
 |                    +---------------------------+------------------------------------------------------------+
 |                    | `put_scrollable`:sup:`*†` | | Output a fixed height content area,                      |
 |                    | `put_scrollable`:sup:`*†` | | Output a fixed height content area,                      |
@@ -168,7 +170,7 @@ except ImportError:
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
-__all__ = ['Position', 'remove', 'scroll_to',
+__all__ = ['Position', 'remove', 'scroll_to', 'put_tabs',
            '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', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup',
            'put_table', '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',
@@ -1072,6 +1074,41 @@ def put_scrollable(content=[], height=400, keep_bottom=False, horizon_scroll=Fal
                       scope=scope, position=position).enable_context_manager()
                       scope=scope, position=position).enable_context_manager()
 
 
 
 
+@safely_destruct_output_when_exp('tabs')
+def put_tabs(tabs, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
+    """Output tabs.
+
+    :param list tabs: Tab list, each item is a dict: ``{"title": , "content":}`` .
+       The ``content`` can be a string, the ``put_xxx()`` calls , or a list of them.
+    :param int scope, position: Those arguments have the same meaning as for `put_text()`
+
+    .. exportable-codeblock::
+        :name: put_tabs
+        :summary: `put_tabs()` usage
+
+        put_tabs([
+            {'title': 'Text', 'content': 'Hello world'},
+            {'title': 'Markdown', 'content': put_markdown('~~Strikethrough~~')},
+            {'title': 'More content', 'content': [
+                put_table([
+                    ['Commodity', 'Price'],
+                    ['Apple', '5.5'],
+                    ['Banana', '7'],
+                ]),
+                put_link('pywebio', 'https://github.com/wang0618/PyWebIO')
+            ]},
+        ])
+
+    .. versionadded:: 1.3
+    """
+
+    for tab in tabs:
+        assert 'title' in tab and 'content' in tab
+
+    spec = _get_output_spec('tabs', tabs=tabs, scope=scope, position=position)
+    return Output(spec)
+
+
 @safely_destruct_output_when_exp('data')
 @safely_destruct_output_when_exp('data')
 def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
 def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """Output your own widget
     """Output your own widget
@@ -1088,7 +1125,7 @@ def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTT
 
 
     .. exportable-codeblock::
     .. exportable-codeblock::
         :name: put_widget
         :name: put_widget
-        :summary: Use `put_widget()`to output your own widget
+        :summary: Use `put_widget()` to output your own widget
 
 
         tpl = '''
         tpl = '''
         <details {{#open}}open{{/open}}>
         <details {{#open}}open{{/open}}>

+ 13 - 0
test/template.py

@@ -296,6 +296,19 @@ def basic_output():
     hobby.append(put_text('Music'), put_text('Drama'))
     hobby.append(put_text('Music'), put_text('Drama'))
     hobby.insert(0, put_markdown('**Coding**'))
     hobby.insert(0, put_markdown('**Coding**'))
 
 
+    put_tabs([
+        {'title': 'Text', 'content': 'Hello world'},
+        {'title': 'Markdown', 'content': put_markdown('~~Strikethrough~~')},
+        {'title': 'More content', 'content': [
+            put_table([
+                ['Commodity', 'Price'],
+                ['Apple', '5.5'],
+                ['Banana', '7'],
+            ]),
+            put_link('pywebio', 'https://github.com/wang0618/PyWebIO')
+        ]},
+    ])
+
 
 
 def background_output():
 def background_output():
     put_text("Background output")
     put_text("Background output")

+ 72 - 13
webiojs/src/models/output.ts

@@ -1,4 +1,4 @@
-import {b64toBlob} from "../utils";
+import {b64toBlob, randomid} from "../utils";
 import * as marked from 'marked';
 import * as marked from 'marked';
 
 
 /*
 /*
@@ -175,21 +175,38 @@ let Table = {
     }
     }
 };
 };
 
 
+const TABS_TPL = `<div class="webio-tabs">
+{{#tabs}}
+    <input type="radio" class="toggle" name="{{#uniqueid}}name{{/uniqueid}}" id="{{#uniqueid}}name{{/uniqueid}}{{index}}" {{#checked}}checked{{/checked}}>
+    <label for="{{#uniqueid}}name{{/uniqueid}}{{index}}">{{title}}</label>
+    <div class="webio-tabs-content">
+    {{#content}}
+        {{& pywebio_output_parse}}
+    {{/content}}
+    </div>
+{{/tabs}}
+</div>`;
+
+let TabsWidget = {
+    handle_type: 'tabs',
+    get_element: function (spec: { tabs: { title: string, content: any, index: number, checked: boolean }[] }) {
+        spec.tabs[0]['checked'] = true;
+        for (let idx = 0; idx < spec.tabs.length; idx++) {
+            spec.tabs[idx]['index'] = idx;
+        }
+
+        return render_tpl(TABS_TPL, spec);
+    }
+};
+
 let CustomWidget = {
 let CustomWidget = {
     handle_type: 'custom_widget',
     handle_type: 'custom_widget',
     get_element: function (spec: { template: string, data: { [i: string]: any } }) {
     get_element: function (spec: { template: string, data: { [i: string]: any } }) {
-        spec.data['pywebio_output_parse'] = function () {
-            if (this.type)
-                return outputSpecToHtml(this);
-            else
-                return outputSpecToHtml({type: 'text', content: this, inline: true});
-        };
-        let html = Mustache.render(spec.template, spec.data);
-        return parseHtml(html);
+        return render_tpl(spec.template, spec.data);
     }
     }
 };
 };
 
 
-let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget];
+let all_widgets: Widget[] = [Text, Markdown, Html, Buttons, File, Table, CustomWidget, TabsWidget];
 
 
 
 
 let type2widget: { [i: string]: Widget } = {};
 let type2widget: { [i: string]: Widget } = {};
@@ -206,9 +223,9 @@ export function getWidgetElement(spec: any) {
         let old_style = elem.attr('style') || '';
         let old_style = elem.attr('style') || '';
         elem.attr({"style": old_style + spec.style});
         elem.attr({"style": old_style + spec.style});
     }
     }
-    if(spec.container_dom_id){
-        let dom_id = 'pywebio-scope-'+spec.container_dom_id;
-        if(spec.container_selector)
+    if (spec.container_dom_id) {
+        let dom_id = 'pywebio-scope-' + spec.container_dom_id;
+        if (spec.container_selector)
             elem.find(spec.container_selector).attr('id', dom_id);
             elem.find(spec.container_selector).attr('id', dom_id);
         else
         else
             elem.attr('id', dom_id);
             elem.attr('id', dom_id);
@@ -230,3 +247,45 @@ export function outputSpecToHtml(spec: any) {
 }
 }
 
 
 
 
+function render_tpl(tpl: string, data: { [i: string]: any }) {
+    data['pywebio_output_parse'] = function () {
+        if (this.type)
+            return outputSpecToHtml(this);
+        else
+            return outputSpecToHtml({type: 'text', content: this, inline: true});
+    };
+
+    // {{#uniqueid}}name{{/uniqueid}}
+    // {{uniqueid}}
+    data['uniqueid'] = function () {
+        let names2id: { [name: string]: any } = {};
+        return function (name: string) {
+            if (name) {
+                if (!(name in names2id))
+                    names2id[name] = 'webio-' + randomid(10);
+
+                return names2id[name];
+            } else {
+                return 'webio-' + randomid(10);
+            }
+        };
+    }
+    // count the function call number
+    let cnt = 0;
+    data['index'] = function () {
+        cnt += 1;
+        return cnt;
+    };
+    let html = Mustache.render(tpl, data);
+    return parseHtml(html);
+}
+
+function gen_widget_from_tpl(name: string, tpl: string) {
+    Mustache.parse(tpl);
+    return {
+        handle_type: name,
+        get_element: function (data: { [i: string]: any }) {
+            return render_tpl(tpl, data);
+        }
+    };
+}