[JS] Make the date parser less strict

and display the expected date formt as a tooltip.
This commit is contained in:
Calixte Denizet 2025-06-19 16:15:02 +02:00
parent 5653458b51
commit 2541d96bf5
6 changed files with 81 additions and 47 deletions

View File

@ -61,6 +61,7 @@ import {
parseAppearanceStream, parseAppearanceStream,
parseDefaultAppearance, parseDefaultAppearance,
} from "./default_appearance.js"; } from "./default_appearance.js";
import { DateFormats, TimeFormats } from "../shared/scripting_utils.js";
import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js"; import { Dict, isName, isRefsEqual, Name, Ref, RefSet } from "./primitives.js";
import { Stream, StringStream } from "./stream.js"; import { Stream, StringStream } from "./stream.js";
import { BaseStream } from "./base_stream.js"; import { BaseStream } from "./base_stream.js";
@ -2780,6 +2781,25 @@ class TextWidgetAnnotation extends WidgetAnnotation {
!this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) && !this.hasFieldFlag(AnnotationFieldFlag.FILESELECT) &&
this.data.maxLen !== 0; this.data.maxLen !== 0;
this.data.doNotScroll = this.hasFieldFlag(AnnotationFieldFlag.DONOTSCROLL); 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() { get hasTextContent() {

View File

@ -1293,6 +1293,10 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement {
element.disabled = this.data.readOnly; element.disabled = this.data.readOnly;
element.name = this.data.fieldName; element.name = this.data.fieldName;
element.tabIndex = 0; element.tabIndex = 0;
const format = this.data.dateFormat || this.data.timeFormat;
if (format) {
element.title = format;
}
this._setRequired(element, this.data.required); this._setRequired(element, this.data.required);

View File

@ -13,6 +13,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { DateFormats, TimeFormats } from "../shared/scripting_utils.js";
import { GlobalConstants } from "./constants.js"; import { GlobalConstants } from "./constants.js";
class AForm { class AForm {
@ -21,23 +22,6 @@ class AForm {
this._app = app; this._app = app;
this._util = util; this._util = util;
this._color = color; 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: // The e-mail address regex below originates from:
// https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
@ -52,17 +36,14 @@ class AForm {
return event.target ? `[ ${event.target.name} ]` : ""; return event.target ? `[ ${event.target.name} ]` : "";
} }
_parseDate(cFormat, cDate, strict = false) { _parseDate(cFormat, cDate) {
let date = null; let date = null;
try { try {
date = this._util._scand(cFormat, cDate, strict); date = this._util._scand(cFormat, cDate, /* strict = */ false);
} catch {} } catch {}
if (date) { if (date) {
return date; return date;
} }
if (strict) {
return null;
}
date = Date.parse(cDate); date = Date.parse(cDate);
return isNaN(date) ? null : new Date(date); return isNaN(date) ? null : new Date(date);
@ -277,9 +258,7 @@ class AForm {
} }
AFDate_Format(pdf) { AFDate_Format(pdf) {
if (pdf >= 0 && pdf < this._dateFormats.length) { this.AFDate_FormatEx(DateFormats[pdf] ?? pdf);
this.AFDate_FormatEx(this._dateFormats[pdf]);
}
} }
AFDate_KeystrokeEx(cFormat) { AFDate_KeystrokeEx(cFormat) {
@ -293,7 +272,7 @@ class AForm {
return; return;
} }
if (this._parseDate(cFormat, value, /* strict = */ true) === null) { if (this._parseDate(cFormat, value) === null) {
const invalid = GlobalConstants.IDS_INVALID_DATE; const invalid = GlobalConstants.IDS_INVALID_DATE;
const invalid2 = GlobalConstants.IDS_INVALID_DATE2; const invalid2 = GlobalConstants.IDS_INVALID_DATE2;
const err = `${invalid} ${this._mkTargetName( const err = `${invalid} ${this._mkTargetName(
@ -305,8 +284,8 @@ class AForm {
} }
AFDate_Keystroke(pdf) { AFDate_Keystroke(pdf) {
if (pdf >= 0 && pdf < this._dateFormats.length) { if (pdf >= 0 && pdf < DateFormats.length) {
this.AFDate_KeystrokeEx(this._dateFormats[pdf]); this.AFDate_KeystrokeEx(DateFormats[pdf]);
} }
} }
@ -617,9 +596,7 @@ class AForm {
} }
AFTime_Format(pdf) { AFTime_Format(pdf) {
if (pdf >= 0 && pdf < this._timeFormats.length) { this.AFDate_FormatEx(TimeFormats[pdf] ?? pdf);
this.AFDate_FormatEx(this._timeFormats[pdf]);
}
} }
AFTime_KeystrokeEx(cFormat) { AFTime_KeystrokeEx(cFormat) {
@ -627,8 +604,8 @@ class AForm {
} }
AFTime_Keystroke(pdf) { AFTime_Keystroke(pdf) {
if (pdf >= 0 && pdf < this._timeFormats.length) { if (pdf >= 0 && pdf < TimeFormats.length) {
this.AFDate_KeystrokeEx(this._timeFormats[pdf]); this.AFDate_KeystrokeEx(TimeFormats[pdf]);
} }
} }

View File

@ -227,7 +227,7 @@ class Util extends PDFObject {
ddd: data => this._days[data.dayOfWeek].substring(0, 3), ddd: data => this._days[data.dayOfWeek].substring(0, 3),
dd: data => data.day.toString().padStart(2, "0"), dd: data => data.day.toString().padStart(2, "0"),
d: data => data.day.toString(), 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"), yy: data => (data.year % 100).toString().padStart(2, "0"),
HH: data => data.hours.toString().padStart(2, "0"), HH: data => data.hours.toString().padStart(2, "0"),
H: data => data.hours.toString(), H: data => data.hours.toString(),

View File

@ -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 };

View File

@ -1065,8 +1065,8 @@ describe("Scripting", function () {
id: refId, id: refId,
value: "", value: "",
actions: { actions: {
Format: [`AFDate_FormatEx("mmddyyyy");`], Format: [`AFDate_FormatEx("mm.dd.yyyy");`],
Keystroke: [`AFDate_KeystrokeEx("mmddyyyy");`], Keystroke: [`AFDate_KeystrokeEx("mm.dd.yyyy");`],
}, },
type: "text", type: "text",
}, },
@ -1080,7 +1080,7 @@ describe("Scripting", function () {
sandbox.createSandbox(data); sandbox.createSandbox(data);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
value: "12062023", value: "12.06.2023",
name: "Keystroke", name: "Keystroke",
willCommit: true, willCommit: true,
}); });
@ -1088,14 +1088,14 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
siblings: null, siblings: null,
value: "12062023", value: "12.06.2023",
formattedValue: "12062023", formattedValue: "12.06.2023",
}); });
send_queue.delete(refId); send_queue.delete(refId);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
value: "1206202", value: "12.06.202",
name: "Keystroke", name: "Keystroke",
willCommit: true, willCommit: true,
}); });
@ -1103,16 +1103,15 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
siblings: null, siblings: null,
value: "", value: "12.06.202",
formattedValue: null, formattedValue: "12.06.0202",
selRange: [0, 0],
}); });
send_queue.delete(refId); send_queue.delete(refId);
sandbox.createSandbox(data); sandbox.createSandbox(data);
await sandbox.dispatchEventInSandbox({ await sandbox.dispatchEventInSandbox({
id: refId, id: refId,
value: "02062023", value: "02.06.2023",
name: "Keystroke", name: "Keystroke",
willCommit: true, willCommit: true,
}); });
@ -1120,8 +1119,24 @@ describe("Scripting", function () {
expect(send_queue.get(refId)).toEqual({ expect(send_queue.get(refId)).toEqual({
id: refId, id: refId,
siblings: null, siblings: null,
value: "02062023", value: "02.06.2023",
formattedValue: "02062023", 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); send_queue.delete(refId);
}); });