function updateItemName(item, name) { if(name === undefined) name = item.name; if(item.infoName === name) return; item.infoName = name; item.name = name; const namePath = name.split('.'); item.order = namePath.reduce((total, level, index) => total + (+level * ([1000000, 1000, 1, 0.001, 0.000001][index])), 0); item.level = namePath.length; item.selfName = namePath.pop(); item.parentName = namePath.join('.'); } function updateChildrenName(item) { if(!item.children) return; item.children.forEach((subItem, subIndex) => { updateItemName(subItem, `${item.name}.${subIndex + 1}`); updateChildrenName(subItem); }); } /** Steps editor component. */ class StepsEditor extends zui.Component { static NAME = 'StepsEditor'; static DEFAULT = { name: 'steps', expectsName: 'expects', data: ['1', '2', '3'], sameLevelIcon: 'plus', subLevelIcon: 'split', moveIcon: 'move', deleteIcon: 'trash', expectDisabledTip: '', deleteStepTip: '', dragNestedTip: '', changeLevelByDrag: false, expectDisabled: true, postDataID: false }; init() { this.reset(this.options.data, true); } afterInit() { this.render(); const $element = this.$element; $element.attr('zui-form-field', this.options.name) .on('focus', 'textarea', e => { const $textarea = $(e.target); $textarea.closest('.steps-editor-col-step').addClass('focus'); $textarea.closest('.steps-editor-item').addClass('is-focus'); }) .on('blur', 'textarea', e => { const $textarea = $(e.target); $textarea.closest('.steps-editor-col-step').removeClass('focus'); $textarea.closest('.steps-editor-item').removeClass('is-focus'); if($textarea.val().length > 0 && $textarea.closest('.steps-editor-row').next('.steps-editor-row').length == 0) { this.addSib($textarea.closest('.steps-editor-body').find('.steps-editor-row[data-level="1"]').last().attr('data-name'), false); } }) .on('click', '.btn-action', e => { const $btn = $(e.currentTarget); if($btn.is('.disabled')) return; const action = $btn.attr('data-action'); const name = $btn.closest('.steps-editor-item').attr('data-name'); if(action === 'delete') this.deleteStep(name); else if(action === 'sib') this.addSib(name); else if(action === 'sub') this.addSub(name); }).on('mouseenter', '.steps-editor-step-move', e => { $element.find('.move-hover').removeClass('move-hover'); $(e.currentTarget).closest('.steps-editor-item').addClass('move-hover'); }).on('mouseleave', '.steps-editor-step-move', e => { $element.find('.move-hover').removeClass('move-hover'); }); $element.draggable( { selector: '.steps-editor-item', handle: '.steps-editor-step-move', beforeDrag: (_event, dragElement) => { const dragName = $(dragElement).attr('data-name'); this.dragName = dragName; this.dragItem = this.getByName(dragName); this.dragMaxLevel = this.dragItem.level; this.isValidLevel = true; $element.find(`.steps-editor-item[data-name^="${dragName}."]`).addClass('is-sub-dragging').each((_index, ele) => { const level = +($(ele).attr('data-level')); if(level > this.dragMaxLevel) this.dragMaxLevel = level; }); $element.removeClass('cursor-not-allowed'); }, onDragStart: (event, dragElement) => { event.dataTransfer.setDragImage($(dragElement).find('.steps-editor-drag-ghost')[0], 0, 0); }, onDragEnd: () => { $element.find('.is-sub-dragging').removeClass('is-sub-dragging'); const dropName = this.dropName; if(dropName) { this.dropName = null; const $dropRow = this.getRow(dropName); if($dropRow && $dropRow.length) $dropRow.removeAttr('data-drop-side').removeAttr('data-drop-level'); } if(!this.isValidLevel) return; const dragName = this.dragName; if(!dropName || dragName === dropName) return; if(this.dropSide === 'bottom') this.moveAfter(dragName, dropName); else this.moveBefore(dragName, dropName); }, onDragOver: (event, dragElement, dropElement) => { const $row = $(dropElement); const dropName = $row.attr('data-name'); let idDiff = false; if(dropName !== this.dropName) { this.dropName = dropName; const $oldRow = this.getRow(dropName); $oldRow.removeAttr('data-drop-side') .removeAttr('data-drop-level') .removeAttr('data-invalid-nested'); $oldRow.find('.steps-editor-step-name').removeClass('is-invalid-drop-level'); this.dropSide = ''; this.dropLevel = ''; this.isValidLevel = undefined; idDiff = true; } const dropItem = this.getByName(dropName); const dropBounding = dropElement.getBoundingClientRect(); const dropSide = event.clientY > (dropBounding.top + dropBounding.height / 2) ? 'bottom' : 'top'; 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)))); const isValidLevel = (this.dragMaxLevel + (dropLevel - this.dragItem.level)) <= 3; if(this.isValidLevel !== isValidLevel) { this.isValidLevel = isValidLevel; $row.toggleClass('is-invalid-drop-level', !isValidLevel); const $name = $row.find('.steps-editor-step-name'); if(isValidLevel) $name.removeAttr('data-invalid-nested'); else $name.attr('data-invalid-nested', this.options.dragNestedTip); } $element.toggleClass('cursor-not-allowed', !isValidLevel); if(this.dropSide !== dropSide) { this.dropSide = dropSide; $row.attr('data-drop-side', dropSide); idDiff = true; } if(this.dropLevel !== dropLevel) { this.dropLevel = dropLevel; $row.attr('data-drop-level', dropLevel); idDiff = true; } }, target: (dragElement) => { return $element.find('.steps-editor-item').not('.is-sub-dragging').not(dragElement); } }); } getByID(id) { return this._items.find(x => x.id === id); } getByName(name) { return this._items.find(x => x.name === name); } getParent(item) { if(typeof item === 'string') item = this.getByName(item); return (item && item.parentName) ? this.getByName(item.parentName) : null; } getRow(name) { return this.$element.find(`.steps-editor-item[data-name="${name}"]`); } reset(data, skipRender) { this._items = []; this.update(data, skipRender); } update(data, skipRender) { data.forEach(item =>{this.updateItem(item, true);}); if(!skipRender) this.render(); } updateItem(item, skipRender) { item = typeof item === 'string' ? {name: item, id: $.guid++, step: '', expect: ''} : $.extend({id: $.guid++, step: '', expect: ''}, item); updateItemName(item); const index = this._items.findIndex(x => x.id === item.id); if(index >= 0) { const oldItem = this._items[index]; const parent = this.getParent(oldItem); if(parent) parent.children.splice(parent.children.indexOf(oldItem), 1); item = $.extend(oldItem, item); this._items.splice(index, 1); updateChildrenName(item); } if(item.level > 1) { this._items.push(item); const parent = this._ensureItem(item.parentName); const siblings = parent.children || []; const oldIndex = siblings.findIndex(x => x.id === item.id); if(oldIndex >= 0) siblings.splice(oldIndex, 1); const index = siblings.findIndex(x => x.order > item.order); if(index === 0) siblings.unshift(item); else if(index < 0) siblings.push(item); else if(index > 0) siblings.splice(index, 0, item); parent.children = siblings; } else { const siblings = this._items; const index = siblings.findIndex(x => x.level === 1 && x.order > item.order); if(index === 0) siblings.unshift(item); else if(index < 0) siblings.push(item); else if(index > 0) siblings.splice(index, 0, item); } if(!skipRender) this.render(); return item; } _ensureItem(name) { return this.getByName(name) || this.updateItem(name); } _createRow(item, options) { const $row = $ ([ `