ember-date-components
Version:
An Ember add-on which provides pure Ember-based date picker components.
574 lines (454 loc) • 12.2 kB
JavaScript
import Component from '@glimmer/component';
import { A as array } from '@ember/array';
import { typeOf as getTypeOf } from '@ember/utils';
import { next } from '@ember/runloop';
import { action } from '@ember/object';
import { guidFor } from '@ember/object/internals';
import moment from 'moment';
import { assert } from '@ember/debug';
import { tracked } from '@glimmer/tracking';
/**
* A versatile date picker component.
* This is 100% ember based and uses no other date picker library.
*
* Attributes:
* - value
* - disabledDates
* - minDate
* - maxDate
* - range
* - placeholder
* - buttonClasses
* - buttonDateFormat
* - options
* - disabled
* - disableMonthPicker
* - disableYearPicker
* - availableYearOffset
* - renderInPlace
* - horizontalPosition
* - verticalPosition
* - startWeekOnSunday
* - onChange
* - onClose
*/
export default class DatePicker extends Component {
dates;
currentMonth;
isToStep = false;
isOpen = false;
guid = guidFor(this);
dateRangeSeparator = ' - ';
_originallyFocusedElement;
_datePickerDropdownElement;
get range() {
return this.args.range || false;
}
get placeholder() {
return this.args.placeholder || 'Select date...';
}
get buttonDateFormat() {
return this.args.buttonDateFormat || 'L';
}
get options() {
return this.args.options || false;
}
get disabled() {
return this.args.disabled || false;
}
get disableMonthPicker() {
return this.args.disableMonthPicker || false;
}
get disableYearPicker() {
return this.args.disableYearPicker || false;
}
get availableYearOffset() {
return this.args.availableYearOffset || 10;
}
get startWeekOnSunday() {
return this.args.startWeekOnSunday || false;
}
get buttonText() {
let { range } = this;
let vals = this.dates || array([]);
let dateFormat = this.buttonDateFormat;
let [dateFrom] = vals;
if (!range) {
if (!dateFrom) {
return this.placeholder;
}
return dateFrom.format(dateFormat);
}
if (!dateFrom) {
return this.placeholder;
}
return dateFrom.format(dateFormat);
}
get buttonToText() {
let vals = this.dates || array([]);
let dateFormat = this.buttonDateFormat;
let [, dateTo] = vals;
if (!dateTo) {
return this.placeholder;
}
return dateTo.format(dateFormat);
}
get buttonFocused() {
let { range: isRange, isOpen, isToStep } = this;
return isRange ? isOpen && !isToStep : isOpen;
}
get buttonToFocused() {
let { range: isRange, isOpen, isToStep } = this;
return isRange ? isOpen && isToStep : false;
}
get selectedDates() {
let arr = [];
let [dateFrom, dateTo] = this.dates;
if (dateFrom) {
arr.push(dateFrom);
}
if (dateTo) {
arr.push(dateTo);
}
return array(arr);
}
get availableOptions() {
let { options, range: isRange, optionsMap } = this;
if (!options) {
return array();
}
// If options is true, return the default options depending on isRange
if (getTypeOf(options) !== 'array') {
options = isRange
? this._defaultDateRangeOptions
: this._defaultDateOptions;
}
return options.map((option) => {
return getTypeOf(option) === 'string' ? optionsMap[option] : option;
});
}
get optionsMap() {
return {
clear: {
action: 'clearDate',
label: 'Clear',
},
today: {
action: 'selectToday',
label: 'Today',
},
last7Days: {
action: 'selectDateRange',
label: 'Last 7 days',
actionValue: [
moment().startOf('day').subtract(6, 'days'),
moment().startOf('day'),
],
},
last30Days: {
action: 'selectDateRange',
label: 'Last 30 days',
actionValue: [
moment().startOf('day').subtract(29, 'days'),
moment().startOf('day'),
],
},
lastYear: {
action: 'selectDateRange',
label: 'Last year',
actionValue: [
moment().startOf('day').subtract(1, 'year').add(1, 'day'),
moment().startOf('day'),
],
},
last3Months: {
action: 'selectDateRange',
label: 'Last 3 months',
actionValue: [
moment().startOf('day').subtract(3, 'months').add(1, 'day'),
moment().startOf('day'),
],
},
last6Months: {
action: 'selectDateRange',
label: 'Last 6 months',
actionValue: [
moment().startOf('day').subtract(6, 'months').add(1, 'day'),
moment().startOf('day'),
],
},
thisWeek: {
action: 'selectDateRange',
label: 'This week',
actionValue: [moment().startOf('isoweek'), moment().startOf('day')],
},
thisMonth: {
action: 'selectDateRange',
label: 'This month',
actionValue: [moment().startOf('month'), moment().startOf('day')],
},
};
}
_defaultDateOptions = array(['clear', 'today']);
_defaultDateRangeOptions = array([
'clear',
'today',
'last7Days',
'last30Days',
'last3Months',
]);
_dropdownApi = null;
// PROPERTIES END ----------------------------------------
constructor() {
super(...arguments);
assert(
'<DatePicker>: You have to specify @onChange or @onClose',
typeof this.args.onChange === 'function' ||
typeof this.args.onClose === 'function'
);
this.valueDidUpdate();
}
valueDidUpdate() {
let { value } = this.args;
let { range } = this;
let dates = array([]);
if (value && getTypeOf(value) === 'array') {
dates = value.slice();
} else if (value) {
dates = array([value]);
}
if (range && dates.length === 1) {
this.isToStep = true;
}
while (range && dates.length < 2) {
dates.push(null);
}
this._setCurrentMonth(dates);
this.dates = dates;
}
selectOption(option) {
let { action, actionValue } = option;
console.log(action, actionValue);
assert(
`<DatePicker>: option has no valid action defined`,
typeof this[action] === 'function'
);
this[action](actionValue);
}
_setCurrentMonth(dates) {
let firstDate = dates.find((date) => date && moment.isMoment(date));
this.currentMonth = firstDate
? firstDate.clone().startOf('month')
: moment().startOf('month');
}
_sendOnChange() {
if (!this.args.onChange || this.disabled) {
return;
}
let value = this._getSelectedValue();
this.args.onChange(value);
}
_sendOnClose() {
if (!this.args.onClose || this.disabled) {
return;
}
let value = this._getSelectedValue();
this.args.onClose(value);
}
focusDatePicker(datePickerDropdown) {
let originallyFocusedElement = document.activeElement;
this._originallyFocusedElement = originallyFocusedElement;
this._datePickerDropdownElement = datePickerDropdown;
this._focusButtonInDatePicker();
}
_focusButtonInDatePicker() {
let datePickerDropdown = this._datePickerDropdownElement;
if (!datePickerDropdown) {
return;
}
let selectedButton =
datePickerDropdown &&
datePickerDropdown.querySelector(
'[data-date-picker-day].date-picker__day--selected'
);
let firstButton =
datePickerDropdown &&
datePickerDropdown.querySelector('[data-date-picker-day]');
let buttonToFocus = selectedButton || firstButton;
if (buttonToFocus && document.body.contains(buttonToFocus)) {
buttonToFocus.focus();
}
}
_resetFocus() {
let originallyFocusedElement = this._originallyFocusedElement;
this._originallyFocusedElement = undefined;
if (
originallyFocusedElement &&
document.body.contains(originallyFocusedElement)
) {
next(() => originallyFocusedElement.focus());
}
}
_close({ forceCloseDropdown = true }) {
this.isOpen = false;
this.isToStep = false;
this._sendOnClose();
if (forceCloseDropdown) {
this._closeDropdown();
this._resetFocus();
}
}
_getSelectedValue() {
let value = this.range ? this.dates : this.dates[0];
return value || null;
}
_closeDropdown() {
this._dropdownApi?.actions.close();
}
_openDropdown() {
this._dropdownApi?.actions.open();
}
_setSingleDate(date) {
let dates = array([date]);
this.dates = dates;
this._sendOnChange();
this._close({ forceCloseDropdown: true });
}
_setFromDate(dateFrom) {
let { dates } = this;
let [, dateTo] = dates;
if (dateFrom && dateTo && dateFrom.valueOf() > dateTo.valueOf()) {
dateTo = null;
}
this.dates = array([dateFrom, dateTo]);
}
_setToDate(dateTo) {
let { dates } = this;
let [dateFrom] = dates;
if (dateTo && dateFrom && dateTo.valueOf() < dateFrom.valueOf()) {
[dateFrom, dateTo] = [dateTo, dateFrom];
}
if (dateTo) {
dateTo = dateTo.endOf('day');
}
this.dates = array([dateFrom, dateTo]);
}
_setDateRange(date) {
let { isToStep } = this;
if (!isToStep) {
this._setFromDate(date);
this._moveToToStep();
this._sendOnChange();
} else {
this._setToDate(date);
this._sendOnChange();
this._close({ forceCloseDropdown: true });
}
}
_moveToFromStep() {
let [month] = this.dates;
if (month) {
this.currentMonth = month.clone().startOf('month');
}
this.isToStep = false;
this._focusButtonInDatePicker();
}
_moveToToStep() {
let [, month] = this.dates;
if (month) {
this.currentMonth = month.clone().startOf('month');
}
this.isToStep = true;
this._focusButtonInDatePicker();
}
async _ensureDropdownIsOpen() {
// Ensure the dropdown is actually opened
await new Promise((resolve) => setTimeout(resolve, 1));
if (!this._dropdownApi?.isOpen) {
this._openDropdown();
}
}
// METHODS END ----------------------------------------
// ACTIONS BEGIN ----------------------------------------
clearDate() {
this.dates = this.range ? array([null, null]) : array([]);
this.isToStep = false;
this._sendOnChange();
this._close({ forceCloseDropdown: true });
}
selectToday() {
let today = moment().startOf('day');
this.dates = this.range ? array([today, today]) : array([today]);
this._sendOnChange();
this._close({ forceCloseDropdown: true });
}
toggleOpen(dd, event) {
let { isOpen, range, isToStep } = this;
event.preventDefault();
this._ensureDropdownIsOpen();
if (!isOpen) {
this._moveToFromStep();
return;
}
// SINGLE
if (!range) {
this._close({ forceCloseDropdown: true });
return;
}
// RANGE
if (isToStep) {
this._moveToFromStep();
} else {
this._close({ forceCloseDropdown: true });
}
}
toggleOpenTo(dd, event) {
let { isOpen, isToStep } = this;
event.preventDefault();
this._ensureDropdownIsOpen();
if (!isToStep || !isOpen) {
this._moveToToStep();
} else {
this._close({ forceCloseDropdown: true });
}
}
gotoMonth(month) {
this.currentMonth = month;
}
selectDate(date) {
let { range } = this;
if (!range) {
this._setSingleDate(date);
} else {
this._setDateRange(date);
}
}
// For options only
selectDateRange(dateRange) {
this.dates = array(dateRange);
this._sendOnChange();
this._close({ forceCloseDropdown: true });
}
async onDropdownClosed() {
// Ensure it is set to closed when clicking the dropdown with outside click
await new Promise((resolve) => setTimeout(resolve, 1));
if (this.isOpen) {
this._close({ forceCloseDropdown: false });
}
}
onDropdownOpened(dropdownApi) {
this._dropdownApi = dropdownApi;
}
// ACTIONS END ----------------------------------------
}