AcroForm: Add support for ResetForm action
- it aims to fix #12721. - Thanks to PR #14023, we've now the fieldObjects in the annotation layer so we can easily map fields names on their id if needed. - Reset values in the storage, in the JS sandbox and in the visible html elements.
This commit is contained in:
parent
db7c91e7b1
commit
aecbd7cd89
@ -1328,6 +1328,20 @@ class Catalog {
|
||||
const actionName = actionType.name;
|
||||
|
||||
switch (actionName) {
|
||||
case "ResetForm":
|
||||
const flags = action.get("Flags");
|
||||
const include = ((isNum(flags) ? flags : 0) & 1) === 0;
|
||||
const fields = [];
|
||||
const refs = [];
|
||||
for (const obj of action.get("Fields") || []) {
|
||||
if (isRef(obj)) {
|
||||
refs.push(obj.toString());
|
||||
} else if (isString(obj)) {
|
||||
fields.push(stringToPDFString(obj));
|
||||
}
|
||||
}
|
||||
resultObj.resetForm = { fields, refs, include };
|
||||
break;
|
||||
case "URI":
|
||||
url = action.get("URI");
|
||||
if (url instanceof Name) {
|
||||
@ -1405,11 +1419,7 @@ class Catalog {
|
||||
}
|
||||
/* falls through */
|
||||
default:
|
||||
if (
|
||||
actionName === "JavaScript" ||
|
||||
actionName === "ResetForm" ||
|
||||
actionName === "SubmitForm"
|
||||
) {
|
||||
if (actionName === "JavaScript" || actionName === "SubmitForm") {
|
||||
// Don't bother the user with a warning for actions that require
|
||||
// scripting support, since those will be handled separately.
|
||||
break;
|
||||
|
@ -429,6 +429,7 @@ class LinkAnnotationElement extends AnnotationElement {
|
||||
parameters.data.dest ||
|
||||
parameters.data.action ||
|
||||
parameters.data.isTooltipOnly ||
|
||||
parameters.data.resetForm ||
|
||||
(parameters.data.actions &&
|
||||
(parameters.data.actions.Action ||
|
||||
parameters.data.actions["Mouse Up"] ||
|
||||
@ -454,17 +455,25 @@ class LinkAnnotationElement extends AnnotationElement {
|
||||
this._bindNamedAction(link, data.action);
|
||||
} else if (data.dest) {
|
||||
this._bindLink(link, data.dest);
|
||||
} else if (
|
||||
data.actions &&
|
||||
(data.actions.Action ||
|
||||
data.actions["Mouse Up"] ||
|
||||
data.actions["Mouse Down"]) &&
|
||||
this.enableScripting &&
|
||||
this.hasJSActions
|
||||
) {
|
||||
this._bindJSAction(link, data);
|
||||
} else {
|
||||
this._bindLink(link, "");
|
||||
let hasClickAction = false;
|
||||
if (
|
||||
data.actions &&
|
||||
(data.actions.Action ||
|
||||
data.actions["Mouse Up"] ||
|
||||
data.actions["Mouse Down"]) &&
|
||||
this.enableScripting &&
|
||||
this.hasJSActions
|
||||
) {
|
||||
hasClickAction = true;
|
||||
this._bindJSAction(link, data);
|
||||
}
|
||||
|
||||
if (data.resetForm) {
|
||||
this._bindResetFormAction(link, data.resetForm);
|
||||
} else if (!hasClickAction) {
|
||||
this._bindLink(link, "");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.quadrilaterals) {
|
||||
@ -557,6 +566,106 @@ class LinkAnnotationElement extends AnnotationElement {
|
||||
}
|
||||
link.className = "internalLink";
|
||||
}
|
||||
|
||||
_bindResetFormAction(link, resetForm) {
|
||||
const otherClickAction = link.onclick;
|
||||
if (!otherClickAction) {
|
||||
link.href = this.linkService.getAnchorUrl("");
|
||||
}
|
||||
link.className = "internalLink";
|
||||
|
||||
if (!this._fieldObjects) {
|
||||
warn(
|
||||
`_bindResetFormAction - "resetForm" action not supported, ` +
|
||||
"ensure that the `fieldObjects` parameter is provided."
|
||||
);
|
||||
if (!otherClickAction) {
|
||||
link.onclick = () => false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
link.onclick = () => {
|
||||
if (otherClickAction) {
|
||||
otherClickAction();
|
||||
}
|
||||
|
||||
const {
|
||||
fields: resetFormFields,
|
||||
refs: resetFormRefs,
|
||||
include,
|
||||
} = resetForm;
|
||||
|
||||
const allFields = [];
|
||||
if (resetFormFields.length !== 0 || resetFormRefs.length !== 0) {
|
||||
const fieldIds = new Set(resetFormRefs);
|
||||
for (const fieldName of resetFormFields) {
|
||||
const fields = this._fieldObjects[fieldName] || [];
|
||||
for (const { id } of fields) {
|
||||
fieldIds.add(id);
|
||||
}
|
||||
}
|
||||
for (const fields of Object.values(this._fieldObjects)) {
|
||||
for (const field of fields) {
|
||||
if (fieldIds.has(field.id) === include) {
|
||||
allFields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const fields of Object.values(this._fieldObjects)) {
|
||||
allFields.push(...fields);
|
||||
}
|
||||
}
|
||||
|
||||
const storage = this.annotationStorage;
|
||||
const allIds = [];
|
||||
for (const field of allFields) {
|
||||
const { id } = field;
|
||||
allIds.push(id);
|
||||
switch (field.type) {
|
||||
case "text": {
|
||||
const value = field.defaultValue || "";
|
||||
storage.setValue(id, { value, valueAsString: value });
|
||||
break;
|
||||
}
|
||||
case "checkbox":
|
||||
case "radiobutton": {
|
||||
const value = field.defaultValue === field.exportValues;
|
||||
storage.setValue(id, { value });
|
||||
break;
|
||||
}
|
||||
case "combobox":
|
||||
case "listbox": {
|
||||
const value = field.defaultValue || "";
|
||||
storage.setValue(id, { value });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
const domElement = document.getElementById(id);
|
||||
if (!domElement || !GetElementsByNameSet.has(domElement)) {
|
||||
continue;
|
||||
}
|
||||
domElement.dispatchEvent(new Event("resetform"));
|
||||
}
|
||||
|
||||
if (this.enableScripting) {
|
||||
// Update the values in the sandbox.
|
||||
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
|
||||
source: this,
|
||||
detail: {
|
||||
id: "app",
|
||||
ids: allIds,
|
||||
name: "ResetForm",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TextAnnotationElement extends AnnotationElement {
|
||||
@ -804,6 +913,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
);
|
||||
});
|
||||
|
||||
element.addEventListener("resetform", event => {
|
||||
const defaultValue = this.data.defaultFieldValue || "";
|
||||
element.value = elementData.userValue = defaultValue;
|
||||
delete elementData.formattedValue;
|
||||
});
|
||||
|
||||
let blurListener = event => {
|
||||
if (elementData.formattedValue) {
|
||||
event.target.value = elementData.formattedValue;
|
||||
@ -1057,6 +1172,11 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
storage.setValue(id, { value: checked });
|
||||
});
|
||||
|
||||
element.addEventListener("resetform", event => {
|
||||
const defaultValue = data.defaultFieldValue || "Off";
|
||||
event.target.checked = defaultValue === data.exportValue;
|
||||
});
|
||||
|
||||
if (this.enableScripting && this.hasJSActions) {
|
||||
element.addEventListener("updatefromsandbox", jsEvent => {
|
||||
const actions = {
|
||||
@ -1129,6 +1249,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
storage.setValue(id, { value: checked });
|
||||
});
|
||||
|
||||
element.addEventListener("resetform", event => {
|
||||
const defaultValue = data.defaultFieldValue;
|
||||
event.target.checked =
|
||||
defaultValue !== null &&
|
||||
defaultValue !== undefined &&
|
||||
defaultValue === data.buttonValue;
|
||||
});
|
||||
|
||||
if (this.enableScripting && this.hasJSActions) {
|
||||
const pdfButtonValue = data.buttonValue;
|
||||
element.addEventListener("updatefromsandbox", jsEvent => {
|
||||
@ -1231,6 +1359,13 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
|
||||
}
|
||||
}
|
||||
|
||||
selectElement.addEventListener("resetform", event => {
|
||||
const defaultValue = this.data.defaultFieldValue;
|
||||
for (const option of selectElement.options) {
|
||||
option.selected = option.value === defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Insert the options into the choice field.
|
||||
for (const option of this.data.options) {
|
||||
const optionElement = document.createElement("option");
|
||||
|
@ -79,6 +79,13 @@ class EventDispatcher {
|
||||
baseEvent.actions,
|
||||
baseEvent.pageNumber
|
||||
);
|
||||
} else if (id === "app" && baseEvent.name === "ResetForm") {
|
||||
for (const fieldId of baseEvent.ids) {
|
||||
const obj = this._objects[fieldId];
|
||||
if (obj) {
|
||||
obj.obj._reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -476,6 +476,10 @@ class Field extends PDFObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this.value = this.valueAsString = this.defaultValue;
|
||||
}
|
||||
|
||||
_runActions(event) {
|
||||
const eventName = event.name;
|
||||
if (!this._actions.has(eventName)) {
|
||||
|
@ -221,3 +221,133 @@ describe("Annotation and storage", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ResetForm action", () => {
|
||||
describe("resetform.pdf", () => {
|
||||
let pages;
|
||||
|
||||
beforeAll(async () => {
|
||||
pages = await loadAndWait("resetform.pdf", "[data-annotation-id='63R']");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must reset all fields", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const base = "hello world";
|
||||
for (let i = 3; i <= 7; i++) {
|
||||
await page.type(`#\\36 ${i}R`, base);
|
||||
}
|
||||
|
||||
const selectors = [69, 71, 75].map(
|
||||
n => `[data-annotation-id='${n}R']`
|
||||
);
|
||||
for (const selector of selectors) {
|
||||
await page.click(selector);
|
||||
}
|
||||
|
||||
await page.select("#\\37 8R", "b");
|
||||
await page.select("#\\38 1R", "f");
|
||||
|
||||
await page.click("[data-annotation-id='82R']");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("#\\\\36 3R").value === ""`
|
||||
);
|
||||
|
||||
for (let i = 3; i <= 8; i++) {
|
||||
const text = await page.$eval(`#\\36 ${i}R`, el => el.value);
|
||||
expect(text).withContext(`In ${browserName}`).toEqual("");
|
||||
}
|
||||
|
||||
const ids = [69, 71, 72, 73, 74, 75, 76, 77];
|
||||
for (const id of ids) {
|
||||
const checked = await page.$eval(
|
||||
`#\\3${Math.floor(id / 10)} ${id % 10}R`,
|
||||
el => el.checked
|
||||
);
|
||||
expect(checked).withContext(`In ${browserName}`).toEqual(false);
|
||||
}
|
||||
|
||||
let selected = await page.$eval(
|
||||
`#\\37 8R [value="a"]`,
|
||||
el => el.selected
|
||||
);
|
||||
expect(selected).withContext(`In ${browserName}`).toEqual(true);
|
||||
|
||||
selected = await page.$eval(
|
||||
`#\\38 1R [value="d"]`,
|
||||
el => el.selected
|
||||
);
|
||||
expect(selected).withContext(`In ${browserName}`).toEqual(true);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must reset some fields", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const base = "hello world";
|
||||
for (let i = 3; i <= 8; i++) {
|
||||
await page.type(`#\\36 ${i}R`, base);
|
||||
}
|
||||
|
||||
const selectors = [69, 71, 72, 73, 75].map(
|
||||
n => `[data-annotation-id='${n}R']`
|
||||
);
|
||||
for (const selector of selectors) {
|
||||
await page.click(selector);
|
||||
}
|
||||
|
||||
await page.select("#\\37 8R", "b");
|
||||
await page.select("#\\38 1R", "f");
|
||||
|
||||
await page.click("[data-annotation-id='84R']");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("#\\\\36 3R").value === ""`
|
||||
);
|
||||
|
||||
for (let i = 3; i <= 8; i++) {
|
||||
const expected = (i - 3) % 2 === 0 ? "" : base;
|
||||
const text = await page.$eval(`#\\36 ${i}R`, el => el.value);
|
||||
expect(text).withContext(`In ${browserName}`).toEqual(expected);
|
||||
}
|
||||
|
||||
let ids = [69, 72, 73, 74, 76, 77];
|
||||
for (const id of ids) {
|
||||
const checked = await page.$eval(
|
||||
`#\\3${Math.floor(id / 10)} ${id % 10}R`,
|
||||
el => el.checked
|
||||
);
|
||||
expect(checked)
|
||||
.withContext(`In ${browserName + id}`)
|
||||
.toEqual(false);
|
||||
}
|
||||
|
||||
ids = [71, 75];
|
||||
for (const id of ids) {
|
||||
const checked = await page.$eval(
|
||||
`#\\3${Math.floor(id / 10)} ${id % 10}R`,
|
||||
el => el.checked
|
||||
);
|
||||
expect(checked).withContext(`In ${browserName}`).toEqual(true);
|
||||
}
|
||||
|
||||
let selected = await page.$eval(
|
||||
`#\\37 8R [value="a"]`,
|
||||
el => el.selected
|
||||
);
|
||||
expect(selected).withContext(`In ${browserName}`).toEqual(true);
|
||||
|
||||
selected = await page.$eval(
|
||||
`#\\38 1R [value="f"]`,
|
||||
el => el.selected
|
||||
);
|
||||
expect(selected).withContext(`In ${browserName}`).toEqual(true);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -385,6 +385,7 @@
|
||||
!IdentityToUnicodeMap_charCodeOf.pdf
|
||||
!PDFJS-9279-reduced.pdf
|
||||
!issue5481.pdf
|
||||
!resetform.pdf
|
||||
!issue5567.pdf
|
||||
!issue5701.pdf
|
||||
!issue6769_no_matrix.pdf
|
||||
|
BIN
test/pdfs/resetform.pdf
Normal file
BIN
test/pdfs/resetform.pdf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user