coral-spectrum/coral-component-calendar/src/scripts/Calendar.js
/**
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {BaseComponent} from '../../../coral-base-component';
import {BaseFormField} from '../../../coral-base-formfield';
import {DateTime} from '../../../coral-datetime';
import '../../../coral-component-button';
import {Icon} from '../../../coral-component-icon';
import calendar from '../templates/calendar';
import container from '../templates/container';
import table from '../templates/table';
import {transform, commons, i18n, Keys} from '../../../coral-utils';
import {Decorator} from '../../../coral-decorator';
/** @ignore */
function isDateInRange(date, startDate, endDate) {
if (!date) {
return false;
}
if (startDate === null && endDate === null) {
return true;
}
else if (startDate === null) {
return date.toDate() <= endDate;
}
else if (endDate === null) {
return date.toDate() >= startDate;
}
return startDate <= date.toDate() && date.toDate() <= endDate;
}
/** @ignore */
function toMoment(value, format) {
if (value === 'today') {
return new DateTime.Moment().startOf('day');
}
else if (DateTime.Moment.isMoment(value)) {
return value.isValid() ? value.clone() : null;
}
// if the value provided is a date it does not make sense to provide a format to parse the date
const result = new DateTime.Moment(value, value instanceof Date ? null : format);
return result.isValid() ? result.startOf('day') : null;
}
/** @ignore */
function validateAsChangedAndValidMoment(newValue, oldValue) {
// if the value is undefined we change it to null since moment considers both to be different
newValue = newValue || null;
oldValue = oldValue || null;
if (newValue !== oldValue && !new DateTime.Moment(newValue).isSame(oldValue, 'day')) {
return newValue === null || newValue.isValid();
}
return false;
}
/**
Slides in new month tables, slides out old tables, and then cleans up the leftovers when it is done.
@ignore
*/
function TableAnimator(host) {
this.host = host;
this._addContainerIfNotPresent = (width, height) => {
if (!this.container) {
// Get a fresh container for the animation:
container.call(
this,
{
width,
height
}
);
this.host.appendChild(this.container);
}
};
this._removeContainerIfEmpty = () => {
if (this.container && this.container.children.length === 0) {
this.host.removeChild(this.container);
this.container = null;
}
};
this.slide = (newTable, direction) => {
const replace = direction === undefined;
const isLeft = direction === 'left';
const oldTable = this.oldTable;
// Should the replace flag be raised, or no old table be present, then do a non-transitioned (re)place and exit
if (replace || !oldTable) {
if (oldTable) {
oldTable.parentNode.removeChild(oldTable);
}
this.host.insertBefore(newTable, this.host.firstChild);
this.oldTable = newTable;
return;
}
const boundingClientRect = oldTable.getBoundingClientRect();
const width = boundingClientRect.width;
let height = boundingClientRect.height;
this._addContainerIfNotPresent(width, height);
// Add both the old and the new table to the container:
this.container.appendChild(oldTable);
this.container.appendChild(newTable);
// Set the existing table to start from being in full view, and mark it to transition on `left` changing
oldTable.classList.add('_coral-Calendar-table--transit');
commons.transitionEnd(oldTable, () => {
oldTable.parentNode.removeChild(oldTable);
this._removeContainerIfEmpty();
});
// Set the new table to start out of view (either left or right depending on the direction of the slide), and mark
// it to transition on `left` changing
newTable.classList.add('_coral-Calendar-table--transit');
newTable.style.left = `${isLeft ? width : -width}px`;
// When the transition is done, have the transition class lifted
commons.transitionEnd(newTable, () => {
newTable.classList.remove('_coral-Calendar-table--transit');
this.host.appendChild(newTable);
this._removeContainerIfEmpty();
});
// Force a redraw by querying the browser for its offsetWidth. Without this, the re-positioning code later on
// would not lead to a transition. Note that there's no significance to the resulting value being assigned to
// 'height'
height = this.container.offsetWidth;
// Set the `left` positions to transition to:
oldTable.style.left = `${isLeft ? -width : width}px`;
newTable.style.left = 0;
this.oldTable = newTable;
};
}
/** @ignore */
const ARRAYOF6 = [0, 0, 0, 0, 0, 0];
/** @ignore */
const ARRAYOF7 = [0, 0, 0, 0, 0, 0, 0];
/** @ignore */
const INTERNAL_FORMAT = 'YYYY-MM-DD';
/** @ignore */
const timeUnit = {
YEAR: 'year',
MONTH: 'month',
WEEK: 'week',
DAY: 'day'
};
const CLASSNAME = '_coral-Calendar';
/**
@class Coral.Calendar
@classdesc A Calendar component that can be used as a date selection form field. Leverages {@link momentJS} if loaded
on the page.
@htmltag coral-calendar
@extends {HTMLElement}
@extends {BaseComponent}
@extends {BaseFormField}
*/
const Calendar = Decorator(class extends BaseFormField(BaseComponent(HTMLElement)) {
/** @ignore */
constructor() {
super();
// Default value
this._value = null;
this._delegateEvents(commons.extend(this._events, {
'click ._coral-Calendar-nextMonth,._coral-Calendar-prevMonth': '_onNextOrPreviousMonthClick',
'click ._coral-Calendar-body ._coral-Calendar-date': '_onDayClick',
'capture:focus ._coral-Calendar-body': '_onBodyFocus',
'mousedown ._coral-Calendar-body ._coral-Calendar-date': '_onDayMouseDown',
'key:up ._coral-Calendar-body': '_onUpKey',
'key:right ._coral-Calendar-body': '_onRightKey',
'key:down ._coral-Calendar-body': '_onDownKey',
'key:left ._coral-Calendar-body': '_onLeftKey',
'key:home ._coral-Calendar-body': '_onHomeOrEndKey',
'key:end ._coral-Calendar-body': '_onHomeOrEndKey',
'key:pageup': '_onPageUpKey',
'key:pagedown': '_onPageDownKey',
// On OSX we use Command+Page Up
'key:meta+pageup': '_onCtrlPageUpKey',
// On OSX we use Command+Page Down
'key:meta+pagedown': '_onCtrlPageDownKey',
// On Windows, we use CTRL+Page Up
'key:ctrl+pageup': '_onCtrlPageUpKey',
// On Windows, we use CTRL+Page Down
'key:ctrl+pagedown': '_onCtrlPageDownKey',
// Use alt+pageup/alt+pagedown and shift+pageup/shift+pagedown to jump by year
'key:alt+pageup': '_onCtrlPageUpKey',
'key:alt+pagedown': '_onCtrlPageDownKey',
'key:shift+pageup': '_onCtrlPageUpKey',
'key:shift+pagedown': '_onCtrlPageDownKey',
'key:enter ._coral-Calendar-body': '_onEnterKey',
'key:return ._coral-Calendar-body': '_onEnterKey',
'key:space ._coral-Calendar-body': '_onEnterKey'
}));
// Prepare templates
this._elements = {};
calendar.call(this._elements, {commons, i18n, Icon});
// Pre-define labellable element
this._labellableElement = this;
// Internal keeper of the month that is currently on display.
this._cursor = null;
// Internal keeper for the id of the currently focused date cell or the cell that would receive focus when the
// calendar body receives focus.
this._activeDescendant = null;
this._animator = new TableAnimator(this._elements.body);
}
/**
Defines the start day for the week, 0 = Sunday, 1 = Monday etc., as depicted on the calendar days grid.
@type {Number}
@default 0
@htmlattribute startday
*/
get startDay() {
if (this._startDay) {
return this._startDay;
}
if (typeof DateTime.Moment.localeData(i18n.locale).firstDayOfWeek !== 'undefined') {
return DateTime.Moment.localeData(i18n.locale).firstDayOfWeek();
}
return 0;
}
set startDay(value) {
value = transform.number(value);
if (value >= 0 && value < 7) {
this._startDay = value;
this._renderCalendar();
}
}
/**
The format used to display the current month and year.
'MMMM YYYY' is supported by default. Include momentjs to support additional format string options see
http://momentjs.com/docs/#/displaying/.
@type {String}
@default "MMMM YYYY"
@htmlattribute headerformat
*/
get headerFormat() {
return this._headerFormat || 'MMMM YYYY';
}
set headerFormat(value) {
this._headerFormat = transform.string(value);
this._renderCalendar();
}
/**
The minimal selectable date in the Calendar view. When passed a string, it needs to be 'YYYY-MM-DD' formatted.
@type {String|Date}
@default null
@htmlattribute min
*/
get min() {
return this._min ? this._min.toDate() : null;
}
set min(value) {
value = toMoment(value, this.valueFormat);
if (validateAsChangedAndValidMoment(value, this._min)) {
this._min = value;
this._renderCalendar();
}
}
/**
The max selectable date in the Calendar view. When passed a string, it needs to be 'YYYY-MM-DD'
formatted.
@type {String|Date}
@default null
@htmlattribute max
*/
get max() {
return this._max ? this._max.toDate() : null;
}
set max(value) {
value = toMoment(value, this.valueFormat);
if (validateAsChangedAndValidMoment(value, this._max)) {
this._max = value;
this._renderCalendar();
}
}
/**
The format to use on expressing the selected date as a string on the <code>value</code> attribute.
'YYYY-MM-DD' is supported by default. Include momentjs to support additional format string options see
http://momentjs.com/docs/#/displaying/.
@type {String}
@default "YYYY-MM-DD"
@htmlattribute valueformat
@htmlattributereflected
*/
get valueFormat() {
return this._valueFormat || INTERNAL_FORMAT;
}
set valueFormat(value) {
value = transform.string(value);
const setValueFormat = (newValue) => {
this._valueFormat = newValue;
this._reflectAttribute('valueformat', this._valueFormat);
};
// Once the valueFormat is set, we make sure the value is also correct
if (!this._valueFormat && this._originalValue) {
setValueFormat(value);
this.value = this._originalValue;
}
else {
setValueFormat(value);
this._elements.input.value = this.value;
}
}
/**
The value returned, or set, as a Date. If the value is '' it will return <code>null</code>.
@type {Date}
@default null
*/
get valueAsDate() {
return this._value ? this._value.toDate() : null;
}
set valueAsDate(value) {
if (value instanceof Date) {
this._valueAsDate = new DateTime.Moment(value);
this.value = this._valueAsDate;
}
else {
this._valueAsDate = null;
this.value = '';
}
}
/**
The current value. When set to 'today', the value is coerced into the clients local date expressed as string
formatted in accordance to the set <code>valueFormat</code>.
@type {String}
@default ""
@htmlattribute value
*/
get value() {
return this._value ? this._value.format(this.valueFormat) : '';
}
set value(value) {
// This is used to change the value if valueformat is also set but afterwards
this._originalValue = value;
value = toMoment(value, this.valueFormat);
if (validateAsChangedAndValidMoment(value, this._value)) {
this._value = value;
this._elements.input.value = this.value;
// resets the view cursor, so the selected month will be in view
this._cursor = null;
this._renderCalendar();
this.required = this.required;
}
}
/**
Name used to submit the data in a form.
@type {String}
@default ""
@htmlattribute name
@htmlattributereflected
*/
get name() {
return this._elements.input.name;
}
set name(value) {
this._reflectAttribute('name', value);
this._elements.input.name = value;
}
/**
Whether this field is required or not.
@type {Boolean}
@default false
@htmlattribute required
@htmlattributereflected
*/
get required() {
return this._required || false;
}
set required(value) {
this._required = transform.booleanAttr(value);
this._reflectAttribute('required', this._required);
this.classList.toggle('is-required', this._required && this._value === null);
}
/**
Whether this field is disabled or not.
@type {Boolean}
@default false
@htmlattribute disabled
@htmlattributereflected
*/
get disabled() {
return this._disabled || false;
}
set disabled(value) {
this._disabled = transform.booleanAttr(value);
this._reflectAttribute('disabled', this._disabled);
this.classList.toggle('is-disabled', this._disabled);
this._elements.prev.disabled = this._disabled;
this._elements.next.disabled = this._disabled;
this._elements.body[this._disabled ? 'setAttribute' : 'removeAttribute']('aria-disabled', this._disabled);
this._elements.body[this._disabled ? 'removeAttribute' : 'setAttribute']('tabindex', '0');
this._renderCalendar();
}
/**
Inherited from {@link BaseFormField#invalid}.
*/
get invalid() {
return super.invalid;
}
set invalid(value) {
super.invalid = value;
this._renderCalendar();
}
/**
Whether this field is readOnly or not. Indicating that the user cannot modify the value of the control.
@type {Boolean}
@default false
@htmlattribute readonly
@htmlattributereflected
*/
get readOnly() {
return this._readOnly || false;
}
set readOnly(value) {
this._readOnly = transform.booleanAttr(value);
this._reflectAttribute('readonly', this._readOnly);
this._elements.prev.disabled = this._readOnly;
this._elements.next.disabled = this._readOnly;
this._elements.body[this._readOnly ? 'removeAttribute' : 'setAttribute']('tabindex', '0');
this.classList.toggle('is-readOnly', this._readOnly);
}
/** @ignore */
_renderCalendar(slide) {
const cursor = this._requireCursor();
const displayYear = cursor.year();
const displayMonth = cursor.month();
const oldTable = this._animator.oldTable;
this._elements.heading.innerHTML = new DateTime.Moment([displayYear, displayMonth, 1]).format(this.headerFormat);
const newTable = this._renderTable(displayYear, displayMonth + 1);
if (oldTable) {
commons.transitionEnd(newTable, () => {
this._setActiveDescendant();
});
}
this._animator.slide(newTable, slide);
const el = this._elements.body.querySelector('.is-selected');
// This will be overwritten later if there is any other function setting the attribute
this._activeDescendant = el ? el.id : null;
this._setActiveDescendant();
}
/**
Returns <code>true</code> if moment specified is before <code>min</code>.
@param {moment} currentMoment
A moment to test.
@param {String} unit
Year, Month, Week, Day
@returns {Boolean}
<code>true</code> if moment specified is before <code>min</code>
@ignore
*/
_isBeforeMin(currentMoment, unit) {
const min = this.min ? new DateTime.Moment(this.min) : null;
return min && currentMoment.isBefore(min, unit);
}
/**
Returns <code>true</code> if moment specified is after <code>max</code>.
@param {moment} currentMoment
A moment to test.
@param {String} unit
Year, Month, Week, Day
@returns {Boolean}
<code>true</code> if moment specified is after <code>max</code>
@ignore
*/
_isAfterMax(currentMoment, unit) {
const max = this.max ? new DateTime.Moment(this.max) : null;
return max && currentMoment.isAfter(max, unit);
}
/**
Returns <code>true</code> if moment specified is greater than or equal to <code>min</code> and less than or equal to <code>max</code>.
@param {moment} currentMoment
A moment to test.
@param {String} unit
Year, Month, Week, Day
@returns {Boolean}
<code>true</code> if moment specified falls within <code>min</code>/<code>max</code> date range.
@ignore
*/
_isInRange(currentMoment, unit) {
return !(this._isBeforeMin(currentMoment, unit) || this._isAfterMax(currentMoment, unit));
}
/**
Updates the aria-activedescendant property for the calendar grid to communicate the currently focused date, or the
date that should get focus when the grid receives focus, to assistive technology.
@ignore
*/
_setActiveDescendant() {
let el;
const isActiveDescendantMissing = () => !this._activeDescendant || !this._elements.body.querySelector(`#${this._activeDescendant} [data-date]`);
if (isActiveDescendantMissing()) {
this._activeDescendant = null;
el = this._elements.body.querySelector('.is-selected');
this._activeDescendant = el && el.id;
if (isActiveDescendantMissing()) {
const currentMoment = this._value;
if (currentMoment) {
const dates = this._elements.body.querySelectorAll('[data-date]');
if (this._isBeforeMin(currentMoment)) {
el = dates[0];
}
else if (this._isAfterMax(currentMoment)) {
el = dates.length ? dates[dates.length - 1] : null;
}
}
else {
el = this._elements.body.querySelector('.is-focused') || this._elements.body.querySelector('.is-today');
}
if (el) {
this._activeDescendant = el.parentElement.id;
}
}
}
el = this._elements.body.querySelector('.is-focused');
if (el) {
el.classList.remove('is-focused');
}
this._elements.body[this._activeDescendant ? 'setAttribute' : 'removeAttribute']('aria-activedescendant', this._activeDescendant);
this._updateTableCaption();
if (!this._activeDescendant) {
return;
}
el = document.getElementById(this._activeDescendant);
if (!el) {
return;
}
const newTable = this.querySelector('._coral-Calendar-table--transit');
const isTransitioning = newTable !== null;
if (isTransitioning) {
window.requestAnimationFrame(() => {
el.querySelector('._coral-Calendar-date').classList.add('is-focused');
});
}
else {
// Focus the selected date
el.querySelector('._coral-Calendar-date').classList.add('is-focused');
}
}
/**
Updates the table caption which serves as a live region to announce the currently focused date to assistive
technology, improving compatibility across operating systems, browsers and screen readers.
@ignore
*/
_updateTableCaption() {
const caption = this._elements.body.querySelector('caption');
if (!caption) {
return;
}
if (caption.firstChild) {
caption.removeChild(caption.firstChild);
}
if (this._activeDescendant) {
const activeDescendant = this._elements.body.querySelector(`#${this._activeDescendant}`);
const captionText = document.createTextNode(activeDescendant.getAttribute('title'));
caption.appendChild(captionText);
}
}
/** @ignore */
_renderTable(year, month) {
const firstDate = new DateTime.Moment([year, month - 1, 1]);
let monthStartsAt = (firstDate.day() - this.startDay) % 7;
const dateLocal = this._value ? this._value.clone().startOf('day') : null;
if (monthStartsAt < 0) {
monthStartsAt += 7;
}
const data = {
i18n: i18n,
commons: commons,
// eslint-disable-next-line no-unused-vars
dayNames: ARRAYOF7.map((currentIndex, index) => {
const dayMoment = new DateTime.Moment().day((index + this.startDay) % 7);
return {
dayAbbr: dayMoment.format('dd'),
dayFullName: dayMoment.format('dddd')
};
}, this),
// eslint-disable-next-line no-unused-vars, arrow-body-style
weeks: ARRAYOF6.map((currentWeekIndex, weekIndex) => {
// eslint-disable-next-line no-unused-vars
return ARRAYOF7.map((currentDayIndex, dayIndex) => {
const result = {};
const cssClass = this.disabled ? ['is-disabled'] : [];
let ariaSelected = false;
let ariaInvalid = false;
const day = weekIndex * 7 + dayIndex - monthStartsAt;
const cursor = new DateTime.Moment([year, month - 1]);
// we use add() since 'day' could be a negative value
cursor.add(day, 'days');
const isCurrentMonth = cursor.month() + 1 === parseFloat(month);
const dayOfWeek = new DateTime.Moment().day((dayIndex + this.startDay) % 7).format('dddd');
const isToday = cursor.isSame(new DateTime.Moment(), 'day');
const cursorLocal = cursor.clone().startOf('day');
if (isToday) {
cssClass.push('is-today');
}
if (dateLocal && cursorLocal.isSame(dateLocal, 'day')) {
ariaSelected = true;
cssClass.push('is-selected');
if (this.invalid) {
ariaInvalid = true;
cssClass.push('is-invalid');
}
}
if (isCurrentMonth) {
cssClass.push('is-currentMonth');
if (!this.disabled && isDateInRange(cursor, this.min, this.max)) {
result.dateAttr = cursorLocal.format(INTERNAL_FORMAT);
result.weekIndex = cursor.week();
result.formattedDate = cursor.format('LL');
}
else {
cssClass.push('is-disabled');
}
}
else {
cssClass.push('is-outsideMonth');
}
result.isDisabled = this.disabled || !result.dateAttr;
result.dateText = cursor.date();
result.cssClass = cssClass.join(' ');
result.isToday = isToday;
result.ariaSelected = ariaSelected;
result.ariaInvalid = ariaInvalid;
result.dateLabel = dayOfWeek;
result.weekIndex = cursor.week();
return result;
}, this);
}, this)
};
const handles = {};
table.call(handles, data);
return handles.table;
}
/** @ignore */
_requireCursor() {
let cursor = this._cursor;
if (!cursor || !cursor.isValid()) {
// When its unknown what month we should be showing, use the set date. If that is not available, use 'today'
cursor = (this._value ? this._value.clone().startOf('day') : new DateTime.Moment()).startOf('month');
this._cursor = cursor;
}
return cursor;
}
/**
Navigate to previous or next timeUnit interval.
@param {String} unit
Year, Month, Week, Day
@param {Boolean} isNext
Whether to navigate forward or backward.
@private
*/
_gotoPreviousOrNextTimeUnit(unit, isNext) {
const direction = isNext ? 'left' : 'right';
const operator = isNext ? 'add' : 'subtract';
const el = this._elements.body.querySelector('._coral-Calendar-date.is-focused');
let currentActive;
let currentMoment;
let newMoment;
let difference;
if (el) {
currentActive = el.dataset.date;
currentMoment = new DateTime.Moment(currentActive);
newMoment = currentMoment[operator](1, unit);
// make sure new moment is in range before transitioning
if (!this._isInRange(newMoment)) {
// advance to closest value in range
if (this._isBeforeMin(newMoment)) {
newMoment = this.min;
}
else if (this._isAfterMax(newMoment)) {
newMoment = this.max;
}
newMoment = new DateTime.Moment(newMoment);
}
difference = Math.abs(new DateTime.Moment(currentActive).diff(newMoment, 'days'));
this._getToNewMoment(direction, operator, difference);
this._setActiveDescendant();
}
else {
this._requireCursor();
// if cursor is out of range
if (!this._isInRange(this._cursor)) {
// advance to closest value in range
if (this._isBeforeMin(this._cursor)) {
newMoment = this.min;
}
else if (this._isAfterMax(this._cursor)) {
newMoment = this.max;
}
newMoment = new DateTime.Moment(newMoment);
difference = Math.abs(this._cursor.diff(newMoment, 'days'));
this._getToNewMoment(direction, operator, difference);
this._setActiveDescendant();
return;
}
this._cursor[operator](1, unit);
this._renderCalendar(direction);
}
}
/**
Checks if the Calendar is valid or not. This is done by checking that the current value is between the
provided <code>min</code> and <code>max</code> values. This check is only performed on user interaction.
@ignore
*/
_validateCalendar() {
const isInvalid = !(this._value === null || isDateInRange(this._value, this.min, this.max));
if (this.invalid !== isInvalid) {
this.invalid = isInvalid;
}
}
/** @ignore */
_onNextOrPreviousMonthClick(event) {
event.preventDefault();
this._gotoPreviousOrNextTimeUnit(
event.altKey || event.metaKey || event.shiftKey ? timeUnit.YEAR : timeUnit.MONTH,
this._elements.next === event.matchedTarget
);
event.matchedTarget.focus();
this._validateCalendar();
}
/** @ignore */
_getToNewMoment(direction, operator, difference) {
const el = this._elements.body.querySelector('._coral-Calendar-date.is-focused');
let currentActive;
if (el) {
currentActive = el.dataset.date;
}
else {
this._requireCursor();
currentActive = this._cursor.format(INTERNAL_FORMAT);
}
const currentMoment = new DateTime.Moment(currentActive);
const currentMonth = currentMoment.month();
const currentYear = currentMoment.year();
const newMoment = currentMoment[operator](difference, 'days');
const newMonth = newMoment.month();
const newYear = newMoment.year();
const newMomentValue = newMoment.format(INTERNAL_FORMAT);
if (newMonth !== currentMonth) {
this._requireCursor();
this._cursor[operator](1, 'months');
this._renderCalendar(direction);
}
else if (newMonth === currentMonth && newYear !== currentYear) {
this._requireCursor();
this._cursor[operator](1, 'years');
this._renderCalendar(direction);
}
const dateQuery = `._coral-Calendar-date[data-date^=${JSON.stringify(newMomentValue)}]`;
const newDescendant = this._elements.body.querySelector(dateQuery);
if (newDescendant) {
this._activeDescendant = newDescendant.parentNode.getAttribute('id');
}
}
/** @ignore */
_onDayMouseDown(event) {
this._activeDescendant = event.target.parentNode.id;
this._setActiveDescendant();
this._elements.body.focus();
this._validateCalendar();
}
/** @ignore */
_onDayClick(event) {
event.preventDefault();
this._elements.body.focus();
const date = new DateTime.Moment(event.target.dataset.date, INTERNAL_FORMAT);
let dateLocal;
// Carry over any user set time info
if (this._value) {
dateLocal = this._value.clone();
}
// Set attribute so a change event will be triggered if the user has selected a different date
if (validateAsChangedAndValidMoment(date, dateLocal)) {
this.value = date;
this.trigger('change');
}
this._validateCalendar();
}
/** @ignore */
_onEnterKey(event) {
event.preventDefault();
const el = this._elements.body.querySelector('._coral-Calendar-date.is-focused');
if (el) {
el.click();
}
this._validateCalendar();
}
/** @ignore */
_onUpKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.WEEK, false);
this._validateCalendar();
}
/** @ignore */
_onDownKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.WEEK, true);
this._validateCalendar();
}
/** @ignore */
_onRightKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.DAY, true);
this._validateCalendar();
}
/** @ignore */
_onLeftKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.DAY, false);
this._validateCalendar();
}
/** @ignore */
_onHomeOrEndKey(event) {
event.preventDefault();
event.stopPropagation();
const isHome = event.keyCode === Keys.keyToCode('home');
const direction = '';
const operator = isHome ? 'subtract' : 'add';
const el = this._elements.body.querySelector('._coral-Calendar-date.is-focused');
if (el) {
const currentActive = el.dataset.date;
const currentMoment = new DateTime.Moment(currentActive);
const difference = isHome ? currentMoment.date() - 1 : currentMoment.daysInMonth() - currentMoment.date();
this._getToNewMoment(direction, operator, difference);
this._setActiveDescendant();
}
this._validateCalendar();
}
/** @ignore */
_onPageDownKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.MONTH, true);
this._validateCalendar();
}
/** @ignore */
_onPageUpKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.MONTH, false);
this._validateCalendar();
}
/** @ignore */
_onCtrlPageDownKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.YEAR, true);
this._validateCalendar();
}
/** @ignore */
_onCtrlPageUpKey(event) {
event.preventDefault();
event.stopPropagation();
this._gotoPreviousOrNextTimeUnit(timeUnit.YEAR, false);
this._validateCalendar();
}
/** @ignore */
_onBodyFocus() {
if (Boolean(this._value)) {
this._setActiveDescendant();
this._validateCalendar();
}
}
/**
sets focus to appropriate descendant
*/
focus() {
const focusedElement = this._elements.body.querySelector('.is-focused');
if (focusedElement !== document.activeElement && !this.disabled) {
this._setActiveDescendant();
this._elements.body.focus();
}
}
static get _attributePropertyMap() {
return commons.extend(super._attributePropertyMap, {
startday: 'startDay',
headerformat: 'headerFormat',
valueformat: 'valueFormat'
});
}
/** @ignore */
static get observedAttributes() {
return super.observedAttributes.concat([
'startday',
'headerformat',
'min',
'max',
'valueformat',
]);
}
/** @ignore */
render() {
super.render();
this.classList.add(CLASSNAME);
this.setAttribute('role', 'group');
// Default reflected attribute
if (!this._valueFormat) {
this.valueFormat = INTERNAL_FORMAT;
}
const frag = document.createDocumentFragment();
// Render template
frag.appendChild(this._elements.input);
frag.appendChild(this._elements.header);
frag.appendChild(this._elements.body);
/// Clean Up (cloneNode support)
while (this.firstChild) {
this.removeChild(this.firstChild);
}
this.appendChild(frag);
// Render the calendar body if it's empty
if (!this._elements.body.firstElementChild) {
this._renderCalendar();
}
}
});
export default Calendar;