input.ts 10 KB

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