Add text wrapping to FreeText editors (issue 18191)

Text wrapping is ensured by preventing the FreeText editor from going
out of bounds.
This commit is contained in:
avdoseferovic 2025-01-18 23:29:31 +01:00
parent 6243afa85e
commit a47cd370af
3 changed files with 128 additions and 2 deletions

View File

@ -409,12 +409,56 @@ class FreeTextEditor extends AnnotationEditor {
// text and one for the br element).
continue;
}
buffer.push(FreeTextEditor.#getNodeContent(child));
if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) {
const visualLines = this.#detectVisualLineBreaks(child);
buffer.push(...visualLines);
} else {
buffer.push(FreeTextEditor.#getNodeContent(child));
}
prevChild = child;
}
return buffer.join("\n");
}
/**
* Detects line breaks within a text node.
* Algorithm is based on this gist:
* https://gist.github.com/bennadel/033e0158f47bff9e066016f99567ebba
* @param {Text} textNode
* @returns {Array<string>}
*/
#detectVisualLineBreaks(textNode) {
const range = document.createRange();
const lines = [];
let lineCharacters = [];
const text = textNode.textContent.trim().replaceAll(/\s+/g, " ");
if (!text) {
return [];
}
for (let i = 0; i < text.length; i++) {
range.setStart(textNode, 0);
range.setEnd(textNode, i + 1);
const lineIndex = range.getClientRects().length - 1;
if (!lines[lineIndex]) {
lines.push((lineCharacters = []));
}
lineCharacters.push(text.charAt(i));
}
range.detach();
return lines
.map(characters => characters.join("").trim())
.filter(line => line.length > 0);
}
#setEditorDimensions() {
const [parentWidth, parentHeight] = this.parentDimensions;
@ -642,6 +686,9 @@ class FreeTextEditor extends AnnotationEditor {
this.div.setAttribute("annotation-id", this.annotationElementId);
}
const [, pageHeight] = this.pageDimensions;
this.setMaxWidth(this.div, this.rotation, pageHeight);
return this.div;
}
@ -867,6 +914,32 @@ class FreeTextEditor extends AnnotationEditor {
);
}
setMaxWidth(div, rotation, pageHeight) {
const style = div.style;
const leftPercent = parseFloat(style.left) || 0;
const topPercent = parseFloat(style.top) || 0;
const fontSize = `calc(${this.#fontSize}px * var(--total-scale-factor))`;
switch (rotation) {
case 0: {
style.maxWidth = `calc(100% - ${leftPercent}% - ${fontSize})`;
break;
}
case 90: {
style.maxWidth = `calc(var(--total-scale-factor) * ${pageHeight}px * ${topPercent} / 100 - ${fontSize})`;
break;
}
case 180: {
style.maxWidth = `calc(${leftPercent}% - ${fontSize})`;
break;
}
case 270: {
style.maxWidth = `calc(var(--total-scale-factor) * ${pageHeight}px * (100 - ${topPercent}) / 100 - ${fontSize})`;
break;
}
}
}
/** @inheritdoc */
renderAnnotationElement(annotation) {
const content = super.renderAnnotationElement(annotation);

View File

@ -3267,4 +3267,56 @@ describe("FreeText Editor", () => {
);
});
});
describe("FreeText text wrapping", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must wrap long text into multiple lines", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
const longText =
"This is a very long text string that should definitely need to wrap onto multiple lines of text so that it can be displayed properly within the FreeText annotation editor.";
await page.type(`${editorSelector} .internal`, longText);
const hasMultipleLines = await page.evaluate(selector => {
const el = document.querySelector(`${selector} .internal`);
const style = window.getComputedStyle(el);
const lineHeight = parseFloat(style.lineHeight);
const totalHeight = el.getBoundingClientRect().height;
return totalHeight > lineHeight;
}, editorSelector);
expect(hasMultipleLines).withContext(`In ${browserName}`).toBeTrue();
await commit(page);
const maintainsWrapping = await page.evaluate(selector => {
const el = document.querySelector(`${selector} .internal`);
const style = window.getComputedStyle(el);
const lineHeight = parseFloat(style.lineHeight);
const totalHeight = el.getBoundingClientRect().height;
return totalHeight > lineHeight * 1.5;
}, editorSelector);
expect(maintainsWrapping).withContext(`In ${browserName}`).toBeTrue();
})
);
});
});
});

View File

@ -523,7 +523,8 @@
border: none;
inset: 0;
overflow: visible;
white-space: nowrap;
white-space: pre-wrap;
word-wrap: break-word;
font: 10px sans-serif;
line-height: var(--freetext-line-height);
user-select: none;