From 690391da98db882cb6eb41bbdeaf80e961ca7046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Sun, 28 Jun 2026 17:59:43 +0200 Subject: [PATCH] wip: replace flatpickr --- assets/controllers/datepicker_controller.js | 74 ++++++++++++--------- assets/styles/components/datepicker.css | 14 ++-- package-lock.json | 20 +++--- package.json | 2 +- templates/forms_admin.html.twig | 28 +++++--- 5 files changed, 80 insertions(+), 58 deletions(-) diff --git a/assets/controllers/datepicker_controller.js b/assets/controllers/datepicker_controller.js index 0440bdab..b123bf23 100644 --- a/assets/controllers/datepicker_controller.js +++ b/assets/controllers/datepicker_controller.js @@ -1,56 +1,70 @@ import { Controller } from '@hotwired/stimulus' -import flatpickr from 'flatpickr'; -import { German } from 'flatpickr/dist/l10n/de'; +import { Calendar } from 'vanilla-calendar-pro' /* stimulusFetch: 'lazy' */ export default class extends Controller { - static targets = [ 'field' ] + static targets = ['field', 'hiddenField'] static values = { minDate: String, maxDate: String, disableWeekends: Boolean, - } - - initialize() { - flatpickr.localize(German) + disabledDates: Array, } connect() { - const minDate = this.minDateValue - const maxDate = this.maxDateValue const options = { - dateFormat: 'Y-m-d', - altFormat: 'd.m.Y', - minDate, - maxDate, - altInput: true, - allowInput: true, - onClose(dates, currentdatestring, instance) { - instance.setDate(instance.altInput.value, true, instance.config.altFormat) - this.element.dispatchEvent(new CustomEvent('datepicker:closed')) - }, - onChange(dates, currentdatestring, instance) { + inputMode: true, + locale: 'de-DE', + selectedTheme: 'light', + selectedDates: this.hiddenFieldTarget.value ? [this.hiddenFieldTarget.value] : [], + onClickDate: (self) => { + const iso = self.context.selectedDates[0] ?? null + this.hiddenFieldTarget.value = iso ?? '' + if (iso) { + const [year, month, day] = iso.split('-') + this.fieldTarget.value = `${day}.${month}.${year}` + } else { + this.fieldTarget.value = '' + } this.element.dispatchEvent(new CustomEvent('datepicker:picked', { - detail: { - date: instance.altInput.value - } + detail: { date: this.fieldTarget.value } })) }, + onHide: () => { + this.element.dispatchEvent(new CustomEvent('datepicker:closed')) + }, } - if (true === this.disableWeekendsValue) { - options['disable'] = [ - function(date) { - return (date.getDay() === 0 || date.getDay() === 6); + if (this.minDateValue) options.dateMin = this.minDateValue + if (this.maxDateValue) options.dateMax = this.maxDateValue + if (this.disableWeekendsValue) options.disableWeekdays = [0, 6] + if (this.disabledDatesValue.length) options.disableDates = this.disabledDatesValue + + this.picker = new Calendar(this.fieldTarget, options) + this.picker.init() + + this.handleManualInput = () => { + const value = this.fieldTarget.value.trim() + if (!value) { + this.hiddenFieldTarget.value = '' + this.picker.set({ selectedDates: [] }, { dates: true }) + } else { + const match = value.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/) + if (match) { + const [, day, month, year] = match + const iso = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}` + this.hiddenFieldTarget.value = iso + this.picker.set({ selectedDates: [iso] }, { dates: true }) } - ] + } + this.element.dispatchEvent(new CustomEvent('datepicker:closed')) } - - this.picker = flatpickr(this.fieldTarget, options) + this.fieldTarget.addEventListener('change', this.handleManualInput) } disconnect() { + this.fieldTarget.removeEventListener('change', this.handleManualInput) this.picker.destroy() } } diff --git a/assets/styles/components/datepicker.css b/assets/styles/components/datepicker.css index d70f11c9..ce0a53cb 100644 --- a/assets/styles/components/datepicker.css +++ b/assets/styles/components/datepicker.css @@ -1,13 +1,7 @@ -@import "flatpickr/dist/flatpickr.css"; +@import "vanilla-calendar-pro/styles/index.css"; +@import "vanilla-calendar-pro/styles/themes/light.css"; -.flatpickr-day.selected, -.flatpickr-day.selected:hover, -.flatpickr-day.selected:focus { +[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn, +[data-vc-theme=light] .vc-date[data-vc-date-selected] .vc-date__btn:hover { background-color: theme('colors.secondary'); - border-color: theme('colors.secondary'); -} - -.flatpickr-monthDropdown-months, -.flatpickr-current-month .flatpickr-monthDropdown-months { - @apply form-select pt-0 pb-0 focus:outline-none focus:shadow-none; } diff --git a/package-lock.json b/package-lock.json index eaffb3c0..acc03e33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "license": "WTFPL", "dependencies": { "@trevoreyre/autocomplete-js": "^3.0.3", - "flatpickr": "^4.6.13", "sortablejs": "^1.15.7", - "stimulus-use": "^0.52.3" + "stimulus-use": "^0.52.3", + "vanilla-calendar-pro": "^3.1.0" }, "devDependencies": { "@babel/core": "^7.17.0", @@ -4393,12 +4393,6 @@ "flat": "cli.js" } }, - "node_modules/flatpickr": { - "version": "4.6.13", - "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", - "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", - "license": "MIT" - }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -8563,6 +8557,16 @@ "dev": true, "license": "MIT" }, + "node_modules/vanilla-calendar-pro": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vanilla-calendar-pro/-/vanilla-calendar-pro-3.1.0.tgz", + "integrity": "sha512-yXDtCaedcKz6i5OOdWGwui0C8MAmjXjj7JzKZyjDlkczSRqnhI8BDGFygqT2K+qL1uY7R2fLYlTlxA6oyFs2yg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://buymeacoffee.com/uvarov" + } + }, "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", diff --git a/package.json b/package.json index c15a9cb9..9fa01098 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@trevoreyre/autocomplete-js": "^3.0.3", - "flatpickr": "^4.6.13", + "vanilla-calendar-pro": "^3.1.0", "sortablejs": "^1.15.7", "stimulus-use": "^0.52.3" } diff --git a/templates/forms_admin.html.twig b/templates/forms_admin.html.twig index b57db567..02d86825 100644 --- a/templates/forms_admin.html.twig +++ b/templates/forms_admin.html.twig @@ -162,21 +162,31 @@ {%- block datepicker_widget -%} {%- set minDate = form.vars.min_date ? form.vars.min_date | date('Y-m-d') : null -%} {%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%} - {%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm')|trim }) -%} + {%- set displayValue = value ? value|date('d.m.Y') : '' -%} + {%- set inputClass = 'block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset text-sm' -%} {%- if errors|length -%} - {%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' }) -%} - {% else %} - {%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-900 focus:ring-primary' }) -%} + {%- set inputClass = inputClass ~ ' ring-red-500 placeholder:red-500 focus:ring-red-500' -%} + {%- else -%} + {%- set inputClass = inputClass ~ ' placeholder:text-gray-900 focus:ring-primary' -%} {%- endif -%} {%- if disabled is defined and disabled == true -%} - {%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%} + {%- set inputClass = inputClass ~ ' cursor-not-allowed' -%} {%- endif -%}
- + +
- {%- if disabled is defined and disabled == true -%} - - {%- endif -%} {%- endblock datepicker_widget %} {%- block autocomplete_widget -%}