UNPKG

@thisbeyond/solid-dnd

Version:

A lightweight drag and drop toolkit for Solid.

1,276 lines (1,263 loc) 38 kB
import { createComponent, memo, Portal, insert, effect, className, style, template, use } from 'solid-js/web'; import { createContext, mergeProps, useContext, onMount, onCleanup, createSignal, Show, createEffect, untrack, For, batch } from 'solid-js'; import { createStore } from 'solid-js/store'; // src/drag-drop-context.tsx // src/layout.ts var Layout = class { x; y; width; height; constructor(rect) { this.x = Math.floor(rect.x); this.y = Math.floor(rect.y); this.width = Math.floor(rect.width); this.height = Math.floor(rect.height); } get rect() { return { x: this.x, y: this.y, width: this.width, height: this.height }; } get left() { return this.x; } get top() { return this.y; } get right() { return this.x + this.width; } get bottom() { return this.y + this.height; } get center() { return { x: this.x + this.width * 0.5, y: this.y + this.height * 0.5 }; } get corners() { return { topLeft: { x: this.left, y: this.top }, topRight: { x: this.right, y: this.top }, bottomRight: { x: this.left, y: this.bottom }, bottomLeft: { x: this.right, y: this.bottom } }; } }; var elementLayout = (element) => { let layout = new Layout(element.getBoundingClientRect()); const { transform } = getComputedStyle(element); if (transform) { layout = stripTransformFromLayout(layout, transform); } return layout; }; var stripTransformFromLayout = (layout, transform) => { let translateX, translateY; if (transform.startsWith("matrix3d(")) { const matrix = transform.slice(9, -1).split(/, /); translateX = +matrix[12]; translateY = +matrix[13]; } else if (transform.startsWith("matrix(")) { const matrix = transform.slice(7, -1).split(/, /); translateX = +matrix[4]; translateY = +matrix[5]; } else { translateX = 0; translateY = 0; } return new Layout({ ...layout, x: layout.x - translateX, y: layout.y - translateY }); }; var noopTransform = () => ({ x: 0, y: 0 }); var transformsAreEqual = (firstTransform, secondTransform) => { return firstTransform.x === secondTransform.x && firstTransform.y === secondTransform.y; }; var transformLayout = (layout, transform) => { return new Layout({ ...layout, x: layout.x + transform.x, y: layout.y + transform.y }); }; var distanceBetweenPoints = (firstPoint, secondPoint) => { return Math.sqrt( Math.pow(firstPoint.x - secondPoint.x, 2) + Math.pow(firstPoint.y - secondPoint.y, 2) ); }; var intersectionRatioOfLayouts = (firstLayout, secondLayout) => { const top = Math.max(firstLayout.top, secondLayout.top); const left = Math.max(firstLayout.left, secondLayout.left); const right = Math.min(firstLayout.right, secondLayout.right); const bottom = Math.min(firstLayout.bottom, secondLayout.bottom); const width = right - left; const height = bottom - top; if (left < right && top < bottom) { const layout1Area = firstLayout.width * firstLayout.height; const layout2Area = secondLayout.width * secondLayout.height; const intersectionArea = width * height; return intersectionArea / (layout1Area + layout2Area - intersectionArea); } return 0; }; var layoutsAreEqual = (firstLayout, secondLayout) => { return firstLayout.x === secondLayout.x && firstLayout.y === secondLayout.y && firstLayout.width === secondLayout.width && firstLayout.height === secondLayout.height; }; // src/collision.ts var closestCenter = (draggable, droppables, context) => { const point1 = draggable.transformed.center; const collision = { distance: Infinity, droppable: null }; for (const droppable of droppables) { const distance = distanceBetweenPoints(point1, droppable.layout.center); if (distance < collision.distance) { collision.distance = distance; collision.droppable = droppable; } else if (distance === collision.distance && droppable.id === context.activeDroppableId) { collision.droppable = droppable; } } return collision.droppable; }; var closestCorners = (draggable, droppables, context) => { const draggableCorners = draggable.transformed.corners; const collision = { distance: Infinity, droppable: null }; for (const droppable of droppables) { const droppableCorners = droppable.layout.corners; const distance = distanceBetweenPoints( droppableCorners.topLeft, draggableCorners.topLeft ) + distanceBetweenPoints( droppableCorners.topRight, draggableCorners.topRight ) + distanceBetweenPoints( droppableCorners.bottomRight, draggableCorners.bottomRight ) + distanceBetweenPoints( droppableCorners.bottomLeft, draggableCorners.bottomLeft ); if (distance < collision.distance) { collision.distance = distance; collision.droppable = droppable; } else if (distance === collision.distance && droppable.id === context.activeDroppableId) { collision.droppable = droppable; } } return collision.droppable; }; var mostIntersecting = (draggable, droppables, context) => { const draggableLayout = draggable.transformed; const collision = { ratio: 0, droppable: null }; for (const droppable of droppables) { const ratio = intersectionRatioOfLayouts(draggableLayout, droppable.layout); if (ratio > collision.ratio) { collision.ratio = ratio; collision.droppable = droppable; } else if (ratio > 0 && ratio === collision.ratio && droppable.id === context.activeDroppableId) { collision.droppable = droppable; } } return collision.droppable; }; // src/drag-drop-context.tsx var Context = createContext(); var DragDropProvider = (passedProps) => { const props = mergeProps({ collisionDetector: mostIntersecting }, passedProps); const [state, setState] = createStore({ draggables: {}, droppables: {}, sensors: {}, active: { draggableId: null, get draggable() { return state.active.draggableId !== null ? state.draggables[state.active.draggableId] : null; }, droppableId: null, get droppable() { return state.active.droppableId !== null ? state.droppables[state.active.droppableId] : null; }, sensorId: null, get sensor() { return state.active.sensorId !== null ? state.sensors[state.active.sensorId] : null; }, overlay: null } }); const addTransformer = (type, id, transformer) => { const displayType = type.substring(0, type.length - 1); if (!untrack(() => state[type][id])) { console.warn(`Cannot add transformer to nonexistent ${displayType} with id: ${id}`); return; } setState(type, id, "transformers", transformer.id, transformer); }; const removeTransformer = (type, id, transformerId) => { const displayType = type.substring(0, type.length - 1); if (!untrack(() => state[type][id])) { console.warn(`Cannot remove transformer from nonexistent ${displayType} with id: ${id}`); return; } if (!untrack(() => state[type][id]["transformers"][transformerId])) { console.warn(`Cannot remove from ${displayType} with id ${id}, nonexistent transformer with id: ${transformerId}`); return; } setState(type, id, "transformers", transformerId, void 0); }; const addDraggable = ({ id, node, layout, data }) => { const existingDraggable = state.draggables[id]; const draggable = { id, node, layout, data, _pendingCleanup: false }; let transformer; if (!existingDraggable) { Object.defineProperties(draggable, { transformers: { enumerable: true, configurable: true, writable: true, value: {} }, transform: { enumerable: true, configurable: true, get: () => { if (state.active.overlay) { return noopTransform(); } const transformers = Object.values(state.draggables[id].transformers); transformers.sort((a, b) => a.order - b.order); return transformers.reduce((transform, transformer2) => { return transformer2.callback(transform); }, noopTransform()); } }, transformed: { enumerable: true, configurable: true, get: () => { return transformLayout(state.draggables[id].layout, state.draggables[id].transform); } } }); } else if (state.active.draggableId === id && !state.active.overlay) { const layoutDelta = { x: existingDraggable.layout.x - layout.x, y: existingDraggable.layout.y - layout.y }; const transformerId = "addDraggable-existing-offset"; const existingTransformer = existingDraggable.transformers[transformerId]; const transformOffset = existingTransformer ? existingTransformer.callback(layoutDelta) : layoutDelta; transformer = { id: transformerId, order: 100, callback: (transform) => { return { x: transform.x + transformOffset.x, y: transform.y + transformOffset.y }; } }; onDragEnd(() => removeTransformer("draggables", id, transformerId)); } batch(() => { setState("draggables", id, draggable); if (transformer) { addTransformer("draggables", id, transformer); } }); if (state.active.draggable) { recomputeLayouts(); } }; const removeDraggable = (id) => { if (!untrack(() => state.draggables[id])) { console.warn(`Cannot remove nonexistent draggable with id: ${id}`); return; } setState("draggables", id, "_pendingCleanup", true); queueMicrotask(() => cleanupDraggable(id)); }; const cleanupDraggable = (id) => { if (state.draggables[id]?._pendingCleanup) { const cleanupActive = state.active.draggableId === id; batch(() => { if (cleanupActive) { setState("active", "draggableId", null); } setState("draggables", id, void 0); }); } }; const addDroppable = ({ id, node, layout, data }) => { const existingDroppable = state.droppables[id]; const droppable = { id, node, layout, data, _pendingCleanup: false }; if (!existingDroppable) { Object.defineProperties(droppable, { transformers: { enumerable: true, configurable: true, writable: true, value: {} }, transform: { enumerable: true, configurable: true, get: () => { const transformers = Object.values(state.droppables[id].transformers); transformers.sort((a, b) => a.order - b.order); return transformers.reduce((transform, transformer) => { return transformer.callback(transform); }, noopTransform()); } }, transformed: { enumerable: true, configurable: true, get: () => { return transformLayout(state.droppables[id].layout, state.droppables[id].transform); } } }); } setState("droppables", id, droppable); if (state.active.draggable) { recomputeLayouts(); } }; const removeDroppable = (id) => { if (!untrack(() => state.droppables[id])) { console.warn(`Cannot remove nonexistent droppable with id: ${id}`); return; } setState("droppables", id, "_pendingCleanup", true); queueMicrotask(() => cleanupDroppable(id)); }; const cleanupDroppable = (id) => { if (state.droppables[id]?._pendingCleanup) { const cleanupActive = state.active.droppableId === id; batch(() => { if (cleanupActive) { setState("active", "droppableId", null); } setState("droppables", id, void 0); }); } }; const addSensor = ({ id, activators }) => { setState("sensors", id, { id, activators, coordinates: { origin: { x: 0, y: 0 }, current: { x: 0, y: 0 }, get delta() { return { x: state.sensors[id].coordinates.current.x - state.sensors[id].coordinates.origin.x, y: state.sensors[id].coordinates.current.y - state.sensors[id].coordinates.origin.y }; } } }); }; const removeSensor = (id) => { if (!untrack(() => state.sensors[id])) { console.warn(`Cannot remove nonexistent sensor with id: ${id}`); return; } const cleanupActive = state.active.sensorId === id; batch(() => { if (cleanupActive) { setState("active", "sensorId", null); } setState("sensors", id, void 0); }); }; const setOverlay = ({ node, layout }) => { const existing = state.active.overlay; const overlay = { node, layout }; if (!existing) { Object.defineProperties(overlay, { id: { enumerable: true, configurable: true, get: () => state.active.draggable?.id }, data: { enumerable: true, configurable: true, get: () => state.active.draggable?.data }, transformers: { enumerable: true, configurable: true, get: () => Object.fromEntries(Object.entries(state.active.draggable ? state.active.draggable.transformers : {}).filter(([id]) => id !== "addDraggable-existing-offset")) }, transform: { enumerable: true, configurable: true, get: () => { const transformers = Object.values(state.active.overlay ? state.active.overlay.transformers : []); transformers.sort((a, b) => a.order - b.order); return transformers.reduce((transform, transformer) => { return transformer.callback(transform); }, noopTransform()); } }, transformed: { enumerable: true, configurable: true, get: () => { return state.active.overlay ? transformLayout(state.active.overlay.layout, state.active.overlay.transform) : new Layout({ x: 0, y: 0, width: 0, height: 0 }); } } }); } setState("active", "overlay", overlay); }; const clearOverlay = () => setState("active", "overlay", null); const sensorStart = (id, coordinates) => { batch(() => { setState("sensors", id, "coordinates", { origin: { ...coordinates }, current: { ...coordinates } }); setState("active", "sensorId", id); }); }; const sensorMove = (coordinates) => { const sensorId = state.active.sensorId; if (!sensorId) { console.warn("Cannot move sensor when no sensor active."); return; } setState("sensors", sensorId, "coordinates", "current", { ...coordinates }); }; const sensorEnd = () => setState("active", "sensorId", null); const draggableActivators = (draggableId, asHandlers) => { const eventMap = {}; for (const sensor of Object.values(state.sensors)) { if (sensor) { for (const [type, activator] of Object.entries(sensor.activators)) { eventMap[type] ??= []; eventMap[type].push({ sensor, activator }); } } } const listeners = {}; for (const key in eventMap) { let handlerKey = key; if (asHandlers) { handlerKey = `on${key}`; } listeners[handlerKey] = (event) => { for (const { activator } of eventMap[key]) { if (state.active.sensor) { break; } activator(event, draggableId); } }; } return listeners; }; const recomputeLayouts = () => { let anyLayoutChanged = false; const draggables = Object.values(state.draggables); const droppables = Object.values(state.droppables); const overlay = state.active.overlay; batch(() => { const cache = /* @__PURE__ */ new WeakMap(); for (const draggable of draggables) { if (draggable) { const currentLayout = draggable.layout; if (!cache.has(draggable.node)) cache.set(draggable.node, elementLayout(draggable.node)); const layout = cache.get(draggable.node); if (!layoutsAreEqual(currentLayout, layout)) { setState("draggables", draggable.id, "layout", layout); anyLayoutChanged = true; } } } for (const droppable of droppables) { if (droppable) { const currentLayout = droppable.layout; if (!cache.has(droppable.node)) cache.set(droppable.node, elementLayout(droppable.node)); const layout = cache.get(droppable.node); if (!layoutsAreEqual(currentLayout, layout)) { setState("droppables", droppable.id, "layout", layout); anyLayoutChanged = true; } } } if (overlay) { const currentLayout = overlay.layout; const layout = elementLayout(overlay.node); if (!layoutsAreEqual(currentLayout, layout)) { setState("active", "overlay", "layout", layout); anyLayoutChanged = true; } } }); return anyLayoutChanged; }; const detectCollisions = () => { const draggable = state.active.overlay ?? state.active.draggable; if (draggable) { const droppable = props.collisionDetector(draggable, Object.values(state.droppables), { activeDroppableId: state.active.droppableId }); const droppableId = droppable ? droppable.id : null; if (state.active.droppableId !== droppableId) { setState("active", "droppableId", droppableId); } } }; const dragStart = (draggableId) => { const transformer = { id: "sensorMove", order: 0, callback: (transform) => { if (state.active.sensor) { return { x: transform.x + state.active.sensor.coordinates.delta.x, y: transform.y + state.active.sensor.coordinates.delta.y }; } return transform; } }; recomputeLayouts(); batch(() => { setState("active", "draggableId", draggableId); addTransformer("draggables", draggableId, transformer); }); detectCollisions(); }; const dragEnd = () => { const draggableId = untrack(() => state.active.draggableId); batch(() => { if (draggableId !== null) { removeTransformer("draggables", draggableId, "sensorMove"); } setState("active", ["draggableId", "droppableId"], null); }); recomputeLayouts(); }; const onDragStart = (handler) => { createEffect(() => { const draggable = state.active.draggable; if (draggable) { untrack(() => handler({ draggable })); } }); }; const onDragMove = (handler) => { createEffect(() => { const draggable = state.active.draggable; if (draggable) { const overlay = untrack(() => state.active.overlay); Object.values(overlay ? overlay.transform : draggable.transform); untrack(() => handler({ draggable, overlay })); } }); }; const onDragOver = (handler) => { createEffect(() => { const draggable = state.active.draggable; const droppable = state.active.droppable; if (draggable) { untrack(() => handler({ draggable, droppable, overlay: state.active.overlay })); } }); }; const onDragEnd = (handler) => { createEffect(({ previousDraggable, previousDroppable, previousOverlay }) => { const draggable = state.active.draggable; const droppable = draggable ? state.active.droppable : null; const overlay = draggable ? state.active.overlay : null; if (!draggable && previousDraggable) { untrack(() => handler({ draggable: previousDraggable, droppable: previousDroppable, overlay: previousOverlay })); } return { previousDraggable: draggable, previousDroppable: droppable, previousOverlay: overlay }; }, { previousDraggable: null, previousDroppable: null, previousOverlay: null }); }; onDragMove(() => detectCollisions()); props.onDragStart && onDragStart(props.onDragStart); props.onDragMove && onDragMove(props.onDragMove); props.onDragOver && onDragOver(props.onDragOver); props.onDragEnd && onDragEnd(props.onDragEnd); const actions = { addTransformer, removeTransformer, addDraggable, removeDraggable, addDroppable, removeDroppable, addSensor, removeSensor, setOverlay, clearOverlay, recomputeLayouts, detectCollisions, draggableActivators, sensorStart, sensorMove, sensorEnd, dragStart, dragEnd, onDragStart, onDragMove, onDragOver, onDragEnd }; const context = [state, actions]; return createComponent(Context.Provider, { value: context, get children() { return props.children; } }); }; var useDragDropContext = () => { return useContext(Context) || null; }; var createPointerSensor = (id = "pointer-sensor") => { const [ state, { addSensor, removeSensor, sensorStart, sensorMove, sensorEnd, dragStart, dragEnd } ] = useDragDropContext(); const activationDelay = 250; const activationDistance = 10; onMount(() => { addSensor({ id, activators: { pointerdown: attach } }); }); onCleanup(() => { removeSensor(id); }); const isActiveSensor = () => state.active.sensorId === id; const initialCoordinates = { x: 0, y: 0 }; let activationDelayTimeoutId = null; let activationDraggableId = null; const attach = (event, draggableId) => { if (event.button !== 0) return; document.addEventListener("pointermove", onPointerMove); document.addEventListener("pointerup", onPointerUp); activationDraggableId = draggableId; initialCoordinates.x = event.clientX; initialCoordinates.y = event.clientY; activationDelayTimeoutId = window.setTimeout(onActivate, activationDelay); }; const detach = () => { if (activationDelayTimeoutId) { clearTimeout(activationDelayTimeoutId); activationDelayTimeoutId = null; } document.removeEventListener("pointermove", onPointerMove); document.removeEventListener("pointerup", onPointerUp); document.removeEventListener("selectionchange", clearSelection); }; const onActivate = () => { if (!state.active.sensor) { sensorStart(id, initialCoordinates); dragStart(activationDraggableId); clearSelection(); document.addEventListener("selectionchange", clearSelection); } else if (!isActiveSensor()) { detach(); } }; const onPointerMove = (event) => { const coordinates = { x: event.clientX, y: event.clientY }; if (!state.active.sensor) { const transform = { x: coordinates.x - initialCoordinates.x, y: coordinates.y - initialCoordinates.y }; if (Math.sqrt(transform.x ** 2 + transform.y ** 2) > activationDistance) { onActivate(); } } if (isActiveSensor()) { event.preventDefault(); sensorMove(coordinates); } }; const onPointerUp = (event) => { detach(); if (isActiveSensor()) { event.preventDefault(); dragEnd(); sensorEnd(); } }; const clearSelection = () => { window.getSelection()?.removeAllRanges(); }; }; // src/drag-drop-sensors.tsx var DragDropSensors = (props) => { createPointerSensor(); return memo(() => props.children); }; // src/style.ts var layoutStyle = (layout) => { return { top: `${layout.y}px`, left: `${layout.x}px`, width: `${layout.width}px`, height: `${layout.height}px` }; }; var transformStyle = (transform) => { return { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` }; }; var maybeTransformStyle = (transform) => { return transformsAreEqual(transform, noopTransform()) ? {} : transformStyle(transform); }; // src/create-draggable.ts var createDraggable = (id, data = {}) => { const [state, { addDraggable, removeDraggable, draggableActivators }] = useDragDropContext(); const [node, setNode] = createSignal(null); onMount(() => { const resolvedNode = node(); if (resolvedNode) { addDraggable({ id, node: resolvedNode, layout: elementLayout(resolvedNode), data }); } }); onCleanup(() => removeDraggable(id)); const isActiveDraggable = () => state.active.draggableId === id; const transform = () => { return state.draggables[id]?.transform || noopTransform(); }; const draggable = Object.defineProperties( (element, accessor) => { const config = accessor ? accessor() : {}; createEffect(() => { const resolvedNode = node(); const activators = draggableActivators(id); if (resolvedNode) { for (const key in activators) { resolvedNode.addEventListener(key, activators[key]); } } onCleanup(() => { if (resolvedNode) { for (const key in activators) { resolvedNode.removeEventListener(key, activators[key]); } } }); }); setNode(element); if (!config.skipTransform) { createEffect(() => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(transform()); element.style.setProperty("transform", style.transform ?? null); } else { element.style.removeProperty("transform"); } }); } }, { ref: { enumerable: true, value: setNode }, isActiveDraggable: { enumerable: true, get: isActiveDraggable }, dragActivators: { enumerable: true, get: () => { return draggableActivators(id, true); } }, transform: { enumerable: true, get: transform } } ); return draggable; }; var createDroppable = (id, data = {}) => { const [state, { addDroppable, removeDroppable }] = useDragDropContext(); const [node, setNode] = createSignal(null); onMount(() => { const resolvedNode = node(); if (resolvedNode) { addDroppable({ id, node: resolvedNode, layout: elementLayout(resolvedNode), data }); } }); onCleanup(() => removeDroppable(id)); const isActiveDroppable = () => state.active.droppableId === id; const transform = () => { return state.droppables[id]?.transform || noopTransform(); }; const droppable = Object.defineProperties( (element, accessor) => { const config = accessor ? accessor() : {}; setNode(element); if (!config.skipTransform) { createEffect(() => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(transform()); element.style.setProperty("transform", style.transform ?? null); } else { element.style.removeProperty("transform"); } }); } }, { ref: { enumerable: true, value: setNode }, isActiveDroppable: { enumerable: true, get: isActiveDroppable }, transform: { enumerable: true, get: transform } } ); return droppable; }; var _tmpl$ = /* @__PURE__ */ template(`<div>`); var DragOverlay = (props) => { const [state, { onDragStart, onDragEnd, setOverlay, clearOverlay }] = useDragDropContext(); let node; onDragStart(({ draggable }) => { setOverlay({ node: draggable.node, layout: draggable.layout }); queueMicrotask(() => { if (node) { const layout = elementLayout(node); const delta = { x: (draggable.layout.width - layout.width) / 2, y: (draggable.layout.height - layout.height) / 2 }; layout.x += delta.x; layout.y += delta.y; setOverlay({ node, layout }); } }); }); onDragEnd(() => queueMicrotask(clearOverlay)); const style$1 = () => { const overlay = state.active.overlay; const draggable = state.active.draggable; if (!overlay || !draggable) return {}; return { position: "fixed", transition: "transform 0s", top: `${overlay.layout.top}px`, left: `${overlay.layout.left}px`, "min-width": `${draggable.layout.width}px`, "min-height": `${draggable.layout.height}px`, ...transformStyle(overlay.transform), ...props.style }; }; return createComponent(Portal, { get mount() { return document.body; }, get children() { return createComponent(Show, { get when() { return state.active.draggable; }, get children() { const _el$ = _tmpl$(); const _ref$ = node; typeof _ref$ === "function" ? use(_ref$, _el$) : node = _el$; insert(_el$, (() => { const _c$ = memo(() => typeof props.children === "function"); return () => _c$() ? props.children(state.active.draggable) : props.children; })()); effect((_p$) => { const _v$ = props.class, _v$2 = style$1(); _v$ !== _p$._v$ && className(_el$, _p$._v$ = _v$); _p$._v$2 = style(_el$, _v$2, _p$._v$2); return _p$; }, { _v$: void 0, _v$2: void 0 }); return _el$; } }); } }); }; // src/move-array-item.ts var moveArrayItem = (array, fromIndex, toIndex) => { const newArray = array.slice(); newArray.splice(toIndex, 0, ...newArray.splice(fromIndex, 1)); return newArray; }; // src/sortable-context.tsx var Context2 = createContext(); var SortableProvider = (props) => { const [dndState] = useDragDropContext(); const [state, setState] = createStore({ initialIds: [], sortedIds: [] }); const isValidIndex = (index) => { return index >= 0 && index < state.initialIds.length; }; createEffect(() => { setState("initialIds", [...props.ids]); setState("sortedIds", [...props.ids]); }); createEffect(() => { if (dndState.active.draggableId && dndState.active.droppableId) { untrack(() => { const fromIndex = state.sortedIds.indexOf(dndState.active.draggableId); const toIndex = state.initialIds.indexOf(dndState.active.droppableId); if (!isValidIndex(fromIndex) || !isValidIndex(toIndex)) { setState("sortedIds", [...props.ids]); } else if (fromIndex !== toIndex) { const resorted = moveArrayItem(state.sortedIds, fromIndex, toIndex); setState("sortedIds", resorted); } }); } else { setState("sortedIds", [...props.ids]); } }); const actions = {}; const context = [state, actions]; return createComponent(Context2.Provider, { value: context, get children() { return props.children; } }); }; var useSortableContext = () => { return useContext(Context2) || null; }; // src/combine-refs.ts var combineRefs = (setRefA, setRefB) => { return (ref) => { setRefA(ref); setRefB(ref); }; }; // src/create-sortable.ts var createSortable = (id, data = {}) => { const [dndState, { addTransformer, removeTransformer }] = useDragDropContext(); const [sortableState] = useSortableContext(); const draggable = createDraggable(id, data); const droppable = createDroppable(id, data); const setNode = combineRefs(draggable.ref, droppable.ref); const initialIndex = () => sortableState.initialIds.indexOf(id); const currentIndex = () => sortableState.sortedIds.indexOf(id); const layoutById = (id2) => dndState.droppables[id2]?.layout || null; const sortedTransform = () => { const delta = noopTransform(); const resolvedInitialIndex = initialIndex(); const resolvedCurrentIndex = currentIndex(); if (resolvedCurrentIndex !== resolvedInitialIndex) { const currentLayout = layoutById(id); const targetLayout = layoutById( sortableState.initialIds[resolvedCurrentIndex] ); if (currentLayout && targetLayout) { delta.x = targetLayout.x - currentLayout.x; delta.y = targetLayout.y - currentLayout.y; } } return delta; }; const transformer = { id: "sortableOffset", order: 100, callback: (transform2) => { const delta = sortedTransform(); return { x: transform2.x + delta.x, y: transform2.y + delta.y }; } }; onMount(() => addTransformer("droppables", id, transformer)); onCleanup(() => removeTransformer("droppables", id, transformer.id)); const transform = () => { return (id === dndState.active.draggableId && !dndState.active.overlay ? dndState.draggables[id]?.transform : dndState.droppables[id]?.transform) || noopTransform(); }; const sortable = Object.defineProperties( (element) => { draggable(element, () => ({ skipTransform: true })); droppable(element, () => ({ skipTransform: true })); createEffect(() => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(transform()); element.style.setProperty("transform", style.transform ?? null); } else { element.style.removeProperty("transform"); } }); }, { ref: { enumerable: true, value: setNode }, transform: { enumerable: true, get: transform }, isActiveDraggable: { enumerable: true, get: () => draggable.isActiveDraggable }, dragActivators: { enumerable: true, get: () => draggable.dragActivators }, isActiveDroppable: { enumerable: true, get: () => droppable.isActiveDroppable } } ); return sortable; }; var _tmpl$2 = /* @__PURE__ */ template(`<div>`); var Highlighter = (props) => { props = mergeProps({ color: "red", active: false }, props); return (() => { const _el$ = _tmpl$2(); insert(_el$, () => props.id); effect((_$p) => style(_el$, { position: "fixed", "pointer-events": "none", ...layoutStyle(props.layout), outline: "1px dashed", "outline-width": props.active ? "4px" : "1px", "outline-color": props.color, display: "flex", color: props.color, "align-items": "flex-end", "justify-content": "flex-end", ...props.style }, _$p)); return _el$; })(); }; var DragDropDebugger = () => { const [state, { recomputeLayouts }] = useDragDropContext(); let ticking = false; const update = () => { if (!ticking) { window.requestAnimationFrame(function() { recomputeLayouts(); ticking = false; }); ticking = true; } }; onMount(() => { document.addEventListener("scroll", update); }); onCleanup(() => { document.removeEventListener("scroll", update); }); return createComponent(Portal, { get mount() { return document.body; }, get children() { return [createComponent(For, { get each() { return Object.values(state.droppables); }, children: (droppable) => droppable ? createComponent(Highlighter, { get id() { return droppable.id; }, get layout() { return droppable.layout; }, get active() { return droppable.id === state.active.droppableId; } }) : null }), createComponent(For, { get each() { return Object.values(state.draggables); }, children: (draggable) => draggable ? createComponent(Highlighter, { get id() { return draggable.id; }, get layout() { return draggable.layout; }, get active() { return draggable.id === state.active.draggableId; }, color: "blue", get style() { return { "align-items": "flex-start", "justify-content": "flex-start", ...transformStyle(draggable.transform) }; } }) : null }), createComponent(Show, { get when() { return state.active.overlay; }, keyed: true, children: (overlay) => createComponent(Highlighter, { get id() { return overlay.id; }, get layout() { return overlay.layout; }, active: true, color: "orange", get style() { return { ...transformStyle(overlay.transform) }; } }) })]; } }); }; export { DragDropDebugger, DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, closestCorners, createDraggable, createDroppable, createPointerSensor, createSortable, layoutStyle, maybeTransformStyle, mostIntersecting, transformStyle, useDragDropContext, useSortableContext };