uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
445 lines (362 loc) • 11.7 kB
JavaScript
import {
$,
$$,
addClass,
append,
assign,
attr,
before,
children,
css,
dimensions,
findIndex,
getEventPos,
height,
index,
isInput,
isTag,
off,
offsetViewport,
on,
parent,
pointerDown,
pointerMove,
pointerUp,
pointInRect,
remove,
removeClass,
scrollParents,
toggleClass,
Transition,
trigger,
} from 'uikit-util';
import Animate from '../mixin/animate';
import Class from '../mixin/class';
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: {},
},
events: {
name: pointerDown,
passive: false,
handler: 'init',
},
computed: {
target: (_, $el) => ($el.tBodies || [$el])[0],
items() {
return children(this.target);
},
isEmpty() {
return !this.items.length;
},
handles({ handle }, $el) {
return handle ? $$(handle, $el) : this.items;
},
},
watch: {
isEmpty(empty) {
toggleClass(this.target, this.clsEmpty, empty);
},
handles(handles, prev) {
css(prev, { touchAction: '', userSelect: '' });
css(handles, { touchAction: 'none', userSelect: 'none' });
},
},
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) => el.contains(target));
if (
!placeholder ||
defaultPrevented ||
button > 0 ||
isInput(target) ||
target.closest(`.${this.clsNoDrag}`) ||
(this.handle && !target.closest(this.handle))
) {
return;
}
e.preventDefault();
this.pos = getEventPos(e);
this.touched = new Set([this]);
this.placeholder = placeholder;
this.origin = { 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 } = dimensions(this.placeholder);
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: throttle(function (e) {
assign(this.pos, getEventPos(e));
if (
!this.drag &&
(Math.abs(this.pos.x - this.origin.x) > this.threshold ||
Math.abs(this.pos.y - this.origin.y) > this.threshold)
) {
this.start(e);
}
this.$emit('move');
}),
end() {
off(document, pointerMove, this.move);
off(document, pointerUp, this.end);
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;
for (const { clsPlaceholder, clsItem } of this.touched) {
for (const sortable of this.touched) {
removeClass(sortable.items, clsPlaceholder, clsItem);
}
}
this.touched = null;
removeClass(document.documentElement, this.clsDragState);
},
insert(element, target) {
addClass(this.items, this.clsItem);
if (target && target.previousElementSibling !== element) {
this.animate(() => before(target, element));
} else if (!target && this.target.lastElementChild !== element) {
this.animate(() => append(this.target, element));
}
},
remove(element) {
if (this.target.contains(element)) {
this.animate(() => 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 += document.scrollingElement.scrollTop;
const dist = (Date.now() - last) * 0.3;
last = Date.now();
scrollParents(document.elementFromPoint(x, pos.y))
.reverse()
.some((scrollEl) => {
let { scrollTop: scroll, scrollHeight } = scrollEl;
const { top, bottom, height } = offsetViewport(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) {
scrollEl.scrollTop = scroll;
return true;
}
});
}, 15);
}
function untrackScroll() {
clearInterval(trackTimer);
}
function appendDrag(container, element) {
let clone;
if (isTag(element, 'li', 'tr')) {
clone = $('<div>');
append(clone, element.cloneNode(true).children);
for (const attribute of element.getAttributeNames()) {
attr(clone, attribute, element.getAttribute(attribute));
}
} else {
clone = element.cloneNode(true);
}
append(container, clone);
css(clone, 'margin', '0', 'important');
css(clone, {
boxSizing: 'border-box',
width: element.offsetWidth,
height: element.offsetHeight,
padding: css(element, 'padding'),
});
height(clone.firstElementChild, height(element.firstElementChild));
return clone;
}
function findTarget(items, point) {
return items[findIndex(items, (item) => pointInRect(point, dimensions(item)))];
}
function findInsertTarget(list, target, placeholder, x, y, sameList) {
if (!children(list).length) {
return;
}
const rect = dimensions(target);
if (!sameList) {
if (!isHorizontal(list, placeholder)) {
return y < rect.top + rect.height / 2 ? target : target.nextElementSibling;
}
return target;
}
const placeholderRect = dimensions(placeholder);
const sameRow = linesIntersect(
[rect.top, rect.bottom],
[placeholderRect.top, placeholderRect.bottom],
);
const [pointerPos, lengthProp, startProp, endProp] = sameRow
? [x, 'width', 'left', 'right']
: [y, 'height', 'top', '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 = dimensions(el);
return items.slice(i + 1).some((el) => {
const rectB = dimensions(el);
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];
}
function throttle(fn) {
let throttled;
return function (...args) {
if (!throttled) {
throttled = true;
fn.call(this, ...args);
requestAnimationFrame(() => (throttled = false));
}
};
}