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
25 changes: 14 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
} from "./shelf";
import { createToolbar, setToolbarNotebook } from "./toolbar";
import { attachCoverTitleEditing } from "./covertitle";
import { shouldSkipShortcut } from "./shortcuts";
import { applyNotebookMeta } from "./notebookmeta";
import { installAppSettings, openAppSettings, getAppSettings } from "./appsettings";
import type { AppSettings } from "./settings";
import { applyDefaultFontStack, fontStack } from "./fonts";
Expand Down Expand Up @@ -226,12 +228,7 @@ function renderShelfPopover(): void {
shelfPopEl.hidden = true;
},
onMetaChange: (meta) => {
const nb = notebooks.find((n) => n.id === meta.id);
if (nb) {
nb.name = meta.name;
nb.cover = meta.cover;
}
renderCover();
if (applyNotebookMeta(notebooks, meta)) renderCover();
},
});
}
Expand Down Expand Up @@ -534,11 +531,17 @@ nextDayEl.addEventListener("click", () =>
todayBtnEl.addEventListener("click", () => void openDate(todayStr));

window.addEventListener("keydown", (event) => {
if (event.target instanceof HTMLElement) {
const tag = event.target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || event.target.isContentEditable) {
return;
}
if (
shouldSkipShortcut(event, {
isPopoverOpen: () => !shelfPopEl.hidden,
isDialogOpen: () => {
if (!confirmOverlayEl.hidden) return true;
const settingsOverlay = document.getElementById("app-settings-overlay");
return settingsOverlay !== null && !settingsOverlay.hidden;
},
})
) {
return;
}
if (event.key === "ArrowLeft") void openDate(shiftDate(currentDate, -1));
else if (event.key === "ArrowRight") void openDate(shiftDate(currentDate, 1));
Expand Down
37 changes: 37 additions & 0 deletions src/notebookmeta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { applyNotebookMeta } from "./notebookmeta";
import type { NotebookMeta } from "./api";

describe("applyNotebookMeta", () => {
const notebooks: NotebookMeta[] = [
{ id: "a", name: "Alpha", cover: "kraft", emoji: "🌷" },
{ id: "b", name: "Beta", cover: "navy" },
];

it("syncs name, cover and emoji from fresh meta", () => {
const nb = applyNotebookMeta(notebooks, {
id: "a",
name: "Renamed",
cover: "navy",
emoji: "🌙",
});
expect(nb).toMatchObject({ id: "a", name: "Renamed", cover: "navy", emoji: "🌙" });
expect(notebooks[0].emoji).toBe("🌙");
});

it("clears the emoji when meta reports null", () => {
applyNotebookMeta(notebooks, { id: "a", name: "Renamed", cover: "navy", emoji: null });
expect(notebooks[0].emoji).toBeNull();
});

it("keeps the existing emoji when meta omits it", () => {
applyNotebookMeta(notebooks, { id: "b", name: "Beta", cover: "kraft", emoji: "🔥" });
applyNotebookMeta(notebooks, { id: "b", name: "Beta 2", cover: "kraft" });
expect(notebooks[1].name).toBe("Beta 2");
expect(notebooks[1].emoji).toBe("🔥");
});

it("returns null for an unknown id", () => {
expect(applyNotebookMeta(notebooks, { id: "zz", name: "x", cover: "kraft" })).toBeNull();
});
});
14 changes: 14 additions & 0 deletions src/notebookmeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { NotebookMeta } from "./api";

/** Sync a notebook entry in the in-memory list with fresh backend metadata. */
export function applyNotebookMeta(
notebooks: NotebookMeta[],
meta: NotebookMeta,
): NotebookMeta | null {
const nb = notebooks.find((n) => n.id === meta.id);
if (!nb) return null;
nb.name = meta.name;
nb.cover = meta.cover;
if (meta.emoji !== undefined) nb.emoji = meta.emoji;
return nb;
}
50 changes: 50 additions & 0 deletions src/shortcuts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import { shouldSkipShortcut } from "./shortcuts";

const closed = { isPopoverOpen: () => false, isDialogOpen: () => false };

function keyEvent(key: string, target?: HTMLElement): KeyboardEvent {
const event = new KeyboardEvent("keydown", { key, bubbles: true });
if (target) {
Object.defineProperty(event, "target", { value: target });
}
return event;
}

describe("shouldSkipShortcut", () => {
it("allows plain shortcuts when nothing is open and body has focus", () => {
expect(shouldSkipShortcut(keyEvent("t"), closed)).toBe(false);
expect(shouldSkipShortcut(keyEvent("ArrowLeft"), closed)).toBe(false);
});

it("skips when the shelf popover or a dialog is open", () => {
const event = keyEvent("t");
expect(
shouldSkipShortcut(event, { ...closed, isPopoverOpen: () => true }),
).toBe(true);
expect(
shouldSkipShortcut(event, { ...closed, isDialogOpen: () => true }),
).toBe(true);
});

it("skips text entry contexts: input, textarea, select, contenteditable", () => {
const input = document.createElement("input");
const textarea = document.createElement("textarea");
const select = document.createElement("select");
const editable = document.createElement("div");
editable.setAttribute("contenteditable", "true");
document.body.appendChild(editable);
for (const el of [input, textarea, select, editable]) {
expect(shouldSkipShortcut(keyEvent("t", el), closed)).toBe(true);
expect(shouldSkipShortcut(keyEvent("ArrowLeft", el), closed)).toBe(true);
}
});

it("keeps arrow keys on buttons for focus movement but allows 't'", () => {
const button = document.createElement("button");
expect(shouldSkipShortcut(keyEvent("ArrowLeft", button), closed)).toBe(true);
expect(shouldSkipShortcut(keyEvent("ArrowRight", button), closed)).toBe(true);
expect(shouldSkipShortcut(keyEvent("t", button), closed)).toBe(false);
});
});
26 changes: 26 additions & 0 deletions src/shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface ShortcutGuardOptions {
isPopoverOpen: () => boolean;
isDialogOpen: () => boolean;
}

export function shouldSkipShortcut(
event: KeyboardEvent,
options: ShortcutGuardOptions,
): boolean {
if (options.isPopoverOpen() || options.isDialogOpen()) return true;
const target = event.target;
if (target instanceof HTMLElement) {
const tag = target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
if (target.isContentEditable || target.getAttribute("contenteditable") === "true") {
return true;
}
if (
(event.key === "ArrowLeft" || event.key === "ArrowRight") &&
tag === "BUTTON"
) {
return true;
}
}
return false;
}
Loading