UNPKG

@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
/** * @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 };