diff --git a/shopfloor_mobile_base/static/wms/src/components/datepicker/date_picker.js b/shopfloor_mobile_base/static/wms/src/components/datepicker/date_picker.js index 5cc16c00ec9..5d127c8153f 100644 --- a/shopfloor_mobile_base/static/wms/src/components/datepicker/date_picker.js +++ b/shopfloor_mobile_base/static/wms/src/components/datepicker/date_picker.js @@ -3,48 +3,176 @@ * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ -import event_hub from "../../services/event_hub.js"; +function maskString(input, mask, maskChar = "#") { + if (!input) return ""; + // 1. Sanitize: Remove existing separators so we only have raw data + // This prevents "2020-3" from becoming "2020--3" on double-processing + const cleanInput = input.replace(/[^a-zA-Z0-9]/g, ""); + + const characters = cleanInput.split(""); + let result = ""; + + for (const char of mask) { + if (characters.length === 0) break; + + if (char === maskChar) { + result += characters.shift(); + } else { + result += char; + } + } + + return result; +} export var DatePicker = Vue.component("date-picker-input", { - props: { - // Method passed from the parent to update the picker's date - // from outside as required. - handler_to_update_date: Function, - }, data: function () { return { - date: "", + date: "", // Iso format (YYYY-MM-DD) + dateInput: "", // Formatted display (DD/MM/YYYY or similar according to locale) + showInvalidDateInputMessage: false, + + // Control menu state manually to prevent closing during month/year navigation. + menu: false, }; }, - watch: { - date: function () { - this.$emit("date_picker_selected", this.date); + computed: { + userLocale: function () { + const lang = this.$root.user?.lang || "en-US"; + return lang.replace("_", "-").toLowerCase(); + }, + dateFormatter: function () { + return new Intl.DateTimeFormat(this.userLocale); + }, + dateFormat: function () { + const sample = new Date(2025, 10, 10); + const parts = this.dateFormatter.formatToParts(sample); + return parts + .map((p) => { + if (p.type === "day") return "dd"; + if (p.type === "month") return "MM"; + if (p.type === "year") return "yyyy"; + return p.value; + }) + .join(""); + }, + dateMask: function () { + const sample = new Date(2025, 10, 10); + return this.dateFormatter.format(sample).replace(/[0-9]/g, "#"); }, }, - mounted() { - event_hub.$on("datepicker:newdate", (data) => { - this.date = this.handler_to_update_date(data); - }); + methods: { + onDateChange(newDate) { + this.menu = false; + this.dateInput = ""; + this.showInvalidDateInputMessage = false; + this.$emit("dateChange", newDate); + }, + /** + * Forces a synchronization between the Vue component state and the native DOM input. + * * This bypasses Vue's reactivity optimization which may skip a DOM update if the + * internal data value remains unchanged after an invalid user input (e.g., typing + * past a character limit). By briefly clearing and then restoring the value + * during the next DOM update cycle, we ensure the rendered element + * accurately reflects the component's state. + * + * @private + * @returns {void} + */ + _force_dateInput_refresh() { + const backup = this.dateInput; + this.dateInput += "a"; + this.$nextTick(() => { + this.dateInput = backup; + }); + }, + onInput(newInput) { + this.showInvalidDateInputMessage = false; + + if (newInput === null) { + this.dateInput = ""; + return; + } + + const lastChar = newInput.slice(-1); + if ( + (!/[0-9]/.test(lastChar) && newInput.length > this.dateInput.length) || + newInput.length > this.dateMask.length + ) { + this._force_dateInput_refresh(); + return; + } + + const maskedValue = maskString(newInput.replace(/\D/g, ""), this.dateMask); + this.dateInput = maskedValue; + }, + validateAndSync() { + if (!this.dateInput) return; + + const sep = this.dateMask.match(/[^#]/)[0]; + + const dateParts = this.dateInput.split(sep); + const fmtParts = this.dateFormat.split(sep); + + if (dateParts.length !== fmtParts.length) { + this.showInvalidDateInputMessage = true; + return; + } + + let year, month, day; + for (let i = 0; i < dateParts.length; i++) { + switch (fmtParts[i]) { + case "dd": + day = dateParts[i]; + break; + case "MM": + month = dateParts[i]; + break; + default: + year = dateParts[i]; + } + } + + // ↓ suppose 2000s in case of < 4 digits year + year = year.padStart(4, "20"); + const isoDate = `${year}-${month}-${day}`; + if (!isNaN(Date.parse(isoDate))) { + this.date = isoDate; + this.menu = false; + this.dateInput = ""; + this.$emit("dateChange", this.date); + } else { + this.showInvalidDateInputMessage = true; + } + }, }, template: ` - - - - + + + + `, });