From 57ce4f8f43d24df0bfe139896f5bb8cdf7e0f7a0 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 22 Jul 2025 21:18:33 +0200 Subject: [PATCH] Use a HTML date/time input when a field requires a date or a time. The user will be able to enter a date in the format corresponding to their locale and it'll be formatted in using the format provided by the pdf. --- src/core/annotation.js | 77 ++++++++++++--- src/core/core_utils.js | 2 +- src/display/annotation_layer.js | 73 +++++++++++++-- src/scripting_api/console.js | 9 +- src/scripting_api/doc.js | 14 +++ src/scripting_api/field.js | 17 ++++ src/scripting_api/initialization.js | 1 + src/scripting_api/util.js | 4 +- test/integration/scripting_spec.mjs | 139 ++++++++++++++++++++++++++++ test/pdfs/.gitignore | 2 + test/pdfs/dates.pdf | Bin 0 -> 7087 bytes test/pdfs/dates_save.pdf | Bin 0 -> 8797 bytes test/unit/api_spec.js | 2 + test/unit/scripting_spec.js | 5 +- 14 files changed, 318 insertions(+), 27 deletions(-) create mode 100755 test/pdfs/dates.pdf create mode 100755 test/pdfs/dates_save.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index f4932c195..676918a40 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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; - } - this.data[m[1] === "Date" ? "dateFormat" : "timeFormat"] = 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"; + } + break; + } + this.data.datetimeType = "time"; + this.data.timeStep = /ss/.test(format) ? 1 : 60; + break; } } @@ -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", }; } diff --git a/src/core/core_utils.js b/src/core/core_utils.js index c5f3ed987..f3218ac61 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -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); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 8ff424df7..a77fe3f09 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -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 @@ -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); @@ -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; @@ -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) { @@ -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); @@ -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, + 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", { diff --git a/src/scripting_api/console.js b/src/scripting_api/console.js index a32550276..9ed15577b 100644 --- a/src/scripting_api/console.js +++ b/src/scripting_api/console.js @@ -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() { diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 0481f02e5..c76ccc047 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -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", diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index e484150aa..b5de544e7 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -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; + this._util = data.util; this._globalEval = data.globalEval; this._appObjects = data.appObjects; @@ -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" || @@ -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; } diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 946e68048..9e4727bcb 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -95,6 +95,7 @@ function initSandbox(params) { obj.doc = _document; obj.fieldPath = name; obj.appObjects = appObjects; + obj.util = util; const otherFields = annotations.slice(1); diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index 3a2e8fbcb..91229b0d7 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -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, diff --git a/test/integration/scripting_spec.mjs b/test/integration/scripting_spec.mjs index ed4543555..8193c3cf9 100644 --- a/test/integration/scripting_spec.mjs +++ b/test/integration/scripting_spec.mjs @@ -2513,4 +2513,143 @@ describe("Interaction", () => { ); }); }); + + describe("Date HTML element", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("dates.pdf", "[data-annotation-id='26R']"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the inputs are correct", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await waitForSandboxTrip(page); + + const firstInputSelector = "[data-annotation-id='26R'] > input"; + await page.waitForSelector(`${firstInputSelector}[type="text"]`); + await page.click(firstInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector(`${firstInputSelector}[type="date"]`); + await page.$eval(firstInputSelector, el => { + el.value = "1975-03-16"; + }); + + const secondInputSelector = "[data-annotation-id='27R'] > input"; + await page.waitForSelector(`${secondInputSelector}[type="text"]`); + await page.click(secondInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector(`${secondInputSelector}[type="time"]`); + await page.$eval(secondInputSelector, el => { + el.value = "01:23:45"; + }); + + const thirdInputSelector = "[data-annotation-id='28R'] > input"; + await page.waitForSelector(`${thirdInputSelector}[type="text"]`); + await page.click(thirdInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector( + `${thirdInputSelector}[type="datetime-local"]` + ); + await page.$eval(thirdInputSelector, el => { + el.value = "1975-03-16T01:23:45"; + }); + + const firstInputValue = await page.$eval( + firstInputSelector, + el => el.value + ); + expect(firstInputValue) + .withContext(`In ${browserName}`) + .toEqual("16-Mar-75"); + + const secondInputValue = await page.$eval( + secondInputSelector, + el => el.value + ); + expect(secondInputValue) + .withContext(`In ${browserName}`) + .toEqual("01:23:45"); + + await page.click(firstInputSelector); + await waitForSandboxTrip(page); + + const thirdInputValue = await page.$eval( + thirdInputSelector, + el => el.value + ); + expect(thirdInputValue) + .withContext(`In ${browserName}`) + .toEqual("3/16/1975 01:23"); + }) + ); + }); + }); + + describe("Date HTML element with initial values", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("dates_save.pdf", "[data-annotation-id='26R']"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the inputs are correct", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForScripting(page); + await waitForSandboxTrip(page); + + const firstInputSelector = "[data-annotation-id='26R'] > input"; + await page.waitForSelector(`${firstInputSelector}[type="text"]`); + await page.click(firstInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector(`${firstInputSelector}[type="date"]`); + const firstInputValue = await page.$eval( + firstInputSelector, + el => el.value + ); + expect(firstInputValue) + .withContext(`In ${browserName}`) + .toEqual("2025-07-01"); + + const secondInputSelector = "[data-annotation-id='27R'] > input"; + await page.waitForSelector(`${secondInputSelector}[type="text"]`); + await page.click(secondInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector(`${secondInputSelector}[type="time"]`); + const secondInputValue = await page.$eval( + secondInputSelector, + el => el.value + ); + expect(secondInputValue) + .withContext(`In ${browserName}`) + .toEqual("00:34:56"); + + const thirdInputSelector = "[data-annotation-id='28R'] > input"; + await page.waitForSelector(`${thirdInputSelector}[type="text"]`); + await page.click(thirdInputSelector); + await waitForSandboxTrip(page); + await page.waitForSelector( + `${thirdInputSelector}[type="datetime-local"]` + ); + const thirdInputValue = await page.$eval( + thirdInputSelector, + el => el.value + ); + expect(thirdInputValue) + .withContext(`In ${browserName}`) + .toEqual("2025-07-02T12:34"); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 3e8a6d274..ff077a83b 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -736,3 +736,5 @@ !issue20065.pdf !bug1708041.pdf !bug1978317.pdf +!dates.pdf +!dates_save.pdf diff --git a/test/pdfs/dates.pdf b/test/pdfs/dates.pdf new file mode 100755 index 0000000000000000000000000000000000000000..20175a03eb71281f2c28125e67e188cb7305e37e GIT binary patch literal 7087 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 NEFmf2yIxOV{R`03hMNEY literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 04c47d61a..e5fdf5d30 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1760,6 +1760,8 @@ describe("api", function () { strokeColor: null, fillColor: null, rotation: 0, + datetimeFormat: undefined, + hasDatetimeHTML: false, type: "text", }, ], diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 0a8170b4b..aeb82fb7a 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -676,9 +676,8 @@ describe("Scripting", function () { ); }; - const year = new Date().getFullYear(); - await check("05", "dd", `${year}/01/05`); - await check("12", "mm", `${year}/12/01`); + await check("05", "dd", "2000/01/05"); + await check("12", "mm", "2000/12/01"); await check("2022", "yyyy", "2022/01/01"); await check("a1$9bbbb21", "dd/mm/yyyy", "2021/09/01"); await check("1/2/2024", "dd/mm/yyyy", "2024/02/01");