Compare commits
No commits in common. "e4cca026a0761b759677ba26c9f3951e19c408ca" and "6b18665ef93f26641099f181c52393b1ce18dba3" have entirely different histories.
e4cca026a0
...
6b18665ef9
@ -29,19 +29,14 @@ export function ContextMenu(props: {
|
|||||||
state: ContextMenuState | null;
|
state: ContextMenuState | null;
|
||||||
nodes: WidgetNode[];
|
nodes: WidgetNode[];
|
||||||
selectionIds: string[];
|
selectionIds: string[];
|
||||||
clipboardHasNodes: boolean;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onAddTextAt: (x: number, y: number) => void;
|
onAddTextAt: (x: number, y: number) => void;
|
||||||
onSelectSingle?: (id?: string) => void;
|
onSelectSingle?: (id?: string) => void;
|
||||||
onSelectAll?: () => void;
|
onSelectAll?: () => void;
|
||||||
onCopySelected?: () => void;
|
|
||||||
onPasteAt?: (x: number, y: number) => void;
|
|
||||||
onDuplicateSelected?: () => void;
|
onDuplicateSelected?: () => void;
|
||||||
onToggleLockSelected?: () => void;
|
onToggleLockSelected?: () => void;
|
||||||
onToggleHideSelected?: () => void;
|
onToggleHideSelected?: () => void;
|
||||||
onBringToFrontSelected?: () => void;
|
onBringToFrontSelected?: () => void;
|
||||||
onBringForwardSelected?: () => void;
|
|
||||||
onSendBackwardSelected?: () => void;
|
|
||||||
onSendToBackSelected?: () => void;
|
onSendToBackSelected?: () => void;
|
||||||
onDeleteSelected?: () => void;
|
onDeleteSelected?: () => void;
|
||||||
}) {
|
}) {
|
||||||
@ -199,24 +194,6 @@ export function ContextMenu(props: {
|
|||||||
|
|
||||||
<div style={{ height: 1, margin: '6px 0', background: 'rgba(255,255,255,0.08)' }} />
|
<div style={{ height: 1, margin: '6px 0', background: 'rgba(255,255,255,0.08)' }} />
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
label="Copy"
|
|
||||||
disabled={!hasSelection || !props.onCopySelected}
|
|
||||||
onClick={() => {
|
|
||||||
props.onCopySelected?.();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
label="Paste"
|
|
||||||
disabled={!props.clipboardHasNodes || !props.onPasteAt}
|
|
||||||
onClick={() => {
|
|
||||||
props.onPasteAt?.(ctx.worldX, ctx.worldY);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Duplicate"
|
label="Duplicate"
|
||||||
disabled={!canModifySelection || !props.onDuplicateSelected}
|
disabled={!canModifySelection || !props.onDuplicateSelected}
|
||||||
@ -265,22 +242,6 @@ export function ContextMenu(props: {
|
|||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
|
||||||
label="Bring Forward"
|
|
||||||
disabled={!canModifySelection || !props.onBringForwardSelected}
|
|
||||||
onClick={() => {
|
|
||||||
props.onBringForwardSelected?.();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
|
||||||
label="Send Backward"
|
|
||||||
disabled={!canModifySelection || !props.onSendBackwardSelected}
|
|
||||||
onClick={() => {
|
|
||||||
props.onSendBackwardSelected?.();
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
label="Send To Back"
|
label="Send To Back"
|
||||||
disabled={!canModifySelection || !props.onSendToBackSelected}
|
disabled={!canModifySelection || !props.onSendToBackSelected}
|
||||||
|
|||||||
@ -502,60 +502,14 @@ export function EditorApp() {
|
|||||||
|
|
||||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>尺寸</Typography.Text>
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>尺寸</Typography.Text>
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
<InputNumber
|
<InputNumber size="small" value={selected?.rect.w ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
size="small"
|
<InputNumber size="small" value={selected?.rect.h ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
value={selected?.rect.w ?? 0}
|
|
||||||
controls={false}
|
|
||||||
min={1}
|
|
||||||
disabled={!selected || selected.locked}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (!selected) return;
|
|
||||||
if (typeof v !== 'number') return;
|
|
||||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { w: v } });
|
|
||||||
}}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
/>
|
|
||||||
<InputNumber
|
|
||||||
size="small"
|
|
||||||
value={selected?.rect.h ?? 0}
|
|
||||||
controls={false}
|
|
||||||
min={1}
|
|
||||||
disabled={!selected || selected.locked}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (!selected) return;
|
|
||||||
if (typeof v !== 'number') return;
|
|
||||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { h: v } });
|
|
||||||
}}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>位置</Typography.Text>
|
<Typography.Text style={{ color: '#94a3b8', fontSize: 12 }}>位置</Typography.Text>
|
||||||
<Space size={8}>
|
<Space size={8}>
|
||||||
<InputNumber
|
<InputNumber size="small" value={selected?.rect.x ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
size="small"
|
<InputNumber size="small" value={selected?.rect.y ?? 0} controls={false} readOnly style={{ width: 120 }} />
|
||||||
value={selected?.rect.x ?? 0}
|
|
||||||
controls={false}
|
|
||||||
disabled={!selected || selected.locked}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (!selected) return;
|
|
||||||
if (typeof v !== 'number') return;
|
|
||||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { x: v } });
|
|
||||||
}}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
/>
|
|
||||||
<InputNumber
|
|
||||||
size="small"
|
|
||||||
value={selected?.rect.y ?? 0}
|
|
||||||
controls={false}
|
|
||||||
disabled={!selected || selected.locked}
|
|
||||||
onChange={(v) => {
|
|
||||||
if (!selected) return;
|
|
||||||
if (typeof v !== 'number') return;
|
|
||||||
dispatch({ type: 'updateWidgetRect', id: selected.id, rect: { y: v } });
|
|
||||||
}}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -682,19 +636,14 @@ export function EditorApp() {
|
|||||||
state={ctxMenu}
|
state={ctxMenu}
|
||||||
nodes={state.doc.screen.nodes}
|
nodes={state.doc.screen.nodes}
|
||||||
selectionIds={state.selection.ids}
|
selectionIds={state.selection.ids}
|
||||||
clipboardHasNodes={state.clipboard.nodes.length > 0}
|
|
||||||
onClose={closeContextMenu}
|
onClose={closeContextMenu}
|
||||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||||
onSelectAll={() => dispatch({ type: 'selectAll' })}
|
onSelectAll={() => dispatch({ type: 'selectAll' })}
|
||||||
onCopySelected={() => dispatchWithMenuSelection({ type: 'copySelected' })}
|
|
||||||
onPasteAt={(x, y) => dispatch({ type: 'pasteClipboard', at: { x, y } })}
|
|
||||||
onDuplicateSelected={() => dispatchWithMenuSelection({ type: 'duplicateSelected' })}
|
onDuplicateSelected={() => dispatchWithMenuSelection({ type: 'duplicateSelected' })}
|
||||||
onToggleLockSelected={() => dispatchWithMenuSelection({ type: 'toggleLockSelected' })}
|
onToggleLockSelected={() => dispatchWithMenuSelection({ type: 'toggleLockSelected' })}
|
||||||
onToggleHideSelected={() => dispatchWithMenuSelection({ type: 'toggleHideSelected' })}
|
onToggleHideSelected={() => dispatchWithMenuSelection({ type: 'toggleHideSelected' })}
|
||||||
onBringToFrontSelected={() => dispatchWithMenuSelection({ type: 'bringToFrontSelected' })}
|
onBringToFrontSelected={() => dispatchWithMenuSelection({ type: 'bringToFrontSelected' })}
|
||||||
onBringForwardSelected={() => dispatchWithMenuSelection({ type: 'bringForwardSelected' })}
|
|
||||||
onSendBackwardSelected={() => dispatchWithMenuSelection({ type: 'sendBackwardSelected' })}
|
|
||||||
onSendToBackSelected={() => dispatchWithMenuSelection({ type: 'sendToBackSelected' })}
|
onSendToBackSelected={() => dispatchWithMenuSelection({ type: 'sendToBackSelected' })}
|
||||||
onDeleteSelected={() => dispatchWithMenuSelection({ type: 'deleteSelected' })}
|
onDeleteSelected={() => dispatchWithMenuSelection({ type: 'deleteSelected' })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -59,15 +59,10 @@ export type EditorAction =
|
|||||||
| { type: 'deleteSelected' }
|
| { type: 'deleteSelected' }
|
||||||
| { type: 'nudgeSelected'; dx: number; dy: number }
|
| { type: 'nudgeSelected'; dx: number; dy: number }
|
||||||
| { type: 'duplicateSelected' }
|
| { type: 'duplicateSelected' }
|
||||||
| { type: 'copySelected' }
|
|
||||||
| { type: 'pasteClipboard'; at?: { x: number; y: number } }
|
|
||||||
| { type: 'toggleLockSelected' }
|
| { type: 'toggleLockSelected' }
|
||||||
| { type: 'toggleHideSelected' }
|
| { type: 'toggleHideSelected' }
|
||||||
| { type: 'bringToFrontSelected' }
|
| { type: 'bringToFrontSelected' }
|
||||||
| { type: 'bringForwardSelected' }
|
|
||||||
| { type: 'sendBackwardSelected' }
|
|
||||||
| { type: 'sendToBackSelected' }
|
| { type: 'sendToBackSelected' }
|
||||||
| { type: 'updateWidgetRect'; id: string; rect: Partial<Rect> }
|
|
||||||
| UpdateWidgetPropsAction;
|
| UpdateWidgetPropsAction;
|
||||||
|
|
||||||
interface DragSession {
|
interface DragSession {
|
||||||
@ -160,66 +155,6 @@ function ensureSelected(state: EditorRuntimeState, id: string): EditorRuntimeSta
|
|||||||
return { ...state, selection: { ids: [id] } };
|
return { ...state, selection: { ids: [id] } };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortNodesBackToFront(nodes: WidgetNode[]): WidgetNode[] {
|
|
||||||
return nodes
|
|
||||||
.map((n, index) => ({ n, index, z: n.zIndex ?? index }))
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.z !== b.z) return a.z - b.z;
|
|
||||||
return a.index - b.index;
|
|
||||||
})
|
|
||||||
.map((e) => e.n);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeZIndexBackToFront(nodesBackToFront: WidgetNode[]): WidgetNode[] {
|
|
||||||
return nodesBackToFront.map((n, idx) => ({ ...n, zIndex: idx }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function reorderSelected(
|
|
||||||
nodes: WidgetNode[],
|
|
||||||
selectedIds: Set<string>,
|
|
||||||
mode: 'toFront' | 'toBack' | 'forward' | 'backward',
|
|
||||||
): WidgetNode[] {
|
|
||||||
const ordered = sortNodesBackToFront(nodes);
|
|
||||||
|
|
||||||
const isSelected = (n: WidgetNode) => selectedIds.has(n.id);
|
|
||||||
|
|
||||||
if (mode === 'toFront') {
|
|
||||||
const rest = ordered.filter((n) => !isSelected(n));
|
|
||||||
const picked = ordered.filter((n) => isSelected(n));
|
|
||||||
return normalizeZIndexBackToFront([...rest, ...picked]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'toBack') {
|
|
||||||
const picked = ordered.filter((n) => isSelected(n));
|
|
||||||
const rest = ordered.filter((n) => !isSelected(n));
|
|
||||||
return normalizeZIndexBackToFront([...picked, ...rest]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const arr = [...ordered];
|
|
||||||
|
|
||||||
if (mode === 'forward') {
|
|
||||||
// swap with next non-selected, from front backwards
|
|
||||||
for (let i = arr.length - 2; i >= 0; i--) {
|
|
||||||
if (!isSelected(arr[i]!)) continue;
|
|
||||||
if (isSelected(arr[i + 1]!)) continue;
|
|
||||||
const tmp = arr[i]!;
|
|
||||||
arr[i] = arr[i + 1]!;
|
|
||||||
arr[i + 1] = tmp;
|
|
||||||
}
|
|
||||||
return normalizeZIndexBackToFront(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mode === 'backward'
|
|
||||||
for (let i = 1; i < arr.length; i++) {
|
|
||||||
if (!isSelected(arr[i]!)) continue;
|
|
||||||
if (isSelected(arr[i - 1]!)) continue;
|
|
||||||
const tmp = arr[i]!;
|
|
||||||
arr[i] = arr[i - 1]!;
|
|
||||||
arr[i - 1] = tmp;
|
|
||||||
}
|
|
||||||
return normalizeZIndexBackToFront(arr);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createInitialState(): EditorRuntimeState {
|
export function createInitialState(): EditorRuntimeState {
|
||||||
const screen = createEmptyScreen({
|
const screen = createEmptyScreen({
|
||||||
width: 1920,
|
width: 1920,
|
||||||
@ -258,7 +193,6 @@ export function createInitialState(): EditorRuntimeState {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
keyboard: { ctrl: false, space: false },
|
keyboard: { ctrl: false, space: false },
|
||||||
clipboard: { nodes: [] },
|
|
||||||
history: { past: [], future: [] },
|
history: { past: [], future: [] },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -337,41 +271,6 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'updateWidgetRect': {
|
|
||||||
const target = state.doc.screen.nodes.find((n) => n.id === action.id);
|
|
||||||
if (!target) return state;
|
|
||||||
if (target.locked) return state;
|
|
||||||
|
|
||||||
const next: Rect = {
|
|
||||||
x: typeof action.rect.x === 'number' ? action.rect.x : target.rect.x,
|
|
||||||
y: typeof action.rect.y === 'number' ? action.rect.y : target.rect.y,
|
|
||||||
w: typeof action.rect.w === 'number' ? action.rect.w : target.rect.w,
|
|
||||||
h: typeof action.rect.h === 'number' ? action.rect.h : target.rect.h,
|
|
||||||
};
|
|
||||||
|
|
||||||
const bounds = { w: state.doc.screen.width, h: state.doc.screen.height };
|
|
||||||
const rect = clampRectToBounds(
|
|
||||||
{
|
|
||||||
x: Math.round(next.x),
|
|
||||||
y: Math.round(next.y),
|
|
||||||
w: Math.round(next.w),
|
|
||||||
h: Math.round(next.h),
|
|
||||||
},
|
|
||||||
bounds,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...historyPush(state),
|
|
||||||
doc: {
|
|
||||||
screen: {
|
|
||||||
...state.doc.screen,
|
|
||||||
nodes: state.doc.screen.nodes.map((n) => (n.id === action.id ? { ...n, rect } : n)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'updateWidgetProps': {
|
case 'updateWidgetProps': {
|
||||||
return updateWidgetProps(state, action);
|
return updateWidgetProps(state, action);
|
||||||
}
|
}
|
||||||
@ -441,56 +340,6 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'copySelected': {
|
|
||||||
if (!state.selection.ids.length) return state;
|
|
||||||
const ids = new Set(state.selection.ids);
|
|
||||||
const nodes = state.doc.screen.nodes.filter((n) => ids.has(n.id));
|
|
||||||
if (!nodes.length) return state;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
clipboard: {
|
|
||||||
nodes: nodes.map((n) => ({ ...n, rect: { ...n.rect } })),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'pasteClipboard': {
|
|
||||||
const clip = state.clipboard.nodes;
|
|
||||||
if (!clip.length) return state;
|
|
||||||
|
|
||||||
// Compute bounds of clipboard content.
|
|
||||||
let minX = Infinity;
|
|
||||||
let minY = Infinity;
|
|
||||||
|
|
||||||
for (const n of clip) {
|
|
||||||
minX = Math.min(minX, n.rect.x);
|
|
||||||
minY = Math.min(minY, n.rect.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = { w: state.doc.screen.width, h: state.doc.screen.height };
|
|
||||||
|
|
||||||
const dx = action.at ? Math.round(action.at.x - minX) : 20;
|
|
||||||
const dy = action.at ? Math.round(action.at.y - minY) : 20;
|
|
||||||
|
|
||||||
const clones: WidgetNode[] = clip.map((n) => {
|
|
||||||
const id = `${n.type}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
||||||
const rect = clampRectToBounds({ ...n.rect, x: n.rect.x + dx, y: n.rect.y + dy }, bounds, 50);
|
|
||||||
return {
|
|
||||||
...n,
|
|
||||||
id,
|
|
||||||
rect,
|
|
||||||
locked: false,
|
|
||||||
hidden: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...historyPush(state),
|
|
||||||
doc: { screen: { ...state.doc.screen, nodes: [...state.doc.screen.nodes, ...clones] } },
|
|
||||||
selection: { ids: clones.map((c) => c.id) },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'toggleLockSelected': {
|
case 'toggleLockSelected': {
|
||||||
if (!state.selection.ids.length) return state;
|
if (!state.selection.ids.length) return state;
|
||||||
const ids = new Set(state.selection.ids);
|
const ids = new Set(state.selection.ids);
|
||||||
@ -533,59 +382,27 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
|
|
||||||
case 'bringToFrontSelected': {
|
case 'bringToFrontSelected': {
|
||||||
if (!state.selection.ids.length) return state;
|
if (!state.selection.ids.length) return state;
|
||||||
const ids = new Set(
|
const ids = new Set(state.selection.ids);
|
||||||
state.doc.screen.nodes
|
const nodes = state.doc.screen.nodes;
|
||||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
|
||||||
.map((n) => n.id),
|
|
||||||
);
|
|
||||||
if (!ids.size) return state;
|
|
||||||
|
|
||||||
return {
|
let maxZ = 0;
|
||||||
...historyPush(state),
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
doc: {
|
const n = nodes[i]!;
|
||||||
screen: {
|
const z = n.zIndex ?? i;
|
||||||
...state.doc.screen,
|
if (z > maxZ) maxZ = z;
|
||||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'toFront'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'bringForwardSelected': {
|
let bump = 0;
|
||||||
if (!state.selection.ids.length) return state;
|
|
||||||
const ids = new Set(
|
|
||||||
state.doc.screen.nodes
|
|
||||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
|
||||||
.map((n) => n.id),
|
|
||||||
);
|
|
||||||
if (!ids.size) return state;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...historyPush(state),
|
...historyPush(state),
|
||||||
doc: {
|
doc: {
|
||||||
screen: {
|
screen: {
|
||||||
...state.doc.screen,
|
...state.doc.screen,
|
||||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'forward'),
|
nodes: nodes.map((n) => {
|
||||||
},
|
if (!ids.has(n.id)) return n;
|
||||||
},
|
bump += 1;
|
||||||
};
|
return { ...n, zIndex: maxZ + bump };
|
||||||
}
|
}),
|
||||||
|
|
||||||
case 'sendBackwardSelected': {
|
|
||||||
if (!state.selection.ids.length) return state;
|
|
||||||
const ids = new Set(
|
|
||||||
state.doc.screen.nodes
|
|
||||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
|
||||||
.map((n) => n.id),
|
|
||||||
);
|
|
||||||
if (!ids.size) return state;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...historyPush(state),
|
|
||||||
doc: {
|
|
||||||
screen: {
|
|
||||||
...state.doc.screen,
|
|
||||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'backward'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -593,19 +410,27 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
|
|||||||
|
|
||||||
case 'sendToBackSelected': {
|
case 'sendToBackSelected': {
|
||||||
if (!state.selection.ids.length) return state;
|
if (!state.selection.ids.length) return state;
|
||||||
const ids = new Set(
|
const ids = new Set(state.selection.ids);
|
||||||
state.doc.screen.nodes
|
const nodes = state.doc.screen.nodes;
|
||||||
.filter((n) => state.selection.ids.includes(n.id) && !n.locked)
|
|
||||||
.map((n) => n.id),
|
|
||||||
);
|
|
||||||
if (!ids.size) return state;
|
|
||||||
|
|
||||||
|
let minZ = 0;
|
||||||
|
for (let i = 0; i < nodes.length; i++) {
|
||||||
|
const n = nodes[i]!;
|
||||||
|
const z = n.zIndex ?? i;
|
||||||
|
if (z < minZ) minZ = z;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bump = 0;
|
||||||
return {
|
return {
|
||||||
...historyPush(state),
|
...historyPush(state),
|
||||||
doc: {
|
doc: {
|
||||||
screen: {
|
screen: {
|
||||||
...state.doc.screen,
|
...state.doc.screen,
|
||||||
nodes: reorderSelected(state.doc.screen.nodes, ids, 'toBack'),
|
nodes: nodes.map((n) => {
|
||||||
|
if (!ids.has(n.id)) return n;
|
||||||
|
bump += 1;
|
||||||
|
return { ...n, zIndex: minZ - bump };
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -55,9 +55,6 @@ export interface EditorState {
|
|||||||
ctrl: boolean;
|
ctrl: boolean;
|
||||||
space: boolean;
|
space: boolean;
|
||||||
};
|
};
|
||||||
clipboard: {
|
|
||||||
nodes: WidgetNode[];
|
|
||||||
};
|
|
||||||
history: {
|
history: {
|
||||||
past: HistoryEntry[];
|
past: HistoryEntry[];
|
||||||
future: HistoryEntry[];
|
future: HistoryEntry[];
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user