PageSourceSearch

https://www.stadtbuecherei-trier.de/_/scripts/rgElements/src/Elements/rgDatetimepicker/rg-datetimepicker.js

js stadtbuecherei-trier.de collected 2026-10-05 02:12:29 UTC 21,933 bytes, 505 lines download raw bytes

1import {rgElement} from '../rgElement/rg-element.js'
2
3/**
4 * Loads the necessary plugins for the flatpickr datepicker.
5 * @param {Array<string>} names array of plugin-names to load. Possible values: confirmDate, labelPlugin, monthSelect, weekSelect, minmaxTimePlugin, momentPlugin, rangePlugin, scrollPlugin
6 * @returns {Array<object>} Array of objects with path, type and insertPoint for each plugin to load.
7 */
8const addPlugins = function(names) {
9    let filelist = [];
10    for(const name of names) {
11        let path = 'https://cdn.regiogate.net/flatpickr/dist/plugins';
12        switch(name) {
13            case 'confirmDate':
14                filelist.push({
15                    path: path + '/confirmDate/confirmDate.js',
16                    type: 'js',
17                    insertPoint: this.shadowRoot
18                });
19                filelist.push({
20                    path: path + '/confirmDate/confirmDate.css',
21                    type: 'css',
22                    insertPoint: this.shadowRoot
23                });
24                break;
25            case 'labelPlugin':
26                filelist.push({
27                    path: path + '/labelPlugin/labelPlugin.js',
28                    type: 'js',
29                    insertPoint: this.shadowRoot
30                });
31                break;
32            case 'monthSelect':
33                filelist.push({
34                    path: path + '/monthSelect/index.js',
35                    type: 'js',
36                    insertPoint: this.shadowRoot
37                });
38                filelist.push({
39                    path: path + '/monthSelect/style.css',
40                    type: 'css',
41                    insertPoint: this.shadowRoot
42                });
43                break;
44            case 'weekSelect':
45                filelist.push({
46                    path: path + '/weekSelect/weekSelect.js',
47                    type: 'js',
48                    insertPoint: this.shadowRoot
49                });
50                break;
51            case 'minmaxTimePlugin':
52                filelist.push({
53                    path: path + '/minmaxTimePlugin.js',
54                    type: 'js',
55                    insertPoint: this.shadowRoot
56                });
57                break;
58            case 'momentPlugin':
59                filelist.push({
60                    path: path + '/momentPlugin.js',
61                    type: 'js',
62                    insertPoint: this.shadowRoot
63                });
64                break;
65            case 'rangePlugin':
66                filelist.push({
67                    path: path + '/rangePlugin.js',
68                    type: 'js',
69                    insertPoint: this.shadowRoot
70                });
71                break;
72            case 'scrollPlugin':
73                filelist.push({
74                    path: path + '/scrollPlugin.js',
75                    type: 'js',
76                    insertPoint: this.shadowRoot
77                });
78                break;
79        }
80    }
81    return filelist;
82}
83/**
84 * Initializes the flatpickr datepicker on the given input element, configuring it based on the element's type and data attributes.
85 * @param {HTMLInputElement} datePicker
86 */
87const setup = function(datePicker) {
88    let plugins = [];
89    switch(datePicker.type) {
90        case 'week':
91            plugins.push(new weekSelect());
92            break;
93        case 'month':
94            plugins.push(new monthSelectPlugin({dateFormat: 'Y-m'}));
95            break;
96        case 'monthview':
97            const prefix = datePicker.dataset.prfx || 'ev';
98            const config = {
99                dateFormat: 'Y-m',
100                module: datePicker.dataset.module,
101                prefix: prefix,
102                entriesPerDay: datePicker.dataset.entriesperday || 10,
103                rgDatePicker: datePicker,
104                param: {}
105            };
106            datePicker.querySelectorAll('script[is="rg-data"] param').forEach(param => {
107                config.param[param.dataset.name] = param.dataset.value;
108            });
109            plugins.push(new monthViewPlugin(config));
110            break;
111    }
112    let enableTime = !!datePicker.dataset.enabletime || datePicker.type === 'datetime-local' || datePicker.type === 'time',
113        datePickerConfig = {
114            appendTo: datePicker.shadowRoot,
115            mode: (datePicker.dataset.mode && [
116                'single',
117                'multiple',
118                'range'
119            ].includes(datePicker.dataset.mode)) ? datePicker.dataset.mode : 'single',
120            enableTime: enableTime,
121            time_24hr: true,
122            plugins: plugins,
123            weekNumbers: datePicker.type === 'week',
124            noCalendar: datePicker.type === 'time',
125            showMonths: datePicker.type === 'week' ? 1 : (!!datePicker.dataset.showmonths ? Number(datePicker.dataset.showmonths) : 1),
126            maxDate: datePicker.dataset.max ? Date.parseDateString(datePicker.dataset.max) : null,
127            minDate: datePicker.dataset.min ? Date.parseDateString(datePicker.dataset.min) : null,
128            disableMobile: true,
129            allowInput: true,
130            clickOpens: false,
131            inline: !!datePicker.dataset.inline,
132            position: function(instance, customPositionElement) {
133                instance.calendarContainer.style.top = '0';
134                instance.calendarContainer.style.left = '0';
135            }.bind(datePicker),
136            onOpen: function(selectedDates, dateStr, instance) {
137                let todayButton = instance.calendarContainer.querySelector('.flatpickr_button-container'),
138                    lastSelected = instance.latestSelectedDateObj;
139                if(lastSelected) {
140                    instance.jumpToDate(lastSelected, false);
141                }
142                if(!todayButton && datePicker.type !== 'time') {
143                    let todayDiv = document.createElement('div'),
144                        todayButtonText = datePicker.type === 'week' ? 'Diese Woche' : 'Heute';
145                    todayDiv.classList.add('flatpickr_button-container');
146                    todayDiv.classList.add('todayButton');
147                    todayButton = document.createElement('button');
148                    todayButton.innerText = todayButtonText;
149                    todayDiv.append(todayButton);
150                    instance.calendarContainer.append(todayDiv);
151                    todayButton.addEventListener('click', function() {
152                        instance.jumpToDate(new Date(), false);
153                    })
154                }
155                if(enableTime) {
156                    let $timelabel = instance.calendarContainer.querySelector('.flatpickr__timelabel-container');
157                    if($timelabel === null) {
158                        const $timepicker = instance.calendarContainer.querySelector('.flatpickr-time');
159                        $timelabel = document.createElement('div');
160                        $timelabel.classList.add('flatpickr__timelabel-container');
161                        const $timelabelHour = document.createElement('div');
162                        $timelabelHour.classList.add('flatpickr__timelabel-hour');
163                        $timelabelHour.innerText = 'Stunden';
164                        const $timelabelMinute = document.createElement('div');
165                        $timelabelMinute.classList.add('flatpickr__timelabel-minute');
166                        $timelabelMinute.innerText = 'Minuten';
167                        $timelabel.appendChild($timelabelHour);
168                        $timelabel.appendChild($timelabelMinute);
169                        $timepicker.insertAdjacentElement('beforebegin', $timelabel);
170                    }
171                }
172                datePicker.setAttribute('aria-hidden', 'false');
173                document.dispatchEvent(new CustomEvent('rgdatetimepicker:open', {
174                    detail: {el: this}
175                }));
176            }.bind(datePicker),
177            onClose: function() {
178                datePicker.setAttribute('aria-hidden', 'true');
179                document.dispatchEvent(new CustomEvent('rgdatetimepicker:close', {
180                    detail: {el: this}
181                }));
182            }.bind(datePicker),
183            onChange: function() {
184                document.dispatchEvent(new CustomEvent('rgdatetimepicker:change', {
185                    detail: {el: this}
186                }));
187            }.bind(datePicker),
188            onReady: function(selectedDates, dateStr, instance) {
189                datePicker.$element.type = datePicker.$element._type;
190            }.bind(datePicker)
191        }
192    if(!validate(datePicker.$element.value, validatorFormatStrings.get(datePicker.type))) {
193        let dateObj = Date.parseDateString(datePicker.dataset.value);
194        if(isNaN(dateObj)) {
195            dateObj = new Date();
196        }
197        datePicker.$element.setAttribute('value', flatpickr.formatDate(dateObj, datePicker.inputFormat.get(datePicker.type)));
198    }
199    datePicker.$element.datePicker = flatpickr(datePicker.$element, datePickerConfig);
200    if(datePicker.$element.datePicker.hourElement) {
201        datePicker.$element.datePicker.hourElement.addEventListener("wheel", handleMouseWheel);
202    }
203    if(datePicker.$element.datePicker.minuteElement) {
204        datePicker.$element.datePicker.minuteElement.addEventListener("wheel", handleMouseWheel);
205    }
206    setTimeout(function() {
207        //Ist das Element in einem Formular und hat "end" im Namen?
208        if(datePicker.form && datePicker.$element.dataset.name && datePicker.$element.dataset.name.match('end')) {
209            //Passendes Start-Element suchen
210            datePicker.dependent = datePicker.form.querySelector('input[data-name*="' + datePicker.$element.dataset.name.replace('end', 'start') + '"]');
211            //Wenn gefunden...
212            if(datePicker.dependent && datePicker.dependent.datePicker && datePicker.dependent !== datePicker.$element) {
213                //MinDate auf das des Start-Elements setzen
214                datePicker.$element.datePicker.set('minDate', datePicker.dependent.datePicker.selectedDates[0]);
215                //Wenn sich der Wert des Start-Elements ändert...
216                datePicker.dependent.datePicker.config.onChange.push(function(selectedDates, dateStr, instance) {
217                    let startDate = selectedDates[0],
218                        endDate = datePicker.$element.datePicker.selectedDates[0];
219                    //Mindate auf den Wert des Start-Elements setzen
220                    datePicker.$element.datePicker.set('minDate', startDate);
221                    if(startDate > endDate) {
222                        //Wenn Start-Datum größer als End-Datum: Werte neu setzen.
223                        datePicker.$element.datePicker.setDate(startDate);
224                        datePicker.$hiddenElement.value = new Date(datePicker.$element.value).format(this.outputFormat.get(this.type));
225                    }
226                }.bind(datePicker));
227            }
228        }
229    }.bind(datePicker), 500);
230}
231const validate = function(input, format) {
232    const regex = new RegExp(format);
233    const result = input.match(regex);
234    //TODO: Werte validieren, nicht nur Struktur
235    return result !== null;
236}
237const handleMouseWheel = function(event) {
238    event.preventDefault();
239    let value = Number(this.value),
240        min = Number(this.min),
241        max = Number(this.max),
242        step = Number(this.step);
243    value = event.wheelDeltaY > 0 ? value + step : value - step;
244    if(value < min) {
245        value = Math.floor(max / step) * step
246    } else if(value > max) {
247        value = min;
248    }
249    this.value = value < 10 ? '0' + value : String(value);
250    this.select();
251    this.focus();
252}
253const validatorFormatStrings = new Map([
254    ['date', '^(\\d{4})-(\\d{2})-(\\d{2})$'],
255    ['datetime-local', '^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})$'],
256    ['time', '^(\\d{2}):(\\d{2})$'],
257    ['week', '^(\\d{4})-W(\\d{2})$'],
258    ['month', '^(\\d{4})-(\\d{2})$']
259]);
260
261/**
262 * usage: <rg-datetimepicker data-name="ev[start]" data-value="28.02.2023 12:00" data-enabletime="1" data-mode="single" data-showmonths="2"></rg-datetimepicker>
263 */
264export class rgDatetimepicker extends rgElement {
265    $element = null;
266    $hiddenElement = null;
267    datePicker = null;
268    dependent = null;
269    outputFormat = new Map([
270        ['date', 'd.m.Y'],
271        ['datetime-local', 'd.m.Y H:i'],
272        ['time', 'H:i'],
273        ['week', '\\WW.Y'],
274        ['month', 'm.Y'],
275        ['monthview', 'd.m.Y'],
276    ]);
277    inputFormat = new Map([
278        ['date', 'Y-m-d'],
279        ['datetime-local', 'Y-m-dTH:i'],
280        ['time', 'H:i'],
281        ['week', 'Y-\\WW'],
282        ['month', 'Y-m'],
283        ['monthview', 'Y-m-dTH:i'],
284    ])
285
286    constructor() {
287        super();
288    }
289
290    connectedCallback() {
291        this.setUpElement();
292        !this.shadowRoot && this.attachShadow({mode: 'open'});
293        this.style.display = 'block';
294        //Vorhandenes Element suchen
295        if(this.form && this.dataset.name) {
296            this.$element = this.form.querySelector('*[name="' + this.dataset.name + '"]');
297        } else if(this.dataset.name) {
298            this.$element = document.querySelector('*[name="' + this.dataset.name + '"]');
299        }
300        //Wenn nicht vorhanden: erzeugen.
301        if(!this.$element || this.$element.tagName !== 'INPUT') {
302            let $input = document.createElement('input');
303            if(this.$element && this.$element.tagName !== 'INPUT') {
304                this.$element.replaceWith($input);
305            } else {
306                this.shadowRoot.host.parentElement.insertBefore($input, this.shadowRoot.host);
307            }
308            this.$element = $input;
309        }
310        //Element könnte bereits vorhanden sein: umbauen
311        this.type = this.dataset.type || 'datetime-local';
312        if(![
313            'date',
314            'datetime-local',
315            'time',
316            'week',
317            'month',
318            'monthview',
319        ].includes(this.type)) {
320            this.type = 'datetime-local';
321        }
322        if(this.type === 'monthview') {
323            this.dataset.inline = 'true';
324        }
325        if(!this.dataset.name) {
326            this.dataset.name = 'date';
327        }
328        const urlParams = new URLSearchParams(location.search);
329        if(urlParams.has(this.dataset.name)) {
330            this.dataset.value = urlParams.get(this.dataset.name);
331        }
332        const attributes = this.getAttributeNames();
333        const ariaAttributes = attributes.filter(name => name.includes('aria'))
334        ariaAttributes.forEach(name => {
335            this.$element.setAttribute(name, this.getAttribute(name))
336        })
337        this.$element.setAttribute('type', this.dataset.inline ? 'hidden' : this.type); //bei inline kein Eingabeelement
338        let dateObj = Date.parseDateString(this.dataset.value);
339        this.$element.value = dateObj.format(this.inputFormat.get(this.type));
340        this.$element.dataset.name = this.dataset.name;
341        this.$element.removeAttribute('name');
342        if(this.dataset.required) {
343            this.$element.setAttribute('required', 'required');
344        }
345        if(this.dataset.min) {
346            this.$element.setAttribute('min', Date.parseDateString(this.dataset.min).format(this.inputFormat.get(this.type)));
347        }
348        if(this.dataset.max) {
349            this.$element.setAttribute('max', Date.parseDateString(this.dataset.max).format(this.inputFormat.get(this.type)));
350        }
351        //Hidden Element
352        //nimmt das Datum im vorgegebenen Format auf
353        //das eigentliche Element handled das Datums-Object
354        this.$hiddenElement = document.createElement('input');
355        this.$hiddenElement.type = "hidden";
356        this.$hiddenElement.value = new Date(this.$element.value).format(this.outputFormat.get(this.type));
357        this.$hiddenElement.setAttribute('name', this.dataset.name);
358        this.shadowRoot.host.parentElement.insertBefore(this.$hiddenElement, this.shadowRoot.host);
359        //dem nativen Element Funktionen hinzufügen
360        this.$element.setValue = this.setValue.bind(this);
361        this.$element.getDate = this.getDate.bind(this);
362        this.$element.setOutputFormat = this.setOutputFormat.bind(this);
363        //Screenreader-Voodoo
364        this.setAttribute('aria-hidden', 'true');
365        // NVDA Screenreader wandelt Enter bzw. Space beim Öffnen des Datumswählers mit der Tastatur
366        // in einen Click (FF) bzw. Pointerdown+Click (Chrome) um.
367        // Der folgende VooDoo sorgt dafür, dass bei Tastaturbedienung immer der native Datumswähler geöffnet wird.
368        // Ebenso öffnen echte Touchevents die native Version. Nur Mausklicks öffnen den Flatpickr.
369        // echter Mausklick: pressure = 0.5, width = 1
370        // echter Touch: pressure > 0, width > 1 (theoretisch auch 1 möglich, aber nie beobachtet → potenzielle Fehlerquelle)
371        // programmatisch erzeugtes Event (durch NVDA auf Chrome): pressure = 0, width = 1
372        // in FF erzeugt NVDA nur ein click-event,
373        // wenn der flatpickr geöffnet wurde verhindert pointerEvent = true, dass ein nachfolgendes click-event zusätzlich den nativen Datepicker öffnet.
374        // Mausklicks feuern immer beide events.
375        let pointerEvent = false;
376        this.$element.addEventListener('change', function(event) {
377            const valid = this.$element.checkValidity();
378            this.$hiddenElement.value = valid ? new Date(this.$element.value).format(this.outputFormat.get(this.type)) : '';
379        }.bind(this));
380        this.$element.addEventListener(
381            'pointerdown',
382            event => {
383                if(event.pressure !== 0 && event.width === 1) {
384                    pointerEvent = true;
385                    event.preventDefault();
386                    document.dispatchEvent(new MouseEvent('mousedown'));
387                    this.$element.datePicker.open();
388                }
389            }
390        )
391        this.$element.addEventListener(
392            'click',
393            function(event) {
394                if(pointerEvent) {
395                    event.preventDefault();
396                    pointerEvent = false;
397                }
398            }
399        )
400        //VooDoo Ende
401        this.$element.addEventListener(
402            'change',
403            event => {
404                if(this.$element.checkValidity()) {
405                    this.$element.datePicker.setDate(this.$element.value);
406                    document.dispatchEvent(new CustomEvent('rgdatetimepicker:externalChange', {
407                        detail: {el: this}
408                    }));
409                } else {
410                    this.$element.value = this.$element.datePicker.selectedDates[0].format(this.inputFormat.get(this.type));
411                }
412            }
413        )
414        if(this.parseAttribute(this.dataset.autosubmit) === true) {
415            this.$element.addEventListener(
416                'change',
417                event => {
418                    this.form?.requestSubmit();
419                }
420            )
421        }
422        this.$element.addEventListener(
423            'keydown',
424            event => {
425                if(event.code === 'Escape' && this.$element.datePicker.isOpen) {
426                    this.$element.datePicker.close();
427                }
428            }
429        )
430        let fileList = [
431            {
432                path: 'https://cdn.regiogate.net/flatpickr/dist/flatpickr' + (!recon.debugmode ? '.min' : '') + '.js',
433                type: 'js',
434                followup: [
435                    {
436                        path: 'https://cdn.regiogate.net/flatpickr/dist/l10n/de.js',
437                        type: 'js',
438                        callback: () => {
439                            flatpickr.localize(flatpickr.l10ns.de);
440                        }
441                    }
442                ]
443            },
444            {
445                path: 'https://cdn.regiogate.net/flatpickr/dist/flatpickr.min.css',
446                type: 'css',
447                insertPoint: this.shadowRoot
448            },
449            {
450                path: 'https://cdn.regiogate.net/flatpickr/dist/themes/airbnb.css',
451                type: 'css',
452                insertPoint: this.shadowRoot
453            },
454            {
455                path: this.rootpath + '/css/rg-datetimepicker.css',
456                type: 'css',
457                insertPoint: this.shadowRoot
458            },
459        ];
460        let neededPlugins = [];
461        switch(this.type) {
462            case 'week':
463                neededPlugins.push('weekSelect');
464                break;
465            case 'month':
466                neededPlugins.push('monthSelect');
467                break;
468            case 'monthview':
469                fileList.push(
470                    {
471                        path: this.rootpath + '/api/monthview.js',
472                        type: 'js',
473                        insertPoint: this.shadowRoot
474                    }
475                )
476                break;
477        }
478        fileList = fileList.concat(addPlugins.call(this, neededPlugins));
479        this.loadFiles(
480            fileList,
481            () => {
482                setup(this);
483            }
484        );
485        const adoptedCSSTag = document.createElement('style');
486        adoptedCSSTag.innerHTML = this.copyStyles('.flatpickr');
487        this.shadowRoot.append(adoptedCSSTag);
488        this.setupComplete();
489    }
490
491    setValue(value) {
492        const date = Date.parseDateString(value);
493        this.$element.value = date.format(this.inputFormat.get(this.type));
494        this.$hiddenElement.value = date.format(this.outputFormat.get(this.type));
495    }
496
497    getDate() {
498        return new Date(this.$element.value);
499    }
500
501    setOutputFormat(format) {
502        this.outputFormat.set(this.type, format);
503        this.$hiddenElement.value = this.$element.getDate().format(this.outputFormat.get(this.type));
504    }
505}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.