input.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import {Command, Session} from "../session";
  2. import {body_scroll_to, LRUMap, make_set} from "../utils";
  3. import {InputItem} from "../models/input/base"
  4. import {state} from '../state'
  5. import {all_input_items} from "../models/input"
  6. import {CommandHandler} from "./base"
  7. /*
  8. * 整个输入区域的控制类
  9. * 管理当前活跃和非活跃的表单
  10. * */
  11. export class InputHandler implements CommandHandler {
  12. accept_command: string[] = ['input', 'input_group', 'update_input', 'destroy_form'];
  13. session: Session;
  14. private form_ctrls: LRUMap;
  15. private readonly container_elem: JQuery;
  16. constructor(session: Session, container_elem: JQuery) {
  17. this.session = session;
  18. this.container_elem = container_elem;
  19. this.form_ctrls = new LRUMap(); // task_id -> stack of FormGroupController
  20. }
  21. private _after_show_form() {
  22. // 解决表单显示后动态添加内容,表单宽高不变的问题
  23. setTimeout(() => {
  24. let curr_card = $('#input-container > .card')[0];
  25. curr_card.style.height = "unset";
  26. curr_card.style.width = "unset";
  27. }, 50);
  28. if (!state.AutoScrollBottom)
  29. return;
  30. if (this.container_elem.height() > $(window).height())
  31. body_scroll_to(this.container_elem, 'top', () => {
  32. $('[auto_focus="true"]').focus();
  33. });
  34. else
  35. body_scroll_to(this.container_elem, 'bottom', () => {
  36. $('[auto_focus="true"]').focus();
  37. });
  38. };
  39. // hide old_ctrls显示的表单,激活 task_id 对应的表单
  40. // 需要保证 task_id 对应有表单
  41. private _activate_form(task_id: string, old_ctrl: InputItem) {
  42. let ctrls = this.form_ctrls.get_value(task_id);
  43. let ctrl = ctrls[ctrls.length - 1];
  44. if (ctrl === old_ctrl || old_ctrl === undefined) {
  45. return ctrl.element.show(state.ShowDuration, () => {
  46. this._after_show_form()
  47. });
  48. }
  49. this.form_ctrls.move_to_top(task_id);
  50. old_ctrl.element.hide(100, () => {
  51. // ctrl.element.show(100);
  52. // 需要在回调中重新获取当前前置表单元素,因为100ms内可能有变化
  53. let t = this.form_ctrls.get_top();
  54. if (t) t[t.length - 1].element.show(state.ShowDuration, () => {
  55. this._after_show_form()
  56. });
  57. });
  58. };
  59. /*
  60. * 每次函数调用返回后,this.form_ctrls.get_top()的栈顶对应的表单为当前活跃表单
  61. * */
  62. handle_message(msg: Command) {
  63. let old_ctrls = this.form_ctrls.get_top();
  64. let old_ctrl = old_ctrls && old_ctrls[old_ctrls.length - 1];
  65. let target_ctrls = this.form_ctrls.get_value(msg.task_id);
  66. if (target_ctrls === undefined) {
  67. this.form_ctrls.push(msg.task_id, []);
  68. target_ctrls = this.form_ctrls.get_value(msg.task_id);
  69. }
  70. // 创建表单
  71. if (msg.command in make_set(['input', 'input_group'])) {
  72. let ctrl = new FormController(this.session, msg.task_id, msg.spec);
  73. target_ctrls.push(ctrl);
  74. this.container_elem.append(ctrl.create_element());
  75. this._activate_form(msg.task_id, old_ctrl);
  76. } else if (msg.command in make_set(['update_input'])) {
  77. // 更新表单
  78. if (target_ctrls.length === 0) {
  79. return console.error('No form to current message. task_id:%s', msg.task_id);
  80. }
  81. target_ctrls[target_ctrls.length - 1].dispatch_ctrl_message(msg.spec);
  82. // 表单前置 removed
  83. // this._activate_form(msg.task_id, old_ctrl);
  84. } else if (msg.command === 'destroy_form') {
  85. if (target_ctrls.length === 0) {
  86. return console.error('No form to current message. task_id:%s', msg.task_id);
  87. }
  88. let deleted = target_ctrls.pop() as FormController;
  89. if (target_ctrls.length === 0)
  90. this.form_ctrls.remove(msg.task_id);
  91. // 销毁的是当前显示的form
  92. if (old_ctrls === target_ctrls) {
  93. deleted.element.hide(100, () => {
  94. deleted.element.remove();
  95. let t = this.form_ctrls.get_top();
  96. if (t) t[t.length - 1].element.show(state.ShowDuration, () => {
  97. this._after_show_form()
  98. });
  99. });
  100. } else {
  101. deleted.element.remove();
  102. }
  103. }
  104. }
  105. }
  106. /*
  107. * 表单控制器
  108. * */
  109. class FormController {
  110. static input_items: { [input_type: string]: typeof InputItem } = {};
  111. session: Session;
  112. element: JQuery = null;
  113. private task_id: string;
  114. private spec: any;
  115. // name -> input_controller
  116. private name2input: { [i: string]: InputItem } = {};
  117. public static register_inputitem(cls: typeof InputItem) {
  118. for (let type of cls.accept_input_types) {
  119. if (type in this.input_items)
  120. throw new Error(`duplicated accept_input_types:[${type}] in ${cls} and ${this.input_items[type]}`);
  121. this.input_items[type] = cls;
  122. }
  123. }
  124. constructor(session: Session, task_id: string, spec: any) {
  125. this.session = session;
  126. this.task_id = task_id;
  127. this.spec = spec;
  128. // this.create_element();
  129. }
  130. create_element(): JQuery {
  131. let tpl = `
  132. <div class="card" style="display: none">
  133. <h5 class="card-header">{{label}}</h5>
  134. <div class="card-body">
  135. <form>
  136. <div class="input-container"></div>
  137. <div class="ws-form-submit-btns">
  138. <button type="submit" class="btn btn-primary">提交</button>
  139. <button type="reset" class="btn btn-warning">重置</button>
  140. {{#cancelable}}<button type="button" class="pywebio_cancel_btn btn btn-danger">取消</button>{{/cancelable}}
  141. </div>
  142. </form>
  143. </div>
  144. </div>`;
  145. let that = this;
  146. const html = Mustache.render(tpl, {label: this.spec.label, cancelable: this.spec.cancelable});
  147. let element = $(html);
  148. element.find('.pywebio_cancel_btn').on('click', function (e) {
  149. that.session.send_message({
  150. event: "from_cancel",
  151. task_id: that.task_id,
  152. data: null
  153. });
  154. });
  155. // 如果表单最后一个输入元素为actions组件,则隐藏默认的"提交"/"重置"按钮
  156. if (this.spec.inputs.length && this.spec.inputs[this.spec.inputs.length - 1].type === 'actions')
  157. element.find('.ws-form-submit-btns').hide();
  158. // 输入控件创建
  159. let body = element.find('.input-container');
  160. for (let idx in this.spec.inputs) {
  161. let input_spec = this.spec.inputs[idx];
  162. if (!(input_spec.type in FormController.input_items))
  163. throw new Error(`Unknown input type '${input_spec.type}'`);
  164. let item_class = FormController.input_items[input_spec.type];
  165. let item = new item_class(this.session, this.task_id, input_spec);
  166. this.name2input[input_spec.name] = item;
  167. body.append(item.create_element());
  168. }
  169. // 事件绑定
  170. element.on('submit', 'form', function (e) {
  171. e.preventDefault(); // avoid to execute the actual submit of the form.
  172. let data: { [i: string]: any } = {};
  173. $.each(that.name2input, (name, ctrl) => {
  174. data[name] = ctrl.get_value();
  175. });
  176. let on_process = undefined;
  177. // 在有文件上传的表单中显示进度条
  178. for (let item of that.spec.inputs) {
  179. if (item.type == 'file') {
  180. on_process = that.make_progress();
  181. break;
  182. }
  183. }
  184. that.session.send_message({
  185. event: "from_submit",
  186. task_id: that.task_id,
  187. data: data
  188. }, on_process);
  189. });
  190. this.element = element;
  191. return element;
  192. };
  193. // 显示提交进度条,返回进度更新函数
  194. make_progress() {
  195. let html = `<div class="progress" style="margin-top: 4px;">
  196. <div class="progress-bar bg-info progress-bar-striped progress-bar-animated" role="progressbar"
  197. style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%
  198. </div>
  199. </div>`;
  200. let elem = $(html);
  201. this.element.find('.card-body').append(elem);
  202. let bar = elem.find('.progress-bar');
  203. return function (loaded: number, total: number) {
  204. let progress = "" + (100.0 * loaded / total).toFixed(1);
  205. bar[0].style.width = progress + "%";
  206. bar.attr("aria-valuenow", progress);
  207. bar.text(progress + "%");
  208. }
  209. };
  210. dispatch_ctrl_message(spec: any) {
  211. if (!(spec.target_name in this.name2input)) {
  212. return console.error('Can\'t find input[name=%s] element in curr form!', spec.target_name);
  213. }
  214. this.name2input[spec.target_name].update_input(spec);
  215. };
  216. }
  217. for (let item of all_input_items)
  218. FormController.register_inputitem(item);