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.
This commit is contained in:
parent
186e3c0db6
commit
0add3f27d5
|
|
@ -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
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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<string, number>();
|
||||
layers().forEach((l, i) => map.set(toSortableId(l, i), i));
|
||||
return map;
|
||||
});
|
||||
|
||||
const updater = () =>
|
||||
side() === "front"
|
||||
|
|
@ -96,23 +104,23 @@ function LayerEditorPanel(props: LayerEditorPanelProps) {
|
|||
|
||||
<DragDropProvider
|
||||
onDragEnd={({ draggable, droppable }) => {
|
||||
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}
|
||||
>
|
||||
<DragDropSensors>
|
||||
<SortableProvider ids={ids()}>
|
||||
<SortableProvider ids={sortableIds()}>
|
||||
<For each={layers()}>
|
||||
{(layer, index) => (
|
||||
<LayerRow
|
||||
store={store}
|
||||
index={index()}
|
||||
sortableId={toSortableId(layer, index())}
|
||||
layer={layer}
|
||||
hovered={hoveredIndex() === index()}
|
||||
onHoverChange={setHoveredIndex}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import alignRightIcon from "./icons/align-right.png";
|
|||
export interface LayerRowProps {
|
||||
store: DeckStore;
|
||||
index: number;
|
||||
sortableId: string;
|
||||
layer: LayerConfig;
|
||||
hovered: boolean;
|
||||
onHoverChange: (idx: number | null) => 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 (
|
||||
<div
|
||||
ref={sortable}
|
||||
use:sortable={sortable}
|
||||
class={`flex items-center gap-1 py-1.5 px-1 relative group border-b border-gray-100 ${selected() ? "bg-blue-50" : ""}`}
|
||||
classList={{
|
||||
"opacity-25": sortable.isActiveDraggable,
|
||||
"transition-transform": !!dndState.active.draggable,
|
||||
"transition-transform":
|
||||
!!dndState.active.draggable && !sortable.isActiveDraggable,
|
||||
}}
|
||||
onMouseEnter={() => props.onHoverChange(props.index)}
|
||||
onMouseLeave={() => props.onHoverChange(null)}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import "solid-js";
|
||||
|
||||
declare module "solid-js" {
|
||||
namespace JSX {
|
||||
interface Directives {
|
||||
sortable: (
|
||||
el: HTMLElement,
|
||||
accessor: () => boolean | { skipTransform?: boolean } | undefined,
|
||||
) => void;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue