UNPKG

uikit

Version:

UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.

433 lines (312 loc) • 11.3 kB
import Animate from '../mixin/animate'; import Class from '../mixin/class'; import {$$, addClass, append, assign, before, children, css, findIndex, getEventPos, getViewport, hasTouch, height, index, isEmpty, isInput, off, offset, on, parent, pointerDown, pointerMove, pointerUp, pointInRect, remove, removeClass, scrollParents, scrollTop, toggleClass, Transition, trigger, within} from 'uikit-util'; export default { mixins: [Class, Animate], props: { group: String, threshold: Number, clsItem: String, clsPlaceholder: String, clsDrag: String, clsDragState: String, clsBase: String, clsNoDrag: String, clsEmpty: String, clsCustom: String, handle: String }, data: { group: false, threshold: 5, clsItem: 'uk-sortable-item', clsPlaceholder: 'uk-sortable-placeholder', clsDrag: 'uk-sortable-drag', clsDragState: 'uk-drag', clsBase: 'uk-sortable', clsNoDrag: 'uk-sortable-nodrag', clsEmpty: 'uk-sortable-empty', clsCustom: '', handle: false, pos: {} }, created() { ['init', 'start', 'move', 'end'].forEach(key => { const fn = this[key]; this[key] = e => { assign(this.pos, getEventPos(e)); fn(e); }; }); }, events: { name: pointerDown, passive: false, handler: 'init' }, computed: { target() { return (this.$el.tBodies || [this.$el])[0]; }, items() { return children(this.target); }, isEmpty: { get() { return isEmpty(this.items); }, watch(empty) { toggleClass(this.target, this.clsEmpty, empty); }, immediate: true }, handles: { get({handle}, el) { return handle ? $$(handle, el) : this.items; }, watch(handles, prev) { css(prev, {touchAction: '', userSelect: ''}); css(handles, {touchAction: hasTouch ? 'none' : '', userSelect: 'none'}); // touchAction set to 'none' causes a performance drop in Chrome 80 }, immediate: true } }, update: { write(data) { if (!this.drag || !parent(this.placeholder)) { return; } const {pos: {x, y}, origin: {offsetTop, offsetLeft}, placeholder} = this; css(this.drag, { top: y - offsetTop, left: x - offsetLeft }); const sortable = this.getSortable(document.elementFromPoint(x, y)); if (!sortable) { return; } const {items} = sortable; if (items.some(Transition.inProgress)) { return; } const target = findTarget(items, {x, y}); if (items.length && (!target || target === placeholder)) { return; } const previous = this.getSortable(placeholder); const insertTarget = findInsertTarget(sortable.target, target, placeholder, x, y, sortable === previous && data.moved !== target); if (insertTarget === false) { return; } if (insertTarget && placeholder === insertTarget) { return; } if (sortable !== previous) { previous.remove(placeholder); data.moved = target; } else { delete data.moved; } sortable.insert(placeholder, insertTarget); this.touched.add(sortable); }, events: ['move'] }, methods: { init(e) { const {target, button, defaultPrevented} = e; const [placeholder] = this.items.filter(el => within(target, el)); if (!placeholder || defaultPrevented || button > 0 || isInput(target) || within(target, `.${this.clsNoDrag}`) || this.handle && !within(target, this.handle) ) { return; } e.preventDefault(); this.touched = new Set([this]); this.placeholder = placeholder; this.origin = assign({target, index: index(placeholder)}, this.pos); on(document, pointerMove, this.move); on(document, pointerUp, this.end); if (!this.threshold) { this.start(e); } }, start(e) { this.drag = appendDrag(this.$container, this.placeholder); const {left, top} = this.placeholder.getBoundingClientRect(); assign(this.origin, {offsetLeft: this.pos.x - left, offsetTop: this.pos.y - top}); addClass(this.drag, this.clsDrag, this.clsCustom); addClass(this.placeholder, this.clsPlaceholder); addClass(this.items, this.clsItem); addClass(document.documentElement, this.clsDragState); trigger(this.$el, 'start', [this, this.placeholder]); trackScroll(this.pos); this.move(e); }, move(e) { if (this.drag) { this.$emit('move'); } else if (Math.abs(this.pos.x - this.origin.x) > this.threshold || Math.abs(this.pos.y - this.origin.y) > this.threshold) { this.start(e); } }, end() { off(document, pointerMove, this.move); off(document, pointerUp, this.end); off(window, 'scroll', this.scroll); if (!this.drag) { return; } untrackScroll(); const sortable = this.getSortable(this.placeholder); if (this === sortable) { if (this.origin.index !== index(this.placeholder)) { trigger(this.$el, 'moved', [this, this.placeholder]); } } else { trigger(sortable.$el, 'added', [sortable, this.placeholder]); trigger(this.$el, 'removed', [this, this.placeholder]); } trigger(this.$el, 'stop', [this, this.placeholder]); remove(this.drag); this.drag = null; this.touched.forEach(({clsPlaceholder, clsItem}) => this.touched.forEach(sortable => removeClass(sortable.items, clsPlaceholder, clsItem) ) ); this.touched = null; removeClass(document.documentElement, this.clsDragState); }, insert(element, target) { addClass(this.items, this.clsItem); const insert = () => target ? before(target, element) : append(this.target, element); if (this.animation) { this.animate(insert); } else { insert(); } }, remove(element) { if (!within(element, this.target)) { return; } if (this.animation) { this.animate(() => remove(element)); } else { remove(element); } }, getSortable(element) { do { const sortable = this.$getComponent(element, 'sortable'); if (sortable && (sortable === this || this.group !== false && sortable.group === this.group)) { return sortable; } } while ((element = parent(element))); } } }; let trackTimer; function trackScroll(pos) { let last = Date.now(); trackTimer = setInterval(() => { let {x, y} = pos; y += window.pageYOffset; const dist = (Date.now() - last) * .3; last = Date.now(); scrollParents(document.elementFromPoint(x, pos.y)).reverse().some(scrollEl => { let {scrollTop: scroll, scrollHeight} = scrollEl; const {top, bottom, height} = offset(getViewport(scrollEl)); if (top < y && top + 35 > y) { scroll -= dist; } else if (bottom > y && bottom - 35 < y) { scroll += dist; } else { return; } if (scroll > 0 && scroll < scrollHeight - height) { scrollTop(scrollEl, scroll); return true; } }); }, 15); } function untrackScroll() { clearInterval(trackTimer); } function appendDrag(container, element) { const clone = append(container, element.outerHTML.replace(/(^<)(?:li|tr)|(?:li|tr)(\/>$)/g, '$1div$2')); css(clone, 'margin', '0', 'important'); css(clone, assign({ boxSizing: 'border-box', width: element.offsetWidth, height: element.offsetHeight }, css(element, ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom']))); height(clone.firstElementChild, height(element.firstElementChild)); return clone; } function findTarget(items, point) { return items[findIndex(items, item => pointInRect(point, item.getBoundingClientRect()))]; } function findInsertTarget(list, target, placeholder, x, y, sameList) { if (!children(list).length) { return; } const rect = target.getBoundingClientRect(); if (!sameList) { if (!isHorizontal(list, placeholder)) { return y < rect.top + rect.height / 2 ? target : target.nextElementSibling; } return target; } const placeholderRect = placeholder.getBoundingClientRect(); const sameRow = linesIntersect( [rect.top, rect.bottom], [placeholderRect.top, placeholderRect.bottom] ); const pointerPos = sameRow ? x : y; const lengthProp = sameRow ? 'width' : 'height'; const startProp = sameRow ? 'left' : 'top'; const endProp = sameRow ? 'right' : 'bottom'; const diff = placeholderRect[lengthProp] < rect[lengthProp] ? rect[lengthProp] - placeholderRect[lengthProp] : 0; if (placeholderRect[startProp] < rect[startProp]) { if (diff && pointerPos < rect[startProp] + diff) { return false; } return target.nextElementSibling; } if (diff && pointerPos > rect[endProp] - diff) { return false; } return target; } function isHorizontal(list, placeholder) { const single = children(list).length === 1; if (single) { append(list, placeholder); } const items = children(list); const isHorizontal = items.some((el, i) => { const rectA = el.getBoundingClientRect(); return items.slice(i + 1).some(el => { const rectB = el.getBoundingClientRect(); return !linesIntersect([rectA.left, rectA.right], [rectB.left, rectB.right]); }); }); if (single) { remove(placeholder); } return isHorizontal; } function linesIntersect(lineA, lineB) { return lineA[1] > lineB[0] && lineB[1] > lineA[0]; }