import Sortable from "sortablejs"; import { insertNodeAt, removeNode } from "./util/htmlHelper"; import { console } from "./util/console"; import { getComponentAttributes, createSortableOption, getValidSortableEntries } from "./core/componentBuilderHelper"; import { computeComponentStructure } from "./core/renderHelper"; import { events } from "./core/sortableEvents"; import { h, defineComponent, nextTick } from "vue"; function emit(evtName, evtData) { nextTick(() => this.$emit(evtName.toLowerCase(), evtData)); } function manage(evtName) { return (evtData, originalElement) => { if (this.realList !== null) { return this[`onDrag${evtName}`](evtData, originalElement); } }; } function manageAndEmit(evtName) { const delegateCallBack = manage.call(this, evtName); return (evtData, originalElement) => { delegateCallBack.call(this, evtData, originalElement); emit.call(this, evtName, evtData); }; } let draggingElement = null; const props = { list: { type: Array, required: false, default: null }, modelValue: { type: Array, required: false, default: null }, itemKey: { type: [String, Function], required: true }, clone: { type: Function, default: original => { return original; } }, tag: { type: String, default: "div" }, move: { type: Function, default: null }, componentData: { type: Object, required: false, default: null } }; const emits = [ "update:modelValue", "change", ...[...events.manageAndEmit, ...events.emit].map(evt => evt.toLowerCase()) ]; const draggableComponent = defineComponent({ name: "draggable", inheritAttrs: false, props, emits, data() { return { error: false }; }, render() { try { this.error = false; const { $slots, $attrs, tag, componentData, realList, getKey } = this; const componentStructure = computeComponentStructure({ $slots, tag, realList, getKey }); this.componentStructure = componentStructure; const attributes = getComponentAttributes({ $attrs, componentData }); return componentStructure.render(h, attributes); } catch (err) { this.error = true; return h("pre", { style: { color: "red" } }, err.stack); } }, created() { if (this.list !== null && this.modelValue !== null) { console.error( "modelValue and list props are mutually exclusive! Please set one or another." ); } }, mounted() { if (this.error) { return; } const { $attrs, $el, componentStructure } = this; componentStructure.updated(); const sortableOptions = createSortableOption({ $attrs, callBackBuilder: { manageAndEmit: event => manageAndEmit.call(this, event), emit: event => emit.bind(this, event), manage: event => manage.call(this, event) } }); const targetDomElement = $el.nodeType === 1 ? $el : $el.parentElement; this._sortable = new Sortable(targetDomElement, sortableOptions); this.targetDomElement = targetDomElement; targetDomElement.__draggable_component__ = this; }, updated() { this.componentStructure.updated(); }, beforeUnmount() { if (this._sortable !== undefined) this._sortable.destroy(); }, computed: { realList() { const { list } = this; return list ? list : this.modelValue; }, getKey() { const { itemKey } = this; if (typeof itemKey === "function") { return itemKey; } return element => element[itemKey]; } }, watch: { $attrs: { handler(newOptionValue) { const { _sortable } = this; if (!_sortable) return; getValidSortableEntries(newOptionValue).forEach(([key, value]) => { _sortable.option(key, value); }); }, deep: true } }, methods: { getUnderlyingVm(domElement) { return this.componentStructure.getUnderlyingVm(domElement) || null; }, getUnderlyingPotencialDraggableComponent(htmElement) { //TODO check case where you need to see component children return htmElement.__draggable_component__; }, emitChanges(evt) { nextTick(() => this.$emit("change", evt)); }, alterList(onList) { if (this.list) { onList(this.list); return; } const newList = [...this.modelValue]; onList(newList); this.$emit("update:modelValue", newList); }, spliceList() { // @ts-ignore const spliceList = list => list.splice(...arguments); this.alterList(spliceList); }, updatePosition(oldIndex, newIndex) { const updatePosition = list => list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]); this.alterList(updatePosition); }, getRelatedContextFromMoveEvent({ to, related }) { const component = this.getUnderlyingPotencialDraggableComponent(to); if (!component) { return { component }; } const list = component.realList; const context = { list, component }; if (to !== related && list) { const destination = component.getUnderlyingVm(related) || {}; return { ...destination, ...context }; } return context; }, getVmIndexFromDomIndex(domIndex) { return this.componentStructure.getVmIndexFromDomIndex( domIndex, this.targetDomElement ); }, onDragStart(evt) { this.context = this.getUnderlyingVm(evt.item); evt.item._underlying_vm_ = this.clone(this.context.element); draggingElement = evt.item; }, onDragAdd(evt) { const element = evt.item._underlying_vm_; if (element === undefined) { return; } removeNode(evt.item); const newIndex = this.getVmIndexFromDomIndex(evt.newIndex); // @ts-ignore this.spliceList(newIndex, 0, element); const added = { element, newIndex }; this.emitChanges({ added }); }, onDragRemove(evt) { insertNodeAt(this.$el, evt.item, evt.oldIndex); if (evt.pullMode === "clone") { removeNode(evt.clone); return; } const { index: oldIndex, element } = this.context; // @ts-ignore this.spliceList(oldIndex, 1); const removed = { element, oldIndex }; this.emitChanges({ removed }); }, onDragUpdate(evt) { removeNode(evt.item); insertNodeAt(evt.from, evt.item, evt.oldIndex); const oldIndex = this.context.index; const newIndex = this.getVmIndexFromDomIndex(evt.newIndex); this.updatePosition(oldIndex, newIndex); const moved = { element: this.context.element, oldIndex, newIndex }; this.emitChanges({ moved }); }, computeFutureIndex(relatedContext, evt) { if (!relatedContext.element) { return 0; } const domChildren = [...evt.to.children].filter( el => el.style["display"] !== "none" ); const currentDomIndex = domChildren.indexOf(evt.related); const currentIndex = relatedContext.component.getVmIndexFromDomIndex( currentDomIndex ); const draggedInList = domChildren.indexOf(draggingElement) !== -1; return draggedInList || !evt.willInsertAfter ? currentIndex : currentIndex + 1; }, onDragMove(evt, originalEvent) { const { move, realList } = this; if (!move || !realList) { return true; } const relatedContext = this.getRelatedContextFromMoveEvent(evt); const futureIndex = this.computeFutureIndex(relatedContext, evt); const draggedContext = { ...this.context, futureIndex }; const sendEvent = { ...evt, relatedContext, draggedContext }; return move(sendEvent, originalEvent); }, onDragEnd() { draggingElement = null; } } }); export default draggableComponent;