mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🐛 Fix spurious mixed text styles on multi-span selection
This commit is contained in:
parent
36c23faae0
commit
62cc555084
@ -278,19 +278,30 @@ export class SelectionController extends EventTarget {
|
||||
// FIXME: I don't like this approximation. Having to iterate nodes twice
|
||||
// is bad for performance. I think we need another way of "computing"
|
||||
// the cascade.
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textNodesInRange = [
|
||||
...this.#textNodeIterator.iterateFrom(startNode, endNode),
|
||||
];
|
||||
for (const textNode of textNodesInRange) {
|
||||
const paragraph = textNode.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(paragraph);
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
// Empty trailing text runs (length 0) often carry paragraph fallback styles
|
||||
// (e.g. font-size 0) and are not user-visible; merging them with real text
|
||||
// yields false "mixed" in the sidebar. Skip empty nodes when the selection
|
||||
// also includes non-empty text; if everything is empty, keep prior behavior.
|
||||
const nonEmptyTextNodes = textNodesInRange.filter(
|
||||
(textNode) => textNode.length > 0,
|
||||
);
|
||||
const spanMergeNodes =
|
||||
nonEmptyTextNodes.length > 0 ? nonEmptyTextNodes : textNodesInRange;
|
||||
|
||||
if (spanMergeNodes.length > 0) {
|
||||
const firstTextSpan = spanMergeNodes[0].parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(firstTextSpan);
|
||||
for (let i = 1; i < spanMergeNodes.length; i++) {
|
||||
const textSpan = spanMergeNodes[i].parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
createParagraph,
|
||||
createParagraphWith,
|
||||
} from "../content/dom/Paragraph.js";
|
||||
import { createTextSpan } from "../content/dom/TextSpan.js";
|
||||
import { createTextSpan, createVoidTextSpan } from "../content/dom/TextSpan.js";
|
||||
import { createLineBreak } from "../content/dom/LineBreak.js";
|
||||
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
||||
import { SelectionController } from "./SelectionController.js";
|
||||
@ -1683,6 +1683,54 @@ describe("SelectionController", () => {
|
||||
expect(selectionController.focusAtEnd).toBeTruthy();
|
||||
})
|
||||
|
||||
test("`currentStyle` ignores empty text nodes when merging span styles (no false mixed font-size)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([
|
||||
createTextSpan(new Text("Hello"), { "font-size": "50" }),
|
||||
createVoidTextSpan({ "font-size": "0" }),
|
||||
]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const paragraph = root.firstChild;
|
||||
const firstTextNode = paragraph.firstChild.firstChild;
|
||||
const lastTextNode = paragraph.firstChild.nextSibling.firstChild;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
textEditorMock.element.focus();
|
||||
selection.setBaseAndExtent(firstTextNode, 0, lastTextNode, 0);
|
||||
document.dispatchEvent(new Event("selectionchange"));
|
||||
expect(selectionController.currentStyle.getPropertyValue("font-size")).toBe(
|
||||
"50px",
|
||||
);
|
||||
});
|
||||
|
||||
test("`currentStyle` stays mixed when two non-empty spans have different font sizes", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([
|
||||
createTextSpan(new Text("Hello"), { "font-size": "50" }),
|
||||
createTextSpan(new Text("World"), { "font-size": "36" }),
|
||||
]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const paragraph = root.firstChild;
|
||||
const firstTextNode = paragraph.firstChild.firstChild;
|
||||
const lastTextNode = paragraph.firstChild.nextSibling.firstChild;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
textEditorMock.element.focus();
|
||||
selection.setBaseAndExtent(firstTextNode, 0, lastTextNode, 5);
|
||||
document.dispatchEvent(new Event("selectionchange"));
|
||||
expect(selectionController.currentStyle.getPropertyValue("font-size")).toBe(
|
||||
"mixed",
|
||||
);
|
||||
});
|
||||
|
||||
test("`currentStyle` uses text span font-size when anchor is paragraph (Firefox-style word selection)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user