Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 75 additions & 85 deletions main/accessibility.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { InputManager } from "./inputmanager.js";
import { ContextManager } from "./context.js";

// Area menu accessed with Ctrl + B to quickly skip to
// different areas on the interface

Expand Down Expand Up @@ -45,54 +48,48 @@ const AreaManager = {
},

setupListeners() {
window.addEventListener(
"keydown",
(e) => {
// Open: Ctrl+B
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "b") {
e.preventDefault();
this.toggle(this.overlay.classList.contains("hidden"));
}
// Close: Escape
if (e.key === "Escape") {
this.toggle(false);
}
// Handle number keys
if (e.key >= "1" && e.key <= "9") {
// Only if the overlay is open (otherwise you can't type numbers)
if (!this.overlay.classList.contains("hidden")) {
// Find the area and set the focus
const area = this.areas.find((a) => a.label === e.key);
if (area) this.activateArea(area);
}
}
// Tab through badges when overlay is open
if (e.key === "Tab" && !this.overlay.classList.contains("hidden")) {
e.preventDefault();
const badges = [
...this.overlay.querySelectorAll(".area-number-badge"),
];
if (badges.length === 0) return;
const currentIndex = badges.indexOf(document.activeElement);
const nextIndex = e.shiftKey
? (currentIndex - 1 + badges.length) % badges.length
: (currentIndex + 1) % badges.length;
badges[nextIndex].focus();
}
// Enter opens the area if a badge is focused
if (e.key === "Enter" && !this.overlay.classList.contains("hidden")) {
const focused = document.activeElement;
// Do nothing if a badge is not focused
if (!focused?.classList.contains("area-number-badge")) return;
e.preventDefault();

// Find the area and set the focus
const area = this.areas.find((a) => a.label === focused.innerText);
if (area) this.activateArea(area);
}
},
true,
); // 'true' uses the capture phase to beat Blockly's listeners
InputManager.on("*", "Mod+KeyB", (e) => {
e.preventDefault();
e.stopPropagation();
this.toggle(this.overlay.classList.contains("hidden"));
});

InputManager.on("OVERLAY", "Escape", () => this.toggle(false));

for (let i = 1; i <= 9; i++) {
InputManager.on("OVERLAY", `Digit${i}`, (e) => {
e.preventDefault();
const area = this.areas.find((a) => a.label === String(i));
if (area) this.activateArea(area);
});
}

const cycleBadges = (reverse) => {
const badges = [...this.overlay.querySelectorAll(".area-number-badge")];
if (badges.length === 0) return;
const currentIndex = badges.indexOf(document.activeElement);
const nextIndex = reverse
? (currentIndex - 1 + badges.length) % badges.length
: (currentIndex + 1) % badges.length;
badges[nextIndex].focus();
};

InputManager.on("OVERLAY", "Tab", (e) => {
e.preventDefault();
cycleBadges(false);
});
InputManager.on("OVERLAY", "Shift+Tab", (e) => {
e.preventDefault();
cycleBadges(true);
});

InputManager.on("OVERLAY", "Enter", (e) => {
const focused = document.activeElement;
if (!focused?.classList.contains("area-number-badge")) return;
e.preventDefault();
const area = this.areas.find((a) => a.label === focused.innerText);
if (area) this.activateArea(area);
});
},

// Set the focus to this area and close overlay
Expand All @@ -105,6 +102,7 @@ const AreaManager = {
) ?? el; // Focus the area itself if no suitable child

focusable?.focus();
if (area.selector === "#gizmoButtons") GizmoMenuManager.toggle(true);
},

renderHighlights() {
Expand Down Expand Up @@ -179,6 +177,17 @@ const GizmoMenuManager = {
if (!this.overlay) return;
if (show) {
this.renderBadges();

// Check if the Gizmo number shortcut overlay should exit
this._watcher = () => {
const ctx = ContextManager.getCurrentContext();
if (ctx !== "GIZMO" && ctx !== "NAVIGATION") this.toggle(false);
};
document.addEventListener("focusin", this._watcher);
document.addEventListener("pointerdown", this._watcher, {
capture: true,
});

// Focus 1st button if nothing in gizmos is already focused,
// but if another gizmo is active, leave focus there
const alreadyFocused = document.activeElement?.closest("#gizmoButtons");
Expand All @@ -194,49 +203,28 @@ const GizmoMenuManager = {
},

setupListeners() {
window.addEventListener(
"keydown",
(e) => {
// Show the overlay on Ctrl+G
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "g") {
e.preventDefault();
e.stopPropagation(); // prevent main.js from also handling this
this.toggle(!this.isOpen());
return;
}
// Toggle gizmo menu with Ctrl + G
InputManager.on("*", "Mod+KeyG", (e) => {
const ctx = ContextManager.getCurrentContext();
if (ctx === "TYPING" || ctx === "OVERLAY") return;
e.preventDefault();
e.stopPropagation();
this.toggle(true);
});

// Do nothing if the overlay isn't open
// Activate gizmo buttons with number keys
for (let i = 1; i <= 9; i++) {
InputManager.on("*", `Digit${i}`, () => {
if (!this.isOpen()) return;
const entry = this.buttons.find((b) => b.label === String(i));
if (entry) this.activateButton(entry);
});
}

// Guard against typing in inputs triggering gizmo shortcuts
const t = e.target;
const tag = (t?.tagName || "").toLowerCase();
if (
t?.isContentEditable ||
tag === "input" ||
tag === "textarea" ||
tag === "select"
)
return;

// If the overlay is open and a number key is pressed,
// activate the gizmo
if (e.key >= "1" && e.key <= "9") {
const entry = this.buttons.find((b) => b.label === e.key);
if (entry) this.activateButton(entry);
}
if (e.key === "Escape") {
e.preventDefault();
this.toggle(false);
}
},
true,
);

// Move the gizmo buttons if the window is resized
const gizmoButtons = document.getElementById("gizmoButtons");
const resizer = document.getElementById("resizer");
if (gizmoButtons) {
// Move the badges if the window is resized
new ResizeObserver(() => {
if (this.isOpen()) this.renderBadges();
}).observe(gizmoButtons);
Expand Down Expand Up @@ -430,6 +418,8 @@ const ShortcutsPanel = {
},

setupListeners() {
// Not handled by InputManager as they are set specifically
// to listen when the panel has focus, not globally
document.addEventListener("click", (e) => {
if (e.target.id === "closeShortcutsPanel") this.hide();
});
Expand Down
8 changes: 7 additions & 1 deletion main/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
if (window.Blockly) {
// Try multiple ways to find the main workspace
const mainWorkspace =
typeof Blockly.getMainWorkspace === "function"

Check failure on line 31 in main/context.js

View workflow job for this annotation

GitHub Actions / eslint

'Blockly' is not defined
? Blockly.getMainWorkspace()

Check failure on line 32 in main/context.js

View workflow job for this annotation

GitHub Actions / eslint

'Blockly' is not defined
: Blockly.common &&

Check failure on line 33 in main/context.js

View workflow job for this annotation

GitHub Actions / eslint

'Blockly' is not defined
typeof Blockly.common.getMainWorkspace === "function"

Check failure on line 34 in main/context.js

View workflow job for this annotation

GitHub Actions / eslint

'Blockly' is not defined
? Blockly.common.getMainWorkspace()
: null;

Expand All @@ -54,8 +54,14 @@
}

// GIZMO: Is a gizmo currently active?
// Yield to EDITOR if the user is actively working in Blockly
if (document.querySelector(".gizmo-button.active")) {
return "GIZMO";
const currentGesture = window.Blockly?.Gesture?.getCurrentGesture?.();
const isBlocklyActive =
currentGesture?.isDragging?.() ||
activeEl?.closest(".blocklySvg") ||
activeEl?.closest(".blocklyToolbox");
if (!isBlocklyActive) return "GIZMO";
}

// RESIZER: Are they changing the canvas size?
Expand Down
13 changes: 6 additions & 7 deletions main/inputmanager.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { ContextManager } from "./context.js";
// Work out what key combo was pressed (e.g. Ctrl+Shift+KeyA)
function _keyCombo(event) {
const mods = [
event.ctrlKey && "Ctrl",
event.metaKey && "Meta",
(event.ctrlKey || event.metaKey) && "Mod",
event.altKey && "Alt",
event.shiftKey && "Shift",
].filter(Boolean);
Expand Down Expand Up @@ -47,13 +46,13 @@ const InputManager = {
return;
}
}
const combo = _keyCombo(event);
const handler =
this._registry[`${context}:${combo}`] ||
this._registry[`*:${combo}`] ||
this._registry[`${context}:${event.code}`] ||
this._registry[`*:${event.code}`];
_debugShow(
_keyCombo(event),
handler ? `${context}:${event.code}` : "external",
);
this._registry[`*:${event.code}`]; // Wildcard context *
_debugShow(combo, handler ? `${context}:${combo}` : "external");
if (handler) handler(event);
},
};
Expand Down
79 changes: 16 additions & 63 deletions main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
translate,
} from "./translation.js";
import { ShortcutsPanel } from "./accessibility.js";
import { InputManager } from "./inputmanager.js";
import "./context.js";

function isEmbedModeEnabled() {
Expand Down Expand Up @@ -584,7 +585,7 @@ function initializeApp() {
}
runCodeButton.addEventListener("click", executeCode);
stopCodeButton.addEventListener("click", stopCode);
exportCodeButton.addEventListener("click", exportCode);
exportCodeButton.addEventListener("click", () => exportCode(workspace));

// Make open button work with keyboard
if (openButton) {
Expand All @@ -603,67 +604,19 @@ function initializeApp() {
// Enable the file input after initialization
fileInput.removeAttribute("disabled");

// keydown event listener (capture phase to ensure shortcuts
// are handled before any other handler can stop propagation)
document.addEventListener(
"keydown",
function (e) {
// Check for modifier key (Ctrl on Windows/Linux, Cmd on Mac)
if (!(e.ctrlKey || e.metaKey)) return;

let key = e.key.toLowerCase();
if (e.code === "KeyM" && key !== "m") key = "m";
if (e.code === "KeyE" && key !== "e") key = "e";

switch (key) {
case "o": // Ctrl+O - Open file
e.preventDefault();
openFile(workspace, executeCode);
break;

case "s": // Ctrl+S - Save/Export
e.preventDefault();
exportCode(workspace); // Or saveWorkspace(workspace) for autosave
break;

case "p": // Ctrl+P - Execute code
e.preventDefault();
const canvas = document.getElementById("renderCanvas");
canvas.focus({ preventScroll: true });
break;

case "/": {
e.preventDefault();
ShortcutsPanel.toggle();
break;
}

case "m": {
// Ctrl+M - Move focus to main menu button
e.preventDefault();
if (menuButton) menuButton.focus();
break;
}

case "g": {
// Ctrl+G - Focus shapes button
e.preventDefault();
const btn = document.getElementById("showShapesButton");
if (btn && !btn.disabled && btn.offsetParent !== null) {
btn.focus();
}
break;
}

case "e": // Ctrl+E - Focus Blockly workspace/editor and move cursor
e.preventDefault();
Blockly.keyboardNavigationController?.setIsActive?.(true);
Blockly.getFocusManager()?.focusTree?.(workspace);
break;
}
},
true,
);
InputManager.on("*", "Mod+KeyO", (e) => { e.preventDefault(); openFile(workspace, executeCode); });
InputManager.on("*", "Mod+KeyS", (e) => { e.preventDefault(); exportCode(workspace); });
InputManager.on("*", "Mod+KeyP", (e) => {
e.preventDefault();
document.getElementById("renderCanvas")?.focus({ preventScroll: true });
});
InputManager.on("*", "Mod+Slash", (e) => { e.preventDefault(); ShortcutsPanel.toggle(); });
InputManager.on("*", "Mod+KeyM", (e) => { e.preventDefault(); if (menuButton) menuButton.focus(); });
InputManager.on("*", "Mod+KeyE", (e) => {
e.preventDefault();
Blockly.keyboardNavigationController?.setIsActive?.(true);
Blockly.getFocusManager()?.focusTree?.(workspace);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (toggleDesignButton) {
toggleDesignButton.addEventListener("click", toggleDesignMode);
}
Expand Down Expand Up @@ -727,7 +680,7 @@ function initializeApp() {
if (projectSave) {
projectSave.addEventListener("click", function (e) {
e.preventDefault();
exportCode();
exportCode(workspace);
document.getElementById("menuDropdown")?.classList.add("hidden");
});
}
Expand Down
18 changes: 6 additions & 12 deletions ui/colourpicker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { translate } from "../main/translation.js";
import { exitGizmoState } from "./gizmos.js";
import { InputManager } from "../main/inputmanager.js";
import { ContextManager } from "../main/context.js";

const COLOR_PALETTES = {
Bright: [
Expand Down Expand Up @@ -1863,18 +1865,11 @@ class CustomColorPicker {
document.getElementById("colorPickerButton")?.classList.add("active");

// Add P shortcut to pick current colour
if (document._colorPickerShortcut) {
document.removeEventListener("keydown", document._colorPickerShortcut);
}
document._colorPickerShortcut = (e) => {
if (e.key !== "p" && e.key !== "P") return;
const tag = (e.target?.tagName || "").toLowerCase();
if (tag === "input" || tag === "textarea" || e.target?.isContentEditable)
return;
InputManager.on("*", "KeyP", (e) => {
if (ContextManager.getCurrentContext() === "TYPING") return;
e.preventDefault();
this.container.querySelector(".color-picker-use")?.click();
};
document.addEventListener("keydown", document._colorPickerShortcut);
});

// --- Positioning (unchanged) ---
const colorButton = document.getElementById("colorPickerButton");
Expand Down Expand Up @@ -2086,8 +2081,7 @@ class CustomColorPicker {
document.getElementById("colorPickerButton")?.classList.remove("active");
document.removeEventListener("click", this.outsideClickHandler, true);
window.removeEventListener("keydown", this.globalEscapeHandler, true);
document.removeEventListener("keydown", document._colorPickerShortcut);
document._colorPickerShortcut = null;
InputManager.off("*", "KeyP");
}

confirmColor() {
Expand Down
Loading
Loading