@progress/kendo-react-sortable
Version:
React Sortable provides a sortable drag-and-drop functionality to elements within a list. KendoReact Sortable package
403 lines (402 loc) • 16.2 kB
JavaScript
/**
* @license
*-------------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the package root for more information
*-------------------------------------------------------------------------------------------
*/
import * as f from "react";
import m from "prop-types";
import { getter as R, getActiveElement as p, getTabIndex as N, KEYS as P, validatePackage as U, Navigation as M, Draggable as O, WatermarkOverlay as F } from "@progress/kendo-react-common";
import { SortableOnDragStartEvent as B } from "./events/SortableOnDragStartEvent.mjs";
import { SortableOnDragOverEvent as k } from "./events/SortableOnDragOverEvent.mjs";
import { SortableOnDragEndEvent as L } from "./events/SortableOnDragEndEvent.mjs";
import { SortableOnNavigateEvent as W } from "./events/SortableOnNavigateEvent.mjs";
import { provideLocalizationService as K, registerForLocalization as Y } from "@progress/kendo-react-intl";
import { noData as w, messages as X } from "./messages/index.mjs";
import { find as z, findIndex as x, closest as H, isFocusable as _, relativeContextElement as q, hasClasses as G } from "./utils/utils.mjs";
import { packageMetadata as $ } from "./package-metadata.mjs";
const y = 200, I = "data-sortable-id", T = "data-sortable-component", A = "[data-sortable-id]:not(.k-disabled)", j = { [T]: !0 }, b = class b extends f.Component {
constructor(c) {
super(c), this.state = {
clientX: 0,
clientY: 0,
isDragging: !1,
activeId: "",
dragCueWidth: 0,
dragCueHeight: 0
}, this.isRtl = !1, this.itemRefsMap = {}, this.oldSizesMap = {}, this.animatingItemMap = {}, this.draggableRef = null, this.isUnmounted = !1, this.focusActiveId = !1, this.isKeyboardNavigated = !1, this.isDragPrevented = !1, this.showLicenseWatermark = !1, this.windowTimeout = (t) => {
window.setTimeout(() => this.animatingItemMap[t] = !1, y);
}, this.swapItems = (t, e, i) => {
const s = t[e];
return t[e] = t[i], t[i] = s, e = i, e;
}, this.generateNewState = (t, e) => {
const { data: i } = this.props, s = [...i];
if (t > e)
for (let n = t - 1; n >= e; n--) {
const o = i[n];
this.isItemDisabled(o) || (t = this.swapItems(s, t, n));
}
else
for (let n = t + 1; n <= e; n++) {
const o = i[n];
this.isItemDisabled(o) || (t = this.swapItems(s, t, n));
}
return s;
}, this.closestSortableItem = (t) => {
let e = t;
for (; e; ) {
const i = e.getAttribute(I);
if (i && this.itemRefsMap[i] === e)
return {
id: i,
element: e
};
e = e.parentElement;
}
return {
id: "",
element: null
};
}, this.isSortable = (t) => !!t.hasAttribute(T), this.closestSortable = (t) => {
let e = t;
for (; e; ) {
if (this.isSortable(e))
return e;
e = e.parentElement;
}
return null;
}, this.isSameSortable = (t) => this.closestSortable(t) === this.container, this.idComparer = (t, e) => t + "" == e + "", this.findItem = (t) => {
const { data: e, idField: i } = this.props;
if (!(t + ""))
return;
const s = R(i);
return z(e, (a) => this.idComparer(s(a), t));
}, this.findIndex = (t) => {
const { data: e, idField: i } = this.props;
return t + "" ? x(e, (s) => this.idComparer(s[i], t)) : -1;
}, this.isItemDisabled = (t) => t && t[this.props.disabledField || ""] === !0, this.shouldResetActive = () => {
const t = p(document);
return t instanceof HTMLElement ? !this.closestSortableItem(t).element : !1;
}, this.widgetTarget = (t) => {
const e = H(t, (i) => G(i, "k-widget") || this.isSortable(i));
return e && !this.isSortable(e);
}, this.allowDrag = (t) => t.hasAttribute(I) || !(_(t) || this.widgetTarget(t)), this.onDragStart = (t) => {
const { event: e } = t, { onDragStart: i } = this.props, s = document.elementFromPoint(e.clientX, e.clientY), { id: a, element: n } = this.closestSortableItem(s), o = this.findItem(a);
if (!a || o && this.isItemDisabled(o) || !this.allowDrag(s) || !this.isSameSortable(s)) {
this.isDragPrevented = !0;
return;
}
e.isTouch && e.originalEvent.preventDefault();
const r = new B(this, this.findIndex(a), s);
i && i.call(void 0, r), this.isDragPrevented = r.isDefaultPrevented(), this.isDragPrevented ? e.originalEvent.preventDefault() : (this.offsetParent = q(this.container), this.setState({
activeId: a,
dragCueWidth: n && n.clientWidth || 0,
dragCueHeight: n && n.clientHeight || 0
}));
}, this.onDragOver = (t) => {
const { event: e } = t, { onDragOver: i, data: s } = this.props;
if (this.isDragPrevented)
return;
e.originalEvent.preventDefault();
const a = this.findIndex(this.state.activeId);
if (a === -1) {
this.resetState();
return;
}
const n = document.elementFromPoint(e.clientX, e.clientY), o = this.closestSortableItem(n), r = this.findIndex(o.id), l = s[r];
if (i && r > -1 && a !== r && !this.isItemDisabled(l) && !this.animatingItemMap[o.id] && this.shouldReorder(o.element, e.clientX, e.clientY)) {
const h = new k(
this,
a,
r,
this.generateNewState(a, r)
);
i.call(void 0, h);
}
const d = this.parentOffset();
this.setState({
clientX: e.clientX - d.left,
clientY: e.clientY - d.top,
isDragging: !0
});
}, this.onDragEnd = (t) => {
const { event: e } = t, i = this.shouldResetActive();
if (this.isDragPrevented)
return;
const { onDragEnd: s, data: a } = this.props, n = document.elementFromPoint(e.clientX, e.clientY), o = this.closestSortableItem(n);
let r = this.findIndex(o.id), l = this.findIndex(this.state.activeId);
const d = this.isItemDisabled(a[r]);
if ((r === -1 || d) && (r = l), s) {
let h = this.generateNewState(l, r);
if (!d) {
const g = this.thresholdRect(o.element);
if (g && (e.clientX < g.left || e.clientX > g.right || e.clientY < g.top || e.clientY > g.bottom)) {
const v = l;
l = r, r = v, h = this.props.data.slice();
}
}
const u = new L(this, l, r, h);
s.call(void 0, u);
}
this.resetState(i);
}, this.shouldReorder = (t, e, i) => {
const s = this.thresholdRect(t);
return s && e > s.left && e < s.right && i > s.top && i < s.bottom;
}, this.thresholdRect = (t) => {
const e = this.state.activeId, i = this.container, a = (i ? Array.from(i.childNodes) : []).find(
(g) => g instanceof HTMLElement && g.getAttribute(I) === e
);
if (!t || !a)
return null;
const { width: n, height: o } = a.getBoundingClientRect(), r = t.getBoundingClientRect(), l = r.top + r.height / 2 - o / 2, d = r.left + r.width / 2 - n / 2, h = l + o, u = d + n;
return { top: l, left: d, bottom: h, right: u };
}, this.onItemBlur = () => {
window.setTimeout(() => {
this.isUnmounted || this.shouldResetActive() && !this.state.isDragging && this.setState({
activeId: ""
// what happends on destroyed component
});
});
}, this.onItemFocus = (t) => {
const { id: e, element: i } = this.closestSortableItem(t.currentTarget);
!this.idComparer(e, this.state.activeId) && this.isSameSortable(t.target) && i === t.target && this.setState({
activeId: e
});
}, this.resetState = (t) => {
this.isDragPrevented = !1, this.setState({
clientX: 0,
clientY: 0,
isDragging: !1,
dragCueWidth: 0,
dragCueHeight: 0,
activeId: t ? "" : this.state.activeId
});
}, this.renderData = () => {
const { data: t, itemUI: e, idField: i, tabIndex: s, navigatable: a } = this.props;
return t.map((n) => {
const r = R(i)(n), l = this.isItemDisabled(n), d = this.idComparer(this.state.activeId, r), h = a ? d ? 0 : -1 : s;
return /* @__PURE__ */ f.createElement(
e,
{
key: r,
forwardRef: (u) => this.refAssign(u, r),
dataItem: n,
isDisabled: l,
isActive: d,
isDragged: d && this.state.isDragging,
isDragCue: !1,
attributes: {
[I]: r,
"aria-disabled": l,
"aria-grabbed": d && this.state.isDragging && !this.isDragPrevented,
"aria-dropeffect": l ? "none" : "move",
tabIndex: N(h, l),
onFocus: this.onItemFocus,
onBlur: this.onItemBlur
},
style: {
cursor: l ? "auto" : "move",
MozUserSelect: "none",
msUserSelect: "none",
WebkitUserSelect: "none",
userSelect: "none"
}
}
);
});
}, this.renderNoData = () => {
const { emptyItemUI: t } = this.props, i = K(this).toLanguageString(w, X[w]);
if (t)
return /* @__PURE__ */ f.createElement(t, { message: i });
}, this.renderDragCue = () => {
const { itemUI: t } = this.props, { isDragging: e, activeId: i, clientX: s, clientY: a } = this.state, n = this.findItem(i);
if (!(!e || !n))
return /* @__PURE__ */ f.createElement(
t,
{
dataItem: n,
isDisabled: !1,
isActive: !0,
isDragged: !0,
isDragCue: !0,
style: {
position: "fixed",
top: a + 10,
left: s + 10,
width: this.state.dragCueWidth,
height: this.state.dragCueHeight
},
attributes: {}
}
);
}, this.refAssign = (t, e) => {
t ? this.itemRefsMap[e] = t : delete this.itemRefsMap[e];
}, this.draggableRefAssign = (t) => {
this.draggableRef = t;
}, this.onKeyDown = (t) => {
var r, l, d;
const { data: e, idField: i } = this.props, { activeId: s } = this.state, a = e.filter((h) => !this.isItemDisabled(h)), n = x(a, (h) => this.idComparer(h[i], s)), o = n < 0 ? 0 : n;
t.key === P.tab && p(document) !== ((r = this.draggableRef) == null ? void 0 : r.element) && (t.preventDefault(), t.stopPropagation()), this.navigation && ((l = this.draggableRef) != null && l.element) && this.navigation.triggerKeyboardEvent({
target: (d = this.draggableRef) == null ? void 0 : d.element.querySelectorAll(A)[o],
key: t.key,
nativeEvent: { type: t.type },
originalEvent: t
});
}, this.handleNext = (t, e, i) => {
var s;
p(document) !== ((s = this.draggableRef) == null ? void 0 : s.element) && (i.originalEvent.metaKey ? this.moveItem(t, e, i, "next") : e.focusNext(t));
}, this.handlePrev = (t, e, i) => {
var s;
p(document) !== ((s = this.draggableRef) == null ? void 0 : s.element) && (i.originalEvent.metaKey ? this.moveItem(t, e, i, "prev") : e.focusPrevious(t));
}, this.moveItem = (t, e, i, s) => {
var a;
if (p(document) !== ((a = this.draggableRef) == null ? void 0 : a.element)) {
const { onNavigate: n, data: o, idField: r } = this.props, l = this.findIndex(this.state.activeId);
if (n) {
let d, h;
s === "next" ? (d = o[l + 1], h = d && d.disabled ? o[l + 2] : o[l + 1]) : (d = o[l - 1], h = d && d.disabled ? o[l - 2] : o[l - 1]);
const u = h && h[r], g = o[l], v = g ? g[r] : "", S = this.findIndex(v), E = this.findIndex(u || v), C = new W(
this,
S,
E,
this.generateNewState(S, E)
);
this.isKeyboardNavigated = !0, n.call(void 0, C);
}
}
}, this.showLicenseWatermark = !U($, { component: "Sortable" }), this.onKeyDown = this.onKeyDown.bind(this);
}
get container() {
return this.draggableRef && this.draggableRef.element;
}
/**
* @hidden
*/
getSnapshotBeforeUpdate() {
const { idField: c, animation: t } = this.props;
return this.oldSizesMap = {}, t && this.props.data.forEach((e) => {
const i = e[c], s = this.itemRefsMap[i];
s && (this.oldSizesMap[i] = s.getBoundingClientRect());
}), null;
}
/**
* @hidden
*/
componentDidUpdate(c) {
const { idField: t, animation: e } = this.props;
this.focusActiveId && (this.focusActiveId = !1, this.itemRefsMap[this.state.activeId].focus()), !(!e || !this.state.isDragging && !this.isKeyboardNavigated) && (this.isKeyboardNavigated = !1, c.data.forEach((i) => {
const s = i[t], a = this.itemRefsMap[s];
if (!a)
return;
const n = a.getBoundingClientRect(), o = this.oldSizesMap[s], r = o.left - n.left, l = o.top - n.top;
r === 0 && l === 0 || requestAnimationFrame(() => {
this.animatingItemMap[s] = !0, a.style.transform = `translate(${r}px, ${l}px)`, a.style.transition = "transform 0s", requestAnimationFrame(() => {
a.style.transform = "", a.style.transition = `transform ${y}ms cubic-bezier(0.2, 0, 0, 1) 0s`, this.windowTimeout(s);
});
});
}));
}
/**
* @hidden
*/
componentDidMount() {
var c, t;
(c = this.draggableRef) != null && c.element && (this.navigation = new M({
tabIndex: 0,
root: { current: (t = this.draggableRef) == null ? void 0 : t.element },
rovingTabIndex: !0,
selectors: [A],
keyboardEvents: {
keydown: {
ArrowDown: this.handleNext,
ArrowRight: this.handleNext,
ArrowUp: this.handlePrev,
ArrowLeft: this.handlePrev,
Enter: (e, i, s) => {
i.focusElement(i.first, e);
},
Escape: (e, i, s) => {
var a, n;
e.setAttribute("tabindex", "-1"), (n = (a = this.draggableRef) == null ? void 0 : a.element) == null || n.focus(), this.setState({ activeId: "" });
}
}
}
})), this.isRtl = this.container && getComputedStyle(this.container).direction === "rtl" || !1;
}
/**
* @hidden
*/
componentWillUnmount() {
this.isUnmounted = !0;
}
/**
* @hidden
*/
parentOffset() {
const c = this.offsetParent;
if (c && c.ownerDocument && c !== c.ownerDocument.body) {
const t = c.getBoundingClientRect();
return {
left: t.left - c.scrollLeft,
top: t.top - c.scrollTop
};
}
return { left: 0, top: 0 };
}
render() {
const { data: c, style: t, className: e, itemsWrapUI: i, tabIndex: s, navigatable: a } = this.props, n = i || "div";
return /* @__PURE__ */ f.createElement(
O,
{
onDragStart: this.onDragStart,
onDrag: this.onDragOver,
onDragEnd: this.onDragEnd,
ref: this.draggableRefAssign
},
/* @__PURE__ */ f.createElement(
n,
{
onKeyDown: (o) => a && this.onKeyDown(o),
...j,
className: e,
style: {
position: "relative",
touchAction: "none",
...t
},
tabIndex: a ? s || 0 : void 0
},
c && c.length ? this.renderData() : this.renderNoData(),
this.renderDragCue(),
this.showLicenseWatermark && /* @__PURE__ */ f.createElement(F, null)
)
);
}
};
b.defaultProps = {
navigation: !0,
animation: !0,
emptyItemUI: (c) => /* @__PURE__ */ f.createElement("div", null, c.message)
}, b.propTypes = {
idField: m.string.isRequired,
disabledField: m.string,
data: m.array.isRequired,
tabIndex: m.number,
navigation: m.bool,
animation: m.bool,
itemsWrapUI: m.any,
itemUI: m.func.isRequired,
emptyItemUI: m.func,
style: m.object,
className: m.string,
onDragStart: m.func,
onDragOver: m.func,
onDragEnd: m.func,
onNavigate: m.func
};
let D = b;
Y(D);
export {
D as Sortable
};