Merge pull request #17968 from calixteman/freetext_update_popup
[Editor] Update popup position and contents after a FreeText has been edited
This commit is contained in:
commit
6860288df2
@ -162,6 +162,8 @@ class AnnotationElement {
|
|||||||
|
|
||||||
#hasBorder = false;
|
#hasBorder = false;
|
||||||
|
|
||||||
|
#popupElement = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
parameters,
|
parameters,
|
||||||
{
|
{
|
||||||
@ -214,6 +216,8 @@ class AnnotationElement {
|
|||||||
if (rect) {
|
if (rect) {
|
||||||
this.#setRectEdited(rect);
|
this.#setRectEdited(rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#popupElement?.popup.updateEdited(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetEdited() {
|
resetEdited() {
|
||||||
@ -221,6 +225,7 @@ class AnnotationElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#setRectEdited(this.#updates.rect);
|
this.#setRectEdited(this.#updates.rect);
|
||||||
|
this.#popupElement?.popup.resetEdited();
|
||||||
this.#updates = null;
|
this.#updates = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,7 +615,7 @@ class AnnotationElement {
|
|||||||
const { container, data } = this;
|
const { container, data } = this;
|
||||||
container.setAttribute("aria-haspopup", "dialog");
|
container.setAttribute("aria-haspopup", "dialog");
|
||||||
|
|
||||||
const popup = new PopupAnnotationElement({
|
const popup = (this.#popupElement = new PopupAnnotationElement({
|
||||||
data: {
|
data: {
|
||||||
color: data.color,
|
color: data.color,
|
||||||
titleObj: data.titleObj,
|
titleObj: data.titleObj,
|
||||||
@ -624,7 +629,7 @@ class AnnotationElement {
|
|||||||
},
|
},
|
||||||
parent: this.parent,
|
parent: this.parent,
|
||||||
elements: [this],
|
elements: [this],
|
||||||
});
|
}));
|
||||||
this.parent.div.append(popup.render());
|
this.parent.div.append(popup.render());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2055,12 +2060,13 @@ class PopupAnnotationElement extends AnnotationElement {
|
|||||||
const { data, elements } = parameters;
|
const { data, elements } = parameters;
|
||||||
super(parameters, { isRenderable: AnnotationElement._hasPopupData(data) });
|
super(parameters, { isRenderable: AnnotationElement._hasPopupData(data) });
|
||||||
this.elements = elements;
|
this.elements = elements;
|
||||||
|
this.popup = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.container.classList.add("popupAnnotation");
|
this.container.classList.add("popupAnnotation");
|
||||||
|
|
||||||
const popup = new PopupElement({
|
const popup = (this.popup = new PopupElement({
|
||||||
container: this.container,
|
container: this.container,
|
||||||
color: this.data.color,
|
color: this.data.color,
|
||||||
titleObj: this.data.titleObj,
|
titleObj: this.data.titleObj,
|
||||||
@ -2072,7 +2078,7 @@ class PopupAnnotationElement extends AnnotationElement {
|
|||||||
parent: this.parent,
|
parent: this.parent,
|
||||||
elements: this.elements,
|
elements: this.elements,
|
||||||
open: this.data.open,
|
open: this.data.open,
|
||||||
});
|
}));
|
||||||
|
|
||||||
const elementIds = [];
|
const elementIds = [];
|
||||||
for (const element of this.elements) {
|
for (const element of this.elements) {
|
||||||
@ -2117,12 +2123,16 @@ class PopupElement {
|
|||||||
|
|
||||||
#popup = null;
|
#popup = null;
|
||||||
|
|
||||||
|
#position = null;
|
||||||
|
|
||||||
#rect = null;
|
#rect = null;
|
||||||
|
|
||||||
#richText = null;
|
#richText = null;
|
||||||
|
|
||||||
#titleObj = null;
|
#titleObj = null;
|
||||||
|
|
||||||
|
#updates = null;
|
||||||
|
|
||||||
#wasVisible = false;
|
#wasVisible = false;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
@ -2188,12 +2198,6 @@ class PopupElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
|
||||||
page: { view },
|
|
||||||
viewport: {
|
|
||||||
rawDims: { pageWidth, pageHeight, pageX, pageY },
|
|
||||||
},
|
|
||||||
} = this.#parent;
|
|
||||||
const popup = (this.#popup = document.createElement("div"));
|
const popup = (this.#popup = document.createElement("div"));
|
||||||
popup.className = "popup";
|
popup.className = "popup";
|
||||||
|
|
||||||
@ -2244,54 +2248,76 @@ class PopupElement {
|
|||||||
header.append(modificationDate);
|
header.append(modificationDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentsObj = this.#contentsObj;
|
const html = this.#html;
|
||||||
const richText = this.#richText;
|
if (html) {
|
||||||
if (
|
|
||||||
richText?.str &&
|
|
||||||
(!contentsObj?.str || contentsObj.str === richText.str)
|
|
||||||
) {
|
|
||||||
XfaLayer.render({
|
XfaLayer.render({
|
||||||
xfaHtml: richText.html,
|
xfaHtml: html,
|
||||||
intent: "richText",
|
intent: "richText",
|
||||||
div: popup,
|
div: popup,
|
||||||
});
|
});
|
||||||
popup.lastChild.classList.add("richText", "popupContent");
|
popup.lastChild.classList.add("richText", "popupContent");
|
||||||
} else {
|
} else {
|
||||||
const contents = this._formatContents(contentsObj);
|
const contents = this._formatContents(this.#contentsObj);
|
||||||
popup.append(contents);
|
popup.append(contents);
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
|
||||||
|
|
||||||
const { style } = this.#container;
|
|
||||||
style.left = `${(100 * (popupLeft - pageX)) / pageWidth}%`;
|
|
||||||
style.top = `${(100 * (popupTop - pageY)) / pageHeight}%`;
|
|
||||||
|
|
||||||
this.#container.append(popup);
|
this.#container.append(popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #fontSize() {
|
||||||
|
return this.#html?.attributes?.style?.fontSize || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #fontColor() {
|
||||||
|
return this.#html?.attributes?.style?.color || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format the contents of the popup by adding newlines where necessary.
|
* Format the contents of the popup by adding newlines where necessary.
|
||||||
*
|
*
|
||||||
@ -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.
|
* Toggle the visibility of the popup.
|
||||||
*/
|
*/
|
||||||
@ -2349,6 +2447,7 @@ class PopupElement {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
if (!this.isVisible) {
|
if (!this.isVisible) {
|
||||||
|
this.#setPosition();
|
||||||
this.#container.hidden = false;
|
this.#container.hidden = false;
|
||||||
this.#container.style.zIndex =
|
this.#container.style.zIndex =
|
||||||
parseInt(this.#container.style.zIndex) + 1000;
|
parseInt(this.#container.style.zIndex) + 1000;
|
||||||
@ -2382,6 +2481,9 @@ class PopupElement {
|
|||||||
if (!this.#wasVisible) {
|
if (!this.#wasVisible) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this.#popup) {
|
||||||
|
this.#show();
|
||||||
|
}
|
||||||
this.#wasVisible = false;
|
this.#wasVisible = false;
|
||||||
this.#container.hidden = false;
|
this.#container.hidden = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -879,6 +879,7 @@ class FreeTextEditor extends AnnotationEditor {
|
|||||||
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
const padding = FreeTextEditor._internalPadding * this.parentScale;
|
||||||
annotation.updateEdited({
|
annotation.updateEdited({
|
||||||
rect: this.getRect(padding, padding),
|
rect: this.getRect(padding, padding),
|
||||||
|
popupContent: this.#content,
|
||||||
});
|
});
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|||||||
@ -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)", () => {
|
describe("FreeText (update existing but not empty ones)", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user