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.