Merge pull request #14869 from calixteman/14862

[JS] Fix few bugs present in the pdf for issue #14862
This commit is contained in:
Jonas Jenwald 2022-05-03 18:31:31 +02:00 committed by GitHub
commit 8135d7ccf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 567 additions and 214 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");
}) })
); );
}); });
@ -992,7 +995,7 @@ describe("Interaction", () => {
await clearInput(page, "#\\33 0R"); await clearInput(page, "#\\33 0R");
await page.focus("#\\32 9R"); await page.focus("#\\32 9R");
await page.type("#\\32 9R", "12A"); await page.type("#\\32 9R", "12A", { delay: 100 });
await page.waitForFunction( await page.waitForFunction(
`document.querySelector("#\\\\32 9R").value !== "12A"` `document.querySelector("#\\\\32 9R").value !== "12A"`
); );
@ -1001,7 +1004,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual("12"); expect(text).withContext(`In ${browserName}`).toEqual("12");
await page.focus("#\\32 9R"); await page.focus("#\\32 9R");
await page.type("#\\32 9R", "34"); await page.type("#\\32 9R", "34", { delay: 100 });
await page.click("[data-annotation-id='30R']"); await page.click("[data-annotation-id='30R']");
await page.waitForFunction( await page.waitForFunction(
@ -1012,7 +1015,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual(""); expect(text).withContext(`In ${browserName}`).toEqual("");
await page.focus("#\\32 9R"); await page.focus("#\\32 9R");
await page.type("#\\32 9R", "12345"); await page.type("#\\32 9R", "12345", { delay: 100 });
await page.click("[data-annotation-id='30R']"); await page.click("[data-annotation-id='30R']");
text = await page.$eval(`#\\32 9R`, el => el.value); text = await page.$eval(`#\\32 9R`, el => el.value);
@ -1049,7 +1052,7 @@ describe("Interaction", () => {
await clearInput(page, "#\\33 0R"); await clearInput(page, "#\\33 0R");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "(123) 456A"); await page.type("#\\33 0R", "(123) 456A", { delay: 100 });
await page.waitForFunction( await page.waitForFunction(
`document.querySelector("#\\\\33 0R").value !== "(123) 456A"` `document.querySelector("#\\\\33 0R").value !== "(123) 456A"`
); );
@ -1058,7 +1061,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual("(123) 456"); expect(text).withContext(`In ${browserName}`).toEqual("(123) 456");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "-789"); await page.type("#\\33 0R", "-789", { delay: 100 });
await page.click("[data-annotation-id='29R']"); await page.click("[data-annotation-id='29R']");
await page.waitForFunction( await page.waitForFunction(
@ -1069,7 +1072,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual(""); expect(text).withContext(`In ${browserName}`).toEqual("");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "(123) 456-7890"); await page.type("#\\33 0R", "(123) 456-7890", { delay: 100 });
await page.click("[data-annotation-id='29R']"); await page.click("[data-annotation-id='29R']");
text = await page.$eval(`#\\33 0R`, el => el.value); text = await page.$eval(`#\\33 0R`, el => el.value);
@ -1108,7 +1111,7 @@ describe("Interaction", () => {
await clearInput(page, "#\\33 0R"); await clearInput(page, "#\\33 0R");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "123A"); await page.type("#\\33 0R", "123A", { delay: 100 });
await page.waitForFunction( await page.waitForFunction(
`document.querySelector("#\\\\33 0R").value !== "123A"` `document.querySelector("#\\\\33 0R").value !== "123A"`
); );
@ -1117,7 +1120,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual("123"); expect(text).withContext(`In ${browserName}`).toEqual("123");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "-456"); await page.type("#\\33 0R", "-456", { delay: 100 });
await page.click("[data-annotation-id='29R']"); await page.click("[data-annotation-id='29R']");
await page.waitForFunction( await page.waitForFunction(
@ -1128,7 +1131,7 @@ describe("Interaction", () => {
expect(text).withContext(`In ${browserName}`).toEqual(""); expect(text).withContext(`In ${browserName}`).toEqual("");
await page.focus("#\\33 0R"); await page.focus("#\\33 0R");
await page.type("#\\33 0R", "123-4567"); await page.type("#\\33 0R", "123-4567", { delay: 100 });
await page.click("[data-annotation-id='29R']"); await page.click("[data-annotation-id='29R']");
text = await page.$eval(`#\\33 0R`, el => el.value); text = await page.$eval(`#\\33 0R`, el => el.value);
@ -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;
} }