Skip to content

Commit cd76eca

Browse files
committed
Add text wrapping to FreeText editors (issue 18191)
Text wrapping is ensured by preventing the FreeText editor from going out of bounds. - max-width is a formula of (100% of the parentElement - the left % of the editor element) - this ensures that clicking on the very right edge of the document does - not create the editor out of bounds.
1 parent 45a32b7 commit cd76eca

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed

src/display/editor/freetext.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,12 +409,56 @@ class FreeTextEditor extends AnnotationEditor {
409409
// text and one for the br element).
410410
continue;
411411
}
412-
buffer.push(FreeTextEditor.#getNodeContent(child));
412+
if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) {
413+
const visualLines = this.#detectVisualLineBreaks(child);
414+
buffer.push(...visualLines);
415+
} else {
416+
buffer.push(FreeTextEditor.#getNodeContent(child));
417+
}
418+
413419
prevChild = child;
414420
}
415421
return buffer.join("\n");
416422
}
417423

424+
/**
425+
* Detects line breaks within a text node.
426+
* Algorithm is based on this gist:
427+
* https://gist.github.com/bennadel/033e0158f47bff9e066016f99567ebba
428+
* @param {Text} textNode
429+
* @returns {Array<string>}
430+
*/
431+
#detectVisualLineBreaks(textNode) {
432+
const range = document.createRange();
433+
const lines = [];
434+
let lineCharacters = [];
435+
436+
const text = textNode.textContent.trim().replaceAll(/\s+/g, " ");
437+
438+
if (!text) {
439+
return [];
440+
}
441+
442+
for (let i = 0; i < text.length; i++) {
443+
range.setStart(textNode, 0);
444+
range.setEnd(textNode, i + 1);
445+
446+
const lineIndex = range.getClientRects().length - 1;
447+
448+
if (!lines[lineIndex]) {
449+
lines.push((lineCharacters = []));
450+
}
451+
452+
lineCharacters.push(text.charAt(i));
453+
}
454+
455+
range.detach();
456+
457+
return lines
458+
.map(characters => characters.join("").trim())
459+
.filter(line => line.length > 0);
460+
}
461+
418462
#setEditorDimensions() {
419463
const [parentWidth, parentHeight] = this.parentDimensions;
420464

@@ -647,6 +691,10 @@ class FreeTextEditor extends AnnotationEditor {
647691
this.div.setAttribute("annotation-id", this.annotationElementId);
648692
}
649693

694+
// Not sure why we need the -16px but it's neccessary to prevent
695+
// text shifting when user finishes typing.
696+
this.div.style.maxWidth = `calc(100% - ${this.div.style.left} - 16px)`;
697+
650698
return this.div;
651699
}
652700

test/integration/freetext_editor_spec.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3519,4 +3519,56 @@ describe("FreeText Editor", () => {
35193519
);
35203520
});
35213521
});
3522+
3523+
describe("FreeText text wrapping", () => {
3524+
let pages;
3525+
3526+
beforeAll(async () => {
3527+
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
3528+
});
3529+
3530+
afterAll(async () => {
3531+
await closePages(pages);
3532+
});
3533+
3534+
it("must wrap long text into multiple lines", async () => {
3535+
await Promise.all(
3536+
pages.map(async ([browserName, page]) => {
3537+
await switchToFreeText(page);
3538+
3539+
const rect = await getRect(page, ".annotationEditorLayer");
3540+
const editorSelector = getEditorSelector(0);
3541+
3542+
await page.mouse.click(rect.x + 100, rect.y + 100);
3543+
await page.waitForSelector(editorSelector, { visible: true });
3544+
3545+
const longText =
3546+
"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.";
3547+
await page.type(`${editorSelector} .internal`, longText);
3548+
3549+
const hasMultipleLines = await page.evaluate(selector => {
3550+
const el = document.querySelector(`${selector} .internal`);
3551+
const style = window.getComputedStyle(el);
3552+
const lineHeight = parseFloat(style.lineHeight);
3553+
const totalHeight = el.getBoundingClientRect().height;
3554+
return totalHeight > lineHeight;
3555+
}, editorSelector);
3556+
3557+
expect(hasMultipleLines).withContext(`In ${browserName}`).toBeTrue();
3558+
3559+
await commit(page);
3560+
3561+
const maintainsWrapping = await page.evaluate(selector => {
3562+
const el = document.querySelector(`${selector} .internal`);
3563+
const style = window.getComputedStyle(el);
3564+
const lineHeight = parseFloat(style.lineHeight);
3565+
const totalHeight = el.getBoundingClientRect().height;
3566+
return totalHeight > lineHeight * 1.5;
3567+
}, editorSelector);
3568+
3569+
expect(maintainsWrapping).withContext(`In ${browserName}`).toBeTrue();
3570+
})
3571+
);
3572+
});
3573+
});
35223574
});

web/annotation_editor_layer_builder.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,8 @@
517517
border: none;
518518
inset: 0;
519519
overflow: visible;
520-
white-space: nowrap;
520+
white-space: pre-wrap;
521+
word-wrap: break-word;
521522
font: 10px sans-serif;
522523
line-height: var(--freetext-line-height);
523524
user-select: none;

0 commit comments

Comments
 (0)