mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-19 14:48:08 +02:00
Merge pull request #18134 from calixteman/hide_annotations
[api-minor][Editor] When switching to editing mode, redraw pages containing editable annotations (bug 1883884)
This commit is contained in:
commit
bdcc4a0feb
13 changed files with 358 additions and 103 deletions
|
@ -680,6 +680,7 @@ class Annotation {
|
|||
hasOwnCanvas: false,
|
||||
noRotate: !!(this.flags & AnnotationFlag.NOROTATE),
|
||||
noHTML: isLocked && isContentLocked,
|
||||
isEditable: false,
|
||||
};
|
||||
|
||||
if (params.collectFields) {
|
||||
|
@ -776,6 +777,10 @@ class Annotation {
|
|||
return this.printable;
|
||||
}
|
||||
|
||||
mustBeViewedWhenEditing() {
|
||||
return !this.data.isEditable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
|
@ -3802,7 +3807,8 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
|||
// It uses its own canvas in order to be hidden if edited.
|
||||
// But if it has the noHTML flag, it means that we don't want to be able
|
||||
// to modify it so we can just draw it on the main canvas.
|
||||
this.data.hasOwnCanvas = !this.data.noHTML;
|
||||
this.data.hasOwnCanvas = this.data.noRotate;
|
||||
this.data.isEditable = !this.data.noHTML;
|
||||
// We want to be able to add mouse listeners to the annotation.
|
||||
this.data.noHTML = false;
|
||||
|
||||
|
|
|
@ -411,6 +411,8 @@ class Page {
|
|||
intent,
|
||||
cacheKey,
|
||||
annotationStorage = null,
|
||||
isEditing = false,
|
||||
modifiedIds = null,
|
||||
}) {
|
||||
const contentStreamPromise = this.getContentStream();
|
||||
const resourcesPromise = this.loadResources([
|
||||
|
@ -579,7 +581,9 @@ class Page {
|
|||
if (
|
||||
intentAny ||
|
||||
(intentDisplay &&
|
||||
annotation.mustBeViewed(annotationStorage, renderForms)) ||
|
||||
annotation.mustBeViewed(annotationStorage, renderForms) &&
|
||||
((isEditing && annotation.mustBeViewedWhenEditing()) ||
|
||||
(!isEditing && !modifiedIds?.has(annotation.data.id)))) ||
|
||||
(intentPrint && annotation.mustBePrinted(annotationStorage))
|
||||
) {
|
||||
opListPromises.push(
|
||||
|
|
|
@ -752,6 +752,8 @@ class WorkerMessageHandler {
|
|||
intent: data.intent,
|
||||
cacheKey: data.cacheKey,
|
||||
annotationStorage: data.annotationStorage,
|
||||
isEditing: data.isEditing,
|
||||
modifiedIds: data.modifiedIds,
|
||||
})
|
||||
.then(
|
||||
function (operatorListInfo) {
|
||||
|
|
|
@ -198,6 +198,10 @@ class AnnotationElement {
|
|||
return !!(titleObj?.str || contentsObj?.str || richText?.str);
|
||||
}
|
||||
|
||||
get _isEditable() {
|
||||
return this.data.isEditable;
|
||||
}
|
||||
|
||||
get hasPopupData() {
|
||||
return AnnotationElement._hasPopupData(this.data);
|
||||
}
|
||||
|
@ -734,10 +738,6 @@ class AnnotationElement {
|
|||
}
|
||||
}
|
||||
|
||||
get _isEditable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
_editOnDoubleClick() {
|
||||
if (!this._isEditable) {
|
||||
return;
|
||||
|
@ -2530,10 +2530,6 @@ class FreeTextAnnotationElement extends AnnotationElement {
|
|||
|
||||
return this.container;
|
||||
}
|
||||
|
||||
get _isEditable() {
|
||||
return this.data.hasOwnCanvas;
|
||||
}
|
||||
}
|
||||
|
||||
class LineAnnotationElement extends AnnotationElement {
|
||||
|
@ -3107,6 +3103,10 @@ class AnnotationLayer {
|
|||
}
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return this.#editableAnnotations.size > 0;
|
||||
}
|
||||
|
||||
#appendElement(element, id) {
|
||||
const contentElement = element.firstChild || element;
|
||||
contentElement.id = `${AnnotationPrefix}${id}`;
|
||||
|
@ -3188,7 +3188,7 @@ class AnnotationLayer {
|
|||
}
|
||||
this.#appendElement(rendered, data.id);
|
||||
|
||||
if (element.annotationEditorType > 0) {
|
||||
if (element._isEditable) {
|
||||
this.#editableAnnotations.set(element.data.id, element);
|
||||
this._annotationEditorUIManager?.renderAnnotationElement(element);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { objectFromMap, unreachable } from "../shared/util.js";
|
||||
import { objectFromMap, shadow, unreachable } from "../shared/util.js";
|
||||
import { AnnotationEditor } from "./editor/editor.js";
|
||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||
|
||||
|
@ -29,6 +29,8 @@ const SerializableEmpty = Object.freeze({
|
|||
class AnnotationStorage {
|
||||
#modified = false;
|
||||
|
||||
#modifiedIds = null;
|
||||
|
||||
#storage = new Map();
|
||||
|
||||
constructor() {
|
||||
|
@ -248,6 +250,34 @@ class AnnotationStorage {
|
|||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
resetModifiedIds() {
|
||||
this.#modifiedIds = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ids: Set<string>, hash: string}}
|
||||
*/
|
||||
get modifiedIds() {
|
||||
if (this.#modifiedIds) {
|
||||
return this.#modifiedIds;
|
||||
}
|
||||
const ids = [];
|
||||
for (const value of this.#storage.values()) {
|
||||
if (
|
||||
!(value instanceof AnnotationEditor) ||
|
||||
!value.annotationElementId ||
|
||||
!value.serialize()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
ids.push(value.annotationElementId);
|
||||
}
|
||||
return (this.#modifiedIds = {
|
||||
ids: new Set(ids),
|
||||
hash: ids.join(","),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -282,6 +312,13 @@ class PrintAnnotationStorage extends AnnotationStorage {
|
|||
get serializable() {
|
||||
return this.#serializable;
|
||||
}
|
||||
|
||||
get modifiedIds() {
|
||||
return shadow(this, "modifiedIds", {
|
||||
ids: new Set(),
|
||||
hash: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { AnnotationStorage, PrintAnnotationStorage, SerializableEmpty };
|
||||
|
|
|
@ -1227,6 +1227,7 @@ class PDFDocumentProxy {
|
|||
* @property {Map<string, HTMLCanvasElement>} [annotationCanvasMap] - Map some
|
||||
* annotation ids with canvases used to render them.
|
||||
* @property {PrintAnnotationStorage} [printAnnotationStorage]
|
||||
* @property {boolean} [isEditing] - Render the page in editing mode.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -1248,6 +1249,7 @@ class PDFDocumentProxy {
|
|||
* from the {@link AnnotationStorage}-instance; useful e.g. for printing.
|
||||
* The default value is `AnnotationMode.ENABLE`.
|
||||
* @property {PrintAnnotationStorage} [printAnnotationStorage]
|
||||
* @property {boolean} [isEditing] - Render the page in editing mode.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -1420,13 +1422,15 @@ class PDFPageProxy {
|
|||
annotationCanvasMap = null,
|
||||
pageColors = null,
|
||||
printAnnotationStorage = null,
|
||||
isEditing = false,
|
||||
}) {
|
||||
this._stats?.time("Overall");
|
||||
|
||||
const intentArgs = this._transport.getRenderingIntent(
|
||||
intent,
|
||||
annotationMode,
|
||||
printAnnotationStorage
|
||||
printAnnotationStorage,
|
||||
isEditing
|
||||
);
|
||||
const { renderingIntent, cacheKey } = intentArgs;
|
||||
// If there was a pending destroy, cancel it so no cleanup happens during
|
||||
|
@ -1560,6 +1564,7 @@ class PDFPageProxy {
|
|||
intent = "display",
|
||||
annotationMode = AnnotationMode.ENABLE,
|
||||
printAnnotationStorage = null,
|
||||
isEditing = false,
|
||||
} = {}) {
|
||||
if (typeof PDFJSDev !== "undefined" && !PDFJSDev.test("GENERIC")) {
|
||||
throw new Error("Not implemented: getOperatorList");
|
||||
|
@ -1576,6 +1581,7 @@ class PDFPageProxy {
|
|||
intent,
|
||||
annotationMode,
|
||||
printAnnotationStorage,
|
||||
isEditing,
|
||||
/* isOpList = */ true
|
||||
);
|
||||
let intentState = this._intentStates.get(intentArgs.cacheKey);
|
||||
|
@ -1812,6 +1818,8 @@ class PDFPageProxy {
|
|||
renderingIntent,
|
||||
cacheKey,
|
||||
annotationStorageSerializable,
|
||||
isEditing,
|
||||
modifiedIds,
|
||||
}) {
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) {
|
||||
assert(
|
||||
|
@ -1828,6 +1836,8 @@ class PDFPageProxy {
|
|||
intent: renderingIntent,
|
||||
cacheKey,
|
||||
annotationStorage: map,
|
||||
isEditing,
|
||||
modifiedIds,
|
||||
},
|
||||
transfer
|
||||
);
|
||||
|
@ -2420,6 +2430,7 @@ class WorkerTransport {
|
|||
intent,
|
||||
annotationMode = AnnotationMode.ENABLE,
|
||||
printAnnotationStorage = null,
|
||||
isEditing = false,
|
||||
isOpList = false
|
||||
) {
|
||||
let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value.
|
||||
|
@ -2438,6 +2449,12 @@ class WorkerTransport {
|
|||
warn(`getRenderingIntent - invalid intent: ${intent}`);
|
||||
}
|
||||
|
||||
const annotationStorage =
|
||||
renderingIntent & RenderingIntentFlag.PRINT &&
|
||||
printAnnotationStorage instanceof PrintAnnotationStorage
|
||||
? printAnnotationStorage
|
||||
: this.annotationStorage;
|
||||
|
||||
switch (annotationMode) {
|
||||
case AnnotationMode.DISABLE:
|
||||
renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISABLE;
|
||||
|
@ -2450,12 +2467,6 @@ class WorkerTransport {
|
|||
case AnnotationMode.ENABLE_STORAGE:
|
||||
renderingIntent += RenderingIntentFlag.ANNOTATIONS_STORAGE;
|
||||
|
||||
const annotationStorage =
|
||||
renderingIntent & RenderingIntentFlag.PRINT &&
|
||||
printAnnotationStorage instanceof PrintAnnotationStorage
|
||||
? printAnnotationStorage
|
||||
: this.annotationStorage;
|
||||
|
||||
annotationStorageSerializable = annotationStorage.serializable;
|
||||
break;
|
||||
default:
|
||||
|
@ -2466,10 +2477,22 @@ class WorkerTransport {
|
|||
renderingIntent += RenderingIntentFlag.OPLIST;
|
||||
}
|
||||
|
||||
const { ids: modifiedIds, hash: modifiedIdsHash } =
|
||||
annotationStorage.modifiedIds;
|
||||
|
||||
const cacheKeyBuf = [
|
||||
renderingIntent,
|
||||
annotationStorageSerializable.hash,
|
||||
isEditing ? 1 : 0,
|
||||
modifiedIdsHash,
|
||||
];
|
||||
|
||||
return {
|
||||
renderingIntent,
|
||||
cacheKey: `${renderingIntent}_${annotationStorageSerializable.hash}`,
|
||||
cacheKey: cacheKeyBuf.join("_"),
|
||||
annotationStorageSerializable,
|
||||
isEditing,
|
||||
modifiedIds,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -503,6 +503,14 @@ describe("ResetForm action", () => {
|
|||
it("must check that the Ink annotation has a popup", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
if (browserName) {
|
||||
// TODO
|
||||
pending(
|
||||
"Re-enable this test when the Ink annotation has been made editable."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("[data-annotation-id='25R']").hidden === false`
|
||||
);
|
||||
|
|
|
@ -45,6 +45,7 @@ import {
|
|||
scrollIntoView,
|
||||
switchToEditor,
|
||||
waitForAnnotationEditorLayer,
|
||||
waitForAnnotationModeChanged,
|
||||
waitForSelectedEditor,
|
||||
waitForSerialized,
|
||||
waitForStorageEntries,
|
||||
|
@ -987,6 +988,29 @@ describe("FreeText Editor", () => {
|
|||
pages.map(async ([browserName, page]) => {
|
||||
await switchToFreeText(page);
|
||||
|
||||
const isEditorWhite = editorRect =>
|
||||
page.evaluate(rect => {
|
||||
const canvas = document.querySelector(".canvasWrapper canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
rect ||= {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
};
|
||||
const { data } = ctx.getImageData(
|
||||
rect.x,
|
||||
rect.y,
|
||||
rect.width,
|
||||
rect.height
|
||||
);
|
||||
return data.every(x => x === 0xff);
|
||||
}, editorRect);
|
||||
|
||||
// The page has been re-rendered but with no freetext annotations.
|
||||
let isWhite = await isEditorWhite();
|
||||
expect(isWhite).withContext(`In ${browserName}`).toBeTrue();
|
||||
|
||||
let editorIds = await getEditors(page, "freeText");
|
||||
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
|
||||
|
||||
|
@ -1041,11 +1065,9 @@ describe("FreeText Editor", () => {
|
|||
// canvas.
|
||||
editorIds = await getEditors(page, "freeText");
|
||||
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
|
||||
const hidden = await page.$eval(
|
||||
"[data-annotation-id='26R'] canvas",
|
||||
el => getComputedStyle(el).display === "none"
|
||||
);
|
||||
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
|
||||
|
||||
isWhite = await isEditorWhite(editorRect);
|
||||
expect(isWhite).withContext(`In ${browserName}`).toBeTrue();
|
||||
|
||||
// Check we've now a div containing the text.
|
||||
const newDivText = await page.$eval(
|
||||
|
@ -1288,10 +1310,12 @@ describe("FreeText Editor", () => {
|
|||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must move an annotation", async () => {
|
||||
it("must edit an annotation", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const modeChangedHandle = await waitForAnnotationModeChanged(page);
|
||||
await page.click("[data-annotation-id='26R']", { count: 2 });
|
||||
await awaitPromise(modeChangedHandle);
|
||||
await page.waitForSelector(`${getEditorSelector(0)}-editor`);
|
||||
|
||||
const [focusedId, editable] = await page.evaluate(() => {
|
||||
|
@ -1347,6 +1371,7 @@ describe("FreeText Editor", () => {
|
|||
|
||||
// TODO: remove this when we switch to BiDi.
|
||||
await hover(page, "[data-annotation-id='23R']");
|
||||
|
||||
// Wait for the popup to be displayed.
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
|
@ -1588,12 +1613,6 @@ describe("FreeText Editor", () => {
|
|||
it("must open an existing annotation and check that the position are good", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await switchToFreeText(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("editorFreeTextParamsToolbar").remove();
|
||||
});
|
||||
|
||||
const toBinary = buf => {
|
||||
for (let i = 0; i < buf.length; i += 4) {
|
||||
const gray =
|
||||
|
@ -1646,8 +1665,12 @@ describe("FreeText Editor", () => {
|
|||
return null;
|
||||
};
|
||||
|
||||
for (const n of [0, 1, 2, 3, 4]) {
|
||||
const rect = await getRect(page, getEditorSelector(n));
|
||||
const firstPixelsAnnotations = new Map();
|
||||
|
||||
// [26, 32, ...] are the annotation ids
|
||||
for (const n of [26, 32, 42, 57, 35, 1]) {
|
||||
const id = `${n}R`;
|
||||
const rect = await getRect(page, `[data-annotation-id="${id}"]`);
|
||||
const editorPng = await page.screenshot({
|
||||
clip: rect,
|
||||
type: "png",
|
||||
|
@ -1658,33 +1681,33 @@ describe("FreeText Editor", () => {
|
|||
editorImage.width,
|
||||
editorImage.height
|
||||
);
|
||||
firstPixelsAnnotations.set(id, { editorFirstPix, rect });
|
||||
}
|
||||
|
||||
await switchToFreeText(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("editorFreeTextParamsToolbar").remove();
|
||||
});
|
||||
|
||||
for (const n of [0, 1, 2, 3, 4]) {
|
||||
const annotationId = await page.evaluate(N => {
|
||||
const editor = document.getElementById(
|
||||
`pdfjs_internal_editor_${N}`
|
||||
);
|
||||
const annId = editor.getAttribute("annotation-id");
|
||||
const annotation = document.querySelector(
|
||||
`[data-annotation-id="${annId}"]`
|
||||
);
|
||||
editor.hidden = true;
|
||||
annotation.hidden = false;
|
||||
return annId;
|
||||
return editor.getAttribute("annotation-id");
|
||||
}, n);
|
||||
await page.waitForSelector(`${getEditorSelector(n)}[hidden]`);
|
||||
await page.waitForSelector(
|
||||
`[data-annotation-id="${annotationId}"]:not([hidden])`
|
||||
);
|
||||
|
||||
const annotationPng = await page.screenshot({
|
||||
const { editorFirstPix: annotationFirstPix, rect } =
|
||||
firstPixelsAnnotations.get(annotationId);
|
||||
const editorPng = await page.screenshot({
|
||||
clip: rect,
|
||||
type: "png",
|
||||
});
|
||||
const annotationImage = PNG.sync.read(annotationPng);
|
||||
const annotationFirstPix = getFirstPixel(
|
||||
annotationImage.data,
|
||||
annotationImage.width,
|
||||
annotationImage.height
|
||||
const editorImage = PNG.sync.read(editorPng);
|
||||
const editorFirstPix = getFirstPixel(
|
||||
editorImage.data,
|
||||
editorImage.width,
|
||||
editorImage.height
|
||||
);
|
||||
|
||||
expect(
|
||||
|
@ -1719,12 +1742,6 @@ describe("FreeText Editor", () => {
|
|||
it("must open an existing rotated annotation and check that the position are good", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await switchToFreeText(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("editorFreeTextParamsToolbar").remove();
|
||||
});
|
||||
|
||||
const toBinary = buf => {
|
||||
for (let i = 0; i < buf.length; i += 4) {
|
||||
const gray =
|
||||
|
@ -1806,13 +1823,15 @@ describe("FreeText Editor", () => {
|
|||
return null;
|
||||
};
|
||||
|
||||
const firstPixelsAnnotations = new Map();
|
||||
for (const [n, start] of [
|
||||
[0, "BL"],
|
||||
[1, "BR"],
|
||||
[2, "TR"],
|
||||
[3, "TL"],
|
||||
[17, "BL"],
|
||||
[18, "BR"],
|
||||
[19, "TR"],
|
||||
[20, "TL"],
|
||||
]) {
|
||||
const rect = await getRect(page, getEditorSelector(n));
|
||||
const id = `${n}R`;
|
||||
const rect = await getRect(page, `[data-annotation-id="${id}"]`);
|
||||
const editorPng = await page.screenshot({
|
||||
clip: rect,
|
||||
type: "png",
|
||||
|
@ -1824,33 +1843,38 @@ describe("FreeText Editor", () => {
|
|||
editorImage.height,
|
||||
start
|
||||
);
|
||||
firstPixelsAnnotations.set(id, { editorFirstPix, rect });
|
||||
}
|
||||
|
||||
await switchToFreeText(page);
|
||||
|
||||
await page.evaluate(() => {
|
||||
document.getElementById("editorFreeTextParamsToolbar").remove();
|
||||
});
|
||||
|
||||
for (const [n, start] of [
|
||||
[0, "BL"],
|
||||
[1, "BR"],
|
||||
[2, "TR"],
|
||||
[3, "TL"],
|
||||
]) {
|
||||
const annotationId = await page.evaluate(N => {
|
||||
const editor = document.getElementById(
|
||||
`pdfjs_internal_editor_${N}`
|
||||
);
|
||||
const annId = editor.getAttribute("annotation-id");
|
||||
const annotation = document.querySelector(
|
||||
`[data-annotation-id="${annId}"]`
|
||||
);
|
||||
editor.hidden = true;
|
||||
annotation.hidden = false;
|
||||
return annId;
|
||||
return editor.getAttribute("annotation-id");
|
||||
}, n);
|
||||
await page.waitForSelector(`${getEditorSelector(n)}[hidden]`);
|
||||
await page.waitForSelector(
|
||||
`[data-annotation-id="${annotationId}"]:not([hidden])`
|
||||
);
|
||||
|
||||
const annotationPng = await page.screenshot({
|
||||
const { editorFirstPix: annotationFirstPix, rect } =
|
||||
firstPixelsAnnotations.get(annotationId);
|
||||
const editorPng = await page.screenshot({
|
||||
clip: rect,
|
||||
type: "png",
|
||||
});
|
||||
const annotationImage = PNG.sync.read(annotationPng);
|
||||
const annotationFirstPix = getFirstPixel(
|
||||
annotationImage.data,
|
||||
annotationImage.width,
|
||||
annotationImage.height,
|
||||
const editorImage = PNG.sync.read(editorPng);
|
||||
const editorFirstPix = getFirstPixel(
|
||||
editorImage.data,
|
||||
editorImage.width,
|
||||
editorImage.height,
|
||||
start
|
||||
);
|
||||
|
||||
|
@ -3552,13 +3576,6 @@ describe("FreeText Editor", () => {
|
|||
);
|
||||
}
|
||||
|
||||
await page.waitForSelector("[data-annotation-id='998R'] canvas");
|
||||
let hidden = await page.$eval(
|
||||
"[data-annotation-id='998R'] canvas",
|
||||
el => getComputedStyle(el).display === "none"
|
||||
);
|
||||
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
|
||||
|
||||
// Check we've now a div containing the text.
|
||||
await page.waitForSelector(
|
||||
"[data-annotation-id='998R'] div.annotationContent"
|
||||
|
@ -3571,6 +3588,24 @@ describe("FreeText Editor", () => {
|
|||
.withContext(`In ${browserName}`)
|
||||
.toEqual("Hello World and edited in Firefox");
|
||||
|
||||
// Check that the canvas has nothing drawn at the annotation position.
|
||||
await page.$eval(
|
||||
"[data-annotation-id='998R']",
|
||||
el => (el.hidden = true)
|
||||
);
|
||||
let editorPng = await page.screenshot({
|
||||
clip: editorRect,
|
||||
type: "png",
|
||||
});
|
||||
await page.$eval(
|
||||
"[data-annotation-id='998R']",
|
||||
el => (el.hidden = false)
|
||||
);
|
||||
let editorImage = PNG.sync.read(editorPng);
|
||||
expect(editorImage.data.every(x => x === 0xff))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toBeTrue();
|
||||
|
||||
const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2);
|
||||
for (const pageNumber of oneToThirteen) {
|
||||
await scrollIntoView(
|
||||
|
@ -3587,6 +3622,19 @@ describe("FreeText Editor", () => {
|
|||
await switchToFreeText(page, /* disable = */ true);
|
||||
|
||||
const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
|
||||
const handlePromise = await createPromise(page, resolve => {
|
||||
const callback = e => {
|
||||
if (e.source.id === 1) {
|
||||
window.PDFViewerApplication.eventBus.off(
|
||||
"pagerendered",
|
||||
callback
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
window.PDFViewerApplication.eventBus.on("pagerendered", callback);
|
||||
});
|
||||
|
||||
for (const pageNumber of thirteenToOne) {
|
||||
await scrollIntoView(
|
||||
page,
|
||||
|
@ -3594,12 +3642,16 @@ describe("FreeText Editor", () => {
|
|||
);
|
||||
}
|
||||
|
||||
await page.waitForSelector("[data-annotation-id='998R'] canvas");
|
||||
hidden = await page.$eval(
|
||||
"[data-annotation-id='998R'] canvas",
|
||||
el => getComputedStyle(el).display === "none"
|
||||
);
|
||||
expect(hidden).withContext(`In ${browserName}`).toBeFalse();
|
||||
await awaitPromise(handlePromise);
|
||||
|
||||
editorPng = await page.screenshot({
|
||||
clip: editorRect,
|
||||
type: "png",
|
||||
});
|
||||
editorImage = PNG.sync.read(editorPng);
|
||||
expect(editorImage.data.every(x => x === 0xff))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toBeFalse();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -564,14 +564,14 @@ describe("Stamp Editor", () => {
|
|||
for (let i = 0; i < pages1.length; i++) {
|
||||
const [, page1] = pages1[i];
|
||||
await page1.bringToFront();
|
||||
await page1.click("#editorStamp");
|
||||
await switchToStamp(page1);
|
||||
|
||||
await copyImage(page1, "../images/firefox_logo.png", 0);
|
||||
await copy(page1);
|
||||
|
||||
const [, page2] = pages2[i];
|
||||
await page2.bringToFront();
|
||||
await page2.click("#editorStamp");
|
||||
await switchToStamp(page2);
|
||||
|
||||
await paste(page2);
|
||||
|
||||
|
|
|
@ -447,11 +447,30 @@ function waitForAnnotationEditorLayer(page) {
|
|||
return createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"annotationeditorlayerrendered",
|
||||
resolve
|
||||
resolve,
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForAnnotationModeChanged(page) {
|
||||
return createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"annotationeditormodechanged",
|
||||
resolve,
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForPageRendered(page) {
|
||||
return createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on("pagerendered", resolve, {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function scrollIntoView(page, selector) {
|
||||
const handle = await page.evaluateHandle(
|
||||
sel => [
|
||||
|
@ -695,8 +714,10 @@ export {
|
|||
serializeBitmapDimensions,
|
||||
switchToEditor,
|
||||
waitForAnnotationEditorLayer,
|
||||
waitForAnnotationModeChanged,
|
||||
waitForEntryInStorage,
|
||||
waitForEvent,
|
||||
waitForPageRendered,
|
||||
waitForSandboxTrip,
|
||||
waitForSelectedEditor,
|
||||
waitForSerialized,
|
||||
|
|
|
@ -182,6 +182,10 @@ class AnnotationLayerBuilder {
|
|||
this.div.hidden = true;
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
|
||||
#updatePresentationModeState(state) {
|
||||
if (!this.div) {
|
||||
return;
|
||||
|
|
|
@ -119,6 +119,8 @@ class PDFPageView {
|
|||
|
||||
#hasRestrictedScaling = false;
|
||||
|
||||
#isEditing = false;
|
||||
|
||||
#layerProperties = null;
|
||||
|
||||
#loadingId = null;
|
||||
|
@ -354,6 +356,10 @@ class PDFPageView {
|
|||
this.pdfPage?.cleanup();
|
||||
}
|
||||
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
|
||||
get _textHighlighter() {
|
||||
return shadow(
|
||||
this,
|
||||
|
@ -582,6 +588,20 @@ class PDFPageView {
|
|||
}
|
||||
}
|
||||
|
||||
toggleEditingMode(isEditing) {
|
||||
if (!this.hasEditableAnnotations()) {
|
||||
return;
|
||||
}
|
||||
this.#isEditing = isEditing;
|
||||
this.reset({
|
||||
keepZoomLayer: true,
|
||||
keepAnnotationLayer: true,
|
||||
keepAnnotationEditorLayer: true,
|
||||
keepXfaLayer: true,
|
||||
keepTextLayer: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PDFPageViewUpdateParameters
|
||||
* @property {number} [scale] The new scale, if specified.
|
||||
|
@ -1037,6 +1057,7 @@ class PDFPageView {
|
|||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors,
|
||||
isEditing: this.#isEditing,
|
||||
};
|
||||
const renderTask = (this.renderTask = pdfPage.render(renderContext));
|
||||
renderTask.onContinue = renderContinueCallback;
|
||||
|
|
|
@ -223,6 +223,10 @@ class PDFViewer {
|
|||
|
||||
#mlManager = null;
|
||||
|
||||
#onPageRenderedCallback = null;
|
||||
|
||||
#switchAnnotationEditorModeTimeoutId = null;
|
||||
|
||||
#getAllTextInProgress = false;
|
||||
|
||||
#hiddenCopyElement = null;
|
||||
|
@ -1117,6 +1121,10 @@ class PDFViewer {
|
|||
|
||||
this.#hiddenCopyElement?.remove();
|
||||
this.#hiddenCopyElement = null;
|
||||
|
||||
this.#onPageRenderedCallback = null;
|
||||
clearTimeout(this.#switchAnnotationEditorModeTimeoutId);
|
||||
this.#switchAnnotationEditorModeTimeoutId = null;
|
||||
}
|
||||
|
||||
#ensurePageViewVisible() {
|
||||
|
@ -1653,6 +1661,32 @@ class PDFViewer {
|
|||
});
|
||||
}
|
||||
|
||||
#switchToEditAnnotationMode() {
|
||||
const visible = this._getVisiblePages();
|
||||
const pagesToRefresh = [];
|
||||
const { ids, views } = visible;
|
||||
for (const page of views) {
|
||||
const { view } = page;
|
||||
if (!view.hasEditableAnnotations()) {
|
||||
ids.delete(view.id);
|
||||
continue;
|
||||
}
|
||||
pagesToRefresh.push(page);
|
||||
}
|
||||
|
||||
if (pagesToRefresh.length === 0) {
|
||||
return null;
|
||||
}
|
||||
this.renderingQueue.renderHighestPriority({
|
||||
first: pagesToRefresh[0],
|
||||
last: pagesToRefresh.at(-1),
|
||||
views: pagesToRefresh,
|
||||
ids,
|
||||
});
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
containsElement(element) {
|
||||
return this.container.contains(element);
|
||||
}
|
||||
|
@ -2259,13 +2293,56 @@ class PDFViewer {
|
|||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
this.#annotationEditorMode = mode;
|
||||
this.eventBus.dispatch("annotationeditormodechanged", {
|
||||
source: this,
|
||||
mode,
|
||||
});
|
||||
|
||||
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
|
||||
const { eventBus } = this;
|
||||
const updater = () => {
|
||||
if (this.#onPageRenderedCallback) {
|
||||
eventBus._off("pagerendered", this.#onPageRenderedCallback);
|
||||
this.#onPageRenderedCallback = null;
|
||||
}
|
||||
if (this.#switchAnnotationEditorModeTimeoutId !== null) {
|
||||
clearTimeout(this.#switchAnnotationEditorModeTimeoutId);
|
||||
this.#switchAnnotationEditorModeTimeoutId = null;
|
||||
}
|
||||
this.#annotationEditorMode = mode;
|
||||
eventBus.dispatch("annotationeditormodechanged", {
|
||||
source: this,
|
||||
mode,
|
||||
});
|
||||
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
|
||||
};
|
||||
|
||||
if (
|
||||
mode === AnnotationEditorType.NONE ||
|
||||
this.#annotationEditorMode === AnnotationEditorType.NONE
|
||||
) {
|
||||
const isEditing = mode !== AnnotationEditorType.NONE;
|
||||
if (!isEditing) {
|
||||
this.pdfDocument.annotationStorage.resetModifiedIds();
|
||||
}
|
||||
for (const pageView of this._pages) {
|
||||
pageView.toggleEditingMode(isEditing);
|
||||
}
|
||||
// We must call #switchToEditAnnotationMode unconditionally to ensure that
|
||||
// page is rendered if it's useful or not.
|
||||
const idsToRefresh = this.#switchToEditAnnotationMode();
|
||||
if (isEditing && editId && idsToRefresh) {
|
||||
// We're editing an existing annotation so we must switch to editing
|
||||
// mode when the rendering is done.
|
||||
const { signal } = this.#eventAbortController;
|
||||
this.#onPageRenderedCallback = ({ pageNumber }) => {
|
||||
idsToRefresh.delete(pageNumber);
|
||||
if (idsToRefresh.size === 0) {
|
||||
eventBus._off("pagerendered", this.#onPageRenderedCallback);
|
||||
this.#onPageRenderedCallback = null;
|
||||
this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0);
|
||||
}
|
||||
};
|
||||
eventBus._on("pagerendered", this.#onPageRenderedCallback, { signal });
|
||||
return;
|
||||
}
|
||||
}
|
||||
updater();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line accessor-pairs
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue