vendor: 6,211 bytes, lines 1-196
1function lastItemOf(arr) { 2 return arr[arr.length - 1]; 3} 4 5// push only the items not included in the array 6function pushUnique(arr, ...items) { 7 items.forEach((item) => { 8 if (arr.includes(item)) { 9 return; 10 } 11 arr.push(item); 12 }); 13 return arr; 14} 15 16function stringToArray(str, separator) { 17 // convert empty string to an empty array 18 return str ? str.split(separator) : []; 19} 20 21function isInRange(testVal, min, max) { 22 const minOK = min === undefined || testVal >= min; 23 const maxOK = max === undefined || testVal <= max; 24 return minOK && maxOK; 25} 26 27function limitToRange(val, min, max) { 28 if (val < min) { 29 return min; 30 } 31 if (val > max) { 32 return max; 33 } 34 return val; 35} 36 37function createTagRepeat(tagName, repeat, attributes = {}, index = 0, html = '') { 38 const openTagSrc = Object.keys(attributes).reduce((src, attr) => { 39 let val = attributes[attr]; 40 if (typeof val === 'function') { 41 val = val(index); 42 } 43 return `${src} ${attr}="${val}"`; 44 }, tagName); 45 html += `<${openTagSrc}></${tagName}>`; 46 47 const next = index + 1; 48 return next < repeat 49 ? createTagRepeat(tagName, repeat, attributes, next, html) 50 : html; 51} 52 53// Remove the spacing surrounding tags for HTML parser not to create text nodes 54// before/after elements 55function optimizeTemplateHTML(html) { 56 return html.replace(/>\s+/g, '>').replace(/\s+</, '<'); 57} 58 59function stripTime(timeValue) { 60 return new Date(timeValue).setHours(0, 0, 0, 0); 61} 62 63function today() { 64 return new Date().setHours(0, 0, 0, 0); 65} 66 67// Get the time value of the start of given date or year, month and day 68function dateValue(...args) { 69 switch (args.length) { 70 case 0: 71 return today(); 72 case 1: 73 return stripTime(args[0]); 74 } 75 76 // use setFullYear() to keep 2-digit year from being mapped to 1900-1999 77 const newDate = new Date(0); 78 newDate.setFullYear(...args); 79 return newDate.setHours(0, 0, 0, 0); 80} 81 82function addDays(date, amount) { 83 const newDate = new Date(date); 84 return newDate.setDate(newDate.getDate() + amount); 85} 86 87function addWeeks(date, amount) { 88 return addDays(date, amount * 7); 89} 90 91function addMonths(date, amount) { 92 // If the day of the date is not in the new month, the last day of the new 93 // month will be returned. e.g. Jan 31 + 1 month â Feb 28 (not Mar 03) 94 const newDate = new Date(date); 95 const monthsToSet = newDate.getMonth() + amount; 96 let expectedMonth = monthsToSet % 12; 97 if (expectedMonth < 0) { 98 expectedMonth += 12; 99 } 100 101 const time = newDate.setMonth(monthsToSet); 102 return newDate.getMonth() !== expectedMonth ? newDate.setDate(0) : time; 103} 104 105function addYears(date, amount) { 106 // If the date is Feb 29 and the new year is not a leap year, Feb 28 of the 107 // new year will be returned. 108 const newDate = new Date(date); 109 const expectedMonth = newDate.getMonth(); 110 const time = newDate.setFullYear(newDate.getFullYear() + amount); 111 return expectedMonth === 1 && newDate.getMonth() === 2 ? newDate.setDate(0) : time; 112} 113 114// Calculate the distance bettwen 2 days of the week 115function dayDiff(day, from) { 116 return (day - from + 7) % 7; 117} 118 119// Get the date of the specified day of the week of given base date 120function dayOfTheWeekOf(baseDate, dayOfWeek, weekStart = 0) { 121 const baseDay = new Date(baseDate).getDay(); 122 return addDays(baseDate, dayDiff(dayOfWeek, weekStart) - dayDiff(baseDay, weekStart)); 123} 124 125function calcWeekNum(dayOfTheWeek, sameDayOfFirstWeek) { 126 return Math.round((dayOfTheWeek - sameDayOfFirstWeek) / 604800000) + 1; 127} 128 129// Get the ISO week number of a date 130function getIsoWeek(date) { 131 // - Start of ISO week is Monday 132 // - Use Thursday for culculation because the first Thursday of ISO week is 133 // always in January 134 const thuOfTheWeek = dayOfTheWeekOf(date, 4, 1); 135 // - Week 1 in ISO week is the week including Jan 04 136 // - Use the Thu of given date's week (instead of given date itself) to 137 // calculate week 1 of the year so that Jan 01 - 03 won't be miscalculated 138 // as week 0 when Jan 04 is Mon - Wed 139 const firstThu = dayOfTheWeekOf(new Date(thuOfTheWeek).setMonth(0, 4), 4, 1); 140 // return Math.round((thuOfTheWeek - firstThu) / 604800000) + 1; 141 return calcWeekNum(thuOfTheWeek, firstThu); 142} 143 144// Calculate week number in traditional week number system 145// @see https://en.wikipedia.org/wiki/Week#Other_week_numbering_systems 146function calcTraditionalWeekNumber(date, weekStart) { 147 // - Week 1 of traditional week is the week including the Jan 01 148 // - Use Jan 01 of given date's year to calculate the start of week 1 149 const startOfFirstWeek = dayOfTheWeekOf(new Date(date).setMonth(0, 1), weekStart, weekStart); 150 const startOfTheWeek = dayOfTheWeekOf(date, weekStart, weekStart); 151 const weekNum = calcWeekNum(startOfTheWeek, startOfFirstWeek); 152 if (weekNum < 53) { 153 return weekNum; 154 } 155 // If the 53rd week includes Jan 01, it's actually next year's week 1 156 const weekOneOfNextYear = dayOfTheWeekOf(new Date(date).setDate(32), weekStart, weekStart); 157 return startOfTheWeek === weekOneOfNextYear ? 1 : weekNum; 158} 159 160// Get the Western traditional week number of a date 161function getWesternTradWeek(date) { 162 // Start of Western traditionl week is Sunday 163 return calcTraditionalWeekNumber(date, 0); 164} 165 166// Get the Middle Eastern week number of a date 167function getMidEasternWeek(date) { 168 // Start of Middle Eastern week is Saturday 169 return calcTraditionalWeekNumber(date, 6); 170} 171 172// Get the start year of the period of years that includes given date 173// years: length of the year period 174function startOfYearPeriod(date, years) { 175 /* @see https://en.wikipedia.org/wiki/Year_zero#ISO_8601 */ 176 const year = new Date(date).getFullYear(); 177 return Math.floor(year / years) * years; 178} 179 180// Convert date to the first/last date of the month/year of the date 181function regularizeDate(date, timeSpan, useLastDate) { 182 if (timeSpan !== 1 && timeSpan !== 2) { 183 return date; 184 } 185 const newDate = new Date(date); 186 if (timeSpan === 1) { 187 useLastDate 188 ? newDate.setMonth(newDate.getMonth() + 1, 0) 189 : newDate.setDate(1); 190 } else { 191 useLastDate 192 ? newDate.setFullYear(newDate.getFullYear() + 1, 0, 0) 193 : newDate.setMonth(0, 1); 194 } 195 return newDate.setHours(0, 0, 0, 0); 196}
vendor: 5,194 bytes, lines 196-380
196 197 198// pattern for format parts 199const reFormatTokens = /dd?|DD?|mm?|MM?|yy?(?:yy)?/; 200// pattern for non date parts 201const reNonDateParts = /[\s!-/:-@[-`{-~å¹´ææ¥]+/; 202// cache for persed formats 203let knownFormats = {}; 204// parse funtions for date parts 205const parseFns = { 206 y(date, year) { 207 return new Date(date).setFullYear(parseInt(year, 10)); 208 }, 209 m(date, month, locale) { 210 const newDate = new Date(date); 211 let monthIndex = parseInt(month, 10) - 1; 212 213 if (isNaN(monthIndex)) { 214 if (!month) { 215 return NaN; 216 } 217 218 const monthName = month.toLowerCase(); 219 const compareNames = name => name.toLowerCase().startsWith(monthName); 220 // compare with both short and full names because some locales have periods 221 // in the short names (not equal to the first X letters of the full names) 222 monthIndex = locale.monthsShort.findIndex(compareNames); 223 if (monthIndex < 0) { 224 monthIndex = locale.months.findIndex(compareNames); 225 } 226 if (monthIndex < 0) { 227 return NaN; 228 } 229 } 230 231 newDate.setMonth(monthIndex); 232 return newDate.getMonth() !== normalizeMonth(monthIndex) 233 ? newDate.setDate(0) 234 : newDate.getTime(); 235 }, 236 d(date, day) { 237 return new Date(date).setDate(parseInt(day, 10)); 238 }, 239}; 240// format functions for date parts 241const formatFns = { 242 d(date) { 243 return date.getDate(); 244 }, 245 dd(date) { 246 return padZero(date.getDate(), 2); 247 }, 248 D(date, locale) { 249 return locale.daysShort[date.getDay()]; 250 }, 251 DD(date, locale) { 252 return locale.days[date.getDay()]; 253 }, 254 m(date) { 255 return date.getMonth() + 1; 256 }, 257 mm(date) { 258 return padZero(date.getMonth() + 1, 2); 259 }, 260 M(date, locale) { 261 return locale.monthsShort[date.getMonth()]; 262 }, 263 MM(date, locale) { 264 return locale.months[date.getMonth()]; 265 }, 266 y(date) { 267 return date.getFullYear(); 268 }, 269 yy(date) { 270 return padZero(date.getFullYear(), 2).slice(-2); 271 }, 272 yyyy(date) { 273 return padZero(date.getFullYear(), 4); 274 }, 275}; 276 277// get month index in normal range (0 - 11) from any number 278function normalizeMonth(monthIndex) { 279 return monthIndex > -1 ? monthIndex % 12 : normalizeMonth(monthIndex + 12); 280} 281 282function padZero(num, length) { 283 return num.toString().padStart(length, '0'); 284} 285 286function parseFormatString(format) { 287 if (typeof format !== 'string') { 288 throw new Error("Invalid date format."); 289 } 290 if (format in knownFormats) { 291 return knownFormats[format]; 292 } 293 294 // sprit the format string into parts and seprators 295 const separators = format.split(reFormatTokens); 296 const parts = format.match(new RegExp(reFormatTokens, 'g')); 297 if (separators.length === 0 || !parts) { 298 throw new Error("Invalid date format."); 299 } 300 301 // collect format functions used in the format 302 const partFormatters = parts.map(token => formatFns[token]); 303 304 // collect parse function keys used in the format 305 // iterate over parseFns' keys in order to keep the order of the keys. 306 const partParserKeys = Object.keys(parseFns).reduce((keys, key) => { 307 const token = parts.find(part => part[0] !== 'D' && part[0].toLowerCase() === key); 308 if (token) { 309 keys.push(key); 310 } 311 return keys; 312 }, []); 313 314 return knownFormats[format] = { 315 parser(dateStr, locale) { 316 const dateParts = dateStr.split(reNonDateParts).reduce((dtParts, part, index) => { 317 if (part.length > 0 && parts[index]) { 318 const token = parts[index][0]; 319 if (token === 'M') { 320 dtParts.m = part; 321 } else if (token !== 'D') { 322 dtParts[token] = part; 323 } 324 } 325 return dtParts; 326 }, {}); 327 328 // iterate over partParserkeys so that the parsing is made in the oder 329 // of year, month and day to prevent the day parser from correcting last 330 // day of month wrongly 331 return partParserKeys.reduce((origDate, key) => { 332 const newDate = parseFns[key](origDate, dateParts[key], locale); 333 // ingnore the part failed to parse 334 return isNaN(newDate) ? origDate : newDate; 335 }, today()); 336 }, 337 formatter(date, locale) { 338 let dateStr = partFormatters.reduce((str, fn, index) => { 339 return str += `${separators[index]}${fn(date, locale)}`; 340 }, ''); 341 // separators' length is always parts' length + 1, 342 return dateStr += lastItemOf(separators); 343 }, 344 }; 345} 346 347function parseDate(dateStr, format, locale) { 348 if (dateStr instanceof Date || typeof dateStr === 'number') { 349 const date = stripTime(dateStr); 350 return isNaN(date) ? undefined : date; 351 } 352 if (!dateStr) { 353 return undefined; 354 } 355 if (dateStr === 'today') { 356 return today(); 357 } 358 359 if (format && format.toValue) { 360 const date = format.toValue(dateStr, format, locale); 361 return isNaN(date) ? undefined : stripTime(date); 362 } 363 364 return parseFormatString(format).parser(dateStr, locale); 365} 366 367function formatDate(date, format, locale) { 368 if (isNaN(date) || (!date && date !== 0)) { 369 return ''; 370 } 371 372 const dateObj = typeof date === 'number' ? new Date(date) : date; 373 374 if (format.toDisplay) { 375 return format.toDisplay(dateObj, format, locale); 376 } 377 378 return parseFormatString(format).formatter(dateObj, locale); 379} 380
vendor: 64,513 bytes, lines 381-2508
381const range = document.createRange(); 382 383function parseHTML(html) { 384 return range.createContextualFragment(html); 385} 386 387function getParent(el) { 388 return el.parentElement 389 || (el.parentNode instanceof ShadowRoot ? el.parentNode.host : undefined); 390} 391 392function isActiveElement(el) { 393 return el.getRootNode().activeElement === el; 394} 395 396function hideElement(el) { 397 if (el.style.display === 'none') { 398 return; 399 } 400 // back up the existing display setting in data-style-display 401 if (el.style.display) { 402 el.dataset.styleDisplay = el.style.display; 403 } 404 el.style.display = 'none'; 405} 406 407function showElement(el) { 408 if (el.style.display !== 'none') { 409 return; 410 } 411 if (el.dataset.styleDisplay) { 412 // restore backed-up dispay property 413 el.style.display = el.dataset.styleDisplay; 414 delete el.dataset.styleDisplay; 415 } else { 416 el.style.display = ''; 417 } 418} 419 420function emptyChildNodes(el) { 421 if (el.firstChild) { 422 el.removeChild(el.firstChild); 423 emptyChildNodes(el); 424 } 425} 426 427function replaceChildNodes(el, newChildNodes) { 428 emptyChildNodes(el); 429 if (newChildNodes instanceof DocumentFragment) { 430 el.appendChild(newChildNodes); 431 } else if (typeof newChildNodes === 'string') { 432 el.appendChild(parseHTML(newChildNodes)); 433 } else if (typeof newChildNodes.forEach === 'function') { 434 newChildNodes.forEach((node) => { 435 el.appendChild(node); 436 }); 437 } 438} 439 440const listenerRegistry = new WeakMap(); 441const {addEventListener, removeEventListener} = EventTarget.prototype; 442 443// Register event listeners to a key object 444// listeners: array of listener definitions; 445// - each definition must be a flat array of event target and the arguments 446// used to call addEventListener() on the target 447function registerListeners(keyObj, listeners) { 448 let registered = listenerRegistry.get(keyObj); 449 if (!registered) { 450 registered = []; 451 listenerRegistry.set(keyObj, registered); 452 } 453 listeners.forEach((listener) => { 454 addEventListener.call(...listener); 455 registered.push(listener); 456 }); 457} 458 459function unregisterListeners(keyObj) { 460 let listeners = listenerRegistry.get(keyObj); 461 if (!listeners) { 462 return; 463 } 464 listeners.forEach((listener) => { 465 removeEventListener.call(...listener); 466 }); 467 listenerRegistry.delete(keyObj); 468} 469 470// Event.composedPath() polyfill for Edge 471// based on https://gist.github.com/kleinfreund/e9787d73776c0e3750dcfcdc89f100ec 472if (!Event.prototype.composedPath) { 473 const getComposedPath = (node, path = []) => { 474 path.push(node); 475 476 let parent; 477 if (node.parentNode) { 478 parent = node.parentNode; 479 } else if (node.host) { // ShadowRoot 480 parent = node.host; 481 } else if (node.defaultView) { // Document 482 parent = node.defaultView; 483 } 484 return parent ? getComposedPath(parent, path) : path; 485 }; 486 487 Event.prototype.composedPath = function () { 488 return getComposedPath(this.target); 489 }; 490} 491 492function findFromPath(path, criteria, currentTarget) { 493 const [node, ...rest] = path; 494 if (criteria(node)) { 495 return node; 496 } 497 if (node === currentTarget || node.tagName === 'HTML' || rest.length === 0) { 498 // stop when reaching currentTarget or <html> 499 return; 500 } 501 return findFromPath(rest, criteria, currentTarget); 502} 503 504// Search for the actual target of a delegated event 505function findElementInEventPath(ev, selector) { 506 const criteria = typeof selector === 'function' 507 ? selector 508 : el => el instanceof Element && el.matches(selector); 509 return findFromPath(ev.composedPath(), criteria, ev.currentTarget); 510} 511 512// default locales 513var locales = { 514 en: { 515 days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], 516 daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], 517 daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"], 518 months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], 519 monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], 520 today: "Today", 521 clear: "Clear", 522 titleFormat: "MM y" 523 } 524}; 525 526// config options updatable by setOptions() and their default values 527var defaultOptions = { 528 autohide: false, 529 beforeShowDay: null, 530 beforeShowDecade: null, 531 beforeShowMonth: null, 532 beforeShowYear: null, 533 clearButton: false, 534 dateDelimiter: ',', 535 datesDisabled: [], 536 daysOfWeekDisabled: [], 537 daysOfWeekHighlighted: [], 538 defaultViewDate: undefined, // placeholder, defaults to today() by the program 539 disableTouchKeyboard: false, 540 enableOnReadonly: true, 541 format: 'mm/dd/yyyy', 542 language: 'en', 543 maxDate: null, 544 maxNumberOfDates: 1, 545 maxView: 3, 546 minDate: null, 547 nextArrow: '»', 548 orientation: 'auto', 549 pickLevel: 0, 550 prevArrow: '«', 551 showDaysOfWeek: true, 552 showOnClick: true, 553 showOnFocus: true, 554 startView: 0, 555 title: '', 556 todayButton: false, 557 todayButtonMode: 0, 558 todayHighlight: false, 559 updateOnBlur: true, 560 weekNumbers: 0, 561 weekStart: 0, 562}; 563 564const { 565 language: defaultLang, 566 format: defaultFormat, 567 weekStart: defaultWeekStart, 568} = defaultOptions; 569 570// Reducer function to filter out invalid day-of-week from the input 571function sanitizeDOW(dow, day) { 572 return dow.length < 6 && day >= 0 && day < 7 573 ? pushUnique(dow, day) 574 : dow; 575} 576 577function determineGetWeekMethod(numberingMode, weekStart) { 578 const methodId = numberingMode === 4 579 ? (weekStart === 6 ? 3 : !weekStart + 1) 580 : numberingMode; 581 switch (methodId) { 582 case 1: 583 return getIsoWeek; 584 case 2: 585 return getWesternTradWeek; 586 case 3: 587 return getMidEasternWeek; 588 } 589} 590 591function updateWeekStart(newValue, config, weekNumbers) { 592 config.weekStart = newValue; 593 config.weekEnd = (newValue + 6) % 7; 594 if (weekNumbers === 4) { 595 config.getWeekNumber = determineGetWeekMethod(4, newValue); 596 } 597 return newValue; 598} 599 600// validate input date. if invalid, fallback to the original value 601function validateDate(value, format, locale, origValue) { 602 const date = parseDate(value, format, locale); 603 return date !== undefined ? date : origValue; 604} 605 606// Validate viewId. if invalid, fallback to the original value 607function validateViewId(value, origValue, max = 3) { 608 const viewId = parseInt(value, 10); 609 return viewId >= 0 && viewId <= max ? viewId : origValue; 610} 611 612function replaceOptions(options, from, to, convert = undefined) { 613 if (from in options) { 614 if (!(to in options)) { 615 options[to] = convert ? convert(options[from]) : options[from]; 616 } 617 delete options[from]; 618 } 619} 620 621// Create Datepicker configuration to set 622function processOptions(options, datepicker) { 623 const inOpts = Object.assign({}, options); 624 const config = {}; 625 const locales = datepicker.constructor.locales; 626 const rangeEnd = !!datepicker.rangeSideIndex; 627 let { 628 datesDisabled, 629 format, 630 language, 631 locale, 632 maxDate, 633 maxView, 634 minDate, 635 pickLevel, 636 startView, 637 weekNumbers, 638 weekStart, 639 } = datepicker.config || {}; 640 641 // for backword compatibility 642 replaceOptions(inOpts, 'calendarWeeks', 'weekNumbers', val => val ? 1 : 0); 643 replaceOptions(inOpts, 'clearBtn', 'clearButton'); 644 replaceOptions(inOpts, 'todayBtn', 'todayButton'); 645 replaceOptions(inOpts, 'todayBtnMode', 'todayButtonMode'); 646 647 if (inOpts.language) { 648 let lang; 649 if (inOpts.language !== language) { 650 if (locales[inOpts.language]) { 651 lang = inOpts.language; 652 } else { 653 // Check if langauge + region tag can fallback to the one without 654 // region (e.g. fr-CA â fr) 655 lang = inOpts.language.split('-')[0]; 656 if (!locales[lang]) { 657 lang = false; 658 } 659 } 660 } 661 delete inOpts.language; 662 if (lang) { 663 language = config.language = lang; 664 665 // update locale as well when updating language 666 const origLocale = locale || locales[defaultLang]; 667 // use default language's properties for the fallback 668 locale = Object.assign({ 669 format: defaultFormat, 670 weekStart: defaultWeekStart 671 }, locales[defaultLang]); 672 if (language !== defaultLang) { 673 Object.assign(locale, locales[language]); 674 } 675 config.locale = locale; 676 // if format and/or weekStart are the same as old locale's defaults, 677 // update them to new locale's defaults 678 if (format === origLocale.format) { 679 format = config.format = locale.format; 680 } 681 if (weekStart === origLocale.weekStart) { 682 weekStart = updateWeekStart(locale.weekStart, config, weekNumbers); 683 } 684 } 685 } 686 687 if (inOpts.format) { 688 const hasToDisplay = typeof inOpts.format.toDisplay === 'function'; 689 const hasToValue = typeof inOpts.format.toValue === 'function'; 690 const validFormatString = reFormatTokens.test(inOpts.format); 691 if ((hasToDisplay && hasToValue) || validFormatString) { 692 format = config.format = inOpts.format; 693 } 694 delete inOpts.format; 695 } 696 697 //*** pick level ***// 698 let newPickLevel = pickLevel; 699 if ('pickLevel' in inOpts) { 700 newPickLevel = validateViewId(inOpts.pickLevel, pickLevel, 2); 701 delete inOpts.pickLevel; 702 } 703 if (newPickLevel !== pickLevel) { 704 if (newPickLevel > pickLevel) { 705 // complement current minDate/madDate so that the existing range will be 706 // expanded to fit the new level later 707 if (!('minDate' in inOpts)) { 708 inOpts.minDate = minDate; 709 } 710 if (!('maxDate' in inOpts)) { 711 inOpts.maxDate = maxDate; 712 } 713 } 714 // complement datesDisabled so that it will be reset later 715 if (datesDisabled && !inOpts.datesDisabled) { 716 inOpts.datesDisabled = []; 717 } 718 pickLevel = config.pickLevel = newPickLevel; 719 } 720 721 //*** dates ***// 722 // while min and maxDate for "no limit" in the options are better to be null 723 // (especially when updating), the ones in the config have to be undefined 724 // because null is treated as 0 (= unix epoch) when comparing with time value 725 let minDt = minDate; 726 let maxDt = maxDate; 727 if ('minDate' in inOpts) { 728 const defaultMinDt = dateValue(0, 0, 1); 729 minDt = inOpts.minDate === null 730 ? defaultMinDt // set 0000-01-01 to prevent negative values for year 731 : validateDate(inOpts.minDate, format, locale, minDt); 732 if (minDt !== defaultMinDt) { 733 minDt = regularizeDate(minDt, pickLevel, false); 734 } 735 delete inOpts.minDate; 736 } 737 if ('maxDate' in inOpts) { 738 maxDt = inOpts.maxDate === null 739 ? undefined 740 : validateDate(inOpts.maxDate, format, locale, maxDt); 741 if (maxDt !== undefined) { 742 maxDt = regularizeDate(maxDt, pickLevel, true); 743 } 744 delete inOpts.maxDate; 745 } 746 if (maxDt < minDt) { 747 minDate = config.minDate = maxDt; 748 maxDate = config.maxDate = minDt; 749 } else { 750 if (minDate !== minDt) { 751 minDate = config.minDate = minDt; 752 } 753 if (maxDate !== maxDt) { 754 maxDate = config.maxDate = maxDt; 755 } 756 } 757 758 if (inOpts.datesDisabled) { 759 const dtsDisabled = inOpts.datesDisabled; 760 if (typeof dtsDisabled === 'function') { 761 config.datesDisabled = null; 762 config.checkDisabled = (timeValue, viewId) => dtsDisabled( 763 new Date(timeValue), 764 viewId, 765 rangeEnd 766 ); 767 } else { 768 const disabled = config.datesDisabled = dtsDisabled.reduce((dates, dt) => { 769 const date = parseDate(dt, format, locale); 770 return date !== undefined 771 ? pushUnique(dates, regularizeDate(date, pickLevel, rangeEnd)) 772 : dates; 773 }, []); 774 config.checkDisabled = timeValue => disabled.includes(timeValue); 775 } 776 delete inOpts.datesDisabled; 777 } 778 if ('defaultViewDate' in inOpts) { 779 const viewDate = parseDate(inOpts.defaultViewDate, format, locale); 780 if (viewDate !== undefined) { 781 config.defaultViewDate = viewDate; 782 } 783 delete inOpts.defaultViewDate; 784 } 785 786 //*** days of week ***// 787 if ('weekStart' in inOpts) { 788 const wkStart = Number(inOpts.weekStart) % 7; 789 if (!isNaN(wkStart)) { 790 weekStart = updateWeekStart(wkStart, config, weekNumbers); 791 } 792 delete inOpts.weekStart; 793 } 794 if (inOpts.daysOfWeekDisabled) { 795 config.daysOfWeekDisabled = inOpts.daysOfWeekDisabled.reduce(sanitizeDOW, []); 796 delete inOpts.daysOfWeekDisabled; 797 } 798 if (inOpts.daysOfWeekHighlighted) { 799 config.daysOfWeekHighlighted = inOpts.daysOfWeekHighlighted.reduce(sanitizeDOW, []); 800 delete inOpts.daysOfWeekHighlighted; 801 } 802 803 //*** week numbers ***// 804 if ('weekNumbers' in inOpts) { 805 let method = inOpts.weekNumbers; 806 if (method) { 807 const getWeekNumber = typeof method === 'function' 808 ? (timeValue, startOfWeek) => method(new Date(timeValue), startOfWeek) 809 : determineGetWeekMethod((method = parseInt(method, 10)), weekStart); 810 if (getWeekNumber) { 811 weekNumbers = config.weekNumbers = method; 812 config.getWeekNumber = getWeekNumber; 813 } 814 } else { 815 weekNumbers = config.weekNumbers = 0; 816 config.getWeekNumber = null; 817 } 818 delete inOpts.weekNumbers; 819 } 820 821 //*** multi date ***// 822 if ('maxNumberOfDates' in inOpts) { 823 const maxNumberOfDates = parseInt(inOpts.maxNumberOfDates, 10); 824 if (maxNumberOfDates >= 0) { 825 config.maxNumberOfDates = maxNumberOfDates; 826 config.multidate = maxNumberOfDates !== 1; 827 } 828 delete inOpts.maxNumberOfDates; 829 } 830 if (inOpts.dateDelimiter) { 831 config.dateDelimiter = String(inOpts.dateDelimiter); 832 delete inOpts.dateDelimiter; 833 } 834 835 //*** view ***// 836 let newMaxView = maxView; 837 if ('maxView' in inOpts) { 838 newMaxView = validateViewId(inOpts.maxView, maxView); 839 delete inOpts.maxView; 840 } 841 // ensure max view >= pick level 842 newMaxView = pickLevel > newMaxView ? pickLevel : newMaxView; 843 if (newMaxView !== maxView) { 844 maxView = config.maxView = newMaxView; 845 } 846 847 let newStartView = startView; 848 if ('startView' in inOpts) { 849 newStartView = validateViewId(inOpts.startView, newStartView); 850 delete inOpts.startView; 851 } 852 // ensure pick level <= start view <= max view 853 if (newStartView < pickLevel) { 854 newStartView = pickLevel; 855 } else if (newStartView > maxView) { 856 newStartView = maxView; 857 } 858 if (newStartView !== startView) { 859 config.startView = newStartView; 860 } 861 862 //*** template ***// 863 if (inOpts.prevArrow) { 864 const prevArrow = parseHTML(inOpts.prevArrow); 865 if (prevArrow.childNodes.length > 0) { 866 config.prevArrow = prevArrow.childNodes; 867 } 868 delete inOpts.prevArrow; 869 } 870 if (inOpts.nextArrow) { 871 const nextArrow = parseHTML(inOpts.nextArrow); 872 if (nextArrow.childNodes.length > 0) { 873 config.nextArrow = nextArrow.childNodes; 874 } 875 delete inOpts.nextArrow; 876 } 877 878 //*** misc ***// 879 if ('disableTouchKeyboard' in inOpts) { 880 config.disableTouchKeyboard = 'ontouchstart' in document && !!inOpts.disableTouchKeyboard; 881 delete inOpts.disableTouchKeyboard; 882 } 883 if (inOpts.orientation) { 884 const orientation = inOpts.orientation.toLowerCase().split(/\s+/g); 885 config.orientation = { 886 x: orientation.find(x => (x === 'left' || x === 'right')) || 'auto', 887 y: orientation.find(y => (y === 'top' || y === 'bottom')) || 'auto', 888 }; 889 delete inOpts.orientation; 890 } 891 if ('todayButtonMode' in inOpts) { 892 switch(inOpts.todayButtonMode) { 893 case 0: 894 case 1: 895 config.todayButtonMode = inOpts.todayButtonMode; 896 } 897 delete inOpts.todayButtonMode; 898 } 899 900 //*** copy the rest ***// 901 Object.entries(inOpts).forEach(([key, value]) => { 902 if (value !== undefined && key in defaultOptions) { 903 config[key] = value; 904 } 905 }); 906 907 return config; 908} 909 910const defaultShortcutKeys = { 911 show: {key: 'ArrowDown'}, 912 hide: null, 913 toggle: {key: 'Escape'}, 914 prevButton: {key: 'ArrowLeft', ctrlOrMetaKey: true}, 915 nextButton: {key: 'ArrowRight', ctrlOrMetaKey: true}, 916 viewSwitch: {key: 'ArrowUp', ctrlOrMetaKey: true}, 917 clearButton: {key: 'Backspace', ctrlOrMetaKey: true}, 918 todayButton: {key: '.', ctrlOrMetaKey: true}, 919 exitEditMode: {key: 'ArrowDown', ctrlOrMetaKey: true}, 920}; 921 922function createShortcutKeyConfig(options) { 923 return Object.keys(defaultShortcutKeys).reduce((keyDefs, shortcut) => { 924 const keyDef = options[shortcut] === undefined 925 ? defaultShortcutKeys[shortcut] 926 : options[shortcut]; 927 const key = keyDef && keyDef.key; 928 if (!key || typeof key !== 'string') { 929 return keyDefs; 930 } 931 932 const normalizedDef = { 933 key, 934 ctrlOrMetaKey: !!(keyDef.ctrlOrMetaKey || keyDef.ctrlKey || keyDef.metaKey), 935 }; 936 if (key.length > 1) { 937 normalizedDef.altKey = !!keyDef.altKey; 938 normalizedDef.shiftKey = !!keyDef.shiftKey; 939 } 940 keyDefs[shortcut] = normalizedDef; 941 return keyDefs; 942 }, {}); 943} 944 945const getButtons = buttonList => buttonList 946 .map(classes => `<button type="button" class="%buttonClass% ${classes}" tabindex="-1"></button>`) 947 .join(''); 948 949var pickerTemplate = optimizeTemplateHTML(`<div class="datepicker"> 950 <div class="datepicker-picker"> 951 <div class="datepicker-header"> 952 <div class="datepicker-title"></div> 953 <div class="datepicker-controls"> 954 ${getButtons([ 955 'prev-button prev-btn', 956 'view-switch', 957 'next-button next-btn', 958 ])} 959 </div> 960 </div> 961 <div class="datepicker-main"></div> 962 <div class="datepicker-footer"> 963 <div class="datepicker-controls"> 964 ${getButtons([ 965 'today-button today-btn', 966 'clear-button clear-btn', 967 ])} 968 </div> 969 </div> 970 </div> 971</div>`); 972 973var daysTemplate = optimizeTemplateHTML(`<div class="days"> 974 <div class="days-of-week">${createTagRepeat('span', 7, {class: 'dow'})}</div> 975 <div class="datepicker-grid">${createTagRepeat('span', 42)}</div> 976</div>`); 977 978var weekNumbersTemplate = optimizeTemplateHTML(`<div class="week-numbers calendar-weeks"> 979 <div class="days-of-week"><span class="dow"></span></div> 980 <div class="weeks">${createTagRepeat('span', 6, {class: 'week'})}</div> 981</div>`); 982 983// Base class of the view classes 984class View { 985 constructor(picker, config) { 986 Object.assign(this, config, { 987 picker, 988 element: parseHTML(`<div class="datepicker-view"></div>`).firstChild, 989 selected: [], 990 isRangeEnd: !!picker.datepicker.rangeSideIndex, 991 }); 992 this.init(this.picker.datepicker.config); 993 } 994 995 init(options) { 996 if ('pickLevel' in options) { 997 this.isMinView = this.id === options.pickLevel; 998 } 999 this.setOptions(options); 1000 this.updateFocus(); 1001 this.updateSelection(); 1002 } 1003 1004 prepareForRender(switchLabel, prevButtonDisabled, nextButtonDisabled) { 1005 // refresh disabled years on every render in order to clear the ones added 1006 // by beforeShow hook at previous render 1007 this.disabled = []; 1008 1009 const picker = this.picker; 1010 picker.setViewSwitchLabel(switchLabel); 1011 picker.setPrevButtonDisabled(prevButtonDisabled); 1012 picker.setNextButtonDisabled(nextButtonDisabled); 1013 } 1014 1015 setDisabled(date, classList) { 1016 classList.add('disabled'); 1017 pushUnique(this.disabled, date); 1018 } 1019 1020 // Execute beforeShow() callback and apply the result to the element 1021 // args: 1022 performBeforeHook(el, timeValue) { 1023 let result = this.beforeShow(new Date(timeValue)); 1024 switch (typeof result) { 1025 case 'boolean': 1026 result = {enabled: result}; 1027 break; 1028 case 'string': 1029 result = {classes: result}; 1030 } 1031 1032 if (result) { 1033 const classList = el.classList; 1034 if (result.enabled === false) { 1035 this.setDisabled(timeValue, classList); 1036 } 1037 if (result.classes) { 1038 const extraClasses = result.classes.split(/\s+/); 1039 classList.add(...extraClasses); 1040 if (extraClasses.includes('disabled')) { 1041 this.setDisabled(timeValue, classList); 1042 } 1043 } 1044 if (result.content) { 1045 replaceChildNodes(el, result.content); 1046 } 1047 } 1048 } 1049 1050 renderCell(el, content, cellVal, date, {selected, range}, outOfScope, extraClasses = []) { 1051 el.textContent = content; 1052 if (this.isMinView) { 1053 el.dataset.date = date; 1054 } 1055 1056 const classList = el.classList; 1057 el.className = `datepicker-cell ${this.cellClass}`; 1058 if (cellVal < this.first) { 1059 classList.add('prev'); 1060 } else if (cellVal > this.last) { 1061 classList.add('next'); 1062 } 1063 classList.add(...extraClasses); 1064 if (outOfScope || this.checkDisabled(date, this.id)) { 1065 this.setDisabled(date, classList); 1066 } 1067 if (range) { 1068 const [rangeStart, rangeEnd] = range; 1069 if (cellVal > rangeStart && cellVal < rangeEnd) { 1070 classList.add('range'); 1071 } 1072 if (cellVal === rangeStart) { 1073 classList.add('range-start'); 1074 } 1075 if (cellVal === rangeEnd) { 1076 classList.add('range-end'); 1077 } 1078 } 1079 if (selected.includes(cellVal)) { 1080 classList.add('selected'); 1081 } 1082 if (cellVal === this.focused) { 1083 classList.add('focused'); 1084 } 1085 1086 if (this.beforeShow) { 1087 this.performBeforeHook(el, date); 1088 } 1089 } 1090 1091 refreshCell(el, cellVal, selected, [rangeStart, rangeEnd]) { 1092 const classList = el.classList; 1093 classList.remove('range', 'range-start', 'range-end', 'selected', 'focused'); 1094 if (cellVal > rangeStart && cellVal < rangeEnd) { 1095 classList.add('range'); 1096 } 1097 if (cellVal === rangeStart) { 1098 classList.add('range-start'); 1099 } 1100 if (cellVal === rangeEnd) { 1101 classList.add('range-end'); 1102 } 1103 if (selected.includes(cellVal)) { 1104 classList.add('selected'); 1105 } 1106 if (cellVal === this.focused) { 1107 classList.add('focused'); 1108 } 1109 } 1110 1111 changeFocusedCell(cellIndex) { 1112 this.grid.querySelectorAll('.focused').forEach((el) => { 1113 el.classList.remove('focused'); 1114 }); 1115 this.grid.children[cellIndex].classList.add('focused'); 1116 } 1117} 1118 1119class DaysView extends View { 1120 constructor(picker) { 1121 super(picker, { 1122 id: 0, 1123 name: 'days', 1124 cellClass: 'day', 1125 }); 1126 } 1127 1128 init(options, onConstruction = true) { 1129 if (onConstruction) { 1130 const inner = parseHTML(daysTemplate).firstChild; 1131 this.dow = inner.firstChild; 1132 this.grid = inner.lastChild; 1133 this.element.appendChild(inner); 1134 } 1135 super.init(options); 1136 } 1137 1138 setOptions(options) { 1139 let updateDOW; 1140 1141 if ('minDate' in options) { 1142 this.minDate = options.minDate; 1143 } 1144 if ('maxDate' in options) { 1145 this.maxDate = options.maxDate; 1146 } 1147 if (options.checkDisabled) { 1148 this.checkDisabled = options.checkDisabled; 1149 } 1150 if (options.daysOfWeekDisabled) { 1151 this.daysOfWeekDisabled = options.daysOfWeekDisabled; 1152 updateDOW = true; 1153 } 1154 if (options.daysOfWeekHighlighted) { 1155 this.daysOfWeekHighlighted = options.daysOfWeekHighlighted; 1156 } 1157 if ('todayHighlight' in options) { 1158 this.todayHighlight = options.todayHighlight; 1159 } 1160 if ('weekStart' in options) { 1161 this.weekStart = options.weekStart; 1162 this.weekEnd = options.weekEnd; 1163 updateDOW = true; 1164 } 1165 if (options.locale) { 1166 const locale = this.locale = options.locale; 1167 this.dayNames = locale.daysMin; 1168 this.switchLabelFormat = locale.titleFormat; 1169 updateDOW = true; 1170 } 1171 if ('beforeShowDay' in options) { 1172 this.beforeShow = typeof options.beforeShowDay === 'function' 1173 ? options.beforeShowDay 1174 : undefined; 1175 } 1176 1177 if ('weekNumbers' in options) { 1178 if (options.weekNumbers && !this.weekNumbers) { 1179 const weeksElem = parseHTML(weekNumbersTemplate).firstChild; 1180 this.weekNumbers = { 1181 element: weeksElem, 1182 dow: weeksElem.firstChild, 1183 weeks: weeksElem.lastChild, 1184 }; 1185 this.element.insertBefore(weeksElem, this.element.firstChild); 1186 } else if (this.weekNumbers && !options.weekNumbers) { 1187 this.element.removeChild(this.weekNumbers.element); 1188 this.weekNumbers = null; 1189 } 1190 } 1191 1192 if ('getWeekNumber' in options) { 1193 this.getWeekNumber = options.getWeekNumber; 1194 } 1195 1196 if ('showDaysOfWeek' in options) { 1197 if (options.showDaysOfWeek) { 1198 showElement(this.dow); 1199 if (this.weekNumbers) { 1200 showElement(this.weekNumbers.dow); 1201 } 1202 } else { 1203 hideElement(this.dow); 1204 if (this.weekNumbers) { 1205 hideElement(this.weekNumbers.dow); 1206 } 1207 } 1208 } 1209 1210 // update days-of-week when locale, daysOfweekDisabled or weekStart is changed 1211 if (updateDOW) { 1212 Array.from(this.dow.children).forEach((el, index) => { 1213 const dow = (this.weekStart + index) % 7; 1214 el.textContent = this.dayNames[dow]; 1215 el.className = this.daysOfWeekDisabled.includes(dow) ? 'dow disabled' : 'dow'; 1216 }); 1217 } 1218 } 1219 1220 // Apply update on the focused date to view's settings 1221 updateFocus() { 1222 const viewDate = new Date(this.picker.viewDate); 1223 const viewYear = viewDate.getFullYear(); 1224 const viewMonth = viewDate.getMonth(); 1225 const firstOfMonth = dateValue(viewYear, viewMonth, 1); 1226 const start = dayOfTheWeekOf(firstOfMonth, this.weekStart, this.weekStart); 1227 1228 this.first = firstOfMonth; 1229 this.last = dateValue(viewYear, viewMonth + 1, 0); 1230 this.start = start; 1231 this.focused = this.picker.viewDate; 1232 } 1233 1234 // Apply update on the selected dates to view's settings 1235 updateSelection() { 1236 const {dates, rangepicker} = this.picker.datepicker; 1237 this.selected = dates; 1238 if (rangepicker) { 1239 this.range = rangepicker.dates; 1240 } 1241 } 1242 1243 // Update the entire view UI 1244 render() { 1245 // update today marker on ever render 1246 this.today = this.todayHighlight ? today() : undefined; 1247 1248 this.prepareForRender( 1249 formatDate(this.focused, this.switchLabelFormat, this.locale), 1250 this.first <= this.minDate, 1251 this.last >= this.maxDate 1252 ); 1253 1254 if (this.weekNumbers) { 1255 const weekStart = this.weekStart; 1256 const startOfWeek = dayOfTheWeekOf(this.first, weekStart, weekStart); 1257 Array.from(this.weekNumbers.weeks.children).forEach((el, index) => { 1258 const dateOfWeekStart = addWeeks(startOfWeek, index); 1259 el.textContent = this.getWeekNumber(dateOfWeekStart, weekStart); 1260 if (index > 3) { 1261 el.classList[dateOfWeekStart > this.last ? 'add' : 'remove']('next'); 1262 } 1263 }); 1264 } 1265 Array.from(this.grid.children).forEach((el, index) => { 1266 const current = addDays(this.start, index); 1267 const dateObj = new Date(current); 1268 const day = dateObj.getDay(); 1269 const extraClasses = []; 1270 1271 if (this.today === current) { 1272 extraClasses.push('today'); 1273 } 1274 if (this.daysOfWeekHighlighted.includes(day)) { 1275 extraClasses.push('highlighted'); 1276 } 1277 1278 this.renderCell( 1279 el, 1280 dateObj.getDate(), 1281 current, 1282 current, 1283 this, 1284 current < this.minDate 1285 || current > this.maxDate 1286 || this.daysOfWeekDisabled.includes(day), 1287 extraClasses 1288 ); 1289 }); 1290 } 1291 1292 // Update the view UI by applying the changes of selected and focused items 1293 refresh() { 1294 const range = this.range || []; 1295 Array.from(this.grid.children).forEach((el) => { 1296 this.refreshCell(el, Number(el.dataset.date), this.selected, range); 1297 }); 1298 } 1299 1300 // Update the view UI by applying the change of focused item 1301 refreshFocus() { 1302 this.changeFocusedCell(Math.round((this.focused - this.start) / 86400000)); 1303 } 1304} 1305 1306function computeMonthRange(range, thisYear) { 1307 if (!range || !range[0] || !range[1]) { 1308 return; 1309 } 1310 1311 const [[startY, startM], [endY, endM]] = range; 1312 if (startY > thisYear || endY < thisYear) { 1313 return; 1314 } 1315 return [ 1316 startY === thisYear ? startM : -1, 1317 endY === thisYear ? endM : 12, 1318 ]; 1319} 1320 1321class MonthsView extends View { 1322 constructor(picker) { 1323 super(picker, { 1324 id: 1, 1325 name: 'months', 1326 cellClass: 'month', 1327 }); 1328 } 1329 1330 init(options, onConstruction = true) { 1331 if (onConstruction) { 1332 this.grid = this.element; 1333 this.element.classList.add('months', 'datepicker-grid'); 1334 this.grid.appendChild(parseHTML(createTagRepeat('span', 12, {'data-month': ix => ix}))); 1335 this.first = 0; 1336 this.last = 11; 1337 } 1338 super.init(options); 1339 } 1340 1341 setOptions(options) { 1342 if (options.locale) { 1343 this.monthNames = options.locale.monthsShort; 1344 } 1345 if ('minDate' in options) { 1346 if (options.minDate === undefined) { 1347 this.minYear = this.minMonth = this.minDate = undefined; 1348 } else { 1349 const minDateObj = new Date(options.minDate); 1350 this.minYear = minDateObj.getFullYear(); 1351 this.minMonth = minDateObj.getMonth(); 1352 this.minDate = minDateObj.setDate(1); 1353 } 1354 } 1355 if ('maxDate' in options) { 1356 if (options.maxDate === undefined) { 1357 this.maxYear = this.maxMonth = this.maxDate = undefined; 1358 } else { 1359 const maxDateObj = new Date(options.maxDate); 1360 this.maxYear = maxDateObj.getFullYear(); 1361 this.maxMonth = maxDateObj.getMonth(); 1362 this.maxDate = dateValue(this.maxYear, this.maxMonth + 1, 0); 1363 } 1364 } 1365 if (options.checkDisabled) { 1366 this.checkDisabled = this.isMinView || options.datesDisabled === null 1367 ? options.checkDisabled 1368 : () => false; 1369 } 1370 if ('beforeShowMonth' in options) { 1371 this.beforeShow = typeof options.beforeShowMonth === 'function' 1372 ? options.beforeShowMonth 1373 : undefined; 1374 } 1375 } 1376 1377 // Update view's settings to reflect the viewDate set on the picker 1378 updateFocus() { 1379 const viewDate = new Date(this.picker.viewDate); 1380 this.year = viewDate.getFullYear(); 1381 this.focused = viewDate.getMonth(); 1382 } 1383 1384 // Update view's settings to reflect the selected dates 1385 updateSelection() { 1386 const {dates, rangepicker} = this.picker.datepicker; 1387 this.selected = dates.reduce((selected, timeValue) => { 1388 const date = new Date(timeValue); 1389 const year = date.getFullYear(); 1390 const month = date.getMonth(); 1391 if (selected[year] === undefined) { 1392 selected[year] = [month]; 1393 } else { 1394 pushUnique(selected[year], month); 1395 } 1396 return selected; 1397 }, {}); 1398 if (rangepicker && rangepicker.dates) { 1399 this.range = rangepicker.dates.map(timeValue => { 1400 const date = new Date(timeValue); 1401 return isNaN(date) ? undefined : [date.getFullYear(), date.getMonth()]; 1402 }); 1403 } 1404 } 1405 1406 // Update the entire view UI 1407 render() { 1408 this.prepareForRender( 1409 this.year, 1410 this.year <= this.minYear, 1411 this.year >= this.maxYear 1412 ); 1413 1414 const selected = this.selected[this.year] || []; 1415 const yrOutOfRange = this.year < this.minYear || this.year > this.maxYear; 1416 const isMinYear = this.year === this.minYear; 1417 const isMaxYear = this.year === this.maxYear; 1418 const range = computeMonthRange(this.range, this.year); 1419 1420 Array.from(this.grid.children).forEach((el, index) => { 1421 const date = regularizeDate(new Date(this.year, index, 1), 1, this.isRangeEnd); 1422 1423 this.renderCell( 1424 el, 1425 this.monthNames[index], 1426 index, 1427 date, 1428 {selected, range}, 1429 yrOutOfRange 1430 || isMinYear && index < this.minMonth 1431 || isMaxYear && index > this.maxMonth 1432 ); 1433 }); 1434 } 1435 1436 // Update the view UI by applying the changes of selected and focused items 1437 refresh() { 1438 const selected = this.selected[this.year] || []; 1439 const range = computeMonthRange(this.range, this.year) || []; 1440 Array.from(this.grid.children).forEach((el, index) => { 1441 this.refreshCell(el, index, selected, range); 1442 }); 1443 } 1444 1445 // Update the view UI by applying the change of focused item 1446 refreshFocus() { 1447 this.changeFocusedCell(this.focused); 1448 } 1449} 1450 1451function toTitleCase(word) { 1452 return [...word].reduce((str, ch, ix) => str += ix ? ch : ch.toUpperCase(), ''); 1453} 1454 1455// Class representing the years and decades view elements 1456class YearsView extends View { 1457 constructor(picker, config) { 1458 super(picker, config); 1459 } 1460 1461 init(options, onConstruction = true) { 1462 if (onConstruction) { 1463 this.navStep = this.step * 10; 1464 this.beforeShowOption = `beforeShow${toTitleCase(this.cellClass)}`; 1465 this.grid = this.element; 1466 this.element.classList.add(this.name, 'datepicker-grid'); 1467 this.grid.appendChild(parseHTML(createTagRepeat('span', 12))); 1468 } 1469 super.init(options); 1470 } 1471 1472 setOptions(options) { 1473 if ('minDate' in options) { 1474 if (options.minDate === undefined) { 1475 this.minYear = this.minDate = undefined; 1476 } else { 1477 this.minYear = startOfYearPeriod(options.minDate, this.step); 1478 this.minDate = dateValue(this.minYear, 0, 1); 1479 } 1480 } 1481 if ('maxDate' in options) { 1482 if (options.maxDate === undefined) { 1483 this.maxYear = this.maxDate = undefined; 1484 } else { 1485 this.maxYear = startOfYearPeriod(options.maxDate, this.step); 1486 this.maxDate = dateValue(this.maxYear, 11, 31); 1487 } 1488 } 1489 if (options.checkDisabled) { 1490 this.checkDisabled = this.isMinView || options.datesDisabled === null 1491 ? options.checkDisabled 1492 : () => false; 1493 } 1494 if (this.beforeShowOption in options) { 1495 const beforeShow = options[this.beforeShowOption]; 1496 this.beforeShow = typeof beforeShow === 'function' ? beforeShow : undefined; 1497 } 1498 } 1499 1500 // Update view's settings to reflect the viewDate set on the picker 1501 updateFocus() { 1502 const viewDate = new Date(this.picker.viewDate); 1503 const first = startOfYearPeriod(viewDate, this.navStep); 1504 const last = first + 9 * this.step; 1505 1506 this.first = first; 1507 this.last = last; 1508 this.start = first - this.step; 1509 this.focused = startOfYearPeriod(viewDate, this.step); 1510 } 1511 1512 // Update view's settings to reflect the selected dates 1513 updateSelection() { 1514 const {dates, rangepicker} = this.picker.datepicker; 1515 this.selected = dates.reduce((years, timeValue) => { 1516 return pushUnique(years, startOfYearPeriod(timeValue, this.step)); 1517 }, []); 1518 if (rangepicker && rangepicker.dates) { 1519 this.range = rangepicker.dates.map(timeValue => { 1520 if (timeValue !== undefined) { 1521 return startOfYearPeriod(timeValue, this.step); 1522 } 1523 }); 1524 } 1525 } 1526 1527 // Update the entire view UI 1528 render() { 1529 this.prepareForRender( 1530 `${this.first}-${this.last}`, 1531 this.first <= this.minYear, 1532 this.last >= this.maxYear 1533 ); 1534 1535 Array.from(this.grid.children).forEach((el, index) => { 1536 const current = this.start + (index * this.step); 1537 const date = regularizeDate(new Date(current, 0, 1), 2, this.isRangeEnd); 1538 1539 el.dataset.year = current; 1540 this.renderCell( 1541 el, 1542 current, 1543 current, 1544 date, 1545 this, 1546 current < this.minYear || current > this.maxYear 1547 ); 1548 }); 1549 } 1550 1551 // Update the view UI by applying the changes of selected and focused items 1552 refresh() { 1553 const range = this.range || []; 1554 Array.from(this.grid.children).forEach((el) => { 1555 this.refreshCell(el, Number(el.textContent), this.selected, range); 1556 }); 1557 } 1558 1559 // Update the view UI by applying the change of focused item 1560 refreshFocus() { 1561 this.changeFocusedCell(Math.round((this.focused - this.start) / this.step)); 1562 } 1563} 1564 1565function triggerDatepickerEvent(datepicker, type) { 1566 const options = { 1567 bubbles: true, 1568 cancelable: true, 1569 detail: { 1570 date: datepicker.getDate(), 1571 viewDate: new Date(datepicker.picker.viewDate), 1572 viewId: datepicker.picker.currentView.id, 1573 datepicker, 1574 }, 1575 }; 1576 datepicker.element.dispatchEvent(new CustomEvent(type, options)); 1577} 1578 1579// direction: -1 (to previous), 1 (to next) 1580function goToPrevOrNext(datepicker, direction) { 1581 const {config, picker} = datepicker; 1582 const {currentView, viewDate} = picker; 1583 let newViewDate; 1584 switch (currentView.id) { 1585 case 0: 1586 newViewDate = addMonths(viewDate, direction); 1587 break; 1588 case 1: 1589 newViewDate = addYears(viewDate, direction); 1590 break; 1591 default: 1592 newViewDate = addYears(viewDate, direction * currentView.navStep); 1593 } 1594 newViewDate = limitToRange(newViewDate, config.minDate, config.maxDate); 1595 picker.changeFocus(newViewDate).render(); 1596} 1597 1598function switchView(datepicker) { 1599 const viewId = datepicker.picker.currentView.id; 1600 if (viewId === datepicker.config.maxView) { 1601 return; 1602 } 1603 datepicker.picker.changeView(viewId + 1).render(); 1604} 1605 1606function clearSelection(datepicker) { 1607 datepicker.setDate({clear: true}); 1608} 1609 1610function goToOrSelectToday(datepicker) { 1611 const currentDate = today(); 1612 if (datepicker.config.todayButtonMode === 1) { 1613 datepicker.setDate(currentDate, {forceRefresh: true, viewDate: currentDate}); 1614 } else { 1615 datepicker.setFocusedDate(currentDate, true); 1616 } 1617} 1618 1619function unfocus(datepicker) { 1620 const onBlur = () => { 1621 if (datepicker.config.updateOnBlur) { 1622 datepicker.update({revert: true}); 1623 } else { 1624 datepicker.refresh('input'); 1625 } 1626 datepicker.hide(); 1627 }; 1628 const element = datepicker.element; 1629 1630 if (isActiveElement(element)) { 1631 element.addEventListener('blur', onBlur, {once: true}); 1632 } else { 1633 onBlur(); 1634 } 1635} 1636 1637function goToSelectedMonthOrYear(datepicker, selection) { 1638 const picker = datepicker.picker; 1639 const viewDate = new Date(picker.viewDate); 1640 const viewId = picker.currentView.id; 1641 const newDate = viewId === 1 1642 ? addMonths(viewDate, selection - viewDate.getMonth()) 1643 : addYears(viewDate, selection - viewDate.getFullYear()); 1644 1645 picker.changeFocus(newDate).changeView(viewId - 1).render(); 1646} 1647 1648function onClickViewSwitch(datepicker) { 1649 switchView(datepicker); 1650} 1651 1652function onClickPrevButton(datepicker) { 1653 goToPrevOrNext(datepicker, -1); 1654} 1655 1656function onClickNextButton(datepicker) { 1657 goToPrevOrNext(datepicker, 1); 1658} 1659 1660// For the picker's main block to delegete the events from `datepicker-cell`s 1661function onClickView(datepicker, ev) { 1662 const target = findElementInEventPath(ev, '.datepicker-cell'); 1663 if (!target || target.classList.contains('disabled')) { 1664 return; 1665 } 1666 1667 const {id, isMinView} = datepicker.picker.currentView; 1668 const data = target.dataset; 1669 if (isMinView) { 1670 datepicker.setDate(Number(data.date)); 1671 } else if (id === 1) { 1672 goToSelectedMonthOrYear(datepicker, Number(data.month)); 1673 } else { 1674 goToSelectedMonthOrYear(datepicker, Number(data.year)); 1675 } 1676} 1677 1678function onMousedownPicker(ev) { 1679 ev.preventDefault(); 1680} 1681 1682const orientClasses = ['left', 'top', 'right', 'bottom'].reduce((obj, key) => { 1683 obj[key] = `datepicker-orient-${key}`; 1684 return obj; 1685}, {}); 1686const toPx = num => num ? `${num}px` : num; 1687 1688function processPickerOptions(picker, options) { 1689 if ('title' in options) { 1690 if (options.title) { 1691 picker.controls.title.textContent = options.title; 1692 showElement(picker.controls.title); 1693 } else { 1694 picker.controls.title.textContent = ''; 1695 hideElement(picker.controls.title); 1696 } 1697 } 1698 if (options.prevArrow) { 1699 const prevButton = picker.controls.prevButton; 1700 emptyChildNodes(prevButton); 1701 options.prevArrow.forEach((node) => { 1702 prevButton.appendChild(node.cloneNode(true)); 1703 }); 1704 } 1705 if (options.nextArrow) { 1706 const nextButton = picker.controls.nextButton; 1707 emptyChildNodes(nextButton); 1708 options.nextArrow.forEach((node) => { 1709 nextButton.appendChild(node.cloneNode(true)); 1710 }); 1711 } 1712 if (options.locale) { 1713 picker.controls.todayButton.textContent = options.locale.today; 1714 picker.controls.clearButton.textContent = options.locale.clear; 1715 } 1716 if ('todayButton' in options) { 1717 if (options.todayButton) { 1718 showElement(picker.controls.todayButton); 1719 } else { 1720 hideElement(picker.controls.todayButton); 1721 } 1722 } 1723 if ('minDate' in options || 'maxDate' in options) { 1724 const {minDate, maxDate} = picker.datepicker.config; 1725 picker.controls.todayButton.disabled = !isInRange(today(), minDate, maxDate); 1726 } 1727 if ('clearButton' in options) { 1728 if (options.clearButton) { 1729 showElement(picker.controls.clearButton); 1730 } else { 1731 hideElement(picker.controls.clearButton); 1732 } 1733 } 1734} 1735 1736// Compute view date to reset, which will be... 1737// - the last item of the selected dates or defaultViewDate if no selection 1738// - limitted to minDate or maxDate if it exceeds the range 1739function computeResetViewDate(datepicker) { 1740 const {dates, config, rangeSideIndex} = datepicker; 1741 const viewDate = dates.length > 0 1742 ? lastItemOf(dates) 1743 : regularizeDate(config.defaultViewDate, config.pickLevel, rangeSideIndex); 1744 return limitToRange(viewDate, config.minDate, config.maxDate); 1745} 1746 1747// Change current view's view date 1748function setViewDate(picker, newDate) { 1749 if (!('_oldViewDate' in picker) && newDate !== picker.viewDate) { 1750 picker._oldViewDate = picker.viewDate; 1751 } 1752 picker.viewDate = newDate; 1753 1754 // return whether the new date is in different period on time from the one 1755 // displayed in the current view 1756 // when true, the view needs to be re-rendered on the next UI refresh. 1757 const {id, year, first, last} = picker.currentView; 1758 const viewYear = new Date(newDate).getFullYear(); 1759 switch (id) { 1760 case 0: 1761 return newDate < first || newDate > last; 1762 case 1: 1763 return viewYear !== year; 1764 default: 1765 return viewYear < first || viewYear > last; 1766 } 1767} 1768 1769function getTextDirection(el) { 1770 return window.getComputedStyle(el).direction; 1771} 1772 1773// find the closet scrollable ancestor elemnt under the body 1774function findScrollParents(el) { 1775 const parent = getParent(el); 1776 if (parent === document.body || !parent) { 1777 return; 1778 } 1779 1780 // checking overflow only is enough because computed overflow cannot be 1781 // visible or a combination of visible and other when either axis is set 1782 // to other than visible. 1783 // (Setting one axis to other than 'visible' while the other is 'visible' 1784 // results in the other axis turning to 'auto') 1785 return window.getComputedStyle(parent).overflow !== 'visible' 1786 ? parent 1787 : findScrollParents(parent); 1788} 1789 1790// Class representing the picker UI 1791class Picker { 1792 constructor(datepicker) { 1793 const {config, inputField} = this.datepicker = datepicker; 1794 1795 const template = pickerTemplate.replace(/%buttonClass%/g, config.buttonClass); 1796 const element = this.element = parseHTML(template).firstChild; 1797 const [header, main, footer] = element.firstChild.children; 1798 const title = header.firstElementChild; 1799 const [prevButton, viewSwitch, nextButton] = header.lastElementChild.children; 1800 const [todayButton, clearButton] = footer.firstChild.children; 1801 const controls = { 1802 title, 1803 prevButton, 1804 viewSwitch, 1805 nextButton, 1806 todayButton, 1807 clearButton, 1808 }; 1809 this.main = main; 1810 this.controls = controls; 1811 1812 const elementClass = inputField ? 'dropdown' : 'inline'; 1813 element.classList.add(`datepicker-${elementClass}`); 1814 1815 processPickerOptions(this, config); 1816 this.viewDate = computeResetViewDate(datepicker); 1817 1818 // set up event listeners 1819 registerListeners(datepicker, [ 1820 [element, 'mousedown', onMousedownPicker], 1821 [main, 'click', onClickView.bind(null, datepicker)], 1822 [controls.viewSwitch, 'click', onClickViewSwitch.bind(null, datepicker)], 1823 [controls.prevButton, 'click', onClickPrevButton.bind(null, datepicker)], 1824 [controls.nextButton, 'click', onClickNextButton.bind(null, datepicker)], 1825 [controls.todayButton, 'click', goToOrSelectToday.bind(null, datepicker)], 1826 [controls.clearButton, 'click', clearSelection.bind(null, datepicker)], 1827 ]); 1828 1829 // set up views 1830 this.views = [ 1831 new DaysView(this), 1832 new MonthsView(this), 1833 new YearsView(this, {id: 2, name: 'years', cellClass: 'year', step: 1}), 1834 new YearsView(this, {id: 3, name: 'decades', cellClass: 'decade', step: 10}), 1835 ]; 1836 this.currentView = this.views[config.startView]; 1837 1838 this.currentView.render(); 1839 this.main.appendChild(this.currentView.element); 1840 if (config.container) { 1841 config.container.appendChild(this.element); 1842 } else { 1843 inputField.after(this.element); 1844 } 1845 } 1846 1847 setOptions(options) { 1848 processPickerOptions(this, options); 1849 this.views.forEach((view) => { 1850 view.init(options, false); 1851 }); 1852 this.currentView.render(); 1853 } 1854 1855 detach() { 1856 this.element.remove(); 1857 } 1858 1859 show() { 1860 if (this.active) { 1861 return; 1862 } 1863 1864 const {datepicker, element} = this; 1865 const inputField = datepicker.inputField; 1866 if (inputField) { 1867 // ensure picker's direction matches input's 1868 const inputDirection = getTextDirection(inputField); 1869 if (inputDirection !== getTextDirection(getParent(element))) { 1870 element.dir = inputDirection; 1871 } else if (element.dir) { 1872 element.removeAttribute('dir'); 1873 } 1874 1875 // Determine the picker's position first to prevent `orientation: 'auto'` 1876 // from being miscalculated to 'bottom' after the picker temporarily 1877 // shown below the input field expands the document height if the field 1878 // is at the bottom edge of the document 1879 this.place(); 1880 element.classList.add('active'); 1881 1882 if (datepicker.config.disableTouchKeyboard) { 1883 inputField.blur(); 1884 } 1885 } else { 1886 element.classList.add('active'); 1887 } 1888 this.active = true; 1889 triggerDatepickerEvent(datepicker, 'show'); 1890 } 1891 1892 hide() { 1893 if (!this.active) { 1894 return; 1895 } 1896 this.datepicker.exitEditMode(); 1897 this.element.classList.remove('active'); 1898 this.active = false; 1899 triggerDatepickerEvent(this.datepicker, 'hide'); 1900 } 1901 1902 place() { 1903 const {classList, style} = this.element; 1904 // temporarily display the picker to get its size and offset parent 1905 style.display = 'block'; 1906 1907 const { 1908 width: calendarWidth, 1909 height: calendarHeight, 1910 } = this.element.getBoundingClientRect(); 1911 const offsetParent = this.element.offsetParent; 1912 // turn the picker back to hidden so that the position is determined with 1913 // the state before it is shown. 1914 style.display = ''; 1915 1916 const {config, inputField} = this.datepicker; 1917 const { 1918 left: inputLeft, 1919 top: inputTop, 1920 right: inputRight, 1921 bottom: inputBottom, 1922 width: inputWidth, 1923 height: inputHeight 1924 } = inputField.getBoundingClientRect(); 1925 let {x: orientX, y: orientY} = config.orientation; 1926 let left = inputLeft; 1927 let top = inputTop; 1928 1929 // caliculate offsetLeft/Top of inputField 1930 if (offsetParent === document.body || !offsetParent) { 1931 left += window.scrollX; 1932 top += window.scrollY; 1933 } else { 1934 const offsetParentRect = offsetParent.getBoundingClientRect(); 1935 left -= offsetParentRect.left - offsetParent.scrollLeft; 1936 top -= offsetParentRect.top - offsetParent.scrollTop; 1937 } 1938 1939 // caliculate the boundaries of the visible area that contains inputField 1940 const scrollParent = findScrollParents(inputField); 1941 let scrollAreaLeft = 0; 1942 let scrollAreaTop = 0; 1943 let { 1944 clientWidth: scrollAreaRight, 1945 clientHeight: scrollAreaBottom, 1946 } = document.documentElement; 1947 1948 if (scrollParent) { 1949 const scrollParentRect = scrollParent.getBoundingClientRect(); 1950 if (scrollParentRect.top > 0) { 1951 scrollAreaTop = scrollParentRect.top; 1952 } 1953 if (scrollParentRect.left > 0) { 1954 scrollAreaLeft = scrollParentRect.left; 1955 } 1956 if (scrollParentRect.right < scrollAreaRight) { 1957 scrollAreaRight = scrollParentRect.right; 1958 } 1959 if (scrollParentRect.bottom < scrollAreaBottom) { 1960 scrollAreaBottom = scrollParentRect.bottom; 1961 } 1962 } 1963 1964 // determine the horizontal orientation and left position 1965 let adjustment = 0; 1966 if (orientX === 'auto') { 1967 if (inputLeft < scrollAreaLeft) { 1968 orientX = 'left'; 1969 adjustment = scrollAreaLeft - inputLeft; 1970 } else if (inputLeft + calendarWidth > scrollAreaRight) { 1971 orientX = 'right'; 1972 if (scrollAreaRight < inputRight) { 1973 adjustment = scrollAreaRight - inputRight; 1974 } 1975 } else if (getTextDirection(inputField) === 'rtl') { 1976 orientX = inputRight - calendarWidth < scrollAreaLeft ? 'left' : 'right'; 1977 } else { 1978 orientX = 'left'; 1979 } 1980 } 1981 if (orientX === 'right') { 1982 left += inputWidth - calendarWidth; 1983 } 1984 left += adjustment; 1985 1986 // determine the vertical orientation and top position 1987 if (orientY === 'auto') { 1988 if (inputTop - calendarHeight > scrollAreaTop) { 1989 orientY = inputBottom + calendarHeight > scrollAreaBottom ? 'top' : 'bottom'; 1990 } else { 1991 orientY = 'bottom'; 1992 } 1993 } 1994 if (orientY === 'top') { 1995 top -= calendarHeight; 1996 } else { 1997 top += inputHeight; 1998 } 1999 2000 classList.remove(...Object.values(orientClasses)); 2001 classList.add(orientClasses[orientX], orientClasses[orientY]); 2002 2003 style.left = toPx(left); 2004 style.top = toPx(top); 2005 } 2006 2007 setViewSwitchLabel(labelText) { 2008 this.controls.viewSwitch.textContent = labelText; 2009 } 2010 2011 setPrevButtonDisabled(disabled) { 2012 this.controls.prevButton.disabled = disabled; 2013 } 2014 2015 setNextButtonDisabled(disabled) { 2016 this.controls.nextButton.disabled = disabled; 2017 } 2018 2019 changeView(viewId) { 2020 const currentView = this.currentView; 2021 if (viewId !== currentView.id) { 2022 if (!this._oldView) { 2023 this._oldView = currentView; 2024 } 2025 this.currentView = this.views[viewId]; 2026 this._renderMethod = 'render'; 2027 } 2028 return this; 2029 } 2030 2031 // Change the focused date (view date) 2032 changeFocus(newViewDate) { 2033 this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refreshFocus'; 2034 this.views.forEach((view) => { 2035 view.updateFocus(); 2036 }); 2037 return this; 2038 } 2039 2040 // Apply the change of the selected dates 2041 update(viewDate = undefined) { 2042 const newViewDate = viewDate === undefined 2043 ? computeResetViewDate(this.datepicker) 2044 : viewDate; 2045 this._renderMethod = setViewDate(this, newViewDate) ? 'render' : 'refresh'; 2046 this.views.forEach((view) => { 2047 view.updateFocus(); 2048 view.updateSelection(); 2049 }); 2050 return this; 2051 } 2052 2053 // Refresh the picker UI 2054 render(quickRender = true) { 2055 const {currentView, datepicker, _oldView: oldView} = this; 2056 const oldViewDate = new Date(this._oldViewDate); 2057 const renderMethod = (quickRender && this._renderMethod) || 'render'; 2058 delete this._oldView; 2059 delete this._oldViewDate; 2060 delete this._renderMethod; 2061 2062 currentView[renderMethod](); 2063 if (oldView) { 2064 this.main.replaceChild(currentView.element, oldView.element); 2065 triggerDatepickerEvent(datepicker, 'changeView'); 2066 } 2067 if (!isNaN(oldViewDate)) { 2068 const newViewDate = new Date(this.viewDate); 2069 if (newViewDate.getFullYear() !== oldViewDate.getFullYear()) { 2070 triggerDatepickerEvent(datepicker, 'changeYear'); 2071 } 2072 if (newViewDate.getMonth() !== oldViewDate.getMonth()) { 2073 triggerDatepickerEvent(datepicker, 'changeMonth'); 2074 } 2075 } 2076 } 2077} 2078 2079// Find the closest date that doesn't meet the condition for unavailable date 2080// Returns undefined if no available date is found 2081// addFn: function to calculate the next date 2082// - args: time value, amount 2083// increase: amount to pass to addFn 2084// testFn: function to test the unavailability of the date 2085// - args: time value; return: true if unavailable 2086function findNextAvailableOne(date, addFn, increase, testFn, min, max) { 2087 if (!isInRange(date, min, max)) { 2088 return; 2089 } 2090 if (testFn(date)) { 2091 const newDate = addFn(date, increase); 2092 return findNextAvailableOne(newDate, addFn, increase, testFn, min, max); 2093 } 2094 return date; 2095} 2096 2097// direction: -1 (left/up), 1 (right/down) 2098// vertical: true for up/down, false for left/right 2099function moveByArrowKey(datepicker, direction, vertical) { 2100 const picker = datepicker.picker; 2101 const currentView = picker.currentView; 2102 const step = currentView.step || 1; 2103 let viewDate = picker.viewDate; 2104 let addFn; 2105 switch (currentView.id) { 2106 case 0: 2107 viewDate = addDays(viewDate, vertical ? direction * 7 : direction); 2108 addFn = addDays; 2109 break; 2110 case 1: 2111 viewDate = addMonths(viewDate, vertical ? direction * 4 : direction); 2112 addFn = addMonths; 2113 break; 2114 default: 2115 viewDate = addYears(viewDate, direction * (vertical ? 4 : 1) * step); 2116 addFn = addYears; 2117 } 2118 viewDate = findNextAvailableOne( 2119 viewDate, 2120 addFn, 2121 direction < 0 ? -step : step, 2122 (date) => currentView.disabled.includes(date), 2123 currentView.minDate, 2124 currentView.maxDate 2125 ); 2126 if (viewDate !== undefined) { 2127 picker.changeFocus(viewDate).render(); 2128 } 2129} 2130 2131function onKeydown(datepicker, ev) { 2132 const {config, picker, editMode} = datepicker; 2133 const active = picker.active; 2134 const {key, altKey, shiftKey} = ev; 2135 const ctrlOrMetaKey = ev.ctrlKey || ev.metaKey; 2136 const cancelEvent = () => { 2137 ev.preventDefault(); 2138 ev.stopPropagation(); 2139 }; 2140 2141 // tab/enter keys should not be taken by shortcut keys 2142 if (key === 'Tab') { 2143 unfocus(datepicker); 2144 return; 2145 } 2146 if (key === 'Enter') { 2147 if (!active) { 2148 datepicker.update(); 2149 } else if (editMode) { 2150 datepicker.exitEditMode({update: true, autohide: config.autohide}); 2151 } else { 2152 const currentView = picker.currentView; 2153 if (currentView.isMinView) { 2154 datepicker.setDate(picker.viewDate); 2155 } else { 2156 picker.changeView(currentView.id - 1).render(); 2157 cancelEvent(); 2158 } 2159 } 2160 return; 2161 } 2162 2163 const shortcutKeys = config.shortcutKeys; 2164 const keyInfo = {key, ctrlOrMetaKey, altKey, shiftKey}; 2165 const shortcut = Object.keys(shortcutKeys).find((item) => { 2166 const keyDef = shortcutKeys[item]; 2167 return !Object.keys(keyDef).find(prop => keyDef[prop] !== keyInfo[prop]); 2168 }); 2169 if (shortcut) { 2170 let action; 2171 if (shortcut === 'toggle') { 2172 action = shortcut; 2173 } else if (editMode) { 2174 if (shortcut === 'exitEditMode') { 2175 action = shortcut; 2176 } 2177 } else if (active) { 2178 if (shortcut === 'hide') { 2179 action = shortcut; 2180 } else if (shortcut === 'prevButton') { 2181 action = [goToPrevOrNext, [datepicker, -1]]; 2182 } else if (shortcut === 'nextButton') { 2183 action = [goToPrevOrNext, [datepicker, 1]]; 2184 } else if (shortcut === 'viewSwitch') { 2185 action = [switchView, [datepicker]]; 2186 } else if (config.clearButton && shortcut === 'clearButton') { 2187 action = [clearSelection, [datepicker]]; 2188 } else if (config.todayButton && shortcut === 'todayButton') { 2189 action = [goToOrSelectToday, [datepicker]]; 2190 } 2191 } else if (shortcut === 'show') { 2192 action = shortcut; 2193 } 2194 if (action) { 2195 if (Array.isArray(action)) { 2196 action[0].apply(null, action[1]); 2197 } else { 2198 datepicker[action](); 2199 } 2200 cancelEvent(); 2201 return; 2202 } 2203 } 2204 2205 // perform as a regular <input> when picker in hidden or in edit mode 2206 if (!active || editMode) { 2207 return; 2208 } 2209 2210 const handleArrowKeyPress = (direction, vertical) => { 2211 if (shiftKey || ctrlOrMetaKey || altKey) { 2212 datepicker.enterEditMode(); 2213 } else { 2214 moveByArrowKey(datepicker, direction, vertical); 2215 ev.preventDefault(); 2216 } 2217 }; 2218 2219 if (key === 'ArrowLeft') { 2220 handleArrowKeyPress(-1, false); 2221 } else if (key === 'ArrowRight') { 2222 handleArrowKeyPress(1, false); 2223 } else if (key === 'ArrowUp') { 2224 handleArrowKeyPress(-1, true); 2225 } else if (key === 'ArrowDown') { 2226 handleArrowKeyPress(1, true); 2227 } else if ( 2228 key === 'Backspace' 2229 || key === 'Delete' 2230 // When autofill is performed, Chromium-based browsers trigger fake 2231 // keydown/keyup events that don't have the `key` property (on Edge, 2232 // keyup only) in addition to the input event. Therefore, we need to 2233 // check the existence of `key`'s value before checking the length. 2234 // (issue #144) 2235 || (key && key.length === 1 && !ctrlOrMetaKey) 2236 ) { 2237 datepicker.enterEditMode(); 2238 } 2239} 2240 2241function onFocus(datepicker) { 2242 if (datepicker.config.showOnFocus && !datepicker._showing) { 2243 datepicker.show(); 2244 } 2245} 2246 2247// for the prevention for entering edit mode while getting focus on click 2248function onMousedown(datepicker, ev) { 2249 const el = ev.target; 2250 if (datepicker.picker.active || datepicker.config.showOnClick) { 2251 el._active = isActiveElement(el); 2252 el._clicking = setTimeout(() => { 2253 delete el._active; 2254 delete el._clicking; 2255 }, 2000); 2256 } 2257} 2258 2259function onClickInput(datepicker, ev) { 2260 const el = ev.target; 2261 if (!el._clicking) { 2262 return; 2263 } 2264 clearTimeout(el._clicking); 2265 delete el._clicking; 2266 2267 if (el._active) { 2268 datepicker.enterEditMode(); 2269 } 2270 delete el._active; 2271 2272 if (datepicker.config.showOnClick) { 2273 datepicker.show(); 2274 } 2275} 2276 2277function onPaste(datepicker, ev) { 2278 if (ev.clipboardData.types.includes('text/plain')) { 2279 datepicker.enterEditMode(); 2280 } 2281} 2282 2283// for the `document` to delegate the events from outside the picker/input field 2284function onClickOutside(datepicker, ev) { 2285 const {element, picker} = datepicker; 2286 // check both picker's and input's activeness to make updateOnBlur work in 2287 // the cases where... 2288 // - picker is hidden by ESC key press â input stays focused 2289 // - input is unfocused by closing mobile keyboard â piker is kept shown 2290 if (!picker.active && !isActiveElement(element)) { 2291 return; 2292 } 2293 const pickerElem = picker.element; 2294 if (findElementInEventPath(ev, el => el === element || el === pickerElem)) { 2295 return; 2296 } 2297 unfocus(datepicker); 2298} 2299 2300function stringifyDates(dates, config) { 2301 return dates 2302 .map(dt => formatDate(dt, config.format, config.locale)) 2303 .join(config.dateDelimiter); 2304} 2305 2306// parse input dates and create an array of time values for selection 2307// returns undefined if there are no valid dates in inputDates 2308// when origDates (current selection) is passed, the function works to mix 2309// the input dates into the current selection 2310function processInputDates(datepicker, inputDates, clear = false) { 2311 if (inputDates.length === 0) { 2312 // empty input is considered valid unless origiDates is passed 2313 return clear ? [] : undefined; 2314 } 2315 2316 const {config, dates: origDates, rangeSideIndex} = datepicker; 2317 const {pickLevel, maxNumberOfDates} = config; 2318 let newDates = inputDates.reduce((dates, dt) => { 2319 let date = parseDate(dt, config.format, config.locale); 2320 if (date === undefined) { 2321 return dates; 2322 } 2323 // adjust to 1st of the month/Jan 1st of the year 2324 // or to the last day of the monh/Dec 31st of the year if the datepicker 2325 // is the range-end picker of a rangepicker 2326 date = regularizeDate(date, pickLevel, rangeSideIndex); 2327 if ( 2328 isInRange(date, config.minDate, config.maxDate) 2329 && !dates.includes(date) 2330 && !config.checkDisabled(date, pickLevel) 2331 && (pickLevel > 0 || !config.daysOfWeekDisabled.includes(new Date(date).getDay())) 2332 ) { 2333 dates.push(date); 2334 } 2335 return dates; 2336 }, []); 2337 if (newDates.length === 0) { 2338 return; 2339 } 2340 if (config.multidate && !clear) { 2341 // get the synmetric difference between origDates and newDates 2342 newDates = newDates.reduce((dates, date) => { 2343 if (!origDates.includes(date)) { 2344 dates.push(date); 2345 } 2346 return dates; 2347 }, origDates.filter(date => !newDates.includes(date))); 2348 } 2349 // do length check always because user can input multiple dates regardless of the mode 2350 return maxNumberOfDates && newDates.length > maxNumberOfDates 2351 ? newDates.slice(maxNumberOfDates * -1) 2352 : newDates; 2353} 2354 2355// refresh the UI elements 2356// modes: 1: input only, 2, picker only, 3 both 2357function refreshUI(datepicker, mode = 3, quickRender = true, viewDate = undefined) { 2358 const {config, picker, inputField} = datepicker; 2359 if (mode & 2) { 2360 const newView = picker.active ? config.pickLevel : config.startView; 2361 picker.update(viewDate).changeView(newView).render(quickRender); 2362 } 2363 if (mode & 1 && inputField) { 2364 inputField.value = stringifyDates(datepicker.dates, config); 2365 } 2366} 2367 2368function setDate(datepicker, inputDates, options) { 2369 const config = datepicker.config; 2370 let {clear, render, autohide, revert, forceRefresh, viewDate} = options; 2371 if (render === undefined) { 2372 render = true; 2373 } 2374 if (!render) { 2375 autohide = forceRefresh = false; 2376 } else if (autohide === undefined) { 2377 autohide = config.autohide; 2378 } 2379 viewDate = parseDate(viewDate, config.format, config.locale); 2380 2381 const newDates = processInputDates(datepicker, inputDates, clear); 2382 if (!newDates && !revert) { 2383 return; 2384 } 2385 if (newDates && newDates.toString() !== datepicker.dates.toString()) { 2386 datepicker.dates = newDates; 2387 refreshUI(datepicker, render ? 3 : 1, true, viewDate); 2388 triggerDatepickerEvent(datepicker, 'changeDate'); 2389 } else { 2390 refreshUI(datepicker, forceRefresh ? 3 : 1, true, viewDate); 2391 } 2392 2393 if (autohide) { 2394 datepicker.hide(); 2395 } 2396} 2397 2398function getOutputConverter(datepicker, format) { 2399 return format 2400 ? date => formatDate(date, format, datepicker.config.locale) 2401 : date => new Date(date); 2402} 2403 2404/** 2405 * Class representing a date picker 2406 */ 2407class Datepicker { 2408 /** 2409 * Create a date picker 2410 * @param {Element} element - element to bind a date picker 2411 * @param {Object} [options] - config options 2412 * @param {DateRangePicker} [rangepicker] - DateRangePicker instance the 2413 * date picker belongs to. Use this only when creating date picker as a part 2414 * of date range picker 2415 */ 2416 constructor(element, options = {}, rangepicker = undefined) { 2417 element.datepicker = this; 2418 this.element = element; 2419 this.dates = []; 2420 2421 // initialize config 2422 const config = this.config = Object.assign({ 2423 buttonClass: (options.buttonClass && String(options.buttonClass)) || 'button', 2424 container: null, 2425 defaultViewDate: today(), 2426 maxDate: undefined, 2427 minDate: undefined, 2428 }, processOptions(defaultOptions, this)); 2429 2430 // configure by type 2431 let inputField; 2432 if (element.tagName === 'INPUT') { 2433 inputField = this.inputField = element; 2434 inputField.classList.add('datepicker-input'); 2435 if (options.container) { 2436 // omit string type check because it doesn't guarantee to avoid errors 2437 // (invalid selector string causes abend with sytax error) 2438 config.container = options.container instanceof HTMLElement 2439 ? options.container 2440 : document.querySelector(options.container); 2441 } 2442 } else { 2443 config.container = element; 2444 } 2445 if (rangepicker) { 2446 // check validiry 2447 const index = rangepicker.inputs.indexOf(inputField); 2448 const datepickers = rangepicker.datepickers; 2449 if (index < 0 || index > 1 || !Array.isArray(datepickers)) { 2450 throw Error('Invalid rangepicker object.'); 2451 } 2452 // attach itaelf to the rangepicker here so that processInputDates() can 2453 // determine if this is the range-end picker of the rangepicker while 2454 // setting inital values when pickLevel > 0 2455 datepickers[index] = this; 2456 this.rangepicker = rangepicker; 2457 this.rangeSideIndex = index; 2458 } 2459 2460 // set up config 2461 this._options = options; 2462 Object.assign(config, processOptions(options, this)); 2463 config.shortcutKeys = createShortcutKeyConfig(options.shortcutKeys || {}); 2464 2465 // process initial value 2466 const initialDates = stringToArray( 2467 element.value || element.dataset.date, 2468 config.dateDelimiter 2469 ); 2470 delete element.dataset.date; 2471 const inputDateValues = processInputDates(this, initialDates); 2472 if (inputDateValues && inputDateValues.length > 0) { 2473 this.dates = inputDateValues; 2474 } 2475 if (inputField) { 2476 inputField.value = stringifyDates(this.dates, config); 2477 } 2478 2479 // set up picekr element 2480 const picker = this.picker = new Picker(this); 2481 2482 const keydownListener = [element, 'keydown', onKeydown.bind(null, this)]; 2483 if (inputField) { 2484 registerListeners(this, [ 2485 keydownListener, 2486 [inputField, 'focus', onFocus.bind(null, this)], 2487 [inputField, 'mousedown', onMousedown.bind(null, this)], 2488 [inputField, 'click', onClickInput.bind(null, this)], 2489 [inputField, 'paste', onPaste.bind(null, this)], 2490 // To detect a click on outside, just listening to mousedown is enough, 2491 // no need to listen to touchstart. 2492 // Actually, listening to touchstart can be a problem because, while 2493 // mousedown is fired only on tapping but not on swiping/pinching, 2494 // touchstart is fired on swiping/pinching as well. 2495 // (issue #95) 2496 [document, 'mousedown', onClickOutside.bind(null, this)], 2497 [window, 'resize', picker.place.bind(picker)] 2498 ]); 2499 } else { 2500 registerListeners(this, [keydownListener]); 2501 this.show(); 2502 } 2503 } 2504 2505 /** 2506 * Format Date object or time value in given format and language 2507 * @param {Date|Number} date - date or time value to format 2508 * @param {String|Object}
2508 format - format string or object that contains 2509 * toDisplay() custom formatter, whose signature is 2510 * - args: 2511 * - date: {Date} - Date instance of the date passed to the method 2512 * - format: {Object} - the format object passed to the method 2513 * - locale: {Object} - locale for the language specified by `lang` 2514 * - return: 2515 * {String} formatted date 2516 * @param {String} [lang=en] - language code for the locale to use 2517 * @return {String} formatted date 2518 */ 2519 static formatDate(date, format, lang) { 2520 return formatDate(date, format, lang && locales[lang] || locales.en); 2521 } 2522 2523 /** 2524 * Parse date string 2525 * @param {String|Date|Number} dateStr - date string, Date object or time 2526 * value to parse 2527 * @param {String|Object} format - format string or object that contains 2528 * toValue() custom parser, whose signature is 2529 * - args: 2530 * - dateStr: {String|Date|Number} - the dateStr passed to the method 2531 * - format: {Object} - the format object passed to the method 2532 * - locale: {Object} - locale for the language specified by `lang` 2533 * - return: 2534 * {Date|Number} parsed date or its time value 2535 * @param {String} [lang=en] - language code for the locale to use 2536 * @return {Number} time value of parsed date 2537 */ 2538 static parseDate(dateStr, format, lang) { 2539 return parseDate(dateStr, format, lang && locales[lang] || locales.en); 2540 } 2541 2542 /** 2543 * @type {Object} - Installed locales in `[languageCode]: localeObject` format 2544 * en`:_English (US)_ is pre-installed. 2545 */ 2546 static get locales() { 2547 return locales; 2548 } 2549 2550 /** 2551 * @type {Boolean} - Whether the picker element is shown. `true` whne shown 2552 */ 2553 get active() { 2554 return !!(this.picker && this.picker.active); 2555 } 2556 2557 /** 2558 * @type {HTMLDivElement} - DOM object of picker element 2559 */ 2560 get pickerElement() { 2561 return this.picker ? this.picker.element : undefined; 2562 } 2563 2564 /** 2565 * Set new values to the config options 2566 * @param {Object} options - config options to update 2567 */ 2568 setOptions(options) { 2569 const newOptions = processOptions(options, this); 2570 Object.assign(this._options, options); 2571 Object.assign(this.config, newOptions); 2572 this.picker.setOptions(newOptions); 2573 2574 refreshUI(this, 3); 2575 } 2576 2577 /** 2578 * Show the picker element 2579 */ 2580 show() { 2581 if (this.inputField) { 2582 const {config, inputField} = this; 2583 if (inputField.disabled || (inputField.readOnly && !config.enableOnReadonly)) { 2584 return; 2585 } 2586 if (!isActiveElement(inputField) && !config.disableTouchKeyboard) { 2587 this._showing = true; 2588 inputField.focus(); 2589 delete this._showing; 2590 } 2591 } 2592 this.picker.show(); 2593 } 2594 2595 /** 2596 * Hide the picker element 2597 * Not available on inline picker 2598 */ 2599 hide() { 2600 if (!this.inputField) { 2601 return; 2602 } 2603 this.picker.hide(); 2604 this.picker.update().changeView(this.config.startView).render(); 2605 } 2606 2607 /** 2608 * Toggle the display of the picker element 2609 * Not available on inline picker 2610 * 2611 * Unlike hide(), the picker does not return to the start view when hiding. 2612 */ 2613 toggle() { 2614 if (!this.picker.active) { 2615 this.show(); 2616 } else if (this.inputField) { 2617 this.picker.hide(); 2618 } 2619 } 2620 2621 /** 2622 * Destroy the Datepicker instance 2623 * @return {Detepicker} - the instance destroyed 2624 */ 2625 destroy() { 2626 this.hide(); 2627 unregisterListeners(this); 2628 this.picker.detach(); 2629 const element = this.element; 2630 element.classList.remove('datepicker-input'); 2631 delete element.datepicker; 2632 return this; 2633 } 2634 2635 /** 2636 * Get the selected date(s) 2637 * 2638 * The method returns a Date object of selected date by default, and returns 2639 * an array of selected dates in multidate mode. If format string is passed, 2640 * it returns date string(s) formatted in given format. 2641 * 2642 * @param {String} [format] - format string to stringify the date(s) 2643 * @return {Date|String|Date[]|String[]} - selected date(s), or if none is 2644 * selected, empty array in multidate mode and undefined in sigledate mode 2645 */ 2646 getDate(format = undefined) { 2647 const callback = getOutputConverter(this, format); 2648 2649 if (this.config.multidate) { 2650 return this.dates.map(callback); 2651 } 2652 if (this.dates.length > 0) { 2653 return callback(this.dates[0]); 2654 } 2655 } 2656 2657 /** 2658 * Set selected date(s) 2659 * 2660 * In multidate mode, you can pass multiple dates as a series of arguments 2661 * or an array. (Since each date is parsed individually, the type of the 2662 * dates doesn't have to be the same.) 2663 * The given dates are used to toggle the select status of each date. The 2664 * number of selected dates is kept from exceeding the length set to 2665 * maxNumberOfDates. 2666 * 2667 * With clear: true option, the method can be used to clear the selection 2668 * and to replace the selection instead of toggling in multidate mode.
2669 * If the option is passed with no date arguments or an empty dates array, 2670 * it works as "clear" (clear the selection then set nothing), and if the 2671 * option is passed with new dates to select, it works as "replace" (clear 2672 * the selection then set the given dates) 2673 * 2674 * When render: false option is used, the method omits re-rendering the 2675 * picker element. In this case, you need to call refresh() method later in 2676 * order for the picker element to reflect the changes. The input field is 2677 * refreshed always regardless of this option. 2678 * 2679 * When invalid (unparsable, repeated, disabled or out-of-range) dates are 2680 * passed, the method ignores them and applies only valid ones. In the case 2681 * that all the given dates are invalid, which is distinguished from passing 2682 * no dates, the method considers it as an error and leaves the selection 2683 * untouched. (The input field also remains untouched unless revert: true 2684 * option is used.) 2685 * Replacing the selection with the same date(s) also causes a similar 2686 * situation. In both cases, the method does not refresh the picker element 2687 * unless forceRefresh: true option is used. 2688 * 2689 * If viewDate option is used, the method changes the focused date to the 2690 * specified date instead of the last item of the selection. 2691 * 2692 * @param {...(Date|Number|String)|Array} [dates] - Date strings, Date 2693 * objects, time values or mix of those for new selection 2694 * @param {Object} [options] - function options 2695 * - clear: {boolean} - Whether to clear the existing selection 2696 * defualt: false 2697 * - render: {boolean} - Whether to re-render the picker element 2698 * default: true 2699 * - autohide: {boolean} - Whether to hide the picker element after re-render 2700 * Ignored when used with render: false 2701 * default: config.autohide 2702 * - revert: {boolean} - Whether to refresh the input field when all the 2703 * passed dates are invalid 2704 * default: false 2705 * - forceRefresh: {boolean} - Whether to refresh the picker element when 2706 * passed dates don't change the existing selection 2707 * default: false 2708 * - viewDate: {Date|Number|String} - Date to be focused after setiing date(s) 2709 * default: The last item of the resulting selection, or defaultViewDate 2710 * config option if none is selected 2711 */ 2712 setDate(...args) { 2713 const dates = [...args]; 2714 const opts = {}; 2715 const lastArg = lastItemOf(args); 2716 if ( 2717 lastArg 2718 && typeof lastArg === 'object' 2719 && !Array.isArray(lastArg) 2720 && !(lastArg instanceof Date) 2721 ) { 2722 Object.assign(opts, dates.pop()); 2723 } 2724 2725 const inputDates = Array.isArray(dates[0]) ? dates[0] : dates; 2726 setDate(this, inputDates, opts); 2727 } 2728 2729 /** 2730 * Update the selected date(s) with input field's value 2731 * Not available on inline picker 2732 * 2733 * The input field will be refreshed with properly formatted date string. 2734 * 2735 * In the case that all the entered dates are invalid (unparsable, repeated, 2736 * disabled or out-of-range), which is distinguished from empty input field, 2737 * the method leaves the input field untouched as well as the selection by 2738 * default. If revert: true option is used in this case, the input field is 2739 * refreshed with the existing selection. 2740 * The method also doesn't refresh the picker element in this case and when 2741 * the entered dates are the same as the existing selection. If 2742 * forceRefresh: true option is used, the picker element is refreshed in 2743 * these cases too. 2744 * 2745 * @param {Object} [options] - function options 2746 * - autohide: {boolean} - whether to hide the picker element after refresh 2747 * default: false 2748 * - revert: {boolean} - Whether to refresh the input field when all the 2749 * passed dates are invalid 2750 * default: false 2751 * - forceRefresh: {boolean} - Whether to refresh the picer element when 2752 * input field's value doesn't change the existing selection 2753 * default: false 2754 */ 2755 update(options = undefined) { 2756 if (!this.inputField) { 2757 return; 2758 } 2759 2760 const opts = Object.assign(options || {}, {clear: true, render: true, viewDate: undefined}); 2761 const inputDates = stringToArray(this.inputField.value, this.config.dateDelimiter); 2762 setDate(this, inputDates, opts); 2763 } 2764 2765 /** 2766 * Get the focused date 2767 * 2768 * The method returns a Date object of focused date by default. If format 2769 * string is passed, it returns date string formatted in given format. 2770 * 2771 * @param {String}
2771 [format] - format string to stringify the date 2772 * @return {Date|String} - focused date (viewDate) 2773 */ 2774 getFocusedDate(format = undefined) { 2775 return getOutputConverter(this, format)(this.picker.viewDate); 2776 } 2777 2778 /** 2779 * Set focused date 2780 * 2781 * By default, the method updates the focus on the view shown at the time, 2782 * or the one set to the startView config option if the picker is hidden. 2783 * When resetView: true is passed, the view displayed is changed to the 2784 * pickLevel config option's if the picker is shown. 2785 * 2786 * @param {Date|Number|String} viewDate - date string, Date object, time 2787 * values of the date to focus 2788 * @param {Boolean} [resetView] - whether to change the view to pickLevel 2789 * config option's when the picker is shown. Ignored when the picker is 2790 * hidden 2791 */ 2792 setFocusedDate(viewDate, resetView = false) { 2793 const {config, picker, active, rangeSideIndex} = this; 2794 const pickLevel = config.pickLevel; 2795 const newViewDate = parseDate(viewDate, config.format, config.locale); 2796 if (newViewDate === undefined) { 2797 return; 2798 } 2799 2800 picker.changeFocus(regularizeDate(newViewDate, pickLevel, rangeSideIndex)); 2801 if (active && resetView) { 2802 picker.changeView(pickLevel); 2803 } 2804 picker.render(); 2805 } 2806 2807 /** 2808 * Refresh the picker element and the associated input field 2809 * @param {String} [target] - target item when refreshing one item only 2810 * 'picker' or 'input' 2811 * @param {Boolean} [forceRender] - whether to re-render the picker element 2812 * regardless of its state instead of optimized refresh 2813 */ 2814 refresh(target = undefined, forceRender = false) { 2815 if (target && typeof target !== 'string') { 2816 forceRender = target; 2817 target = undefined; 2818 } 2819 2820 let mode; 2821 if (target === 'picker') { 2822 mode = 2; 2823 } else if (target === 'input') { 2824 mode = 1; 2825 } else { 2826 mode = 3; 2827 } 2828 refreshUI(this, mode, !forceRender); 2829 } 2830 2831 /** 2832 * Enter edit mode 2833 * Not available on inline picker or when the picker element is hidden 2834 */ 2835 enterEditMode() { 2836 const inputField = this.inputField; 2837 if (!inputField || inputField.readOnly || !this.picker.active || this.editMode) { 2838 return; 2839 } 2840 this.editMode = true; 2841 inputField.classList.add('in-edit'); 2842 } 2843 2844 /** 2845 * Exit from edit mode 2846 * Not available on inline picker 2847 * @param {Object} [options] - function options 2848 * - update: {boolean} - whether to call update() after exiting 2849 * If false, input field is revert to the existing selection 2850 * default: false 2851 */ 2852 exitEditMode(options = undefined) { 2853 if (!this.inputField || !this.editMode) { 2854 return; 2855 } 2856 const opts = Object.assign({update: false}, options); 2857 delete this.editMode; 2858 this.inputField.classList.remove('in-edit'); 2859 if (opts.update) { 2860 this.update(opts); 2861 } 2862 } 2863} 2864 2865/** 2866 * German translation for bootstrap-datepicker 2867 * Sam Zurcher <[email protected]> 2868 */ 2869var de = { 2870 de: { 2871 days: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"], 2872 daysShort: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], 2873 daysMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"], 2874 months: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], 2875 monthsShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"], 2876 today: "Heute", 2877 monthsTitle: "Monate", 2878 clear: "Löschen", 2879 weekStart: 1, 2880 format: "dd.mm.yyyy" 2881 } 2882}; 2883 2884// 2885Object.assign(Datepicker.locales, de); 2886function init() { 2887 var datepickers = document.querySelectorAll('.vjsdatepicker'); 2888 datepickers.forEach(function (node) { 2889 var minDate = node.getAttribute('mindate'); 2890 var maxDate = node.getAttribute('maxdate'); 2891 new Datepicker(node, { 2892 minDate: minDate, 2893 maxDate: maxDate, 2894 maxNumberOfDates: 99, 2895 buttonClass: 'btn', 2896 language: 'de' 2897 }); 2898 }); 2899} 2900var _datepicker = {}; 2901var Init = init; 2902 2903export { Init, _datepicker as default }; 2904//# sourceMappingURL=_datepicker-7c518a59.js.map
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.