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 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 |
|
54 | function 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 |
|
68 | function 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 |
|
91 | const eventsListened = ["Start", "Add", "Remove", "Update", "End"];
|
92 | const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"];
|
93 | const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map(
|
94 | evt => "on" + evt
|
95 | );
|
96 | var draggingElement = null;
|
97 |
|
98 | const 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 |
|
139 | const 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 |
|
295 |
|
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 |
|
463 | if (typeof window !== "undefined" && "Vue" in window) {
|
464 | window.Vue.component("draggable", draggableComponent);
|
465 | }
|
466 |
|
467 | export default draggableComponent;
|