211 lines
7.5 KiB
JavaScript
211 lines
7.5 KiB
JavaScript
import {registerListeners, unregisterListeners} from './lib/event.js';
|
|
import {formatDate} from './lib/date-format.js';
|
|
import Datepicker from './Datepicker.js';
|
|
|
|
// filter out the config options inapproprite to pass to Datepicker
|
|
function filterOptions(options) {
|
|
const newOpts = Object.assign({}, options);
|
|
|
|
delete newOpts.inputs;
|
|
delete newOpts.allowOneSidedRange;
|
|
delete newOpts.maxNumberOfDates; // to ensure each datepicker handles a single date
|
|
|
|
return newOpts;
|
|
}
|
|
|
|
function setupDatepicker(rangepicker, changeDateListener, el, options) {
|
|
registerListeners(rangepicker, [
|
|
[el, 'changeDate', changeDateListener],
|
|
]);
|
|
new Datepicker(el, options, rangepicker);
|
|
}
|
|
|
|
function onChangeDate(rangepicker, ev) {
|
|
// to prevent both datepickers trigger the other side's update each other
|
|
if (rangepicker._updating) {
|
|
return;
|
|
}
|
|
rangepicker._updating = true;
|
|
|
|
const target = ev.target;
|
|
if (target.datepicker === undefined) {
|
|
return;
|
|
}
|
|
|
|
const datepickers = rangepicker.datepickers;
|
|
const setDateOptions = {render: false};
|
|
const changedSide = rangepicker.inputs.indexOf(target);
|
|
const otherSide = changedSide === 0 ? 1 : 0;
|
|
const changedDate = datepickers[changedSide].dates[0];
|
|
const otherDate = datepickers[otherSide].dates[0];
|
|
|
|
if (changedDate !== undefined && otherDate !== undefined) {
|
|
// if the start of the range > the end, swap them
|
|
if (changedSide === 0 && changedDate > otherDate) {
|
|
datepickers[0].setDate(otherDate, setDateOptions);
|
|
datepickers[1].setDate(changedDate, setDateOptions);
|
|
} else if (changedSide === 1 && changedDate < otherDate) {
|
|
datepickers[0].setDate(changedDate, setDateOptions);
|
|
datepickers[1].setDate(otherDate, setDateOptions);
|
|
}
|
|
} else if (!rangepicker.allowOneSidedRange) {
|
|
// to prevent the range from becoming one-sided, copy changed side's
|
|
// selection (no matter if it's empty) to the other side
|
|
if (changedDate !== undefined || otherDate !== undefined) {
|
|
setDateOptions.clear = true;
|
|
datepickers[otherSide].setDate(datepickers[changedSide].dates, setDateOptions);
|
|
}
|
|
}
|
|
datepickers[0].picker.update().render();
|
|
datepickers[1].picker.update().render();
|
|
delete rangepicker._updating;
|
|
}
|
|
|
|
/**
|
|
* Class representing a date range picker
|
|
*/
|
|
export default class DateRangePicker {
|
|
/**
|
|
* Create a date range picker
|
|
* @param {Element} element - element to bind a date range picker
|
|
* @param {Object} [options] - config options
|
|
*/
|
|
constructor(element, options = {}) {
|
|
const inputs = Array.isArray(options.inputs)
|
|
? options.inputs
|
|
: Array.from(element.querySelectorAll('input'));
|
|
if (inputs.length < 2) {
|
|
return;
|
|
}
|
|
|
|
element.rangepicker = this;
|
|
this.element = element;
|
|
this.inputs = inputs.slice(0, 2);
|
|
this.allowOneSidedRange = !!options.allowOneSidedRange;
|
|
|
|
const changeDateListener = onChangeDate.bind(null, this);
|
|
const cleanOptions = filterOptions(options);
|
|
// in order for initial date setup to work right when pcicLvel > 0,
|
|
// let Datepicker constructor add the instance to the rangepicker
|
|
const datepickers = [];
|
|
Object.defineProperty(this, 'datepickers', {
|
|
get() {
|
|
return datepickers;
|
|
},
|
|
});
|
|
setupDatepicker(this, changeDateListener, this.inputs[0], cleanOptions);
|
|
setupDatepicker(this, changeDateListener, this.inputs[1], cleanOptions);
|
|
Object.freeze(datepickers);
|
|
// normalize the range if inital dates are given
|
|
if (datepickers[0].dates.length > 0) {
|
|
onChangeDate(this, {target: this.inputs[0]});
|
|
} else if (datepickers[1].dates.length > 0) {
|
|
onChangeDate(this, {target: this.inputs[1]});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {Array} - selected date of the linked date pickers
|
|
*/
|
|
get dates() {
|
|
return this.datepickers.length === 2
|
|
? [
|
|
this.datepickers[0].dates[0],
|
|
this.datepickers[1].dates[0],
|
|
]
|
|
: undefined;
|
|
}
|
|
|
|
/**
|
|
* Set new values to the config options
|
|
* @param {Object} options - config options to update
|
|
*/
|
|
setOptions(options) {
|
|
this.allowOneSidedRange = !!options.allowOneSidedRange;
|
|
|
|
const cleanOptions = filterOptions(options);
|
|
this.datepickers[0].setOptions(cleanOptions);
|
|
this.datepickers[1].setOptions(cleanOptions);
|
|
}
|
|
|
|
/**
|
|
* Destroy the DateRangePicker instance
|
|
* @return {DateRangePicker} - the instance destroyed
|
|
*/
|
|
destroy() {
|
|
this.datepickers[0].destroy();
|
|
this.datepickers[1].destroy();
|
|
unregisterListeners(this);
|
|
delete this.element.rangepicker;
|
|
}
|
|
|
|
/**
|
|
* Get the start and end dates of the date range
|
|
*
|
|
* The method returns Date objects by default. If format string is passed,
|
|
* it returns date strings formatted in given format.
|
|
* The result array always contains 2 items (start date/end date) and
|
|
* undefined is used for unselected side. (e.g. If none is selected,
|
|
* the result will be [undefined, undefined]. If only the end date is set
|
|
* when allowOneSidedRange config option is true, [undefined, endDate] will
|
|
* be returned.)
|
|
*
|
|
* @param {String} [format] - Format string to stringify the dates
|
|
* @return {Array} - Start and end dates
|
|
*/
|
|
getDates(format = undefined) {
|
|
const callback = format
|
|
? date => formatDate(date, format, this.datepickers[0].config.locale)
|
|
: date => new Date(date);
|
|
|
|
return this.dates.map(date => date === undefined ? date : callback(date));
|
|
}
|
|
|
|
/**
|
|
* Set the start and end dates of the date range
|
|
*
|
|
* The method calls datepicker.setDate() internally using each of the
|
|
* arguments in start→end order.
|
|
*
|
|
* When a clear: true option object is passed instead of a date, the method
|
|
* clears the date.
|
|
*
|
|
* If an invalid date, the same date as the current one or an option object
|
|
* without clear: true is passed, the method considers that argument as an
|
|
* "ineffective" argument because calling datepicker.setDate() with those
|
|
* values makes no changes to the date selection.
|
|
*
|
|
* When the allowOneSidedRange config option is false, passing {clear: true}
|
|
* to clear the range works only when it is done to the last effective
|
|
* argument (in other words, passed to rangeEnd or to rangeStart along with
|
|
* ineffective rangeEnd). This is because when the date range is changed,
|
|
* it gets normalized based on the last change at the end of the changing
|
|
* process.
|
|
*
|
|
* @param {Date|Number|String|Object} rangeStart - Start date of the range
|
|
* or {clear: true} to clear the date
|
|
* @param {Date|Number|String|Object} rangeEnd - End date of the range
|
|
* or {clear: true} to clear the date
|
|
*/
|
|
setDates(rangeStart, rangeEnd) {
|
|
const [datepicker0, datepicker1] = this.datepickers;
|
|
const origDates = this.dates;
|
|
|
|
// If range normalization runs on every change, we can't set a new range
|
|
// that starts after the end of the current range correctly because the
|
|
// normalization process swaps start↔︎end right after setting the new start
|
|
// date. To prevent this, the normalization process needs to run once after
|
|
// both of the new dates are set.
|
|
this._updating = true;
|
|
datepicker0.setDate(rangeStart);
|
|
datepicker1.setDate(rangeEnd);
|
|
delete this._updating;
|
|
|
|
if (datepicker1.dates[0] !== origDates[1]) {
|
|
onChangeDate(this, {target: this.inputs[1]});
|
|
} else if (datepicker0.dates[0] !== origDates[0]) {
|
|
onChangeDate(this, {target: this.inputs[0]});
|
|
}
|
|
}
|
|
}
|