v1.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. function updateItemName(item, name)
  2. {
  3. if(name === undefined) name = item.name;
  4. if(item.infoName === name) return;
  5. item.infoName = name;
  6. item.name = name;
  7. const namePath = name.split('.');
  8. item.order = namePath.reduce((total, level, index) => total + (+level * ([1000000, 1000, 1, 0.001, 0.000001][index])), 0);
  9. item.level = namePath.length;
  10. item.selfName = namePath.pop();
  11. item.parentName = namePath.join('.');
  12. }
  13. function updateChildrenName(item)
  14. {
  15. if(!item.children) return;
  16. item.children.forEach((subItem, subIndex) =>
  17. {
  18. updateItemName(subItem, `${item.name}.${subIndex + 1}`);
  19. updateChildrenName(subItem);
  20. });
  21. }
  22. /** Steps editor component. */
  23. class ThinkOptions extends zui.Component
  24. {
  25. static NAME = 'StepsEditor';
  26. static DEFAULT =
  27. {
  28. name: 'fields',
  29. data: ['', '', ''],
  30. sameLevelIcon: 'plus',
  31. moveIcon: 'move',
  32. deleteIcon: 'trash',
  33. deleteStepTip: '',
  34. dragNestedTip: '',
  35. changeLevelByDrag: false,
  36. enterPlaceholder: '',
  37. };
  38. init()
  39. {
  40. this.reset(this.options.data, true);
  41. }
  42. afterInit()
  43. {
  44. this.render();
  45. const $element = this.$element;
  46. $element.on('focus', 'textarea', e =>
  47. {
  48. const $textarea = $(e.target);
  49. $textarea.closest('.think-options-col-step').addClass('focus');
  50. $textarea.closest('.think-options-item').addClass('is-focus');
  51. })
  52. .on('blur', 'textarea', e =>
  53. {
  54. const $textarea = $(e.target);
  55. $textarea.closest('.think-options-col-step').removeClass('focus');
  56. $textarea.closest('.think-options-item').removeClass('is-focus');
  57. })
  58. .on('click', '.btn-action', e =>
  59. {
  60. const $btn = $(e.currentTarget);
  61. if($btn.is('.disabled')) return;
  62. const action = $btn.attr('data-action');
  63. const name = $btn.closest('.think-options-item').attr('data-name');
  64. if(action === 'delete') this.deleteStep(name);
  65. else if(action === 'sib') this.addSib(name);
  66. }).on('mouseenter', '.think-options-step-move', e =>
  67. {
  68. $element.find('.move-hover').removeClass('move-hover');
  69. $(e.currentTarget).closest('.think-options-item').addClass('move-hover');
  70. }).on('mouseleave', '.think-options-step-move', e =>
  71. {
  72. $element.find('.move-hover').removeClass('move-hover');
  73. });
  74. $element.draggable(
  75. {
  76. selector: '.think-options-item',
  77. handle: '.think-options-step-move',
  78. beforeDrag: (_event, dragElement) =>
  79. {
  80. const dragName = $(dragElement).attr('data-name');
  81. this.dragName = dragName;
  82. this.dragItem = this.getByName(dragName);
  83. this.dragMaxLevel = this.dragItem.level;
  84. this.isValidLevel = true;
  85. $element.find(`.think-options-item[data-name^="${dragName}."]`).addClass('is-sub-dragging').each((_index, ele) =>
  86. {
  87. const level = +($(ele).attr('data-level'));
  88. if(level > this.dragMaxLevel) this.dragMaxLevel = level;
  89. });
  90. $element.removeClass('cursor-not-allowed');
  91. },
  92. onDragStart: (event, dragElement) =>
  93. {
  94. event.dataTransfer.setDragImage($(dragElement).find('.think-options-drag-ghost')[0], 0, 0);
  95. },
  96. onDragEnd: () =>
  97. {
  98. $element.find('.is-sub-dragging').removeClass('is-sub-dragging');
  99. const dropName = this.dropName;
  100. if(dropName)
  101. {
  102. this.dropName = null;
  103. const $dropRow = this.getRow(dropName);
  104. if($dropRow && $dropRow.length) $dropRow.removeAttr('data-drop-side').removeAttr('data-drop-level');
  105. }
  106. if(!this.isValidLevel) return;
  107. const dragName = this.dragName;
  108. if(!dropName || dragName === dropName) return;
  109. if(this.dropSide === 'bottom') this.moveAfter(dragName, dropName);
  110. else this.moveBefore(dragName, dropName);
  111. },
  112. onDragOver: (event, dragElement, dropElement) =>
  113. {
  114. const $row = $(dropElement);
  115. const dropName = $row.attr('data-name');
  116. let idDiff = false;
  117. if(dropName !== this.dropName)
  118. {
  119. this.dropName = dropName;
  120. const $oldRow = this.getRow(dropName);
  121. $oldRow.removeAttr('data-drop-side')
  122. .removeAttr('data-drop-level')
  123. .removeAttr('data-invalid-nested');
  124. $oldRow.find('.think-options-step-name').removeClass('is-invalid-drop-level');
  125. this.dropSide = '';
  126. this.dropLevel = '';
  127. this.isValidLevel = undefined;
  128. idDiff = true;
  129. }
  130. const dropItem = this.getByName(dropName);
  131. const dropBounding = dropElement.getBoundingClientRect();
  132. const dropSide = event.clientY > (dropBounding.top + dropBounding.height / 2) ? 'bottom' : 'top';
  133. const dropLevel = Math.max(1, Math.min(3, (!this.options.changeLevelByDrag || event.clientX <= (dropBounding.left + 24)) ? dropItem.level : (Math.round((event.clientX - dropBounding.left - 24 - 8) / 14))));
  134. const isValidLevel = (this.dragMaxLevel + (dropLevel - this.dragItem.level)) <= 3;
  135. if(this.isValidLevel !== isValidLevel)
  136. {
  137. this.isValidLevel = isValidLevel;
  138. $row.toggleClass('is-invalid-drop-level', !isValidLevel);
  139. const $name = $row.find('.think-options-step-name');
  140. if(isValidLevel) $name.removeAttr('data-invalid-nested');
  141. else $name.attr('data-invalid-nested', this.options.dragNestedTip);
  142. }
  143. $element.toggleClass('cursor-not-allowed', !isValidLevel);
  144. if(this.dropSide !== dropSide)
  145. {
  146. this.dropSide = dropSide;
  147. $row.attr('data-drop-side', dropSide);
  148. idDiff = true;
  149. }
  150. if(this.dropLevel !== dropLevel)
  151. {
  152. this.dropLevel = dropLevel;
  153. $row.attr('data-drop-level', dropLevel);
  154. idDiff = true;
  155. }
  156. },
  157. target: (dragElement) =>
  158. {
  159. return $element.find('.think-options-item').not('.is-sub-dragging').not(dragElement);
  160. }
  161. });
  162. }
  163. getByID(id)
  164. {
  165. return this._items.find(x => x.id === id);
  166. }
  167. getByName(name)
  168. {
  169. return this._items.find(x => x.name === name);
  170. }
  171. getParent(item)
  172. {
  173. if(typeof item === 'string') item = this.getByName(item);
  174. return (item && item.parentName) ? this.getByName(item.parentName) : null;
  175. }
  176. getRow(name)
  177. {
  178. return this.$element.find(`.think-options-item[data-name="${name}"]`);
  179. }
  180. reset(data, skipRender)
  181. {
  182. this._items = [];
  183. this.update(data, skipRender);
  184. }
  185. update(data, skipRender)
  186. {
  187. data.forEach(item =>{this.updateItem(item, true);});
  188. if(!skipRender) this.render();
  189. }
  190. updateItem(item, skipRender)
  191. {
  192. item = typeof item === 'string' ? {name: item, id: $.guid++, step: ''} : $.extend({id: $.guid++, step: ''}, item);
  193. updateItemName(item);
  194. const index = this._items.findIndex(x => x.id === item.id);
  195. if(index >= 0)
  196. {
  197. const oldItem = this._items[index];
  198. const parent = this.getParent(oldItem);
  199. if(parent) parent.children.splice(parent.children.indexOf(oldItem), 1);
  200. item = $.extend(oldItem, item);
  201. this._items.splice(index, 1);
  202. updateChildrenName(item);
  203. }
  204. if(item.level > 1)
  205. {
  206. this._items.push(item);
  207. const parent = this._ensureItem(item.parentName);
  208. const siblings = parent.children || [];
  209. const oldIndex = siblings.findIndex(x => x.id === item.id);
  210. if(oldIndex >= 0) siblings.splice(oldIndex, 1);
  211. const index = siblings.findIndex(x => x.order > item.order);
  212. if(index === 0) siblings.unshift(item);
  213. else if(index < 0) siblings.push(item);
  214. else if(index > 0) siblings.splice(index, 0, item);
  215. parent.children = siblings;
  216. }
  217. else
  218. {
  219. const siblings = this._items;
  220. const index = siblings.findIndex(x => x.level === 1 && x.order > item.order);
  221. if(index === 0) siblings.unshift(item);
  222. else if(index < 0) siblings.push(item);
  223. else if(index > 0) siblings.splice(index, 0, item);
  224. }
  225. if(!skipRender) this.render();
  226. return item;
  227. }
  228. _ensureItem(name)
  229. {
  230. return this.getByName(name) || this.updateItem(name);
  231. }
  232. _createRow(item, options)
  233. {
  234. const name = options.data[item.index] || '';
  235. const $row = $
  236. ([
  237. `<div class="think-options-row think-options-item" data-id="${item.id}">`,
  238. '<div class="think-options-drag-ghost"></div>',
  239. '<div class="think-options-col think-options-col-step form-control">',
  240. '<div class="think-options-step-name"></div>',
  241. `<textarea class="think-options-step-text" rows="1" placeholder="${options.enterPlaceholder}">${name}</textarea>`,
  242. '</div>',
  243. '<div class="think-options-col think-options-col-add">',
  244. `<div><button type="button" class="btn ghost rounded size-sm square btn-action" data-action="sib"><i class="icon icon-${options.sameLevelIcon}"></i></button></div>`,
  245. '<div class="think-options-col think-options-col-delete">',
  246. `<div><button type="button" class="btn ghost rounded size-sm square btn-action" data-action="delete"><i class="icon icon-${options.deleteIcon}"></i></button></div>`,
  247. '</div>',
  248. '<div class="think-options-step-move"><i class="icon icon-move"></i></div>',
  249. '</div>',
  250. '</div>'
  251. ].join(''));
  252. $row.find('textarea').autoHeight();
  253. return $row;
  254. }
  255. _renderRow(item, $preRow, $list, $rows)
  256. {
  257. const options = this.options;
  258. let $row = $rows.filter(`[data-id="${item.id}"]`);
  259. if(!$row.length) $row = this._createRow(item, options);
  260. if($preRow) $row.insertAfter($preRow);
  261. else $list.prepend($row);
  262. const hasSub = !!(item.children && item.children.length);
  263. $row.attr('data-level', item.level)
  264. .attr('data-name', item.name)
  265. .attr('data-index', item.index)
  266. .toggleClass('has-children', hasSub)
  267. .toggleClass('no-child', !hasSub)
  268. .removeClass('is-expired');
  269. $row.css('--name-indent', `${(item.level - 1) * 14}px`).find('.think-options-step-name').text(item.name).css('paddingLeft', (item.level - 1) * 14);
  270. $row.find('.think-options-step-text').attr('name', `${options.name}[${item.name}]`);
  271. $row.find('.think-options-col-delete .btn').toggleClass('disabled', hasSub || this._items.length == 1);
  272. $row.find('.think-options-col-delete .btn.disabled').attr('title', options.deleteStepTip);
  273. $row.find('.think-options-col-add .btn-action[data-action="sub"]').toggleClass('disabled', item.level >= 3);
  274. return $row;
  275. }
  276. render()
  277. {
  278. const items = this._items;
  279. let rootIndex = 0;
  280. items.forEach(item =>
  281. {
  282. updateItemName(item);
  283. if(item.level === 1)
  284. {
  285. item.index = rootIndex;
  286. rootIndex++;
  287. updateItemName(item, `${rootIndex}`);
  288. }
  289. if(item.children)
  290. {
  291. item.children.forEach((subItem, subIndex) =>
  292. {
  293. subItem.index = subIndex;
  294. updateItemName(subItem, `${item.name}.${subIndex + 1}`);
  295. });
  296. }
  297. });
  298. items.sort((a, b) => a.order - b.order);
  299. const $list = this.$element.find('.think-options-body');
  300. const $rows = $list.find('.think-options-item').addClass('is-expired');
  301. let $preRow = null;
  302. items.forEach(item =>
  303. {
  304. $preRow = this._renderRow(item, $preRow, $list, $rows);
  305. });
  306. $rows.filter('.is-expired').remove();
  307. }
  308. deleteStep(name)
  309. {
  310. const index = this._items.findIndex(x => x.name === name);
  311. if (index < 0) return;
  312. const item = this._items[index];
  313. this._items.splice(index, 1);
  314. const parent = this.getParent(item);
  315. if(parent)
  316. {
  317. const siblings = parent.children;
  318. const index = siblings.indexOf(item);
  319. if(index > -1) siblings.splice(index, 1);
  320. }
  321. if (!this._items.length) this.update(['1'], false);
  322. this.render();
  323. }
  324. focus(name)
  325. {
  326. const $step = this.$element.find('.think-options-item[data-name="' + name + '"] .think-options-step-text');
  327. if($step.length) $step[0].focus();
  328. }
  329. addSib(fromName, focus = true)
  330. {
  331. const item = this.getByName(fromName);
  332. if(!item) return;
  333. const newItem = this.updateItem(item.name);
  334. if(focus) this.focus(newItem.name);
  335. this.$element.find(`.think-options-row[data-id="${newItem.id}"] .think-options-step-text`).val('');
  336. }
  337. moveAfter(fromName, toName)
  338. {
  339. if(fromName === toName) return;
  340. const item = this.getByName(fromName);
  341. if(!item) return;
  342. item.name = toName;
  343. this.updateItem(item);
  344. }
  345. moveBefore(fromName, toName)
  346. {
  347. if(fromName === toName) return;
  348. const item = this.getByName(fromName);
  349. const toItem = this.getByName(toName);
  350. if(!item || !toItem) return;
  351. item.name = toItem.level > 1 ? `${toItem.parentName}.${+toItem.selfName - 1}` : `${+toItem.selfName - 1}`;
  352. this.updateItem(item);
  353. }
  354. }
  355. /* Define $.fn.stepsEditor() helper. */
  356. ThinkOptions.defineFn();
  357. /* Extend StepsEditor to zui object. */
  358. $.extend(zui, {ThinkOptions});