mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-02 05:05:47 +02:00
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:
parent
6243afa85e
commit
a47cd370af
@ -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);
|
||||
|
||||
@ -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();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user