You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
319 lines
7.8 KiB
319 lines
7.8 KiB
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;
|
|
|