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;
|
const actionName = actionType.name;
|
||||||
|
|
||||||
switch (actionName) {
|
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":
|
case "URI":
|
||||||
url = action.get("URI");
|
url = action.get("URI");
|
||||||
if (url instanceof Name) {
|
if (url instanceof Name) {
|
||||||
@ -1405,11 +1419,7 @@ class Catalog {
|
|||||||
}
|
}
|
||||||
/* falls through */
|
/* falls through */
|
||||||
default:
|
default:
|
||||||
if (
|
if (actionName === "JavaScript" || actionName === "SubmitForm") {
|
||||||
actionName === "JavaScript" ||
|
|
||||||
actionName === "ResetForm" ||
|
|
||||||
actionName === "SubmitForm"
|
|
||||||
) {
|
|
||||||
// Don't bother the user with a warning for actions that require
|
// Don't bother the user with a warning for actions that require
|
||||||
// scripting support, since those will be handled separately.
|
// scripting support, since those will be handled separately.
|
||||||
break;
|
break;
|
||||||
|
@ -429,6 +429,7 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
parameters.data.dest ||
|
parameters.data.dest ||
|
||||||
parameters.data.action ||
|
parameters.data.action ||
|
||||||
parameters.data.isTooltipOnly ||
|
parameters.data.isTooltipOnly ||
|
||||||
|
parameters.data.resetForm ||
|
||||||
(parameters.data.actions &&
|
(parameters.data.actions &&
|
||||||
(parameters.data.actions.Action ||
|
(parameters.data.actions.Action ||
|
||||||
parameters.data.actions["Mouse Up"] ||
|
parameters.data.actions["Mouse Up"] ||
|
||||||
@ -454,17 +455,25 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
this._bindNamedAction(link, data.action);
|
this._bindNamedAction(link, data.action);
|
||||||
} else if (data.dest) {
|
} else if (data.dest) {
|
||||||
this._bindLink(link, 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 {
|
} 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) {
|
if (this.quadrilaterals) {
|
||||||
@ -557,6 +566,106 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
}
|
}
|
||||||
link.className = "internalLink";
|
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 {
|
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 => {
|
let blurListener = event => {
|
||||||
if (elementData.formattedValue) {
|
if (elementData.formattedValue) {
|
||||||
event.target.value = elementData.formattedValue;
|
event.target.value = elementData.formattedValue;
|
||||||
@ -1057,6 +1172,11 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
|
|||||||
storage.setValue(id, { value: checked });
|
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) {
|
if (this.enableScripting && this.hasJSActions) {
|
||||||
element.addEventListener("updatefromsandbox", jsEvent => {
|
element.addEventListener("updatefromsandbox", jsEvent => {
|
||||||
const actions = {
|
const actions = {
|
||||||
@ -1129,6 +1249,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
|
|||||||
storage.setValue(id, { value: checked });
|
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) {
|
if (this.enableScripting && this.hasJSActions) {
|
||||||
const pdfButtonValue = data.buttonValue;
|
const pdfButtonValue = data.buttonValue;
|
||||||
element.addEventListener("updatefromsandbox", jsEvent => {
|
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.
|
// Insert the options into the choice field.
|
||||||
for (const option of this.data.options) {
|
for (const option of this.data.options) {
|
||||||
const optionElement = document.createElement("option");
|
const optionElement = document.createElement("option");
|
||||||
|
@ -79,6 +79,13 @@ class EventDispatcher {
|
|||||||
baseEvent.actions,
|
baseEvent.actions,
|
||||||
baseEvent.pageNumber
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -476,6 +476,10 @@ class Field extends PDFObject {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_reset() {
|
||||||
|
this.value = this.valueAsString = this.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
_runActions(event) {
|
_runActions(event) {
|
||||||
const eventName = event.name;
|
const eventName = event.name;
|
||||||
if (!this._actions.has(eventName)) {
|
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
|
!IdentityToUnicodeMap_charCodeOf.pdf
|
||||||
!PDFJS-9279-reduced.pdf
|
!PDFJS-9279-reduced.pdf
|
||||||
!issue5481.pdf
|
!issue5481.pdf
|
||||||
|
!resetform.pdf
|
||||||
!issue5567.pdf
|
!issue5567.pdf
|
||||||
!issue5701.pdf
|
!issue5701.pdf
|
||||||
!issue6769_no_matrix.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