@thisbeyond/solid-dnd
Version:
A lightweight drag and drop toolkit for Solid.
1,254 lines (1,236 loc) • 37.3 kB
JSX
// 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
};