65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
/** Drag-and-drop MIME + helpers for reordering sidebar labels / folders. */
|
|
|
|
export const SIDEBAR_NAV_DND_MIME = "application/x-ultimail-sidebar-nav"
|
|
|
|
export type SidebarNavDragPayload =
|
|
| { kind: "label"; id: string }
|
|
| { kind: "folder"; id: string }
|
|
|
|
export type SidebarNavDropPlacement = "before" | "after" | "inside"
|
|
|
|
export function setSidebarNavDragData(
|
|
e: React.DragEvent,
|
|
payload: SidebarNavDragPayload
|
|
) {
|
|
const encoded = JSON.stringify(payload)
|
|
e.dataTransfer.setData(SIDEBAR_NAV_DND_MIME, encoded)
|
|
/** Fallback for browsers that hide custom types until drop. */
|
|
e.dataTransfer.setData("text/plain", encoded)
|
|
e.dataTransfer.effectAllowed = "move"
|
|
}
|
|
|
|
export function readSidebarNavDragData(
|
|
e: React.DragEvent,
|
|
active: SidebarNavDragPayload | null = null
|
|
): SidebarNavDragPayload | null {
|
|
for (const mime of [SIDEBAR_NAV_DND_MIME, "text/plain"]) {
|
|
const raw = e.dataTransfer.getData(mime)
|
|
if (!raw) continue
|
|
try {
|
|
const parsed = JSON.parse(raw) as SidebarNavDragPayload
|
|
if (parsed?.kind === "label" || parsed?.kind === "folder") {
|
|
if (typeof parsed.id === "string" && parsed.id.length > 0) return parsed
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return active
|
|
}
|
|
|
|
export function hasSidebarNavDrag(e: React.DragEvent): boolean {
|
|
return e.dataTransfer.types.includes(SIDEBAR_NAV_DND_MIME)
|
|
}
|
|
|
|
/** Prefer React state — `dataTransfer.types` is unreliable during `dragover`. */
|
|
export function isSidebarNavDragEvent(
|
|
e: React.DragEvent,
|
|
active: SidebarNavDragPayload | null
|
|
): boolean {
|
|
return active !== null || hasSidebarNavDrag(e)
|
|
}
|
|
|
|
/** Top/bottom thirds = reorder; middle third = nest (folders only). */
|
|
export function resolveNavDropPlacement(
|
|
e: React.DragEvent<Element>,
|
|
allowNest: boolean
|
|
): SidebarNavDropPlacement {
|
|
const rect = e.currentTarget.getBoundingClientRect()
|
|
const ratio = (e.clientY - rect.top) / Math.max(rect.height, 1)
|
|
if (!allowNest) return ratio < 0.5 ? "before" : "after"
|
|
if (ratio < 0.25) return "before"
|
|
if (ratio > 0.75) return "after"
|
|
return "inside"
|
|
}
|