diff --git a/CHANGELOG.md b/CHANGELOG.md index db2cd584e..82b2c8675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ The release run heads these entries with the version and opens a fresh being edited, so a reader who tapped a cell twice lost it. A cell now holds its pin there; a row or column header still clears on that click. +- Editing scope `paragraph` takes `odr.editing.format` inside one paragraph and + refuses it over two, rather than refusing every call. The `formatBold` chords + stay behind the gate. + ## v7.1.0 - 2026-09-19 - The pdf view reports the pending annotation count on diff --git a/src/odr/internal/html/frontend/document.js b/src/odr/internal/html/frontend/document.js index b4eda931e..94642c146 100644 --- a/src/odr/internal/html/frontend/document.js +++ b/src/odr/internal/html/frontend/document.js @@ -752,7 +752,8 @@ /// States @p style on @p at, for a host's `format` and for a chord. A /// collapsed caret marks the word it sits in; at a word boundary, or in a /// paragraph holding no run, the mark waits for the next typed text. - /// Formatting sits behind the scope gate whole. + /// Scope `paragraph` holds the range, not the style: a mark inside one + /// paragraph opens none. function format(style, at) { gesture += 1; if (!odr.editing.isEnabled()) { @@ -767,7 +768,7 @@ refuse(null, "range", at); return false; } - if (odr.editing.scope() === "paragraph") { + if (outOfScope(at)) { refuse(null, "outOfScope", at); return false; } @@ -1499,6 +1500,12 @@ if (!odr.takesKeys("shortcuts")) { return; } + // The host draws no button for a key, so it cannot offer less here. + // The chord keeps the gate `format` no longer carries. + if (odr.editing.scope() === "paragraph") { + refuse(event, "outOfScope", at); + return; + } toggle(property, at); return; } diff --git a/test/browser/text/tests.html b/test/browser/text/tests.html index 2adc3c608..745366bfa 100644 --- a/test/browser/text/tests.html +++ b/test/browser/text/tests.html @@ -799,15 +799,17 @@ document.body.setAttribute("data-odr-editing-scope", "paragraph"); check("until the host narrows it", odr.editing.scope() === "paragraph"); - // Formatting sits behind the gate whole, so these land in runs no other - // refusal of this group names. + // The gate holds the range, not the style. The chord keeps it: the + // host draws no button for a key, so it cannot offer less. select(run(12).firstChild, 2); - check("a chord is refused", input("formatBold") === "refused"); + check("a chord is still refused", input("formatBold") === "refused"); check("as out of scope", refusals.pop() === "outOfScope 1010"); selectRuns(51, 0, 51, 4); - check("and so is the host's format", odr.editing.format({ bold: true }) === false); - check("as out of scope too", refusals.pop() === "outOfScope 1010"); - check("with nothing marked", run(51).getAttribute("style") === null); + check("while the host's own format is taken", odr.editing.format({ bold: true }) === true); + check("with the run marked", run(51).getAttribute("style") !== null); + selectRuns(11, 2, 31, 2); + check("a format over two paragraphs is refused", odr.editing.format({ bold: true }) === false); + check("as out of scope", refusals.pop().indexOf("outOfScope ") === 0); select(run(11).firstChild, 5); check("typing inside a run is taken", input("insertText", "X") === "taken"); @@ -864,7 +866,12 @@ check( "the log names no paragraph operation", ops().every(function (op) { - return op.op === "setText" || op.op === "removeElement" || op.op === "insertText"; + return ( + op.op === "setText" || + op.op === "setTextStyle" || + op.op === "removeElement" || + op.op === "insertText" + ); }) );