uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
345 lines (249 loc) • 9.81 kB
JavaScript
import Animate from '../mixin/animate';
import Class from '../mixin/class';
import {$$, 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';
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
},
created() {
['init', 'start', 'move', 'end'].forEach(key => {
const fn = this[key];
this[key] = e => {
this.scrollY = window.pageYOffset;
const {x, y} = getEventPos(e, 'page');
this.pos = {x, y};
fn(e);
};
});
},
events: {
name: pointerDown,
passive: false,
handler: 'init'
},
update: {
write() {
if (this.clsEmpty) {
toggleClass(this.$el, this.clsEmpty, isEmpty(this.$el.children));
}
css(this.handle ? $$(this.handle, this.$el) : this.$el.children, {touchAction: 'none', userSelect: 'none'});
if (this.drag) {
// clamp to viewport
const {right, bottom} = offset(window);
offset(this.drag, {
top: clamp(this.pos.y + this.origin.top, 0, bottom - this.drag.offsetHeight),
left: clamp(this.pos.x + this.origin.left, 0, right - this.drag.offsetWidth)
});
trackScroll(this.pos);
}
}
},
methods: {
init(e) {
const {target, button, defaultPrevented} = e;
const [placeholder] = toNodes(this.$el.children).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 = [this];
this.placeholder = placeholder;
this.origin = assign({target, index: index(placeholder)}, this.pos);
on(document, pointerMove, this.move);
on(document, pointerUp, this.end);
on(window, 'scroll', this.scroll);
if (!this.threshold) {
this.start(e);
}
},
start(e) {
this.drag = append(this.$container, this.placeholder.outerHTML.replace(/^<li/i, '<div').replace(/li>$/i, 'div>'));
css(this.drag, assign({
boxSizing: 'border-box',
width: this.placeholder.offsetWidth,
height: this.placeholder.offsetHeight,
overflow: 'hidden'
}, css(this.placeholder, ['paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom'])));
attr(this.drag, 'uk-no-boot', '');
addClass(this.drag, this.clsDrag, this.clsCustom);
height(this.drag.firstElementChild, height(this.placeholder.firstElementChild));
const {left, top} = offset(this.placeholder);
assign(this.origin, {left: left - this.pos.x, top: top - this.pos.y});
addClass(this.placeholder, this.clsPlaceholder);
addClass(this.$el.children, this.clsItem);
addClass(document.documentElement, this.clsDragState);
trigger(this.$el, 'start', [this, this.placeholder]);
this.move(e);
},
move(e) {
if (!this.drag) {
if (Math.abs(this.pos.x - this.origin.x) > this.threshold || Math.abs(this.pos.y - this.origin.y) > this.threshold) {
this.start(e);
}
return;
}
this.$emit();
let target = e.type === 'mousemove' ? e.target : document.elementFromPoint(this.pos.x - window.pageXOffset, this.pos.y - window.pageYOffset);
const sortable = this.getSortable(target);
const previous = this.getSortable(this.placeholder);
const move = sortable !== previous;
if (!sortable || within(target, this.placeholder) || move && (!sortable.group || sortable.group !== previous.group)) {
return;
}
target = sortable.$el === target.parentNode && target || toNodes(sortable.$el.children).filter(element => within(target, element))[0];
if (move) {
previous.remove(this.placeholder);
} else if (!target) {
return;
}
sortable.insert(this.placeholder, target);
if (!includes(this.touched, sortable)) {
this.touched.push(sortable);
}
},
end(e) {
off(document, pointerMove, this.move);
off(document, pointerUp, this.end);
off(window, 'scroll', this.scroll);
if (!this.drag) {
if (e.type === 'touchend') {
e.target.click();
}
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;
const classes = this.touched.map(sortable => `${sortable.clsPlaceholder} ${sortable.clsItem}`).join(' ');
this.touched.forEach(sortable => removeClass(sortable.$el.children, classes));
removeClass(document.documentElement, this.clsDragState);
},
scroll() {
const scroll = window.pageYOffset;
if (scroll !== this.scrollY) {
this.pos.y += scroll - this.scrollY;
this.scrollY = scroll;
this.$emit();
}
},
insert(element, target) {
addClass(this.$el.children, this.clsItem);
const insert = () => {
if (target) {
if (!within(element, this.$el) || isPredecessor(element, target)) {
before(target, element);
} else {
after(target, element);
}
} else {
append(this.$el, element);
}
};
if (this.animation) {
this.animate(insert);
} else {
insert();
}
},
remove(element) {
if (!within(element, this.$el)) {
return;
}
css(this.handle ? $$(this.handle, element) : element, {touchAction: '', userSelect: ''});
if (this.animation) {
this.animate(() => remove(element));
} else {
remove(element);
}
},
getSortable(element) {
return element && (this.$getComponent(element, 'sortable') || this.getSortable(element.parentNode));
}
}
};
function isPredecessor(element, target) {
return element.parentNode === target.parentNode && index(element) > index(target);
}
let trackTimer;
function trackScroll({x, y}) {
clearTimeout(trackTimer);
scrollParents(document.elementFromPoint(x - window.pageXOffset, y - window.pageYOffset)).some(scrollEl => {
let {scrollTop: scroll, scrollHeight} = scrollEl;
if (getScrollingElement() === scrollEl) {
scrollEl = window;
scrollHeight -= window.innerHeight;
}
const {top, bottom} = offset(scrollEl);
if (top < y && top + 30 > y) {
scroll -= 5;
} else if (bottom > y && bottom - 20 < y) {
scroll += 5;
}
if (scroll > 0 && scroll < scrollHeight) {
return trackTimer = setTimeout(() => {
scrollTop(scrollEl, scroll);
trackScroll({x, y});
}, 10);
}
});
}
function untrackScroll() {
clearTimeout(trackTimer);
}
const overflowRe = /auto|scroll/;
function scrollParents(element) {
const scrollEl = getScrollingElement();
return parents(element, parent => parent === scrollEl || overflowRe.test(css(parent, 'overflow')));
}
function parents(element, fn) {
const parents = [];
do {
if (fn(element)) {
parents.unshift(element);
}
} while (element && (element = element.parentElement));
return parents;
}
function getScrollingElement() {
return document.scrollingElement || document.documentElement;
}