Skip to content

Commit

Permalink
wip: deprecate pointer-event in favour of NestedDroppablePlugin
Browse files Browse the repository at this point in the history
Works on mobile and doesn't conflict with dnd-kit setPointerCapture bugfix, which is necessary to ensure reparenting on mobile devices don't break pointer tracking.
  • Loading branch information
chrisvxd committed Sep 26, 2024
1 parent d1f3f18 commit 46626a4
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 288 deletions.
202 changes: 202 additions & 0 deletions packages/core/components/DragDropContext/NestedDroppablePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { DragDropManager } from "@dnd-kit/dom";
import { Plugin } from "@dnd-kit/abstract";

import type { Droppable } from "@dnd-kit/dom";

import { effects } from "../../../../../dnd-kit/packages/state/dist";
import { BoundingRectangle } from "@dnd-kit/geometry";
import { throttle } from "../../lib/throttle";

interface Position {
x: number;
y: number;
}

function isPositionInsideRect(
position: Position,
rect: BoundingRectangle
): boolean {
return (
position.x >= rect.left &&
position.x <= rect.right &&
position.y >= rect.top &&
position.y <= rect.bottom
);
}

type NestedDroppablePluginOptions = {
onChange: (params: {
deepestAreaId: string | null;
deepestZoneId: string | null;
}) => void;
};

// Something is going wrong with tearing down the classes.
// This is an awful hack to prevent the plugin from running more than once.
let globallyRegistered = false;

// Pixels to buffer sortable items by, helping when
// 2 items are butted up against each-other
const BUFFER_ZONE = 8;

// Force shapes to refresh on mouse move.
// TODO Expensive - can I remove this? Or restrict to during drag only?
const REFRESH_ON_MOVE = true;

const depthSort = (candidates: Droppable[]) => {
return candidates.sort((a, b) => {
if (!a.element || !b.element) return 0;

// TODO could be swapped out for `data.depth` on the candidate for better performance
if (a.element.contains(b.element)) return -1;

return 1;
});
};

const getZoneId = (candidate: Droppable | undefined) => {
let id: string | null = candidate?.id as string;

if (!candidate) return null;

if (!candidate.data.zone) {
if (candidate.data.containsActiveZone) {
id = null;
} else {
id = candidate.data.group;
}
}

return id;
};

const getAreaId = (candidate: Droppable) => {
if (candidate.data.containsActiveZone) {
return candidate.id as string;
}

return null;
};

const getDeepestId = (
candidates: Droppable[],
idFn: (candidate: Droppable) => string | null
) => {
let id: string | null = null;

for (let i = 0; i < candidates.length; i++) {
const candidate = candidates[i];

id = idFn(candidate);

if (id) break;
}

return id;
};

const expandHitBox = (rect: BoundingRectangle): BoundingRectangle => {
return {
bottom: rect.bottom + BUFFER_ZONE,
top: rect.top - BUFFER_ZONE,
width: rect.width + BUFFER_ZONE * 2,
height: rect.height + BUFFER_ZONE * 2,
left: rect.left - BUFFER_ZONE,
right: rect.right + BUFFER_ZONE,
};
};

export const createNestedDroppablePlugin = ({
onChange,
}: NestedDroppablePluginOptions) =>
class NestedDroppablePlugin extends Plugin<DragDropManager, {}> {
constructor(manager: DragDropManager, options?: {}) {
super(manager);

const cleanupEffect = effects(() => {
const getPointerCollisions = (position: Position) => {
const candidates: Droppable[] = [];
for (const droppable of manager.registry.droppables.value) {
if (droppable.shape) {
let rect = droppable.shape.boundingRectangle;

const isNotSourceZone =
droppable.id !==
(manager.dragOperation.source?.data.group ||
manager.dragOperation.source?.id);

const isNotTargetZone =
droppable.id !==
(manager.dragOperation.source?.data.group ||
manager.dragOperation.source?.id);

// Expand hitboxes on zones
if (droppable.data.zone && isNotSourceZone && isNotTargetZone) {
rect = expandHitBox(rect);
}

if (isPositionInsideRect(position, rect)) {
candidates.push(droppable);
}
}
}

return candidates;
};

const handleMove = (position: Position) => {
if (REFRESH_ON_MOVE) {
for (const droppable of manager.registry.droppables.value) {
droppable.refreshShape();
}
}

const candidates = getPointerCollisions(position);

if (candidates.length > 0) {
const sortedCandidates = depthSort(candidates);

const draggedCandidateIndex = sortedCandidates.findIndex(
(candidate) => candidate.id === manager.dragOperation.source?.id
);

const nonDraggedCandidates =
draggedCandidateIndex > -1
? sortedCandidates.slice(0, draggedCandidateIndex)
: sortedCandidates;

nonDraggedCandidates.reverse();

const deepestZoneId = getZoneId(nonDraggedCandidates[0]);
const deepestAreaId = getDeepestId(nonDraggedCandidates, getAreaId);

onChange({ deepestZoneId, deepestAreaId });
}
};

const handleMoveThrottled = throttle(handleMove, 50);

const handlePointerMove = (event: PointerEvent) => {
handleMoveThrottled({
x: event.clientX,
y: event.clientY,
});
};

// For some reason, this is getting instantiated multiple times. Hack to avoid it.
if (globallyRegistered) {
return;
}

document.body.addEventListener("pointermove", handlePointerMove);

globallyRegistered = true;

this.destroy = () => {
globallyRegistered = false;
document.body.removeEventListener("pointermove", handlePointerMove);
cleanupEffect();
};
});
}
};
21 changes: 20 additions & 1 deletion packages/core/components/DragDropContext/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getItem, ItemSelector } from "../../lib/get-item";
import { PathData } from "../DropZone/context";
import { getZoneId } from "../../lib/get-zone-id";
import { Direction } from "../DraggableComponent/collision/dynamic";
import { createNestedDroppablePlugin } from "./NestedDroppablePlugin";

type Events = DragDropEvents<Draggable, Droppable, DragDropManager>;
type DragCbs = Partial<{ [eventName in keyof Events]: Events[eventName][] }>;
Expand Down Expand Up @@ -51,7 +52,23 @@ export function useDragListener(
export const DragDropContext = ({ children }: { children: ReactNode }) => {
const { state, config, deferred, dispatch } = useAppContext();
const { data } = deferred?.isDeferred ? deferred.state : state;
const [manager] = useState(new DragDropManager({ plugins: [Feedback] }));
const [deepest, setDeepest] = useState<{
zone: string | null;
area: string | null;
} | null>(null);

const [manager] = useState(
new DragDropManager({
plugins: [
Feedback,
createNestedDroppablePlugin({
onChange: ({ deepestZoneId, deepestAreaId }) => {
setDeepest({ zone: deepestZoneId, area: deepestAreaId });
},
}),
],
})
);

const [draggedItem, setDraggedItem] = useState<Draggable | null>();

Expand Down Expand Up @@ -249,6 +266,8 @@ export const DragDropContext = ({ children }: { children: ReactNode }) => {
collisionPriority: 1,
registerPath,
pathData,
deepestZone: deepest?.zone,
deepestArea: deepest?.area,
}}
>
{children}
Expand Down
78 changes: 23 additions & 55 deletions packages/core/components/DraggableComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const DraggableComponent = ({
isSelected = false,
debug,
label,
indicativeHover = false,
isEnabled,
dragAxis,
inDroppableZone = true,
Expand All @@ -56,7 +55,6 @@ export const DraggableComponent = ({
label?: string;
isLoading: boolean;
isEnabled?: boolean;
indicativeHover?: boolean;
dragAxis: DragAxis;
inDroppableZone: boolean;
}) => {
Expand All @@ -66,23 +64,6 @@ export const DraggableComponent = ({

const overlayRef = useRef<HTMLDivElement>(null);

const { ref: sortableRef, status } = useSortable({
id,
index,
group: zoneCompound,
data: { group: zoneCompound, index, componentType },
collisionPriority: isEnabled ? collisionPriority : 0,
collisionDetector: createDynamicCollisionDetector(dragAxis),
disabled: !isEnabled,
// handle: overlayRef,
});

const userIsDragging = !!ctx?.draggedItem;

const thisIsDragging = status === "dragging";

const ref = useRef<Element>();

const [localZones, setLocalZones] = useState<Record<string, boolean>>({});

// TODO 26/08/24 this doesn't work when we have more than one level of zone
Expand All @@ -105,6 +86,28 @@ export const DraggableComponent = ({
const containsActiveZone =
Object.values(localZones).filter(Boolean).length > 0;

const { ref: sortableRef, status } = useSortable({
id,
index,
group: zoneCompound,
data: {
group: zoneCompound,
index,
componentType,
containsActiveZone,
},
collisionPriority: isEnabled ? collisionPriority : 0,
collisionDetector: createDynamicCollisionDetector(dragAxis),
disabled: !isEnabled,
// handle: overlayRef,
});

const userIsDragging = !!ctx?.draggedItem;

const thisIsDragging = status === "dragging";

const ref = useRef<Element>();

const refSetter = useCallback(
(el: Element | null) => {
sortableRef(el);
Expand Down Expand Up @@ -199,17 +202,7 @@ export const DraggableComponent = ({

const [hover, setHover] = useState(false);

const activateParent = useCallback(() => {
if (inDroppableZone) {
if (ctx?.setHoveringArea) {
ctx.setHoveringArea(ctx.areaId || "");
}

if (ctx?.setHoveringZone) {
ctx.setHoveringZone(zoneCompound);
}
}
}, [inDroppableZone, ctx, zoneCompound]);
const indicativeHover = ctx?.hoveringComponent === id;

useEffect(() => {
if (!ref.current) {
Expand All @@ -231,10 +224,6 @@ export const DraggableComponent = ({
}

e.stopPropagation();

if (!containsActiveZone) {
activateParent();
}
};

const _onMouseOut = (e: Event) => {
Expand Down Expand Up @@ -389,27 +378,6 @@ export const DraggableComponent = ({
</div>
</div>
<div className={getClassName("overlay")} />

{ctx?.hoveringArea === id && (
<>
<div
className={getClassName("parentHitboxTop")}
onMouseOver={activateParent}
></div>
<div
className={getClassName("parentHitboxBottom")}
onMouseOver={activateParent}
></div>
<div
className={getClassName("parentHitboxLeft")}
onMouseOver={activateParent}
></div>
<div
className={getClassName("parentHitboxRight")}
onMouseOver={activateParent}
></div>
</>
)}
</div>,
document.getElementById("puck-preview") || document.body
)}
Expand Down
Loading

0 comments on commit 46626a4

Please sign in to comment.