Sfoglia il codice sorgente

前端支持button控件

wangweimin 5 anni fa
parent
commit
f2599aea82
6 ha cambiato i file con 143 aggiunte e 50 eliminazioni
  1. 4 4
      doc/spec.md
  2. 10 2
      test.py
  3. 20 6
      wsrepl/html/bs.html
  4. 88 22
      wsrepl/html/js/form.js
  5. 20 15
      wsrepl/interact.py
  6. 1 1
      wsrepl/ioloop.py

+ 4 - 4
doc/spec.md

@@ -50,10 +50,10 @@ ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/textarea
 <button>
 ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button
 
-type=buttons
+type=actions
     label
     name
-    actions 字典列表 {*value:, *label:, disabled}
+    buttons 字典列表 {*value:, *label:, disabled}
 
 input_group:
     label: # todo change to label
@@ -64,7 +64,7 @@ input_group:
 控制类指令
 update_input:
     target_name: input主键name
-    ~target_value:str 用于checkbox, radio 过滤input 
+    ~target_value:str 用于checkbox, radio, button 过滤input 
     attributes: {
         valid_status: bool 输入值的有效性,通过/不通过
         value:
@@ -92,7 +92,7 @@ output:
     数据项2:
 
 input_event
-    event_name: on_blur
+    event_name: blur, click
     name:
     value:
 

+ 10 - 2
test.py

@@ -12,6 +12,13 @@ from tornado.gen import sleep
 def say_hello():
     # 向用户输出文字
     text_print("Welcome!!!")
+    res = yield from actions('Action button', [
+        {'value': '1', 'label': 'One', 'disabled': False},
+        {'value': '2', 'label': 'Two', 'disabled': False},
+        {'value': '3', 'label': 'Three', 'disabled': True},
+    ])
+    text_print('Your input:%s' % res)
+
     res = yield from select('This is select input', [
         {'value': 1, 'label': 'one', 'selected': False, 'disabled': False},
         {'value': 2, 'label': 'two', 'selected': True, 'disabled': False},
@@ -30,14 +37,15 @@ def say_hello():
         {'value': 1, 'label': 'one', 'selected': False, 'disabled': False},
         {'value': 2, 'label': 'two', 'selected': True, 'disabled': False},
         {'value': 2, 'label': 'three disabled', 'selected': False, 'disabled': True},
-    ], type=RADIO, multiple=True)
+    ], type=RADIO)
     text_print('Your input:%s' % res)
 
     res = yield from select('This is CHECKBOX input', [
         {'value': 1, 'label': 'one', 'selected': False, 'disabled': False},
         {'value': 2, 'label': 'two', 'selected': True, 'disabled': False},
         {'value': 2, 'label': 'three disabled', 'selected': False, 'disabled': True},
-    ], type=CHECKBOX, multiple=True)
+    ], type=CHECKBOX)
+
     text_print('Your input:%s' % res)
 
     res = yield from input('This is single input')

+ 20 - 6
wsrepl/html/bs.html

@@ -44,6 +44,7 @@
             <input type="checkbox" class="form-check-input" id="exampleCheck1">
             <label class="form-check-label" for="exampleCheck1">Check me out</label>
         </div>
+
         <button type="submit" class="btn btn-primary">Submit</button>
     </form>
 
@@ -51,7 +52,17 @@
     <form id="f">
         <div class="form-group">
             <label for="num_input">Input</label>
-            <input type="number" class="form-control"  id="num_input">
+            <input type="number" class="form-control" id="num_input">
+        </div>
+        <div class="form-group">
+            <label for="submit_input">submit_input</label> <br>
+            <input type="submit" class="btn btn-primary" id="submit_input">
+            <input type="submit" class="btn btn-primary" value="哈哈">
+            <button value="s" aria-describedby="emailHelp" class="btn btn-primary">No Submit</button>
+            <button type="submit" name="submit" value="s" aria-describedby="emailHelp" class="btn btn-primary">Submit</button>
+            <div class="invalid-feedback">invalid-feedback.</div>  <!-- input 添加 is-invalid 类 -->
+            <div class="valid-feedback">valid-feedback!</div> <!-- input 添加 is-valid 类 -->
+            <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
         </div>
         <div class="form-group">
             <label for="exampleFormControlSelect1">Select</label>
@@ -128,7 +139,8 @@
                 </label>
             </div>
             <div class="form-check form-check-inline">
-                <input class="form-check-input is-invalid" type="radio" name="exampleRadios1" id="exampleRadios22" value="option2">
+                <input class="form-check-input is-invalid" type="radio" name="exampleRadios1" id="exampleRadios22"
+                       value="option2">
                 <label class="form-check-label" for="exampleRadios22">
                     Option two
                 </label>
@@ -140,14 +152,16 @@
         <div class="form-group">
             <label>Checkout</label>
             <div class="form-check">
-                <input class="form-check-input is-invalid" type="checkbox" value="" id="defaultCheck1" name="defaultCheck1">
+                <input class="form-check-input is-invalid" type="checkbox" value="" id="defaultCheck1"
+                       name="defaultCheck1">
                 <label class="form-check-label" for="defaultCheck1">
                     Option one is this and that—be sure to include why it's great
                 </label>
 
             </div>
             <div class="form-check">
-                <input class="form-check-input is-invalid" type="checkbox" value="" id="defaultCheck2" name="defaultCheck1">
+                <input class="form-check-input is-invalid" type="checkbox" value="" id="defaultCheck2"
+                       name="defaultCheck1">
                 <label class="form-check-label" for="defaultCheck2">
                     Option two is disabled
                 </label>
@@ -159,13 +173,13 @@
         <div class="form-group">
             <label>Checkout inline</label> <br>
             <div class="form-check form-check-inline">
-                <input class="form-check-input" type="checkbox" value="" id="defaultCheck21"  name="defaultCheck2">
+                <input class="form-check-input" type="checkbox" value="" id="defaultCheck21" name="defaultCheck2">
                 <label class="form-check-label" for="defaultCheck21">
                     Option one
                 </label>
             </div>
             <div class="form-check form-check-inline">
-                <input class="form-check-input" type="checkbox" value="" id="defaultCheck22"  name="defaultCheck2">
+                <input class="form-check-input" type="checkbox" value="" id="defaultCheck22" name="defaultCheck2">
                 <label class="form-check-label" for="defaultCheck22">
                     Option two
                 </label>

+ 88 - 22
wsrepl/html/js/form.js

@@ -119,7 +119,7 @@
                 console.log('开:%s', ctrl.spec.label);
                 return ctrl.element.show(200, function () {
                     // 有时候autofocus属性不生效,手动激活一下
-                    $('input[autofocus]').focus();
+                    $('[autofocus]').focus();
                 });
             }
             this.form_ctrls.move_to_top(coro_id);
@@ -130,7 +130,7 @@
                 var t = that.form_ctrls.get_top();
                 if (t) t[t.length - 1].element.show(200, function () {
                     // 有时候autofocus属性不生效,手动激活一下
-                    $('input[autofocus]').focus();
+                    $('[autofocus]').focus();
                 });
             });
         };
@@ -193,7 +193,7 @@
                         deleted.element.remove();
                         var t = that.form_ctrls.get_top();
                         if (t) t[t.length - 1].element.show(200, function () {
-                            $('input[autofocus]').focus();
+                            $('[autofocus]').focus();
                         });
                     });
                 } else {
@@ -220,11 +220,13 @@
         this.spec = spec;
 
         this.element = undefined;
-        this.input_controllers = {};  // name -> input_controller
+        this.name2input_controllers = {};  // name -> input_controller
 
         this.create_element();
     }
 
+    FormController.prototype.input_controllers = [CommonInputController, CheckboxRadioController, ButtonsController];
+
     FormController.prototype.create_element = function () {
         var tpl = `
         <div class="card" style="display: none">
@@ -244,16 +246,22 @@
         // 输入控件创建
         var body = this.element.find('.input-container');
         for (var idx in this.spec.inputs) {
-            var i = this.spec.inputs[idx];
-            var ctrl;
-            if (i.type in make_set(CommonInputController.prototype.accept_input_types)) {
-                ctrl = new CommonInputController(this.ws_client, this.coro_id, i);
-            } else if (i.type in make_set(CheckboxRadioController.prototype.accept_input_types)) {
-                ctrl = new CheckboxRadioController(this.ws_client, this.coro_id, i);
+            var input_spec = this.spec.inputs[idx];
+            var ctrl = undefined;
+            for (var i in this.input_controllers) {
+                var ctrl_cls = this.input_controllers[i];
+                // console.log(ctrl_cls, ctrl_cls.prototype.accept_input_types);
+                if (input_spec.type in make_set(ctrl_cls.prototype.accept_input_types)) {
+                    ctrl = new ctrl_cls(this.ws_client, this.coro_id, input_spec);
+                    break;
+                }
+            }
+            if (ctrl) {
+                this.name2input_controllers[input_spec.name] = ctrl;
+                body.append(ctrl.element);
+            } else {
+                console.error('Unvalid input type:%s', input_spec.type);
             }
-
-            this.input_controllers[i.name] = ctrl;
-            body.append(ctrl.element);
         }
 
         // 事件绑定
@@ -261,7 +269,7 @@
         this.element.on('submit', 'form', function (e) {
             e.preventDefault(); // avoid to execute the actual submit of the form.
             var data = {};
-            $.each(that.input_controllers, (name, ctrl) => {
+            $.each(that.name2input_controllers, (name, ctrl) => {
                 data[name] = ctrl.get_value();
             });
             ws.send(JSON.stringify({
@@ -273,11 +281,11 @@
     };
 
     FormController.prototype.dispatch_ctrl_message = function (spec) {
-        if (!(spec.target_name in this.input_controllers)) {
+        if (!(spec.target_name in this.name2input_controllers)) {
             return console.error('Can\'t find input[name=%s] element in curr form!', spec.target_name);
         }
 
-        this.input_controllers[spec.target_name].update_input(spec);
+        this.name2input_controllers[spec.target_name].update_input(spec);
     };
 
 
@@ -295,8 +303,8 @@
                 coro_id: that.coro_id,
                 data: {
                     event_name: e.type.toLowerCase(),
-                    name: this_elem.attr('name'),
-                    value: this_elem.val()
+                    name: that.spec.name,
+                    value: that.get_value()
                 }
             }));
         };
@@ -384,7 +392,14 @@
         input_elem.on('blur', this.send_value_listener);
 
         // 将额外的html参数加到input标签上
-        const ignore_keys = {'type': '', 'label': '', 'invalid_feedback': '', 'valid_feedback': '', 'help_text': '', 'options':''};
+        const ignore_keys = {
+            'type': '',
+            'label': '',
+            'invalid_feedback': '',
+            'valid_feedback': '',
+            'help_text': '',
+            'options': ''
+        };
         for (var key in this.spec) {
             if (key in ignore_keys) continue;
             input_elem.attr(key, this.spec[key]);
@@ -414,7 +429,7 @@
     <label>{{label}}</label> {{#inline}}<br>{{/inline}}
     {{#options}}
     <div class="form-check {{#inline}}form-check-inline{{/inline}}">
-        <input type="{{type}}" id="{{id_name_prefix}}-{{idx}}" name="{{name}}" value="{{value}}" {{#selected}}checked{{/selected}} class="form-check-input">
+        <input type="{{type}}" id="{{id_name_prefix}}-{{idx}}" name="{{name}}" value="{{value}}" {{#selected}}checked{{/selected}} {{#disabled}}disabled{{/disabled}} class="form-check-input">
         <label class="form-check-label" for="{{id_name_prefix}}-{{idx}}">
             {{label}}
         </label>
@@ -446,7 +461,7 @@
             // 将额外的html参数加到input标签上
             for (var key in this.spec.options[idx]) {
                 if (key in ignore_keys) continue;
-                input_elem.attr(key, this.spec[key]);
+                input_elem.attr(key, this.spec.options[idx][key]);
             }
         }
     };
@@ -456,7 +471,7 @@
         var idx = -1;
         if ('target_value' in spec) {
             this.element.find('input').each(function (index) {
-                if ($(this).val() == spec.target_value) {
+                if ($(this).val() === spec.target_value) {
                     idx = index;
                     return false;
                 }
@@ -480,6 +495,57 @@
         }
     };
 
+    function ButtonsController(ws_client, coro_id, spec) {
+        FormItemController.apply(this, arguments);
+
+        this.last_checked_value = undefined;  // 上次点击按钮的value
+        this.create_element();
+    }
+
+    ButtonsController.prototype.accept_input_types = ["actions"];
+
+    const buttons_tpl = `
+<div class="form-group">
+    <label>{{label}}</label> <br>
+    {{#buttons}}
+    <button type="submit" value="{{value}}" aria-describedby="{{name}}_help" {{#disabled}}disabled{{/disabled}} class="btn btn-primary">{{label}}</button>
+    {{/buttons}}
+    <div class="invalid-feedback">{{invalid_feedback}}</div>  <!-- input 添加 is-invalid 类 -->
+    <div class="valid-feedback">{{valid_feedback}}</div> <!-- input 添加 is-valid 类 -->
+    <small id="{{name}}_help" class="form-text text-muted">{{help_text}}</small>
+</div>`;
+
+    ButtonsController.prototype.create_element = function () {
+        var spec = deep_copy(this.spec);
+        const html = Mustache.render(buttons_tpl, this.spec);
+        this.element = $(html);
+
+        // todo:是否有必要监听click事件,因为点击后即提交了表单
+        var that = this;
+        this.element.find('button').on('click', function (e) {
+            var btn = $(this);
+            that.last_checked_value = btn.val();
+            that.send_value_listener.apply(this, arguments);
+        });
+    };
+
+    ButtonsController.prototype.update_input = function (spec) {
+        var attributes = spec.attributes;
+        var idx = -1;
+        if ('target_value' in spec) {
+            this.element.find('button').each(function (index) {
+                if ($(this).val() === spec.target_value) {
+                    idx = index;
+                    return false;
+                }
+            });
+        }
+        this.update_input_helper(idx, attributes);
+    };
+
+    ButtonsController.prototype.get_value = function () {
+        return this.last_checked_value;
+    };
 
 
     function WSREPLController(ws_client, output_container_elem, input_container_elem) {

+ 20 - 15
wsrepl/interact.py

@@ -45,7 +45,7 @@ def _input_event_handle(valid_funcs, whole_valid_func=None):
         event_name, event_data = event['event'], event['data']
         if event_name == 'input_event':
             input_event = event_data['event_name']
-            if input_event == 'on_blur':
+            if input_event == 'blur':
                 onblur_name = event_data['name']
                 valid_func = valid_funcs.get(onblur_name)
                 if valid_func is None:
@@ -106,12 +106,12 @@ def _make_input_spec(label, type, name, valid_func=None, multiple=None, inline=N
         logger.warning('valid_func can\'t be used when type in (CHECKBOX, RADIO)')
 
     if inline is not None and type not in {CHECKBOX, RADIO}:
-        logger.warning('inline 只能用于 CHECKBOX, RADIO type')
+        logger.warning('inline 只能用于 CHECKBOX, RADIO type, now type:%s', type)
     elif inline is not None:
         input_item['inline'] = inline
 
     if multiple is not None and type != SELECT:
-        logger.warning('multiple 参数只能用于SELECT type')
+        logger.warning('multiple 参数只能用于SELECT type, now type:%s', type)
     elif multiple is not None:
         input_item['multiple'] = multiple
 
@@ -136,12 +136,16 @@ def _make_input_spec(label, type, name, valid_func=None, multiple=None, inline=N
 
         input_item['options'] = opts_res
 
-    # todo spec参数中,为默认值的可以不发送
+    # todo spec参数中,为None的表示使用默认,不发送
+    for attr, val in list(input_item.items()):
+        if val is None:
+            del input_item[attr]
+
     return input_item
 
 
-def input(label, type=TEXT, *, valid_func=None, name='data', value='', placeholder='', required=False, readonly=False,
-          disabled=False, **other_html_attrs):
+def input(label, type=TEXT, *, valid_func=None, name='data', value='', placeholder='', required=None, readonly=None,
+          disabled=None, **other_html_attrs):
     input_kwargs = dict(locals())
     input_kwargs['label'] = ''
     input_kwargs['__name__'] = input.__name__
@@ -156,7 +160,7 @@ def input(label, type=TEXT, *, valid_func=None, name='data', value='', placehold
 
 
 def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name='data', value='', placeholder='',
-           required=False, readonly=False, disabled=False, inline=None, **other_html_attrs):
+           required=None, readonly=None, disabled=None, inline=None, **other_html_attrs):
     """
     参数值为None表示不指定,使用默认值
 
@@ -182,6 +186,7 @@ def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name=
     input_kwargs = dict(locals())
     input_kwargs['label'] = ''
     input_kwargs['__name__'] = select.__name__
+    input_kwargs.setdefault('autofocus', True)  # 如果没有设置autofocus参数,则开启参数
 
     allowed_type = {CHECKBOX, RADIO, SELECT}
     assert type in allowed_type, 'Input type not allowed.'
@@ -190,7 +195,7 @@ def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name=
     return data[name]
 
 
-def _make_actions_input_spec(label, actions, name):
+def _make_actions_input_spec(label, buttons, name):
     """
     :param label:
     :param actions: action 列表
@@ -201,7 +206,7 @@ def _make_actions_input_spec(label, actions, name):
     :return:
     """
     act_res = []
-    for act in actions:
+    for act in buttons:
         if isinstance(act, Mapping):
             assert 'value' in act and 'label' in act, 'actions item must have value and label key'
         elif isinstance(act, Sequence):
@@ -211,11 +216,11 @@ def _make_actions_input_spec(label, actions, name):
             act = dict(value=act, label=act)
         act_res.append(act)
 
-    input_item = dict(type='buttons', label=label, name=name, actions=actions)
+    input_item = dict(type='actions', label=label, name=name, buttons=buttons)
     return input_item
 
 
-def actions(label, actions, name='data'):
+def actions(label, buttons, name='data'):
     """
     选择一个动作。UI为多个按钮,点击后会将整个表单提交
     :param label:
@@ -232,8 +237,8 @@ def actions(label, actions, name='data'):
         ...
     ]
     """
-    input_kwargs = dict(label='', actions=actions, name=name)
-    input_kwargs['__name__'] = select.__name__
+    input_kwargs = dict(label='', buttons=buttons, name=name)
+    input_kwargs['__name__'] = actions.__name__
     data = yield from input_group(label=label, inputs=[input_kwargs])
     return data[name]
 
@@ -248,7 +253,7 @@ def input_group(label, inputs, valid_func=None):
     make_spec_funcs = {
         actions.__name__: _make_actions_input_spec,
         input.__name__: _make_input_spec,
-        select.__name__: _make_input_spec
+        select.__name__: _make_input_spec,
     }
 
     item_valid_funcs = {}
@@ -262,7 +267,7 @@ def input_group(label, inputs, valid_func=None):
             func_name = input_g.__name__
 
         input_name = input_kwargs['name']
-        item_valid_funcs[input_name] = input_kwargs['valid_func']
+        item_valid_funcs[input_name] = input_kwargs.get('valid_func')
         input_item = make_spec_funcs[func_name](**input_kwargs)
         spec_inputs.append(input_item)
 

+ 1 - 1
wsrepl/ioloop.py

@@ -60,7 +60,7 @@ def start_ioloop(coro_func, port=8080):
 
         @coroutine
         def on_message(self, message):
-            print('on_message', message)
+            # print('on_message', message)
             # { event:, coro_id:, data: }
             data = json.loads(message)
             coro_id = data['coro_id']