[JS] Fix few bugs present in the pdf for issue #14862

- since resetForm function reset a field value a calculateNow is consequently triggered.
  But the calculate callback can itself call resetForm, hence an infinite recursive loop.
  So basically, prevent calculeNow to be triggered by itself.
- in Firefox, the letters entered in some fields were duplicated: "AaBb" instead of "AB".
  It was mainly because beforeInput was triggering a Keystroke which was itself triggering
  an input value update and then the input event was triggered.
  So in order to avoid that, beforeInput calls preventDefault and then it's up to the JS to
  handle the event.
- fields have a property valueAsString which returns the value as a string. In the
  implementation it was wrongly used to store the formatted value of a field (2€ when the user
  entered 2). So this patch implements correctly valueAsString.
- non-rendered fields can be updated in using JS but when they're, they must take some properties
  in the annotationStorage. It was implemented for field values, but it wasn't for
  display, colors, ...
- it fixes #14862 and #14705.
This commit is contained in:
Calixte Denizet 2022-05-02 19:28:00 +02:00
parent 85b7e60425
commit 094ff38da0
14 changed files with 558 additions and 205 deletions

View File

@ -1518,7 +1518,8 @@ class WidgetAnnotation extends Annotation {
const storageEntry = annotationStorage const storageEntry = annotationStorage
? annotationStorage.get(this.data.id) ? annotationStorage.get(this.data.id)
: undefined; : undefined;
let value = storageEntry && storageEntry.value; let value =
storageEntry && (storageEntry.formattedValue || storageEntry.value);
if (value === undefined) { if (value === undefined) {
if (!this._hasValueFromXFA || this.appearance) { if (!this._hasValueFromXFA || this.appearance) {
// The annotation hasn't been rendered so use the appearance. // The annotation hasn't been rendered so use the appearance.
@ -1981,7 +1982,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
return { return {
id: this.data.id, id: this.data.id,
value: this.data.fieldValue, value: this.data.fieldValue,
defaultValue: this.data.defaultFieldValue, defaultValue: this.data.defaultFieldValue || "",
multiline: this.data.multiLine, multiline: this.data.multiLine,
password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD), password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD),
charLimit: this.data.maxLen, charLimit: this.data.maxLen,

View File

@ -297,6 +297,110 @@ class AnnotationElement {
return container; return container;
} }
get _commonActions() {
const setColor = (jsName, styleName, event) => {
const color = event.detail[jsName];
event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`](
color.slice(1)
);
};
return shadow(this, "_commonActions", {
display: event => {
const hidden = event.detail.display % 2 === 1;
event.target.style.visibility = hidden ? "hidden" : "visible";
this.annotationStorage.setValue(this.data.id, {
hidden,
print: event.detail.display === 0 || event.detail.display === 3,
});
},
print: event => {
this.annotationStorage.setValue(this.data.id, {
print: event.detail.print,
});
},
hidden: event => {
event.target.style.visibility = event.detail.hidden
? "hidden"
: "visible";
this.annotationStorage.setValue(this.data.id, {
hidden: event.detail.hidden,
});
},
focus: event => {
setTimeout(() => event.target.focus({ preventScroll: false }), 0);
},
userName: event => {
// tooltip
event.target.title = event.detail.userName;
},
readonly: event => {
if (event.detail.readonly) {
event.target.setAttribute("readonly", "");
} else {
event.target.removeAttribute("readonly");
}
},
required: event => {
if (event.detail.required) {
event.target.setAttribute("required", "");
} else {
event.target.removeAttribute("required");
}
},
bgColor: event => {
setColor("bgColor", "backgroundColor", event);
},
fillColor: event => {
setColor("fillColor", "backgroundColor", event);
},
fgColor: event => {
setColor("fgColor", "color", event);
},
textColor: event => {
setColor("textColor", "color", event);
},
borderColor: event => {
setColor("borderColor", "borderColor", event);
},
strokeColor: event => {
setColor("strokeColor", "borderColor", event);
},
});
}
_dispatchEventFromSandbox(actions, jsEvent) {
const commonActions = this._commonActions;
for (const name of Object.keys(jsEvent.detail)) {
const action = actions[name] || commonActions[name];
if (action) {
action(jsEvent);
}
}
}
_setDefaultPropertiesFromJS(element) {
if (!this.enableScripting) {
return;
}
// Some properties may have been updated thanks to JS.
const storedData = this.annotationStorage.getRawValue(this.data.id);
if (!storedData) {
return;
}
const commonActions = this._commonActions;
for (const [actionName, detail] of Object.entries(storedData)) {
const action = commonActions[actionName];
if (action) {
action({ detail, target: element });
// The action has been consumed: no need to keep it.
delete storedData[actionName];
}
}
}
/** /**
* Create quadrilaterals from the annotation's quadpoints. * Create quadrilaterals from the annotation's quadpoints.
* *
@ -657,7 +761,7 @@ class LinkAnnotationElement extends AnnotationElement {
switch (field.type) { switch (field.type) {
case "text": { case "text": {
const value = field.defaultValue || ""; const value = field.defaultValue || "";
storage.setValue(id, { value, valueAsString: value }); storage.setValue(id, { value });
break; break;
} }
case "checkbox": case "checkbox":
@ -794,85 +898,6 @@ class WidgetAnnotationElement extends AnnotationElement {
? "transparent" ? "transparent"
: Util.makeHexColor(color[0], color[1], color[2]); : Util.makeHexColor(color[0], color[1], color[2]);
} }
_dispatchEventFromSandbox(actions, jsEvent) {
const setColor = (jsName, styleName, event) => {
const color = event.detail[jsName];
event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`](
color.slice(1)
);
};
const commonActions = {
display: event => {
const hidden = event.detail.display % 2 === 1;
event.target.style.visibility = hidden ? "hidden" : "visible";
this.annotationStorage.setValue(this.data.id, {
hidden,
print: event.detail.display === 0 || event.detail.display === 3,
});
},
print: event => {
this.annotationStorage.setValue(this.data.id, {
print: event.detail.print,
});
},
hidden: event => {
event.target.style.visibility = event.detail.hidden
? "hidden"
: "visible";
this.annotationStorage.setValue(this.data.id, {
hidden: event.detail.hidden,
});
},
focus: event => {
setTimeout(() => event.target.focus({ preventScroll: false }), 0);
},
userName: event => {
// tooltip
event.target.title = event.detail.userName;
},
readonly: event => {
if (event.detail.readonly) {
event.target.setAttribute("readonly", "");
} else {
event.target.removeAttribute("readonly");
}
},
required: event => {
if (event.detail.required) {
event.target.setAttribute("required", "");
} else {
event.target.removeAttribute("required");
}
},
bgColor: event => {
setColor("bgColor", "backgroundColor", event);
},
fillColor: event => {
setColor("fillColor", "backgroundColor", event);
},
fgColor: event => {
setColor("fgColor", "color", event);
},
textColor: event => {
setColor("textColor", "color", event);
},
borderColor: event => {
setColor("borderColor", "borderColor", event);
},
strokeColor: event => {
setColor("strokeColor", "borderColor", event);
},
};
for (const name of Object.keys(jsEvent.detail)) {
const action = actions[name] || commonActions[name];
if (action) {
action(jsEvent);
}
}
}
} }
class TextWidgetAnnotationElement extends WidgetAnnotationElement { class TextWidgetAnnotationElement extends WidgetAnnotationElement {
@ -909,12 +934,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
// from parsing the elements correctly for the reference tests. // from parsing the elements correctly for the reference tests.
const storedData = storage.getValue(id, { const storedData = storage.getValue(id, {
value: this.data.fieldValue, value: this.data.fieldValue,
valueAsString: this.data.fieldValue,
}); });
const textContent = storedData.valueAsString || storedData.value || ""; const textContent = storedData.formattedValue || storedData.value || "";
const elementData = { const elementData = {
userValue: null, userValue: null,
formattedValue: null, formattedValue: null,
valueOnFocus: "",
}; };
if (this.data.multiLine) { if (this.data.multiLine) {
@ -944,14 +969,15 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
}); });
element.addEventListener("resetform", event => { element.addEventListener("resetform", event => {
const defaultValue = this.data.defaultFieldValue || ""; const defaultValue = this.data.defaultFieldValue ?? "";
element.value = elementData.userValue = defaultValue; element.value = elementData.userValue = defaultValue;
delete elementData.formattedValue; elementData.formattedValue = null;
}); });
let blurListener = event => { let blurListener = event => {
if (elementData.formattedValue) { const { formattedValue } = elementData;
event.target.value = elementData.formattedValue; if (formattedValue !== null && formattedValue !== undefined) {
event.target.value = formattedValue;
} }
// Reset the cursor position to the start of the field (issue 12359). // Reset the cursor position to the start of the field (issue 12359).
event.target.scrollLeft = 0; event.target.scrollLeft = 0;
@ -962,32 +988,33 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (elementData.userValue) { if (elementData.userValue) {
event.target.value = elementData.userValue; event.target.value = elementData.userValue;
} }
elementData.valueOnFocus = event.target.value;
}); });
element.addEventListener("updatefromsandbox", jsEvent => { element.addEventListener("updatefromsandbox", jsEvent => {
const actions = { const actions = {
value(event) { value(event) {
elementData.userValue = event.detail.value || ""; elementData.userValue = event.detail.value ?? "";
storage.setValue(id, { value: elementData.userValue.toString() }); storage.setValue(id, { value: elementData.userValue.toString() });
if (!elementData.formattedValue) {
event.target.value = elementData.userValue; event.target.value = elementData.userValue;
}
}, },
valueAsString(event) { formattedValue(event) {
elementData.formattedValue = event.detail.valueAsString || ""; const { formattedValue } = event.detail;
if (event.target !== document.activeElement) { elementData.formattedValue = formattedValue;
if (
formattedValue !== null &&
formattedValue !== undefined &&
event.target !== document.activeElement
) {
// Input hasn't the focus so display formatted string // Input hasn't the focus so display formatted string
event.target.value = elementData.formattedValue; event.target.value = formattedValue;
} }
storage.setValue(id, { storage.setValue(id, {
formattedValue: elementData.formattedValue, formattedValue,
}); });
}, },
selRange(event) { selRange(event) {
const [selStart, selEnd] = event.detail.selRange; event.target.setSelectionRange(...event.detail.selRange);
if (selStart >= 0 && selEnd < event.target.value.length) {
event.target.setSelectionRange(selStart, selEnd);
}
}, },
}; };
this._dispatchEventFromSandbox(actions, jsEvent); this._dispatchEventFromSandbox(actions, jsEvent);
@ -1009,14 +1036,18 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (commitKey === -1) { if (commitKey === -1) {
return; return;
} }
const { value } = event.target;
if (elementData.valueOnFocus === value) {
return;
}
// Save the entered value // Save the entered value
elementData.userValue = event.target.value; elementData.userValue = value;
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this, source: this,
detail: { detail: {
id, id,
name: "Keystroke", name: "Keystroke",
value: event.target.value, value,
willCommit: true, willCommit: true,
commitKey, commitKey,
selStart: event.target.selectionStart, selStart: event.target.selectionStart,
@ -1027,15 +1058,16 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
const _blurListener = blurListener; const _blurListener = blurListener;
blurListener = null; blurListener = null;
element.addEventListener("blur", event => { element.addEventListener("blur", event => {
elementData.userValue = event.target.value; const { value } = event.target;
if (this._mouseState.isDown) { elementData.userValue = value;
if (this._mouseState.isDown && elementData.valueOnFocus !== value) {
// Focus out using the mouse: data are committed // Focus out using the mouse: data are committed
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this, source: this,
detail: { detail: {
id, id,
name: "Keystroke", name: "Keystroke",
value: event.target.value, value,
willCommit: true, willCommit: true,
commitKey: 1, commitKey: 1,
selStart: event.target.selectionStart, selStart: event.target.selectionStart,
@ -1048,19 +1080,56 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (this.data.actions?.Keystroke) { if (this.data.actions?.Keystroke) {
element.addEventListener("beforeinput", event => { element.addEventListener("beforeinput", event => {
elementData.formattedValue = "";
const { data, target } = event; const { data, target } = event;
const { value, selectionStart, selectionEnd } = target; const { value, selectionStart, selectionEnd } = target;
let selStart = selectionStart,
selEnd = selectionEnd;
switch (event.inputType) {
// https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
case "deleteWordBackward": {
const match = value
.substring(0, selectionStart)
.match(/\w*[^\w]*$/);
if (match) {
selStart -= match[0].length;
}
break;
}
case "deleteWordForward": {
const match = value
.substring(selectionStart)
.match(/^[^\w]*\w*/);
if (match) {
selEnd += match[0].length;
}
break;
}
case "deleteContentBackward":
if (selectionStart === selectionEnd) {
selStart -= 1;
}
break;
case "deleteContentForward":
if (selectionStart === selectionEnd) {
selEnd += 1;
}
break;
}
// We handle the event ourselves.
event.preventDefault();
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
source: this, source: this,
detail: { detail: {
id, id,
name: "Keystroke", name: "Keystroke",
value, value,
change: data, change: data || "",
willCommit: false, willCommit: false,
selStart: selectionStart, selStart,
selEnd: selectionEnd, selEnd,
}, },
}); });
}); });
@ -1104,6 +1173,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
this._setTextStyle(element); this._setTextStyle(element);
this._setBackgroundColor(element); this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element);
this.container.appendChild(element); this.container.appendChild(element);
return this.container; return this.container;
@ -1213,6 +1283,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement {
} }
this._setBackgroundColor(element); this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element);
this.container.appendChild(element); this.container.appendChild(element);
return this.container; return this.container;
@ -1300,6 +1371,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement {
} }
this._setBackgroundColor(element); this._setBackgroundColor(element);
this._setDefaultPropertiesFromJS(element);
this.container.appendChild(element); this.container.appendChild(element);
return this.container; return this.container;
@ -1322,6 +1394,8 @@ class PushButtonWidgetAnnotationElement extends LinkAnnotationElement {
container.title = this.data.alternativeText; container.title = this.data.alternativeText;
} }
this._setDefaultPropertiesFromJS(container);
return container; return container;
} }
} }
@ -1534,6 +1608,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement {
} }
this._setBackgroundColor(selectElement); this._setBackgroundColor(selectElement);
this._setDefaultPropertiesFromJS(selectElement);
this.container.appendChild(selectElement); this.container.appendChild(selectElement);
return this.container; return this.container;

View File

@ -50,6 +50,18 @@ class AnnotationStorage {
return Object.assign(defaultValue, value); return Object.assign(defaultValue, value);
} }
/**
* Get the value for a given key.
*
* @public
* @memberof AnnotationStorage
* @param {string} key
* @returns {Object}
*/
getRawValue(key) {
return this._storage.get(key);
}
/** /**
* Set the value for a given key * Set the value for a given key
* *

View File

@ -434,7 +434,7 @@ class App extends PDFObject {
oDoc = null, oDoc = null,
oCheckbox = null oCheckbox = null
) { ) {
if (typeof cMsg === "object") { if (cMsg && typeof cMsg === "object") {
nType = cMsg.nType; nType = cMsg.nType;
cMsg = cMsg.cMsg; cMsg = cMsg.cMsg;
} }
@ -580,7 +580,7 @@ class App extends PDFObject {
} }
response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") {
if (typeof cQuestion === "object") { if (cQuestion && typeof cQuestion === "object") {
cDefault = cQuestion.cDefault; cDefault = cQuestion.cDefault;
cQuestion = cQuestion.cQuestion; cQuestion = cQuestion.cQuestion;
} }
@ -590,7 +590,7 @@ class App extends PDFObject {
} }
setInterval(cExpr, nMilliseconds = 0) { setInterval(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") { if (cExpr && typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0; nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr; cExpr = cExpr.cExpr;
} }
@ -609,7 +609,7 @@ class App extends PDFObject {
} }
setTimeOut(cExpr, nMilliseconds = 0) { setTimeOut(cExpr, nMilliseconds = 0) {
if (typeof cExpr === "object") { if (cExpr && typeof cExpr === "object") {
nMilliseconds = cExpr.nMilliseconds || 0; nMilliseconds = cExpr.nMilliseconds || 0;
cExpr = cExpr.cExpr; cExpr = cExpr.cExpr;
} }

View File

@ -820,8 +820,8 @@ class Doc extends PDFObject {
/* Not implemented */ /* Not implemented */
} }
getField(cName) { _getField(cName) {
if (typeof cName === "object") { if (cName && typeof cName === "object") {
cName = cName.cName; cName = cName.cName;
} }
if (typeof cName !== "string") { if (typeof cName !== "string") {
@ -859,6 +859,14 @@ class Doc extends PDFObject {
return null; return null;
} }
getField(cName) {
const field = this._getField(cName);
if (!field) {
return null;
}
return field.wrapped;
}
_getChildren(fieldName) { _getChildren(fieldName) {
// Children of foo.bar are foo.bar.oof, foo.bar.rab // Children of foo.bar are foo.bar.oof, foo.bar.rab
// but not foo.bar.oof.FOO. // but not foo.bar.oof.FOO.
@ -889,7 +897,7 @@ class Doc extends PDFObject {
} }
getNthFieldName(nIndex) { getNthFieldName(nIndex) {
if (typeof nIndex === "object") { if (nIndex && typeof nIndex === "object") {
nIndex = nIndex.nIndex; nIndex = nIndex.nIndex;
} }
if (typeof nIndex !== "number") { if (typeof nIndex !== "number") {
@ -1027,7 +1035,7 @@ class Doc extends PDFObject {
bAnnotations = true, bAnnotations = true,
printParams = null printParams = null
) { ) {
if (typeof bUI === "object") { if (bUI && typeof bUI === "object") {
nStart = bUI.nStart; nStart = bUI.nStart;
nEnd = bUI.nEnd; nEnd = bUI.nEnd;
bSilent = bUI.bSilent; bSilent = bUI.bSilent;
@ -1103,30 +1111,52 @@ class Doc extends PDFObject {
} }
resetForm(aFields = null) { resetForm(aFields = null) {
if (aFields && !Array.isArray(aFields) && typeof aFields === "object") { // Handle the case resetForm({ aFields: ... })
if (aFields && typeof aFields === "object") {
aFields = aFields.aFields; aFields = aFields.aFields;
} }
if (aFields && !Array.isArray(aFields)) {
aFields = [aFields];
}
let mustCalculate = false; let mustCalculate = false;
let fieldsToReset;
if (aFields) { if (aFields) {
fieldsToReset = [];
for (const fieldName of aFields) { for (const fieldName of aFields) {
if (!fieldName) { if (!fieldName) {
continue; continue;
} }
const field = this.getField(fieldName); if (typeof fieldName !== "string") {
// In Acrobat if a fieldName is not a string all the fields are reset.
fieldsToReset = null;
break;
}
const field = this._getField(fieldName);
if (!field) { if (!field) {
continue; continue;
} }
field.value = field.defaultValue; fieldsToReset.push(field);
field.valueAsString = field.value;
mustCalculate = true; mustCalculate = true;
} }
} else { }
if (!fieldsToReset) {
fieldsToReset = this._fields.values();
mustCalculate = this._fields.size !== 0; mustCalculate = this._fields.size !== 0;
for (const field of this._fields.values()) {
field.value = field.defaultValue;
field.valueAsString = field.value;
} }
for (const field of fieldsToReset) {
field.obj.value = field.obj.defaultValue;
this._send({
id: field.obj._id,
value: field.obj.defaultValue,
formattedValue: null,
selRange: [0, 0],
});
} }
if (mustCalculate) { if (mustCalculate) {
this.calculateNow(); this.calculateNow();
} }

View File

@ -45,6 +45,7 @@ class EventDispatcher {
this._objects = objects; this._objects = objects;
this._document.obj._eventDispatcher = this; this._document.obj._eventDispatcher = this;
this._isCalculating = false;
} }
mergeChange(event) { mergeChange(event) {
@ -129,24 +130,37 @@ class EventDispatcher {
return; return;
case "Action": case "Action":
this.runActions(source, source, event, name); this.runActions(source, source, event, name);
if (this._document.obj.calculate) {
this.runCalculate(source, event); this.runCalculate(source, event);
}
return; return;
} }
this.runActions(source, source, event, name); this.runActions(source, source, event, name);
if (name === "Keystroke") { if (name !== "Keystroke") {
return;
}
if (event.rc) { if (event.rc) {
if (event.willCommit) { if (event.willCommit) {
this.runValidation(source, event); this.runValidation(source, event);
} else if ( } else {
event.change !== savedChange.change || const value = (source.obj.value = this.mergeChange(event));
let selStart, selEnd;
if (
event.selStart !== savedChange.selStart || event.selStart !== savedChange.selStart ||
event.selEnd !== savedChange.selEnd event.selEnd !== savedChange.selEnd
) { ) {
source.wrapped.value = this.mergeChange(event); // Selection has been changed by the script so apply the changes.
selStart = event.selStart;
selEnd = event.selEnd;
} else {
selEnd = selStart = savedChange.selStart + event.change.length;
}
source.obj._send({
id: source.obj._id,
value,
selRange: [selStart, selEnd],
});
} }
} else if (!event.willCommit) { } else if (!event.willCommit) {
source.obj._send({ source.obj._send({
@ -160,30 +174,40 @@ class EventDispatcher {
source.obj._send({ source.obj._send({
id: source.obj._id, id: source.obj._id,
value: "", value: "",
formattedValue: null,
selRange: [0, 0], selRange: [0, 0],
}); });
} }
} }
}
runValidation(source, event) { runValidation(source, event) {
const hasRan = this.runActions(source, source, event, "Validate"); const didValidateRun = this.runActions(source, source, event, "Validate");
if (event.rc) { if (event.rc) {
if (hasRan) {
source.wrapped.value = event.value;
source.wrapped.valueAsString = event.value;
} else {
source.obj.value = event.value; source.obj.value = event.value;
source.obj.valueAsString = event.value;
}
if (this._document.obj.calculate) {
this.runCalculate(source, event); this.runCalculate(source, event);
const savedValue = (event.value = source.obj.value);
let formattedValue = null;
if (this.runActions(source, source, event, "Format")) {
formattedValue = event.value;
} }
event.value = source.obj.value; source.obj._send({
this.runActions(source, source, event, "Format"); id: source.obj._id,
source.wrapped.valueAsString = event.value; value: savedValue,
formattedValue,
});
event.value = savedValue;
} else if (didValidateRun) {
// The value is not valid.
source.obj._send({
id: source.obj._id,
value: "",
formattedValue: null,
selRange: [0, 0],
});
} }
} }
@ -198,17 +222,42 @@ class EventDispatcher {
} }
calculateNow() { calculateNow() {
if (!this._calculationOrder) { // This function can be called by a JS script (doc.calculateNow()).
// If !this._calculationOrder then there is nothing to calculate.
// _isCalculating is here to prevent infinite recursion with calculateNow.
// If !this._document.obj.calculate then the script doesn't want to have
// a calculate.
if (
!this._calculationOrder ||
this._isCalculating ||
!this._document.obj.calculate
) {
return; return;
} }
this._isCalculating = true;
const first = this._calculationOrder[0]; const first = this._calculationOrder[0];
const source = this._objects[first]; const source = this._objects[first];
globalThis.event = new Event({}); globalThis.event = new Event({});
try {
this.runCalculate(source, globalThis.event); this.runCalculate(source, globalThis.event);
} catch (error) {
this._isCalculating = false;
throw error;
}
this._isCalculating = false;
} }
runCalculate(source, event) { runCalculate(source, event) {
if (!this._calculationOrder) { // _document.obj.calculate is equivalent to doc.calculate and can be
// changed by a script to allow a future calculate or not.
// This function is either called by calculateNow or when an action
// is triggered (in this case we cannot be currently calculating).
// So there are no need to check for _isCalculating because it has
// been already done in calculateNow.
if (!this._calculationOrder || !this._document.obj.calculate) {
return; return;
} }
@ -218,31 +267,43 @@ class EventDispatcher {
} }
if (!this._document.obj.calculate) { if (!this._document.obj.calculate) {
// An action may have changed calculate value. // An action could have changed calculate value.
continue; break;
} }
event.value = null; event.value = null;
const target = this._objects[targetId]; const target = this._objects[targetId];
let savedValue = target.obj.value;
this.runActions(source, target, event, "Calculate"); this.runActions(source, target, event, "Calculate");
if (!event.rc) { if (!event.rc) {
continue; continue;
} }
if (event.value !== null) { if (event.value !== null) {
target.wrapped.value = event.value; // A new value has been calculated so set it.
target.obj.value = event.value;
} }
event.value = target.obj.value; event.value = target.obj.value;
this.runActions(target, target, event, "Validate"); this.runActions(target, target, event, "Validate");
if (!event.rc) { if (!event.rc) {
if (target.obj.value !== savedValue) {
target.wrapped.value = savedValue;
}
continue; continue;
} }
event.value = target.obj.value; savedValue = event.value = target.obj.value;
this.runActions(target, target, event, "Format"); let formattedValue = null;
if (event.value !== null) { if (this.runActions(target, target, event, "Format")) {
target.wrapped.valueAsString = event.value; formattedValue = event.value;
} }
target.obj._send({
id: target.obj._id,
value: savedValue,
formattedValue,
});
} }
} }
} }

View File

@ -87,8 +87,6 @@ class Field extends PDFObject {
this._globalEval = data.globalEval; this._globalEval = data.globalEval;
this._appObjects = data.appObjects; this._appObjects = data.appObjects;
this.valueAsString = data.valueAsString || this._value;
} }
get currentValueIndices() { get currentValueIndices() {
@ -252,14 +250,11 @@ class Field extends PDFObject {
} }
get valueAsString() { get valueAsString() {
if (this._valueAsString === undefined) { return (this._value ?? "").toString();
this._valueAsString = this._value ? this._value.toString() : "";
}
return this._valueAsString;
} }
set valueAsString(val) { set valueAsString(_) {
this._valueAsString = val ? val.toString() : ""; // Do nothing.
} }
browseForFileToSubmit() { browseForFileToSubmit() {
@ -376,7 +371,9 @@ class Field extends PDFObject {
} }
if (this._children === null) { if (this._children === null) {
this._children = this._document.obj._getChildren(this._fieldPath); this._children = this._document.obj
._getChildren(this._fieldPath)
.map(child => child.wrapped);
} }
return this._children; return this._children;
} }
@ -481,7 +478,7 @@ class Field extends PDFObject {
} }
_reset() { _reset() {
this.value = this.valueAsString = this.defaultValue; this.value = this.defaultValue;
} }
_runActions(event) { _runActions(event) {

View File

@ -120,8 +120,8 @@ function initSandbox(params) {
} }
const wrapped = new Proxy(field, proxyHandler); const wrapped = new Proxy(field, proxyHandler);
doc._addField(name, wrapped);
const _object = { obj: field, wrapped }; const _object = { obj: field, wrapped };
doc._addField(name, _object);
for (const object of objs) { for (const object of objs) {
appObjects[object.id] = _object; appObjects[object.id] = _object;
} }

View File

@ -237,7 +237,7 @@ describe("Interaction", () => {
await page.click("[data-annotation-id='402R']"); await page.click("[data-annotation-id='402R']");
await Promise.all( await Promise.all(
["16", "22", "19", "05", "27"].map(id => ["16", "22", "19", "05"].map(id =>
page.waitForFunction( page.waitForFunction(
`document.querySelector("#\\\\34 ${id}R").value === ""` `document.querySelector("#\\\\34 ${id}R").value === ""`
) )
@ -256,11 +256,14 @@ describe("Interaction", () => {
text = await page.$eval("#\\34 05R", el => el.value); text = await page.$eval("#\\34 05R", el => el.value);
expect(text).toEqual(""); expect(text).toEqual("");
const sum = await page.$eval("#\\34 27R", el => el.value);
expect(sum).toEqual("");
checked = await page.$eval("#\\34 49R", el => el.checked); checked = await page.$eval("#\\34 49R", el => el.checked);
expect(checked).toEqual(false); expect(checked).toEqual(false);
const visibility = await page.$eval(
"#\\34 27R",
el => getComputedStyle(el).visibility
);
expect(visibility).toEqual("hidden");
}) })
); );
}); });
@ -1137,4 +1140,166 @@ describe("Interaction", () => {
); );
}); });
}); });
describe("in issue14862.pdf", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue14862.pdf", "#\\32 7R");
pages.map(async ([, page]) => {
page.on("dialog", async dialog => {
await dialog.dismiss();
});
});
});
afterAll(async () => {
await closePages(pages);
});
it("must convert input in uppercase", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
await page.type("#\\32 7R", "Hello", { delay: 100 });
await page.waitForFunction(
`document.querySelector("#\\\\32 7R").value !== "Hello"`
);
let text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HELLO");
await page.type("#\\32 7R", " world", { delay: 100 });
await page.waitForFunction(
`document.querySelector("#\\\\32 7R").value !== "HELLO world"`
);
text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORLD");
await page.keyboard.press("Backspace");
await page.keyboard.press("Backspace");
await page.waitForFunction(
`document.querySelector("#\\\\32 7R").value !== "HELLO WORLD"`
);
text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HELLO WOR");
await page.type("#\\32 7R", "12.dL", { delay: 100 });
await page.waitForFunction(
`document.querySelector("#\\\\32 7R").value !== "HELLO WOR"`
);
text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORDL");
await page.type("#\\32 7R", " ", { delay: 100 });
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
await page.waitForFunction(
`document.querySelector("#\\\\32 7R").value !== "HELLO WORDL "`
);
text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HELLO ");
await page.$eval("#\\32 7R", el => {
// Select LL
el.selectionStart = 2;
el.selectionEnd = 4;
});
await page.keyboard.press("a");
text = await page.$eval("#\\32 7R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("HEAO ");
})
);
});
it("must check that an infinite loop is not triggered", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
await page.type("#\\32 8R", "Hello", { delay: 100 });
await page.waitForFunction(
`document.querySelector("#\\\\32 8R").value !== "123"`
);
let text = await page.$eval("#\\32 8R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("Hello123");
// The action will trigger a calculateNow which itself
// will trigger a resetForm (inducing a calculateNow) and a
// calculateNow.
await page.click("[data-annotation-id='31R']");
await page.waitForFunction(
`document.querySelector("#\\\\32 8R").value !== "Hello123"`
);
// Without preventing against infinite loop the field is empty.
text = await page.$eval("#\\32 8R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("123");
})
);
});
});
describe("in issue14705.pdf", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue14705.pdf", "#\\32 9R");
pages.map(async ([, page]) => {
page.on("dialog", async dialog => {
await dialog.dismiss();
});
});
});
afterAll(async () => {
await closePages(pages);
});
it("must check that field value is correctly updated", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForFunction(
"window.PDFViewerApplication.scriptingReady === true"
);
await page.type("#\\32 9R", "Hello World", { delay: 100 });
await page.click("#\\32 7R");
await page.waitForFunction(
`document.querySelector("#\\\\32 9R").value !== "Hello World"`
);
let text = await page.$eval("#\\32 9R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("checked");
await page.click("#\\32 7R");
await page.waitForFunction(
`document.querySelector("#\\\\32 9R").value !== "checked"`
);
text = await page.$eval("#\\32 9R", el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("unchecked");
})
);
});
});
}); });

View File

@ -520,3 +520,5 @@
!issue14502.pdf !issue14502.pdf
!issue13211.pdf !issue13211.pdf
!issue14627.pdf !issue14627.pdf
!issue14862.pdf
!issue14705.pdf

BIN
test/pdfs/issue14705.pdf Normal file

Binary file not shown.

BIN
test/pdfs/issue14862.pdf Executable file

Binary file not shown.

View File

@ -1320,7 +1320,7 @@ describe("api", function () {
{ {
id: "25R", id: "25R",
value: "", value: "",
defaultValue: null, defaultValue: "",
multiline: false, multiline: false,
password: false, password: false,
charLimit: null, charLimit: null,

View File

@ -89,7 +89,7 @@ describe("Scripting", function () {
return s; return s;
} }
const number = 123; const number = 123;
const expected = (((number - 1) * number) / 2).toString(); const expected = ((number - 1) * number) / 2;
const refId = getId(); const refId = getId();
const data = { const data = {
@ -120,7 +120,8 @@ describe("Scripting", function () {
expect(send_queue.has(refId)).toEqual(true); expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
valueAsString: expected, value: expected,
formattedValue: null,
}); });
}); });
}); });
@ -406,6 +407,7 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
value: "hella", value: "hella",
selRange: [5, 5],
}); });
}); });
@ -478,7 +480,7 @@ describe("Scripting", function () {
expect(send_queue.get(refId1)).toEqual({ expect(send_queue.get(refId1)).toEqual({
id: refId1, id: refId1,
value: "world", value: "world",
valueAsString: "world", formattedValue: null,
}); });
}); });
}); });
@ -799,7 +801,7 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
value: "123456.789", value: "123456.789",
valueAsString: "123456.789", formattedValue: null,
}); });
}); });
@ -978,7 +980,7 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
value: "321", value: "321",
valueAsString: "321", formattedValue: null,
}); });
}); });
@ -1076,7 +1078,7 @@ describe("Scripting", function () {
expect(send_queue.get(refIds[3])).toEqual({ expect(send_queue.get(refIds[3])).toEqual({
id: refIds[3], id: refIds[3],
value: 1, value: 1,
valueAsString: "1", formattedValue: null,
}); });
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
@ -1089,7 +1091,7 @@ describe("Scripting", function () {
expect(send_queue.get(refIds[3])).toEqual({ expect(send_queue.get(refIds[3])).toEqual({
id: refIds[3], id: refIds[3],
value: 3, value: 3,
valueAsString: "3", formattedValue: null,
}); });
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
@ -1102,7 +1104,7 @@ describe("Scripting", function () {
expect(send_queue.get(refIds[3])).toEqual({ expect(send_queue.get(refIds[3])).toEqual({
id: refIds[3], id: refIds[3],
value: 6, value: 6,
valueAsString: "6", formattedValue: null,
}); });
}); });
}); });
@ -1137,7 +1139,8 @@ describe("Scripting", function () {
selStart: 0, selStart: 0,
selEnd: 0, selEnd: 0,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
@ -1148,7 +1151,8 @@ describe("Scripting", function () {
selStart: 1, selStart: 1,
selEnd: 1, selEnd: 1,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
@ -1159,7 +1163,8 @@ describe("Scripting", function () {
selStart: 2, selStart: 2,
selEnd: 2, selEnd: 2,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
@ -1187,7 +1192,8 @@ describe("Scripting", function () {
selStart: 3, selStart: 3,
selEnd: 3, selEnd: 3,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
@ -1200,7 +1206,8 @@ describe("Scripting", function () {
expect(send_queue.has(refId)).toEqual(true); expect(send_queue.has(refId)).toEqual(true);
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
valueAsString: "3F?0", value: "3F?0",
formattedValue: null,
}); });
}); });
}); });
@ -1242,7 +1249,8 @@ describe("Scripting", function () {
selStart: i, selStart: i,
selEnd: i, selEnd: i,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
value += change; value += change;
} }
@ -1301,7 +1309,8 @@ describe("Scripting", function () {
selStart: i, selStart: i,
selEnd: i, selEnd: i,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
value += change; value += change;
} }
@ -1360,7 +1369,8 @@ describe("Scripting", function () {
selStart: i, selStart: i,
selEnd: i, selEnd: i,
}); });
expect(send_queue.has(refId)).toEqual(false); expect(send_queue.has(refId)).toEqual(true);
send_queue.delete(refId);
value += change; value += change;
} }