mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-25 16:37:22 +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).
|
// text and one for the br element).
|
||||||
continue;
|
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;
|
prevChild = child;
|
||||||
}
|
}
|
||||||
return buffer.join("\n");
|
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() {
|
#setEditorDimensions() {
|
||||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||||
|
|
||||||
@ -642,6 +686,9 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
this.div.setAttribute("annotation-id", this.annotationElementId);
|
this.div.setAttribute("annotation-id", this.annotationElementId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [, pageHeight] = this.pageDimensions;
|
||||||
|
this.setMaxWidth(this.div, this.rotation, pageHeight);
|
||||||
|
|
||||||
return this.div;
|
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 */
|
/** @inheritdoc */
|
||||||
renderAnnotationElement(annotation) {
|
renderAnnotationElement(annotation) {
|
||||||
const content = super.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;
|
border: none;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
white-space: nowrap;
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
font: 10px sans-serif;
|
font: 10px sans-serif;
|
||||||
line-height: var(--freetext-line-height);
|
line-height: var(--freetext-line-height);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user