fix(pdf-editor): a replaced run is text, so let it be edited like text

Testing 0.3.0 on a real install showed the gap immediately: select a run you
had replaced and the selection bar offered duplicate and delete and nothing
else. The one mark made entirely of words was the one with no way to change
them, double-clicking it did nothing, and the size stepper and the bold and
italic buttons all stayed hidden. Everything else that holds text could be
reopened; this could not.

The cause was three places testing `kind === "text"` where the question was
really "does this mark hold words". A replacement holds words.

Reusing the dialog exposed a second, quieter fault. A run lifted out of a
document is whatever size the document set — 11 pt, 9.5 pt — while the size
picker lists round numbers. Selecting a value the list does not contain leaves
the select empty, and the size on the way out fell back to 12. Editing the
wording of an 11 pt line would silently have resized it. The dialog now adds
the document's own size as an option for as long as it is open, and falls back
to the size it started with rather than to a guess.
This commit is contained in:
Local Dev 2026-09-22 23:09:58 +02:00
parent 9a59bf1025
commit c4dbdb25fb
2 changed files with 23 additions and 8 deletions

View file

@ -560,7 +560,9 @@ const selbar = $("selbar");
function paintSelectionBar(id) { function paintSelectionBar(id) {
const a = id && session.model ? session.model.annot(id) : null; const a = id && session.model ? session.model.annot(id) : null;
if (!a) { selbar.hidden = true; return; } if (!a) { selbar.hidden = true; return; }
const isText = a.kind === "text"; // A replaced run is text too. Leaving it out meant the one mark made of
// words offered no way to change them — the exact gap this bar exists for.
const isText = a.kind === "text" || a.kind === "textedit";
const isShape = a.kind === "rect" || a.kind === "ellipse"; const isShape = a.kind === "rect" || a.kind === "ellipse";
$("sb-edit").hidden = !isText; $("sb-edit").hidden = !isText;
$("sb-textsize").hidden = !isText; $("sb-textsize").hidden = !isText;
@ -633,13 +635,13 @@ $("sb-bigger").addEventListener("click", () => stepTextSize(1));
function toggleTextStyle(which) { function toggleTextStyle(which) {
const a = session.model?.annot(session.tools?.selected); const a = session.model?.annot(session.tools?.selected);
if (!a || a.kind !== "text") return; if (!a || (a.kind !== "text" && a.kind !== "textedit")) return;
delete a._w; // the glyphs change width; re-measure delete a._w; // the glyphs change width; re-measure
session.tools.patchSelected({ [which]: !a[which] }); session.tools.patchSelected({ [which]: !a[which] });
} }
function stepTextSize(dir) { function stepTextSize(dir) {
const a = session.model?.annot(session.tools?.selected); const a = session.model?.annot(session.tools?.selected);
if (!a || a.kind !== "text") return; if (!a || (a.kind !== "text" && a.kind !== "textedit")) return;
const steps = [6, 8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96]; const steps = [6, 8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96];
const at = a.size || 12; const at = a.size || 12;
const next = dir > 0 ? steps.find((v) => v > at + 0.01) : [...steps].reverse().find((v) => v < at - 0.01); const next = dir > 0 ? steps.find((v) => v > at + 0.01) : [...steps].reverse().find((v) => v < at - 0.01);
@ -657,20 +659,33 @@ async function onTextModal(id, at) {
const existing = id ? session.model?.annot(id) : null; const existing = id ? session.model?.annot(id) : null;
$("modal-text-title").textContent = existing ? "Edit text" : "Text stamp"; $("modal-text-title").textContent = existing ? "Edit text" : "Text stamp";
$("text-body").value = existing ? existing.text : ""; $("text-body").value = existing ? existing.text : "";
$("text-size").value = String(existing ? existing.size || 12 : 12); // A run lifted out of a document is whatever size the document set — 11 pt,
// 9.5 pt — and the picker only lists round numbers. Without this the select
// falls back to empty and the size silently becomes 12 on the way out.
const wanted = existing ? existing.size || 12 : 12;
const sizeSel = $("text-size");
if (![...sizeSel.options].some((o) => Number(o.value) === Number(wanted))) {
const opt = document.createElement("option");
opt.value = String(wanted);
opt.textContent = String(Math.round(wanted * 10) / 10);
opt.dataset.adhoc = "1";
sizeSel.append(opt);
}
sizeSel.value = String(wanted);
$("text-bold").classList.toggle("active", !!existing?.bold); $("text-bold").classList.toggle("active", !!existing?.bold);
$("text-italic").classList.toggle("active", !!existing?.italic); $("text-italic").classList.toggle("active", !!existing?.italic);
$("text-warn").hidden = true; $("text-warn").hidden = true;
const p = openModal("modal-text"); const p = openModal("modal-text");
setTimeout(() => { $("text-body").focus(); $("text-body").select(); }, 30); setTimeout(() => { $("text-body").focus(); $("text-body").select(); }, 30);
const ok = await p; const ok = await p;
if (!ok) return null;
const props = { const props = {
text: $("text-body").value, text: $("text-body").value,
size: parseFloat($("text-size").value) || 12, size: parseFloat($("text-size").value) || wanted,
bold: $("text-bold").classList.contains("active"), bold: $("text-bold").classList.contains("active"),
italic: $("text-italic").classList.contains("active"), italic: $("text-italic").classList.contains("active"),
}; };
for (const o of [...$("text-size").options]) if (o.dataset.adhoc) o.remove();
if (!ok) return null;
return existing ? session.tools.updateText(id, props) : session.tools.placeText(at, props); return existing ? session.tools.updateText(id, props) : session.tools.placeText(at, props);
} }
for (const b of [$("text-bold"), $("text-italic")]) { for (const b of [$("text-bold"), $("text-italic")]) {
@ -1066,7 +1081,7 @@ window.addEventListener("keydown", (e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
const sel = session.tools?.selected; const sel = session.tools?.selected;
const a = sel && session.model?.annot(sel); const a = sel && session.model?.annot(sel);
if (a?.kind === "text") { onTextModal(sel, null); e.preventDefault(); } if (a?.kind === "text" || a?.kind === "textedit") { onTextModal(sel, null); e.preventDefault(); }
return; return;
} }
if (e.key === "+" || e.key === "=") { session.strip?.stepScale(1); syncZoomSelect(); e.preventDefault(); return; } if (e.key === "+" || e.key === "=") { session.strip?.stepScale(1); syncZoomSelect(); e.preventDefault(); return; }

View file

@ -206,7 +206,7 @@ export class Tools {
e.preventDefault(); e.preventDefault();
if (double) { if (double) {
const a = this.model.annot(id); const a = this.model.annot(id);
if (a?.kind === "text") { this.host.editText?.(id, null); return; } if (a?.kind === "text" || a?.kind === "textedit") { this.host.editText?.(id, null); return; }
} }
this.drag = { kind: "move", uid: hit.uid, id, x0: x, y0: y, this.drag = { kind: "move", uid: hit.uid, id, x0: x, y0: y,
orig: structuredClone(this.model.annot(id)), cx: e.clientX, cy: e.clientY, moved: false }; orig: structuredClone(this.model.annot(id)), cx: e.clientX, cy: e.clientY, moved: false };