import Sortable from "sortablejs"; import { insertNodeAt, camelize, console, removeNode } from "./util/helper"; function buildAttribute(object, propName, value) { if (value === undefined) { return object; } object = object || {}; object[propName] = value; return object; } function computeVmIndex(vnodes, element) { return vnodes.map(elt => elt.elm).indexOf(element); } function computeIndexes(slots, children, isTransition, footerOffset) { if (!slots) { return []; } const elmFromNodes = slots.map(elt => elt.elm); const footerIndex = children.length - footerOffset; const rawIndexes = [...children].map((elt, idx) => idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt) ); return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes; } function emit(evtName, evtData) { this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData)); } function delegateAndEmit(evtName) { return evtData => { if (this.realList !== null) { this["onDrag" + evtName](evtData); } emit.call(this, evtName, evtData); }; } function isTransitionName(name) { return ["transition-group", "TransitionGroup"].includes(name); } function isTransition(slots) { if (!slots || slots.length !== 1) { return false; } const [{ componentOptions }] = slots; if (!componentOptions) { return false; } return isTransitionName(componentOptions.tag); } function getSlot(slot, scopedSlot, key) { return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined); } function computeChildrenAndOffsets(children, slot, scopedSlot) { let headerOffset = 0; let footerOffset = 0; const header = getSlot(slot, scopedSlot, "header"); if (header) { headerOffset = header.length; children = children ? [...header, ...children] : [...header]; } const footer = getSlot(slot, scopedSlot, "footer"); if (footer) { footerOffset = footer.length; children = children ? [...children, ...footer] : [...footer]; } return { children, headerOffset, footerOffset }; } function getComponentAttributes($attrs, componentData) { let attributes = null; const update = (name, value) => { attributes = buildAttribute(attributes, name, value); }; const attrs = Object.keys($attrs) .filter(key => key === "id" || key.startsWith("data-")) .reduce((res, key) => { res[key] = $attrs[key]; return res; }, {}); update("attrs", attrs); if (!componentData) { return attributes; } const { on, props, attrs: componentDataAttrs } = componentData; update("on", on); update("props", props); Object.assign(attributes.attrs, componentDataAttrs); return attributes; } const eventsListened = ["Start", "Add", "Remove", "Update", "End"]; const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"]; const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map( evt => "on" + evt ); var draggingElement = null; const props = { options: Object, list: { type: Array, required: false, default: null }, value: { type: Array, required: false, default: null }, noTransitionOnDrag: { type: Boolean, default: false }, clone: { type: Function, default: original => { return original; } }, element: { type: String, default: "div" }, tag: { type: String, default: null }, move: { type: Function, default: null }, componentData: { type: Object, required: false, default: null } }; const draggableComponent = { name: "draggable", inheritAttrs: false, props, data() { return { transitionMode: false, noneFunctionalComponentMode: false }; }, render(h) { const slots = this.$slots.default; this.transitionMode = isTransition(slots); const { children, headerOffset, footerOffset } = computeChildrenAndOffsets( slots, this.$slots, this.$scopedSlots ); this.headerOffset = headerOffset; this.footerOffset = footerOffset; const attributes = getComponentAttributes(this.$attrs, this.componentData); return h(this.getTag(), attributes, children); }, created() { if (this.list !== null && this.value !== null) { console.error( "Value and list props are mutually exclusive! Please set one or another." ); } if (this.element !== "div") { console.warn( "Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props" ); } if (this.options !== undefined) { console.warn( "Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props" ); } }, mounted() { this.noneFunctionalComponentMode = this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() && !this.getIsFunctional(); if (this.noneFunctionalComponentMode && this.transitionMode) { throw new Error( `Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}` ); } const optionsAdded = {}; eventsListened.forEach(elt => { optionsAdded["on" + elt] = delegateAndEmit.call(this, elt); }); eventsToEmit.forEach(elt => { optionsAdded["on" + elt] = emit.bind(this, elt); }); const attributes = Object.keys(this.$attrs).reduce((res, key) => { res[camelize(key)] = this.$attrs[key]; return res; }, {}); const options = Object.assign({}, this.options, attributes, optionsAdded, { onMove: (evt, originalEvent) => { return this.onDragMove(evt, originalEvent); } }); !("draggable" in options) && (options.draggable = ">*"); this._sortable = new Sortable(this.rootContainer, options); this.computeIndexes(); }, beforeDestroy() { if (this._sortable !== undefined) this._sortable.destroy(); }, computed: { rootContainer() { return this.transitionMode ? this.$el.children[0] : this.$el; }, realList() { return this.list ? this.list : this.value; } }, watch: { options: { handler(newOptionValue) { this.updateOptions(newOptionValue); }, deep: true }, $attrs: { handler(newOptionValue) { this.updateOptions(newOptionValue); }, deep: true }, realList() { this.computeIndexes(); } }, methods: { getIsFunctional() { const { fnOptions } = this._vnode; return fnOptions && fnOptions.functional; }, getTag() { return this.tag || this.element; }, updateOptions(newOptionValue) { for (var property in newOptionValue) { const value = camelize(property); if (readonlyProperties.indexOf(value) === -1) { this._sortable.option(value, newOptionValue[property]); } } }, getChildrenNodes() { if (this.noneFunctionalComponentMode) { return this.$children[0].$slots.default; } const rawNodes = this.$slots.default; return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes; }, computeIndexes() { this.$nextTick(() => { this.visibleIndexes = computeIndexes( this.getChildrenNodes(), this.rootContainer.children, this.transitionMode, this.footerOffset ); }); }, getUnderlyingVm(htmlElt) { const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt); if (index === -1) { //Edge case during move callback: related element might be //an element different from collection return null; } const element = this.realList[index]; return { index, element }; }, getUnderlyingPotencialDraggableComponent({ __vue__: vue }) { if ( !vue || !vue.$options || !isTransitionName(vue.$options._componentTag) ) { if ( !("realList" in vue) && vue.$children.length === 1 && "realList" in vue.$children[0] ) return vue.$children[0]; return vue; } return vue.$parent; }, emitChanges(evt) { this.$nextTick(() => { this.$emit("change", evt); }); }, alterList(onList) { if (this.list) { onList(this.list); return; } const newList = [...this.value]; onList(newList); this.$emit("input", newList); }, spliceList() { 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 && component.getUnderlyingVm) { const destination = component.getUnderlyingVm(related); if (destination) { return Object.assign(destination, context); } } return context; }, getVmIndex(domIndex) { const indexes = this.visibleIndexes; const numberIndexes = indexes.length; return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex]; }, getComponent() { return this.$slots.default[0].componentInstance; }, resetTransitionData(index) { if (!this.noTransitionOnDrag || !this.transitionMode) { return; } var nodes = this.getChildrenNodes(); nodes[index].data = null; const transitionContainer = this.getComponent(); transitionContainer.children = []; transitionContainer.kept = undefined; }, 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.getVmIndex(evt.newIndex); this.spliceList(newIndex, 0, element); this.computeIndexes(); const added = { element, newIndex }; this.emitChanges({ added }); }, onDragRemove(evt) { insertNodeAt(this.rootContainer, evt.item, evt.oldIndex); if (evt.pullMode === "clone") { removeNode(evt.clone); return; } const oldIndex = this.context.index; this.spliceList(oldIndex, 1); const removed = { element: this.context.element, oldIndex }; this.resetTransitionData(oldIndex); this.emitChanges({ removed }); }, onDragUpdate(evt) { removeNode(evt.item); insertNodeAt(evt.from, evt.item, evt.oldIndex); const oldIndex = this.context.index; const newIndex = this.getVmIndex(evt.newIndex); this.updatePosition(oldIndex, newIndex); const moved = { element: this.context.element, oldIndex, newIndex }; this.emitChanges({ moved }); }, updateProperty(evt, propertyName) { evt.hasOwnProperty(propertyName) && (evt[propertyName] += this.headerOffset); }, 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.getVmIndex(currentDOMIndex); const draggedInList = domChildren.indexOf(draggingElement) !== -1; return draggedInList || !evt.willInsertAfter ? currentIndex : currentIndex + 1; }, onDragMove(evt, originalEvent) { const onMove = this.move; if (!onMove || !this.realList) { return true; } const relatedContext = this.getRelatedContextFromMoveEvent(evt); const draggedContext = this.context; const futureIndex = this.computeFutureIndex(relatedContext, evt); Object.assign(draggedContext, { futureIndex }); const sendEvt = Object.assign({}, evt, { relatedContext, draggedContext }); return onMove(sendEvt, originalEvent); }, onDragEnd() { this.computeIndexes(); draggingElement = null; } } }; if (typeof window !== "undefined" && "Vue" in window) { window.Vue.component("draggable", draggableComponent); } export default draggableComponent;