UNPKG

13.5 kBJavaScriptView Raw
1import Sortable from "sortablejs";
2import { insertNodeAt, camelize, console, removeNode } from "./util/helper";
3
4function buildAttribute(object, propName, value) {
5 if (value === undefined) {
6 return object;
7 }
8 object = object || {};
9 object[propName] = value;
10 return object;
11}
12
13function computeVmIndex(vnodes, element) {
14 return vnodes.map(elt => elt.elm).indexOf(element);
15}
16
17function 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
30function emit(evtName, evtData) {
31 this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData));
32}
33
34function 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
43function isTransitionName(name) {
44 return ["transition-group", "TransitionGroup"].includes(name);
45}
46
47function 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
58function getSlot(slot, scopedSlot, key) {
59 return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined);
60}
61
62function 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
78function 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
101const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
102const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];
103const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map(
104 evt => "on" + evt
105);
106var draggingElement = null;
107
108const 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
149const 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 //Edge case during move callback: related element might be
306 //an element different from collection
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
481if (typeof window !== "undefined" && "Vue" in window) {
482 window.Vue.component("draggable", draggableComponent);
483}
484
485export default draggableComponent;