Merge pull request #20116 from calixteman/add_date_picker
Use a HTML date/time input when a field requires a date or a time.
This commit is contained in:
commit
542514efbd
@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -95,6 +95,7 @@ function initSandbox(params) {
|
||||
obj.doc = _document;
|
||||
obj.fieldPath = name;
|
||||
obj.appObjects = appObjects;
|
||||
obj.util = util;
|
||||
|
||||
const otherFields = annotations.slice(1);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -736,3 +736,5 @@
|
||||
!issue20065.pdf
|
||||
!bug1708041.pdf
|
||||
!bug1978317.pdf
|
||||
!dates.pdf
|
||||
!dates_save.pdf
|
||||
|
||||
BIN
test/pdfs/dates.pdf
Executable file
BIN
test/pdfs/dates.pdf
Executable file
Binary file not shown.
BIN
test/pdfs/dates_save.pdf
Executable file
BIN
test/pdfs/dates_save.pdf
Executable file
Binary file not shown.
@ -1760,6 +1760,8 @@ describe("api", function () {
|
||||
strokeColor: null,
|
||||
fillColor: null,
|
||||
rotation: 0,
|
||||
datetimeFormat: undefined,
|
||||
hasDatetimeHTML: false,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user