1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import Flatpickr from 'flatpickr';
|
9 | import settings from '../../globals/js/settings';
|
10 | import mixin from '../../globals/js/misc/mixin';
|
11 | import createComponent from '../../globals/js/mixins/create-component';
|
12 | import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
|
13 | import handles from '../../globals/js/mixins/handles';
|
14 | import on from '../../globals/js/misc/on';
|
15 | import { componentsX } from '../../globals/js/feature-flags';
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | function flattenOptions(options) {
|
24 | const o = {};
|
25 |
|
26 | for (const key in options) {
|
27 | o[key] = options[key];
|
28 | }
|
29 | return o;
|
30 | }
|
31 |
|
32 |
|
33 | Flatpickr.l10ns.en.weekdays.shorthand.forEach((day, index) => {
|
34 | const currentDay = Flatpickr.l10ns.en.weekdays.shorthand;
|
35 | if (currentDay[index] === 'Thu' || currentDay[index] === 'Th') {
|
36 | currentDay[index] = 'Th';
|
37 | } else {
|
38 | currentDay[index] = currentDay[index].charAt(0);
|
39 | }
|
40 | });
|
41 |
|
42 | const toArray = arrayLike => Array.prototype.slice.call(arrayLike);
|
43 |
|
44 | class DatePicker extends mixin(createComponent, initComponentBySearch, handles) {
|
45 | |
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | constructor(element, options) {
|
53 | super(element, options);
|
54 | const type = this.element.getAttribute(this.options.attribType);
|
55 | this.calendar = this._initDatePicker(type);
|
56 | if (this.calendar.calendarContainer) {
|
57 | this.manage(
|
58 | on(this.element, 'keydown', e => {
|
59 | if (e.which === 40) {
|
60 | this.calendar.calendarContainer.focus();
|
61 | }
|
62 | })
|
63 | );
|
64 | this.manage(
|
65 | on(this.calendar.calendarContainer, 'keydown', e => {
|
66 | if (e.which === 9 && type === 'range') {
|
67 | this._updateClassNames(this.calendar);
|
68 | this.element.querySelector(this.options.selectorDatePickerInputFrom).focus();
|
69 | }
|
70 | })
|
71 | );
|
72 | }
|
73 | }
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 |
|
80 | _handleFocus = () => {
|
81 | if (this.calendar) {
|
82 | this.calendar.open();
|
83 | }
|
84 | };
|
85 |
|
86 | |
87 |
|
88 |
|
89 |
|
90 |
|
91 | _handleBlur = event => {
|
92 | if (this.calendar) {
|
93 | const focusTo = event.relatedTarget;
|
94 | if (
|
95 | !focusTo ||
|
96 | (!this.element.contains(focusTo) &&
|
97 | (!this.calendar.calendarContainer || !this.calendar.calendarContainer.contains(focusTo)))
|
98 | ) {
|
99 | this.calendar.close();
|
100 | }
|
101 | }
|
102 | };
|
103 |
|
104 | _initDatePicker = type => {
|
105 | if (type === 'range') {
|
106 |
|
107 |
|
108 | const doc = this.element.ownerDocument;
|
109 | const rangeInput = doc.createElement('input');
|
110 | rangeInput.className = this.options.classVisuallyHidden;
|
111 | rangeInput.setAttribute('aria-hidden', 'true');
|
112 | this.element.appendChild(rangeInput);
|
113 | this._rangeInput = rangeInput;
|
114 |
|
115 |
|
116 |
|
117 | const w = doc.defaultView;
|
118 | const hasFocusin = 'onfocusin' in w;
|
119 | const hasFocusout = 'onfocusout' in w;
|
120 | const focusinEventName = hasFocusin ? 'focusin' : 'focus';
|
121 | const focusoutEventName = hasFocusout ? 'focusout' : 'blur';
|
122 | this.manage(on(this.element, focusinEventName, this._handleFocus, !hasFocusin));
|
123 | this.manage(on(this.element, focusoutEventName, this._handleBlur, !hasFocusout));
|
124 | this.manage(
|
125 | on(this.element.querySelector(this.options.selectorDatePickerIcon), focusoutEventName, this._handleBlur, !hasFocusout)
|
126 | );
|
127 | }
|
128 | const self = this;
|
129 | const date = type === 'range' ? this._rangeInput : this.element.querySelector(this.options.selectorDatePickerInput);
|
130 | const { onClose, onChange, onMonthChange, onYearChange, onOpen, onValueUpdate } = this.options;
|
131 | const calendar = new Flatpickr(
|
132 | date,
|
133 | Object.assign(flattenOptions(this.options), {
|
134 | allowInput: true,
|
135 | mode: type,
|
136 | positionElement: type === 'range' && this.element.querySelector(this.options.selectorDatePickerInputFrom),
|
137 | onClose(selectedDates, ...remainder) {
|
138 |
|
139 |
|
140 | if (self.shouldForceOpen) {
|
141 | if (self.calendar.calendarContainer) {
|
142 | self.calendar.calendarContainer.classList.add('open');
|
143 | }
|
144 | self.calendar.isOpen = true;
|
145 | }
|
146 | if (!onClose || onClose.call(this, selectedDates, ...remainder) !== false) {
|
147 | self._updateClassNames(calendar);
|
148 | self._updateInputFields(selectedDates, type);
|
149 | }
|
150 | },
|
151 | onChange(...args) {
|
152 | if (!onChange || onChange.call(this, ...args) !== false) {
|
153 | self._updateClassNames(calendar);
|
154 | if (type === 'range') {
|
155 | if (calendar.selectedDates.length === 1 && calendar.isOpen) {
|
156 | self.element.querySelector(self.options.selectorDatePickerInputTo).classList.add(self.options.classFocused);
|
157 | } else {
|
158 | self.element.querySelector(self.options.selectorDatePickerInputTo).classList.remove(self.options.classFocused);
|
159 | }
|
160 | }
|
161 | }
|
162 | },
|
163 | onMonthChange(...args) {
|
164 | if (!onMonthChange || onMonthChange.call(this, ...args) !== false) {
|
165 | self._updateClassNames(calendar);
|
166 | }
|
167 | },
|
168 | onYearChange(...args) {
|
169 | if (!onYearChange || onYearChange.call(this, ...args) !== false) {
|
170 | self._updateClassNames(calendar);
|
171 | }
|
172 | },
|
173 | onOpen(...args) {
|
174 |
|
175 |
|
176 | self.shouldForceOpen = true;
|
177 | setTimeout(() => {
|
178 | self.shouldForceOpen = false;
|
179 | }, 0);
|
180 | if (!onOpen || onOpen.call(this, ...args) !== false) {
|
181 | self._updateClassNames(calendar);
|
182 | }
|
183 | },
|
184 | onValueUpdate(...args) {
|
185 | if ((!onValueUpdate || onValueUpdate.call(this, ...args) !== false) && type === 'range') {
|
186 | self._updateInputFields(self.calendar.selectedDates, type);
|
187 | }
|
188 | },
|
189 | nextArrow: this._rightArrowHTML(),
|
190 | prevArrow: this._leftArrowHTML(),
|
191 | })
|
192 | );
|
193 | if (type === 'range') {
|
194 | this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputFrom), 0);
|
195 | this._addInputLogic(this.element.querySelector(this.options.selectorDatePickerInputTo), 1);
|
196 | }
|
197 | this.manage(
|
198 | on(this.element.querySelector(this.options.selectorDatePickerIcon), 'click', () => {
|
199 | calendar.open();
|
200 | })
|
201 | );
|
202 | this._updateClassNames(calendar);
|
203 | if (type !== 'range') {
|
204 | this._addInputLogic(date);
|
205 | }
|
206 | return calendar;
|
207 | };
|
208 |
|
209 | _rightArrowHTML() {
|
210 | return componentsX
|
211 | ? `
|
212 | <svg
|
213 | focusable="false"
|
214 | preserveAspectRatio="xMidYMid meet"
|
215 | style="will-change: transform;"
|
216 | xmlns="http://www.w3.org/2000/svg"
|
217 | width="16"
|
218 | height="16"
|
219 | viewBox="0 0 16 16"
|
220 | aria-hidden="true">
|
221 | <path d="M11 8l-5 5-.7-.7L9.6 8 5.3 3.7 6 3z"></path>
|
222 | </svg>`
|
223 | : `
|
224 | <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
|
225 | <path d="M0 10.6L4.7 6 0 1.4 1.4 0l6.1 6-6.1 6z"></path>
|
226 | </svg>`;
|
227 | }
|
228 |
|
229 | _leftArrowHTML() {
|
230 | return componentsX
|
231 | ? `
|
232 | <svg
|
233 | focusable="false"
|
234 | preserveAspectRatio="xMidYMid meet"
|
235 | style="will-change: transform;"
|
236 | xmlns="http://www.w3.org/2000/svg"
|
237 | width="16"
|
238 | height="16"
|
239 | viewBox="0 0 16 16"
|
240 | aria-hidden="true"
|
241 | >
|
242 | <path d="M5 8l5-5 .7.7L6.4 8l4.3 4.3-.7.7z"></path>
|
243 | </svg>`
|
244 | : `
|
245 | <svg width="8" height="12" viewBox="0 0 8 12" fill-rule="evenodd">
|
246 | <path d="M7.5 10.6L2.8 6l4.7-4.6L6.1 0 0 6l6.1 6z"></path>
|
247 | </svg>`;
|
248 | }
|
249 |
|
250 | _addInputLogic = (input, index) => {
|
251 | if (!isNaN(index) && (index < 0 || index > 1)) {
|
252 | throw new RangeError(`The index of <input> (${index}) is out of range.`);
|
253 | }
|
254 | const inputField = input;
|
255 | this.manage(
|
256 | on(inputField, 'change', evt => {
|
257 | if (evt.isTrusted || (evt.detail && evt.detail.isNotFromFlatpickr)) {
|
258 | const inputDate = this.calendar.parseDate(inputField.value);
|
259 | if (inputDate && !isNaN(inputDate.valueOf())) {
|
260 | if (isNaN(index)) {
|
261 | this.calendar.setDate(inputDate);
|
262 | } else {
|
263 | const { selectedDates } = this.calendar;
|
264 | selectedDates[index] = inputDate;
|
265 | this.calendar.setDate(selectedDates);
|
266 | }
|
267 | }
|
268 | }
|
269 | this._updateClassNames(this.calendar);
|
270 | })
|
271 | );
|
272 |
|
273 |
|
274 | this.manage(
|
275 | on(inputField, 'keydown', evt => {
|
276 | const origInput = this.calendar._input;
|
277 | this.calendar._input = evt.target;
|
278 | setTimeout(() => {
|
279 | this.calendar._input = origInput;
|
280 | });
|
281 | })
|
282 | );
|
283 | };
|
284 |
|
285 | _updateClassNames = ({ calendarContainer, selectedDates }) => {
|
286 | if (calendarContainer) {
|
287 | calendarContainer.classList.add(this.options.classCalendarContainer);
|
288 | calendarContainer.querySelector('.flatpickr-month').classList.add(this.options.classMonth);
|
289 | calendarContainer.querySelector('.flatpickr-weekdays').classList.add(this.options.classWeekdays);
|
290 | calendarContainer.querySelector('.flatpickr-days').classList.add(this.options.classDays);
|
291 | toArray(calendarContainer.querySelectorAll('.flatpickr-weekday')).forEach(item => {
|
292 | const currentItem = item;
|
293 | currentItem.innerHTML = currentItem.innerHTML.replace(/\s+/g, '');
|
294 | currentItem.classList.add(this.options.classWeekday);
|
295 | });
|
296 | toArray(calendarContainer.querySelectorAll('.flatpickr-day')).forEach(item => {
|
297 | item.classList.add(this.options.classDay);
|
298 | if (item.classList.contains('today') && selectedDates.length > 0) {
|
299 | item.classList.add('no-border');
|
300 | } else if (item.classList.contains('today') && selectedDates.length === 0) {
|
301 | item.classList.remove('no-border');
|
302 | }
|
303 | });
|
304 | }
|
305 | };
|
306 |
|
307 | _updateInputFields = (selectedDates, type) => {
|
308 | if (type === 'range') {
|
309 | if (selectedDates.length === 2) {
|
310 | this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
|
311 | this.element.querySelector(this.options.selectorDatePickerInputTo).value = this._formatDate(selectedDates[1]);
|
312 | } else if (selectedDates.length === 1) {
|
313 | this.element.querySelector(this.options.selectorDatePickerInputFrom).value = this._formatDate(selectedDates[0]);
|
314 | }
|
315 | } else if (selectedDates.length === 1) {
|
316 | this.element.querySelector(this.options.selectorDatePickerInput).value = this._formatDate(selectedDates[0]);
|
317 | }
|
318 | this._updateClassNames(this.calendar);
|
319 | };
|
320 |
|
321 | _formatDate = date => this.calendar.formatDate(date, this.calendar.config.dateFormat);
|
322 |
|
323 | release() {
|
324 | if (this._rangeInput && this._rangeInput.parentNode) {
|
325 | this._rangeInput.parentNode.removeChild(this._rangeInput);
|
326 | }
|
327 | if (this.calendar) {
|
328 | try {
|
329 | this.calendar.destroy();
|
330 | } catch (err) {}
|
331 | this.calendar = null;
|
332 | }
|
333 | return super.release();
|
334 | }
|
335 |
|
336 | |
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 |
|
343 | static get options() {
|
344 | const { prefix } = settings;
|
345 | return {
|
346 | selectorInit: '[data-date-picker]',
|
347 | selectorDatePickerInput: '[data-date-picker-input]',
|
348 | selectorDatePickerInputFrom: '[data-date-picker-input-from]',
|
349 | selectorDatePickerInputTo: '[data-date-picker-input-to]',
|
350 | selectorDatePickerIcon: '[data-date-picker-icon]',
|
351 | classCalendarContainer: `${prefix}--date-picker__calendar`,
|
352 | classMonth: `${prefix}--date-picker__month`,
|
353 | classWeekdays: `${prefix}--date-picker__weekdays`,
|
354 | classDays: `${prefix}--date-picker__days`,
|
355 | classWeekday: `${prefix}--date-picker__weekday`,
|
356 | classDay: `${prefix}--date-picker__day`,
|
357 | classFocused: `${prefix}--focused`,
|
358 | classVisuallyHidden: `${prefix}--visually-hidden`,
|
359 | attribType: 'data-date-picker-type',
|
360 | dateFormat: 'm/d/Y',
|
361 | };
|
362 | }
|
363 |
|
364 | |
365 |
|
366 |
|
367 |
|
368 | static components = new WeakMap();
|
369 | }
|
370 |
|
371 | export default DatePicker;
|