Multi-selection
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
function handleMouseUp(e: MouseEvent) {
|
function handleMouseUp(e: MouseEvent) {
|
||||||
if (isPanning && !hasPanned && e.target === container) {
|
if (isPanning && !hasPanned && e.target === container) {
|
||||||
state.select(null);
|
state.clearSelection();
|
||||||
state.focus(null);
|
state.focus(null);
|
||||||
}
|
}
|
||||||
isPanning = false;
|
isPanning = false;
|
||||||
@@ -142,38 +142,50 @@
|
|||||||
const centerX = (rect.width / 2 - state.viewport.x) / state.viewport.zoom;
|
const centerX = (rect.width / 2 - state.viewport.x) / state.viewport.zoom;
|
||||||
const centerY = (rect.height / 2 - state.viewport.y) / state.viewport.zoom;
|
const centerY = (rect.height / 2 - state.viewport.y) / state.viewport.zoom;
|
||||||
|
|
||||||
if (mod && e.key === 'c' && state.selectedId) {
|
const hasSelection = state.selectedIds.size > 0;
|
||||||
|
const singleSelection = state.selectedIds.size === 1;
|
||||||
|
const firstSelectedId = singleSelection ? [...state.selectedIds][0] : null;
|
||||||
|
|
||||||
|
if (mod && e.key === 'c' && hasSelection) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.copyItem(state.selectedId);
|
state.copySelected();
|
||||||
} else if (mod && e.key === 'v') {
|
} else if (mod && e.key === 'v') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newId = state.pasteItem(centerX, centerY);
|
const newIds = state.pasteItems(centerX, centerY);
|
||||||
if (newId) state.select(newId);
|
if (newIds.length > 0) state.select(newIds[0]);
|
||||||
} else if (mod && e.key === 'd' && state.selectedId) {
|
for (let i = 1; i < newIds.length; i++) {
|
||||||
|
state.toggleSelection(newIds[i]);
|
||||||
|
}
|
||||||
|
} else if (mod && e.key === 'd' && hasSelection) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const newId = state.duplicateItem(state.selectedId, centerX, centerY);
|
const newIds = state.duplicateSelected(centerX, centerY);
|
||||||
if (newId) state.select(newId);
|
if (newIds.length > 0) state.select(newIds[0]);
|
||||||
|
for (let i = 1; i < newIds.length; i++) {
|
||||||
|
state.toggleSelection(newIds[i]);
|
||||||
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
state.select(null);
|
state.clearSelection();
|
||||||
state.focus(null);
|
state.focus(null);
|
||||||
} else if (e.key === '[' && state.selectedId) {
|
} else if (e.key === '[' && hasSelection) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.sendBackward(state.selectedId);
|
for (const id of state.selectedIds) {
|
||||||
} else if (e.key === ']' && state.selectedId) {
|
state.sendBackward(id);
|
||||||
|
}
|
||||||
|
} else if (e.key === ']' && hasSelection) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.bringForward(state.selectedId);
|
for (const id of state.selectedIds) {
|
||||||
} else if (e.key.startsWith('Arrow') && state.selectedId) {
|
state.bringForward(id);
|
||||||
|
}
|
||||||
|
} else if (e.key.startsWith('Arrow') && hasSelection) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const step = e.shiftKey ? 10 : 1;
|
const step = e.shiftKey ? 10 : 1;
|
||||||
const item = state.getItem(state.selectedId);
|
|
||||||
if (!item) return;
|
|
||||||
const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0;
|
const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0;
|
||||||
const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0;
|
const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0;
|
||||||
state.updateItem(state.selectedId, { x: item.x + dx, y: item.y + dy });
|
state.moveSelected(dx, dy);
|
||||||
} else if (e.key === 'e' && state.selectedId && !mod) {
|
} else if (e.key === 'e' && singleSelection && !mod) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
state.focus(state.selectedId);
|
state.focus(firstSelectedId!);
|
||||||
state.edit(state.selectedId);
|
state.edit(firstSelectedId!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
|
|
||||||
let { item }: { item: Item } = $props();
|
let { item }: { item: Item } = $props();
|
||||||
|
|
||||||
let isSelected = $derived(appState.selectedId === item.id);
|
let isSelected = $derived(appState.selectedIds.has(item.id));
|
||||||
let isFocused = $derived(appState.focusedId === item.id);
|
let isFocused = $derived(appState.focusedId === item.id);
|
||||||
let isDragging = $state(false);
|
let isDragging = $state(false);
|
||||||
let isResizing = $state(false);
|
let isResizing = $state(false);
|
||||||
let isRotating = $state(false);
|
let isRotating = $state(false);
|
||||||
let dragStart = { x: 0, y: 0, itemX: 0, itemY: 0 };
|
let dragStart = { x: 0, y: 0, itemX: 0, itemY: 0 };
|
||||||
|
let dragStartPositions: Map<string, { x: number; y: number }> = new Map();
|
||||||
let resizeStart = { x: 0, y: 0, width: 0, height: 0, itemX: 0, itemY: 0, corner: '', aspectRatio: 1 };
|
let resizeStart = { x: 0, y: 0, width: 0, height: 0, itemX: 0, itemY: 0, corner: '', aspectRatio: 1 };
|
||||||
let rotateStart = { angle: 0, startAngle: 0 };
|
let rotateStart = { angle: 0, startAngle: 0 };
|
||||||
|
|
||||||
@@ -39,8 +40,13 @@
|
|||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
if (isFocused) return;
|
if (isFocused) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
appState.select(item.id);
|
|
||||||
appState.bringToFront(item.id);
|
if (e.shiftKey) {
|
||||||
|
appState.toggleSelection(item.id);
|
||||||
|
} else if (!isSelected) {
|
||||||
|
appState.select(item.id);
|
||||||
|
appState.bringToFront(item.id);
|
||||||
|
}
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragStart = {
|
dragStart = {
|
||||||
@@ -49,6 +55,13 @@
|
|||||||
itemX: item.x,
|
itemX: item.x,
|
||||||
itemY: item.y
|
itemY: item.y
|
||||||
};
|
};
|
||||||
|
dragStartPositions = new Map();
|
||||||
|
for (const id of appState.selectedIds) {
|
||||||
|
const selectedItem = appState.getItem(id);
|
||||||
|
if (selectedItem) {
|
||||||
|
dragStartPositions.set(id, { x: selectedItem.x, y: selectedItem.y });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleResizeStart(e: MouseEvent, corner: string) {
|
function handleResizeStart(e: MouseEvent, corner: string) {
|
||||||
@@ -84,10 +97,12 @@
|
|||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
const dx = (e.clientX - dragStart.x) / appState.viewport.zoom;
|
const dx = (e.clientX - dragStart.x) / appState.viewport.zoom;
|
||||||
const dy = (e.clientY - dragStart.y) / appState.viewport.zoom;
|
const dy = (e.clientY - dragStart.y) / appState.viewport.zoom;
|
||||||
appState.updateItem(item.id, {
|
for (const [id, startPos] of dragStartPositions) {
|
||||||
x: dragStart.itemX + dx,
|
appState.updateItem(id, {
|
||||||
y: dragStart.itemY + dy
|
x: startPos.x + dx,
|
||||||
});
|
y: startPos.y + dy
|
||||||
|
});
|
||||||
|
}
|
||||||
} else if (isResizing) {
|
} else if (isResizing) {
|
||||||
const dx = (e.clientX - resizeStart.x) / appState.viewport.zoom;
|
const dx = (e.clientX - resizeStart.x) / appState.viewport.zoom;
|
||||||
const dy = (e.clientY - resizeStart.y) / appState.viewport.zoom;
|
const dy = (e.clientY - resizeStart.y) / appState.viewport.zoom;
|
||||||
@@ -147,7 +162,7 @@
|
|||||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||||
if ((e.target as HTMLElement)?.isContentEditable) return;
|
if ((e.target as HTMLElement)?.isContentEditable) return;
|
||||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||||
appState.removeItem(item.id);
|
appState.removeSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,11 @@ function createState() {
|
|||||||
});
|
});
|
||||||
let assets = $state<AssetStore>({});
|
let assets = $state<AssetStore>({});
|
||||||
let viewport = $state<Viewport>({ x: 0, y: 0, zoom: 1 });
|
let viewport = $state<Viewport>({ x: 0, y: 0, zoom: 1 });
|
||||||
let selectedId = $state<string | null>(null);
|
let selectedIds = $state<Set<string>>(new Set());
|
||||||
let editingId = $state<string | null>(null);
|
let editingId = $state<string | null>(null);
|
||||||
let editingGlobal = $state<boolean>(false);
|
let editingGlobal = $state<boolean>(false);
|
||||||
let focusedId = $state<string | null>(null);
|
let focusedId = $state<string | null>(null);
|
||||||
let clipboard = $state<Omit<Item, 'id'> | null>(null);
|
let clipboard = $state<Omit<Item, 'id'>[]>([]);
|
||||||
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let animationId: number | null = null;
|
let animationId: number | null = null;
|
||||||
@@ -153,7 +153,7 @@ function createState() {
|
|||||||
delete assets[item.assetId];
|
delete assets[item.assetId];
|
||||||
}
|
}
|
||||||
manifest.items.splice(idx, 1);
|
manifest.items.splice(idx, 1);
|
||||||
if (selectedId === id) selectedId = null;
|
selectedIds.delete(id);
|
||||||
if (editingId === id) editingId = null;
|
if (editingId === id) editingId = null;
|
||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,21 @@ function createState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function select(id: string | null) {
|
function select(id: string | null) {
|
||||||
selectedId = id;
|
selectedIds = new Set(id ? [id] : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelection(id: string) {
|
||||||
|
const newSet = new Set(selectedIds);
|
||||||
|
if (newSet.has(id)) {
|
||||||
|
newSet.delete(id);
|
||||||
|
} else {
|
||||||
|
newSet.add(id);
|
||||||
|
}
|
||||||
|
selectedIds = newSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedIds = new Set();
|
||||||
}
|
}
|
||||||
|
|
||||||
function edit(id: string | null) {
|
function edit(id: string | null) {
|
||||||
@@ -202,24 +216,48 @@ function createState() {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyItem(id: string) {
|
function copySelected() {
|
||||||
const item = manifest.items.find((i) => i.id === id);
|
if (selectedIds.size === 0) return;
|
||||||
if (!item) return;
|
const items = manifest.items.filter((i) => selectedIds.has(i.id));
|
||||||
const { id: _, ...rest } = item;
|
clipboard = items.map(({ id, ...rest }) => rest);
|
||||||
clipboard = rest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pasteItem(x: number, y: number): string | null {
|
function pasteItems(x: number, y: number): string[] {
|
||||||
if (!clipboard) return null;
|
if (clipboard.length === 0) return [];
|
||||||
const id = crypto.randomUUID();
|
const newIds: string[] = [];
|
||||||
const newItem = { ...clipboard, id, x, y, zIndex: maxZIndex + 1 };
|
const centerX = clipboard.reduce((sum, i) => sum + i.x, 0) / clipboard.length;
|
||||||
addItem(newItem);
|
const centerY = clipboard.reduce((sum, i) => sum + i.y, 0) / clipboard.length;
|
||||||
return id;
|
for (const item of clipboard) {
|
||||||
|
const id = crypto.randomUUID();
|
||||||
|
const offsetX = item.x - centerX;
|
||||||
|
const offsetY = item.y - centerY;
|
||||||
|
addItem({ ...item, id, x: x + offsetX, y: y + offsetY, zIndex: maxZIndex + 1 });
|
||||||
|
newIds.push(id);
|
||||||
|
}
|
||||||
|
return newIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateItem(id: string, x: number, y: number): string | null {
|
function duplicateSelected(x: number, y: number): string[] {
|
||||||
copyItem(id);
|
copySelected();
|
||||||
return pasteItem(x, y);
|
return pasteItems(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSelected() {
|
||||||
|
const ids = [...selectedIds];
|
||||||
|
for (const id of ids) {
|
||||||
|
removeItem(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSelected(dx: number, dy: number) {
|
||||||
|
for (const id of selectedIds) {
|
||||||
|
const item = manifest.items.find((i) => i.id === id);
|
||||||
|
if (item) {
|
||||||
|
item.x += dx;
|
||||||
|
item.y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendBackward(id: string) {
|
function sendBackward(id: string) {
|
||||||
@@ -324,7 +362,7 @@ function createState() {
|
|||||||
};
|
};
|
||||||
assets = {};
|
assets = {};
|
||||||
viewport = { x: 0, y: 0, zoom: 1 };
|
viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
selectedId = null;
|
selectedIds = new Set();
|
||||||
editingId = null;
|
editingId = null;
|
||||||
editingGlobal = false;
|
editingGlobal = false;
|
||||||
focusedId = null;
|
focusedId = null;
|
||||||
@@ -336,7 +374,7 @@ function createState() {
|
|||||||
manifest = newManifest;
|
manifest = newManifest;
|
||||||
assets = newAssets;
|
assets = newAssets;
|
||||||
viewport = { x: 0, y: 0, zoom: 1 };
|
viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
selectedId = null;
|
selectedIds = new Set();
|
||||||
editingId = null;
|
editingId = null;
|
||||||
editingGlobal = false;
|
editingGlobal = false;
|
||||||
focusedId = null;
|
focusedId = null;
|
||||||
@@ -355,8 +393,8 @@ function createState() {
|
|||||||
get viewport() {
|
get viewport() {
|
||||||
return viewport;
|
return viewport;
|
||||||
},
|
},
|
||||||
get selectedId() {
|
get selectedIds() {
|
||||||
return selectedId;
|
return selectedIds;
|
||||||
},
|
},
|
||||||
get editingId() {
|
get editingId() {
|
||||||
return editingId;
|
return editingId;
|
||||||
@@ -376,15 +414,19 @@ function createState() {
|
|||||||
addAsset,
|
addAsset,
|
||||||
getItem,
|
getItem,
|
||||||
select,
|
select,
|
||||||
|
toggleSelection,
|
||||||
|
clearSelection,
|
||||||
edit,
|
edit,
|
||||||
editGlobal,
|
editGlobal,
|
||||||
focus,
|
focus,
|
||||||
updateSharedCss,
|
updateSharedCss,
|
||||||
updateAppCss,
|
updateAppCss,
|
||||||
bringToFront,
|
bringToFront,
|
||||||
copyItem,
|
copySelected,
|
||||||
pasteItem,
|
pasteItems,
|
||||||
duplicateItem,
|
duplicateSelected,
|
||||||
|
removeSelected,
|
||||||
|
moveSelected,
|
||||||
sendBackward,
|
sendBackward,
|
||||||
bringForward,
|
bringForward,
|
||||||
pan,
|
pan,
|
||||||
|
|||||||
Reference in New Issue
Block a user