123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- import {lastItemOf, stringToArray, isInRange} from './lib/utils.js';
- import {today} from './lib/date.js';
- import {parseDate, formatDate} from './lib/date-format.js';
- import {registerListeners, unregisterListeners} from './lib/event.js';
- import {locales} from './i18n/base-locales.js';
- import defaultOptions from './options/defaultOptions.js';
- import processOptions from './options/processOptions.js';
- import Picker from './picker/Picker.js';
- import {triggerDatepickerEvent} from './events/functions.js';
- import {onKeydown, onFocus, onMousedown, onClickInput, onPaste} from './events/inputFieldListeners.js';
- import {onClickOutside} from './events/otherListeners.js';
- function stringifyDates(dates, config) {
- return dates
- .map(dt => formatDate(dt, config.format, config.locale))
- .join(config.dateDelimiter);
- }
- // parse input dates and create an array of time values for selection
- // returns undefined if there are no valid dates in inputDates
- // when origDates (current selection) is passed, the function works to mix
- // the input dates into the current selection
- function processInputDates(datepicker, inputDates, clear = false) {
- const {config, dates: origDates, rangepicker} = datepicker;
- if (inputDates.length === 0) {
- // empty input is considered valid unless origiDates is passed
- return clear ? [] : undefined;
- }
- const rangeEnd = rangepicker && datepicker === rangepicker.datepickers[1];
- let newDates = inputDates.reduce((dates, dt) => {
- let date = parseDate(dt, config.format, config.locale);
- if (date === undefined) {
- return dates;
- }
- if (config.pickLevel > 0) {
- // adjust to 1st of the month/Jan 1st of the year
- // or to the last day of the monh/Dec 31st of the year if the datepicker
- // is the range-end picker of a rangepicker
- const dt = new Date(date);
- if (config.pickLevel === 1) {
- date = rangeEnd
- ? dt.setMonth(dt.getMonth() + 1, 0)
- : dt.setDate(1);
- } else {
- date = rangeEnd
- ? dt.setFullYear(dt.getFullYear() + 1, 0, 0)
- : dt.setMonth(0, 1);
- }
- }
- if (
- isInRange(date, config.minDate, config.maxDate)
- && !dates.includes(date)
- && !config.datesDisabled.includes(date)
- && !config.daysOfWeekDisabled.includes(new Date(date).getDay())
- ) {
- dates.push(date);
- }
- return dates;
- }, []);
- if (newDates.length === 0) {
- return;
- }
- if (config.multidate && !clear) {
- // get the synmetric difference between origDates and newDates
- newDates = newDates.reduce((dates, date) => {
- if (!origDates.includes(date)) {
- dates.push(date);
- }
- return dates;
- }, origDates.filter(date => !newDates.includes(date)));
- }
- // do length check always because user can input multiple dates regardless of the mode
- return config.maxNumberOfDates && newDates.length > config.maxNumberOfDates
- ? newDates.slice(config.maxNumberOfDates * -1)
- : newDates;
- }
- // refresh the UI elements
- // modes: 1: input only, 2, picker only, 3 both
- function refreshUI(datepicker, mode = 3, quickRender = true) {
- const {config, picker, inputField} = datepicker;
- if (mode & 2) {
- const newView = picker.active ? config.pickLevel : config.startView;
- picker.update().changeView(newView).render(quickRender);
- }
- if (mode & 1 && inputField) {
- inputField.value = stringifyDates(datepicker.dates, config);
- }
- }
- function setDate(datepicker, inputDates, options) {
- let {clear, render, autohide} = options;
- if (render === undefined) {
- render = true;
- }
- if (!render) {
- autohide = false;
- } else if (autohide === undefined) {
- autohide = datepicker.config.autohide;
- }
- const newDates = processInputDates(datepicker, inputDates, clear);
- if (!newDates) {
- return;
- }
- if (newDates.toString() !== datepicker.dates.toString()) {
- datepicker.dates = newDates;
- refreshUI(datepicker, render ? 3 : 1);
- triggerDatepickerEvent(datepicker, 'changeDate');
- } else {
- refreshUI(datepicker, 1);
- }
- if (autohide) {
- datepicker.hide();
- }
- }
- /**
- * Class representing a date picker
- */
- export default class Datepicker {
- /**
- * Create a date picker
- * @param {Element} element - element to bind a date picker
- * @param {Object} [options] - config options
- * @param {DateRangePicker} [rangepicker] - DateRangePicker instance the
- * date picker belongs to. Use this only when creating date picker as a part
- * of date range picker
- */
- constructor(element, options = {}, rangepicker = undefined) {
- element.datepicker = this;
- this.element = element;
- // set up config
- const config = this.config = Object.assign({
- buttonClass: (options.buttonClass && String(options.buttonClass)) || 'button',
- container: document.body,
- defaultViewDate: today(),
- maxDate: undefined,
- minDate: undefined,
- }, processOptions(defaultOptions, this));
- this._options = options;
- Object.assign(config, processOptions(options, this));
- // configure by type
- const inline = this.inline = element.tagName !== 'INPUT';
- let inputField;
- let initialDates;
- if (inline) {
- config.container = element;
- initialDates = stringToArray(element.dataset.date, config.dateDelimiter);
- delete element.dataset.date;
- } else {
- const container = options.container ? document.querySelector(options.container) : null;
- if (container) {
- config.container = container;
- }
- inputField = this.inputField = element;
- inputField.classList.add('datepicker-input');
- initialDates = stringToArray(inputField.value, config.dateDelimiter);
- }
- if (rangepicker) {
- // check validiry
- const index = rangepicker.inputs.indexOf(inputField);
- const datepickers = rangepicker.datepickers;
- if (index < 0 || index > 1 || !Array.isArray(datepickers)) {
- throw Error('Invalid rangepicker object.');
- }
- // attach itaelf to the rangepicker here so that processInputDates() can
- // determine if this is the range-end picker of the rangepicker while
- // setting inital values when pickLevel > 0
- datepickers[index] = this;
- // add getter for rangepicker
- Object.defineProperty(this, 'rangepicker', {
- get() {
- return rangepicker;
- },
- });
- }
- // set initial dates
- this.dates = [];
- // process initial value
- const inputDateValues = processInputDates(this, initialDates);
- if (inputDateValues && inputDateValues.length > 0) {
- this.dates = inputDateValues;
- }
- if (inputField) {
- inputField.value = stringifyDates(this.dates, config);
- }
- const picker = this.picker = new Picker(this);
- if (inline) {
- this.show();
- } else {
- // set up event listeners in other modes
- const onMousedownDocument = onClickOutside.bind(null, this);
- const listeners = [
- [inputField, 'keydown', onKeydown.bind(null, this)],
- [inputField, 'focus', onFocus.bind(null, this)],
- [inputField, 'mousedown', onMousedown.bind(null, this)],
- [inputField, 'click', onClickInput.bind(null, this)],
- [inputField, 'paste', onPaste.bind(null, this)],
- [document, 'mousedown', onMousedownDocument],
- [document, 'touchstart', onMousedownDocument],
- [window, 'resize', picker.place.bind(picker)]
- ];
- registerListeners(this, listeners);
- }
- }
- /**
- * Format Date object or time value in given format and language
- * @param {Date|Number} date - date or time value to format
- * @param {String|Object} format - format string or object that contains
- * toDisplay() custom formatter, whose signature is
- * - args:
- * - date: {Date} - Date instance of the date passed to the method
- * - format: {Object} - the format object passed to the method
- * - locale: {Object} - locale for the language specified by `lang`
- * - return:
- * {String} formatted date
- * @param {String} [lang=en] - language code for the locale to use
- * @return {String} formatted date
- */
- static formatDate(date, format, lang) {
- return formatDate(date, format, lang && locales[lang] || locales.en);
- }
- /**
- * Parse date string
- * @param {String|Date|Number} dateStr - date string, Date object or time
- * value to parse
- * @param {String|Object} format - format string or object that contains
- * toValue() custom parser, whose signature is
- * - args:
- * - dateStr: {String|Date|Number} - the dateStr passed to the method
- * - format: {Object} - the format object passed to the method
- * - locale: {Object} - locale for the language specified by `lang`
- * - return:
- * {Date|Number} parsed date or its time value
- * @param {String} [lang=en] - language code for the locale to use
- * @return {Number} time value of parsed date
- */
- static parseDate(dateStr, format, lang) {
- return parseDate(dateStr, format, lang && locales[lang] || locales.en);
- }
- /**
- * @type {Object} - Installed locales in `[languageCode]: localeObject` format
- * en`:_English (US)_ is pre-installed.
- */
- static get locales() {
- return locales;
- }
- /**
- * @type {Boolean} - Whether the picker element is shown. `true` whne shown
- */
- get active() {
- return !!(this.picker && this.picker.active);
- }
- /**
- * @type {HTMLDivElement} - DOM object of picker element
- */
- get pickerElement() {
- return this.picker ? this.picker.element : undefined;
- }
- /**
- * Set new values to the config options
- * @param {Object} options - config options to update
- */
- setOptions(options) {
- const picker = this.picker;
- const newOptions = processOptions(options, this);
- Object.assign(this._options, options);
- Object.assign(this.config, newOptions);
- picker.setOptions(newOptions);
- refreshUI(this, 3);
- }
- /**
- * Show the picker element
- */
- show() {
- if (this.inputField) {
- if (this.inputField.disabled) {
- return;
- }
- if (this.inputField !== document.activeElement) {
- this._showing = true;
- this.inputField.focus();
- delete this._showing;
- }
- }
- this.picker.show();
- }
- /**
- * Hide the picker element
- * Not available on inline picker
- */
- hide() {
- if (this.inline) {
- return;
- }
- this.picker.hide();
- this.picker.update().changeView(this.config.startView).render();
- }
- /**
- * Destroy the Datepicker instance
- * @return {Detepicker} - the instance destroyed
- */
- destroy() {
- this.hide();
- unregisterListeners(this);
- this.picker.detach();
- if (!this.inline) {
- this.inputField.classList.remove('datepicker-input');
- }
- delete this.element.datepicker;
- return this;
- }
- /**
- * Get the selected date(s)
- *
- * The method returns a Date object of selected date by default, and returns
- * an array of selected dates in multidate mode. If format string is passed,
- * it returns date string(s) formatted in given format.
- *
- * @param {String} [format] - Format string to stringify the date(s)
- * @return {Date|String|Date[]|String[]} - selected date(s), or if none is
- * selected, empty array in multidate mode and untitled in sigledate mode
- */
- getDate(format = undefined) {
- const callback = format
- ? date => formatDate(date, format, this.config.locale)
- : date => new Date(date);
- if (this.config.multidate) {
- return this.dates.map(callback);
- }
- if (this.dates.length > 0) {
- return callback(this.dates[0]);
- }
- }
- /**
- * Set selected date(s)
- *
- * In multidate mode, you can pass multiple dates as a series of arguments
- * or an array. (Since each date is parsed individually, the type of the
- * dates doesn't have to be the same.)
- * The given dates are used to toggle the select status of each date. The
- * number of selected dates is kept from exceeding the length set to
- * maxNumberOfDates.
- *
- * With clear: true option, the method can be used to clear the selection
- * and to replace the selection instead of toggling in multidate mode.
- * If the option is passed with no date arguments or an empty dates array,
- * it works as "clear" (clear the selection then set nothing), and if the
- * option is passed with new dates to select, it works as "replace" (clear
- * the selection then set the given dates)
- *
- * When render: false option is used, the method omits re-rendering the
- * picker element. In this case, you need to call refresh() method later in
- * order for the picker element to reflect the changes. The input field is
- * refreshed always regardless of this option.
- *
- * When invalid (unparsable, repeated, disabled or out-of-range) dates are
- * passed, the method ignores them and applies only valid ones. In the case
- * that all the given dates are invalid, which is distinguished from passing
- * no dates, the method considers it as an error and leaves the selection
- * untouched.
- *
- * @param {...(Date|Number|String)|Array} [dates] - Date strings, Date
- * objects, time values or mix of those for new selection
- * @param {Object} [options] - function options
- * - clear: {boolean} - Whether to clear the existing selection
- * defualt: false
- * - render: {boolean} - Whether to re-render the picker element
- * default: true
- * - autohide: {boolean} - Whether to hide the picker element after re-render
- * Ignored when used with render: false
- * default: config.autohide
- */
- setDate(...args) {
- const dates = [...args];
- const opts = {};
- const lastArg = lastItemOf(args);
- if (
- typeof lastArg === 'object'
- && !Array.isArray(lastArg)
- && !(lastArg instanceof Date)
- && lastArg
- ) {
- Object.assign(opts, dates.pop());
- }
- const inputDates = Array.isArray(dates[0]) ? dates[0] : dates;
- setDate(this, inputDates, opts);
- }
- /**
- * Update the selected date(s) with input field's value
- * Not available on inline picker
- *
- * The input field will be refreshed with properly formatted date string.
- *
- * @param {Object} [options] - function options
- * - autohide: {boolean} - whether to hide the picker element after refresh
- * default: false
- */
- update(options = undefined) {
- if (this.inline) {
- return;
- }
- const opts = {clear: true, autohide: !!(options && options.autohide)};
- const inputDates = stringToArray(this.inputField.value, this.config.dateDelimiter);
- setDate(this, inputDates, opts);
- }
- /**
- * Refresh the picker element and the associated input field
- * @param {String} [target] - target item when refreshing one item only
- * 'picker' or 'input'
- * @param {Boolean} [forceRender] - whether to re-render the picker element
- * regardless of its state instead of optimized refresh
- */
- refresh(target = undefined, forceRender = false) {
- if (target && typeof target !== 'string') {
- forceRender = target;
- target = undefined;
- }
- let mode;
- if (target === 'picker') {
- mode = 2;
- } else if (target === 'input') {
- mode = 1;
- } else {
- mode = 3;
- }
- refreshUI(this, mode, !forceRender);
- }
- /**
- * Enter edit mode
- * Not available on inline picker or when the picker element is hidden
- */
- enterEditMode() {
- if (this.inline || !this.picker.active || this.editMode) {
- return;
- }
- this.editMode = true;
- this.inputField.classList.add('in-edit');
- }
- /**
- * Exit from edit mode
- * Not available on inline picker
- * @param {Object} [options] - function options
- * - update: {boolean} - whether to call update() after exiting
- * If false, input field is revert to the existing selection
- * default: false
- */
- exitEditMode(options = undefined) {
- if (this.inline || !this.editMode) {
- return;
- }
- const opts = Object.assign({update: false}, options);
- delete this.editMode;
- this.inputField.classList.remove('in-edit');
- if (opts.update) {
- this.update(opts);
- }
- }
- }
|