UNPKG

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