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
87 changes: 87 additions & 0 deletions src/covertitle.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
47 changes: 47 additions & 0 deletions src/covertitle.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
}
31 changes: 31 additions & 0 deletions src/editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>(".entry-image .img-resize-handle.corner");
expect(el).not.toBeNull();
const img = host.querySelector<HTMLImageElement>(".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();
});
});
4 changes: 3 additions & 1 deletion src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
);
Expand Down
37 changes: 23 additions & 14 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down
9 changes: 3 additions & 6 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1175,7 +1175,9 @@ button {
}

.entry-image {
margin: 1em 0;
position: relative;
display: table;
margin: 1em auto;
}

.entry-image[data-text-align="center"] {
Expand All @@ -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;
Expand Down
Loading