UNPKG

grapesjs-clot

Version:

Free and Open Source Web Builder Framework

1,392 lines (1,224 loc) 43.2 kB
import Backbone from 'backbone'; import { isString, isFunction, isArray, result, each, bindAll } from 'underscore'; import { on, off, matches, getElement, getPointerEvent, isTextNode, getModel } from 'utils/mixins'; import { getComponentIds, setComponentIds, setComponentIdsWithArray } from '../dom_components/model/Components'; import { parse, stringify, toJSON } from 'flatted'; import { ClientState, ClientStateEnum, setState, ApplyingLocalOp, ApplyingBufferedLocalOp } from './WebSocket'; const $ = Backbone.$; const noop = () => {}; export default Backbone.View.extend({ initialize(opt) { this.opt = opt || {}; bindAll(this, 'startSort', 'onMove', 'endMove', 'rollback', 'updateOffset', 'moveDragHelper'); var o = opt || {}; this.elT = 0; this.elL = 0; this.borderOffset = o.borderOffset || 10; var el = o.container; this.el = typeof el === 'string' ? document.querySelector(el) : el; this.$el = $(this.el); this.containerSel = o.containerSel || 'div'; this.itemSel = o.itemSel || 'div'; this.draggable = o.draggable || true; this.nested = o.nested || 0; this.pfx = o.pfx || ''; this.ppfx = o.ppfx || ''; this.freezeClass = o.freezeClass || this.pfx + 'freezed'; this.onStart = o.onStart || noop; this.onEndMove = o.onEndMove || ''; this.customTarget = o.customTarget; this.onEnd = o.onEnd; this.direction = o.direction || 'v'; // v (vertical), h (horizontal), a (auto) this.onMoveClb = o.onMove || ''; this.relative = o.relative || 0; this.ignoreViewChildren = o.ignoreViewChildren || 0; this.ignoreModels = o.ignoreModels || 0; this.plh = o.placer || ''; // Frame offset this.wmargin = o.wmargin || 0; this.offTop = o.offsetTop || 0; this.offLeft = o.offsetLeft || 0; this.document = o.document || document; this.$document = $(this.document); this.dropContent = null; this.em = o.em || null; this.dragHelper = null; this.canvasRelative = o.canvasRelative || 0; this.selectOnEnd = !o.avoidSelectOnEnd; this.scale = o.scale; this.activeTextModel = null; if (this.em && this.em.on) { this.em.on('change:canvasOffset', this.updateOffset); this.updateOffset(); } }, getScale() { return result(this, scale) || 1; }, getContainerEl(elem) { if (elem) this.el = elem; if (!this.el) { var el = this.opt.container; this.el = typeof el === 'string' ? document.querySelector(el) : el; this.$el = $(this.el); } return this.el; }, getDocuments(el) { const em = this.em; const elDoc = el ? el.ownerDocument : em && em.get('Canvas').getBody().ownerDocument; const docs = [document]; elDoc && docs.push(elDoc); return docs; }, /** * Triggered when the offset of the editro is changed */ updateOffset() { const offset = this.em?.get('canvasOffset') || {}; this.offTop = offset.top; this.offLeft = offset.left; }, /** * Set content to drop * @param {String|Object} content */ setDropContent(content) { this.dropModel = null; this.dropContent = content; }, updateTextViewCursorPosition(e) { const { em } = this; if (!em) return; const Canvas = em.get('Canvas'); const targetDoc = Canvas.getDocument(); let range = null; if (targetDoc.caretRangeFromPoint) { // Chrome const poiner = getPointerEvent(e); range = targetDoc.caretRangeFromPoint(poiner.clientX, poiner.clientY); } else if (e.rangeParent) { // Firefox range = targetDoc.createRange(); range.setStart(e.rangeParent, e.rangeOffset); } const sel = Canvas.getWindow().getSelection(); Canvas.getFrameEl().focus(); sel.removeAllRanges(); range && sel.addRange(range); this.setContentEditable(this.activeTextModel, true); }, setContentEditable(model, mode) { if (model) { const el = model.getEl(); if (el.contentEditable != mode) el.contentEditable = mode; } }, /** * Toggle cursor while sorting * @param {Boolean} active */ toggleSortCursor(active) { const { em } = this; const cv = em && em.get('Canvas'); // Avoid updating body className as it causes a huge repaint // Noticeable with "fast" drag of blocks cv && (active ? cv.startAutoscroll() : cv.stopAutoscroll()); }, /** * Set drag helper * @param {HTMLElement} el * @param {Event} event */ setDragHelper(el, event) { const ev = event || ''; const clonedEl = el.cloneNode(1); const rect = el.getBoundingClientRect(); const computed = getComputedStyle(el); let style = ''; for (var i = 0; i < computed.length; i++) { const prop = computed[i]; style += `${prop}:${computed.getPropertyValue(prop)};`; } document.body.appendChild(clonedEl); clonedEl.className += ` ${this.pfx}bdrag`; clonedEl.setAttribute('style', style); this.dragHelper = clonedEl; clonedEl.style.width = `${rect.width}px`; clonedEl.style.height = `${rect.height}px`; ev && this.moveDragHelper(ev); // Listen mouse move events if (this.em) { $(this.em.get('Canvas').getBody().ownerDocument) .off('mousemove', this.moveDragHelper) .on('mousemove', this.moveDragHelper); } $(document).off('mousemove', this.moveDragHelper).on('mousemove', this.moveDragHelper); }, /** * Update the position of the helper * @param {Event} e */ moveDragHelper(e) { const doc = e.target.ownerDocument; if (!this.dragHelper || !doc) { return; } let posY = e.pageY; let posX = e.pageX; let addTop = 0; let addLeft = 0; const window = doc.defaultView || doc.parentWindow; const frame = window.frameElement; const dragHelperStyle = this.dragHelper.style; // If frame is present that means mouse has moved over the editor's canvas, // which is rendered inside the iframe and the mouse move event comes from // the iframe, not the parent window. Mouse position relative to the frame's // parent window needs to account for the frame's position relative to the // parent window. if (frame) { const frameRect = frame.getBoundingClientRect(); addTop = frameRect.top + document.documentElement.scrollTop; addLeft = frameRect.left + document.documentElement.scrollLeft; posY = e.clientY; posX = e.clientX; } dragHelperStyle.top = posY + addTop + 'px'; dragHelperStyle.left = posX + addLeft + 'px'; }, /** * Returns true if the element matches with selector * @param {Element} el * @param {String} selector * @return {Boolean} */ matches(el, selector, useBody) { return matches.call(el, selector); }, /** * Closest parent * @param {Element} el * @param {String} selector * @return {Element|null} */ closest(el, selector) { if (!el) return; var elem = el.parentNode; while (elem && elem.nodeType === 1) { if (this.matches(elem, selector)) return elem; elem = elem.parentNode; } return null; }, /** * Get the offset of the element * @param {HTMLElement} el * @return {Object} */ offset(el) { var rect = el.getBoundingClientRect(); return { top: rect.top + document.body.scrollTop, left: rect.left + document.body.scrollLeft, }; }, /** * Create placeholder * @return {HTMLElement} */ createPlaceholder() { var pfx = this.pfx; var el = document.createElement('div'); var ins = document.createElement('div'); el.className = pfx + 'placeholder'; el.style.display = 'none'; el.style['pointer-events'] = 'none'; ins.className = pfx + 'placeholder-int'; el.appendChild(ins); return el; }, /** * Picking component to move * @param {HTMLElement} src * */ startSort(src, opts = {}) { const { em, itemSel, containerSel, plh } = this; const container = this.getContainerEl(opts.container); const docs = this.getDocuments(src); let srcModel; this.dropModel = null; this.target = null; this.prevTarget = null; this.moved = 0; // Check if the start element is a valid one, if not, try the closest valid one if (src && !this.matches(src, `${itemSel}, ${containerSel}`)) { src = this.closest(src, itemSel); } this.sourceEl = src; // Create placeholder if doesn't exist yet if (!plh) { this.plh = this.createPlaceholder(); container.appendChild(this.plh); } if (src) { srcModel = this.getSourceModel(src); srcModel && srcModel.set && srcModel.set('status', 'freezed'); this.srcModel = srcModel; } on(container, 'mousemove dragover', this.onMove); on(docs, 'mouseup dragend touchend', this.endMove); on(docs, 'keydown', this.rollback); this.onStart({ sorter: this, target: srcModel, parent: srcModel && srcModel.parent?.(), index: srcModel && srcModel.index?.(), }); // Avoid strange effects on dragging em?.clearSelection(); this.toggleSortCursor(1); em?.trigger('sorter:drag:start', src, srcModel); }, /** * Get the model from HTMLElement target * @return {Model|null} */ getTargetModel(el) { let elem = el || this.target; return $(elem).data('model'); }, /** * Get the model of the current source element (element to drag) * @return {Model} */ getSourceModel(source, { target, avoidChildren = 1 } = {}) { const { em, sourceEl } = this; const src = source || sourceEl; let { dropModel, dropContent } = this; const isTextable = src => src && target && src.opt && src.opt.avoidChildren && this.isTextableActive(src, target); if (dropContent && em) { if (isTextable(dropModel)) { dropModel = null; } if (!dropModel) { const comps = em.get('DomComponents').getComponents(); const opts = { avoidChildren, avoidStore: 1, avoidUpdateStyle: 1, }; const tempModel = comps.add(dropContent, { ...opts, temporary: 1 }); dropModel = comps.remove(tempModel, opts); dropModel = dropModel instanceof Array ? dropModel[0] : dropModel; this.dropModel = dropModel; if (isTextable(dropModel)) { return this.getSourceModel(src, { target, avoidChildren: 0 }); } } return dropModel; } return src && $(src).data('model'); }, /** * Highlight target * @param {Model|null} model */ selectTargetModel(model, source) { if (model instanceof Backbone.Collection) { return; } // Prevents loops in Firefox // https://github.com/artf/grapesjs/issues/2911 if (source && source === model) return; const { targetModel } = this; // Reset the previous model but not if it's the same as the source // https://github.com/artf/grapesjs/issues/2478#issuecomment-570314736 if (targetModel && targetModel !== this.srcModel) { targetModel.set('status', ''); } if (model && model.set) { model.set('status', 'selected-parent'); this.targetModel = model; } }, /** * During move * @param {Event} e * */ onMove(e) { const ev = e; const { em, onMoveClb, plh, customTarget } = this; this.moved = 1; // Turn placeholder visibile var dsp = plh.style.display; if (!dsp || dsp === 'none') plh.style.display = 'block'; // Cache all necessary positions var eO = this.offset(this.el); this.elT = this.wmargin ? Math.abs(eO.top) : eO.top; this.elL = this.wmargin ? Math.abs(eO.left) : eO.left; var rY = e.pageY - this.elT + this.el.scrollTop; var rX = e.pageX - this.elL + this.el.scrollLeft; if (this.canvasRelative && em) { const mousePos = em.get('Canvas').getMouseRelativeCanvas(e, { noScroll: 1 }); rX = mousePos.x; rY = mousePos.y; } this.rX = rX; this.rY = rY; this.eventMove = e; //var targetNew = this.getTargetFromEl(e.target); const sourceModel = this.getSourceModel(); const targetEl = customTarget ? customTarget({ sorter: this, event: e }) : e.target; const dims = this.dimsFromTarget(targetEl, rX, rY); const target = this.target; const targetModel = target && this.getTargetModel(target); this.selectTargetModel(targetModel, sourceModel); if (!targetModel) plh.style.display = 'none'; if (!target) return; this.lastDims = dims; const pos = this.findPosition(dims, rX, rY); if (this.isTextableActive(sourceModel, targetModel)) { this.activeTextModel = targetModel; plh.style.display = 'none'; this.lastPos = pos; this.updateTextViewCursorPosition(ev); } else { this.disableTextable(); this.activeTextModel = null; // If there is a significant changes with the pointer if (!this.lastPos || this.lastPos.index != pos.index || this.lastPos.method != pos.method) { this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim); if (!this.$plh) this.$plh = $(this.plh); // With canvasRelative the offset is calculated automatically for // each element if (!this.canvasRelative) { if (this.offTop) this.$plh.css('top', '+=' + this.offTop + 'px'); if (this.offLeft) this.$plh.css('left', '+=' + this.offLeft + 'px'); } this.lastPos = pos; } } isFunction(onMoveClb) && onMoveClb({ event: e, target: sourceModel, parent: targetModel, index: pos.index + (pos.method == 'after' ? 1 : 0), }); em && em.trigger('sorter:drag', { target, targetModel, sourceModel, dims, pos, x: rX, y: rY, }); }, isTextableActive(src, trg) { return src && src.get && src.get('textable') && trg && trg.is('text'); }, disableTextable() { const { activeTextModel } = this; activeTextModel && activeTextModel.getView().disableEditing(); this.setContentEditable(activeTextModel, false); }, /** * Returns true if the elements is in flow, so is not in flow where * for example the component is with float:left * @param {HTMLElement} el * @param {HTMLElement} parent * @return {Boolean} * @private * */ isInFlow(el, parent) { if (!el) return false; parent = parent || document.body; var ch = -1, h; var elem = el; h = elem.offsetHeight; if (/*h < ch || */ !this.styleInFlow(elem, parent)) return false; else return true; }, /** * Check if el has style to be in flow * @param {HTMLElement} el * @param {HTMLElement} parent * @return {Boolean} * @private */ styleInFlow(el, parent) { if (isTextNode(el)) return; const style = el.style || {}; const $el = $(el); const $parent = parent && $(parent); if (style.overflow && style.overflow !== 'visible') return; const propFloat = $el.css('float'); if (propFloat && propFloat !== 'none') return; if ($parent && $parent.css('display') == 'flex' && $parent.css('flex-direction') !== 'column') return; switch (style.position) { case 'static': case 'relative': case '': break; default: return; } switch (el.tagName) { case 'TR': case 'TBODY': case 'THEAD': case 'TFOOT': return true; } switch ($el.css('display')) { case 'block': case 'list-item': case 'table': case 'flex': return true; } return; }, /** * Check if the target is valid with the actual source * @param {HTMLElement} trg * @return {Boolean} */ validTarget(trg, src) { const trgModel = this.getTargetModel(trg); const srcModel = this.getSourceModel(src, { target: trgModel }); src = srcModel && srcModel.view && srcModel.view.el; trg = trgModel && trgModel.view && trgModel.view.el; let result = { valid: true, src, srcModel, trg, trgModel, }; if (!src || !trg) { result.valid = false; return result; } // Check if the source is draggable in target let draggable = srcModel.get('draggable'); if (isFunction(draggable)) { const res = draggable(srcModel, trgModel); result.dragInfo = res; result.draggable = res; draggable = res; } else { draggable = draggable instanceof Array ? draggable.join(', ') : draggable; result.dragInfo = draggable; draggable = isString(draggable) ? this.matches(trg, draggable) : draggable; result.draggable = draggable; } // Check if the target could accept the source let droppable = trgModel.get('droppable'); if (isFunction(droppable)) { const res = droppable(srcModel, trgModel); result.droppable = res; result.dropInfo = res; droppable = res; } else { droppable = droppable instanceof Backbone.Collection ? 1 : droppable; droppable = droppable instanceof Array ? droppable.join(', ') : droppable; result.dropInfo = droppable; droppable = isString(droppable) ? this.matches(src, droppable) : droppable; droppable = draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable; result.droppable = droppable; } if (!droppable || !draggable) { result.valid = false; } return result; }, /** * Get dimensions of nodes relative to the coordinates * @param {HTMLElement} target * @param {number} rX Relative X position * @param {number} rY Relative Y position * @return {Array<Array>} */ dimsFromTarget(target, rX, rY) { const em = this.em; var dims = []; if (!target) { return dims; } // Select the first valuable target if (!this.matches(target, `${this.itemSel}, ${this.containerSel}`)) { target = this.closest(target, this.itemSel); } // If draggable is an array the target will be one of those if (this.draggable instanceof Array) { target = this.closest(target, this.draggable.join(',')); } if (!target) { return dims; } // Check if the target is different from the previous one if (this.prevTarget && this.prevTarget != target) { this.prevTarget = null; } // New target found if (!this.prevTarget) { this.targetP = this.closest(target, this.containerSel); // Check if the source is valid with the target let validResult = this.validTarget(target); em && em.trigger('sorter:drag:validation', validResult); if (!validResult.valid && this.targetP) { return this.dimsFromTarget(this.targetP, rX, rY); } this.prevTarget = target; this.prevTargetDim = this.getDim(target); this.cacheDimsP = this.getChildrenDim(this.targetP); this.cacheDims = this.getChildrenDim(target); } // If the target is the previous one will return the cached dims if (this.prevTarget == target) dims = this.cacheDims; // Target when I will drop element to sort this.target = this.prevTarget; // Generally, on any new target the poiner enters inside its area and // triggers nearBorders(), so have to take care of this if (this.nearBorders(this.prevTargetDim, rX, rY) || (!this.nested && !this.cacheDims.length)) { const targetParent = this.targetP; if (targetParent && this.validTarget(targetParent).valid) { dims = this.cacheDimsP; this.target = targetParent; } } this.lastPos = null; return dims; }, /** * Get valid target from element * This method should replace dimsFromTarget() * @param {HTMLElement} el * @return {HTMLElement} */ getTargetFromEl(el) { let target = el; let targetParent; let targetPrev = this.targetPrev; const em = this.em; const containerSel = this.containerSel; const itemSel = this.itemSel; // Select the first valuable target if (!this.matches(target, `${itemSel}, ${containerSel}`)) { target = this.closest(target, itemSel); } // If draggable is an array the target will be one of those // TODO check if this options is used somewhere if (this.draggable instanceof Array) { target = this.closest(target, this.draggable.join(',')); } // Check if the target is different from the previous one if (targetPrev && targetPrev != target) { this.targetPrev = ''; } // New target found if (!this.targetPrev) { targetParent = this.closest(target, containerSel); // If the current target is not valid (src/trg reasons) try with // the parent one (if exists) const validResult = this.validTarget(target); em && em.trigger('sorter:drag:validation', validResult); if (!validResult.valid && targetParent) { return this.getTargetFromEl(targetParent); } this.targetPrev = target; } // Generally, on any new target the poiner enters inside its area and // triggers nearBorders(), so have to take care of this if (this.nearElBorders(target)) { targetParent = this.closest(target, containerSel); if (targetParent && this.validTarget(targetParent).valid) { target = targetParent; } } return target; }, /** * Check if the current pointer is neare to element borders * @return {Boolen} */ nearElBorders(el) { const off = 10; const rect = el.getBoundingClientRect(); const body = el.ownerDocument.body; const { x, y } = this.getCurrentPos(); const top = rect.top + body.scrollTop; const left = rect.left + body.scrollLeft; const width = rect.width; const height = rect.height; if ( y < top + off || // near top edge y > top + height - off || // near bottom edge x < left + off || // near left edge x > left + width - off // near right edge ) { return 1; } }, getCurrentPos() { const ev = this.eventMove; const x = ev.pageX || 0; const y = ev.pageY || 0; return { x, y }; }, /** * Returns dimensions and positions about the element * @param {HTMLElement} el * @return {Array<number>} */ getDim(el) { const { em, canvasRelative } = this; const canvas = em && em.get('Canvas'); const offsets = canvas ? canvas.getElementOffsets(el) : {}; let top, left, height, width; if (canvasRelative && em) { const pos = canvas.getElementPos(el, { noScroll: 1 }); top = pos.top; // - offsets.marginTop; left = pos.left; // - offsets.marginLeft; height = pos.height; // + offsets.marginTop + offsets.marginBottom; width = pos.width; // + offsets.marginLeft + offsets.marginRight; } else { var o = this.offset(el); top = this.relative ? el.offsetTop : o.top - (this.wmargin ? -1 : 1) * this.elT; left = this.relative ? el.offsetLeft : o.left - (this.wmargin ? -1 : 1) * this.elL; height = el.offsetHeight; width = el.offsetWidth; } return { top, left, height, width, offsets }; }, /** * Get children dimensions * @param {HTMLELement} el Element root * @return {Array} * */ getChildrenDim(trg) { const dims = []; if (!trg) return dims; // Get children based on getChildrenContainer const trgModel = this.getTargetModel(trg); if (trgModel && trgModel.view && !this.ignoreViewChildren) { const view = trgModel.getCurrentView ? trgModel.getCurrentView() : trgModel.view; trg = view.getChildrenContainer(); } each(trg.children, (el, i) => { const model = getModel(el, $); const elIndex = model && model.index ? model.index() : i; if (!isTextNode(el) && !this.matches(el, this.itemSel)) { return; } const dim = this.getDim(el); let dir = this.direction; if (dir == 'v') dir = true; else if (dir == 'h') dir = false; else dir = this.isInFlow(el, trg); dim.dir = dir; dim.el = el; dim.indexEl = elIndex; dims.push(dim); }); return dims; }, /** * Check if the coordinates are near to the borders * @param {Array<number>} dim * @param {number} rX Relative X position * @param {number} rY Relative Y position * @return {Boolean} * */ nearBorders(dim, rX, rY) { var result = 0; var off = this.borderOffset; var x = rX || 0; var y = rY || 0; var t = dim.top; var l = dim.left; var h = dim.height; var w = dim.width; if (t + off > y || y > t + h - off || l + off > x || x > l + w - off) result = 1; return !!result; }, /** * Find the position based on passed dimensions and coordinates * @param {Array<Array>} dims Dimensions of nodes to parse * @param {number} posX X coordindate * @param {number} posY Y coordindate * @return {Object} * */ findPosition(dims, posX, posY) { var result = { index: 0, indexEl: 0, method: 'before' }; var leftLimit = 0, xLimit = 0, dimRight = 0, yLimit = 0, xCenter = 0, yCenter = 0, dimDown = 0, dim = 0; // Each dim is: Top, Left, Height, Width for (var i = 0, len = dims.length; i < len; i++) { dim = dims[i]; const { top, left, height, width } = dim; // Right position of the element. Left + Width dimRight = left + width; // Bottom position of the element. Top + Height dimDown = top + height; // X center position of the element. Left + (Width / 2) xCenter = left + width / 2; // Y center position of the element. Top + (Height / 2) yCenter = top + height / 2; // Skip if over the limits if ( (xLimit && left > xLimit) || (yLimit && yCenter >= yLimit) || // >= avoid issue with clearfixes (leftLimit && dimRight < leftLimit) ) continue; result.index = i; result.indexEl = dim.indexEl; // If it's not in flow (like 'float' element) if (!dim.dir) { if (posY < dimDown) yLimit = dimDown; //If x lefter than center if (posX < xCenter) { xLimit = xCenter; result.method = 'before'; } else { leftLimit = xCenter; result.method = 'after'; } } else { // If y upper than center if (posY < yCenter) { result.method = 'before'; break; } else result.method = 'after'; // After last element } } return result; }, /** * Updates the position of the placeholder * @param {HTMLElement} phl * @param {Array<Array>} dims * @param {Object} pos Position object * @param {Array<number>} trgDim target dimensions ([top, left, height, width]) * */ movePlaceholder(plh, dims, pos, trgDim) { var marg = 0, t = 0, l = 0, w = 0, h = 0, un = 'px', margI = 5, method = pos.method; const elDim = dims[pos.index]; // Placeholder orientation plh.classList.remove('vertical'); plh.classList.add('horizontal'); if (elDim) { // If it's not in flow (like 'float' element) const { top, left, height, width } = elDim; if (!elDim.dir) { w = 'auto'; h = height - marg * 2 + un; t = top + marg; l = method == 'before' ? left - marg : left + width - marg; plh.classList.remove('horizontal'); plh.classList.add('vertical'); } else { w = width + un; h = 'auto'; t = method == 'before' ? top - marg : top + height - marg; l = left; } } else { // Placeholder inside the component if (!this.nested) { plh.style.display = 'none'; return; } if (trgDim) { const offset = trgDim.offsets || {}; const pT = offset.paddingTop || margI; const pL = offset.paddingLeft || margI; t = trgDim.top + pT; l = trgDim.left + pL; w = parseInt(trgDim.width) - pL * 2 + un; h = 'auto'; } } plh.style.top = t + un; plh.style.left = l + un; if (w) plh.style.width = w; if (h) plh.style.height = h; }, /** * Leave item * @param event * * @return void * */ endMove(e) { //console.log('utils/Sorter.js => endMove start'); const src = this.sourceEl; const moved = []; const docs = this.getDocuments(); const container = this.getContainerEl(); const onEndMove = this.onEndMove; const onEnd = this.onEnd; let { target, lastPos } = this; let srcModel; off(container, 'mousemove dragover', this.onMove); off(docs, 'mouseup dragend touchend', this.endMove); off(docs, 'keydown', this.rollback); this.plh.style.display = 'none'; if (src) { srcModel = this.getSourceModel(); if (this.selectOnEnd && srcModel && srcModel.set) { srcModel.set('status', ''); srcModel.set('status', 'selected'); } } if (this.moved && target) { const toMove = this.toMove; const toMoveArr = isArray(toMove) ? toMove : toMove ? [toMove] : [src]; toMoveArr.forEach(model => { moved.push(this.move(target, model, lastPos)); }); } if (this.plh) this.plh.style.display = 'none'; var dragHelper = this.dragHelper; if (dragHelper) { dragHelper.parentNode.removeChild(dragHelper); this.dragHelper = null; } this.disableTextable(); this.selectTargetModel(); this.toggleSortCursor(); this.toMove = null; this.eventMove = 0; this.dropModel = null; if (isFunction(onEndMove)) { const data = { target: srcModel, parent: srcModel && srcModel.parent(), index: srcModel && srcModel.index(), }; moved.length ? moved.forEach(m => onEndMove(m, this, data)) : onEndMove(null, this, { ...data, cancelled: 1 }); } isFunction(onEnd) && onEnd({ sorter: this }); //console.log('utils/Sorter.js => endMove end'); }, myMove(paramOpts, action) { const domc = this.em.get('DomComponents'); const { em, dropContent } = this; let dst = paramOpts.dst; let src = paramOpts.src; let pos = paramOpts.pos; let trgModel = this.getTargetModel(dst); let srcModel = src ? domc.getById(paramOpts.srcId) : null; let draggable = paramOpts.draggable; let droppable = paramOpts.droppable; const srcEl = getElement(src); const warns = []; const index = pos.method === 'after' ? pos.indexEl + 1 : pos.indexEl; let validResult; let modelToDrop, created; if (srcModel) { srcModel.set('status', ''); srcModel.set('status', 'selected'); } //dst.classList.remove('gjs-selected-parent'); if (!srcModel && action === 'move-component') return; let targetCollection = $(this.document.getElementById(paramOpts.dstId)).data('collection'); if (targetCollection && droppable && draggable) { const opts = { at: index, action: 'move-component' }; const isTextable = this.isTextableActive(srcModel, trgModel); if (!dropContent) { const srcIndex = srcModel.collection.indexOf(srcModel); const sameCollection = targetCollection === srcModel.collection; const sameIndex = srcIndex === index || srcIndex === index - 1; const canRemove = !sameCollection || !sameIndex || isTextable; if (canRemove) { modelToDrop = srcModel.collection.remove(srcModel, { temporary: true }); if (sameCollection && index > srcIndex) { opts.at = index - 1; } } } else { modelToDrop = isFunction(dropContent) ? dropContent() : dropContent; opts.avoidUpdateStyle = true; opts.action = 'add-component'; } if (modelToDrop) { if (isTextable) { delete opts.at; created = trgModel.getView().insertComponent(modelToDrop, opts); } else { // add modelToDrop at index opts.at of targetCollection created = targetCollection.add(modelToDrop, opts); } } // set ids on the new component if (created && paramOpts.idArray) { setComponentIdsWithArray(created, paramOpts.idArray); } this.dropContent = null; this.prevTarget = null; // This will recalculate children dimensions } else if (em) { const dropInfo = paramOpts.dropInfo || trgModel?.get('droppable'); const dragInfo = paramOpts.dragInfo || srcModel?.get('draggable'); !targetCollection && warns.push('Target collection not found'); !droppable && dropInfo && warns.push(`Target is not droppable, accepts [${dropInfo}]`); !draggable && dragInfo && warns.push(`Component not draggable, acceptable by [${dragInfo}]`); em.logWarning('Invalid target position', { errors: warns, model: srcModel, context: 'sorter', target: trgModel, }); } em?.trigger('sorter:drag:end', { targetCollection, modelToDrop, warns, validResult, dst, srcEl, }); return created; }, myTextMove(paramOpts, action) { const domc = this.em.get('DomComponents'); const { em, dropContent } = this; let dst = paramOpts.dst; let src = paramOpts.src; let pos = paramOpts.pos; let trgModel = this.getTargetModel(dst); let srcModel = src ? domc.getById(paramOpts.srcId) : null; let draggable = paramOpts.draggable; let droppable = paramOpts.droppable; const srcEl = getElement(src); const warns = []; const index = pos.method === 'after' ? pos.indexEl + 1 : pos.indexEl; let validResult; let modelToDrop, created; if (srcModel) { srcModel.set('status', ''); srcModel.set('status', 'selected'); } //dst.classList.remove('gjs-selected-parent'); if (!srcModel && action === 'move-component') return; let targetCollection = $(this.document.getElementById(paramOpts.dstId)).data('collection'); if (targetCollection && droppable && draggable) { const opts = { at: index, action: 'move-component' }; const isTextable = this.isTextableActive(srcModel, trgModel); if (!dropContent) { const srcIndex = srcModel.collection.indexOf(srcModel); const sameCollection = targetCollection === srcModel.collection; const sameIndex = srcIndex === index || srcIndex === index - 1; const canRemove = !sameCollection || !sameIndex || isTextable; if (canRemove) { modelToDrop = srcModel.collection.remove(srcModel, { temporary: true }); if (sameCollection && index > srcIndex) { opts.at = index - 1; } } } else { modelToDrop = isFunction(dropContent) ? dropContent() : dropContent; opts.avoidUpdateStyle = true; opts.action = 'add-component'; } if (modelToDrop) { if (isTextable) { delete opts.at; created = trgModel.getView().insertComponent(modelToDrop, opts); } else { // add modelToDrop at index opts.at of targetCollection created = targetCollection.add(modelToDrop, opts); } } // set ids on the new component if (created && paramOpts.idArray) { setComponentIdsWithArray(created, paramOpts.idArray); } this.dropContent = null; this.prevTarget = null; // This will recalculate children dimensions } else if (em) { const dropInfo = paramOpts.dropInfo || trgModel?.get('droppable'); const dragInfo = paramOpts.dragInfo || srcModel?.get('draggable'); !targetCollection && warns.push('Target collection not found'); !droppable && dropInfo && warns.push(`Target is not droppable, accepts [${dropInfo}]`); !draggable && dragInfo && warns.push(`Component not draggable, acceptable by [${dragInfo}]`); em.logWarning('Invalid target position', { errors: warns, model: srcModel, context: 'sorter', target: trgModel, }); } em?.trigger('sorter:drag:end', { targetCollection, modelToDrop, warns, validResult, dst, srcEl, }); let op = {}; let tmpNode = document.createElement('div'); tmpNode.appendChild(dst.cloneNode()); let dstString = tmpNode.innerHTML; paramOpts.idArray = []; paramOpts.dst = dstString; op.opts = paramOpts; op.action = 'add-component'; if (op.action == 'add-component' && !paramOpts.dropContent) return null; if (ClientState == ClientStateEnum.Synced) { // set state to ApplyingLocalOp setState(ClientStateEnum.ApplyingLocalOp); // increase localTS and set localOp ApplyingLocalOp(op); } else if (ClientState == ClientStateEnum.AwaitingACK || ClientState == ClientStateEnum.AwaitingWithBuffer) { // set state to ApplyingBufferedLocalOp setState(ClientStateEnum.ApplyingBufferedLocalOp); // push the op to buffer ApplyingBufferedLocalOp(op); } return created; }, /** * Move component to new position * @param {HTMLElement} dst Destination target * @param {HTMLElement} src Element to move * @param {Object} pos Object with position coordinates * */ move(dst, src, pos) { //console.log('Sorter.js => move start'); let { em, dropContent } = this; const srcEl = getElement(src); const warns = []; const index = pos.method === 'after' ? pos.indexEl + 1 : pos.indexEl; const validResult = this.validTarget(dst, srcEl); const { trgModel, srcModel, draggable } = validResult; const targetCollection = $(dst).data('collection'); const droppable = trgModel instanceof Backbone.Collection ? 1 : validResult.droppable; let modelToDrop, created; let tmpNode = document.createElement('div'); tmpNode.appendChild(dst.cloneNode()); let dstString = tmpNode.innerHTML; let op = {}; let opOpts = { dst: dstString, dstId: dst.id, src: src, srcId: src ? src.getId() : null, srcParentId: src ? src.parent().getId() : null, srcIndex: src ? src.collection.indexOf(src) : null, pos: pos, dropContent: parse(stringify(dropContent)), draggable: draggable, droppable: droppable, dragInfo: validResult.dragInfo, dropInfo: validResult.dropInfo, }; let idArray; if (targetCollection && droppable && draggable) { const opts = { at: index, action: 'move-component' }; const isTextable = this.isTextableActive(srcModel, trgModel); if (!dropContent) { const srcIndex = srcModel.collection.indexOf(srcModel); const sameCollection = targetCollection === srcModel.collection; const sameIndex = srcIndex === index || srcIndex === index - 1; const canRemove = !sameCollection || !sameIndex || isTextable; if (canRemove) { modelToDrop = srcModel.collection.remove(srcModel, { temporary: true }); if (sameCollection && index > srcIndex) { opts.at = index - 1; } } } else { modelToDrop = isFunction(dropContent) ? dropContent() : dropContent; opts.avoidUpdateStyle = true; opts.action = 'add-component'; } if (modelToDrop) { if (isTextable) { delete opts.at; created = trgModel.getView().insertComponent(modelToDrop, opts); } else { // add modelToDrop at index opts.at of targetCollection created = targetCollection.add(modelToDrop, opts); } } if (created) { // set new component setComponentIds(created); // get the ids of the new components idArray = getComponentIds(created); } this.dropContent = null; this.prevTarget = null; // This will recalculate children dimensions opOpts.idArray = idArray; op.opts = opOpts; op.action = src ? 'move-component' : 'add-component'; if (op.action == 'add-component' && !opOpts.dropContent) return null; if (ClientState == ClientStateEnum.Synced) { // set state to ApplyingLocalOp setState(ClientStateEnum.ApplyingLocalOp); // increase localTS and set localOp ApplyingLocalOp(op); } else if (ClientState == ClientStateEnum.AwaitingACK || ClientState == ClientStateEnum.AwaitingWithBuffer) { // set state to ApplyingBufferedLocalOp setState(ClientStateEnum.ApplyingBufferedLocalOp); // push the op to buffer ApplyingBufferedLocalOp(op); } } else if (em) { const dropInfo = validResult.dropInfo || trgModel?.get('droppable'); const dragInfo = validResult.dragInfo || srcModel?.get('draggable'); !targetCollection && warns.push('Target collection not found'); !droppable && dropInfo && warns.push(`Target is not droppable, accepts [${dropInfo}]`); !draggable && dragInfo && warns.push(`Component not draggable, acceptable by [${dragInfo}]`); em.logWarning('Invalid target position', { errors: warns, model: srcModel, context: 'sorter', target: trgModel, }); } em?.trigger('sorter:drag:end', { targetCollection, modelToDrop, warns, validResult, dst, srcEl, }); //console.log('utils/Sorter.js move end'); return created; }, /** * Rollback to previous situation * @param {Event} * @param {Bool} Indicates if rollback in anycase * */ rollback(e) { off(this.getDocuments(), 'keydown', this.rollback); const key = e.which || e.keyCode; if (key == 27) { this.moved = 0; this.endMove(); } }, });