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.