DateRangePicker.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import {registerListeners, unregisterListeners} from './lib/event.js';
  2. import {formatDate} from './lib/date-format.js';
  3. import Datepicker from './Datepicker.js';
  4. // filter out the config options inapproprite to pass to Datepicker
  5. function filterOptions(options) {
  6. const newOpts = Object.assign({}, options);
  7. delete newOpts.inputs;
  8. delete newOpts.allowOneSidedRange;
  9. delete newOpts.maxNumberOfDates; // to ensure each datepicker handles a single date
  10. return newOpts;
  11. }
  12. function setupDatepicker(rangepicker, changeDateListener, el, options) {
  13. registerListeners(rangepicker, [
  14. [el, 'changeDate', changeDateListener],
  15. ]);
  16. new Datepicker(el, options, rangepicker);
  17. }
  18. function onChangeDate(rangepicker, ev) {
  19. // to prevent both datepickers trigger the other side's update each other
  20. if (rangepicker._updating) {
  21. return;
  22. }
  23. rangepicker._updating = true;
  24. const target = ev.target;
  25. if (target.datepicker === undefined) {
  26. return;
  27. }
  28. const datepickers = rangepicker.datepickers;
  29. const setDateOptions = {render: false};
  30. const changedSide = rangepicker.inputs.indexOf(target);
  31. const otherSide = changedSide === 0 ? 1 : 0;
  32. const changedDate = datepickers[changedSide].dates[0];
  33. const otherDate = datepickers[otherSide].dates[0];
  34. if (changedDate !== undefined && otherDate !== undefined) {
  35. // if the start of the range > the end, swap them
  36. if (changedSide === 0 && changedDate > otherDate) {
  37. datepickers[0].setDate(otherDate, setDateOptions);
  38. datepickers[1].setDate(changedDate, setDateOptions);
  39. } else if (changedSide === 1 && changedDate < otherDate) {
  40. datepickers[0].setDate(changedDate, setDateOptions);
  41. datepickers[1].setDate(otherDate, setDateOptions);
  42. }
  43. } else if (!rangepicker.allowOneSidedRange) {
  44. // to prevent the range from becoming one-sided, copy changed side's
  45. // selection (no matter if it's empty) to the other side
  46. if (changedDate !== undefined || otherDate !== undefined) {
  47. setDateOptions.clear = true;
  48. datepickers[otherSide].setDate(datepickers[changedSide].dates, setDateOptions);
  49. }
  50. }
  51. datepickers[0].picker.update().render();
  52. datepickers[1].picker.update().render();
  53. delete rangepicker._updating;
  54. }
  55. /**
  56. * Class representing a date range picker
  57. */
  58. export default class DateRangePicker {
  59. /**
  60. * Create a date range picker
  61. * @param {Element} element - element to bind a date range picker
  62. * @param {Object} [options] - config options
  63. */
  64. constructor(element, options = {}) {
  65. const inputs = Array.isArray(options.inputs)
  66. ? options.inputs
  67. : Array.from(element.querySelectorAll('input'));
  68. if (inputs.length < 2) {
  69. return;
  70. }
  71. element.rangepicker = this;
  72. this.element = element;
  73. this.inputs = inputs.slice(0, 2);
  74. this.allowOneSidedRange = !!options.allowOneSidedRange;
  75. const changeDateListener = onChangeDate.bind(null, this);
  76. const cleanOptions = filterOptions(options);
  77. // in order for initial date setup to work right when pcicLvel > 0,
  78. // let Datepicker constructor add the instance to the rangepicker
  79. const datepickers = [];
  80. Object.defineProperty(this, 'datepickers', {
  81. get() {
  82. return datepickers;
  83. },
  84. });
  85. setupDatepicker(this, changeDateListener, this.inputs[0], cleanOptions);
  86. setupDatepicker(this, changeDateListener, this.inputs[1], cleanOptions);
  87. Object.freeze(datepickers);
  88. // normalize the range if inital dates are given
  89. if (datepickers[0].dates.length > 0) {
  90. onChangeDate(this, {target: this.inputs[0]});
  91. } else if (datepickers[1].dates.length > 0) {
  92. onChangeDate(this, {target: this.inputs[1]});
  93. }
  94. }
  95. /**
  96. * @type {Array} - selected date of the linked date pickers
  97. */
  98. get dates() {
  99. return this.datepickers.length === 2
  100. ? [
  101. this.datepickers[0].dates[0],
  102. this.datepickers[1].dates[0],
  103. ]
  104. : undefined;
  105. }
  106. /**
  107. * Set new values to the config options
  108. * @param {Object} options - config options to update
  109. */
  110. setOptions(options) {
  111. this.allowOneSidedRange = !!options.allowOneSidedRange;
  112. const cleanOptions = filterOptions(options);
  113. this.datepickers[0].setOptions(cleanOptions);
  114. this.datepickers[1].setOptions(cleanOptions);
  115. }
  116. /**
  117. * Destroy the DateRangePicker instance
  118. * @return {DateRangePicker} - the instance destroyed
  119. */
  120. destroy() {
  121. this.datepickers[0].destroy();
  122. this.datepickers[1].destroy();
  123. unregisterListeners(this);
  124. delete this.element.rangepicker;
  125. }
  126. /**
  127. * Get the start and end dates of the date range
  128. *
  129. * The method returns Date objects by default. If format string is passed,
  130. * it returns date strings formatted in given format.
  131. * The result array always contains 2 items (start date/end date) and
  132. * undefined is used for unselected side. (e.g. If none is selected,
  133. * the result will be [undefined, undefined]. If only the end date is set
  134. * when allowOneSidedRange config option is true, [undefined, endDate] will
  135. * be returned.)
  136. *
  137. * @param {String} [format] - Format string to stringify the dates
  138. * @return {Array} - Start and end dates
  139. */
  140. getDates(format = undefined) {
  141. const callback = format
  142. ? date => formatDate(date, format, this.datepickers[0].config.locale)
  143. : date => new Date(date);
  144. return this.dates.map(date => date === undefined ? date : callback(date));
  145. }
  146. /**
  147. * Set the start and end dates of the date range
  148. *
  149. * The method calls datepicker.setDate() internally using each of the
  150. * arguments in start→end order.
  151. *
  152. * When a clear: true option object is passed instead of a date, the method
  153. * clears the date.
  154. *
  155. * If an invalid date, the same date as the current one or an option object
  156. * without clear: true is passed, the method considers that argument as an
  157. * "ineffective" argument because calling datepicker.setDate() with those
  158. * values makes no changes to the date selection.
  159. *
  160. * When the allowOneSidedRange config option is false, passing {clear: true}
  161. * to clear the range works only when it is done to the last effective
  162. * argument (in other words, passed to rangeEnd or to rangeStart along with
  163. * ineffective rangeEnd). This is because when the date range is changed,
  164. * it gets normalized based on the last change at the end of the changing
  165. * process.
  166. *
  167. * @param {Date|Number|String|Object} rangeStart - Start date of the range
  168. * or {clear: true} to clear the date
  169. * @param {Date|Number|String|Object} rangeEnd - End date of the range
  170. * or {clear: true} to clear the date
  171. */
  172. setDates(rangeStart, rangeEnd) {
  173. const [datepicker0, datepicker1] = this.datepickers;
  174. const origDates = this.dates;
  175. // If range normalization runs on every change, we can't set a new range
  176. // that starts after the end of the current range correctly because the
  177. // normalization process swaps start↔︎end right after setting the new start
  178. // date. To prevent this, the normalization process needs to run once after
  179. // both of the new dates are set.
  180. this._updating = true;
  181. datepicker0.setDate(rangeStart);
  182. datepicker1.setDate(rangeEnd);
  183. delete this._updating;
  184. if (datepicker1.dates[0] !== origDates[1]) {
  185. onChangeDate(this, {target: this.inputs[1]});
  186. } else if (datepicker0.dates[0] !== origDates[0]) {
  187. onChangeDate(this, {target: this.inputs[0]});
  188. }
  189. }
  190. }