Skip to content

Instantly share code, notes, and snippets.

@thesandybridge
Last active May 8, 2025 13:08
Show Gist options
  • Save thesandybridge/06d76fcd6bec2f64eda4f40ef18a2262 to your computer and use it in GitHub Desktop.
Save thesandybridge/06d76fcd6bec2f64eda4f40ef18a2262 to your computer and use it in GitHub Desktop.
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