PageSourceSearch

https://www.faehre.de/_assets/6020ad64298e304d8b844c8dc69c472e/Jav…/Lib/datepicker/js/datepicker.js?1748336042

js faehre.de collected 2026-10-02 07:47:45 UTC 93,386 bytes, 2,870 lines download raw bytes

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

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.