211 lines
7.3 KiB
JavaScript
211 lines
7.3 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]});
|
||
}
|
||
}
|
||
}
|