From b01eb41eec4b1e699dd6a1491fbc32673e5686b3 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 2 Sep 2025 17:34:34 +0200 Subject: [PATCH] [IMP] shopfloor_mobile_base: Improves date-picker UX Allows to enter the date with the keyboard --- .../components/datepicker/date_picker.esm.js | 189 +++++++++++++++--- 1 file changed, 163 insertions(+), 26 deletions(-) diff --git a/shopfloor_mobile_base/static/src/components/datepicker/date_picker.esm.js b/shopfloor_mobile_base/static/src/components/datepicker/date_picker.esm.js index e280a4b4..41429f64 100644 --- a/shopfloor_mobile_base/static/src/components/datepicker/date_picker.esm.js +++ b/shopfloor_mobile_base/static/src/components/datepicker/date_picker.esm.js @@ -1,24 +1,96 @@ /** * Copyright 2022 Camptocamp SA (http://www.camptocamp.com) + * Copyright 2025 ACSONE SA/NV (https://www.acsone.com) * License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). */ import event_hub from "../../services/event_hub.esm.js"; +function getFormatAndMask(locale) { + const sample = new Date(2025, 8, 2); // 2 septembre 2025 + const parts = new Intl.DateTimeFormat(locale).formatToParts(sample); + + let format = ""; + let mask = ""; + + for (const p of parts) { + if (p.type === "day") { + format += "dd"; + mask += "##"; + } else if (p.type === "month") { + format += "MM"; + mask += "##"; + } else if (p.type === "year") { + format += "yyyy"; + mask += "####"; + } else if (p.type === "literal") { + format += p.value; + mask += p.value; + } + } + return {format, mask}; +} + +Vue.directive("date-mask", { + bind(el, binding) { + const locale = binding.value || navigator.language || "en-US"; + const {format, mask} = getFormatAndMask(locale); + + el.addEventListener("input", (e) => { + let v = e.target.value.replace(/\D/g, ""); // Only digits + if (v.length > 8) v = v.slice(0, 8); + + // Detects separator (first non # in the mask) + const sep = mask.match(/[^#]/)?.[0] || "/"; + const order = format.split(sep); // Ex: ["dd","MM","yyyy"] + + const chunks = []; + let cursor = 0; + + order.forEach((token) => { + const size = token === "yyyy" ? 4 : 2; + if (v.length >= cursor + size) { + chunks.push(v.slice(cursor, cursor + size)); + } else if (v.length > cursor) { + chunks.push(v.slice(cursor)); + } + cursor += size; + }); + + e.target.value = chunks.join(sep); + + // Propagate to v-model + e.target.dispatchEvent(new Event("input")); + }); + }, +}); + 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, + locale: { + type: String, + default: () => navigator.language || "en-US", + }, }, - data: function () { + data() { + const {format, mask} = getFormatAndMask(this.locale); return { - date: "", + date: "", // Iso format (YYYY-MM-DD) + dateInput: "", // Formatted display (DD/MM/YYYY or similar according to locale) + menu: false, + format, + mask, }; }, watch: { - date: function () { - this.$emit("date_picker_selected", this.date); + date(newVal) { + if (newVal) { + this.dateInput = this.formatISOToInput(newVal); + } else { + this.dateInput = ""; + } + this.$emit("date_picker_selected", newVal); }, }, mounted() { @@ -26,25 +98,90 @@ export var DatePicker = Vue.component("date-picker-input", { this.date = this.handler_to_update_date(data); }); }, + methods: { + parseInput() { + if (!this.dateInput) return null; + + // Detects separator (first non # in the mask) + const sepMatch = this.mask.match(/[^#]/); + const sep = sepMatch ? sepMatch[0] : "/"; + + const parts = this.dateInput.split(sep); + const order = this.format.split(sep); // Ex: ["dd","MM","yyyy"] + + let day = null, + month = null, + year = null; + order.forEach((token, i) => { + if (token === "dd") day = parts[i]; + if (token === "MM") month = parts[i]; + if (token === "yyyy") year = parts[i]; + }); + + if (!day || !month || !year) return null; + + const parsed = new Date(`${year}-${month}-${day}`); + return isNaN(parsed.getTime()) ? null : parsed.toISOString().substr(0, 10); + }, + + onBlur() { + if (!this.dateInput) { + this.date = ""; + return; + } + const parts = new Intl.DateTimeFormat(this.locale) + .formatToParts(new Date(2025, 8, 2)) // Model + .filter((p) => ["day", "month", "year"].includes(p.type)) + .map((p) => p.type); + + const sep = this.dateInput.match(/\D/)[0]; + const values = this.dateInput.split(sep); + + let day = null, + month = null, + year = null; + parts.forEach((token, i) => { + if (token === "day") day = values[i]; + if (token === "month") month = values[i]; + if (token === "year") year = values[i]; + }); + + const parsed = new Date(`${year}-${month}-${day}`); + if (!isNaN(parsed.getTime())) { + this.date = parsed.toISOString().substr(0, 10); + } + }, + + formatISOToInput(iso) { + if (!iso) return ""; + const d = new Date(iso); + return new Intl.DateTimeFormat(this.locale).format(d); + }, + }, template: ` - - - - - `, + + + + + `, });