form.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global = global || self, global.WSREPL = factory());
  5. }(this, (function () {
  6. 'use strict';
  7. function extend(Child, Parent) {
  8. var F = function () {
  9. };
  10. F.prototype = Parent.prototype;
  11. Child.prototype = new F();
  12. Child.prototype.constructor = Child;
  13. Child.uber = Parent.prototype;
  14. }
  15. function make_set(arr) {
  16. var set = {};
  17. for (var idx in arr)
  18. set[arr[idx]] = '';
  19. return set;
  20. }
  21. function deep_copy(obj) {
  22. return JSON.parse(JSON.stringify(obj));
  23. }
  24. function LRUMap() {
  25. this.keys = [];
  26. this.map = {};
  27. this.push = function (key, value) {
  28. if (key in this.map)
  29. return console.error("LRUMap: key:%s already in map", key);
  30. this.keys.push(key);
  31. this.map[key] = value;
  32. };
  33. this.get_value = function (key) {
  34. return this.map[key];
  35. };
  36. this.get_top = function () {
  37. var top_key = this.keys[this.keys.length - 1];
  38. return this.map[top_key];
  39. };
  40. this.set_value = function (key, value) {
  41. if (!(key in this.map))
  42. return console.error("LRUMap: key:%s not in map when call `set_value`", key);
  43. this.map[key] = value;
  44. };
  45. this.move_to_top = function (key) {
  46. const index = this.keys.indexOf(key);
  47. if (index > -1) {
  48. this.keys.splice(index, 1);
  49. this.keys.push(key);
  50. } else {
  51. return console.error("LRUMap: key:%s not in map when call `move_to_top`", key);
  52. }
  53. };
  54. this.remove = function (key) {
  55. if (key in this.map) {
  56. delete this.map[key];
  57. this.keys.splice(this.keys.indexOf(key), 1);
  58. } else {
  59. return console.error("LRUMap: key:%s not in map when call `remove`", key);
  60. }
  61. };
  62. }
  63. function OutputController(ws_client, container_elem) {
  64. this.ws_client = ws_client;
  65. this.container_elem = container_elem;
  66. this.md_parser = new Mditor.Parser();
  67. this.handle_message = function (msg) {
  68. this.container_elem[0].innerHTML += this.md_parser.parse(msg.spec.content);
  69. }
  70. }
  71. OutputController.prototype.accept_command = ['output'];
  72. FormsController.prototype.accept_command = ['input', 'input_group', 'update_input', 'destroy_form'];
  73. function FormsController(ws_client, container_elem) {
  74. this.ws_client = ws_client;
  75. this.container_elem = container_elem;
  76. this.form_ctrls = new LRUMap(); // coro_id -> stack of FormGroupController
  77. // hide old_ctrls显示的表单,激活coro_id对应的表单
  78. // 需要保证 coro_id 对应有表单
  79. this._activate_form = function (coro_id, old_ctrl) {
  80. var ctrls = this.form_ctrls.get_value(coro_id);
  81. var ctrl = ctrls[ctrls.length - 1];
  82. if (ctrl === old_ctrl || old_ctrl === undefined)
  83. return ctrl.element.show(100);
  84. this.form_ctrls.move_to_top(coro_id);
  85. old_ctrl.element.hide(100, () => {
  86. ctrl.element.show(100);
  87. });
  88. };
  89. /*
  90. * 每次函数调用返回后,this.form_ctrls.get_top()的栈顶对应的表单为当前活跃表单
  91. * */
  92. this.handle_message = function (msg) {
  93. var old_ctrls = this.form_ctrls.get_top();
  94. var old_ctrl = old_ctrls && old_ctrls[old_ctrls.length - 1];
  95. var target_ctrls = this.form_ctrls.get_value(msg.coro_id);
  96. if (target_ctrls === undefined) {
  97. this.form_ctrls.push(msg.coro_id, []);
  98. target_ctrls = this.form_ctrls.get_value(msg.coro_id);
  99. }
  100. // 创建表单
  101. if (msg.command in make_set(['input', 'input_group'])) {
  102. var ctrl = new FormController(this.ws_client, msg.coro_id, msg.spec);
  103. target_ctrls.push(ctrl);
  104. this.container_elem.append(ctrl.element);
  105. this._activate_form(msg.coro_id, old_ctrl);
  106. } else if (msg.command in make_set(['update_input'])) {
  107. // 更新表单
  108. if (target_ctrls.length === 0) {
  109. return console.error('No form to current message. coro_id:%s', msg.coro_id);
  110. }
  111. target_ctrls[target_ctrls.length - 1].dispatch_ctrl_message(msg.spec);
  112. // 表单前置
  113. this._activate_form(msg.coro_id, old_ctrl);
  114. } else if (msg.command === 'destroy_form') {
  115. if (target_ctrls.length === 0) {
  116. return console.error('No form to current message. coro_id:%s', msg.coro_id);
  117. }
  118. var deleted = target_ctrls.pop();
  119. if (target_ctrls.length === 0)
  120. this.form_ctrls.remove(msg.coro_id);
  121. // 销毁的是当前显示的form
  122. if (old_ctrls === target_ctrls) {
  123. var that = this;
  124. deleted.element.hide(100, () => {
  125. var t = that.form_ctrls.get_top();
  126. if (t) t[t.length - 1].element.show(100);
  127. });
  128. }
  129. }
  130. // todo: 如果当前栈顶key is not coro_id, hide show, move to top
  131. }
  132. }
  133. function FormStack() {
  134. push();
  135. pop();
  136. empty();
  137. show();// 显示栈顶元素
  138. hide();// 隐藏栈顶元素
  139. }
  140. function FormController(ws_client, coro_id, spec) {
  141. this.ws_client = ws_client;
  142. this.coro_id = coro_id;
  143. this.spec = spec;
  144. this.element = undefined;
  145. this.input_controllers = {}; // name -> input_controller
  146. this.create_element();
  147. }
  148. FormController.prototype.create_element = function () {
  149. var tpl = `
  150. <div class="card" style="display: none">
  151. <h5 class="card-header">{{label}}</h5>
  152. <div class="card-body">
  153. <form>
  154. <div class="input-container"></div>
  155. <button type="submit" class="btn btn-primary">提交</button>
  156. <button type="reset" class="btn btn-warning">重置</button>
  157. </form>
  158. </div>
  159. </div>`;
  160. const html = Mustache.render(tpl, {label: this.spec.label});
  161. this.element = $(html);
  162. // 输入控件创建
  163. var body = this.element.find('.input-container');
  164. for (var idx in this.spec.inputs) {
  165. var i = this.spec.inputs[idx];
  166. var ctrl;
  167. if (i.type in make_set(CommonInputController.prototype.accept_input_types)) {
  168. ctrl = new CommonInputController(this.ws_client, this.coro_id, i);
  169. } else if (i.type in make_set(CheckboxRadioController.prototype.accept_input_types)) {
  170. ctrl = new CheckboxRadioController(this.ws_client, this.coro_id, i);
  171. }
  172. this.input_controllers[i.name] = ctrl;
  173. body.append(ctrl.element);
  174. }
  175. // 事件绑定
  176. var that = this;
  177. this.element.on('submit', 'form', function (e) {
  178. e.preventDefault(); // avoid to execute the actual submit of the form.
  179. var inputs = $(this).serializeArray();
  180. var data = {};
  181. $.each(inputs, (idx, item) => {
  182. if (data[item.name] === undefined) data[item.name] = [];
  183. data[item.name].push(item.value);
  184. });
  185. ws.send(JSON.stringify({
  186. event: "from_submit",
  187. coro_id: that.coro_id,
  188. data: data
  189. }));
  190. })
  191. };
  192. FormController.prototype.dispatch_ctrl_message = function (spec) {
  193. if (!(spec.target_name in this.input_controllers)) {
  194. return console.error('Can\'t find input[name=%s] element in curr form!' , spec.target_name);
  195. }
  196. this.input_controllers[spec.target_name].update_input(spec);
  197. };
  198. function FormItemController(ws_client, coro_id, spec) {
  199. this.ws_client = ws_client;
  200. this.coro_id = coro_id;
  201. this.spec = spec;
  202. this.element = undefined;
  203. var that = this;
  204. this.send_value_listener = function (e) {
  205. var this_elem = $(this);
  206. that.ws_client.send(JSON.stringify({
  207. event: "input_event",
  208. coro_id: that.coro_id,
  209. data: {
  210. event_name: e.type.toLowerCase(),
  211. name: this_elem.attr('name'),
  212. value: this_elem.val()
  213. }
  214. }));
  215. };
  216. /*
  217. * input_idx: 更新作用对象input标签的索引, -1 为不指定对象
  218. * attributes:更新值字典
  219. * */
  220. this.update_input_helper = function (input_idx, attributes) {
  221. var attr2selector = {
  222. 'invalid_feedback': 'div.invalid-feedback',
  223. 'valid_feedback': 'div.valid-feedback',
  224. 'help_text': 'small.text-muted'
  225. };
  226. for (var attribute in attr2selector) {
  227. if (attribute in attributes) {
  228. if (input_idx === -1)
  229. this.element.find(attr2selector[attribute]).text(attributes[attribute]);
  230. else
  231. this.element.find(attr2selector[attribute]).eq(input_idx).text(attributes[attribute]);
  232. delete attributes[attribute];
  233. }
  234. }
  235. var input_elem = this.element.find('input');
  236. if (input_idx >= 0)
  237. input_elem = input_elem.eq(input_idx);
  238. if ('valid_status' in attributes) {
  239. var class_name = attributes.valid_status ? 'is-valid' : 'is-invalid';
  240. input_elem.removeClass('is-valid is-invalid').addClass(class_name);
  241. delete attributes.valid_status;
  242. }
  243. input_elem.attr(attributes);
  244. }
  245. }
  246. function CommonInputController(ws_client, coro_id, spec) {
  247. FormItemController.apply(this, arguments);
  248. this.create_element();
  249. }
  250. CommonInputController.prototype.accept_input_types = ["text", "password", "number", "color", "date", "range", "time"];
  251. /*
  252. *
  253. * type=
  254. * */
  255. const common_input_tpl = `
  256. <div class="form-group">
  257. <label for="{{id_name}}">{{label}}</label>
  258. <input type="{{type}}" id="{{id_name}}" aria-describedby="{{id_name}}_help" class="form-control">
  259. <div class="invalid-feedback">{{invalid_feedback}}</div> <!-- input 添加 is-invalid 类 -->
  260. <div class="valid-feedback">{{valid_feedback}}</div> <!-- input 添加 is-valid 类 -->
  261. <small id="{{id_name}}_help" class="form-text text-muted">{{help_text}}</small>
  262. </div>`;
  263. CommonInputController.prototype.create_element = function () {
  264. var spec = deep_copy(this.spec);
  265. const id_name = spec.name + '-' + Math.floor(Math.random() * Math.floor(9999));
  266. spec['id_name'] = id_name;
  267. const html = Mustache.render(common_input_tpl, spec);
  268. this.element = $(html);
  269. var input_elem = this.element.find('#' + id_name);
  270. // blur事件时,发送当前值到服务器
  271. input_elem.on('blur', this.send_value_listener);
  272. // 将额外的html参数加到input标签上
  273. const ignore_keys = {'type': '', 'label': '', 'invalid_feedback': '', 'valid_feedback': '', 'help_text': ''};
  274. for (var key in this.spec) {
  275. if (key in ignore_keys) continue;
  276. input_elem.attr(key, this.spec[key]);
  277. }
  278. };
  279. CommonInputController.prototype.update_input = function (spec) {
  280. var attributes = spec.attributes;
  281. this.update_input_helper(-1, attributes);
  282. };
  283. function CheckboxRadioController(ws_client, coro_id, spec) {
  284. FormItemController.apply(this, arguments);
  285. this.create_element();
  286. }
  287. CheckboxRadioController.prototype.accept_input_types = ["checkbox", "radio"];
  288. const checkbox_radio_tpl = `
  289. <div class="form-group">
  290. <label>{{label}}</label> {{#inline}}<br>{{/inline}}
  291. {{#options}}
  292. <div class="form-check {{#inline}}form-check-inline{{/inline}}">
  293. <input type="{{type}}" id="{{id_name_prefix}}-{{idx}}" class="form-check-input" name="{{name}}" value="{{value}}">
  294. <label class="form-check-label" for="{{id_name_prefix}}-{{idx}}">
  295. {{label}}
  296. </label>
  297. <div class="invalid-feedback">{{invalid_feedback}}</div> <!-- input 添加 is-invalid 类 -->
  298. <div class="valid-feedback">{{valid_feedback}}</div> <!-- input 添加 is-valid 类 -->
  299. </div>
  300. {{/options}}
  301. <small id="{{id_name}}_help" class="form-text text-muted">{{help_text}}</small>
  302. </div>`;
  303. CheckboxRadioController.prototype.create_element = function () {
  304. var spec = deep_copy(this.spec);
  305. const id_name_prefix = spec.name + '-' + Math.floor(Math.random() * Math.floor(9999));
  306. spec['id_name_prefix'] = id_name_prefix;
  307. for (var idx in spec.options) {
  308. spec.options[idx]['idx'] = idx;
  309. }
  310. const html = Mustache.render(checkbox_radio_tpl, spec);
  311. var elem = $(html);
  312. this.element = elem;
  313. const ignore_keys = {'value': '', 'label': ''};
  314. for (idx = 0; idx < this.spec.options.length; idx++) {
  315. var input_elem = elem.find('#' + id_name_prefix + '-' + idx);
  316. // blur事件时,发送当前值到服务器
  317. input_elem.on('blur', this.send_value_listener);
  318. // 将额外的html参数加到input标签上
  319. for (var key in this.spec.options[idx]) {
  320. if (key in ignore_keys) continue;
  321. input_elem.attr(key, this.spec[key]);
  322. }
  323. }
  324. };
  325. CheckboxRadioController.prototype.update_input = function (spec) {
  326. var attributes = spec.attributes;
  327. var idx = -1;
  328. if ('target_value' in spec) {
  329. this.element.find('input').each(function (index) {
  330. if ($(this).val() == spec.target_value) {
  331. idx = index;
  332. return false;
  333. }
  334. });
  335. }
  336. this.update_input_helper(idx, attributes);
  337. };
  338. function WSREPLController(ws_client, output_container_elem, input_container_elem) {
  339. this.output_ctrl = new OutputController(ws_client, output_container_elem);
  340. this.input_ctrl = new FormsController(ws_client, input_container_elem);
  341. this.output_cmds = make_set(this.output_ctrl.accept_command);
  342. this.input_cmds = make_set(this.input_ctrl.accept_command);
  343. this.handle_message = function (msg) {
  344. if (msg.command in this.input_cmds)
  345. this.input_ctrl.handle_message(msg);
  346. else if (msg.command in this.output_cmds)
  347. this.output_ctrl.handle_message(msg);
  348. else
  349. console.error('Unknown command:%s', msg.command);
  350. };
  351. }
  352. return {
  353. 'WSREPLController': WSREPLController
  354. }
  355. })));