Last active
May 8, 2025 13:08
-
-
Save thesandybridge/06d76fcd6bec2f64eda4f40ef18a2262 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { UniqueIdentifier } from '@dnd-kit/core' | |
import { BaseBlock, BlockIndex } from '../types/block' | |
import { extractUUID } from './helper' | |
/** | |
* Creates a shallow clone of a Map. | |
* | |
* @template K - The type of the map's keys | |
* @template V - The type of the map's values | |
* @param {Map<K, V>} map - The map to clone | |
* @returns {Map<K, V>} A new map with the same key-value pairs | |
*/ | |
export function cloneMap<K, V>(map: Map<K, V>): Map<K, V> { | |
return new Map(map) | |
} | |
/** | |
* Deep clones a map of parent-child relationships, where values are arrays. | |
* | |
* @param {Map<string | null, string[]>} map - Map of parent IDs to ordered child ID arrays | |
* @returns {Map<string | null, string[]>} A new map with cloned arrays to preserve immutability | |
*/ | |
export function cloneParentMap(map: Map<string | null, string[]>): Map<string | null, string[]> { | |
const newMap = new Map<string | null, string[]>() | |
for (const [k, v] of map.entries()) { | |
newMap.set(k, [...v]) | |
} | |
return newMap | |
} | |
/** | |
* Reparents and reorders a block within a nested tree structure. | |
* | |
* Uses DnD hover zone metadata to determine whether the block should move into or after another target, | |
* then updates both `byId` and `byParent` mappings to reflect the new state. | |
* | |
* @template T - The block type extending BaseBlock | |
* @param {BlockIndex<T>} state - The current block index (byId and byParent) | |
* @param {UniqueIdentifier} activeId - The ID of the dragged block | |
* @param {string} hoverZone - The drop target identifier (e.g., "into-uuid", "after-uuid") | |
* @returns {BlockIndex<T>} The updated block index, or the original if no change is needed | |
*/ | |
export function reparentBlockIndex<T extends BaseBlock>( | |
state: BlockIndex<T>, | |
activeId: UniqueIdentifier, | |
hoverZone: string | |
): BlockIndex<T> { | |
const byId = cloneMap(state.byId) | |
const byParent = cloneParentMap(state.byParent) | |
const dragged = byId.get(String(activeId)) | |
if (!dragged) return state | |
const zoneTargetId = extractUUID(hoverZone) | |
const isAfter = hoverZone.startsWith('after-') | |
const isInto = hoverZone.startsWith('into-') | |
const target = byId.get(zoneTargetId) | |
const oldParentId = dragged.parentId ?? null | |
const newParentId = isInto ? zoneTargetId : target?.parentId ?? null | |
// Prevent section nesting or self-reparenting | |
if (dragged.type === 'section' && newParentId !== null) return state | |
if (dragged.id === zoneTargetId) return state | |
// Remove dragged from its old parent | |
const oldList = byParent.get(oldParentId) ?? [] | |
const filtered = oldList.filter(id => id !== dragged.id) | |
byParent.set(oldParentId, filtered) | |
// Insert dragged into its new parent | |
const newList = [...(byParent.get(newParentId) ?? [])] | |
let insertIndex = newList.length | |
if (!isInto) { | |
const idx = newList.indexOf(zoneTargetId) | |
insertIndex = idx === -1 ? newList.length : isAfter ? idx + 1 : idx | |
} | |
const currentIndex = newList.indexOf(dragged.id) | |
// No-op: already in correct position | |
if ( | |
dragged.parentId === newParentId && | |
currentIndex === insertIndex | |
) { | |
return state | |
} | |
newList.splice(insertIndex, 0, dragged.id) | |
byParent.set(newParentId, newList) | |
byId.set(dragged.id, { | |
...dragged, | |
parentId: newParentId, | |
}) | |
return { byId, byParent } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment