Skip to content

Commit

Permalink
Add text wrapping to FreeText editors (issue 18191)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
avdoseferovic committed Jan 19, 2025
1 parent 45a32b7 commit cd76eca
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 2 deletions.
50 changes: 49 additions & 1 deletion src/display/editor/freetext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

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

return this.div;
}

Expand Down
52 changes: 52 additions & 0 deletions test/integration/freetext_editor_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3519,4 +3519,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();
})
);
});
});
});
3 changes: 2 additions & 1 deletion web/annotation_editor_layer_builder.css
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,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;
Expand Down

0 comments on commit cd76eca

Please sign in to comment.