From 8f2cf249cafacc1028a1f725c25aa6474b578685 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Tue, 27 Oct 2026 04:43:22 +0000 Subject: [PATCH] Fix image centering + stale resize attrs; guard cover-title edits per notebook - Merge the duplicate .entry-image blocks; display:table with margin auto restores centering for aligned/default images while keeping resize handles positioned (#21). - Resize commit() reads attrs from the node currently at the position instead of the node captured at node-view creation (#21). - Cover title editing captures the notebook id at focus, commits against that id on blur, reverts on Escape, and refuses cross-notebook renames; cover subtitle gets the same focus-time capture (#18). Co-authored-by: setzor --- src/covertitle.test.ts | 87 ++++++++++++++++++++++++++++++++++++++++++ src/covertitle.ts | 47 +++++++++++++++++++++++ src/editor.test.ts | 31 +++++++++++++++ src/editor.ts | 4 +- src/main.ts | 37 +++++++++++------- src/styles.css | 9 ++--- 6 files changed, 194 insertions(+), 21 deletions(-) create mode 100644 src/covertitle.test.ts create mode 100644 src/covertitle.ts diff --git a/src/covertitle.test.ts b/src/covertitle.test.ts new file mode 100644 index 0000000..eafb794 --- /dev/null +++ b/src/covertitle.test.ts @@ -0,0 +1,87 @@ +// @vitest-environment happy-dom +import { describe, expect, it, vi } from "vitest"; +import { attachCoverTitleEditing } from "./covertitle"; + +interface Nb { + id: string; + name: string; +} + +function setup(notebooks: Nb[], activeId: string) { + const el = document.createElement("div"); + el.contentEditable = "true"; + document.body.appendChild(el); + const rename = vi.fn(async (id: string, name: string) => ({ id, name })); + let active = activeId; + const reverted: string[] = []; + const renamed: Array<[string, string]> = []; + attachCoverTitleEditing(el, { + getNotebook: (id) => notebooks.find((n) => n.id === id), + getActiveNotebookId: () => active, + renameNotebook: rename, + onRenamed: (id, meta) => renamed.push([id, meta.name]), + onReverted: (name) => reverted.push(name), + }); + return { + el, + rename, + renamed, + reverted, + setActive: (id: string) => (active = id), + }; +} + +function focus(el: HTMLElement): void { + el.dispatchEvent(new FocusEvent("focus")); +} + +function blur(el: HTMLElement): void { + el.dispatchEvent(new FocusEvent("blur")); +} + +describe("cover title editing", () => { + const notebooks = [ + { id: "a", name: "Alpha" }, + { id: "b", name: "Beta" }, + ]; + + it("renames the focused notebook on blur", async () => { + const h = setup(notebooks, "a"); + focus(h.el); + h.el.textContent = "New Name"; + blur(h.el); + await vi.waitFor(() => expect(h.rename).toHaveBeenCalled()); + expect(h.rename).toHaveBeenCalledWith("a", "New Name"); + expect(h.renamed).toEqual([["a", "New Name"]]); + }); + + it("reverts the text when the name is unchanged or empty", () => { + const h = setup(notebooks, "a"); + focus(h.el); + h.el.textContent = ""; + blur(h.el); + expect(h.el.textContent).toBe("Alpha"); + expect(h.rename).not.toHaveBeenCalled(); + }); + + it("does not rename a different notebook when the active one changed mid-edit", async () => { + const h = setup(notebooks, "a"); + focus(h.el); + h.el.textContent = "Renamed Alpha"; + h.setActive("b"); + blur(h.el); + await new Promise((r) => setTimeout(r, 20)); + expect(h.rename).not.toHaveBeenCalled(); + }); + + it("Escape reverts the edited text to the saved name", () => { + const h = setup(notebooks, "a"); + focus(h.el); + h.el.textContent = "Mistake"; + h.el.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), + ); + expect(h.el.textContent).toBe("Alpha"); + expect(h.rename).not.toHaveBeenCalled(); + }); +}); diff --git a/src/covertitle.ts b/src/covertitle.ts new file mode 100644 index 0000000..a67300d --- /dev/null +++ b/src/covertitle.ts @@ -0,0 +1,47 @@ +export interface CoverTitleEditingOptions { + getNotebook: (id: string | null) => { id: string; name: string } | undefined; + getActiveNotebookId: () => string; + renameNotebook: (id: string, name: string) => Promise<{ name: string }>; + onRenamed: (id: string, meta: { name: string }) => void; + onReverted: (name: string) => void; +} + +export function attachCoverTitleEditing( + el: HTMLElement, + options: CoverTitleEditingOptions, +): void { + let editNotebookId: string | null = null; + + const commit = (): void => { + const id = editNotebookId; + editNotebookId = null; + const nb = options.getNotebook(id); + if (!nb) return; + const newName = (el.textContent ?? "").trim(); + if (newName && newName !== nb.name) { + if (id !== options.getActiveNotebookId()) return; + void options.renameNotebook(id, newName).then((meta) => { + options.onRenamed(id, meta); + }); + } else { + el.textContent = nb.name; + options.onReverted(nb.name); + } + }; + + el.addEventListener("focus", () => { + editNotebookId = options.getActiveNotebookId(); + }); + + el.addEventListener("blur", commit); + + el.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + event.preventDefault(); + const nb = options.getNotebook(editNotebookId); + if (nb) el.textContent = nb.name; + editNotebookId = null; + el.blur(); + } + }); +} diff --git a/src/editor.test.ts b/src/editor.test.ts index 5e3eaf5..edb5b40 100644 --- a/src/editor.test.ts +++ b/src/editor.test.ts @@ -108,3 +108,34 @@ describe("image rendering", () => { editor.destroy(); }); }); + +describe("image resize commit", () => { + it("merges the resize width with the node's current attrs", () => { + const host = document.createElement("div"); + document.body.appendChild(host); + const editor = createEditor(host, "", { onUpdate: () => {} }); + editor.commands.setImage({ src: "pic.png", alt: "pic" }); + const before = editor.getJSON(); + const imgNode = before.content?.[0]; + expect(imgNode?.type).toBe("image"); + + const el = host.querySelector(".entry-image .img-resize-handle.corner"); + expect(el).not.toBeNull(); + const img = host.querySelector(".entry-image img")!; + Object.defineProperty(img, "getBoundingClientRect", { + value: () => ({ width: 250, height: 100 } as DOMRect), + }); + + el!.dispatchEvent(new MouseEvent("mousedown", { clientX: 100, bubbles: true })); + document.dispatchEvent(new MouseEvent("mousemove", { clientX: 160 })); + document.dispatchEvent(new MouseEvent("mouseup")); + + const after = editor.getJSON(); + const saved = after.content?.[0]; + expect(saved?.type).toBe("image"); + expect(saved?.attrs?.width).toBe(250); + expect(saved?.attrs?.alt).toBe("pic"); + expect(saved?.attrs?.src).toBe("pic.png"); + editor.destroy(); + }); +}); diff --git a/src/editor.ts b/src/editor.ts index 4619bcf..ba9416b 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -122,9 +122,11 @@ const LocalMediaImage = Image.extend({ const commit = (px: number): void => { const pos = typeof getPos === "function" ? getPos() : null; if (pos === null) return; + const currentNode = + ed.view.state.doc.nodeAt(pos) ?? node; ed.view.dispatch( ed.view.state.tr.setNodeMarkup(pos, undefined, { - ...node.attrs, + ...currentNode.attrs, width: px, }), ); diff --git a/src/main.ts b/src/main.ts index de0443d..2c8fe2c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,6 +42,7 @@ import { saveLastNotebook, } from "./shelf"; import { createToolbar, setToolbarNotebook } from "./toolbar"; +import { attachCoverTitleEditing } from "./covertitle"; import { installAppSettings, openAppSettings, getAppSettings } from "./appsettings"; import type { AppSettings } from "./settings"; import { applyDefaultFontStack, fontStack } from "./fonts"; @@ -274,24 +275,32 @@ for (const el of [coverBrandEl, coverSubEl]) { }); } -coverBrandEl.addEventListener("blur", () => { - const nb = activeNotebook(); - const newName = (coverBrandEl.textContent ?? "").trim(); - if (!nb) return; - if (newName && newName !== nb.name) { - void renameNotebook(nb.id, newName).then((meta) => { - nb.name = meta.name; - renderCover(); - renderShelfPopover(); - }); - } else { - coverBrandEl.textContent = nb.name; - } +attachCoverTitleEditing(coverBrandEl, { + getNotebook: (id) => notebooks.find((n) => n.id === id), + getActiveNotebookId: () => activeNotebookId, + renameNotebook: (id, name) => renameNotebook(id, name), + onRenamed: (id, meta) => { + const nb = notebooks.find((n) => n.id === id); + if (nb) nb.name = meta.name; + renderCover(); + renderShelfPopover(); + }, + onReverted: () => {}, +}); + +let subtitleEditNotebookId: string | null = null; + +coverSubEl.addEventListener("focus", () => { + subtitleEditNotebookId = activeNotebookId; }); coverSubEl.addEventListener("blur", () => { + const editId = subtitleEditNotebookId; + subtitleEditNotebookId = null; const value = (coverSubEl.textContent ?? "").trim(); - if (activeNotebookId) saveSubtitleFor(activeNotebookId, value); + if (editId && editId === activeNotebookId) { + saveSubtitleFor(editId, value); + } }); document.addEventListener("click", (event) => { diff --git a/src/styles.css b/src/styles.css index 76e625f..fcc6cd6 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1175,7 +1175,9 @@ button { } .entry-image { - margin: 1em 0; + position: relative; + display: table; + margin: 1em auto; } .entry-image[data-text-align="center"] { @@ -1198,11 +1200,6 @@ button { box-shadow: var(--shadow); } -.entry-image { - position: relative; - display: inline-block; -} - .entry-image .img-resize-handle { position: absolute; z-index: 5;