diff --git a/src/core/annotation.js b/src/core/annotation.js index 8ef4e335c..f5fe324d8 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -61,6 +61,7 @@ import { parseAppearanceStream, parseDefaultAppearance, } from "./default_appearance.js"; +import { DateFormats, TimeFormats } from "../shared/scripting_utils.js"; import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js"; import { Stream, StringStream } from "./stream.js"; import { BaseStream } from "./base_stream.js"; @@ -2780,6 +2781,25 @@ class TextWidgetAnnotation extends WidgetAnnotation { !this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) && this.data.maxLen !== 0; this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); + + // Check if we have a date or time. + 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; + } + this.data[m[1] === "Date" ? "dateFormat" : "timeFormat"] = format; + break; + } + } } get hasTextContent() { diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 5ecac4947..e62ef7792 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -1293,6 +1293,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; + } this._setRequired(element, this.data.required); diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js index 58bda5e03..17f3f1aea 100644 --- a/src/scripting_api/aform.js +++ b/src/scripting_api/aform.js @@ -13,6 +13,7 @@ * limitations under the License. */ +import { DateFormats, TimeFormats } from "../shared/scripting_utils.js"; import { GlobalConstants } from "./constants.js"; class AForm { @@ -21,23 +22,6 @@ class AForm { this._app = app; this._util = util; this._color = color; - this._dateFormats = [ - "m/d", - "m/d/yy", - "mm/dd/yy", - "mm/yy", - "d-mmm", - "d-mmm-yy", - "dd-mmm-yy", - "yy-mm-dd", - "mmm-yy", - "mmmm-yy", - "mmm d, yyyy", - "mmmm d, yyyy", - "m/d/yy h:MM tt", - "m/d/yy HH:MM", - ]; - this._timeFormats = ["HH:MM", "h:MM tt", "HH:MM:ss", "h:MM:ss tt"]; // The e-mail address regex below originates from: // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address @@ -52,17 +36,14 @@ class AForm { return event.target ? `[ ${event.target.name} ]` : ""; } - _parseDate(cFormat, cDate, strict = false) { + _parseDate(cFormat, cDate) { let date = null; try { - date = this._util._scand(cFormat, cDate, strict); + date = this._util._scand(cFormat, cDate, /* strict = */ false); } catch {} if (date) { return date; } - if (strict) { - return null; - } date = Date.parse(cDate); return isNaN(date) ? null : new Date(date); @@ -277,9 +258,7 @@ class AForm { } AFDate_Format(pdf) { - if (pdf >= 0 && pdf < this._dateFormats.length) { - this.AFDate_FormatEx(this._dateFormats[pdf]); - } + this.AFDate_FormatEx(DateFormats[pdf] ?? pdf); } AFDate_KeystrokeEx(cFormat) { @@ -293,7 +272,7 @@ class AForm { return; } - if (this._parseDate(cFormat, value, /* strict = */ true) === null) { + if (this._parseDate(cFormat, value) === null) { const invalid = GlobalConstants.IDS_INVALID_DATE; const invalid2 = GlobalConstants.IDS_INVALID_DATE2; const err = `${invalid} ${this._mkTargetName( @@ -305,8 +284,8 @@ class AForm { } AFDate_Keystroke(pdf) { - if (pdf >= 0 && pdf < this._dateFormats.length) { - this.AFDate_KeystrokeEx(this._dateFormats[pdf]); + if (pdf >= 0 && pdf < DateFormats.length) { + this.AFDate_KeystrokeEx(DateFormats[pdf]); } } @@ -617,9 +596,7 @@ class AForm { } AFTime_Format(pdf) { - if (pdf >= 0 && pdf < this._timeFormats.length) { - this.AFDate_FormatEx(this._timeFormats[pdf]); - } + this.AFDate_FormatEx(TimeFormats[pdf] ?? pdf); } AFTime_KeystrokeEx(cFormat) { @@ -627,8 +604,8 @@ class AForm { } AFTime_Keystroke(pdf) { - if (pdf >= 0 && pdf < this._timeFormats.length) { - this.AFDate_KeystrokeEx(this._timeFormats[pdf]); + if (pdf >= 0 && pdf < TimeFormats.length) { + this.AFDate_KeystrokeEx(TimeFormats[pdf]); } } diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index 66f014c50..3a2e8fbcb 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -227,7 +227,7 @@ class Util extends PDFObject { ddd: data => this._days[data.dayOfWeek].substring(0, 3), dd: data => data.day.toString().padStart(2, "0"), d: data => data.day.toString(), - yyyy: data => data.year.toString(), + yyyy: data => data.year.toString().padStart(4, "0"), yy: data => (data.year % 100).toString().padStart(2, "0"), HH: data => data.hours.toString().padStart(2, "0"), H: data => data.hours.toString(), diff --git a/src/shared/scripting_utils.js b/src/shared/scripting_utils.js index dbf2fddc0..f5405e031 100644 --- a/src/shared/scripting_utils.js +++ b/src/shared/scripting_utils.js @@ -105,4 +105,22 @@ class ColorConverters { } } -export { ColorConverters }; +const DateFormats = [ + "m/d", + "m/d/yy", + "mm/dd/yy", + "mm/yy", + "d-mmm", + "d-mmm-yy", + "dd-mmm-yy", + "yy-mm-dd", + "mmm-yy", + "mmmm-yy", + "mmm d, yyyy", + "mmmm d, yyyy", + "m/d/yy h:MM tt", + "m/d/yy HH:MM", +]; +const TimeFormats = ["HH:MM", "h:MM tt", "HH:MM:ss", "h:MM:ss tt"]; + +export { ColorConverters, DateFormats, TimeFormats }; diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 9d2d49124..0a8170b4b 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -1065,8 +1065,8 @@ describe("Scripting", function () { id: refId, value: "", actions: { - Format: [`AFDate_FormatEx("mmddyyyy");`], - Keystroke: [`AFDate_KeystrokeEx("mmddyyyy");`], + Format: [`AFDate_FormatEx("mm.dd.yyyy");`], + Keystroke: [`AFDate_KeystrokeEx("mm.dd.yyyy");`], }, type: "text", }, @@ -1080,7 +1080,7 @@ describe("Scripting", function () { sandbox.createSandbox(data); await sandbox.dispatchEventInSandbox({ id: refId, - value: "12062023", + value: "12.06.2023", name: "Keystroke", willCommit: true, }); @@ -1088,14 +1088,14 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, siblings: null, - value: "12062023", - formattedValue: "12062023", + value: "12.06.2023", + formattedValue: "12.06.2023", }); send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, - value: "1206202", + value: "12.06.202", name: "Keystroke", willCommit: true, }); @@ -1103,16 +1103,15 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, siblings: null, - value: "", - formattedValue: null, - selRange: [0, 0], + value: "12.06.202", + formattedValue: "12.06.0202", }); send_queue.delete(refId); sandbox.createSandbox(data); await sandbox.dispatchEventInSandbox({ id: refId, - value: "02062023", + value: "02.06.2023", name: "Keystroke", willCommit: true, }); @@ -1120,8 +1119,24 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, siblings: null, - value: "02062023", - formattedValue: "02062023", + value: "02.06.2023", + formattedValue: "02.06.2023", + }); + send_queue.delete(refId); + + sandbox.createSandbox(data); + await sandbox.dispatchEventInSandbox({ + id: refId, + value: "2.6.2023", + name: "Keystroke", + willCommit: true, + }); + expect(send_queue.has(refId)).toEqual(true); + expect(send_queue.get(refId)).toEqual({ + id: refId, + siblings: null, + value: "2.6.2023", + formattedValue: "02.06.2023", }); send_queue.delete(refId); });