dnd-block-tree 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useDroppable, useSensors, useSensor, PointerSensor, TouchSensor, KeyboardSensor, DragOverlay as DragOverlay$1, DndContext, useDraggable } from '@dnd-kit/core';
2
- import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, createContext, useContext, useState } from 'react';
2
+ import { memo, useCallback, useEffect, Fragment, useMemo, useRef, useReducer, useState, createContext, useLayoutEffect, useContext } from 'react';
3
3
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
 
5
5
  // src/core/types.ts
@@ -133,7 +133,7 @@ function useConfiguredSensors(config = {}) {
133
133
  distance: activationDistance
134
134
  };
135
135
  touchConstraint = {
136
- delay: 200,
136
+ delay: config.longPressDelay ?? 200,
137
137
  tolerance: 5
138
138
  };
139
139
  }
@@ -195,6 +195,11 @@ function debounce(fn, delay) {
195
195
  function generateId() {
196
196
  return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
197
197
  }
198
+ function triggerHaptic(durationMs = 10) {
199
+ if (typeof navigator !== "undefined" && typeof navigator.vibrate === "function") {
200
+ navigator.vibrate(durationMs);
201
+ }
202
+ }
198
203
  function DropZoneComponent({
199
204
  id,
200
205
  parentId,
@@ -276,10 +281,15 @@ function TreeRenderer({
276
281
  draggedBlock,
277
282
  focusedId,
278
283
  selectedIds,
279
- onBlockClick
284
+ onBlockClick,
285
+ animation,
286
+ virtualVisibleIds
280
287
  }) {
281
288
  const items = blocksByParent.get(parentId) ?? [];
282
- const filteredBlocks = items.filter((block) => block.id !== activeId);
289
+ let filteredBlocks = items.filter((block) => block.id !== activeId);
290
+ if (virtualVisibleIds && depth === 0) {
291
+ filteredBlocks = filteredBlocks.filter((block) => virtualVisibleIds.has(block.id));
292
+ }
283
293
  const showGhostHere = previewPosition?.parentId === parentId && draggedBlock;
284
294
  const containerClass = depth === 0 ? rootClassName : indentClassName;
285
295
  return /* @__PURE__ */ jsxs("div", { className: containerClass, style: { minWidth: 0 }, children: [
@@ -323,7 +333,11 @@ function TreeRenderer({
323
333
  onBlockClick,
324
334
  children: ({ isDragging }) => {
325
335
  if (isContainer) {
326
- const childContent = isExpanded ? /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx(
336
+ const expandStyle = animation?.expandDuration ? {
337
+ transition: `opacity ${animation.expandDuration}ms ${animation.easing ?? "ease"}`,
338
+ opacity: isExpanded ? 1 : 0
339
+ } : void 0;
340
+ const childContent = isExpanded ? /* @__PURE__ */ jsx("div", { style: expandStyle, children: /* @__PURE__ */ jsx(
327
341
  TreeRenderer,
328
342
  {
329
343
  blocks,
@@ -345,7 +359,9 @@ function TreeRenderer({
345
359
  draggedBlock,
346
360
  focusedId,
347
361
  selectedIds,
348
- onBlockClick
362
+ onBlockClick,
363
+ animation,
364
+ virtualVisibleIds
349
365
  }
350
366
  ) }) : null;
351
367
  return Renderer({
@@ -784,18 +800,21 @@ function BlockTree({
784
800
  canDrop,
785
801
  collisionDetection,
786
802
  sensors: sensorConfig,
803
+ animation,
787
804
  initialExpanded,
788
805
  orderingStrategy = "integer",
789
806
  maxDepth,
790
807
  keyboardNavigation = false,
791
808
  multiSelect = false,
792
809
  selectedIds: externalSelectedIds,
793
- onSelectionChange
810
+ onSelectionChange,
811
+ virtualize
794
812
  }) {
795
813
  const sensors = useConfiguredSensors({
796
814
  activationDistance: sensorConfig?.activationDistance ?? activationDistance,
797
815
  activationDelay: sensorConfig?.activationDelay,
798
- tolerance: sensorConfig?.tolerance
816
+ tolerance: sensorConfig?.tolerance,
817
+ longPressDelay: sensorConfig?.longPressDelay
799
818
  });
800
819
  const initialExpandedMap = useMemo(
801
820
  () => computeInitialExpanded(blocks, containerTypes, initialExpanded),
@@ -1006,12 +1025,15 @@ function BlockTree({
1006
1025
  setSelectedIds(/* @__PURE__ */ new Set([id]));
1007
1026
  }
1008
1027
  }
1028
+ if (sensorConfig?.hapticFeedback) {
1029
+ triggerHaptic();
1030
+ }
1009
1031
  stateRef.current.activeId = id;
1010
1032
  stateRef.current.isDragging = true;
1011
1033
  initialBlocksRef.current = [...blocks];
1012
1034
  cachedReorderRef.current = null;
1013
1035
  forceRender();
1014
- }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds]);
1036
+ }, [blocks, canDrag, onDragStart, multiSelect, selectedIds, setSelectedIds, visibleBlockIds, sensorConfig?.hapticFeedback]);
1015
1037
  const handleDragMove = useCallback((event) => {
1016
1038
  if (!onDragMove) return;
1017
1039
  const id = stateRef.current.activeId;
@@ -1205,6 +1227,61 @@ function BlockTree({
1205
1227
  forceRender();
1206
1228
  }, [blocks, onExpandChange]);
1207
1229
  toggleExpandRef.current = handleToggleExpand;
1230
+ const virtualContainerRef = useRef(null);
1231
+ const [virtualScroll, setVirtualScroll] = useState({ scrollTop: 0, clientHeight: 0 });
1232
+ useEffect(() => {
1233
+ if (!virtualize) return;
1234
+ const el = virtualContainerRef.current;
1235
+ if (!el) return;
1236
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1237
+ const onScroll = () => {
1238
+ setVirtualScroll({ scrollTop: el.scrollTop, clientHeight: el.clientHeight });
1239
+ };
1240
+ el.addEventListener("scroll", onScroll, { passive: true });
1241
+ return () => el.removeEventListener("scroll", onScroll);
1242
+ }, [virtualize]);
1243
+ const virtualResult = useMemo(() => {
1244
+ if (!virtualize) return null;
1245
+ const { itemHeight, overscan = 5 } = virtualize;
1246
+ const { scrollTop, clientHeight } = virtualScroll;
1247
+ const totalHeight = visibleBlockIds.length * itemHeight;
1248
+ const startRaw = Math.floor(scrollTop / itemHeight);
1249
+ const visibleCount = Math.ceil(clientHeight / itemHeight);
1250
+ const start = Math.max(0, startRaw - overscan);
1251
+ const end = Math.min(visibleBlockIds.length - 1, startRaw + visibleCount + overscan);
1252
+ const offsetY = start * itemHeight;
1253
+ const visibleSet = /* @__PURE__ */ new Set();
1254
+ for (let i = start; i <= end; i++) {
1255
+ visibleSet.add(visibleBlockIds[i]);
1256
+ }
1257
+ return { totalHeight, offsetY, visibleSet };
1258
+ }, [virtualize, virtualScroll, visibleBlockIds]);
1259
+ const treeContent = /* @__PURE__ */ jsx(
1260
+ TreeRenderer,
1261
+ {
1262
+ blocks,
1263
+ blocksByParent,
1264
+ parentId: null,
1265
+ activeId: stateRef.current.activeId,
1266
+ expandedMap: stateRef.current.expandedMap,
1267
+ renderers,
1268
+ containerTypes,
1269
+ onHover: handleHover,
1270
+ onToggleExpand: handleToggleExpand,
1271
+ dropZoneClassName,
1272
+ dropZoneActiveClassName,
1273
+ indentClassName,
1274
+ rootClassName: className,
1275
+ canDrag,
1276
+ previewPosition,
1277
+ draggedBlock,
1278
+ focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1279
+ selectedIds: multiSelect ? selectedIds : void 0,
1280
+ onBlockClick: multiSelect ? handleBlockClick : void 0,
1281
+ animation,
1282
+ virtualVisibleIds: virtualResult?.visibleSet ?? null
1283
+ }
1284
+ );
1208
1285
  return /* @__PURE__ */ jsxs(
1209
1286
  DndContext,
1210
1287
  {
@@ -1216,7 +1293,20 @@ function BlockTree({
1216
1293
  onDragEnd: handleDragEnd,
1217
1294
  onDragCancel: handleDragCancel,
1218
1295
  children: [
1219
- /* @__PURE__ */ jsx(
1296
+ virtualize ? /* @__PURE__ */ jsx(
1297
+ "div",
1298
+ {
1299
+ ref: (el) => {
1300
+ virtualContainerRef.current = el;
1301
+ rootRef.current = el;
1302
+ },
1303
+ className,
1304
+ style: { minWidth: 0, overflow: "auto", position: "relative" },
1305
+ onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1306
+ role: keyboardNavigation ? "tree" : void 0,
1307
+ children: /* @__PURE__ */ jsx("div", { style: { height: virtualResult.totalHeight, position: "relative" }, children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", top: virtualResult.offsetY, left: 0, right: 0 }, children: treeContent }) })
1308
+ }
1309
+ ) : /* @__PURE__ */ jsx(
1220
1310
  "div",
1221
1311
  {
1222
1312
  ref: rootRef,
@@ -1224,30 +1314,7 @@ function BlockTree({
1224
1314
  style: { minWidth: 0 },
1225
1315
  onKeyDown: keyboardNavigation ? handleKeyDown : void 0,
1226
1316
  role: keyboardNavigation ? "tree" : void 0,
1227
- children: /* @__PURE__ */ jsx(
1228
- TreeRenderer,
1229
- {
1230
- blocks,
1231
- blocksByParent,
1232
- parentId: null,
1233
- activeId: stateRef.current.activeId,
1234
- expandedMap: stateRef.current.expandedMap,
1235
- renderers,
1236
- containerTypes,
1237
- onHover: handleHover,
1238
- onToggleExpand: handleToggleExpand,
1239
- dropZoneClassName,
1240
- dropZoneActiveClassName,
1241
- indentClassName,
1242
- rootClassName: className,
1243
- canDrag,
1244
- previewPosition,
1245
- draggedBlock,
1246
- focusedId: keyboardNavigation ? focusedIdRef.current : void 0,
1247
- selectedIds: multiSelect ? selectedIds : void 0,
1248
- onBlockClick: multiSelect ? handleBlockClick : void 0
1249
- }
1250
- )
1317
+ children: treeContent
1251
1318
  }
1252
1319
  ),
1253
1320
  /* @__PURE__ */ jsx(DragOverlay, { activeBlock, selectedCount: multiSelect ? selectedIds.size : 0, children: dragOverlay })
@@ -1255,6 +1322,16 @@ function BlockTree({
1255
1322
  }
1256
1323
  );
1257
1324
  }
1325
+ function BlockTreeSSR({ fallback = null, ...props }) {
1326
+ const [mounted, setMounted] = useState(false);
1327
+ useEffect(() => {
1328
+ setMounted(true);
1329
+ }, []);
1330
+ if (!mounted) {
1331
+ return /* @__PURE__ */ jsx(Fragment$1, { children: fallback });
1332
+ }
1333
+ return /* @__PURE__ */ jsx(BlockTree, { ...props });
1334
+ }
1258
1335
  function blockReducer(state, action, containerTypes = [], orderingStrategy = "integer", maxDepth) {
1259
1336
  switch (action.type) {
1260
1337
  case "ADD_ITEM": {
@@ -1659,7 +1736,141 @@ function useBlockHistory(initialBlocks, options = {}) {
1659
1736
  canRedo: state.future.length > 0
1660
1737
  };
1661
1738
  }
1739
+ function useLayoutAnimation(containerRef, options = {}) {
1740
+ const {
1741
+ duration = 200,
1742
+ easing = "ease",
1743
+ selector = "[data-block-id]"
1744
+ } = options;
1745
+ const prevPositions = useRef(/* @__PURE__ */ new Map());
1746
+ useLayoutEffect(() => {
1747
+ const container = containerRef.current;
1748
+ if (!container) return;
1749
+ const children = container.querySelectorAll(selector);
1750
+ const currentPositions = /* @__PURE__ */ new Map();
1751
+ children.forEach((el) => {
1752
+ const id = el.dataset.blockId;
1753
+ if (id) {
1754
+ currentPositions.set(id, el.getBoundingClientRect());
1755
+ }
1756
+ });
1757
+ children.forEach((el) => {
1758
+ const htmlEl = el;
1759
+ const id = htmlEl.dataset.blockId;
1760
+ if (!id) return;
1761
+ const prev = prevPositions.current.get(id);
1762
+ const curr = currentPositions.get(id);
1763
+ if (!prev || !curr) return;
1764
+ const deltaY = prev.top - curr.top;
1765
+ const deltaX = prev.left - curr.left;
1766
+ if (deltaY === 0 && deltaX === 0) return;
1767
+ htmlEl.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
1768
+ htmlEl.style.transition = "none";
1769
+ requestAnimationFrame(() => {
1770
+ htmlEl.style.transition = `transform ${duration}ms ${easing}`;
1771
+ htmlEl.style.transform = "";
1772
+ const onEnd = () => {
1773
+ htmlEl.style.transition = "";
1774
+ htmlEl.removeEventListener("transitionend", onEnd);
1775
+ };
1776
+ htmlEl.addEventListener("transitionend", onEnd);
1777
+ });
1778
+ });
1779
+ prevPositions.current = currentPositions;
1780
+ });
1781
+ }
1782
+ function useVirtualTree({
1783
+ containerRef,
1784
+ itemCount,
1785
+ itemHeight,
1786
+ overscan = 5
1787
+ }) {
1788
+ const [scrollTop, setScrollTop] = useState(0);
1789
+ const [containerHeight, setContainerHeight] = useState(0);
1790
+ const rafId = useRef(0);
1791
+ const handleScroll = useCallback(() => {
1792
+ cancelAnimationFrame(rafId.current);
1793
+ rafId.current = requestAnimationFrame(() => {
1794
+ const el = containerRef.current;
1795
+ if (el) {
1796
+ setScrollTop(el.scrollTop);
1797
+ setContainerHeight(el.clientHeight);
1798
+ }
1799
+ });
1800
+ }, [containerRef]);
1801
+ useEffect(() => {
1802
+ const el = containerRef.current;
1803
+ if (!el) return;
1804
+ setScrollTop(el.scrollTop);
1805
+ setContainerHeight(el.clientHeight);
1806
+ el.addEventListener("scroll", handleScroll, { passive: true });
1807
+ return () => {
1808
+ el.removeEventListener("scroll", handleScroll);
1809
+ cancelAnimationFrame(rafId.current);
1810
+ };
1811
+ }, [containerRef, handleScroll]);
1812
+ const totalHeight = itemCount * itemHeight;
1813
+ const startRaw = Math.floor(scrollTop / itemHeight);
1814
+ const visibleCount = Math.ceil(containerHeight / itemHeight);
1815
+ const start = Math.max(0, startRaw - overscan);
1816
+ const end = Math.min(itemCount - 1, startRaw + visibleCount + overscan);
1817
+ const offsetY = start * itemHeight;
1818
+ return {
1819
+ visibleRange: { start, end },
1820
+ totalHeight,
1821
+ offsetY
1822
+ };
1823
+ }
1824
+
1825
+ // src/utils/serialization.ts
1826
+ function flatToNested(blocks) {
1827
+ const byParent = /* @__PURE__ */ new Map();
1828
+ for (const block of blocks) {
1829
+ const key = block.parentId ?? null;
1830
+ const list = byParent.get(key);
1831
+ if (list) {
1832
+ list.push(block);
1833
+ } else {
1834
+ byParent.set(key, [block]);
1835
+ }
1836
+ }
1837
+ for (const list of byParent.values()) {
1838
+ list.sort((a, b) => {
1839
+ if (typeof a.order === "string" && typeof b.order === "string") {
1840
+ return a.order < b.order ? -1 : a.order > b.order ? 1 : 0;
1841
+ }
1842
+ return Number(a.order) - Number(b.order);
1843
+ });
1844
+ }
1845
+ function buildChildren(parentId) {
1846
+ const siblings = byParent.get(parentId) ?? [];
1847
+ return siblings.map((block) => {
1848
+ const { parentId: _p, order: _o, ...rest } = block;
1849
+ return {
1850
+ ...rest,
1851
+ children: buildChildren(block.id)
1852
+ };
1853
+ });
1854
+ }
1855
+ return buildChildren(null);
1856
+ }
1857
+ function nestedToFlat(nested) {
1858
+ const result = [];
1859
+ function walk(nodes, parentId) {
1860
+ for (let i = 0; i < nodes.length; i++) {
1861
+ const { children, ...rest } = nodes[i];
1862
+ result.push({
1863
+ ...rest,
1864
+ parentId,
1865
+ order: i
1866
+ });
1867
+ walk(children, rest.id);
1868
+ }
1869
+ }
1870
+ walk(nested, null);
1871
+ return result;
1872
+ }
1662
1873
 
1663
- export { BlockTree, DragOverlay, DropZone, TreeRenderer, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, reparentBlockIndex, reparentMultipleBlocks, useBlockHistory, useConfiguredSensors, weightedVerticalCollision };
1874
+ export { BlockTree, BlockTreeSSR, DragOverlay, DropZone, TreeRenderer, buildOrderedBlocks, cloneMap, cloneParentMap, closestCenterCollision, compareFractionalKeys, computeNormalizedIndex, createBlockState, createStickyCollision, createTreeState, debounce, deleteBlockAndDescendants, extractBlockId, extractUUID, flatToNested, generateId, generateInitialKeys, generateKeyBetween, generateNKeysBetween, getBlockDepth, getDescendantIds, getDropZoneType, getSensorConfig, getSubtreeDepth, initFractionalOrder, nestedToFlat, reparentBlockIndex, reparentMultipleBlocks, triggerHaptic, useBlockHistory, useConfiguredSensors, useLayoutAnimation, useVirtualTree, weightedVerticalCollision };
1664
1875
  //# sourceMappingURL=index.mjs.map
1665
1876
  //# sourceMappingURL=index.mjs.map