UNPKG

@thisbeyond/solid-dnd

Version:

A lightweight drag and drop toolkit for Solid.

1,254 lines (1,236 loc) 37.3 kB
// src/drag-drop-context.tsx import { batch, createContext, createEffect, mergeProps, untrack, useContext } from "solid-js"; import { createStore } from "solid-js/store"; // 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 <Context.Provider value={context}>{props.children}</Context.Provider>; }; var useDragDropContext = () => { return useContext(Context) || null; }; // src/create-pointer-sensor.ts import { onCleanup, onMount } from "solid-js"; 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 <>{props.children}</>; }; // src/create-draggable.ts import { createEffect as createEffect2, createSignal, onCleanup as onCleanup2, onMount as onMount2 } from "solid-js"; // 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); onMount2(() => { const resolvedNode = node(); if (resolvedNode) { addDraggable({ id, node: resolvedNode, layout: elementLayout(resolvedNode), data }); } }); onCleanup2(() => 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() : {}; createEffect2(() => { const resolvedNode = node(); const activators = draggableActivators(id); if (resolvedNode) { for (const key in activators) { resolvedNode.addEventListener(key, activators[key]); } } onCleanup2(() => { if (resolvedNode) { for (const key in activators) { resolvedNode.removeEventListener(key, activators[key]); } } }); }); setNode(element); if (!config.skipTransform) { createEffect2(() => { 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; }; // src/create-droppable.ts import { createEffect as createEffect3, createSignal as createSignal2, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js"; var createDroppable = (id, data = {}) => { const [state, { addDroppable, removeDroppable }] = useDragDropContext(); const [node, setNode] = createSignal2(null); onMount3(() => { const resolvedNode = node(); if (resolvedNode) { addDroppable({ id, node: resolvedNode, layout: elementLayout(resolvedNode), data }); } }); onCleanup3(() => 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) { createEffect3(() => { 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; }; // src/drag-overlay.tsx import { Portal } from "solid-js/web"; import { Show } from "solid-js"; 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 = () => { 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 <Portal mount={document.body}><Show when={state.active.draggable}><div ref={node} class={props.class} style={style()}>{typeof props.children === "function" ? props.children(state.active.draggable) : props.children}</div></Show></Portal>; }; // src/sortable-context.tsx import { createContext as createContext2, createEffect as createEffect4, untrack as untrack2, useContext as useContext2 } from "solid-js"; import { createStore as createStore2 } from "solid-js/store"; // 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 = createContext2(); var SortableProvider = (props) => { const [dndState] = useDragDropContext(); const [state, setState] = createStore2({ initialIds: [], sortedIds: [] }); const isValidIndex = (index) => { return index >= 0 && index < state.initialIds.length; }; createEffect4(() => { setState("initialIds", [...props.ids]); setState("sortedIds", [...props.ids]); }); createEffect4(() => { if (dndState.active.draggableId && dndState.active.droppableId) { untrack2(() => { 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 <Context2.Provider value={context}>{props.children}</Context2.Provider>; }; var useSortableContext = () => { return useContext2(Context2) || null; }; // src/create-sortable.ts import { createEffect as createEffect5, onCleanup as onCleanup4, onMount as onMount4 } from "solid-js"; // 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 }; } }; onMount4(() => addTransformer("droppables", id, transformer)); onCleanup4(() => 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 })); createEffect5(() => { 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; }; // src/drag-drop-debugger.tsx import { For, mergeProps as mergeProps2, onCleanup as onCleanup5, onMount as onMount5, Show as Show2 } from "solid-js"; import { Portal as Portal2 } from "solid-js/web"; var Highlighter = (props) => { props = mergeProps2({ color: "red", active: false }, props); return <div style={{ 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 }} >{props.id}</div>; }; var DragDropDebugger = () => { const [state, { recomputeLayouts }] = useDragDropContext(); let ticking = false; const update = () => { if (!ticking) { window.requestAnimationFrame(function() { recomputeLayouts(); ticking = false; }); ticking = true; } }; onMount5(() => { document.addEventListener("scroll", update); }); onCleanup5(() => { document.removeEventListener("scroll", update); }); return <Portal2 mount={document.body}> <For each={Object.values(state.droppables)}>{(droppable) => droppable ? <Highlighter id={droppable.id} layout={droppable.layout} active={droppable.id === state.active.droppableId} /> : null}</For> <For each={Object.values(state.draggables)}>{(draggable) => draggable ? <Highlighter id={draggable.id} layout={draggable.layout} active={draggable.id === state.active.draggableId} color="blue" style={{ "align-items": "flex-start", "justify-content": "flex-start", ...transformStyle(draggable.transform) }} /> : null}</For> <Show2 when={state.active.overlay} keyed>{(overlay) => <Highlighter id={overlay.id} layout={overlay.layout} active={true} color="orange" style={{ ...transformStyle(overlay.transform) }} />}</Show2> </Portal2>; }; export { DragDropDebugger, DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter, closestCorners, createDraggable, createDroppable, createPointerSensor, createSortable, layoutStyle, maybeTransformStyle, mostIntersecting, transformStyle, useDragDropContext, useSortableContext };