Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2804,19 +2804,72 @@ class TextWidgetAnnotation extends WidgetAnnotation {
const {
data: { actions },
} = this;
for (const keystrokeAction of actions?.Keystroke || []) {
const m = keystrokeAction
.trim()
.match(/^AF(Date|Time)_Keystroke(?:Ex)?\(['"]?([^'"]+)['"]?\);$/);
if (m) {
let format = m[2];
const num = parseInt(format, 10);
if (!isNaN(num) && Math.floor(Math.log10(num)) + 1 === m[2].length) {
format = (m[1] === "Date" ? DateFormats : TimeFormats)[num] ?? format;

if (!actions) {
return;
}

const AFDateTime =
/^AF(Date|Time)_(?:Keystroke|Format)(?:Ex)?\(['"]?([^'"]+)['"]?\);$/;
let canUseHTMLDateTime = false;
if (
(actions.Format?.length === 1 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Format[0]) &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Format?.length === 0 &&
actions.Keystroke?.length === 1 &&
AFDateTime.test(actions.Keystroke[0])) ||
(actions.Keystroke?.length === 0 &&
actions.Format?.length === 1 &&
AFDateTime.test(actions.Format[0]))
) {
// If the Format and Keystroke actions are the same, we can just use
// the Format action.
canUseHTMLDateTime = true;
}
const actionsToVisit = [];
if (actions.Format) {
actionsToVisit.push(...actions.Format);
}
if (actions.Keystroke) {
actionsToVisit.push(...actions.Keystroke);
}
if (canUseHTMLDateTime) {
delete actions.Keystroke;
actions.Format = actionsToVisit;
}

for (const formatAction of actionsToVisit) {
const m = formatAction.match(AFDateTime);
if (!m) {
continue;
}
const isDate = m[1] === "Date";
let format = m[2];
const num = parseInt(format, 10);
if (!isNaN(num) && Math.floor(Math.log10(num)) + 1 === m[2].length) {
format = (isDate ? DateFormats : TimeFormats)[num] ?? format;
}
this.data.datetimeFormat = format;
if (!canUseHTMLDateTime) {
// The datetime format will just be used as a tooltip.
break;
}
if (isDate) {
// We can have a date and a time so we'll use a time input in this
// case.
if (/HH|MM|ss|h/.test(format)) {
this.data.datetimeType = "datetime-local";
this.data.timeStep = /ss/.test(format) ? 1 : 60;
} else {
this.data.datetimeType = "date";
}
this.data[m[1] === "Date" ? "dateFormat" : "timeFormat"] = format;
break;
}
this.data.datetimeType = "time";
this.data.timeStep = /ss/.test(format) ? 1 : 60;
break;
}
}

Expand Down Expand Up @@ -3013,6 +3066,8 @@ class TextWidgetAnnotation extends WidgetAnnotation {
strokeColor: this.data.borderColor,
fillColor: this.data.backgroundColor,
rotation: this.rotation,
datetimeFormat: this.data.datetimeFormat,
hasDatetimeHTML: !!this.data.datetimeType,
type: "text",
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/core/core_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ function _collectJS(entry, xref, list, parents) {
/* keepEscapeSequence = */ true
).replaceAll("\x00", "");
if (code) {
list.push(code);
list.push(code.trim());
}
}
_collectJS(entry.getRaw("Next"), xref, list, parents);
Expand Down
73 changes: 65 additions & 8 deletions src/display/annotation_layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { XfaLayer } from "./xfa_layer.js";

const DEFAULT_FONT_SIZE = 9;
const GetElementsByNameSet = new WeakSet();
const TIMEZONE_OFFSET = new Date().getTimezoneOffset() * 60 * 1000;

/**
* @typedef {Object} AnnotationElementParameters
Expand Down Expand Up @@ -1354,9 +1355,10 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
element.disabled = this.data.readOnly;
element.name = this.data.fieldName;
element.tabIndex = 0;
const format = this.data.dateFormat || this.data.timeFormat;
if (format) {
element.title = format;
const { datetimeFormat, datetimeType, timeStep } = this.data;
const hasDateOrTime = !!datetimeType && this.enableScripting;
if (datetimeFormat) {
element.title = datetimeFormat;
}

this._setRequired(element, this.data.required);
Expand Down Expand Up @@ -1397,8 +1399,34 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
return;
}
const { target } = event;
if (hasDateOrTime) {
target.type = datetimeType;
if (timeStep) {
target.step = timeStep;
}
}

if (elementData.userValue) {
target.value = elementData.userValue;
const value = elementData.userValue;
if (hasDateOrTime) {
if (datetimeType === "time") {
const date = new Date(value);
const parts = [
date.getHours(),
date.getMinutes(),
date.getSeconds(),
];
target.value = parts
.map(v => v.toString().padStart(2, "0"))
.join(":");
} else {
target.value = new Date(value - TIMEZONE_OFFSET)
.toISOString()
.split(datetimeType === "date" ? "T" : ".", 1)[0];
}
} else {
target.value = value;
}
}
elementData.lastCommittedValue = target.value;
elementData.commitKey = 1;
Expand All @@ -1412,7 +1440,11 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
const actions = {
value(event) {
elementData.userValue = event.detail.value ?? "";
storage.setValue(id, { value: elementData.userValue.toString() });
if (!hasDateOrTime) {
storage.setValue(id, {
value: elementData.userValue.toString(),
});
}
event.target.value = elementData.userValue;
},
formattedValue(event) {
Expand All @@ -1426,9 +1458,16 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
// Input hasn't the focus so display formatted string
event.target.value = formattedValue;
}
storage.setValue(id, {
const data = {
formattedValue,
});
};
if (hasDateOrTime) {
// If the field is a date or time, we store the formatted value
// in the `value` property, so that it can be used by the
// `Keystroke` action.
data.value = formattedValue;
}
storage.setValue(id, data);
},
selRange(event) {
event.target.setSelectionRange(...event.detail.selRange);
Expand Down Expand Up @@ -1516,7 +1555,25 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
if (!this.data.actions?.Blur) {
elementData.focused = false;
}
const { value } = event.target;
const { target } = event;
let { value } = target;
if (hasDateOrTime) {
if (value && datetimeType === "time") {
const parts = value.split(":").map(v => parseInt(v, 10));
value = new Date(
2000,
Comment thread
calixteman marked this conversation as resolved.
0,
1,
parts[0],
parts[1],
parts[2] || 0
).valueOf();
target.step = "";
} else {
value = new Date(value).valueOf();
}
target.type = "text";
}
elementData.userValue = value;
if (elementData.lastCommittedValue !== value) {
this.linkService.eventBus?.dispatch("dispatcheventinsandbox", {
Expand Down
9 changes: 7 additions & 2 deletions src/scripting_api/console.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ class Console extends PDFObject {
}

println(msg) {
if (typeof msg === "string") {
this._send({ command: "println", value: "PDF.js Console:: " + msg });
if (typeof msg !== "string") {
try {
msg = JSON.stringify(msg);
} catch {
msg = msg.toString?.() || "[Unserializable object]";
}
}
this._send({ command: "println", value: "PDF.js Console:: " + msg });
}

show() {
Expand Down
14 changes: 14 additions & 0 deletions src/scripting_api/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ class Doc extends PDFObject {
}

_initActions() {
for (const { obj } of this._fields.values()) {
// Some fields may have compute their values so we need to send them
// to the view.
const initialValue = obj._initialValue;
if (initialValue) {
this._send({
id: obj._id,
siblings: obj._siblings,
value: initialValue,
formattedValue: obj.value.toString(),
});
}
}

const dontRun = new Set([
"WillClose",
"WillSave",
Expand Down
17 changes: 17 additions & 0 deletions src/scripting_api/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ class Field extends PDFObject {
this._fieldType = getFieldType(this._actions);
this._siblings = data.siblings || null;
this._rotation = data.rotation || 0;
this._datetimeFormat = data.datetimeFormat || null;
this._hasDateOrTime = !!data.hasDatetimeHTML;
Comment thread
calixteman marked this conversation as resolved.
this._util = data.util;

this._globalEval = data.globalEval;
this._appObjects = data.appObjects;
Expand Down Expand Up @@ -246,6 +249,16 @@ class Field extends PDFObject {
return;
}

if (this._hasDateOrTime && value) {
const date = this._util.scand(this._datetimeFormat, value);
if (date) {
this._originalValue = date.valueOf();
value = this._util.printd(this._datetimeFormat, date);
this._value = !isNaN(value) ? parseFloat(value) : value;
return;
}
}

if (
value === "" ||
typeof value !== "string" ||
Expand All @@ -262,6 +275,10 @@ class Field extends PDFObject {
this._value = !isNaN(_value) ? parseFloat(_value) : value;
}

get _initialValue() {
return (this._hasDateOrTime && this._originalValue) || null;
}

_getValue() {
return this._originalValue ?? this.value;
}
Expand Down
1 change: 1 addition & 0 deletions src/scripting_api/initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function initSandbox(params) {
obj.doc = _document;
obj.fieldPath = name;
obj.appObjects = appObjects;
obj.util = util;

const otherFields = annotations.slice(1);

Expand Down
4 changes: 2 additions & 2 deletions src/scripting_api/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,10 +619,10 @@ class Util extends PDFObject {
}

const data = {
year: new Date().getFullYear(),
year: 2000, // 2000 because it's 00 in yy format.
month: 0,
day: 1,
hours: 12,
hours: 0,
minutes: 0,
seconds: 0,
am: null,
Expand Down
Loading