Skip to content

Commit

Permalink
[Editor] Update popup position and contents after a FreeText has been…
Browse files Browse the repository at this point in the history
… edited
  • Loading branch information
calixteman committed May 21, 2024
1 parent dce9c6d commit 2369e40
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 43 deletions.
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

0 comments on commit 2369e40

Please sign in to comment.