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