datepicker.js

  1. /* global moment */
  2. /**
  3. * @param {HTMLElement} element DOM element for component instantiation and scope
  4. * @param {Object} options
  5. * @param {String} options.datepickerFormat Format for dates
  6. */
  7. export class Datepicker {
  8. /**
  9. * @static
  10. * Shorthand for instance creation and initialisation.
  11. *
  12. * @param {HTMLElement} root DOM element for component instantiation and scope
  13. *
  14. * @return {Datepicker} An instance of Datepicker.
  15. */
  16. static autoInit(root, { DATEPICKER: defaultOptions = {} } = {}) {
  17. const datepicker = new Datepicker(root, defaultOptions);
  18. datepicker.init();
  19. root.ECLDatepicker = datepicker;
  20. return datepicker;
  21. }
  22. constructor(
  23. element,
  24. {
  25. format = '',
  26. theme = 'ecl-datepicker-theme',
  27. yearRange = 40,
  28. reposition = false,
  29. attachResizeListener = true,
  30. i18n = {
  31. previousMonth: 'Previous Month',
  32. nextMonth: 'Next Month',
  33. months: [
  34. 'January',
  35. 'February',
  36. 'March',
  37. 'April',
  38. 'May',
  39. 'June',
  40. 'July',
  41. 'August',
  42. 'September',
  43. 'October',
  44. 'November',
  45. 'December',
  46. ],
  47. weekdays: [
  48. 'Sunday',
  49. 'Monday',
  50. 'Tuesday',
  51. 'Wednesday',
  52. 'Thursday',
  53. 'Friday',
  54. 'Saturday',
  55. ],
  56. weekdaysShort: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
  57. },
  58. showDaysInNextAndPreviousMonths = true,
  59. enableSelectionDaysInNextAndPreviousMonths = true,
  60. } = {},
  61. ) {
  62. // Check element
  63. if (!element || element.nodeType !== Node.ELEMENT_NODE) {
  64. throw new TypeError(
  65. 'DOM element should be given to initialize this widget.',
  66. );
  67. }
  68. this.element = element;
  69. // Options
  70. this.picker = null;
  71. this.resizeTimer = null;
  72. this.pikadayEl = null;
  73. this.format = format;
  74. this.theme = theme;
  75. this.yearRange = yearRange;
  76. this.i18n = i18n;
  77. this.showDaysInNextAndPreviousMonths = showDaysInNextAndPreviousMonths;
  78. this.enableSelectionDaysInNextAndPreviousMonths =
  79. enableSelectionDaysInNextAndPreviousMonths;
  80. this.reposition = reposition;
  81. this.direction = 'ltr';
  82. this.attachResizeListener = attachResizeListener;
  83. // Bindings
  84. this.handleResize = this.handleResize.bind(this);
  85. }
  86. /**
  87. * Initialise component.
  88. */
  89. init() {
  90. if (typeof window.Pikaday === 'undefined') {
  91. throw new TypeError(
  92. 'Pikaday is not available. Make sure to include Pikaday in your project if you want to use the ECL datepicker',
  93. );
  94. }
  95. if (!ECL) {
  96. throw new TypeError('Called init but ECL is not present');
  97. }
  98. ECL.components = ECL.components || new Map();
  99. this.direction = getComputedStyle(this.element).direction;
  100. if (this.attachResizeListener) {
  101. window.addEventListener('resize', this.handleResize);
  102. }
  103. /**
  104. * Resize logic.
  105. *
  106. * @param {HTMLElement} el - The pikaday dropdown.
  107. */
  108. Datepicker.resizeLogic = (el) => {
  109. this.pikadayEl = el;
  110. const vw = Math.max(
  111. document.documentElement.clientWidth || 0,
  112. window.innerWidth || 0,
  113. );
  114. const { direction } = getComputedStyle(el);
  115. const elRect = el.getBoundingClientRect();
  116. if (direction === 'rtl') {
  117. const pickerMargin = vw - elRect.right;
  118. if (vw < 768) {
  119. el.style.left = `${pickerMargin}px`;
  120. } else {
  121. el.style.left = 'auto';
  122. }
  123. } else {
  124. const pickerMargin = elRect.left;
  125. if (vw < 768) {
  126. el.style.right = `${pickerMargin}px`;
  127. } else {
  128. el.style.right = 'auto';
  129. }
  130. }
  131. };
  132. const options = {
  133. field: this.element,
  134. yearRange: this.yearRange,
  135. firstDay: 1,
  136. i18n: this.i18n,
  137. theme: this.theme,
  138. reposition: this.reposition,
  139. isRTL: this.direction === 'rtl',
  140. position: this.direction === 'rtl' ? 'bottom right' : 'bottom left',
  141. showDaysInNextAndPreviousMonths: this.showDaysInNextAndPreviousMonths,
  142. enableSelectionDaysInNextAndPreviousMonths:
  143. this.enableSelectionDaysInNextAndPreviousMonths,
  144. };
  145. if (this.format !== '') {
  146. options.format = this.format;
  147. } else {
  148. options.toString = (date) => {
  149. const day = `0${date.getDate()}`.slice(-2);
  150. const month = `0${date.getMonth() + 1}`.slice(-2);
  151. const year = date.getFullYear();
  152. return `${day}-${month}-${year}`;
  153. };
  154. }
  155. // eslint-disable-next-line no-undef
  156. this.picker = new Pikaday({
  157. ...options,
  158. parse(input, format) {
  159. if (!options.format) {
  160. // Here we are using the default DD-MM-YYYY
  161. const [day, month, year] = input.split('-').map(Number);
  162. const fullYear = year < 100 ? 2000 + year : year;
  163. return new Date(fullYear, month - 1, day);
  164. }
  165. // If we have a custom format we rely on moment.js
  166. if (typeof moment !== 'undefined' && input !== '') {
  167. const parsedDate = moment(input, format, false);
  168. if (parsedDate.isValid()) {
  169. return parsedDate.toDate();
  170. }
  171. }
  172. return input;
  173. },
  174. onOpen() {
  175. // Call the static method to check styles.
  176. Datepicker.resizeLogic(this.el);
  177. },
  178. });
  179. // Set ecl initialized attribute
  180. this.element.setAttribute('data-ecl-auto-initialized', 'true');
  181. ECL.components.set(this.element, this);
  182. return this.picker;
  183. }
  184. /**
  185. * Destroy component.
  186. */
  187. destroy() {
  188. if (this.picker) {
  189. this.picker.destroy();
  190. this.picker = null;
  191. }
  192. if (this.attachResizeListener) {
  193. window.removeEventListener('resize', this.handleResize);
  194. }
  195. if (this.element) {
  196. this.element.removeAttribute('data-ecl-auto-initialized');
  197. ECL.components.delete(this.element);
  198. }
  199. }
  200. /**
  201. * Instance method to handle resizing with debouncing
  202. */
  203. handleResize() {
  204. clearTimeout(this.resizeTimer);
  205. this.resizeTimer = setTimeout(() => {
  206. if (this.pikadayEl) {
  207. Datepicker.resizeLogic(this.pikadayEl);
  208. }
  209. }, 150);
  210. }
  211. }
  212. export default Datepicker;