Skip to content

Commit

Permalink
Merge pull request #14083 from calixteman/reset
Browse files Browse the repository at this point in the history
AcroForm: Add support for ResetForm action
  • Loading branch information
calixteman authored Sep 30, 2021
2 parents db7c91e + aecbd7c commit 09361a4
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 15 deletions.
20 changes: 15 additions & 5 deletions src/core/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
155 changes: 145 additions & 10 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"] ||
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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");
Expand Down
7 changes: 7 additions & 0 deletions src/scripting_api/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/scripting_api/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
130 changes: 130 additions & 0 deletions test/integration/annotation_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})
);
});
});
});
Loading

0 comments on commit 09361a4

Please sign in to comment.