input.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import {Command, Session} from "../session";
  2. import {error_alert, LRUMap, make_set, serialize_json} 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; // task_id -> stack of FormGroupController
  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. if (state.AutoFocusOnInput)
  35. $('[auto_focus="true"]').focus();
  36. };
  37. // hide old_ctrls显示的表单,激活 task_id 对应的表单
  38. // 需要保证 task_id 对应有表单
  39. private _activate_form(task_id: string, old_ctrl: InputItem) {
  40. let ctrls = this.form_ctrls.get_value(task_id);
  41. let ctrl = ctrls[ctrls.length - 1];
  42. if (ctrl === old_ctrl || old_ctrl === undefined) {
  43. return ctrl.element.show(state.ShowDuration, () => {
  44. this._after_show_form()
  45. });
  46. }
  47. this.form_ctrls.move_to_top(task_id);
  48. old_ctrl.element.hide(100, () => {
  49. // ctrl.element.show(100);
  50. // 需要在回调中重新获取当前前置表单元素,因为100ms内可能有变化
  51. let t = this.form_ctrls.get_top();
  52. if (t) t[t.length - 1].element.show(state.ShowDuration, () => {
  53. this._after_show_form()
  54. });
  55. });
  56. };
  57. /*
  58. * 每次函数调用返回后,this.form_ctrls.get_top()的栈顶对应的表单为当前活跃表单
  59. * */
  60. handle_message(msg: Command) {
  61. let old_ctrls = this.form_ctrls.get_top();
  62. let old_ctrl = old_ctrls && old_ctrls[old_ctrls.length - 1];
  63. let target_ctrls = this.form_ctrls.get_value(msg.task_id);
  64. if (target_ctrls === undefined) {
  65. this.form_ctrls.push(msg.task_id, []);
  66. target_ctrls = this.form_ctrls.get_value(msg.task_id);
  67. }
  68. // 创建表单
  69. if (msg.command in make_set(['input', 'input_group'])) {
  70. let ctrl = new FormController(this.session, msg.task_id, msg.spec);
  71. target_ctrls.push(ctrl);
  72. this.container_elem.append(ctrl.create_element());
  73. ctrl.after_add_to_dom();
  74. this._activate_form(msg.task_id, old_ctrl);
  75. } else if (msg.command in make_set(['update_input'])) {
  76. // 更新表单
  77. if (target_ctrls.length === 0) {
  78. return console.error('No form to current message. task_id:%s', msg.task_id);
  79. }
  80. target_ctrls[target_ctrls.length - 1].dispatch_ctrl_message(msg.spec);
  81. // 表单前置 removed
  82. // this._activate_form(msg.task_id, old_ctrl);
  83. } else if (msg.command === 'destroy_form') {
  84. if (target_ctrls.length === 0) {
  85. return console.error('No form to current message. task_id:%s', msg.task_id);
  86. }
  87. let deleted = target_ctrls.pop() as FormController;
  88. if (target_ctrls.length === 0)
  89. this.form_ctrls.remove(msg.task_id);
  90. // 销毁的是当前显示的form
  91. if (old_ctrls === target_ctrls) {
  92. deleted.element.hide(100, () => {
  93. deleted.element.remove();
  94. close_input();
  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. close_input();
  103. }
  104. }
  105. }
  106. }
  107. /*
  108. * 表单控制器
  109. * */
  110. class FormController {
  111. static input_items: { [input_type: string]: typeof InputItem } = {};
  112. session: Session;
  113. element: JQuery = null;
  114. private task_id: string;
  115. private spec: any;
  116. // name -> input_controller
  117. private name2input: { [i: string]: InputItem } = {};
  118. private show_count: number = 0;
  119. public static register_inputitem(cls: typeof InputItem) {
  120. for (let type of cls.accept_input_types) {
  121. if (type in this.input_items)
  122. throw new Error(`duplicated accept_input_types:[${type}] in ${cls} and ${this.input_items[type]}`);
  123. this.input_items[type] = cls;
  124. }
  125. }
  126. constructor(session: Session, task_id: string, spec: any) {
  127. this.session = session;
  128. this.task_id = task_id;
  129. this.spec = spec;
  130. // this.create_element();
  131. }
  132. create_element(): JQuery {
  133. let tpl = `
  134. <div class="card" style="display: none">
  135. <h5 class="card-header">{{label}}</h5>
  136. <div class="card-body">
  137. <form>
  138. <div class="input-container"></div>
  139. <div class="ws-form-submit-btns">
  140. <button type="submit" class="btn btn-primary">${t("submit")}</button>
  141. <button type="reset" class="btn btn-warning">${t("reset")}</button>
  142. {{#cancelable}}<button type="button" class="pywebio_cancel_btn btn btn-danger">${t("cancel")}</button>{{/cancelable}}
  143. </div>
  144. </form>
  145. </div>
  146. </div>`;
  147. let that = this;
  148. const html = Mustache.render(tpl, {label: this.spec.label, cancelable: this.spec.cancelable});
  149. let element = $(html);
  150. element.find('.pywebio_cancel_btn').on('click', function (e) {
  151. element.find('button').prop("disabled", true);
  152. that.session.send_message({
  153. event: "from_cancel",
  154. task_id: that.task_id,
  155. data: null
  156. });
  157. });
  158. // 隐藏默认的"提交"/"重置"按钮
  159. if (this.spec.inputs.length && this.spec.inputs[this.spec.inputs.length - 1].type === 'actions') {
  160. for (let btn of this.spec.inputs[this.spec.inputs.length - 1].buttons) {
  161. if (btn.type === 'submit') {
  162. element.find('.ws-form-submit-btns').hide();
  163. break;
  164. }
  165. }
  166. }
  167. // 输入控件创建
  168. let body = element.find('.input-container');
  169. for (let idx in this.spec.inputs) {
  170. let input_spec = this.spec.inputs[idx];
  171. if (!(input_spec.type in FormController.input_items))
  172. throw new Error(`Unknown input type '${input_spec.type}'`);
  173. let item_class = FormController.input_items[input_spec.type];
  174. let item = new item_class(input_spec, this.task_id, (event, input_item) => {
  175. this.session.send_message({
  176. event: "input_event",
  177. task_id: this.task_id,
  178. data: {
  179. event_name: event,
  180. name: input_spec.name,
  181. value: input_item.get_value()
  182. }
  183. });
  184. });
  185. this.name2input[input_spec.name] = item;
  186. body.append(item.create_element());
  187. }
  188. // submit event
  189. element.on('submit', 'form', function (e) {
  190. e.preventDefault(); // avoid to execute the actual submit of the form.
  191. element.find('button').prop("disabled", true);
  192. for (let name in that.name2input){
  193. if (!that.name2input[name].check_valid()){
  194. element.find('button').prop("disabled", false);
  195. return error_alert(t("error_in_input"));
  196. }
  197. }
  198. let data_keys: string[] = [];
  199. let data_values: any[] = [];
  200. $.each(that.name2input, (name, ctrl) => {
  201. data_keys.push(name as string);
  202. data_values.push(ctrl.get_value());
  203. });
  204. let on_process = (loaded: number, total: number) => {
  205. };
  206. // show process bar when there is a file input field
  207. for (let item of that.spec.inputs) {
  208. if (item.type == 'file') {
  209. on_process = that.make_progress();
  210. break;
  211. }
  212. }
  213. Promise.all(data_values).then((values) => {
  214. let input_data: { [i: string]: any } = {};
  215. let files: Blob[] = [];
  216. for (let idx in data_keys) {
  217. input_data[data_keys[idx]] = values[idx];
  218. if (that.spec.inputs[idx].type == 'file') {
  219. input_data[data_keys[idx]] = [];
  220. files.push(...values[idx]);
  221. }
  222. }
  223. let msg = {
  224. event: "from_submit",
  225. task_id: that.task_id,
  226. data: input_data
  227. };
  228. if (files.length) {
  229. that.session.send_buffer(new Blob([serialize_json(msg), ...files], {type: 'application/octet-stream'}), on_process);
  230. } else {
  231. that.session.send_message(msg, on_process);
  232. }
  233. });
  234. });
  235. this.element = element;
  236. return element;
  237. };
  238. // 显示提交进度条,返回进度更新函数
  239. make_progress() {
  240. let html = `<div class="progress" style="margin-top: 4px;">
  241. <div class="progress-bar bg-info progress-bar-striped progress-bar-animated" role="progressbar"
  242. style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%
  243. </div>
  244. </div>`;
  245. let elem = $(html);
  246. this.element.find('.card-body').append(elem);
  247. let bar = elem.find('.progress-bar');
  248. return function (loaded: number, total: number) {
  249. let progress = "" + (100.0 * loaded / total).toFixed(1);
  250. bar[0].style.width = progress + "%";
  251. bar.attr("aria-valuenow", progress);
  252. bar.text(progress + "%");
  253. }
  254. };
  255. dispatch_ctrl_message(spec: any) {
  256. // 恢复原本可点击的按钮
  257. this.element.find('button:not([data-pywebio-disabled])').prop("disabled", false);
  258. // 移除上传进度条
  259. this.element.find('.progress').remove();
  260. if (!(spec.target_name in this.name2input)) {
  261. return console.error('Can\'t find input[name=%s] element in curr form!', spec.target_name);
  262. }
  263. this.name2input[spec.target_name].update_input(spec);
  264. };
  265. // 在表单加入DOM树后,触发输入项的on_add_to_dom回调
  266. after_add_to_dom() {
  267. for (let name in this.name2input) {
  268. this.name2input[name].after_add_to_dom();
  269. }
  270. }
  271. // 在表单被显示后,触发输入项的after_show回调
  272. after_show() {
  273. for (let name in this.name2input) {
  274. this.name2input[name].after_show(this.show_count === 0);
  275. }
  276. this.show_count += 1;
  277. }
  278. }
  279. for (let item of all_input_items)
  280. FormController.register_inputitem(item);