UNPKG

9.81 kBJavaScriptView Raw
1import Animate from '../mixin/animate';
2import Class from '../mixin/class';
3import {$$, addClass, after, assign, append, attr, before, clamp, css, getEventPos, height, includes, index, isEmpty, isInput, offset, off, on, pointerDown, pointerMove, pointerUp, remove, removeClass, scrollTop, toggleClass, toNodes, trigger, within} from 'uikit-util';
4
5export default {
6
7 mixins: [Class, Animate],
8
9 props: {
10 group: String,
11 threshold: Number,
12 clsItem: String,
13 clsPlaceholder: String,
14 clsDrag: String,
15 clsDragState: String,
16 clsBase: String,
17 clsNoDrag: String,
18 clsEmpty: String,
19 clsCustom: String,
20 handle: String
21 },
22
23 data: {
24 group: false,
25 threshold: 5,
26 clsItem: 'uk-sortable-item',
27 clsPlaceholder: 'uk-sortable-placeholder',
28 clsDrag: 'uk-sortable-drag',
29 clsDragState: 'uk-drag',
30 clsBase: 'uk-sortable',
31 clsNoDrag: 'uk-sortable-nodrag',
32 clsEmpty: 'uk-sortable-empty',
33 clsCustom: '',
34 handle: false
35 },
36
37 created() {
38 ['init', 'start', 'move', 'end'].forEach(key => {
39 const fn = this[key];
40 this[key] = e => {
41 this.scrollY = window.pageYOffset;
42 const {x, y} = getEventPos(e, 'page');
43 this.pos = {x, y};
44
45 fn(e);
46 };
47 });
48 },
49
50 events: {
51
52 name: pointerDown,
53 passive: false,
54 handler: 'init'
55
56 },
57
58 update: {
59
60 write() {
61
62 if (this.clsEmpty) {
63 toggleClass(this.$el, this.clsEmpty, isEmpty(this.$el.children));
64 }
65
66 css(this.handle ? $$(this.handle, this.$el) : this.$el.children, {touchAction: 'none', userSelect: 'none'});
67
68 if (this.drag) {
69
70 // clamp to viewport
71 const {right, bottom} = offset(window);
72 offset(this.drag, {
73 top: clamp(this.pos.y + this.origin.top, 0, bottom - this.drag.offsetHeight),
74 left: clamp(this.pos.x + this.origin.left, 0, right - this.drag.offsetWidth)
75 });
76
77 trackScroll(this.pos);
78
79 }
80
81 }
82
83 },
84
85 methods: {
86
87 init(e) {
88
89 const {target, button, defaultPrevented} = e;
90 const [placeholder] = toNodes(this.$el.children).filter(el => within(target, el));
91
92 if (!placeholder
93 || defaultPrevented
94 || button > 0
95 || isInput(target)
96 || within(target, `.${this.clsNoDrag}`)
97 || this.handle && !within(target, this.handle)
98 ) {
99 return;
100 }
101
102 e.preventDefault();
103
104 this.touched = [this];
105 this.placeholder = placeholder;
106 this.origin = assign({target, index: index(placeholder)}, this.pos);
107
108 on(document, pointerMove, this.move);
109 on(document, pointerUp, this.end);
110 on(window, 'scroll', this.scroll);
111
112 if (!this.threshold) {
113 this.start(e);
114 }
115
116 },
117
118 start(e) {
119
120 this.drag = append(this.$container, this.placeholder.outerHTML.replace(/^<li/i, '<div').replace(/li>$/i, 'div>'));
121
122 css(this.drag, assign({
123 boxSizing: 'border-box',
124 width: this.placeholder.offsetWidth,
125 height: this.placeholder.offsetHeight,
126 overflow: 'hidden'
127 }, css(this.placeholder, ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom'])));
128 attr(this.drag, 'uk-no-boot', '');
129 addClass(this.drag, this.clsDrag, this.clsCustom);
130
131 height(this.drag.firstElementChild, height(this.placeholder.firstElementChild));
132
133 const {left, top} = offset(this.placeholder);
134 assign(this.origin, {left: left - this.pos.x, top: top - this.pos.y});
135
136 addClass(this.placeholder, this.clsPlaceholder);
137 addClass(this.$el.children, this.clsItem);
138 addClass(document.documentElement, this.clsDragState);
139
140 trigger(this.$el, 'start', [this, this.placeholder]);
141
142 this.move(e);
143 },
144
145 move(e) {
146
147 if (!this.drag) {
148
149 if (Math.abs(this.pos.x - this.origin.x) > this.threshold || Math.abs(this.pos.y - this.origin.y) > this.threshold) {
150 this.start(e);
151 }
152
153 return;
154 }
155
156 this.$emit();
157
158 let target = e.type === 'mousemove' ? e.target : document.elementFromPoint(this.pos.x - window.pageXOffset, this.pos.y - window.pageYOffset);
159
160 const sortable = this.getSortable(target);
161 const previous = this.getSortable(this.placeholder);
162 const move = sortable !== previous;
163
164 if (!sortable || within(target, this.placeholder) || move && (!sortable.group || sortable.group !== previous.group)) {
165 return;
166 }
167
168 target = sortable.$el === target.parentNode && target || toNodes(sortable.$el.children).filter(element => within(target, element))[0];
169
170 if (move) {
171 previous.remove(this.placeholder);
172 } else if (!target) {
173 return;
174 }
175
176 sortable.insert(this.placeholder, target);
177
178 if (!includes(this.touched, sortable)) {
179 this.touched.push(sortable);
180 }
181
182 },
183
184 end(e) {
185
186 off(document, pointerMove, this.move);
187 off(document, pointerUp, this.end);
188 off(window, 'scroll', this.scroll);
189
190 if (!this.drag) {
191 if (e.type === 'touchend') {
192 e.target.click();
193 }
194
195 return;
196 }
197
198 untrackScroll();
199
200 const sortable = this.getSortable(this.placeholder);
201
202 if (this === sortable) {
203 if (this.origin.index !== index(this.placeholder)) {
204 trigger(this.$el, 'moved', [this, this.placeholder]);
205 }
206 } else {
207 trigger(sortable.$el, 'added', [sortable, this.placeholder]);
208 trigger(this.$el, 'removed', [this, this.placeholder]);
209 }
210
211 trigger(this.$el, 'stop', [this, this.placeholder]);
212
213 remove(this.drag);
214 this.drag = null;
215
216 const classes = this.touched.map(sortable => `${sortable.clsPlaceholder} ${sortable.clsItem}`).join(' ');
217 this.touched.forEach(sortable => removeClass(sortable.$el.children, classes));
218
219 removeClass(document.documentElement, this.clsDragState);
220
221 },
222
223 scroll() {
224 const scroll = window.pageYOffset;
225 if (scroll !== this.scrollY) {
226 this.pos.y += scroll - this.scrollY;
227 this.scrollY = scroll;
228 this.$emit();
229 }
230 },
231
232 insert(element, target) {
233
234 addClass(this.$el.children, this.clsItem);
235
236 const insert = () => {
237
238 if (target) {
239
240 if (!within(element, this.$el) || isPredecessor(element, target)) {
241 before(target, element);
242 } else {
243 after(target, element);
244 }
245
246 } else {
247 append(this.$el, element);
248 }
249
250 };
251
252 if (this.animation) {
253 this.animate(insert);
254 } else {
255 insert();
256 }
257
258 },
259
260 remove(element) {
261
262 if (!within(element, this.$el)) {
263 return;
264 }
265
266 css(this.handle ? $$(this.handle, element) : element, {touchAction: '', userSelect: ''});
267
268 if (this.animation) {
269 this.animate(() => remove(element));
270 } else {
271 remove(element);
272 }
273
274 },
275
276 getSortable(element) {
277 return element && (this.$getComponent(element, 'sortable') || this.getSortable(element.parentNode));
278 }
279
280 }
281
282};
283
284function isPredecessor(element, target) {
285 return element.parentNode === target.parentNode && index(element) > index(target);
286}
287
288let trackTimer;
289function trackScroll({x, y}) {
290
291 clearTimeout(trackTimer);
292
293 scrollParents(document.elementFromPoint(x - window.pageXOffset, y - window.pageYOffset)).some(scrollEl => {
294
295 let {scrollTop: scroll, scrollHeight} = scrollEl;
296
297 if (getScrollingElement() === scrollEl) {
298 scrollEl = window;
299 scrollHeight -= window.innerHeight;
300 }
301
302 const {top, bottom} = offset(scrollEl);
303
304 if (top < y && top + 30 > y) {
305 scroll -= 5;
306 } else if (bottom > y && bottom - 20 < y) {
307 scroll += 5;
308 }
309
310 if (scroll > 0 && scroll < scrollHeight) {
311 return trackTimer = setTimeout(() => {
312 scrollTop(scrollEl, scroll);
313 trackScroll({x, y});
314 }, 10);
315 }
316
317 });
318
319}
320
321function untrackScroll() {
322 clearTimeout(trackTimer);
323}
324
325const overflowRe = /auto|scroll/;
326
327function scrollParents(element) {
328 const scrollEl = getScrollingElement();
329 return parents(element, parent => parent === scrollEl || overflowRe.test(css(parent, 'overflow')));
330}
331
332function parents(element, fn) {
333 const parents = [];
334 do {
335 if (fn(element)) {
336 parents.unshift(element);
337 }
338 } while (element && (element = element.parentElement));
339 return parents;
340}
341
342function getScrollingElement() {
343 return document.scrollingElement || document.documentElement;
344}