UNPKG

@atrubchik/vue-slicksort

Version:

Set of mixins to turn any list into a sortable, touch-friendly, animated list

1,243 lines (1,232 loc) 51.3 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var vue = require('vue'); // Export Sortable Element Component Mixin const ElementMixin = vue.defineComponent({ inject: ['manager'], props: { index: { type: Number, required: true, }, disabled: { type: Boolean, default: false, }, }, data() { return {}; }, watch: { index(newIndex) { if (this.$el && this.$el.sortableInfo) { this.$el.sortableInfo.index = newIndex; } }, disabled(isDisabled) { if (isDisabled) { this.removeDraggable(); } else { this.setDraggable(this.index); } }, }, mounted() { const { disabled, index } = this.$props; if (!disabled) { this.setDraggable(index); } }, beforeUnmount() { if (!this.disabled) this.removeDraggable(); }, methods: { setDraggable(index) { const node = this.$el; node.sortableInfo = { index, manager: this.manager, }; this.ref = { node }; this.manager.add(this.ref); }, removeDraggable() { this.manager.remove(this.ref); }, }, }); class Manager { constructor() { this.refs = []; this.active = null; } add(ref) { if (!this.refs) { this.refs = []; } this.refs.push(ref); } remove(ref) { const index = this.getIndex(ref); if (index !== -1) { this.refs.splice(index, 1); } } isActive() { return !!this.active; } getActive() { return this.refs.find(({ node }) => { var _a, _b; return ((_a = node === null || node === void 0 ? void 0 : node.sortableInfo) === null || _a === void 0 ? void 0 : _a.index) == ((_b = this === null || this === void 0 ? void 0 : this.active) === null || _b === void 0 ? void 0 : _b.index); }) || null; } getIndex(ref) { return this.refs.indexOf(ref); } getRefs() { return this.refs; } getOrderedRefs() { return this.refs.sort((a, b) => { return a.node.sortableInfo.index - b.node.sortableInfo.index; }); } } const isTouch = (e) => { return e.touches != null; }; // eslint-disable-next-line @typescript-eslint/ban-types function hasOwnProperty(obj, prop) { return !!obj && Object.prototype.hasOwnProperty.call(obj, prop); } function arrayMove(arr, previousIndex, newIndex) { const array = arr.slice(0); if (newIndex >= array.length) { let k = newIndex - array.length; while (k-- + 1) { array.push(undefined); } } array.splice(newIndex, 0, array.splice(previousIndex, 1)[0]); return array; } function arrayRemove(arr, previousIndex) { const array = arr.slice(0); if (previousIndex >= array.length) return array; array.splice(previousIndex, 1); return array; } function arrayInsert(arr, newIndex, value) { const array = arr.slice(0); if (newIndex === array.length) { array.push(value); } else { array.splice(newIndex, 0, value); } return array; } const events = { start: ['touchstart', 'mousedown'], move: ['touchmove', 'mousemove'], end: ['touchend', 'mouseup'], cancel: ['touchcancel', 'keyup'], }; function closest(el, fn) { while (el) { if (fn(el)) return el; el = el.parentNode; } } function limit(min, max, value) { if (value < min) { return min; } if (value > max) { return max; } return value; } function getCSSPixelValue(stringValue) { if (stringValue.substr(-2) === 'px') { return parseFloat(stringValue); } return 0; } function getElementMargin(element) { const style = window.getComputedStyle(element); return { top: getCSSPixelValue(style.marginTop), right: getCSSPixelValue(style.marginRight), bottom: getCSSPixelValue(style.marginBottom), left: getCSSPixelValue(style.marginLeft), }; } function getPointerOffset(e, reference = 'page') { const x = `${reference}X`; const y = `${reference}Y`; return { x: isTouch(e) ? e.touches[0][x] : e[x], y: isTouch(e) ? e.touches[0][y] : e[y], }; } function offsetParents(node) { const nodes = [node]; for (; node; node = node.offsetParent) { nodes.unshift(node); } return nodes; } function commonOffsetParent(node1, node2) { const parents1 = offsetParents(node1); const parents2 = offsetParents(node2); if (parents1[0] != parents2[0]) throw 'No common ancestor!'; for (let i = 0; i < parents1.length; i++) { if (parents1[i] != parents2[i]) return parents1[i - 1]; } } function getEdgeOffset(node, container, offset = { top: 0, left: 0 }) { // Get the actual offsetTop / offsetLeft value, no matter how deep the node is nested if (node) { const nodeOffset = { top: offset.top + node.offsetTop, left: offset.left + node.offsetLeft, }; if (node.offsetParent !== container.offsetParent) { return getEdgeOffset(node.offsetParent, container, nodeOffset); } else { return nodeOffset; } } return { top: 0, left: 0 }; } function cloneNode(node) { const fields = node.querySelectorAll('input, textarea, select'); const clonedNode = node.cloneNode(true); const clonedFields = [...clonedNode.querySelectorAll('input, textarea, select')]; // Convert NodeList to Array clonedFields.forEach((field, index) => { if (field.type !== 'file' && fields[index]) { field.value = fields[index].value; } }); return clonedNode; } function getLockPixelOffsets(lockOffset, width, height) { if (typeof lockOffset == 'string') { lockOffset = +lockOffset; } if (!Array.isArray(lockOffset)) { lockOffset = [lockOffset, lockOffset]; } if (lockOffset.length !== 2) { throw new Error(`lockOffset prop of SortableContainer should be a single value or an array of exactly two values. Given ${lockOffset}`); } const [minLockOffset, maxLockOffset] = lockOffset; return [getLockPixelOffset(minLockOffset, width, height), getLockPixelOffset(maxLockOffset, width, height)]; } function getLockPixelOffset(lockOffset, width, height) { let offsetX = lockOffset; let offsetY = lockOffset; let unit = 'px'; if (typeof lockOffset === 'string') { const match = /^[+-]?\d*(?:\.\d*)?(px|%)$/.exec(lockOffset); if (match === null) { throw new Error(`lockOffset value should be a number or a string of a number followed by "px" or "%". Given ${lockOffset}`); } offsetX = offsetY = parseFloat(lockOffset); unit = match[1]; } if (!isFinite(offsetX) || !isFinite(offsetY)) { throw new Error(`lockOffset value should be a finite. Given ${lockOffset}`); } if (unit === '%') { offsetX = (offsetX * width) / 100; offsetY = (offsetY * height) / 100; } return { x: offsetX, y: offsetY, }; } function getDistance(x1, y1, x2, y2) { const x = x1 - x2; const y = y1 - y2; return Math.sqrt(x * x + y * y); } function getRectCenter(clientRect) { return { x: clientRect.left + clientRect.width / 2, y: clientRect.top + clientRect.height / 2, }; } function resetTransform(nodes = []) { for (let i = 0, len = nodes.length; i < len; i++) { const node = nodes[i]; const el = node.node; if (!el) return; // Clear the cached offsetTop / offsetLeft value node.edgeOffset = null; // Remove the transforms / transitions setTransform(el); } } function setTransform(el, transform = '', duration = '') { if (!el) return; el.style['transform'] = transform; el.style['transitionDuration'] = duration; } function withinBounds(pos, top, bottom) { const upper = Math.max(top, bottom); const lower = Math.min(top, bottom); return lower <= pos && pos <= upper; } function isPointWithinRect({ x, y }, { top, left, width, height }) { const withinX = withinBounds(x, left, left + width); const withinY = withinBounds(y, top, top + height); return withinX && withinY; } /* eslint-disable @typescript-eslint/ban-ts-comment */ // eslint-disable-next-line @typescript-eslint/ban-types const timeout = setTimeout; // Export Sortable Container Component Mixin const ContainerMixin = vue.defineComponent({ inject: ['SlicksortHub'], provide() { return { manager: this.manager, }; }, props: { list: { type: Array, required: true }, axis: { type: String, default: 'y' }, distance: { type: Number, default: 0 }, pressDelay: { type: Number, default: 0 }, pressThreshold: { type: Number, default: 5 }, useDragHandle: { type: Boolean, default: false }, useWindowAsScrollContainer: { type: Boolean, default: false }, hideSortableGhost: { type: Boolean, default: true }, lockToContainerEdges: { type: Boolean, default: false }, lockOffset: { type: [String, Number, Array], default: '50%' }, transitionDuration: { type: Number, default: 300 }, appendTo: { type: String, default: 'body' }, draggedSettlingDuration: { type: Number, default: null }, group: { type: String, default: '' }, accept: { type: [Boolean, Array, Function], default: null }, insert: { type: Function, default: () => true }, cancelKey: { type: String, default: 'Escape' }, block: { type: Array, default: () => [] }, lockAxis: { type: String, default: '' }, helperClass: { type: String, default: '' }, contentWindow: { type: Object, default: null }, shouldCancelStart: { type: Function, default: (e) => { // Cancel sorting if the event target is an `input`, `textarea`, `select` or `option` const disabledElements = ['input', 'textarea', 'select', 'option', 'button']; return disabledElements.indexOf(e.target.tagName.toLowerCase()) !== -1; }, }, getHelperDimensions: { type: Function, default: ({ node }) => ({ width: node.offsetWidth, height: node.offsetHeight, }), }, }, emits: ['sort-start', 'sort-move', 'sort-end', 'sort-cancel', 'sort-insert', 'sort-remove', 'sort-failed-to-insert', 'update:list'], data() { let useHub = false; if (this.group) { // If the group option is set, it is assumed the user intends // to drag between containers and the required plugin has been installed if (this.SlicksortHub) { useHub = true; } else if (process.env.NODE_ENV !== 'production') { throw new Error('Slicksort plugin required to use "group" prop'); } } return { sorting: false, hub: useHub ? this.SlicksortHub : null, manager: new Manager(), }; }, mounted() { if (this.hub) { this.id = this.hub.getId(); } this.container = this.$el; this.document = this.container.ownerDocument || document; this._window = this.contentWindow || window; this.scrollContainer = this.useWindowAsScrollContainer ? { scrollLeft: 0, scrollTop: 0 } : this.container; this.events = { start: this.handleStart, move: this.handleMove, end: this.handleEnd, }; for (const key in this.events) { if (hasOwnProperty(this.events, key)) { // @ts-ignore events[key].forEach((eventName) => this.container.addEventListener(eventName, this.events[key])); } } if (this.hub) { this.hub.addContainer(this); } }, beforeUnmount() { for (const key in this.events) { if (hasOwnProperty(this.events, key)) { // @ts-ignore events[key].forEach((eventName) => this.container.removeEventListener(eventName, this.events[key])); } } if (this.hub) { this.hub.removeContainer(this); } if (this.dragendTimer) clearTimeout(this.dragendTimer); if (this.cancelTimer) clearTimeout(this.cancelTimer); if (this.pressTimer) clearTimeout(this.pressTimer); if (this.autoscrollInterval) clearInterval(this.autoscrollInterval); }, methods: { handleStart(e) { const { distance, shouldCancelStart } = this.$props; if ((!isTouch(e) && e.button === 2) || shouldCancelStart(e)) { return false; } this._touched = true; this._pos = getPointerOffset(e); const target = e.target; const node = closest(target, (el) => el.sortableInfo != null); if (node && node.sortableInfo && this.nodeIsChild(node) && !this.sorting) { const { useDragHandle } = this.$props; const { index } = node.sortableInfo; if (useDragHandle && !closest(target, (el) => el.sortableHandle != null)) return; this.manager.active = { index }; /* * Fixes a bug in Firefox where the :active state of anchor tags * prevent subsequent 'mousemove' events from being fired * (see https://github.com/clauderic/react-sortable-hoc/issues/118) */ if (target.tagName.toLowerCase() === 'a') { e.preventDefault(); } if (!distance) { if (this.pressDelay === 0) { this.handlePress(e); } else { this.pressTimer = timeout(() => this.handlePress(e), this.pressDelay); } } } }, nodeIsChild(node) { return node.sortableInfo.manager === this.manager; }, handleFailedToInsert() { const payload = this.getPayload(); this.$emit('sort-failed-to-insert', payload); }, handleMove(e) { const { distance, pressThreshold } = this.$props; if (!this.sorting && this._touched) { const offset = getPointerOffset(e); this._delta = { x: this._pos.x - offset.x, y: this._pos.y - offset.y, }; const delta = Math.abs(this._delta.x) + Math.abs(this._delta.y); if (!distance && (!pressThreshold || (pressThreshold && delta >= pressThreshold))) { if (this.cancelTimer) clearTimeout(this.cancelTimer); this.cancelTimer = timeout(this.cancel, 0); } else if (distance && delta >= distance && this.manager.isActive()) { this.handlePress(e); } } }, handleEnd() { if (!this._touched) return; const { distance } = this.$props; this._touched = false; if (!distance) { this.cancel(); } }, cancel() { if (!this.sorting) { if (this.pressTimer) clearTimeout(this.pressTimer); this.manager.active = null; if (this.hub) this.hub.cancel(); } }, handleSortCancel(e) { if (isTouch(e) || e.key === this.cancelKey) { this.newIndex = this.index; this.canceling = true; this.translate = { x: 0, y: 0 }; this.animateNodes(); this.handleSortEnd(e); } }, handlePress(e) { e.stopPropagation(); const active = this.manager.getActive(); if (active) { const { getHelperDimensions, helperClass, hideSortableGhost, appendTo } = this.$props; const { node } = active; const { index } = node.sortableInfo; const margin = getElementMargin(node); const containerBoundingRect = this.container.getBoundingClientRect(); const dimensions = getHelperDimensions({ index, node }); this.node = node; this.margin = margin; this.width = dimensions.width; this.height = dimensions.height; this.marginOffset = { x: this.margin.left + this.margin.right, y: Math.max(this.margin.top, this.margin.bottom), }; this.boundingClientRect = node.getBoundingClientRect(); this.containerBoundingRect = containerBoundingRect; this.index = index; this.newIndex = index; const clonedNode = cloneNode(node); this.helper = this.document.querySelector(appendTo).appendChild(clonedNode); this.helper.style.position = 'fixed'; this.helper.style.top = `${this.boundingClientRect.top - margin.top}px`; this.helper.style.left = `${this.boundingClientRect.left - margin.left}px`; this.helper.style.width = `${this.width}px`; this.helper.style.height = `${this.height}px`; this.helper.style.boxSizing = 'border-box'; this.helper.style.pointerEvents = 'none'; if (hideSortableGhost) { this.sortableGhost = node; node.style.visibility = 'hidden'; node.style.opacity = '0'; } if (this.hub) { this.hub.sortStart(this); this.hub.helper = this.helper; this.hub.ghost = this.sortableGhost; } this.intializeOffsets(e, this.boundingClientRect); this.offsetEdge = getEdgeOffset(node, this.container); if (helperClass) { this.helper.classList.add(...helperClass.split(' ')); } this.listenerNode = isTouch(e) ? node : this._window; // @ts-ignore events.move.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortMove)); // @ts-ignore events.end.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortEnd)); // @ts-ignore events.cancel.forEach((eventName) => this.listenerNode.addEventListener(eventName, this.handleSortCancel)); this.sorting = true; this.$emit('sort-start', { event: e, node, index }); } }, handleSortMove(e) { e.preventDefault(); // Prevent scrolling on mobile this.updatePosition(e); if (this.hub) { const payload = this.list[this.index]; this.hub.handleSortMove(e, payload); } if (!this.hub || this.hub.isDest(this)) { this.animateNodes(); this.autoscroll(); } this.$emit('sort-move', { event: e }); }, getPayload() { return this.list[this.index]; }, handleDropOut() { const removed = this.list[this.index]; const newValue = arrayRemove(this.list, this.index); this.$emit('sort-remove', { oldIndex: this.index, }); this.$emit('update:list', newValue); return removed; }, handleDropIn(payload) { const newValue = arrayInsert(this.list, this.newIndex, payload); this.$emit('sort-insert', { newIndex: this.newIndex, value: payload, }); this.$emit('update:list', newValue); this.handleDragEnd(); }, handleDragOut() { if (this.autoscrollInterval) { clearInterval(this.autoscrollInterval); this.autoscrollInterval = null; } if (this.hub.isSource(this)) { // Trick to animate all nodes up this.translate = { x: 10000, y: 10000, }; this.animateNodes(); } else { this.manager.getRefs().forEach((ref) => { ref.node.style['transform'] = ''; }); this.dragendTimer = timeout(this.handleDragEnd, this.transitionDuration || 0); } }, handleDragEnd() { if (this.autoscrollInterval) { clearInterval(this.autoscrollInterval); this.autoscrollInterval = null; } resetTransform(this.manager.getRefs()); if (this.sortableGhost) { this.sortableGhost.remove(); this.sortableGhost = null; } if (this.dragendTimer) { clearTimeout(this.dragendTimer); this.dragendTimer = null; } this.manager.active = null; this._touched = false; this.sorting = false; }, intializeOffsets(e, clientRect) { const { useWindowAsScrollContainer, containerBoundingRect, _window } = this; this.marginOffset = { x: this.margin.left + this.margin.right, y: Math.max(this.margin.top, this.margin.bottom), }; this._axis = { x: this.axis.indexOf('x') >= 0, y: this.axis.indexOf('y') >= 0, }; this.initialOffset = getPointerOffset(e); // initialScroll; this.initialScroll = { top: this.scrollContainer.scrollTop, left: this.scrollContainer.scrollLeft, }; // initialWindowScroll; this.initialWindowScroll = { top: window.pageYOffset, left: window.pageXOffset, }; this.translate = { x: 0, y: 0 }; this.minTranslate = {}; this.maxTranslate = {}; if (this._axis.x) { this.minTranslate.x = (useWindowAsScrollContainer ? 0 : containerBoundingRect.left) - clientRect.left - this.width / 2; this.maxTranslate.x = (useWindowAsScrollContainer ? _window.innerWidth : containerBoundingRect.left + containerBoundingRect.width) - clientRect.left - this.width / 2; } if (this._axis.y) { this.minTranslate.y = (useWindowAsScrollContainer ? 0 : containerBoundingRect.top) - clientRect.top - this.height / 2; this.maxTranslate.y = (useWindowAsScrollContainer ? _window.innerHeight : containerBoundingRect.top + containerBoundingRect.height) - clientRect.top - this.height / 2; } }, handleDragIn(e, sortableGhost, helper) { if (this.hub.isSource(this)) { return; } if (this.dragendTimer) { this.handleDragEnd(); clearTimeout(this.dragendTimer); this.dragendTimer = null; } const nodes = this.manager.getRefs(); this.index = nodes.length; this.manager.active = { index: this.index }; const containerBoundingRect = this.container.getBoundingClientRect(); const helperBoundingRect = helper.getBoundingClientRect(); this.containerBoundingRect = containerBoundingRect; this.sortableGhost = cloneNode(sortableGhost); this.container.appendChild(this.sortableGhost); const ghostRect = this.sortableGhost.getBoundingClientRect(); this.boundingClientRect = ghostRect; this.margin = getElementMargin(this.sortableGhost); this.width = ghostRect.width; this.height = ghostRect.height; // XY coords of the inserted node, relative to the top-left corner of the container this.offsetEdge = getEdgeOffset(this.sortableGhost, this.container); this.intializeOffsets(e, ghostRect); // Move the initialOffset back to the insertion point of the // sortableGhost (end of the list), as if we had started the drag there. this.initialOffset.x += ghostRect.x - helperBoundingRect.x; this.initialOffset.y += ghostRect.y - helperBoundingRect.y; // Turn on dragging this.sorting = true; }, handleSortEnd(e) { var _a, _b; const payload = this.getPayload(); if (this.hub && !((_b = (_a = this.hub) === null || _a === void 0 ? void 0 : _a.getDest()) === null || _b === void 0 ? void 0 : _b.insert(payload)) && !this.canceling) { this.newIndex = this.index; this.canceling = true; this.translate = { x: 0, y: 0 }; this.animateNodes(); this.handleSortEnd(e); this.handleFailedToInsert(); } // Remove the event listeners if the node is still in the DOM if (this.listenerNode) { events.move.forEach((eventName) => // @ts-ignore this.listenerNode.removeEventListener(eventName, this.handleSortMove)); events.end.forEach((eventName) => // @ts-ignore this.listenerNode.removeEventListener(eventName, this.handleSortEnd)); events.cancel.forEach((eventName) => // @ts-ignore this.listenerNode.removeEventListener(eventName, this.handleSortCancel)); } const nodes = this.manager.getRefs(); // Remove the helper class(es) early to give it a chance to transition back if (this.helper && this.helperClass) { this.helper.classList.remove(...this.helperClass.split(' ')); } // Stop autoscroll if (this.autoscrollInterval) clearInterval(this.autoscrollInterval); this.autoscrollInterval = null; const onEnd = () => { // Remove the helper from the DOM if (this.helper) { this.helper.remove(); this.helper = null; } if (this.hideSortableGhost && this.sortableGhost) { this.sortableGhost.style.visibility = ''; this.sortableGhost.style.opacity = ''; } resetTransform(nodes); // Update state if (this.hub && !this.hub.isDest(this)) { this.canceling ? this.hub.cancel() : this.hub.handleSortEnd(e); } else if (this.canceling) { this.$emit('sort-cancel', { event: e }); } else { this.$emit('sort-end', { event: e, oldIndex: this.index, newIndex: this.newIndex, }); this.$emit('update:list', arrayMove(this.list, this.index, this.newIndex)); } this.manager.active = null; this._touched = false; this.canceling = false; this.sorting = false; }; if (this.transitionDuration || this.draggedSettlingDuration) { this.transitionHelperIntoPlace(nodes, onEnd); } else { onEnd(); } }, transitionHelperIntoPlace(nodes, cb) { if (this.draggedSettlingDuration === 0 || nodes.length === 0 || !this.helper) { return Promise.resolve(); } const indexNode = nodes[this.index].node; let targetX = 0; let targetY = 0; const scrollDifference = { top: window.pageYOffset - this.initialWindowScroll.top, left: window.pageXOffset - this.initialWindowScroll.left, }; if (this.hub && !this.hub.isDest(this) && !this.canceling) { const dest = this.hub.getDest(); if (!dest) return; const destIndex = dest.newIndex; const destRefs = dest.manager.getOrderedRefs(); const destNode = destIndex < destRefs.length ? destRefs[destIndex].node : dest.sortableGhost; const ancestor = commonOffsetParent(indexNode, destNode); const sourceOffset = getEdgeOffset(indexNode, ancestor); const targetOffset = getEdgeOffset(destNode, ancestor); targetX = targetOffset.left - sourceOffset.left - scrollDifference.left; targetY = targetOffset.top - sourceOffset.top - scrollDifference.top; } else { const newIndexNode = nodes[this.newIndex].node; const deltaScroll = { left: this.scrollContainer.scrollLeft - this.initialScroll.left + scrollDifference.left, top: this.scrollContainer.scrollTop - this.initialScroll.top + scrollDifference.top, }; targetX = -deltaScroll.left; if (this.translate && this.translate.x > 0) { // Diff against right edge when moving to the right targetX += newIndexNode.offsetLeft + newIndexNode.offsetWidth - (indexNode.offsetLeft + indexNode.offsetWidth); } else { targetX += newIndexNode.offsetLeft - indexNode.offsetLeft; } targetY = -deltaScroll.top; if (this.translate && this.translate.y > 0) { // Diff against the bottom edge when moving down targetY += newIndexNode.offsetTop + newIndexNode.offsetHeight - (indexNode.offsetTop + indexNode.offsetHeight); } else { targetY += newIndexNode.offsetTop - indexNode.offsetTop; } } const duration = this.draggedSettlingDuration !== null ? this.draggedSettlingDuration : this.transitionDuration; setTransform(this.helper, `translate3d(${targetX}px,${targetY}px, 0)`, `${duration}ms`); // Register an event handler to clean up styles when the transition // finishes. const cleanup = (event) => { if (!event || event.propertyName === 'transform') { clearTimeout(cleanupTimer); setTransform(this.helper); cb(); } }; // Force cleanup in case 'transitionend' never fires const cleanupTimer = setTimeout(cleanup, duration + 10); this.helper.addEventListener('transitionend', cleanup); }, updatePosition(e) { const { lockAxis, lockToContainerEdges } = this.$props; const offset = getPointerOffset(e); const translate = { x: offset.x - this.initialOffset.x, y: offset.y - this.initialOffset.y, }; // Adjust for window scroll translate.y -= window.pageYOffset - this.initialWindowScroll.top; translate.x -= window.pageXOffset - this.initialWindowScroll.left; this.translate = translate; if (lockToContainerEdges) { const [minLockOffset, maxLockOffset] = getLockPixelOffsets(this.lockOffset, this.height, this.width); const minOffset = { x: this.width / 2 - minLockOffset.x, y: this.height / 2 - minLockOffset.y, }; const maxOffset = { x: this.width / 2 - maxLockOffset.x, y: this.height / 2 - maxLockOffset.y, }; if (this.minTranslate.x && this.maxTranslate.x) translate.x = limit(this.minTranslate.x + minOffset.x, this.maxTranslate.x - maxOffset.x, translate.x); if (this.minTranslate.y && this.maxTranslate.y) translate.y = limit(this.minTranslate.y + minOffset.y, this.maxTranslate.y - maxOffset.y, translate.y); } if (lockAxis === 'x') { translate.y = 0; } else if (lockAxis === 'y') { translate.x = 0; } if (this.helper) { this.helper.style['transform'] = `translate3d(${translate.x}px,${translate.y}px, 0)`; } }, animateNodes() { const { transitionDuration, hideSortableGhost } = this.$props; const nodes = this.manager.getOrderedRefs(); const deltaScroll = { left: this.scrollContainer.scrollLeft - this.initialScroll.left, top: this.scrollContainer.scrollTop - this.initialScroll.top, }; const sortingOffset = { left: this.offsetEdge.left + this.translate.x + deltaScroll.left, top: this.offsetEdge.top + this.translate.y + deltaScroll.top, }; const scrollDifference = { top: window.pageYOffset - this.initialWindowScroll.top, left: window.pageXOffset - this.initialWindowScroll.left, }; this.newIndex = null; for (let i = 0, len = nodes.length; i < len; i++) { const { node } = nodes[i]; const index = node.sortableInfo.index; const width = node.offsetWidth; const height = node.offsetHeight; const offset = { width: this.width > width ? width / 2 : this.width / 2, height: this.height > height ? height / 2 : this.height / 2, }; const translate = { x: 0, y: 0, }; let { edgeOffset } = nodes[i]; // If we haven't cached the node's offsetTop / offsetLeft value if (!edgeOffset) { nodes[i].edgeOffset = edgeOffset = getEdgeOffset(node, this.container); } // Get a reference to the next and previous node const nextNode = i < nodes.length - 1 && nodes[i + 1]; const prevNode = i > 0 && nodes[i - 1]; // Also cache the next node's edge offset if needed. // We need this for calculating the animation in a grid setup if (nextNode && !nextNode.edgeOffset) { nextNode.edgeOffset = getEdgeOffset(nextNode.node, this.container); } // If the node is the one we're currently animating, skip it if (index === this.index) { /* * With windowing libraries such as `react-virtualized`, the sortableGhost * node may change while scrolling down and then back up (or vice-versa), * so we need to update the reference to the new node just to be safe. */ if (hideSortableGhost) { this.sortableGhost = node; node.style.visibility = 'hidden'; node.style.opacity = '0'; } continue; } if (transitionDuration) { node.style['transitionDuration'] = `${transitionDuration}ms`; } if (this._axis.x) { if (this._axis.y) { // Calculations for a grid setup if (index < this.index && ((sortingOffset.left + scrollDifference.left - offset.width <= edgeOffset.left && sortingOffset.top + scrollDifference.top <= edgeOffset.top + offset.height) || sortingOffset.top + scrollDifference.top + offset.height <= edgeOffset.top)) { // If the current node is to the left on the same row, or above the node that's being dragged // then move it to the right translate.x = this.width + this.marginOffset.x; if (edgeOffset.left + translate.x > this.containerBoundingRect.width - offset.width && nextNode) { // If it moves passed the right bounds, then animate it to the first position of the next row. // We just use the offset of the next node to calculate where to move, because that node's original position // is exactly where we want to go translate.x = nextNode.edgeOffset.left - edgeOffset.left; translate.y = nextNode.edgeOffset.top - edgeOffset.top; } if (this.newIndex === null) { this.newIndex = index; } } else if (index > this.index && ((sortingOffset.left + scrollDifference.left + offset.width >= edgeOffset.left && sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top) || sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top + height)) { // If the current node is to the right on the same row, or below the node that's being dragged // then move it to the left translate.x = -(this.width + this.marginOffset.x); if (edgeOffset.left + translate.x < this.containerBoundingRect.left + offset.width && prevNode) { // If it moves passed the left bounds, then animate it to the last position of the previous row. // We just use the offset of the previous node to calculate where to move, because that node's original position // is exactly where we want to go translate.x = prevNode.edgeOffset.left - edgeOffset.left; translate.y = prevNode.edgeOffset.top - edgeOffset.top; } this.newIndex = index; } } else { if (index > this.index && sortingOffset.left + scrollDifference.left + offset.width >= edgeOffset.left) { translate.x = -(this.width + this.marginOffset.x); this.newIndex = index; } else if (index < this.index && sortingOffset.left + scrollDifference.left <= edgeOffset.left + offset.width) { translate.x = this.width + this.marginOffset.x; if (this.newIndex == null) { this.newIndex = index; } } } } else if (this._axis.y) { if (index > this.index && sortingOffset.top + scrollDifference.top + offset.height >= edgeOffset.top) { translate.y = -(this.height + this.marginOffset.y); this.newIndex = index; } else if (index < this.index && sortingOffset.top + scrollDifference.top <= edgeOffset.top + offset.height) { translate.y = this.height + this.marginOffset.y; if (this.newIndex == null) { this.newIndex = index; } } } node.style['transform'] = `translate3d(${translate.x}px,${translate.y}px,0)`; } if (this.newIndex == null) { this.newIndex = this.index; } }, autoscroll() { const translate = this.translate; const direction = { x: 0, y: 0, }; const speed = { x: 1, y: 1, }; const acceleration = { x: 10, y: 10, }; if (translate.y >= this.maxTranslate.y - this.height / 2) { direction.y = 1; // Scroll Down speed.y = acceleration.y * Math.abs((this.maxTranslate.y - this.height / 2 - translate.y) / this.height); } else if (translate.x >= this.maxTranslate.x - this.width / 2) { direction.x = 1; // Scroll Right speed.x = acceleration.x * Math.abs((this.maxTranslate.x - this.width / 2 - translate.x) / this.width); } else if (translate.y <= this.minTranslate.y + this.height / 2) { direction.y = -1; // Scroll Up speed.y = acceleration.y * Math.abs((translate.y - this.height / 2 - this.minTranslate.y) / this.height); } else if (translate.x <= this.minTranslate.x + this.width / 2) { direction.x = -1; // Scroll Left speed.x = acceleration.x * Math.abs((translate.x - this.width / 2 - this.minTranslate.x) / this.width); } if (this.autoscrollInterval) { clearInterval(this.autoscrollInterval); this.autoscrollInterval = null; } if (direction.x !== 0 || direction.y !== 0) { this.autoscrollInterval = window.setInterval(() => { const offset = { left: 1 * speed.x * direction.x, top: 1 * speed.y * direction.y, }; if (this.useWindowAsScrollContainer) { this._window.scrollBy(offset.left, offset.top); } else { this.scrollContainer.scrollTop += offset.top; this.scrollContainer.scrollLeft += offset.left; } this.translate.x += offset.left; this.translate.y += offset.top; this.animateNodes(); }, 5); } }, }, }); // Export Sortable Element Handle Directive const HandleDirective = { beforeMount(el) { el.sortableHandle = true; }, }; const SlickItem = vue.defineComponent({ name: 'SlickItem', mixins: [ElementMixin], props: { tag: { type: String, default: 'div', }, }, render() { var _a, _b; return vue.h(this.tag, (_b = (_a = this.$slots).default) === null || _b === void 0 ? void 0 : _b.call(_a)); }, }); const SlickList = vue.defineComponent({ name: 'SlickList', mixins: [ContainerMixin], props: { tag: { type: String, default: 'div', }, itemKey: { type: [String, Function], default: 'id', }, }, render() { var _a, _b; if (this.$slots.item) { return vue.h(this.tag, this.list.map((item, index) => { let key; if (item == null) { return; } else if (typeof this.itemKey === 'function') { key = this.itemKey(item); } else if (typeof item === 'object' && hasOwnProperty(item, this.itemKey) && typeof item[this.itemKey] == 'string') { key = item[this.itemKey]; } else if (typeof item === 'string') { key = item; } else { throw new Error('Cannot find key for item, use the item-key prop and pass a function or string'); } return vue.h(SlickItem, { key, index, }, { default: () => { var _a, _b; return (_b = (_a = this.$slots).item) === null || _b === void 0 ? void 0 : _b.call(_a, { item, index }); }, }); })); } return vue.h(this.tag, (_b = (_a = this.$slots).default) === null || _b === void 0 ? void 0 : _b.call(_a)); }, }); const DragHandle = vue.defineComponent({ props: { tag: { type: String, default: 'span', }, }, mounted() { this.$el.sortableHandle = true; }, render() { var _a, _b; return vue.h(this.tag, (_b = (_a = this.$slots).default) === null || _b === void 0 ? void 0 : _b.call(_a)); }, }); let containerIDCounter = 1; /** * Always allow when dest === source * Defer to 'dest.accept()' if it is a function * Allow any group in the accept lists * Deny any group in the block list * Allow the same group by default, this can be overridden with the block prop */ function canAcceptElement(dest, source, payload) { if (source.id === dest.id) return true; if (dest.block && dest.block.includes(source.group)) return false; if (typeof dest.accept === 'function') { return dest.accept({ dest, source, payload }); } if (typeof dest.accept === 'boolean') { return dest.accept; } if (dest.accept && dest.accept.includes(source.group)) return true; if (dest.group === source.group) return true; return false; } function findClosestDest({ x, y }, refs, currentDest) { // Quickly check if we are within the bounds of the current destination if (isPointWithinRect({ x, y }, currentDest.container.getBoundingClientRect())) { return currentDest; } let closest = null; let minDistance = Infinity; for (let i = 0; i < refs.length; i++) { const ref = refs[i]; const rect = ref.container.getBoundingClientRect(); const isWithin = isPointWithinRect({ x, y }, rect); if (isWithin) { // If we are within another destination, stop here return ref; } const center = getRectCenter(rect); const distance = getDistance(x, y, center.x, center.y); if (distance < minDistance) { closest = ref; minDistance = distance; } } // Try to guess the closest destination return closest; } class SlicksortHub { constructor() { this.helper = null; this.ghost = null; this.refs = []; this.source = null; this.dest = null; } getId() { return '' + containerIDCounter++; } isSource({ id }) { var _a; return ((_a = this.source) === null || _a === void 0 ? void 0 : _a.id) === id; } getSource() { return this.source; } isDest({ id }) { var _a; return ((_a = this.dest) === null || _a === void 0 ? void 0 : _a.id) === id; } getDest() { return this.dest; } addContainer(ref) { this.refs.push(ref); } removeContainer(ref) { this.refs = this.refs.filter((c) => c.id !== ref.id); } sortStart(ref) { this.source = ref; this.dest = ref; } handleSortMove(e, payload) { var _a, _b, _c, _d; const dest = this.dest; const source = this.source; if (!dest || !source) return; const refs = this.refs; const pointer = getPointerOffset(e, 'client'); const newDest = findClosestDest(pointer, refs, dest) || dest; if (dest.id !== newDest.id && canAcceptElement(newDest, source, payload)) { this.dest = newDest; dest.handleDragOut(); newDest.handleDragIn(e, this.ghost, this.helper); } if (dest.id !== ((_a = this.source) === null || _a === void 0 ? void 0 : _a.id)) { (_b = this.dest) === null || _b === void 0 ? void 0