JS -- Plug PageOpen and PageClose actions

This commit is contained in:
Calixte Denizet 2021-01-05 15:53:30 +01:00
parent ed3758f84d
commit 6523f8880b
5 changed files with 172 additions and 16 deletions

View File

@ -1136,9 +1136,8 @@ class PDFPageProxy {
} }
/** /**
* @param {GetAnnotationsParameters} params - Annotation parameters. * @returns {Promise<Object>} A promise that is resolved with an
* @returns {Promise<Array<any>>} A promise that is resolved with an * {Object} with JS actions.
* {Array} of the annotation objects.
*/ */
getJSActions() { getJSActions() {
if (!this._jsActionsPromise) { if (!this._jsActionsPromise) {

View File

@ -91,6 +91,7 @@ class Doc extends PDFObject {
this._zoom = data.zoom || 100; this._zoom = data.zoom || 100;
this._actions = createActionsMap(data.actions); this._actions = createActionsMap(data.actions);
this._globalEval = data.globalEval; this._globalEval = data.globalEval;
this._pageActions = new Map();
} }
_dispatchDocEvent(name) { _dispatchDocEvent(name) {
@ -114,22 +115,28 @@ class Doc extends PDFObject {
} }
} }
_dispatchPageEvent(name, action, pageNumber) { _dispatchPageEvent(name, actions, pageNumber) {
if (name === "PageOpen") { if (name === "PageOpen") {
if (!this._pageActions.has(pageNumber)) {
this._pageActions.set(pageNumber, createActionsMap(actions));
}
this._pageNum = pageNumber - 1; this._pageNum = pageNumber - 1;
} }
this._globalEval(action); actions = this._pageActions.get(pageNumber)?.get(name);
if (actions) {
for (const action of actions) {
this._globalEval(action);
}
}
} }
_runActions(name) { _runActions(name) {
if (!this._actions.has(name)) {
return;
}
const actions = this._actions.get(name); const actions = this._actions.get(name);
for (const action of actions) { if (actions) {
this._globalEval(action); for (const action of actions) {
this._globalEval(action);
}
} }
} }

View File

@ -73,11 +73,10 @@ class EventDispatcher {
} }
if (id === "doc") { if (id === "doc") {
this._document.obj._dispatchDocEvent(event.name); this._document.obj._dispatchDocEvent(event.name);
} } else if (id === "page") {
if (id === "page") {
this._document.obj._dispatchPageEvent( this._document.obj._dispatchPageEvent(
event.name, event.name,
baseEvent.action, baseEvent.actions,
baseEvent.pageNumber baseEvent.pageNumber
); );
} }

View File

@ -16,8 +16,10 @@
const { clearInput, closePages, loadAndWait } = require("./test_utils.js"); const { clearInput, closePages, loadAndWait } = require("./test_utils.js");
describe("Interaction", () => { describe("Interaction", () => {
async function actAndWaitForInput(page, selector, action) { async function actAndWaitForInput(page, selector, action, clear = true) {
await clearInput(page, selector); if (clear) {
await clearInput(page, selector);
}
await action(); await action();
await page.waitForFunction( await page.waitForFunction(
`document.querySelector("${selector.replace("\\", "\\\\")}").value !== ""` `document.querySelector("${selector.replace("\\", "\\\\")}").value !== ""`
@ -307,6 +309,18 @@ describe("Interaction", () => {
if (process.platform === "win32" && browserName === "firefox") { if (process.platform === "win32" && browserName === "firefox") {
pending("Disabled in Firefox on Windows, because of bug 1662471."); pending("Disabled in Firefox on Windows, because of bug 1662471.");
} }
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
await clearInput(page, "#\\34 7R");
await page.evaluate(_ => {
window.document.activeElement.blur();
});
await page.waitForFunction(
`document.querySelector("#\\\\34 7R").value === ""`
);
let text = await actAndWaitForInput(page, "#\\34 7R", async () => { let text = await actAndWaitForInput(page, "#\\34 7R", async () => {
await page.click("#print"); await page.click("#print");
}); });
@ -337,6 +351,10 @@ describe("Interaction", () => {
it("must execute WillSave and DidSave actions", async () => { it("must execute WillSave and DidSave actions", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
try { try {
// Disable download in chrome // Disable download in chrome
// (it leads to an error in firefox so the try...) // (it leads to an error in firefox so the try...)
@ -345,6 +363,13 @@ describe("Interaction", () => {
}); });
} catch (_) {} } catch (_) {}
await clearInput(page, "#\\34 7R"); await clearInput(page, "#\\34 7R");
await page.evaluate(_ => {
window.document.activeElement.blur();
});
await page.waitForFunction(
`document.querySelector("#\\\\34 7R").value === ""`
);
let text = await actAndWaitForInput(page, "#\\34 7R", async () => { let text = await actAndWaitForInput(page, "#\\34 7R", async () => {
await page.click("#download"); await page.click("#download");
}); });
@ -360,4 +385,70 @@ describe("Interaction", () => {
); );
}); });
}); });
describe("in doc_actions.pdf for page actions", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("doc_actions.pdf", "#\\34 7R");
});
afterAll(async () => {
await closePages(pages);
});
it("must execute PageOpen and PageClose actions", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
let text = await page.$eval("#\\34 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("PageOpen 1");
for (let run = 0; run < 5; run++) {
for (const ref of [18, 19, 20, 21, 47, 50]) {
await page.evaluate(refElem => {
const element = window.document.getElementById(`${refElem}R`);
if (element) {
element.value = "";
}
}, ref);
}
for (const [refOpen, refClose, pageNumOpen, pageNumClose] of [
[18, 50, 2, 1],
[21, 19, 3, 2],
[47, 20, 1, 3],
]) {
text = await actAndWaitForInput(
page,
`#\\3${Math.floor(refOpen / 10)} ${refOpen % 10}R`,
async () => {
await page.evaluate(refElem => {
window.document
.getElementById(`${refElem}R`)
.scrollIntoView();
}, refOpen);
},
false
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`PageOpen ${pageNumOpen}`);
text = await page.$eval(
`#\\3${Math.floor(refClose / 10)} ${refClose % 10}R`,
el => el.value
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(`PageClose ${pageNumClose}`);
}
}
})
);
});
});
}); });

View File

@ -1542,6 +1542,64 @@ const PDFViewerApplication = {
}; };
internalEvents.set("updatefromsandbox", updateFromSandbox); internalEvents.set("updatefromsandbox", updateFromSandbox);
const visitedPages = new Map();
const pageOpen = ({ pageNumber }) => {
visitedPages.set(
pageNumber,
(async () => {
// Avoid sending, and thus serializing, the `actions` data
// when the same page is open several times.
let actions = null;
if (!visitedPages.has(pageNumber)) {
// visitedPages doesn't contain pageNumber: first visit.
const pageView = this.pdfViewer.getPageView(
/* index = */ pageNumber - 1
);
if (pageView?.pdfPage) {
actions = await pageView.pdfPage.getJSActions();
} else {
actions = await pdfDocument.getPage(pageNumber).getJSActions();
}
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the actions resolved.
}
}
this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "page",
name: "PageOpen",
pageNumber,
actions,
});
})()
);
};
const pageClose = async ({ pageNumber }) => {
const promise = visitedPages.get(pageNumber);
if (!promise) {
return;
}
visitedPages.set(pageNumber, null);
// Wait for PageOpen has been sent.
await promise;
if (pdfDocument !== this.pdfDocument) {
return; // The document was closed while the actions resolved.
}
this._scriptingInstance?.scripting.dispatchEventInSandbox({
id: "page",
name: "PageClose",
pageNumber,
});
};
internalEvents.set("pageopen", pageOpen);
internalEvents.set("pageclose", pageClose);
const dispatchEventInSandbox = ({ detail }) => { const dispatchEventInSandbox = ({ detail }) => {
scripting.dispatchEventInSandbox(detail); scripting.dispatchEventInSandbox(detail);
}; };
@ -1613,6 +1671,8 @@ const PDFViewerApplication = {
name: "Open", name: "Open",
}); });
await pageOpen({ pageNumber: this.pdfViewer.currentPageNumber });
// Used together with the integration-tests, see the `scriptingReady` // Used together with the integration-tests, see the `scriptingReady`
// getter, to enable awaiting full initialization of the scripting/sandbox. // getter, to enable awaiting full initialization of the scripting/sandbox.
// (Defer this slightly, to make absolutely sure that everything is done.) // (Defer this slightly, to make absolutely sure that everything is done.)