diff --git a/website/canvas.html b/website/canvas.html
index deeacda..3b2a950 100644
--- a/website/canvas.html
+++ b/website/canvas.html
@@ -3844,6 +3844,17 @@
return;
}
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "a") {
+ e.preventDefault();
+ for (const el of canvasContent.children) {
+ if (el.id && typeof el.x === "number" && !el.forgotten) {
+ selectedShapeIds.add(el.id);
+ }
+ }
+ updateSelectionVisuals();
+ return;
+ }
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
const key = e.key.toLowerCase();
@@ -5733,28 +5744,58 @@
});
// ── Delete selected shapes (forget, not hard-delete) ──
+ function doDeleteSelected() {
+ const did = getLocalDID();
+ for (const id of selectedShapeIds) {
+ const el = document.getElementById(id);
+ const state = sync.getShapeVisualState(id);
+ if (state === 'forgotten') {
+ sync.hardDeleteShape(id);
+ if (el) el.remove();
+ } else {
+ sync.forgetShape(id, did);
+ if (el) el.forgotten = true;
+ }
+ }
+ deselectAll();
+ }
+
document.addEventListener("keydown", (e) => {
if ((e.key === "Delete" || e.key === "Backspace") &&
!e.target.closest("input, textarea, [contenteditable]") &&
selectedShapeIds.size > 0) {
- const did = getLocalDID();
- for (const id of selectedShapeIds) {
- const el = document.getElementById(id);
- const state = sync.getShapeVisualState(id);
- if (state === 'forgotten') {
- // Already forgotten — hard delete
- sync.hardDeleteShape(id);
- if (el) el.remove();
- } else {
- // Present — forget (fade)
- sync.forgetShape(id, did);
- if (el) el.forgotten = true;
- }
+ if (selectedShapeIds.size > 5) {
+ showBulkDeleteConfirm(selectedShapeIds.size);
+ } else {
+ doDeleteSelected();
}
- deselectAll();
}
});
+ // ── Bulk delete confirmation dialog ──
+ function showBulkDeleteConfirm(count) {
+ const overlay = document.createElement("div");
+ overlay.style.cssText = "position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:10000;display:flex;align-items:center;justify-content:center";
+ const dialog = document.createElement("div");
+ dialog.style.cssText = "background:var(--rs-bg-surface,#1e1b4b);border:1px solid var(--rs-border,#334155);border-radius:12px;padding:1.5rem 2rem;max-width:380px;text-align:center;color:var(--rs-text-primary,#e2e8f0);font-family:system-ui,sans-serif";
+ dialog.innerHTML = `
+
Delete ${count} elements?
+ You are about to delete a large number of elements. This action cannot be easily undone.
+
+
+
+
`;
+ overlay.appendChild(dialog);
+ document.body.appendChild(overlay);
+
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) { overlay.remove(); } });
+ dialog.querySelector("#bulk-delete-cancel").addEventListener("click", () => overlay.remove());
+ dialog.querySelector("#bulk-delete-confirm").addEventListener("click", () => {
+ overlay.remove();
+ doDeleteSelected();
+ });
+ }
+
// ── Canvas pointer interaction: pan-first + hold-to-select ──
let isPanning = false;
let panPointerId = null;
@@ -5764,11 +5805,11 @@
let selectStartX = 0, selectStartY = 0;
const selectRect = document.getElementById("select-rect");
- // Pan-first interaction state
+ // Select-first interaction state
let interactionMode = "none"; // "none" | "pending" | "pan" | "select"
let holdTimer = null;
- const HOLD_DELAY = 250; // ms to hold before switching to marquee
- const PAN_THRESHOLD = 4; // px movement to confirm pan intent
+ const HOLD_DELAY = 250; // ms to hold before switching to pan
+ const SELECT_THRESHOLD = 4; // px movement to confirm select intent
canvas.addEventListener("pointerdown", (e) => {
if (isTouchPanning) return; // two-finger gesture owns the canvas
@@ -5828,17 +5869,12 @@
selectStartY = e.clientY;
canvas.setPointerCapture(e.pointerId);
- // After HOLD_DELAY ms without significant movement → switch to marquee select
+ // After HOLD_DELAY ms without significant movement → switch to pan
holdTimer = setTimeout(() => {
if (interactionMode !== "pending") return;
- interactionMode = "select";
- isSelecting = true;
- selectRect.style.display = "block";
- selectRect.style.left = selectStartX + "px";
- selectRect.style.top = selectStartY + "px";
- selectRect.style.width = "0";
- selectRect.style.height = "0";
- canvas.style.cursor = "crosshair";
+ interactionMode = "pan";
+ isPanning = true;
+ canvas.style.cursor = "grabbing";
}, HOLD_DELAY);
});
@@ -5857,22 +5893,21 @@
return;
}
- // Pending → if moved beyond threshold, commit to pan
+ // Pending → if moved beyond threshold, commit to marquee select
if (interactionMode === "pending") {
const dx = e.clientX - selectStartX;
const dy = e.clientY - selectStartY;
- if (Math.abs(dx) > PAN_THRESHOLD || Math.abs(dy) > PAN_THRESHOLD) {
+ if (Math.abs(dx) > SELECT_THRESHOLD || Math.abs(dy) > SELECT_THRESHOLD) {
clearTimeout(holdTimer);
holdTimer = null;
- interactionMode = "pan";
- isPanning = true;
- canvas.style.cursor = "grabbing";
- // Apply the initial movement
- panX += dx;
- panY += dy;
- panStartX = e.clientX;
- panStartY = e.clientY;
- updateCanvasTransform();
+ interactionMode = "select";
+ isSelecting = true;
+ selectRect.style.display = "block";
+ selectRect.style.left = selectStartX + "px";
+ selectRect.style.top = selectStartY + "px";
+ selectRect.style.width = "0";
+ selectRect.style.height = "0";
+ canvas.style.cursor = "crosshair";
}
return;
}