Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { useCallback, useRef, useState } from "react";
// Append-to-end drop slot for the has-pins case. Dragging a frecent (a new pin,
// not an existing one) opens one reserved, net-zero placeholder right after the
// last pin, hidden until the cursor is over it — the same mechanic as
// useZeroPinDrop, just at the end of an existing group. It layers on top of
// useTopSitesDnD's insert-anywhere reflow: over a pin you get the reflow, over
// this slot you append.
//
// Placeholders aren't native drop targets in grouped mode (_allowDrop gates on
// isPinned), so the drop is caught at the list via geometry. The preview and the
// commit reuse useTopSitesDnD's onDragEvent: a no-index dragover clears the
// reflow while this slot owns the interaction, and the drop replays at
// appendIndex so the existing reorder/telemetry path commits it.
export function useAppendPinDrop({
baseSites,
draggedSite,
isMovable,
onDragEvent, // shared with useTopSitesDnD
previewActive, // a reflow preview is showing (cursor is over a pin)
}) {
const [over, setOver] = useState(false);
const placeholderElRef = useRef(null);
const setRef = useCallback(el => {
placeholderElRef.current = el;
}, []);
const enabled = !!draggedSite && isMovable(draggedSite);
// First free slot after the contiguous pinned block.
const lastPinIndex = baseSites.reduce(
(last, site, i) => (site?.isPinned ? i : last),
-1
);
const appendIndex = lastPinIndex + 1;
const isOver = useCallback(event => {
const rect = placeholderElRef.current?.getBoundingClientRect();
return (
!!rect &&
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom
);
}, []);
const onDragOver = useCallback(
event => {
if (!enabled) {
return;
}
const nowOver = isOver(event);
if (nowOver) {
event.preventDefault();
// This slot owns the preview now; drop the insert-anywhere reflow.
onDragEvent(event);
}
if (over !== nowOver) {
setOver(nowOver);
}
},
[enabled, isOver, over, onDragEvent]
);
const onDrop = useCallback(
event => {
if (!enabled || !isOver(event)) {
return;
}
event.preventDefault();
// Commit through the reorder hook's drop at the append slot.
onDragEvent(event, appendIndex);
},
[enabled, isOver, onDragEvent, appendIndex]
);
const onDragLeave = useCallback(
event => {
if (event.currentTarget.contains(event.relatedTarget)) {
return;
}
if (over) {
setOver(false);
}
},
[over]
);
// Collapse the dragged source out of flow so the reserved slot nets zero cells.
const sites = enabled
? baseSites.map(site =>
site && site.url === draggedSite.url
? { ...site, isCollapsed: true }
: site
)
: baseSites;
return {
sites,
listProps: { onDragOver, onDrop, onDragLeave },
decorations: {
// Hide the reserved slot while the reflow drives (cursor over a pin), so we
// don't add a cell on top of the reflow's own growth.
zeroPinSlot: enabled && !previewActive ? appendIndex : -1,
overZeroPin: over,
setZeroPinRef: setRef,
},
};
}