Files
awesome-copilot/extensions/draw/public/keys.js
T

149 lines
6.0 KiB
JavaScript

// Keyboard shortcuts and clipboard.
import { isShape, SHAPE_TYPES } from "/lib/model.mjs";
import { describeElement } from "./announce.js";
export const TOOL_KEYS = { v: "select", h: "hand", r: "rect", o: "ellipse", d: "diamond", c: "cylinder", a: "arrow", t: "text", p: "pen" };
const DIRS = { ArrowRight: "right", ArrowLeft: "left", ArrowDown: "down", ArrowUp: "up" };
const STEPS = { right: [1, 0], left: [-1, 0], down: [0, 1], up: [0, -1] };
export function attachKeys(ed, ui) {
const typing = (target) =>
!!target && target !== ed.svg && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable || !!target.closest?.(".popover"));
// A focused button keeps its normal keys: Space and Enter press it, and Tab moves focus on.
const control = (target) =>
!!target && target !== ed.svg && !!target.closest?.("button, a[href], select, summary, [role='button']");
// N and Shift+N reach every element in turn, and say which one is selected.
const selectStep = (delta) => {
const hit = ed.selectStep(delta);
ui.announce(hit ? `${describeElement(hit.el, ed.byId)}, ${hit.index + 1} of ${hit.total}` : "The drawing is empty.");
};
// The canvas takes focus on every click and after every label edit, so it only shows a focus ring
// while the keyboard is in use: a click hides the ring, and Tab brings it back.
ed.svg.classList.add("pointer-used");
window.addEventListener("pointerdown", () => ed.svg.classList.add("pointer-used"), true);
window.addEventListener("keydown", (e) => {
if (e.key === "Tab") ed.svg.classList.remove("pointer-used");
if (e.defaultPrevented || e.isComposing || typing(e.target)) return;
const key = e.key;
if ((key === " " || key === "Enter" || key === "Tab") && control(e.target)) return;
const lower = key.length === 1 ? key.toLowerCase() : key;
const mod = e.ctrlKey || e.metaKey;
const one = ed.single();
const done = () => e.preventDefault();
if (key === " ") {
done();
if (!e.repeat && !ed.spaceDown) {
ed.spaceDown = true;
ed.refreshCursor?.();
}
return;
}
if (key === "Escape") {
if (ed.pointerActive?.()) ed.cancelPointer();
else if (ui.closePopover()) ed.focusCanvas();
// From the tools, the style bar or the zoom buttons, Esc goes back to the canvas.
else if (e.target !== ed.svg && ed.stage.contains(e.target)) ed.focusCanvas();
else if (ed.tool !== "select") {
ed.setTool("select");
ui.announceTool("select");
} else ed.select([]);
return done();
}
if (mod) {
if (lower === "z") {
if (e.shiftKey) ed.redo();
else ed.undo();
return done();
}
if (lower === "y") { ed.redo(); return done(); }
if (lower === "a") { ed.selectAll(); return done(); }
if (lower === "d") { ed.duplicate(); return done(); }
if (lower === "s") { ui.toast("Drawings save automatically."); return done(); }
if (key === "0") { ed.zoomTo(1); return done(); }
if (key === "=" || key === "+") { ed.zoomBy(1.25); return done(); }
if (key === "-" || key === "_") { ed.zoomBy(0.8); return done(); }
if (key === "]") { ed.reorder(true); return done(); }
if (key === "[") { ed.reorder(false); return done(); }
if (DIRS[key] && e.shiftKey) {
const [dx, dy] = STEPS[DIRS[key]];
if (!ed.interaction) ed.resizeBy(dx * 10, dy * 10);
return done();
}
if (DIRS[key] && one && isShape(one)) { ed.addConnected(one.id, DIRS[key]); return done(); }
return;
}
if (e.altKey && !DIRS[key]) return;
if (key === "Delete" || key === "Backspace") {
const gone = ed.selected();
const what = gone.length === 1 ? describeElement(gone[0], ed.byId) : `${gone.length} elements`;
ed.deleteSelection();
if (gone.length) ui.announce(`Deleted ${what}.`);
return done();
}
// With a shape or text tool, Enter adds one. With the arrow tool, it starts or ends an arrow.
if (key === "Enter" && !ed.interaction && (SHAPE_TYPES.includes(ed.tool) || ed.tool === "text")) {
ed.addAtCenter(ed.tool);
return done();
}
if (key === "Enter" && !ed.interaction && ed.tool === "arrow") { ed.linkStep(); return done(); }
if ((key === "Enter" || key === "F2") && one && one.type !== "pen") { ed.labels.open(one.id); return done(); }
if (lower === "n") {
if (!ed.interaction) selectStep(e.shiftKey ? -1 : 1);
return done();
}
if (DIRS[key]) {
const px = e.altKey ? 1 : e.shiftKey ? 50 : 10;
const [dx, dy] = STEPS[DIRS[key]];
if (ed.selection.size) ed.nudge(dx * px, dy * px);
else ed.panBy(dx * px * 4, dy * px * 4);
return done();
}
if (key === "!" || (e.shiftKey && e.code === "Digit1")) { ed.fit({ maxZoom: 2 }); return done(); }
if (key === "?") { ui.toggleHelp(); return done(); }
if (!e.shiftKey && TOOL_KEYS[lower]) {
ed.setTool(TOOL_KEYS[lower]);
ui.announceTool(TOOL_KEYS[lower]);
return done();
}
});
window.addEventListener("keyup", (e) => {
if (e.key === " " && ed.spaceDown) {
ed.spaceDown = false;
ed.refreshCursor?.();
}
});
window.addEventListener("blur", () => {
if (ed.spaceDown) {
ed.spaceDown = false;
ed.refreshCursor?.();
}
});
document.addEventListener("copy", (e) => {
if (typing(e.target) || !ed.selection.size) return;
const text = ed.copyText();
if (!text) return;
e.clipboardData.setData("text/plain", text);
e.preventDefault();
});
document.addEventListener("cut", (e) => {
if (typing(e.target) || !ed.selection.size) return;
const text = ed.copyText();
if (!text) return;
e.clipboardData.setData("text/plain", text);
ed.deleteSelection();
e.preventDefault();
});
document.addEventListener("paste", (e) => {
if (typing(e.target)) return;
const text = e.clipboardData?.getData("text/plain");
if (!text) return;
ed.paste(text);
e.preventDefault();
});
}