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
JavaScript
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" // ' '
);
$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]);
}
});