Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Editor] Update popup position and contents after a FreeText has been edited #17968

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 145 additions & 43 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ class AnnotationElement {

#hasBorder = false;

#popupElement = null;

constructor(
parameters,
{
Expand Down Expand Up @@ -214,13 +216,16 @@ class AnnotationElement {
if (rect) {
this.#setRectEdited(rect);
}

this.#popupElement?.popup.updateEdited(params);
}

resetEdited() {
if (!this.#updates) {
return;
}
this.#setRectEdited(this.#updates.rect);
this.#popupElement?.popup.resetEdited();
this.#updates = null;
}

Expand Down Expand Up @@ -610,7 +615,7 @@ class AnnotationElement {
const { container, data } = this;
container.setAttribute("aria-haspopup", "dialog");

const popup = new PopupAnnotationElement({
const popup = (this.#popupElement = new PopupAnnotationElement({
data: {
color: data.color,
titleObj: data.titleObj,
Expand All @@ -624,7 +629,7 @@ class AnnotationElement {
},
parent: this.parent,
elements: [this],
});
}));
this.parent.div.append(popup.render());
}

Expand Down Expand Up @@ -2055,12 +2060,13 @@ class PopupAnnotationElement extends AnnotationElement {
const { data, elements } = parameters;
super(parameters, { isRenderable: AnnotationElement._hasPopupData(data) });
this.elements = elements;
this.popup = null;
}

render() {
this.container.classList.add("popupAnnotation");

const popup = new PopupElement({
const popup = (this.popup = new PopupElement({
container: this.container,
color: this.data.color,
titleObj: this.data.titleObj,
Expand All @@ -2072,7 +2078,7 @@ class PopupAnnotationElement extends AnnotationElement {
parent: this.parent,
elements: this.elements,
open: this.data.open,
});
}));

const elementIds = [];
for (const element of this.elements) {
Expand Down Expand Up @@ -2117,12 +2123,16 @@ class PopupElement {

#popup = null;

#position = null;

#rect = null;

#richText = null;

#titleObj = null;

#updates = null;

#wasVisible = false;

constructor({
Expand Down Expand Up @@ -2188,12 +2198,6 @@ class PopupElement {
return;
}

const {
page: { view },
viewport: {
rawDims: { pageWidth, pageHeight, pageX, pageY },
},
} = this.#parent;
const popup = (this.#popup = document.createElement("div"));
popup.className = "popup";

Expand Down Expand Up @@ -2244,52 +2248,74 @@ class PopupElement {
header.append(modificationDate);
}

const contentsObj = this.#contentsObj;
const richText = this.#richText;
if (
richText?.str &&
(!contentsObj?.str || contentsObj.str === richText.str)
) {
const html = this.#html;
if (html) {
XfaLayer.render({
xfaHtml: richText.html,
xfaHtml: html,
intent: "richText",
div: popup,
});
popup.lastChild.classList.add("richText", "popupContent");
} else {
const contents = this._formatContents(contentsObj);
const contents = this._formatContents(this.#contentsObj);
popup.append(contents);
}
this.#container.append(popup);
}

let useParentRect = !!this.#parentRect;
let rect = useParentRect ? this.#parentRect : this.#rect;
for (const element of this.#elements) {
if (!rect || Util.intersect(element.data.rect, rect) !== null) {
rect = element.data.rect;
useParentRect = true;
break;
}
get #html() {
const richText = this.#richText;
const contentsObj = this.#contentsObj;
if (
richText?.str &&
(!contentsObj?.str || contentsObj.str === richText.str)
) {
return this.#richText.html || null;
}
return null;
}

const normalizedRect = Util.normalizeRect([
rect[0],
view[3] - rect[1] + view[1],
rect[2],
view[3] - rect[3] + view[1],
]);

const HORIZONTAL_SPACE_AFTER_ANNOTATION = 5;
const parentWidth = useParentRect
? rect[2] - rect[0] + HORIZONTAL_SPACE_AFTER_ANNOTATION
: 0;
const popupLeft = normalizedRect[0] + parentWidth;
const popupTop = normalizedRect[1];
get #fontSize() {
return this.#html?.attributes?.style?.fontSize || 0;
}

const { style } = this.#container;
style.left = `${(100 * (popupLeft - pageX)) / pageWidth}%`;
style.top = `${(100 * (popupTop - pageY)) / pageHeight}%`;
get #fontColor() {
return this.#html?.attributes?.style?.color || null;
}

this.#container.append(popup);
#makePopupContent(text) {
const popupLines = [];
const popupContent = {
str: text,
html: {
name: "div",
attributes: {
dir: "auto",
},
children: [
{
name: "p",
children: popupLines,
},
],
},
};
const lineAttributes = {
style: {
color: this.#fontColor,
fontSize: this.#fontSize
? `calc(${this.#fontSize}px * var(--scale-factor))`
: "",
},
};
for (const line of text.split("\n")) {
popupLines.push({
name: "span",
value: line,
attributes: lineAttributes,
});
}
return popupContent;
}

/**
Expand Down Expand Up @@ -2325,6 +2351,78 @@ class PopupElement {
}
}

updateEdited({ rect, popupContent }) {
this.#updates ||= {
contentsObj: this.#contentsObj,
richText: this.#richText,
};
if (rect) {
this.#position = null;
}
if (popupContent) {
this.#richText = this.#makePopupContent(popupContent);
this.#contentsObj = null;
}
this.#popup?.remove();
this.#popup = null;
}

resetEdited() {
if (!this.#updates) {
return;
}
({ contentsObj: this.#contentsObj, richText: this.#richText } =
this.#updates);
this.#updates = null;
this.#popup?.remove();
this.#popup = null;
this.#position = null;
}

#setPosition() {
if (this.#position !== null) {
return;
}
const {
page: { view },
viewport: {
rawDims: { pageWidth, pageHeight, pageX, pageY },
},
} = this.#parent;

let useParentRect = !!this.#parentRect;
let rect = useParentRect ? this.#parentRect : this.#rect;
for (const element of this.#elements) {
if (!rect || Util.intersect(element.data.rect, rect) !== null) {
rect = element.data.rect;
useParentRect = true;
break;
}
}

const normalizedRect = Util.normalizeRect([
rect[0],
view[3] - rect[1] + view[1],
rect[2],
view[3] - rect[3] + view[1],
]);

const HORIZONTAL_SPACE_AFTER_ANNOTATION = 5;
const parentWidth = useParentRect
? rect[2] - rect[0] + HORIZONTAL_SPACE_AFTER_ANNOTATION
: 0;
const popupLeft = normalizedRect[0] + parentWidth;
const popupTop = normalizedRect[1];
this.#position = [
(100 * (popupLeft - pageX)) / pageWidth,
(100 * (popupTop - pageY)) / pageHeight,
];

const { style } = this.#container;
style.left = `${this.#position[0]}%`;
style.top = `${this.#position[1]}%`;
}

/**
* Toggle the visibility of the popup.
*/
Expand All @@ -2349,6 +2447,7 @@ class PopupElement {
this.render();
}
if (!this.isVisible) {
this.#setPosition();
this.#container.hidden = false;
this.#container.style.zIndex =
parseInt(this.#container.style.zIndex) + 1000;
Expand Down Expand Up @@ -2382,6 +2481,9 @@ class PopupElement {
if (!this.#wasVisible) {
return;
}
if (!this.#popup) {
this.#show();
}
this.#wasVisible = false;
this.#container.hidden = false;
}
Expand Down
1 change: 1 addition & 0 deletions src/display/editor/freetext.js
Original file line number Diff line number Diff line change
Expand Up @@ -879,6 +879,7 @@ class FreeTextEditor extends AnnotationEditor {
const padding = FreeTextEditor._internalPadding * this.parentScale;
annotation.updateEdited({
rect: this.getRect(padding, padding),
popupContent: this.#content,
});

return content;
Expand Down
79 changes: 79 additions & 0 deletions test/integration/freetext_editor_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,85 @@ describe("FreeText Editor", () => {
});
});

describe("FreeText (update existing and popups)", () => {
let pages;

beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", "[data-annotation-id='32R']");
});

afterAll(async () => {
await closePages(pages);
});

it("must update an existing annotation and show the right popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Show the popup on "Hello World from Firefox"
await page.click(`[data-annotation-id='32R']`);
await page.waitForSelector(`[data-annotation-id='popup_32R']`, {
visible: true,
});

await switchToFreeText(page);
await page.waitForSelector(`[data-annotation-id='popup_32R']`, {
visible: false,
});

const editorSelector = getEditorSelector(1);
const editorRect = await page.$eval(editorSelector, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ count: 2 }
);
await page.waitForSelector(
`${editorSelector} .overlay:not(.enabled)`
);

await kbGoToEnd(page);
await page.waitForFunction(
sel =>
document.getSelection().anchorOffset ===
document.querySelector(sel).innerText.length,
{},
`${editorSelector} .internal`
);

await page.type(
`${editorSelector} .internal`,
" and edited in Firefox"
);

// Commit.
await page.keyboard.press("Escape");
await page.waitForSelector(`${editorSelector} .overlay.enabled`);

// Disable editing mode.
await page.click("#editorFreeText");
await page.waitForSelector(
`.annotationEditorLayer:not(.freetextEditing)`
);

await page.waitForSelector(`[data-annotation-id='popup_32R']`, {
visible: true,
});

const newPopupText = await page.$eval(
"[data-annotation-id='popup_32R'] .popupContent",
el => el.innerText.replaceAll("\xa0", " ")
);
expect(newPopupText)
.withContext(`In ${browserName}`)
.toEqual("Hello World From Firefox and edited in Firefox");
})
);
});
});

describe("FreeText (update existing but not empty ones)", () => {
let pages;

Expand Down