UNPKG

bootstrap-vue

Version:

With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens

1,151 lines (1,082 loc) 43 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import Vue from '../../vue'; import { NAME_CALENDAR } from '../../constants/components'; import { CALENDAR_GREGORY, CALENDAR_LONG, CALENDAR_NARROW, CALENDAR_SHORT, DATE_FORMAT_2_DIGIT, DATE_FORMAT_NUMERIC } from '../../constants/date'; import { CODE_DOWN, CODE_END, CODE_ENTER, CODE_HOME, CODE_LEFT, CODE_PAGEDOWN, CODE_PAGEUP, CODE_RIGHT, CODE_SPACE, CODE_UP } from '../../constants/key-codes'; import identity from '../../utils/identity'; import looseEqual from '../../utils/loose-equal'; import { arrayIncludes, concat } from '../../utils/array'; import { getComponentConfig } from '../../utils/config'; import { createDate, createDateFormatter, constrainDate as _constrainDate, datesEqual, firstDateOfMonth, formatYMD, lastDateOfMonth, oneMonthAgo, oneMonthAhead, oneYearAgo, oneYearAhead, oneDecadeAgo, oneDecadeAhead, parseYMD, resolveLocale } from '../../utils/date'; import { attemptBlur, attemptFocus, requestAF } from '../../utils/dom'; import { stopEvent } from '../../utils/events'; import { isArray, isFunction, isPlainObject, isString } from '../../utils/inspect'; import { isLocaleRTL } from '../../utils/locale'; import { mathMax } from '../../utils/math'; import { toInteger } from '../../utils/number'; import { toString } from '../../utils/string'; import attrsMixin from '../../mixins/attrs'; import idMixin from '../../mixins/id'; import normalizeSlotMixin from '../../mixins/normalize-slot'; import { BIconChevronLeft, BIconChevronDoubleLeft, BIconChevronBarLeft, BIconCircleFill } from '../../icons/icons'; // --- BCalendar component --- // @vue/component export var BCalendar = Vue.extend({ name: NAME_CALENDAR, // Mixin order is important! mixins: [attrsMixin, idMixin, normalizeSlotMixin], model: { // Even though this is the default that Vue assumes, we need // to add it for the docs to reflect that this is the model // And also for some validation libraries to work prop: 'value', event: 'input' }, props: { value: { type: [String, Date] // default: null }, valueAsDate: { // Always return the `v-model` value as a date object type: Boolean, default: false }, initialDate: { // This specifies the calendar year/month/day that will be shown when // first opening the datepicker if no v-model value is provided // Default is the current date (or `min`/`max`) type: [String, Date] // default: null }, disabled: { type: Boolean, default: false }, readonly: { type: Boolean, default: false }, min: { type: [String, Date] // default: null }, max: { type: [String, Date] // default: null }, dateDisabledFn: { type: Function // default: null }, startWeekday: { // `0` (Sunday), `1` (Monday), ... `6` (Saturday) // Day of week to start calendar on type: [Number, String], default: 0 }, locale: { // Locale(s) to use // Default is to use page/browser default setting type: [String, Array] // default: null }, direction: { // 'ltr', 'rtl', or `null` (for auto detect) type: String // default: null }, selectedVariant: { // Variant color to use for the selected date type: String, default: getComponentConfig(NAME_CALENDAR, 'selectedVariant') }, todayVariant: { // Variant color to use for today's date (defaults to `selectedVariant`) type: String, default: getComponentConfig(NAME_CALENDAR, 'todayVariant') }, navButtonVariant: { // Variant color to use for the navigation buttons type: String, default: getComponentConfig(NAME_CALENDAR, 'navButtonVariant') }, noHighlightToday: { // Disable highlighting today's date type: Boolean, default: false }, dateInfoFn: { // Function to set a class of (classes) on the date cell // if passed a string or an array // TODO: // If the function returns an object, look for class prop for classes, // and other props for handling events/details/descriptions type: Function // default: null }, width: { // Has no effect if prop `block` is set type: String, default: '270px' }, block: { // Makes calendar the full width of its parent container type: Boolean, default: false }, hideHeader: { // When true makes the selected date header `sr-only` type: Boolean, default: false }, showDecadeNav: { // When `true` enables the decade navigation buttons type: Boolean, default: false }, hidden: { // When `true`, renders a comment node, but keeps the component instance active // Mainly for <b-form-date>, so that we can get the component's value and locale // But we might just use separate date formatters, using the resolved locale // (adjusted for the gregorian calendar) type: Boolean, default: false }, ariaControls: { type: String // default: null }, noKeyNav: { type: Boolean, default: false }, roleDescription: { type: String // default: null }, // Labels for buttons and keyboard shortcuts labelPrevDecade: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelPrevDecade'); } }, labelPrevYear: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelPrevYear'); } }, labelPrevMonth: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelPrevMonth'); } }, labelCurrentMonth: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelCurrentMonth'); } }, labelNextMonth: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelNextMonth'); } }, labelNextYear: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelNextYear'); } }, labelNextDecade: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelNextDecade'); } }, labelToday: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelToday'); } }, labelSelected: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelSelected'); } }, labelNoDateSelected: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelNoDateSelected'); } }, labelCalendar: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelCalendar'); } }, labelNav: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelNav'); } }, labelHelp: { type: String, default: function _default() { return getComponentConfig(NAME_CALENDAR, 'labelHelp'); } }, dateFormatOptions: { // `Intl.DateTimeFormat` object // Note: This value is *not* to be placed in the global config type: Object, default: function _default() { return { year: DATE_FORMAT_NUMERIC, month: CALENDAR_LONG, day: DATE_FORMAT_NUMERIC, weekday: CALENDAR_LONG }; } }, weekdayHeaderFormat: { // Format of the weekday names at the top of the calendar // Note: This value is *not* to be placed in the global config type: String, // `short` is typically a 3 letter abbreviation, // `narrow` is typically a single letter // `long` is the full week day name // Although some locales may override this (i.e `ar`, etc.) default: CALENDAR_SHORT, validator: function validator(value) { return arrayIncludes([CALENDAR_LONG, CALENDAR_SHORT, CALENDAR_NARROW], value); } } }, data: function data() { var selected = formatYMD(this.value) || ''; return { // Selected date selectedYMD: selected, // Date in calendar grid that has `tabindex` of `0` activeYMD: selected || formatYMD(_constrainDate(this.initialDate || this.getToday()), this.min, this.max), // Will be true if the calendar grid has/contains focus gridHasFocus: false, // Flag to enable the `aria-live` region(s) after mount // to prevent screen reader "outbursts" when mounting isLive: false }; }, computed: { valueId: function valueId() { return this.safeId(); }, widgetId: function widgetId() { return this.safeId('_calendar-wrapper_'); }, navId: function navId() { return this.safeId('_calendar-nav_'); }, gridId: function gridId() { return this.safeId('_calendar-grid_'); }, gridCaptionId: function gridCaptionId() { return this.safeId('_calendar-grid-caption_'); }, gridHelpId: function gridHelpId() { return this.safeId('_calendar-grid-help_'); }, activeId: function activeId() { return this.activeYMD ? this.safeId("_cell-".concat(this.activeYMD, "_")) : null; }, // TODO: Use computed props to convert `YYYY-MM-DD` to `Date` object selectedDate: function selectedDate() { // Selected as a `Date` object return parseYMD(this.selectedYMD); }, activeDate: function activeDate() { // Active as a `Date` object return parseYMD(this.activeYMD); }, computedMin: function computedMin() { return parseYMD(this.min); }, computedMax: function computedMax() { return parseYMD(this.max); }, computedWeekStarts: function computedWeekStarts() { // `startWeekday` is a prop (constrained to `0` through `6`) return mathMax(toInteger(this.startWeekday, 0), 0) % 7; }, computedLocale: function computedLocale() { // Returns the resolved locale used by the calendar return resolveLocale(concat(this.locale).filter(identity), CALENDAR_GREGORY); }, calendarLocale: function calendarLocale() { // This locale enforces the gregorian calendar (for use in formatter functions) // Needed because IE 11 resolves `ar-IR` as islamic-civil calendar // and IE 11 (and some other browsers) do not support the `calendar` option // And we currently only support the gregorian calendar var fmt = new Intl.DateTimeFormat(this.computedLocale, { calendar: CALENDAR_GREGORY }); var calendar = fmt.resolvedOptions().calendar; var locale = fmt.resolvedOptions().locale; /* istanbul ignore if: mainly for IE 11 and a few other browsers, hard to test in JSDOM */ if (calendar !== CALENDAR_GREGORY) { // Ensure the locale requests the gregorian calendar // Mainly for IE 11, and currently we can't handle non-gregorian calendars // TODO: Should we always return this value? locale = locale.replace(/-u-.+$/i, '').concat('-u-ca-gregory'); } return locale; }, calendarYear: function calendarYear() { return this.activeDate.getFullYear(); }, calendarMonth: function calendarMonth() { return this.activeDate.getMonth(); }, calendarFirstDay: function calendarFirstDay() { // We set the time for this date to 12pm to work around // date formatting issues in Firefox and Safari // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/5818 return createDate(this.calendarYear, this.calendarMonth, 1, 12); }, calendarDaysInMonth: function calendarDaysInMonth() { // We create a new date as to not mutate the original var date = createDate(this.calendarFirstDay); date.setMonth(date.getMonth() + 1, 0); return date.getDate(); }, computedVariant: function computedVariant() { return "btn-".concat(this.selectedVariant || 'primary'); }, computedTodayVariant: function computedTodayVariant() { return "btn-outline-".concat(this.todayVariant || this.selectedVariant || 'primary'); }, computedNavButtonVariant: function computedNavButtonVariant() { return "btn-outline-".concat(this.navButtonVariant || 'primary'); }, isRTL: function isRTL() { // `true` if the language requested is RTL var dir = toString(this.direction).toLowerCase(); if (dir === 'rtl') { /* istanbul ignore next */ return true; } else if (dir === 'ltr') { /* istanbul ignore next */ return false; } return isLocaleRTL(this.computedLocale); }, context: function context() { var selectedYMD = this.selectedYMD, activeYMD = this.activeYMD; var selectedDate = parseYMD(selectedYMD); var activeDate = parseYMD(activeYMD); return { // The current value of the `v-model` selectedYMD: selectedYMD, selectedDate: selectedDate, selectedFormatted: selectedDate ? this.formatDateString(selectedDate) : this.labelNoDateSelected, // Which date cell is considered active due to navigation activeYMD: activeYMD, activeDate: activeDate, activeFormatted: activeDate ? this.formatDateString(activeDate) : '', // `true` if the date is disabled (when using keyboard navigation) disabled: this.dateDisabled(activeDate), // Locales used in formatting dates locale: this.computedLocale, calendarLocale: this.calendarLocale, rtl: this.isRTL }; }, // Computed props that return a function reference dateOutOfRange: function dateOutOfRange() { // Check whether a date is within the min/max range // Returns a new function ref if the pops change // We do this as we need to trigger the calendar computed prop // to update when these props update var min = this.computedMin, max = this.computedMax; return function (date) { // Handle both `YYYY-MM-DD` and `Date` objects date = parseYMD(date); return min && date < min || max && date > max; }; }, dateDisabled: function dateDisabled() { // Returns a function for validating if a date is within range // We grab this variables first to ensure a new function ref // is generated when the props value changes // We do this as we need to trigger the calendar computed prop // to update when these props update var rangeFn = this.dateOutOfRange; var disabledFn = isFunction(this.dateDisabledFn) ? this.dateDisabledFn : function () { return false; }; // Return the function ref return function (date) { // Handle both `YYYY-MM-DD` and `Date` objects date = parseYMD(date); var ymd = formatYMD(date); return !!(rangeFn(date) || disabledFn(ymd, date)); }; }, // Computed props that return date formatter functions formatDateString: function formatDateString() { // Returns a date formatter function return createDateFormatter(this.calendarLocale, _objectSpread(_objectSpread({ // Ensure we have year, month, day shown for screen readers/ARIA // If users really want to leave one of these out, they can // pass `undefined` for the property value year: DATE_FORMAT_NUMERIC, month: DATE_FORMAT_2_DIGIT, day: DATE_FORMAT_2_DIGIT }, this.dateFormatOptions), {}, { // Ensure hours/minutes/seconds are not shown // As we do not support the time portion (yet) hour: undefined, minute: undefined, second: undefined, // Ensure calendar is gregorian calendar: CALENDAR_GREGORY })); }, formatYearMonth: function formatYearMonth() { // Returns a date formatter function return createDateFormatter(this.calendarLocale, { year: DATE_FORMAT_NUMERIC, month: CALENDAR_LONG, calendar: CALENDAR_GREGORY }); }, formatWeekdayName: function formatWeekdayName() { // Long weekday name for weekday header aria-label return createDateFormatter(this.calendarLocale, { weekday: CALENDAR_LONG, calendar: CALENDAR_GREGORY }); }, formatWeekdayNameShort: function formatWeekdayNameShort() { // Weekday header cell format // defaults to 'short' 3 letter days, where possible return createDateFormatter(this.calendarLocale, { weekday: this.weekdayHeaderFormat || CALENDAR_SHORT, calendar: CALENDAR_GREGORY }); }, formatDay: function formatDay() { // Calendar grid day number formatter // We don't use DateTimeFormatter here as it can place extra // character(s) after the number (i.e the `zh` locale) var nf = new Intl.NumberFormat([this.computedLocale], { style: 'decimal', minimumIntegerDigits: 1, minimumFractionDigits: 0, maximumFractionDigits: 0, notation: 'standard' }); // Return a formatter function instance return function (date) { return nf.format(date.getDate()); }; }, // Disabled states for the nav buttons prevDecadeDisabled: function prevDecadeDisabled() { var min = this.computedMin; return this.disabled || min && lastDateOfMonth(oneDecadeAgo(this.activeDate)) < min; }, prevYearDisabled: function prevYearDisabled() { var min = this.computedMin; return this.disabled || min && lastDateOfMonth(oneYearAgo(this.activeDate)) < min; }, prevMonthDisabled: function prevMonthDisabled() { var min = this.computedMin; return this.disabled || min && lastDateOfMonth(oneMonthAgo(this.activeDate)) < min; }, thisMonthDisabled: function thisMonthDisabled() { // TODO: We could/should check if today is out of range return this.disabled; }, nextMonthDisabled: function nextMonthDisabled() { var max = this.computedMax; return this.disabled || max && firstDateOfMonth(oneMonthAhead(this.activeDate)) > max; }, nextYearDisabled: function nextYearDisabled() { var max = this.computedMax; return this.disabled || max && firstDateOfMonth(oneYearAhead(this.activeDate)) > max; }, nextDecadeDisabled: function nextDecadeDisabled() { var max = this.computedMax; return this.disabled || max && firstDateOfMonth(oneDecadeAhead(this.activeDate)) > max; }, // Calendar dates generation calendar: function calendar() { var matrix = []; var firstDay = this.calendarFirstDay; var calendarYear = firstDay.getFullYear(); var calendarMonth = firstDay.getMonth(); var daysInMonth = this.calendarDaysInMonth; var startIndex = firstDay.getDay(); // `0`..`6` var weekOffset = (this.computedWeekStarts > startIndex ? 7 : 0) - this.computedWeekStarts; // TODO: Change `dateInfoFn` to handle events and notes as well as classes var dateInfoFn = isFunction(this.dateInfoFn) ? this.dateInfoFn : function () { return {}; }; // Build the calendar matrix var currentDay = 0 - weekOffset - startIndex; for (var week = 0; week < 6 && currentDay < daysInMonth; week++) { // For each week matrix[week] = []; // The following could be a map function for (var j = 0; j < 7; j++) { // For each day in week currentDay++; var date = createDate(calendarYear, calendarMonth, currentDay); var month = date.getMonth(); var dayYMD = formatYMD(date); var dayDisabled = this.dateDisabled(date); // TODO: This could be a normalizer method var dateInfo = dateInfoFn(dayYMD, parseYMD(dayYMD)); dateInfo = isString(dateInfo) || isArray(dateInfo) ? /* istanbul ignore next */ { class: dateInfo } : isPlainObject(dateInfo) ? _objectSpread({ class: '' }, dateInfo) : /* istanbul ignore next */ { class: '' }; matrix[week].push({ ymd: dayYMD, // Cell content day: this.formatDay(date), label: this.formatDateString(date), // Flags for styling isThisMonth: month === calendarMonth, isDisabled: dayDisabled, // TODO: Handle other dateInfo properties such as notes/events info: dateInfo }); } } return matrix; }, calendarHeadings: function calendarHeadings() { var _this = this; return this.calendar[0].map(function (d) { return { text: _this.formatWeekdayNameShort(parseYMD(d.ymd)), label: _this.formatWeekdayName(parseYMD(d.ymd)) }; }); } }, watch: { value: function value(newVal, oldVal) { var selected = formatYMD(newVal) || ''; var old = formatYMD(oldVal) || ''; if (!datesEqual(selected, old)) { this.activeYMD = selected || this.activeYMD; this.selectedYMD = selected; } }, selectedYMD: function selectedYMD(newYMD, oldYMD) { // TODO: // Should we compare to `formatYMD(this.value)` and emit // only if they are different? if (newYMD !== oldYMD) { this.$emit('input', this.valueAsDate ? parseYMD(newYMD) || null : newYMD || ''); } }, context: function context(newVal, oldVal) { if (!looseEqual(newVal, oldVal)) { this.$emit('context', newVal); } }, hidden: function hidden(newVal) { // Reset the active focused day when hidden this.activeYMD = this.selectedYMD || formatYMD(this.value || this.constrainDate(this.initialDate || this.getToday())); // Enable/disable the live regions this.setLive(!newVal); } }, created: function created() { var _this2 = this; this.$nextTick(function () { _this2.$emit('context', _this2.context); }); }, mounted: function mounted() { this.setLive(true); }, /* istanbul ignore next */ activated: function activated() /* istanbul ignore next */ { this.setLive(true); }, /* istanbul ignore next */ deactivated: function deactivated() /* istanbul ignore next */ { this.setLive(false); }, beforeDestroy: function beforeDestroy() { this.setLive(false); }, methods: { // Public method(s) focus: function focus() { if (!this.disabled) { attemptFocus(this.$refs.grid); } }, blur: function blur() { if (!this.disabled) { attemptBlur(this.$refs.grid); } }, // Private methods setLive: function setLive(on) { var _this3 = this; if (on) { this.$nextTick(function () { requestAF(function () { _this3.isLive = true; }); }); } else { this.isLive = false; } }, getToday: function getToday() { return parseYMD(createDate()); }, constrainDate: function constrainDate(date) { // Constrains a date between min and max // returns a new `Date` object instance return _constrainDate(date, this.computedMin, this.computedMax); }, emitSelected: function emitSelected(date) { var _this4 = this; // Performed in a `$nextTick()` to (probably) ensure // the input event has emitted first this.$nextTick(function () { _this4.$emit('selected', formatYMD(date) || '', parseYMD(date) || null); }); }, // Event handlers setGridFocusFlag: function setGridFocusFlag(evt) { // Sets the gridHasFocus flag to make date "button" look focused this.gridHasFocus = !this.disabled && evt.type === 'focus'; }, onKeydownWrapper: function onKeydownWrapper(evt) { // Calendar keyboard navigation // Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN // Focuses grid after updating if (this.noKeyNav) { /* istanbul ignore next */ return; } var altKey = evt.altKey, ctrlKey = evt.ctrlKey, keyCode = evt.keyCode; if (!arrayIncludes([CODE_PAGEUP, CODE_PAGEDOWN, CODE_END, CODE_HOME, CODE_LEFT, CODE_UP, CODE_RIGHT, CODE_DOWN], keyCode)) { /* istanbul ignore next */ return; } stopEvent(evt); var activeDate = createDate(this.activeDate); var checkDate = createDate(this.activeDate); var day = activeDate.getDate(); var constrainedToday = this.constrainDate(this.getToday()); var isRTL = this.isRTL; if (keyCode === CODE_PAGEUP) { // PAGEUP - Previous month/year activeDate = (altKey ? ctrlKey ? oneDecadeAgo : oneYearAgo : oneMonthAgo)(activeDate); // We check the first day of month to be in rage checkDate = createDate(activeDate); checkDate.setDate(1); } else if (keyCode === CODE_PAGEDOWN) { // PAGEDOWN - Next month/year activeDate = (altKey ? ctrlKey ? oneDecadeAhead : oneYearAhead : oneMonthAhead)(activeDate); // We check the last day of month to be in rage checkDate = createDate(activeDate); checkDate.setMonth(checkDate.getMonth() + 1); checkDate.setDate(0); } else if (keyCode === CODE_LEFT) { // LEFT - Previous day (or next day for RTL) activeDate.setDate(day + (isRTL ? 1 : -1)); activeDate = this.constrainDate(activeDate); checkDate = activeDate; } else if (keyCode === CODE_RIGHT) { // RIGHT - Next day (or previous day for RTL) activeDate.setDate(day + (isRTL ? -1 : 1)); activeDate = this.constrainDate(activeDate); checkDate = activeDate; } else if (keyCode === CODE_UP) { // UP - Previous week activeDate.setDate(day - 7); activeDate = this.constrainDate(activeDate); checkDate = activeDate; } else if (keyCode === CODE_DOWN) { // DOWN - Next week activeDate.setDate(day + 7); activeDate = this.constrainDate(activeDate); checkDate = activeDate; } else if (keyCode === CODE_HOME) { // HOME - Today activeDate = constrainedToday; checkDate = activeDate; } else if (keyCode === CODE_END) { // END - Selected date, or today if no selected date activeDate = parseYMD(this.selectedDate) || constrainedToday; checkDate = activeDate; } if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) { // We only jump to date if within min/max // We don't check for individual disabled dates though (via user function) this.activeYMD = formatYMD(activeDate); } // Ensure grid is focused this.focus(); }, onKeydownGrid: function onKeydownGrid(evt) { // Pressing enter/space on grid to select active date var keyCode = evt.keyCode; var activeDate = this.activeDate; if (keyCode === CODE_ENTER || keyCode === CODE_SPACE) { stopEvent(evt); if (!this.disabled && !this.readonly && !this.dateDisabled(activeDate)) { this.selectedYMD = formatYMD(activeDate); this.emitSelected(activeDate); } // Ensure grid is focused this.focus(); } }, onClickDay: function onClickDay(day) { // Clicking on a date "button" to select it var selectedDate = this.selectedDate, activeDate = this.activeDate; var clickedDate = parseYMD(day.ymd); if (!this.disabled && !day.isDisabled && !this.dateDisabled(clickedDate)) { if (!this.readonly) { // If readonly mode, we don't set the selected date, just the active date // If the clicked date is equal to the already selected date, we don't update the model this.selectedYMD = formatYMD(datesEqual(clickedDate, selectedDate) ? selectedDate : clickedDate); this.emitSelected(clickedDate); } this.activeYMD = formatYMD(datesEqual(clickedDate, activeDate) ? activeDate : createDate(clickedDate)); // Ensure grid is focused this.focus(); } }, gotoPrevDecade: function gotoPrevDecade() { this.activeYMD = formatYMD(this.constrainDate(oneDecadeAgo(this.activeDate))); }, gotoPrevYear: function gotoPrevYear() { this.activeYMD = formatYMD(this.constrainDate(oneYearAgo(this.activeDate))); }, gotoPrevMonth: function gotoPrevMonth() { this.activeYMD = formatYMD(this.constrainDate(oneMonthAgo(this.activeDate))); }, gotoCurrentMonth: function gotoCurrentMonth() { // TODO: Maybe this goto date should be configurable? this.activeYMD = formatYMD(this.constrainDate(this.getToday())); }, gotoNextMonth: function gotoNextMonth() { this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate))); }, gotoNextYear: function gotoNextYear() { this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate))); }, gotoNextDecade: function gotoNextDecade() { this.activeYMD = formatYMD(this.constrainDate(oneDecadeAhead(this.activeDate))); }, onHeaderClick: function onHeaderClick() { if (!this.disabled) { this.activeYMD = this.selectedYMD || formatYMD(this.getToday()); this.focus(); } } }, render: function render(h) { var _this5 = this; // If `hidden` prop is set, render just a placeholder node if (this.hidden) { return h(); } var valueId = this.valueId, widgetId = this.widgetId, navId = this.navId, gridId = this.gridId, gridCaptionId = this.gridCaptionId, gridHelpId = this.gridHelpId, activeId = this.activeId, disabled = this.disabled, noKeyNav = this.noKeyNav, isLive = this.isLive, isRTL = this.isRTL, activeYMD = this.activeYMD, selectedYMD = this.selectedYMD, safeId = this.safeId; var hideDecadeNav = !this.showDecadeNav; var todayYMD = formatYMD(this.getToday()); var highlightToday = !this.noHighlightToday; // Header showing current selected date var $header = h('output', { staticClass: 'form-control form-control-sm text-center', class: { 'text-muted': disabled, readonly: this.readonly || disabled }, attrs: { id: valueId, for: gridId, role: 'status', tabindex: disabled ? null : '-1', // Mainly for testing purposes, as we do not know // the exact format `Intl` will format the date string 'data-selected': toString(selectedYMD), // We wait until after mount to enable `aria-live` // to prevent initial announcement on page render 'aria-live': isLive ? 'polite' : 'off', 'aria-atomic': isLive ? 'true' : null }, on: { // Transfer focus/click to focus grid // and focus active date (or today if no selection) click: this.onHeaderClick, focus: this.onHeaderClick } }, this.selectedDate ? [// We use `bdi` elements here in case the label doesn't match the locale // Although IE 11 does not deal with <BDI> at all (equivalent to a span) h('bdi', { staticClass: 'sr-only' }, " (".concat(toString(this.labelSelected), ") ")), h('bdi', this.formatDateString(this.selectedDate))] : this.labelNoDateSelected || "\xA0" // '&nbsp;' ); $header = h('header', { staticClass: 'b-calendar-header', class: { 'sr-only': this.hideHeader }, attrs: { title: this.selectedDate ? this.labelSelectedDate || null : null } }, [$header]); // Content for the date navigation buttons var navScope = { isRTL: isRTL }; var navProps = { shiftV: 0.5 }; var navPrevProps = _objectSpread(_objectSpread({}, navProps), {}, { flipH: isRTL }); var navNextProps = _objectSpread(_objectSpread({}, navProps), {}, { flipH: !isRTL }); var $prevDecadeIcon = this.normalizeSlot('nav-prev-decade', navScope) || h(BIconChevronBarLeft, { props: navPrevProps }); var $prevYearIcon = this.normalizeSlot('nav-prev-year', navScope) || h(BIconChevronDoubleLeft, { props: navPrevProps }); var $prevMonthIcon = this.normalizeSlot('nav-prev-month', navScope) || h(BIconChevronLeft, { props: navPrevProps }); var $thisMonthIcon = this.normalizeSlot('nav-this-month', navScope) || h(BIconCircleFill, { props: navProps }); var $nextMonthIcon = this.normalizeSlot('nav-next-month', navScope) || h(BIconChevronLeft, { props: navNextProps }); var $nextYearIcon = this.normalizeSlot('nav-next-year', navScope) || h(BIconChevronDoubleLeft, { props: navNextProps }); var $nextDecadeIcon = this.normalizeSlot('nav-next-decade', navScope) || h(BIconChevronBarLeft, { props: navNextProps }); // Utility to create the date navigation buttons var makeNavBtn = function makeNavBtn(content, label, handler, btnDisabled, shortcut) { return h('button', { staticClass: 'btn btn-sm border-0 flex-fill', class: [_this5.computedNavButtonVariant, { disabled: btnDisabled }], attrs: { title: label || null, type: 'button', tabindex: noKeyNav ? '-1' : null, 'aria-label': label || null, 'aria-disabled': btnDisabled ? 'true' : null, 'aria-keyshortcuts': shortcut || null }, on: btnDisabled ? {} : { click: handler } }, [h('div', { attrs: { 'aria-hidden': 'true' } }, [content])]); }; // Generate the date navigation buttons var $nav = h('div', { staticClass: 'b-calendar-nav d-flex', attrs: { id: navId, role: 'group', tabindex: noKeyNav ? '-1' : null, 'aria-hidden': disabled ? 'true' : null, 'aria-label': this.labelNav || null, 'aria-controls': gridId } }, [hideDecadeNav ? h() : makeNavBtn($prevDecadeIcon, this.labelPrevDecade, this.gotoPrevDecade, this.prevDecadeDisabled, 'Ctrl+Alt+PageDown'), makeNavBtn($prevYearIcon, this.labelPrevYear, this.gotoPrevYear, this.prevYearDisabled, 'Alt+PageDown'), makeNavBtn($prevMonthIcon, this.labelPrevMonth, this.gotoPrevMonth, this.prevMonthDisabled, 'PageDown'), makeNavBtn($thisMonthIcon, this.labelCurrentMonth, this.gotoCurrentMonth, this.thisMonthDisabled, 'Home'), makeNavBtn($nextMonthIcon, this.labelNextMonth, this.gotoNextMonth, this.nextMonthDisabled, 'PageUp'), makeNavBtn($nextYearIcon, this.labelNextYear, this.gotoNextYear, this.nextYearDisabled, 'Alt+PageUp'), hideDecadeNav ? h() : makeNavBtn($nextDecadeIcon, this.labelNextDecade, this.gotoNextDecade, this.nextDecadeDisabled, 'Ctrl+Alt+PageUp')]); // Caption for calendar grid var $gridCaption = h('header', { key: 'grid-caption', staticClass: 'b-calendar-grid-caption text-center font-weight-bold', class: { 'text-muted': disabled }, attrs: { id: gridCaptionId, 'aria-live': isLive ? 'polite' : null, 'aria-atomic': isLive ? 'true' : null } }, this.formatYearMonth(this.calendarFirstDay)); // Calendar weekday headings var $gridWeekDays = h('div', { staticClass: 'b-calendar-grid-weekdays row no-gutters border-bottom', attrs: { 'aria-hidden': 'true' } }, this.calendarHeadings.map(function (d, idx) { return h('small', { key: idx, staticClass: 'col text-truncate', class: { 'text-muted': disabled }, attrs: { title: d.label === d.text ? null : d.label, 'aria-label': d.label } }, d.text); })); // Calendar day grid var $gridBody = this.calendar.map(function (week) { var $cells = week.map(function (day, dIndex) { var _class; var isSelected = day.ymd === selectedYMD; var isActive = day.ymd === activeYMD; var isToday = day.ymd === todayYMD; var idCell = safeId("_cell-".concat(day.ymd, "_")); // "fake" button var $btn = h('span', { staticClass: 'btn border-0 rounded-circle text-nowrap', // Should we add some classes to signify if today/selected/etc? class: (_class = { // Give the fake button a focus ring focus: isActive && _this5.gridHasFocus, // Styling disabled: day.isDisabled || disabled, active: isSelected }, _defineProperty(_class, _this5.computedVariant, isSelected), _defineProperty(_class, _this5.computedTodayVariant, isToday && highlightToday && !isSelected && day.isThisMonth), _defineProperty(_class, 'btn-outline-light', !(isToday && highlightToday) && !isSelected && !isActive), _defineProperty(_class, 'btn-light', !(isToday && highlightToday) && !isSelected && isActive), _defineProperty(_class, 'text-muted', !day.isThisMonth && !isSelected), _defineProperty(_class, 'text-dark', !(isToday && highlightToday) && !isSelected && !isActive && day.isThisMonth), _defineProperty(_class, 'font-weight-bold', (isSelected || day.isThisMonth) && !day.isDisabled), _class), on: { click: function click() { return _this5.onClickDay(day); } } }, day.day); return h('div', // Cell with button { key: dIndex, staticClass: 'col p-0', class: day.isDisabled ? 'bg-light' : day.info.class || '', attrs: { id: idCell, role: 'button', 'data-date': day.ymd, // Primarily for testing purposes // Only days in the month are presented as buttons to screen readers 'aria-hidden': day.isThisMonth ? null : 'true', 'aria-disabled': day.isDisabled || disabled ? 'true' : null, 'aria-label': [day.label, isSelected ? "(".concat(_this5.labelSelected, ")") : null, isToday ? "(".concat(_this5.labelToday, ")") : null].filter(identity).join(' '), // NVDA doesn't convey `aria-selected`, but does `aria-current`, // ChromeVox doesn't convey `aria-current`, but does `aria-selected`, // so we set both attributes for robustness 'aria-selected': isSelected ? 'true' : null, 'aria-current': isSelected ? 'date' : null } }, [$btn]); }); // Return the week "row" // We use the first day of the weeks YMD value as a // key for efficient DOM patching / element re-use return h('div', { key: week[0].ymd, staticClass: 'row no-gutters' }, $cells); }); $gridBody = h('div', { // A key is only required on the body if we add in transition support // key: this.activeYMD.slice(0, -3), staticClass: 'b-calendar-grid-body', style: disabled ? { pointerEvents: 'none' } : {} }, $gridBody); var $gridHelp = h('footer', { staticClass: 'b-calendar-grid-help border-top small text-muted text-center bg-light', attrs: { id: gridHelpId } }, [h('div', { staticClass: 'small' }, this.labelHelp)]); var $grid = h('div', { ref: 'grid', staticClass: 'b-calendar-grid form-control h-auto text-center', attrs: { id: gridId, role: 'application', tabindex: noKeyNav ? '-1' : disabled ? null : '0', 'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing 'aria-roledescription': this.labelCalendar || null, 'aria-labelledby': gridCaptionId, 'aria-describedby': gridHelpId, // `aria-readonly` is not considered valid on `role="application"` // https://www.w3.org/TR/wai-aria-1.1/#aria-readonly // 'aria-readonly': this.readonly && !disabled ? 'true' : null, 'aria-disabled': disabled ? 'true' : null, 'aria-activedescendant': activeId }, on: { keydown: this.onKeydownGrid, focus: this.setGridFocusFlag, blur: this.setGridFocusFlag } }, [$gridCaption, $gridWeekDays, $gridBody, $gridHelp]); // Optional bottom slot var $slot = this.normalizeSlot(); $slot = $slot ? h('footer', { staticClass: 'b-calendar-footer' }, $slot) : h(); var $widget = h('div', { staticClass: 'b-calendar-inner', style: this.block ? {} : { width: this.width }, attrs: { id: widgetId, dir: isRTL ? 'rtl' : 'ltr', lang: this.computedLocale || null, role: 'group', 'aria-disabled': disabled ? 'true' : null, // If datepicker controls an input, this will specify the ID of the input 'aria-controls': this.ariaControls || null, // This should be a prop (so it can be changed to Date picker, etc, localized 'aria-roledescription': this.roleDescription || null, 'aria-describedby': [// Should the attr (if present) go last? // Or should this attr be a prop? this.bvAttrs['aria-describedby'], valueId, gridHelpId].filter(identity).join(' ') }, on: { keydown: this.onKeydownWrapper } }, [$header, $nav, $grid, $slot]); // Wrap in an outer div that can be styled return h('div', { staticClass: 'b-calendar', class: { 'd-block': this.block } }, [$widget]); } });