1 | import Sortable from "sortablejs";
|
2 | import { insertNodeAt, camelize, console, removeNode } from "./util/helper";
|
3 |
|
4 | function buildAttribute(object, propName, value) {
|
5 | if (value === undefined) {
|
6 | return object;
|
7 | }
|
8 | object = object || {};
|
9 | object[propName] = value;
|
10 | return object;
|
11 | }
|
12 |
|
13 | function computeVmIndex(vnodes, element) {
|
14 | return vnodes.map(elt => elt.elm).indexOf(element);
|
15 | }
|
16 |
|
17 | function computeIndexes(slots, children, isTransition, footerOffset) {
|
18 | if (!slots) {
|
19 | return [];
|
20 | }
|
21 |
|
22 | const elmFromNodes = slots.map(elt => elt.elm);
|
23 | const footerIndex = children.length - footerOffset;
|
24 | const rawIndexes = [...children].map((elt, idx) =>
|
25 | idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt)
|
26 | );
|
27 | return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes;
|
28 | }
|
29 |
|
30 | function emit(evtName, evtData) {
|
31 | this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
|
32 | }
|
33 |
|
34 | function delegateAndEmit(evtName) {
|
35 | return evtData => {
|
36 | if (this.realList !== null) {
|
37 | this["onDrag" + evtName](evtData);
|
38 | }
|
39 | emit.call(this, evtName, evtData);
|
40 | };
|
41 | }
|
42 |
|
43 | function isTransitionName(name) {
|
44 | return ["transition-group", "TransitionGroup"].includes(name);
|
45 | }
|
46 |
|
47 | function isTransition(slots) {
|
48 | if (!slots || slots.length !== 1) {
|
49 | return false;
|
50 | }
|
51 | const [{ componentOptions }] = slots;
|
52 | if (!componentOptions) {
|
53 | return false;
|
54 | }
|
55 | return isTransitionName(componentOptions.tag);
|
56 | }
|
57 |
|
58 | function getSlot(slot, scopedSlot, key) {
|
59 | return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined);
|
60 | }
|
61 |
|
62 | function computeChildrenAndOffsets(children, slot, scopedSlot) {
|
63 | let headerOffset = 0;
|
64 | let footerOffset = 0;
|
65 | const header = getSlot(slot, scopedSlot, "header");
|
66 | if (header) {
|
67 | headerOffset = header.length;
|
68 | children = children ? [...header, ...children] : [...header];
|
69 | }
|
70 | const footer = getSlot(slot, scopedSlot, "footer");
|
71 | if (footer) {
|
72 | footerOffset = footer.length;
|
73 | children = children ? [...children, ...footer] : [...footer];
|
74 | }
|
75 | return { children, headerOffset, footerOffset };
|
76 | }
|
77 |
|
78 | function getComponentAttributes($attrs, componentData) {
|
79 | let attributes = null;
|
80 | const update = (name, value) => {
|
81 | attributes = buildAttribute(attributes, name, value);
|
82 | };
|
83 | const attrs = Object.keys($attrs)
|
84 | .filter(key => key === "id" || key.startsWith("data-"))
|
85 | .reduce((res, key) => {
|
86 | res[key] = $attrs[key];
|
87 | return res;
|
88 | }, {});
|
89 | update("attrs", attrs);
|
90 |
|
91 | if (!componentData) {
|
92 | return attributes;
|
93 | }
|
94 | const { on, props, attrs: componentDataAttrs } = componentData;
|
95 | update("on", on);
|
96 | update("props", props);
|
97 | Object.assign(attributes.attrs, componentDataAttrs);
|
98 | return attributes;
|
99 | }
|
100 |
|
101 | const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
|
102 | const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];
|
103 | const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map(
|
104 | evt => "on" + evt
|
105 | );
|
106 | var draggingElement = null;
|
107 |
|
108 | const props = {
|
109 | options: Object,
|
110 | list: {
|
111 | type: Array,
|
112 | required: false,
|
113 | default: null
|
114 | },
|
115 | value: {
|
116 | type: Array,
|
117 | required: false,
|
118 | default: null
|
119 | },
|
120 | noTransitionOnDrag: {
|
121 | type: Boolean,
|
122 | default: false
|
123 | },
|
124 | clone: {
|
125 | type: Function,
|
126 | default: original => {
|
127 | return original;
|
128 | }
|
129 | },
|
130 | element: {
|
131 | type: String,
|
132 | default: "div"
|
133 | },
|
134 | tag: {
|
135 | type: String,
|
136 | default: null
|
137 | },
|
138 | move: {
|
139 | type: Function,
|
140 | default: null
|
141 | },
|
142 | componentData: {
|
143 | type: Object,
|
144 | required: false,
|
145 | default: null
|
146 | }
|
147 | };
|
148 |
|
149 | const draggableComponent = {
|
150 | name: "draggable",
|
151 |
|
152 | inheritAttrs: false,
|
153 |
|
154 | props,
|
155 |
|
156 | data() {
|
157 | return {
|
158 | transitionMode: false,
|
159 | noneFunctionalComponentMode: false
|
160 | };
|
161 | },
|
162 |
|
163 | render(h) {
|
164 | const slots = this.$slots.default;
|
165 | this.transitionMode = isTransition(slots);
|
166 | const { children, headerOffset, footerOffset } = computeChildrenAndOffsets(
|
167 | slots,
|
168 | this.$slots,
|
169 | this.$scopedSlots
|
170 | );
|
171 | this.headerOffset = headerOffset;
|
172 | this.footerOffset = footerOffset;
|
173 | const attributes = getComponentAttributes(this.$attrs, this.componentData);
|
174 | return h(this.getTag(), attributes, children);
|
175 | },
|
176 |
|
177 | created() {
|
178 | if (this.list !== null && this.value !== null) {
|
179 | console.error(
|
180 | "Value and list props are mutually exclusive! Please set one or another."
|
181 | );
|
182 | }
|
183 |
|
184 | if (this.element !== "div") {
|
185 | console.warn(
|
186 | "Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"
|
187 | );
|
188 | }
|
189 |
|
190 | if (this.options !== undefined) {
|
191 | console.warn(
|
192 | "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"
|
193 | );
|
194 | }
|
195 | },
|
196 |
|
197 | mounted() {
|
198 | this.noneFunctionalComponentMode =
|
199 | this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() &&
|
200 | !this.getIsFunctional();
|
201 | if (this.noneFunctionalComponentMode && this.transitionMode) {
|
202 | throw new Error(
|
203 | `Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`
|
204 | );
|
205 | }
|
206 | const optionsAdded = {};
|
207 | eventsListened.forEach(elt => {
|
208 | optionsAdded["on" + elt] = delegateAndEmit.call(this, elt);
|
209 | });
|
210 |
|
211 | eventsToEmit.forEach(elt => {
|
212 | optionsAdded["on" + elt] = emit.bind(this, elt);
|
213 | });
|
214 |
|
215 | const attributes = Object.keys(this.$attrs).reduce((res, key) => {
|
216 | res[camelize(key)] = this.$attrs[key];
|
217 | return res;
|
218 | }, {});
|
219 |
|
220 | const options = Object.assign({}, this.options, attributes, optionsAdded, {
|
221 | onMove: (evt, originalEvent) => {
|
222 | return this.onDragMove(evt, originalEvent);
|
223 | }
|
224 | });
|
225 | !("draggable" in options) && (options.draggable = ">*");
|
226 | this._sortable = new Sortable(this.rootContainer, options);
|
227 | this.computeIndexes();
|
228 | },
|
229 |
|
230 | beforeDestroy() {
|
231 | if (this._sortable !== undefined) this._sortable.destroy();
|
232 | },
|
233 |
|
234 | computed: {
|
235 | rootContainer() {
|
236 | return this.transitionMode ? this.$el.children[0] : this.$el;
|
237 | },
|
238 |
|
239 | realList() {
|
240 | return this.list ? this.list : this.value;
|
241 | }
|
242 | },
|
243 |
|
244 | watch: {
|
245 | options: {
|
246 | handler(newOptionValue) {
|
247 | this.updateOptions(newOptionValue);
|
248 | },
|
249 | deep: true
|
250 | },
|
251 |
|
252 | $attrs: {
|
253 | handler(newOptionValue) {
|
254 | this.updateOptions(newOptionValue);
|
255 | },
|
256 | deep: true
|
257 | },
|
258 |
|
259 | realList() {
|
260 | this.computeIndexes();
|
261 | }
|
262 | },
|
263 |
|
264 | methods: {
|
265 | getIsFunctional() {
|
266 | const { fnOptions } = this._vnode;
|
267 | return fnOptions && fnOptions.functional;
|
268 | },
|
269 |
|
270 | getTag() {
|
271 | return this.tag || this.element;
|
272 | },
|
273 |
|
274 | updateOptions(newOptionValue) {
|
275 | for (var property in newOptionValue) {
|
276 | const value = camelize(property);
|
277 | if (readonlyProperties.indexOf(value) === -1) {
|
278 | this._sortable.option(value, newOptionValue[property]);
|
279 | }
|
280 | }
|
281 | },
|
282 |
|
283 | getChildrenNodes() {
|
284 | if (this.noneFunctionalComponentMode) {
|
285 | return this.$children[0].$slots.default;
|
286 | }
|
287 | const rawNodes = this.$slots.default;
|
288 | return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes;
|
289 | },
|
290 |
|
291 | computeIndexes() {
|
292 | this.$nextTick(() => {
|
293 | this.visibleIndexes = computeIndexes(
|
294 | this.getChildrenNodes(),
|
295 | this.rootContainer.children,
|
296 | this.transitionMode,
|
297 | this.footerOffset
|
298 | );
|
299 | });
|
300 | },
|
301 |
|
302 | getUnderlyingVm(htmlElt) {
|
303 | const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt);
|
304 | if (index === -1) {
|
305 |
|
306 |
|
307 | return null;
|
308 | }
|
309 | const element = this.realList[index];
|
310 | return { index, element };
|
311 | },
|
312 |
|
313 | getUnderlyingPotencialDraggableComponent({ __vue__: vue }) {
|
314 | if (
|
315 | !vue ||
|
316 | !vue.$options ||
|
317 | !isTransitionName(vue.$options._componentTag)
|
318 | ) {
|
319 | if (
|
320 | !("realList" in vue) &&
|
321 | vue.$children.length === 1 &&
|
322 | "realList" in vue.$children[0]
|
323 | )
|
324 | return vue.$children[0];
|
325 |
|
326 | return vue;
|
327 | }
|
328 | return vue.$parent;
|
329 | },
|
330 |
|
331 | emitChanges(evt) {
|
332 | this.$nextTick(() => {
|
333 | this.$emit("change", evt);
|
334 | });
|
335 | },
|
336 |
|
337 | alterList(onList) {
|
338 | if (this.list) {
|
339 | onList(this.list);
|
340 | return;
|
341 | }
|
342 | const newList = [...this.value];
|
343 | onList(newList);
|
344 | this.$emit("input", newList);
|
345 | },
|
346 |
|
347 | spliceList() {
|
348 | const spliceList = list => list.splice(...arguments);
|
349 | this.alterList(spliceList);
|
350 | },
|
351 |
|
352 | updatePosition(oldIndex, newIndex) {
|
353 | const updatePosition = list =>
|
354 | list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]);
|
355 | this.alterList(updatePosition);
|
356 | },
|
357 |
|
358 | getRelatedContextFromMoveEvent({ to, related }) {
|
359 | const component = this.getUnderlyingPotencialDraggableComponent(to);
|
360 | if (!component) {
|
361 | return { component };
|
362 | }
|
363 | const list = component.realList;
|
364 | const context = { list, component };
|
365 | if (to !== related && list && component.getUnderlyingVm) {
|
366 | const destination = component.getUnderlyingVm(related);
|
367 | if (destination) {
|
368 | return Object.assign(destination, context);
|
369 | }
|
370 | }
|
371 | return context;
|
372 | },
|
373 |
|
374 | getVmIndex(domIndex) {
|
375 | const indexes = this.visibleIndexes;
|
376 | const numberIndexes = indexes.length;
|
377 | return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex];
|
378 | },
|
379 |
|
380 | getComponent() {
|
381 | return this.$slots.default[0].componentInstance;
|
382 | },
|
383 |
|
384 | resetTransitionData(index) {
|
385 | if (!this.noTransitionOnDrag || !this.transitionMode) {
|
386 | return;
|
387 | }
|
388 | var nodes = this.getChildrenNodes();
|
389 | nodes[index].data = null;
|
390 | const transitionContainer = this.getComponent();
|
391 | transitionContainer.children = [];
|
392 | transitionContainer.kept = undefined;
|
393 | },
|
394 |
|
395 | onDragStart(evt) {
|
396 | this.context = this.getUnderlyingVm(evt.item);
|
397 | evt.item._underlying_vm_ = this.clone(this.context.element);
|
398 | draggingElement = evt.item;
|
399 | },
|
400 |
|
401 | onDragAdd(evt) {
|
402 | const element = evt.item._underlying_vm_;
|
403 | if (element === undefined) {
|
404 | return;
|
405 | }
|
406 | removeNode(evt.item);
|
407 | const newIndex = this.getVmIndex(evt.newIndex);
|
408 | this.spliceList(newIndex, 0, element);
|
409 | this.computeIndexes();
|
410 | const added = { element, newIndex };
|
411 | this.emitChanges({ added });
|
412 | },
|
413 |
|
414 | onDragRemove(evt) {
|
415 | insertNodeAt(this.rootContainer, evt.item, evt.oldIndex);
|
416 | if (evt.pullMode === "clone") {
|
417 | removeNode(evt.clone);
|
418 | return;
|
419 | }
|
420 | const oldIndex = this.context.index;
|
421 | this.spliceList(oldIndex, 1);
|
422 | const removed = { element: this.context.element, oldIndex };
|
423 | this.resetTransitionData(oldIndex);
|
424 | this.emitChanges({ removed });
|
425 | },
|
426 |
|
427 | onDragUpdate(evt) {
|
428 | removeNode(evt.item);
|
429 | insertNodeAt(evt.from, evt.item, evt.oldIndex);
|
430 | const oldIndex = this.context.index;
|
431 | const newIndex = this.getVmIndex(evt.newIndex);
|
432 | this.updatePosition(oldIndex, newIndex);
|
433 | const moved = { element: this.context.element, oldIndex, newIndex };
|
434 | this.emitChanges({ moved });
|
435 | },
|
436 |
|
437 | updateProperty(evt, propertyName) {
|
438 | evt.hasOwnProperty(propertyName) &&
|
439 | (evt[propertyName] += this.headerOffset);
|
440 | },
|
441 |
|
442 | computeFutureIndex(relatedContext, evt) {
|
443 | if (!relatedContext.element) {
|
444 | return 0;
|
445 | }
|
446 | const domChildren = [...evt.to.children].filter(
|
447 | el => el.style["display"] !== "none"
|
448 | );
|
449 | const currentDOMIndex = domChildren.indexOf(evt.related);
|
450 | const currentIndex = relatedContext.component.getVmIndex(currentDOMIndex);
|
451 | const draggedInList = domChildren.indexOf(draggingElement) !== -1;
|
452 | return draggedInList || !evt.willInsertAfter
|
453 | ? currentIndex
|
454 | : currentIndex + 1;
|
455 | },
|
456 |
|
457 | onDragMove(evt, originalEvent) {
|
458 | const onMove = this.move;
|
459 | if (!onMove || !this.realList) {
|
460 | return true;
|
461 | }
|
462 |
|
463 | const relatedContext = this.getRelatedContextFromMoveEvent(evt);
|
464 | const draggedContext = this.context;
|
465 | const futureIndex = this.computeFutureIndex(relatedContext, evt);
|
466 | Object.assign(draggedContext, { futureIndex });
|
467 | const sendEvt = Object.assign({}, evt, {
|
468 | relatedContext,
|
469 | draggedContext
|
470 | });
|
471 | return onMove(sendEvt, originalEvent);
|
472 | },
|
473 |
|
474 | onDragEnd() {
|
475 | this.computeIndexes();
|
476 | draggingElement = null;
|
477 | }
|
478 | }
|
479 | };
|
480 |
|
481 | if (typeof window !== "undefined" && "Vue" in window) {
|
482 | window.Vue.component("draggable", draggableComponent);
|
483 | }
|
484 |
|
485 | export default draggableComponent;
|