interact.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import json
  2. import logging
  3. from collections.abc import Mapping
  4. from base64 import b64decode
  5. from .framework import Global
  6. from .input_ctrl import send_msg, single_input, input_control
  7. logger = logging.getLogger(__name__)
  8. def run_async(coro):
  9. Global.active_ws.inactive_coro_instances.append(coro)
  10. TEXT = 'text'
  11. NUMBER = "number"
  12. PASSWORD = "password"
  13. CHECKBOX = 'checkbox'
  14. RADIO = 'radio'
  15. SELECT = 'select'
  16. TEXTAREA = 'textarea'
  17. def _parse_args(kwargs):
  18. """处理传给各类input函数的原始参数,
  19. :return:(spec参数,valid_func)
  20. """
  21. # 对为None的参数忽律处理
  22. kwargs = {k: v for k, v in kwargs.items() if v is not None}
  23. kwargs.update(kwargs.get('other_html_attrs', {}))
  24. kwargs.pop('other_html_attrs', None)
  25. valid_func = kwargs.pop('valid_func', lambda _: None)
  26. return kwargs, valid_func
  27. def input(label, type=TEXT, *, valid_func=None, name='data', value=None, placeholder=None, required=None,
  28. readonly=None, disabled=None, help_text=None, **other_html_attrs):
  29. """可以通过datalist提供候选输入"""
  30. item_spec, valid_func = _parse_args(locals())
  31. # 参数检查
  32. allowed_type = {TEXT, NUMBER, PASSWORD, TEXTAREA}
  33. assert type in allowed_type, 'Input type not allowed.'
  34. def preprocess_func(d):
  35. if type == NUMBER:
  36. return int(d)
  37. return d
  38. return single_input(item_spec, valid_func, preprocess_func)
  39. def textarea(label, rows=6, *, code=None, valid_func=None, name='data', value=None, placeholder=None, required=None,
  40. maxlength=None, minlength=None, readonly=None, disabled=None, help_text=None, **other_html_attrs):
  41. """提供codemirror参数产生代码输入样式"""
  42. item_spec, valid_func = _parse_args(locals())
  43. item_spec['type'] = TEXTAREA
  44. return single_input(item_spec, valid_func, lambda d: d)
  45. def _parse_select_options(options):
  46. # option 可用形式:
  47. # {value:, label:, [selected:,] [disabled:]}
  48. # (value, label, [selected,] [disabled])
  49. # value 单值,label等于value
  50. opts_res = []
  51. for opt in options:
  52. if isinstance(opt, Mapping):
  53. assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
  54. elif isinstance(opt, list):
  55. assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
  56. opt = dict(zip(('value', 'label', 'selected', 'disabled'), opt))
  57. else:
  58. opt = dict(value=opt, label=opt)
  59. opts_res.append(opt)
  60. return opts_res
  61. def select(label, options, type=SELECT, *, multiple=None, valid_func=None, name='data', value=None,
  62. placeholder=None, required=None, readonly=None, disabled=None, inline=None, help_text=None,
  63. **other_html_attrs):
  64. """
  65. 参数值为None表示不指定,使用默认值
  66. :param label:
  67. :param options: option 列表
  68. option 可用形式:
  69. {value:, label:, [selected:,] [disabled:]}
  70. (value, label, [selected,] [disabled])
  71. value 单值,label等于value
  72. :param type:
  73. :param multiple:
  74. :param valid_func:
  75. :param name:
  76. :param value:
  77. :param placeholder:
  78. :param required:
  79. :param readonly:
  80. :param disabled:
  81. :param inline:
  82. :param other_html_attrs:
  83. :return:
  84. """
  85. item_spec, valid_func = _parse_args(locals())
  86. item_spec['options'] = _parse_select_options(options)
  87. allowed_type = {CHECKBOX, RADIO, SELECT}
  88. assert type in allowed_type, 'Input type not allowed.'
  89. if inline is not None and type not in {CHECKBOX, RADIO}:
  90. del item_spec['inline']
  91. logger.warning('inline 只能用于 CHECKBOX, RADIO type, now type:%s', type)
  92. if multiple is not None and type != SELECT:
  93. del item_spec['multiple']
  94. logger.warning('multiple 参数只能用于SELECT type, now type:%s', type)
  95. return single_input(item_spec, valid_func, lambda d: d)
  96. def _parse_action_buttons(buttons):
  97. """
  98. :param label:
  99. :param actions: action 列表
  100. action 可用形式:
  101. {value:, label:, [disabled:]}
  102. (value, label, [disabled])
  103. value 单值,label等于value
  104. :return:
  105. """
  106. act_res = []
  107. for act in buttons:
  108. if isinstance(act, Mapping):
  109. assert 'value' in act and 'label' in act, 'actions item must have value and label key'
  110. elif isinstance(act, list):
  111. assert len(act) in (2, 3), 'actions item format error'
  112. act = dict(zip(('value', 'label', 'disabled'), act))
  113. else:
  114. act = dict(value=act, label=act)
  115. act_res.append(act)
  116. return act_res
  117. def actions(label, buttons, name='data', help_text=None):
  118. """
  119. 选择一个动作。UI为多个按钮,点击后会将整个表单提交
  120. :param label:
  121. :param actions: action 列表
  122. action 可用形式:
  123. {value:, label:, [disabled:]}
  124. (value, label, [disabled])
  125. value 单值,label等于value
  126. 实现方式:
  127. 多个type=submit的input组成
  128. [
  129. <button data-name value>label</button>,
  130. ...
  131. ]
  132. """
  133. item_spec, valid_func = _parse_args(locals())
  134. item_spec['type'] = 'actions'
  135. item_spec['buttons'] = _parse_action_buttons(buttons)
  136. return single_input(item_spec, valid_func, lambda d: d)
  137. def file_upload(label, accept=None, name='data', placeholder='Choose file', help_text=None, **other_html_attrs):
  138. """
  139. :param label:
  140. :param accept: 表明服务器端可接受的文件类型;该属性的值必须为一个逗号分割的列表,包含了多个唯一的内容类型声明:
  141. 以 STOP 字符 (U+002E) 开始的文件扩展名。(例如:".jpg,.png,.doc")
  142. 一个有效的 MIME 类型,但没有扩展名
  143. audio/* 表示音频文件
  144. video/* 表示视频文件
  145. image/* 表示图片文件
  146. :param placeholder:
  147. :param help_text:
  148. :param other_html_attrs:
  149. :return:
  150. """
  151. item_spec, valid_func = _parse_args(locals())
  152. item_spec['type'] = 'file'
  153. def read_file(data): # data: {'filename':, 'dataurl'}
  154. header, encoded = data['dataurl'].split(",", 1)
  155. data['content'] = b64decode(encoded)
  156. del data['dataurl']
  157. return data
  158. return single_input(item_spec, valid_func, read_file)
  159. def confirm():
  160. pass
  161. def input_group(label, inputs, valid_func=None):
  162. """
  163. :param label:
  164. :param inputs: list of single_input coro
  165. :param valid_func: callback(data) -> (name, error_msg)
  166. :return:
  167. """
  168. spec_inputs = []
  169. preprocess_funcs = {}
  170. item_valid_funcs = {}
  171. for single_input_cr in inputs:
  172. input_kwargs = dict(single_input_cr.cr_frame.f_locals) # 拷贝一份,不可以对locals进行修改
  173. single_input_cr.close()
  174. input_name = input_kwargs['item_spec']['name']
  175. preprocess_funcs[input_name] = input_kwargs['preprocess_func']
  176. item_valid_funcs[input_name] = input_kwargs['valid_func']
  177. spec_inputs.append(input_kwargs['item_spec'])
  178. # def add_autofocus(spec_inputs):
  179. if all('autofocus' not in i for i in spec_inputs): # 每一个输入项都没有设置autofocus参数
  180. for i in spec_inputs:
  181. text_inputs = {TEXT, NUMBER, PASSWORD, SELECT} # todo update
  182. if i.get('type') in text_inputs:
  183. i['autofocus'] = True
  184. break
  185. spec = dict(label=label, inputs=spec_inputs)
  186. return input_control(spec, preprocess_funcs=preprocess_funcs, item_valid_funcs=item_valid_funcs,
  187. form_valid_funcs=valid_func)