StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1031 lines
36 KiB
1031 lines
36 KiB
4 years ago
|
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 '../../utils/vue';
|
||
|
import KeyCodes from '../../utils/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, datesEqual, firstDateOfMonth, formatYMD, lastDateOfMonth, oneMonthAgo, oneMonthAhead, oneYearAgo, oneYearAhead, parseYMD, resolveLocale } from '../../utils/date';
|
||
|
import { requestAF } from '../../utils/dom';
|
||
|
import { isArray, isFunction, isPlainObject, isString } from '../../utils/inspect';
|
||
|
import { toInteger } from '../../utils/number';
|
||
|
import { toString } from '../../utils/string';
|
||
|
import idMixin from '../../mixins/id';
|
||
|
import normalizeSlotMixin from '../../mixins/normalize-slot';
|
||
|
import { BIconChevronLeft, BIconCircleFill } from '../../icons/icons';
|
||
|
import { BIconstack } from '../../icons/iconstack'; // --- Constants ---
|
||
|
|
||
|
var NAME = 'BCalendar'; // Key Codes
|
||
|
|
||
|
var UP = KeyCodes.UP,
|
||
|
DOWN = KeyCodes.DOWN,
|
||
|
LEFT = KeyCodes.LEFT,
|
||
|
RIGHT = KeyCodes.RIGHT,
|
||
|
PAGEUP = KeyCodes.PAGEUP,
|
||
|
PAGEDOWN = KeyCodes.PAGEDOWN,
|
||
|
HOME = KeyCodes.HOME,
|
||
|
END = KeyCodes.END,
|
||
|
ENTER = KeyCodes.ENTER,
|
||
|
SPACE = KeyCodes.SPACE; // Languages that are RTL
|
||
|
|
||
|
var RTL_LANGS = ['ar', 'az', 'ckb', 'fa', 'he', 'ks', 'lrc', 'mzn', 'ps', 'sd', 'te', 'ug', 'ur', 'yi'].map(function (locale) {
|
||
|
return locale.toLowerCase();
|
||
|
}); // --- Helper utilities ---
|
||
|
|
||
|
export var isLocaleRTL = function isLocaleRTL(locale) {
|
||
|
// Determines if the locale is RTL (only single locale supported)
|
||
|
var parts = toString(locale).toLowerCase().replace(/-u-.+/, '').split('-');
|
||
|
var locale1 = parts.slice(0, 2).join('-');
|
||
|
var locale2 = parts[0];
|
||
|
return arrayIncludes(RTL_LANGS, locale1) || arrayIncludes(RTL_LANGS, locale2);
|
||
|
}; // --- BCalendar component ---
|
||
|
// @vue/component
|
||
|
|
||
|
export var BCalendar = Vue.extend({
|
||
|
name: NAME,
|
||
|
mixins: [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
|
||
|
},
|
||
|
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: 'primary'
|
||
|
},
|
||
|
todayVariant: {
|
||
|
// Variant color to use for today's date (defaults to `variant`)
|
||
|
type: String // default: null
|
||
|
|
||
|
},
|
||
|
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
|
||
|
},
|
||
|
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
|
||
|
|
||
|
},
|
||
|
roleDescription: {
|
||
|
type: String // default: null
|
||
|
|
||
|
},
|
||
|
// Labels for buttons and keyboard shortcuts
|
||
|
labelPrevYear: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelPrevYear');
|
||
|
}
|
||
|
},
|
||
|
labelPrevMonth: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelPrevMonth');
|
||
|
}
|
||
|
},
|
||
|
labelCurrentMonth: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelCurrentMonth');
|
||
|
}
|
||
|
},
|
||
|
labelNextMonth: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelNextMonth');
|
||
|
}
|
||
|
},
|
||
|
labelNextYear: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelNextYear');
|
||
|
}
|
||
|
},
|
||
|
labelToday: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelToday');
|
||
|
}
|
||
|
},
|
||
|
labelSelected: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelSelected');
|
||
|
}
|
||
|
},
|
||
|
labelNoDateSelected: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelNoDateSelected');
|
||
|
}
|
||
|
},
|
||
|
labelCalendar: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelCalendar');
|
||
|
}
|
||
|
},
|
||
|
labelNav: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelNav');
|
||
|
}
|
||
|
},
|
||
|
labelHelp: {
|
||
|
type: String,
|
||
|
default: function _default() {
|
||
|
return getComponentConfig(NAME, 'labelHelp');
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
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(this.getToday()),
|
||
|
// 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: {
|
||
|
// 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 Math.max(toInteger(this.startWeekday) || 0, 0) % 7;
|
||
|
},
|
||
|
computedLocale: function computedLocale() {
|
||
|
// Returns the resolved locale used by the calendar
|
||
|
return resolveLocale(concat(this.locale).filter(identity), '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: '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 !== '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() {
|
||
|
return createDate(this.calendarYear, this.calendarMonth, 1);
|
||
|
},
|
||
|
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');
|
||
|
},
|
||
|
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;
|
||
|
var selectedDate = parseYMD(selectedYMD);
|
||
|
var activeYMD = this.activeYMD;
|
||
|
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 wether 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;
|
||
|
var 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, {
|
||
|
year: 'numeric',
|
||
|
month: 'long',
|
||
|
day: 'numeric',
|
||
|
weekday: 'long',
|
||
|
calendar: 'gregory'
|
||
|
});
|
||
|
},
|
||
|
formatYearMonth: function formatYearMonth() {
|
||
|
// Returns a date formatter function
|
||
|
return createDateFormatter(this.calendarLocale, {
|
||
|
year: 'numeric',
|
||
|
month: 'long',
|
||
|
calendar: 'gregory'
|
||
|
});
|
||
|
},
|
||
|
formatWeekdayName: function formatWeekdayName() {
|
||
|
return createDateFormatter(this.calendarLocale, {
|
||
|
weekday: 'long',
|
||
|
calendar: 'gregory'
|
||
|
});
|
||
|
},
|
||
|
formatWeekdayNameShort: function formatWeekdayNameShort() {
|
||
|
// Used as the header cells
|
||
|
return createDateFormatter(this.calendarLocale, {
|
||
|
weekday: 'short',
|
||
|
calendar: 'gregory'
|
||
|
});
|
||
|
},
|
||
|
formatDay: function formatDay() {
|
||
|
return createDateFormatter(this.calendarLocale, {
|
||
|
day: 'numeric',
|
||
|
calendar: 'gregory'
|
||
|
});
|
||
|
},
|
||
|
// Disabled states for the nav buttons
|
||
|
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;
|
||
|
},
|
||
|
// Calendar 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) ? {
|
||
|
class: dateInfo
|
||
|
} : isPlainObject(dateInfo) ? _objectSpread({
|
||
|
class: ''
|
||
|
}, dateInfo) : {
|
||
|
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) || formatYMD(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);
|
||
|
},
|
||
|
activated: function activated()
|
||
|
/* istanbul ignore next */
|
||
|
{
|
||
|
this.setLive(true);
|
||
|
},
|
||
|
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) {
|
||
|
try {
|
||
|
this.$refs.grid.focus();
|
||
|
} catch (_unused) {}
|
||
|
}
|
||
|
},
|
||
|
blur: function blur() {
|
||
|
try {
|
||
|
this.$refs.grid.blur();
|
||
|
} catch (_unused2) {}
|
||
|
},
|
||
|
// 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
|
||
|
date = parseYMD(date);
|
||
|
var min = this.computedMin || date;
|
||
|
var max = this.computedMax || date;
|
||
|
return createDate(date < min ? min : date > max ? max : date);
|
||
|
},
|
||
|
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
|
||
|
var keyCode = evt.keyCode;
|
||
|
var altKey = evt.altKey;
|
||
|
|
||
|
if (!arrayIncludes([PAGEUP, PAGEDOWN, END, HOME, LEFT, UP, RIGHT, DOWN], keyCode)) {
|
||
|
/* istanbul ignore next */
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
evt.preventDefault();
|
||
|
evt.stopPropagation();
|
||
|
var activeDate = createDate(this.activeDate);
|
||
|
var checkDate = createDate(this.activeDate);
|
||
|
var day = activeDate.getDate();
|
||
|
var isRTL = this.isRTL;
|
||
|
|
||
|
if (keyCode === PAGEUP) {
|
||
|
// PAGEUP - Previous month/year
|
||
|
activeDate = (altKey ? oneYearAgo : oneMonthAgo)(activeDate); // We check the first day of month to be in rage
|
||
|
|
||
|
checkDate = createDate(activeDate);
|
||
|
checkDate.setDate(1);
|
||
|
} else if (keyCode === PAGEDOWN) {
|
||
|
// PAGEDOWN - Next month/year
|
||
|
activeDate = (altKey ? 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 === LEFT) {
|
||
|
// LEFT - Previous day (or next day for RTL)
|
||
|
activeDate.setDate(day + (isRTL ? 1 : -1));
|
||
|
checkDate = activeDate;
|
||
|
} else if (keyCode === RIGHT) {
|
||
|
// RIGHT - Next day (or previous day for RTL)
|
||
|
activeDate.setDate(day + (isRTL ? -1 : 1));
|
||
|
checkDate = activeDate;
|
||
|
} else if (keyCode === UP) {
|
||
|
// UP - Previous week
|
||
|
activeDate.setDate(day - 7);
|
||
|
checkDate = activeDate;
|
||
|
} else if (keyCode === DOWN) {
|
||
|
// DOWN - Next week
|
||
|
activeDate.setDate(day + 7);
|
||
|
checkDate = activeDate;
|
||
|
} else if (keyCode === HOME) {
|
||
|
// HOME - Today
|
||
|
activeDate = this.getToday();
|
||
|
checkDate = activeDate;
|
||
|
} else if (keyCode === END) {
|
||
|
// END - Selected date, or today if no selected date
|
||
|
activeDate = parseYMD(this.selectedDate) || this.getToday();
|
||
|
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 === ENTER || keyCode === SPACE) {
|
||
|
evt.preventDefault();
|
||
|
evt.stopPropagation();
|
||
|
|
||
|
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
|
||
|
// TODO: Change to lookup the `data-data` attribute
|
||
|
var selectedDate = this.selectedDate;
|
||
|
var 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();
|
||
|
}
|
||
|
},
|
||
|
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.getToday());
|
||
|
},
|
||
|
gotoNextMonth: function gotoNextMonth() {
|
||
|
this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate)));
|
||
|
},
|
||
|
gotoNextYear: function gotoNextYear() {
|
||
|
this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate)));
|
||
|
}
|
||
|
},
|
||
|
render: function render(h) {
|
||
|
var _this5 = this;
|
||
|
|
||
|
// If hidden prop is set, render just a placeholder node
|
||
|
if (this.hidden) {
|
||
|
return h();
|
||
|
}
|
||
|
|
||
|
var isRTL = this.isRTL;
|
||
|
var todayYMD = formatYMD(this.getToday());
|
||
|
var selectedYMD = this.selectedYMD;
|
||
|
var activeYMD = this.activeYMD;
|
||
|
var highlightToday = !this.noHighlightToday;
|
||
|
var safeId = this.safeId; // Flag for making the `aria-live` regions live
|
||
|
|
||
|
var isLive = this.isLive; // Pre-compute some IDs
|
||
|
|
||
|
var idWidget = safeId();
|
||
|
var idValue = safeId('_calendar-value_');
|
||
|
var idNav = safeId('_calendar-nav_');
|
||
|
var idGrid = safeId('_calendar-grid_');
|
||
|
var idGridCaption = safeId('_calendar-grid-caption_');
|
||
|
var idGridHelp = safeId('_calendar-grid-help_');
|
||
|
var idActive = activeYMD ? safeId("_cell-".concat(activeYMD, "_")) : null; // Header showing current selected date
|
||
|
|
||
|
var $header = h('output', {
|
||
|
staticClass: 'd-block text-center rounded border small p-1 mb-1',
|
||
|
class: {
|
||
|
'text-muted': this.disabled,
|
||
|
readonly: this.readonly || this.disabled
|
||
|
},
|
||
|
attrs: {
|
||
|
id: idValue,
|
||
|
for: idGrid,
|
||
|
role: 'status',
|
||
|
// 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
|
||
|
}
|
||
|
}, 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', {
|
||
|
class: this.hideHeader ? 'sr-only' : 'mb-1',
|
||
|
attrs: {
|
||
|
title: this.selectedDate ? this.labelSelectedDate || null : null
|
||
|
}
|
||
|
}, [$header]); // Content for the date navigation buttons
|
||
|
|
||
|
var $prevYearIcon = h(BIconstack, {
|
||
|
props: {
|
||
|
shiftV: 0.5,
|
||
|
flipH: isRTL
|
||
|
}
|
||
|
}, [h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftH: -2
|
||
|
}
|
||
|
}), h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftH: 2
|
||
|
}
|
||
|
})]);
|
||
|
var $prevMonthIcon = h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftV: 0.5,
|
||
|
flipH: isRTL
|
||
|
}
|
||
|
});
|
||
|
var $thisMonthIcon = h(BIconCircleFill, {
|
||
|
props: {
|
||
|
shiftV: 0.5
|
||
|
}
|
||
|
});
|
||
|
var $nextMonthIcon = h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftV: 0.5,
|
||
|
flipH: !isRTL
|
||
|
}
|
||
|
});
|
||
|
var $nextYearIcon = h(BIconstack, {
|
||
|
props: {
|
||
|
shiftV: 0.5,
|
||
|
flipH: !isRTL
|
||
|
}
|
||
|
}, [h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftH: -2
|
||
|
}
|
||
|
}), h(BIconChevronLeft, {
|
||
|
props: {
|
||
|
shiftH: 2
|
||
|
}
|
||
|
})]); // Utility to create the date navigation buttons
|
||
|
|
||
|
var makeNavBtn = function makeNavBtn(content, label, handler, btnDisabled, shortcut) {
|
||
|
return h('button', {
|
||
|
staticClass: 'btn btn-sm btn-outline-secondary border-0 flex-fill p-1 mx-1',
|
||
|
class: {
|
||
|
disabled: btnDisabled
|
||
|
},
|
||
|
attrs: {
|
||
|
title: label || null,
|
||
|
type: 'button',
|
||
|
'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 mx-n1 mb-1',
|
||
|
attrs: {
|
||
|
id: idNav,
|
||
|
role: 'group',
|
||
|
'aria-hidden': this.disabled ? 'true' : null,
|
||
|
'aria-label': this.labelNav || null,
|
||
|
'aria-controls': idGrid
|
||
|
}
|
||
|
}, [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')]); // Caption for calendar grid
|
||
|
|
||
|
var $gridCaption = h('header', {
|
||
|
key: 'grid-caption',
|
||
|
staticClass: 'text-center font-weight-bold p-1 m-0',
|
||
|
class: {
|
||
|
'text-muted': this.disabled
|
||
|
},
|
||
|
attrs: {
|
||
|
id: idGridCaption,
|
||
|
'aria-live': isLive ? 'polite' : null,
|
||
|
'aria-atomic': isLive ? 'true' : null
|
||
|
}
|
||
|
}, this.formatYearMonth(this.calendarFirstDay)); // Calendar weekday headings
|
||
|
|
||
|
var $gridWeekDays = h('div', {
|
||
|
staticClass: 'row no-gutters border-bottom',
|
||
|
attrs: {
|
||
|
'aria-hidden': 'true'
|
||
|
}
|
||
|
}, this.calendarHeadings.map(function (d, idx) {
|
||
|
return h('small', {
|
||
|
key: idx,
|
||
|
staticClass: 'col',
|
||
|
class: {
|
||
|
'text-muted': _this5.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 || _this5.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 || _this5.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: this.disabled ? {
|
||
|
pointerEvents: 'none'
|
||
|
} : {}
|
||
|
}, $gridBody);
|
||
|
var $gridHelp = h('footer', {
|
||
|
staticClass: 'border-top small text-muted text-center bg-light',
|
||
|
attrs: {
|
||
|
id: idGridHelp
|
||
|
}
|
||
|
}, [h('div', {
|
||
|
staticClass: 'small'
|
||
|
}, this.labelHelp)]);
|
||
|
var $grid = h('div', {
|
||
|
ref: 'grid',
|
||
|
staticClass: 'form-control h-auto text-center p-0 mb-0',
|
||
|
attrs: {
|
||
|
id: idGrid,
|
||
|
role: 'application',
|
||
|
tabindex: this.disabled ? null : '0',
|
||
|
'data-month': activeYMD.slice(0, -3),
|
||
|
// `YYYY-MM`, mainly for testing
|
||
|
// tabindex: this.disabled ? null : '0',
|
||
|
'aria-roledescription': this.labelCalendar || null,
|
||
|
'aria-labelledby': idGridCaption,
|
||
|
'aria-describedby': idGridHelp,
|
||
|
// `aria-readonly` is not considered valid on `role="application"`
|
||
|
// https://www.w3.org/TR/wai-aria-1.1/#aria-readonly
|
||
|
// 'aria-readonly': this.readonly && !this.disabled ? 'true' : null,
|
||
|
'aria-disabled': this.disabled ? 'true' : null,
|
||
|
'aria-activedescendant': idActive
|
||
|
},
|
||
|
on: {
|
||
|
keydown: this.onKeydownGrid,
|
||
|
focus: this.setGridFocusFlag,
|
||
|
blur: this.setGridFocusFlag
|
||
|
}
|
||
|
}, [$gridCaption, $gridWeekDays, $gridBody, $gridHelp]); // Optional bottom slot
|
||
|
|
||
|
var $slot = this.normalizeSlot('default');
|
||
|
$slot = $slot ? h('footer', {
|
||
|
staticClass: 'mt-2'
|
||
|
}, $slot) : h();
|
||
|
var $widget = h('div', {
|
||
|
class: this.block ? 'd-block' : 'd-inline-block',
|
||
|
style: this.block ? {} : {
|
||
|
width: this.width
|
||
|
},
|
||
|
attrs: {
|
||
|
id: idWidget,
|
||
|
dir: isRTL ? 'rtl' : 'ltr',
|
||
|
lang: this.computedLocale || null,
|
||
|
role: 'group',
|
||
|
'aria-disabled': this.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.$attrs['aria-describedby'], idValue, idGridHelp].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',
|
||
|
// We use a style here rather than class `d-inline-block` so that users can
|
||
|
// override the display value (`d-*` classes use the `!important` flag)
|
||
|
style: this.block ? {} : {
|
||
|
display: 'inline-block'
|
||
|
}
|
||
|
}, [$widget]);
|
||
|
}
|
||
|
});
|