Merge pull request #21664 from Snuffleupagus/getJSActions-api

[api-minor] Convert `getJSActions` to return data in a Map
This commit is contained in:
Jonas Jenwald 2026-07-31 11:29:21 +02:00 committed by GitHub
commit ce4ff55faa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 108 additions and 95 deletions

View File

@ -2887,10 +2887,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL);
// Check if we have a date or time.
const {
data: { actions },
} = this;
const { actions } = this.data;
if (!actions) {
return;
}
@ -2898,32 +2895,35 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const AFDateTime =
/^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/;
let canUseHTMLDateTime = false;
const aFormat = actions.get("Format"),
aKeystroke = actions.get("Keystroke");
if (
(actions.Format?.length === 1 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Format[0]) &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Format?.length === 0 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Keystroke?.length === 0 &&
actions.Format?.length === 1 &&
AFDateTime.test(actions.Format[0]))
(aFormat?.length === 1 &&
aKeystroke?.length === 1 &&
AFDateTime.test(aFormat[0]) &&
AFDateTime.test(aKeystroke[0])) ||
(aFormat?.length === 0 &&
aKeystroke?.length === 1 &&
AFDateTime.test(aKeystroke[0])) ||
(aKeystroke?.length === 0 &&
aFormat?.length === 1 &&
AFDateTime.test(aFormat[0]))
) {
// If the Format and Keystroke actions are the same, we can just use
// the Format action.
canUseHTMLDateTime = true;
}
const actionsToVisit = [];
if (actions.Format) {
actionsToVisit.push(...actions.Format);
if (aFormat) {
actionsToVisit.push(...aFormat);
}
if (actions.Keystroke) {
actionsToVisit.push(...actions.Keystroke);
if (aKeystroke) {
actionsToVisit.push(...aKeystroke);
}
if (canUseHTMLDateTime) {
delete actions.Keystroke;
actions.Format = actionsToVisit;
actions.delete("Keystroke");
actions.set("Format", actionsToVisit);
}
for (const formatAction of actionsToVisit) {

View File

@ -19,6 +19,7 @@ import {
DocumentActionEventType,
FormatError,
info,
makeArr,
PermissionFlag,
shadow,
stringToUTF8String,
@ -1287,10 +1288,10 @@ class Catalog {
);
if (javaScript) {
actions ??= Object.create(null);
actions ??= new Map();
for (const [key, val] of javaScript) {
(actions[key] ??= []).push(val);
actions.getOrInsertComputed(key, makeArr).push(val);
}
}
return shadow(this, "jsActions", actions);

View File

@ -449,7 +449,7 @@ function _collectJS(entry, xref, list, parents) {
}
function collectActions(xref, dict, eventType) {
const actions = Object.create(null);
const actions = new Map();
const additionalActionsDicts = getInheritableProperty({
dict,
key: "AA",
@ -475,7 +475,7 @@ function collectActions(xref, dict, eventType) {
const list = [];
_collectJS(rawActionDict, xref, list, parents);
if (list.length > 0) {
actions[action] = list;
actions.set(action, list);
}
}
}
@ -487,10 +487,10 @@ function collectActions(xref, dict, eventType) {
const list = [];
_collectJS(actionDict, xref, list, parents);
if (list.length > 0) {
actions.Action = list;
actions.set("Action", list);
}
}
return Object.keys(actions).length ? actions : null;
return actions.size ? actions : null;
}
const XMLEntities = {

View File

@ -1012,9 +1012,9 @@ class LinkAnnotationElement extends AnnotationElement {
} else {
if (
data.actions &&
(data.actions.Action ||
data.actions["Mouse Up"] ||
data.actions["Mouse Down"]) &&
(data.actions.has("Action") ||
data.actions.has("Mouse Up") ||
data.actions.has("Mouse Down")) &&
this.enableScripting &&
this.hasJSActions
) {
@ -1158,14 +1158,14 @@ class LinkAnnotationElement extends AnnotationElement {
* @param {Object} data
* @memberof LinkAnnotationElement
*/
_bindJSAction(link, data) {
_bindJSAction(link, { actions, id, overlaidText }) {
link.href = this.linkService.getAnchorUrl("");
const map = new Map([
["Action", "onclick"],
["Mouse Up", "onmouseup"],
["Mouse Down", "onmousedown"],
]);
for (const name of Object.keys(data.actions)) {
for (const name of actions.keys()) {
const jsName = map.get(name);
if (!jsName) {
continue;
@ -1173,16 +1173,13 @@ class LinkAnnotationElement extends AnnotationElement {
link[jsName] = () => {
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this,
detail: {
id: data.id,
name,
},
detail: { id, name },
});
return false;
};
}
if (data.overlaidText) {
link.title = data.overlaidText;
if (overlaidText) {
link.title = overlaidText;
}
link.onclick ||= () => false;
@ -1379,8 +1376,10 @@ class WidgetAnnotationElement extends AnnotationElement {
}
_setEventListeners(element, elementData, names, getter) {
const { actions } = this.data;
for (const [baseName, eventName] of names) {
if (eventName === "Action" || this.data.actions?.[eventName]) {
if (eventName === "Action" || actions?.has(eventName)) {
if (eventName === "Focus" || eventName === "Blur") {
elementData ||= { focused: false };
}
@ -1391,10 +1390,10 @@ class WidgetAnnotationElement extends AnnotationElement {
eventName,
getter
);
if (eventName === "Focus" && !this.data.actions?.Blur) {
if (eventName === "Focus" && !actions?.has("Blur")) {
// Ensure that elementData will have the correct value.
this._setEventListener(element, elementData, "blur", "Blur", null);
} else if (eventName === "Blur" && !this.data.actions?.Focus) {
} else if (eventName === "Blur" && !actions?.has("Focus")) {
this._setEventListener(element, elementData, "focus", "Focus", null);
}
}
@ -1630,7 +1629,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
}
elementData.lastCommittedValue = target.value;
elementData.commitKey = 1;
if (!this.data.actions?.Focus) {
if (!this.data.actions?.has("Focus")) {
elementData.focused = true;
}
});
@ -1752,7 +1751,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (!elementData.focused || !event.relatedTarget) {
return;
}
if (!this.data.actions?.Blur) {
if (!this.data.actions?.has("Blur")) {
elementData.focused = false;
}
const { target } = event;
@ -1800,7 +1799,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
_blurListener(event);
});
if (this.data.actions?.Keystroke) {
if (this.data.actions?.has("Keystroke")) {
element.addEventListener("beforeinput", event => {
elementData.lastCommittedValue = null;
const { data, target } = event;

View File

@ -867,8 +867,8 @@ class PDFDocumentProxy {
}
/**
* @returns {Promise<Object | null>} A promise that is resolved with
* an {Object} with the JavaScript actions:
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with
* the JavaScript actions:
* - from the name tree.
* - from A or AA entries in the catalog dictionary.
* , or `null` if no JavaScript exists.
@ -1429,8 +1429,8 @@ class PDFPageProxy {
}
/**
* @returns {Promise<Object>} A promise that is resolved with an
* {Object} with JS actions.
* @returns {Promise<Map | null>} A promise that is resolved with a {Map} with
* the JavaScript actions, or `null` if no JavaScript exists.
*/
getJSActions() {
return this._transport.getPageJSActions(this._pageIndex);

View File

@ -19,7 +19,9 @@ class SandboxSupport extends SandboxSupportBase {
exportValueToSandbox(val) {
// The communication with the Quickjs sandbox is based on strings
// So we use JSON.stringfy to serialize
return JSON.stringify(val);
return JSON.stringify(val, (k, v) =>
v instanceof Map ? Object.fromEntries(v) : v
);
}
importValueFromSandbox(val) {
@ -65,7 +67,7 @@ class Sandbox {
let success = false;
let buf = 0;
try {
const sandboxData = JSON.stringify(data);
const sandboxData = this.support.exportValueToSandbox(data);
// "pdfjsScripting.initSandbox..." MUST be the last line to be evaluated
// since the returned value is used for the communication.
code.push(`pdfjsScripting.initSandbox({ data: ${sandboxData} })`);

View File

@ -22,7 +22,9 @@ const FieldType = {
};
function createActionsMap(actions) {
return new Map(actions ? Object.entries(actions) : null);
return actions instanceof Map
? actions
: new Map(actions ? Object.entries(actions) : null);
}
function getFieldType(actions) {
@ -30,10 +32,8 @@ function getFieldType(actions) {
if (!format) {
return FieldType.none;
}
format = format[0].trim();
format = format[0];
format = format.trim();
if (format.startsWith("AFNumber_")) {
return FieldType.number;
}

View File

@ -173,9 +173,9 @@ class Doc extends PDFObject {
_dispatchPageEvent(name, actions, pageNumber) {
if (name === "PageOpen") {
this.#pageActions ??= new Map();
if (!this.#pageActions.has(pageNumber)) {
this.#pageActions.set(pageNumber, createActionsMap(actions));
}
this.#pageActions.getOrInsertComputed(pageNumber, () =>
createActionsMap(actions)
);
this._pageNum = pageNumber - 1;
}

View File

@ -2460,17 +2460,14 @@ describe("annotation", function () {
annotationGlobalsMock,
idFactoryMock
);
const fieldObject = await annotation.getFieldObject();
const actions = fieldObject.actions;
expect(actions["Mouse Enter"]).toEqual(["hello()"]);
expect(actions["Mouse Exit"]).toEqual([
"world()",
"olleh()",
"foo()",
"dlrow()",
"oof()",
]);
expect(actions["Mouse Down"]).toEqual(["bar()"]);
const { actions } = await annotation.getFieldObject();
expect(actions).toEqual(
new Map([
["Mouse Enter", ["hello()"]],
["Mouse Exit", ["world()", "olleh()", "foo()", "dlrow()", "oof()"]],
["Mouse Down", ["bar()"]],
])
);
});
it("should save Japanese text", async function () {
@ -3728,7 +3725,7 @@ describe("annotation", function () {
);
expect(data.annotationType).toEqual(AnnotationType.WIDGET);
expect(data.pushButton).toBeTrue();
expect(data.actions.Action).toEqual(["do_something();"]);
expect(data.actions.get("Action")).toEqual(["do_something();"]);
});
it("should handle push buttons that act as a tooltip only", async function () {

View File

@ -986,9 +986,9 @@ describe("api", function () {
expect(pdfDocument.numPages).toEqual(1);
const jsActions = await pdfDocument.getJSActions();
expect(jsActions).toEqual({
OpenAction: ["func=function(){app.alert(1)};func();"],
});
expect(jsActions).toEqual(
new Map([["OpenAction", ["func=function(){app.alert(1)};func();"]]])
);
const page = await pdfDocument.getPage(1);
expect(page).toBeInstanceOf(PDFPageProxy);
@ -1943,12 +1943,17 @@ describe("api", function () {
// PDF document with "JavaScript" action in the OpenAction dictionary.
const loadingTask = getDocument(buildGetDocumentParams("issue6106.pdf"));
const pdfDoc = await loadingTask.promise;
const { OpenAction } = await pdfDoc.getJSActions();
const jsActions = await pdfDoc.getJSActions();
expect(OpenAction).toEqual([
"this.print({bUI:true,bSilent:false,bShrinkToFit:true});",
]);
expect(OpenAction[0]).toMatch(AutoPrintRegExp);
expect(jsActions).toEqual(
new Map([
[
"OpenAction",
["this.print({bUI:true,bSilent:false,bShrinkToFit:true});"],
],
])
);
expect(jsActions.get("OpenAction")[0]).toMatch(AutoPrintRegExp);
await loadingTask.destroy();
});
@ -1988,21 +1993,27 @@ describe("api", function () {
const page3 = await pdfDoc.getPage(3);
const page3Actions = await page3.getJSActions();
expect(docActions).toEqual({
DidPrint: [`this.getField("Text2").value = "DidPrint";`],
DidSave: [`this.getField("Text2").value = "DidSave";`],
WillClose: [`this.getField("Text1").value = "WillClose";`],
WillPrint: [`this.getField("Text1").value = "WillPrint";`],
WillSave: [`this.getField("Text1").value = "WillSave";`],
});
expect(page1Actions).toEqual({
PageOpen: [`this.getField("Text1").value = "PageOpen 1";`],
PageClose: [`this.getField("Text2").value = "PageClose 1";`],
});
expect(page3Actions).toEqual({
PageOpen: [`this.getField("Text5").value = "PageOpen 3";`],
PageClose: [`this.getField("Text6").value = "PageClose 3";`],
});
expect(docActions).toEqual(
new Map([
["DidPrint", [`this.getField("Text2").value = "DidPrint";`]],
["DidSave", [`this.getField("Text2").value = "DidSave";`]],
["WillClose", [`this.getField("Text1").value = "WillClose";`]],
["WillPrint", [`this.getField("Text1").value = "WillPrint";`]],
["WillSave", [`this.getField("Text1").value = "WillSave";`]],
])
);
expect(page1Actions).toEqual(
new Map([
["PageOpen", [`this.getField("Text1").value = "PageOpen 1";`]],
["PageClose", [`this.getField("Text2").value = "PageClose 1";`]],
])
);
expect(page3Actions).toEqual(
new Map([
["PageOpen", [`this.getField("Text5").value = "PageOpen 3";`]],
["PageClose", [`this.getField("Text6").value = "PageClose 3";`]],
])
);
await loadingTask.destroy();
});
@ -2051,11 +2062,14 @@ describe("api", function () {
name: "Button1",
rect: [455.436, 719.678, 527.436, 739.678],
hidden: false,
actions: {
Action: [
`this.getField("Text1").value = this.info.authors.join("::");`,
actions: new Map([
[
"Action",
[
`this.getField("Text1").value = this.info.authors.join("::");`,
],
],
},
]),
page: 0,
strokeColor: null,
fillColor: new Uint8ClampedArray([192, 192, 192]),