From 0add3f27d569b8bc3d7022d44e1ea7b9d43e4e86 Mon Sep 17 00:00:00 2001 From: hyper Date: Fri, 3 Jul 2026 21:49:18 +0800 Subject: [PATCH] fix(layer-editor): use stable _key for drag-and-drop sorting Add a unique `_key` to each layer config to maintain stable sortable IDs across reorder operations. Update LayerRow to use the sortableId prop and the `use:sortable` directive. Refactor layer-crud utilities to generate and propagate `_key` values on creation and initialization. --- content/deck-test.md | 4 +- .../md-deck/editor-panel/LayerEditorPanel.tsx | 28 +++++++----- .../md-deck/editor-panel/LayerRow.tsx | 8 ++-- src/components/md-deck/hooks/deckStore.ts | 8 +++- src/components/md-deck/hooks/layer-crud.ts | 43 +++++++++++++++---- src/components/md-deck/types.ts | 13 +++--- src/solid-dnd.directives.d.ts | 12 ++++++ 7 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 src/solid-dnd.directives.d.ts diff --git a/content/deck-test.md b/content/deck-test.md index e7f2a16..1e03fe5 100644 --- a/content/deck-test.md +++ b/content/deck-test.md @@ -2,6 +2,8 @@ :md-deck[./names.csv]{size="54x86" grid="5x8" bleed="1" padding="2" layers="name1:1,1-2,2f12 name2:4,7-5,8f12s num1:1,3-2,4f12 num2:4,5-5,6f12s"} +:md-deck[./names.csv]{size="54x86" grid="5x8" layers="num1:1,1-2,2 num2:4,5-5,6f12s name1:1,1-2,2 name2:1,1-2,2 name1:1,1-2,2" } + ## 使用 yaml/tag 语法创建 md-deck ```yaml/tag @@ -12,4 +14,4 @@ grid: 5x8 bleed: 1 padding: 2 layers: name1:1,1-2,2f12 name2:4,7-5,8f12s num1:1,3-2,4f12 num2:4,5-5,6f12s -``` \ No newline at end of file +``` diff --git a/src/components/md-deck/editor-panel/LayerEditorPanel.tsx b/src/components/md-deck/editor-panel/LayerEditorPanel.tsx index 06a43ac..3960d1b 100644 --- a/src/components/md-deck/editor-panel/LayerEditorPanel.tsx +++ b/src/components/md-deck/editor-panel/LayerEditorPanel.tsx @@ -3,10 +3,10 @@ import { DragDropProvider, DragDropSensors, SortableProvider, - useDragDropContext, closestCenter, } from "@thisbeyond/solid-dnd"; import type { DeckStore } from "../hooks/deckStore"; +import { toSortableId } from "../hooks/layer-crud"; import { LayerRow } from "./LayerRow"; export interface LayerEditorPanelProps { @@ -26,8 +26,16 @@ function LayerEditorPanel(props: LayerEditorPanelProps) { ? store.state.frontLayerConfigs : store.state.backLayerConfigs; - // Stable IDs for SortableProvider — simply the indices - const ids = createMemo(() => layers().map((_, i) => i)); + // Stable IDs from _key — don't change on reorder + const sortableIds = createMemo(() => + layers().map((l, i) => toSortableId(l, i)), + ); + // Lookup: sortable string ID → current array index + const idToIndex = createMemo(() => { + const map = new Map(); + layers().forEach((l, i) => map.set(toSortableId(l, i), i)); + return map; + }); const updater = () => side() === "front" @@ -96,23 +104,23 @@ function LayerEditorPanel(props: LayerEditorPanelProps) { { - if (droppable && draggable.id !== droppable.id) { - store.actions.reorderLayers( - side(), - draggable.id as number, - droppable.id as number, - ); + if (!droppable) return; + const from = idToIndex().get(draggable.id as string); + const to = idToIndex().get(droppable.id as string); + if (from !== undefined && to !== undefined && from !== to) { + store.actions.reorderLayers(side(), from, to); } }} collisionDetector={closestCenter} > - + {(layer, index) => ( void; @@ -64,7 +65,7 @@ function alignSrc(v: string) { } export function LayerRow(props: LayerRowProps) { - const sortable = createSortable(props.index); + const sortable = createSortable(props.sortableId); const [dndState] = useDragDropContext()!; const [fontOpen, setFontOpen] = createSignal(false); let fontRef: HTMLDivElement | undefined; @@ -89,11 +90,12 @@ export function LayerRow(props: LayerRowProps) { return (
props.onHoverChange(props.index)} onMouseLeave={() => props.onHoverChange(null)} diff --git a/src/components/md-deck/hooks/deckStore.ts b/src/components/md-deck/hooks/deckStore.ts index e99455a..544c18e 100644 --- a/src/components/md-deck/hooks/deckStore.ts +++ b/src/components/md-deck/hooks/deckStore.ts @@ -463,8 +463,12 @@ export function createDeckStore(initialSrc: string = ""): DeckStore { setState({ cards: data, activeTab: 0, - frontLayerConfigs: initLayerConfigsForSide(data, layersStr), - backLayerConfigs: initLayerConfigsForSide(data, backLayersStr), + frontLayerConfigs: layerCrud.withKeys( + initLayerConfigsForSide(data, layersStr), + ), + backLayerConfigs: layerCrud.withKeys( + initLayerConfigsForSide(data, backLayersStr), + ), isLoading: false, }); updateDimensions(); diff --git a/src/components/md-deck/hooks/layer-crud.ts b/src/components/md-deck/hooks/layer-crud.ts index 0b03d8e..9308870 100644 --- a/src/components/md-deck/hooks/layer-crud.ts +++ b/src/components/md-deck/hooks/layer-crud.ts @@ -1,8 +1,19 @@ -import type { CardSide, LayerConfig } from "../types"; +import type { LayerConfig } from "../types"; -/** Creates a new LayerConfig with default placement. */ +let nextKey = 1; + +export function resetKeyCounter(value = 1) { + nextKey = value; +} + +/** Creates a new LayerConfig with default placement and a stable _key. */ export function createDefaultLayer(prop: string): LayerConfig { - return { prop, visible: true, x1: 1, y1: 1, x2: 2, y2: 2 }; + return { prop, visible: true, x1: 1, y1: 1, x2: 2, y2: 2, _key: nextKey++ }; +} + +/** Assign stable _key to layers that don't have one (e.g. parsed from string). */ +export function withKeys(layers: LayerConfig[]): LayerConfig[] { + return layers.map((l) => (l._key ? l : { ...l, _key: nextKey++ })); } /** Insert a layer and return the new array. */ @@ -11,18 +22,33 @@ export function addLayer(configs: LayerConfig[], prop: string): LayerConfig[] { } /** Remove a layer by index and return the new array. */ -export function removeLayer(configs: LayerConfig[], index: number): LayerConfig[] { +export function removeLayer( + configs: LayerConfig[], + index: number, +): LayerConfig[] { return configs.filter((_, i) => i !== index); } /** Reorder a layer within the array. Returns new array. */ -export function reorderLayers(configs: LayerConfig[], from: number, to: number): LayerConfig[] { +export function reorderLayers( + configs: LayerConfig[], + from: number, + to: number, +): LayerConfig[] { const next = [...configs]; const [item] = next.splice(from, 1); next.splice(to, 0, item); return next; } +/** + * Convert layer _key to flake ID (unique string) for SortableProvider. + * Allows multiple instances of the same field name. + */ +export function toSortableId(layer: LayerConfig, index: number): string { + return layer._key ? `layer-${layer._key}` : `layer-idx-${index}`; +} + /** * Recalculate selectedLayer after a structural change (add/remove/reorder). * Returns the new selectedLayer index, or null. @@ -42,11 +68,12 @@ export function adjustSelectedLayer( if (action === "reorder" && to !== undefined) { if (currentSelected === from) return to; - if (from < to && currentSelected > from && currentSelected <= to) return currentSelected - 1; - if (from > to && currentSelected >= to && currentSelected < from) return currentSelected + 1; + if (from < to && currentSelected > from && currentSelected <= to) + return currentSelected - 1; + if (from > to && currentSelected >= to && currentSelected < from) + return currentSelected + 1; return currentSelected; } - // "add" — doesn't affect existing indices return currentSelected; } diff --git a/src/components/md-deck/types.ts b/src/components/md-deck/types.ts index e945fd5..6cb6485 100644 --- a/src/components/md-deck/types.ts +++ b/src/components/md-deck/types.ts @@ -2,9 +2,9 @@ export interface CardData { [key: string]: string; } -export type CardSide = 'front' | 'back'; +export type CardSide = "front" | "back"; -export type { CardShape } from '../../plotcutter/contour'; +export type { CardShape } from "../../plotcutter/contour"; export interface Layer { prop: string; @@ -12,9 +12,9 @@ export interface Layer { y1: number; x2: number; y2: number; - orientation?: 'n' | 's' | 'e' | 'w'; + orientation?: "n" | "s" | "e" | "w"; fontSize?: number; - align?: 'l' | 'c' | 'r'; + align?: "l" | "c" | "r"; } export interface LayerConfig { @@ -24,9 +24,10 @@ export interface LayerConfig { y1: number; x2: number; y2: number; - orientation?: 'n' | 's' | 'e' | 'w'; + orientation?: "n" | "s" | "e" | "w"; fontSize?: number; - align?: 'l' | 'c' | 'r'; + align?: "l" | "c" | "r"; + _key?: number; } export interface Dimensions { diff --git a/src/solid-dnd.directives.d.ts b/src/solid-dnd.directives.d.ts new file mode 100644 index 0000000..48a0d6b --- /dev/null +++ b/src/solid-dnd.directives.d.ts @@ -0,0 +1,12 @@ +import "solid-js"; + +declare module "solid-js" { + namespace JSX { + interface Directives { + sortable: ( + el: HTMLElement, + accessor: () => boolean | { skipTransform?: boolean } | undefined, + ) => void; + } + } +}