diff --git a/sass/components/_chips.scss b/sass/components/_chips.scss index 5104445628..0750b176d5 100644 --- a/sass/components/_chips.scss +++ b/sass/components/_chips.scss @@ -75,7 +75,6 @@ flex-wrap: wrap; border: none; - border-bottom: 1px solid var(--md-sys-color-on-surface-variant); box-shadow: none; margin: 0 0 8px 0; @@ -90,8 +89,12 @@ box-shadow: 0 1px 0 0 var(--md-sys-color-primary); } - &:hover { - cursor: text; + &.input-field { + border-bottom: 1px solid var(--md-sys-color-on-surface-variant); + + &:hover { + cursor: text; + } } input:not([type]):not(.browser-default).input { diff --git a/spec/tests/chips/chipsSpec.js b/spec/tests/chips/chipsSpec.js index 68db00185f..1b65ddc647 100644 --- a/spec/tests/chips/chipsSpec.js +++ b/spec/tests/chips/chipsSpec.js @@ -1,10 +1,12 @@ /* eslint-disable no-undef */ describe('Chips', () => { - const fixture = `
-
-
-
`; + const fixture = `
+
+
+
+
+
`; beforeEach(() => { XloadHtml(fixture); @@ -21,11 +23,29 @@ describe('Chips', () => { } ] }); - M.Chips.init(document.querySelector('.chips-placeholder'), { + M.Chips.init(document.querySelector('.chips.input-field'), { + allowUserInput: true + }); + M.Chips.init(document.querySelector('.chips-initial.input-field'), { + allowUserInput: true, + data: [ + { id: 12, text: 'Apple' }, + { id: 13, text: 'Microsoft' }, + { + id: 42, + text: 'Google', + image: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' + } + ] + }); + M.Chips.init(document.querySelector('.chips-placeholder.input-field'), { + allowUserInput: true, placeholder: 'Enter a tag', secondaryPlaceholder: '+Tag' }); - M.Chips.init(document.querySelector('.chips-autocomplete'), { + M.Chips.init(document.querySelector('.chips-autocomplete.input-field'), { + allowUserInput: true, autocompleteOptions: { data: [ { id: 12, text: 'Apple' }, @@ -38,20 +58,21 @@ describe('Chips', () => { afterEach(() => XunloadFixtures()); describe('chips plugin', () => { - let chips, input; + let chips, chipsUserInput, input; it('should work with multiple initializations', () => { chips = document.querySelector('.chips'); M.Chips.init(chips); M.Chips.init(chips); M.Chips.init(chips); - M.Chips.init(chips); - input = chips.querySelectorAll('input'); + chipsUserInput = document.querySelector('.chips.input-field'); + M.Chips.init(chips, {allowUserInput: true}); + input = chipsUserInput.querySelectorAll('input'); expect(input.length).toEqual(1, 'Should dynamically generate chips structure.'); }); it('should be able to add chip', (done) => { - chips = document.querySelector('.chips'); + chips = document.querySelector('.chips.input-field'); input = chips.querySelector('input'); input.value = 'one'; keydown(input, 13); @@ -68,11 +89,12 @@ describe('Chips', () => { }); it('should be able to delete chip', (done) => { - chips = document.querySelector('.chips.chips-initial'); - input = chips.querySelector('input'); + chips = document.querySelector('.chips.chips-initial.input-field'); let numChips = chips.querySelectorAll('.chip').length; expect(numChips).toEqual(3, '3 initial chips should have been added'); - click(chips.querySelector('.chip .close')); + let chipCloseButton = chips.querySelectorAll('.chip .close'); + expect(chipCloseButton.length).toEqual(3, 'expected all chips to have close button'); + click(chipCloseButton[0]); setTimeout(() => { numChips = chips.querySelectorAll('.chip').length; expect(numChips).toEqual(2, 'one chip should have been deleted'); @@ -81,7 +103,7 @@ describe('Chips', () => { }); it('should have working callbacks', (done) => { - chips = document.querySelector('.chips'); + chips = document.querySelector('.chips.input-field'); let chipWasAdded = false; let chipAddedElem = null; let chipSelect = false; @@ -90,6 +112,7 @@ describe('Chips', () => { let chipDeleted = null; M.Chips.init(chips, { + allowUserInput: true, data: [{ id: 'One' }, { id: 'Two' }, { id: 'Three' }], onChipAdd: (chipsEl, chipEl) => { chipAddedElem = chipEl; diff --git a/spec/tests/datepicker/datepickerSpec.js b/spec/tests/datepicker/datepickerSpec.js index d2cc8f613d..63b91596a1 100644 --- a/spec/tests/datepicker/datepickerSpec.js +++ b/spec/tests/datepicker/datepickerSpec.js @@ -7,14 +7,16 @@ describe('Datepicker Plugin', () => { `; - beforeEach(() => { - XloadHtml(fixture); - M.Datepicker.init(document.querySelectorAll('.datepicker')); - }); + beforeEach(() => XloadHtml(fixture)); afterEach(() => XunloadFixtures()); describe('Datepicker', () => { + afterEach(() => { + M.Datepicker.getInstance(document.querySelector('.datepicker')).destroy(); + }); + it('should open and close programmatically', (done) => { + M.Datepicker.init(document.querySelectorAll('.datepicker')); const input = document.querySelector('#datepickerInput'); const modal = document.querySelector('.datepicker-modal'); expect(modal).toBeHidden('Should be hidden before datepicker input is focused.'); @@ -39,17 +41,18 @@ describe('Datepicker Plugin', () => { it('can have a string format', (done) => { const input = document.querySelector('#datepickerInput'); const today = new Date(); - M.Datepicker.init(input, { format: 'mm/dd/yyyy' }).open(); - M.Datepicker.getInstance(input).open(); + M.Datepicker.init(input, { format: 'mm/dd/yyyy' }); + const datepicker = M.Datepicker.getInstance(input); + datepicker.open(); setTimeout(() => { const day1 = document.querySelector('.datepicker-modal button[data-day="1"]'); day1.click(); + document.querySelector('.datepicker-done').click(); setTimeout(() => { const year = today.getFullYear(); - let month = today.getMonth() + 1; - month = month < 10 ? `0${month}` : month; - const value = M.Datepicker.getInstance(input).toString(); - expect(value).toEqual(`${month}/01/${year}`); + const month = today.getMonth() + 1; + const value = datepicker.toString(); + expect(value).toEqual(`${month < 10 ? `0${month}` : month}/01/${year}`); done(); }, 400); }, 400); @@ -58,20 +61,57 @@ describe('Datepicker Plugin', () => { it('can have a format function', (done) => { const input = document.querySelector('#datepickerInput'); const today = new Date(); - const formatFn = (date) => `${date.getFullYear() - 100}-${date.getMonth() + 1}-99`; - M.Datepicker.init(input, { format: formatFn }).open(); - M.Datepicker.getInstance(input).open(); + const formatFn = `${today.getFullYear() - 100}-${today.getMonth() + 1}-99`; + M.Datepicker.init(input, { format: formatFn }); + const datepicker = M.Datepicker.getInstance(input); + datepicker.open(); setTimeout(() => { const day1 = document.querySelector('.datepicker-modal button[data-day="1"]'); day1.click(); + document.querySelector('.datepicker-done').click(); setTimeout(() => { const year = today.getFullYear() - 100; const month = today.getMonth() + 1; - const value = M.Datepicker.getInstance(input).toString(); - expect(value).toEqual(`${year}-${month}-99`); + expect(datepicker.toString()).toEqual(`${year}-${month < 10 ? `0${month}` : month}-99`); done(); }, 400); }, 400); }); + + it('can change the calendar modal selected date by input', (done) => { + const input = document.querySelector('#datepickerInput'); + M.Datepicker.init(input, { format: 'mm/dd/yyyy' }); + const today = new Date(); + let month = today.getMonth(); + const year = today.getFullYear() - 44; + const day = 11; + input.value = `${month < 10 ? `0${month}` : month}/${day}/${year}`; + input.dispatchEvent( + new KeyboardEvent('change', { bubbles: true, cancelable: true }) + ); + keydown(input, 13); + setTimeout(() => { + expect(document.querySelector('.datepicker-modal')).toHaveClass( + 'open', + 'modal should be shown after input is submitted.' + ); + const selectMonthElem = document.querySelector('.datepicker-select.orig-select-month'); + const selectYearElem = document.querySelector('.datepicker-select.orig-select-year'); + const selectedDayElem = document.querySelector(`.datepicker-row td[data-day="${day}"]`); + expect(selectMonthElem.querySelector('option[selected="selected"]').value === (month - 1).toString()).toEqual(true, `selected month should be ${month}, given value ${selectMonthElem.querySelector('option[selected="selected"]').value}`); + expect(selectYearElem.querySelector('option[selected="selected"]').value === year.toString()).toEqual(true, `selected year should be ${year}, given value ${selectYearElem.querySelector('option[selected="selected"]').value}`); + expect(selectedDayElem.classList.contains('is-selected')).toEqual(true, `selected day should be ${day}, given value ${selectedDayElem.classList}`); + done(); + }, 10); + }); + + it('should have a date range input field if date range option is enabled', (done) => { + const input = document.querySelector('#datepickerInput'); + M.Datepicker.init(input, { isDateRange: true }); + setTimeout(() => { + expect(document.querySelector('.datepicker-end-date')).toExist('end date input should exist'); + done(); + }, 10); + }); }); }); diff --git a/src/chips.ts b/src/chips.ts index 9680f59ec8..a63101ed8c 100644 --- a/src/chips.ts +++ b/src/chips.ts @@ -53,6 +53,11 @@ export interface ChipsOptions extends BaseOptions{ * @default 'material-icons' */ closeIconClass: string; + /** + * Specifies option to render user input field + * @default false; + */ + allowUserInput: boolean; /** * Callback for chip add. * @default null @@ -78,6 +83,7 @@ let _defaults: ChipsOptions = { autocompleteOptions: {}, autocompleteOnly: false, limit: Infinity, + allowUserInput: false, onChipAdd: null, onChipSelect: null, onChipDelete: null @@ -109,26 +115,25 @@ export class Chips extends Component { ...options }; - this.el.classList.add('chips', 'input-field'); + this.el.classList.add('chips'); this.chipsData = []; this._chips = []; - this._setupInput(); - this.hasAutocomplete = Object.keys(this.options.autocompleteOptions).length > 0; - - // Set input id - if (!this._input.getAttribute('id')) - this._input.setAttribute('id', Utils.guid()); // Render initial chips if (this.options.data.length) { this.chipsData = this.options.data; this._renderChips(); } - // Setup autocomplete if needed - if (this.hasAutocomplete) this._setupAutocomplete(); - this._setPlaceholder(); this._setupLabel(); - this._setupEventHandlers(); + + // Render input element, setup event handlers + if(this.options.allowUserInput) { + this.el.classList.add('input-field'); + this._setupInput(); + this._setupEventHandlers(); + // move input to end + this.el.append(this._input); + } } static get defaults() { @@ -165,7 +170,9 @@ export class Chips extends Component { } destroy() { - this._removeEventHandlers(); + if(this.options.allowUserInput) { + this._removeEventHandlers(); + } this._chips.forEach(c => c.remove()); this._chips = []; (this.el as any).M_Chips = undefined; @@ -302,17 +309,19 @@ export class Chips extends Component { const renderedChip = document.createElement('div'); renderedChip.classList.add('chip'); renderedChip.innerText = chip.text || chip.id; - renderedChip.setAttribute('tabindex', "0"); - const closeIcon = document.createElement('i'); - closeIcon.classList.add(this.options.closeIconClass, 'close'); - closeIcon.innerText = 'close'; // attach image if needed if (chip.image) { const img = document.createElement('img'); img.setAttribute('src', chip.image); renderedChip.insertBefore(img, renderedChip.firstChild); } - renderedChip.appendChild(closeIcon); + if(this.options.allowUserInput) { + renderedChip.setAttribute('tabindex', '0'); + const closeIcon = document.createElement('i'); + closeIcon.classList.add(this.options.closeIconClass, 'close'); + closeIcon.innerText = 'close'; + renderedChip.appendChild(closeIcon); + } return renderedChip; } @@ -323,8 +332,6 @@ export class Chips extends Component { this.el.appendChild(chipElem); this._chips.push(chipElem); } - // move input to end - this.el.append(this._input); } _setupAutocomplete() { @@ -347,6 +354,13 @@ export class Chips extends Component { this.el.append(this._input); } this._input.classList.add('input'); + this.hasAutocomplete = Object.keys(this.options.autocompleteOptions).length > 0; + // Setup autocomplete if needed + if (this.hasAutocomplete) this._setupAutocomplete(); + this._setPlaceholder(); + // Set input id + if (!this._input.getAttribute('id')) + this._input.setAttribute('id', Utils.guid()); } _setupLabel() { diff --git a/src/datepicker.ts b/src/datepicker.ts index 94015e0f4f..3652018a7c 100644 --- a/src/datepicker.ts +++ b/src/datepicker.ts @@ -1,7 +1,7 @@ -import { Modal } from "./modal"; -import { Utils } from "./utils"; -import { FormSelect } from "./select"; -import { BaseOptions, Component, InitElements, MElement, I18nOptions } from "./component"; +import { Modal } from './modal'; +import { Utils } from './utils'; +import { FormSelect } from './select'; +import { BaseOptions, Component, I18nOptions, InitElements, MElement } from './component'; export interface DateI18nOptions extends I18nOptions { previousMonth: string; @@ -31,16 +31,31 @@ export interface DatepickerOptions extends BaseOptions { * @default null */ parse: ((value: string, format: string) => Date) | null; + /** + * The initial condition if the datepicker is based on date range. + * @default false + */ + isDateRange: boolean; /** * The initial date to view when first opened. * @default null */ defaultDate: Date | null; + /** + * The initial end date to view when first opened. + * @default null + */ + defaultEndDate: Date | null; /** * Make the `defaultDate` the initial selected value. * @default false */ setDefaultDate: boolean; + /** + * Make the `defaultEndDate` the initial selected value. + * @default false + */ + setDefaultEndDate: boolean; /** * Prevent selection of any date on the weekend. * @default false @@ -156,10 +171,16 @@ let _defaults: DatepickerOptions = { format: 'mmm dd, yyyy', // Used to create date object from current input string parse: null, + // The initial condition if the datepicker is based on date range + isDateRange: false, // The initial date to view when first opened defaultDate: null, + // The initial end date to view when first opened + defaultEndDate: null, // Make the `defaultDate` the initial selected value setDefaultDate: false, + // Make the `defaultEndDate` the initial selected value + setDefaultEndDate: false, disableWeekends: false, disableDayFn: null, // First day of week (0: Sunday, 1: Monday etc) @@ -240,6 +261,7 @@ export class Datepicker extends Component { id: string; /** If the picker is open. */ isOpen: boolean; + multiple: boolean = false; modal: Modal; calendarEl: HTMLElement; /** CLEAR button instance. */ @@ -250,8 +272,10 @@ export class Datepicker extends Component { modalEl: HTMLElement; yearTextEl: HTMLElement; dateTextEl: HTMLElement; + endDateEl: HTMLInputElement; /** The selected Date. */ date: Date; + endDate: null|Date; formats: any; calendars: any; private _y: any; @@ -291,7 +315,7 @@ export class Datepicker extends Component { if (Datepicker._isDate(defDate)) { if (this.options.setDefaultDate) { this.setDate(defDate, true); - this.setInputValue(); + this.setInputValue(this.el, defDate); } else { this.gotoDate(defDate); @@ -300,12 +324,18 @@ export class Datepicker extends Component { else { this.gotoDate(new Date()); } - this.isOpen = false; - - // HTML5 input date field support - if(this.el.type == 'date') { - this.el.classList.add('datepicker-date-input'); + if (this.options.isDateRange) { + this.multiple = true; + let defEndDate = this.options.defaultEndDate; + if(Datepicker._isDate(defEndDate)) + { + if (this.options.setDefaultEndDate) { + this.setDate(defEndDate, true, true); + this.setInputValue(this.endDateEl, defEndDate); + } + } } + this.isOpen = false; } static get defaults() { @@ -342,6 +372,9 @@ export class Datepicker extends Component { return day === 0 || day === 6; } + /** + * @deprecated since it does nothing as it's only updating the encapsulated variable (no return/global param change) + */ static _setToStartOfDay(date) { if (Datepicker._isDate(date)) date.setHours(0, 0, 0, 0); } @@ -362,6 +395,14 @@ export class Datepicker extends Component { return a.getTime() === b.getTime(); } + static _compareWithinRange(day: Date, date: Date, dateEnd: Date) { + return day.getTime() > date.getTime() && day.getTime() < dateEnd.getTime(); + } + + static _comparePastDate(a: Date, b: Date) { + return a.getTime() < b.getTime() + } + static getInstance(el: HTMLElement): Datepicker { return (el as any).M_Datepicker; } @@ -386,6 +427,20 @@ export class Datepicker extends Component { } _insertHTMLIntoDOM() { + // HTML5 input date field support + if(this.el.type == 'date') { + this.el.classList.add('datepicker-date-input'); + } + + if (this.options.isDateRange) { + this.endDateEl = this.el.cloneNode(true); + this.endDateEl.classList.add('datepicker-end-date'); + this.endDateEl.addEventListener('click', this._handleInputClick); + this.endDateEl.addEventListener('keypress', this._handleInputKeydown); + this.endDateEl.addEventListener('change', this._handleInputChange); + this.el.parentElement.appendChild(this.endDateEl); + } + if (this.options.showClearBtn) { this.clearBtn.style.visibility = ''; this.clearBtn.innerText = this.options.i18n.clear; @@ -415,33 +470,45 @@ export class Datepicker extends Component { } /** - * Gets a string representation of the selected date. + * Gets a string representation of the given date. */ - toString(format: string | ((d: Date) => string) = null): string { + toString(date: Date = this.date, format: string | ((d: Date) => string) = null): string { format = format || this.options.format; - if (typeof format === 'function') return format(this.date); - if (!Datepicker._isDate(this.date)) return ''; + if (typeof format === 'function') return format(date); + if (!Datepicker._isDate(date)) return ''; // String Format - return this.formatDate(format); + return this.formatDate(date, format); } - formatDate(format) { + /** + * Returns the formatted date. + */ + formatDate(date: Date, format: string) { const formatArray = format.split(/(d{1,4}|m{1,4}|y{4}|yy|!.)/g); - const formattedDate = formatArray - .map(label => this.formats[label] ? this.formats[label]() : label) + return formatArray + .map(label => this.formats[label] ? this.formats[label](date) : label) .join(''); - return formattedDate; + } + + /** + * Sets date from input field. + */ + setDateFromInput(el: HTMLInputElement) { + const date = new Date(Date.parse(el.value)); + this.setDate(date, false, el == this.endDateEl); } /** * Set a date on the datepicker. * @param date Date to set on the datepicker. * @param preventOnSelect Undocumented as of 5 March 2018. + * @param isEndDate */ - setDate(date: Date | string = null, preventOnSelect: boolean = false) { + setDate(date: Date = null, preventOnSelect: boolean = false, isEndDate: boolean = false) { + let selectedDate; if (!date) { - this.date = null; - this._renderDateDisplay(); + selectedDate = null; + this._renderDateDisplay(date); return this.draw(); } if (typeof date === 'string') { @@ -458,43 +525,62 @@ export class Datepicker extends Component { else if (Datepicker._isDate(max) && date > max) { date = max; } - this.date = new Date(date.getTime()); - this._renderDateDisplay(); - Datepicker._setToStartOfDay(this.date); - this.gotoDate(this.date); + selectedDate = new Date(date.getTime()); + if(!isEndDate) { + this.date = selectedDate; + } else if(isEndDate) { + this.endDate = selectedDate; + } + Datepicker._setToStartOfDay(selectedDate); + this.gotoDate(!isEndDate ? this.date : this.endDate); if (!preventOnSelect && typeof this.options.onSelect === 'function') { - this.options.onSelect.call(this, this.date); + this.options.onSelect.call(this, selectedDate); } } /** * Sets the data-date attribute on the date input field */ - setDataDate() { - this.el.setAttribute('data-date', this.toString()) + setDataDate(el, date) { + el.setAttribute('data-date', this.toString(date)) } /** - * Sets current date as the input value. + * Sets dates on the input values. */ - setInputValue() { - if(this.el.type == 'date') { - this.setDataDate() - this.el.value = this.formatDate('yyyy-mm-dd') + setInputValues() { + this.setInputValue(this.el, this.date); + if (this.options.isDateRange) { + this.setInputValue(this.endDateEl, this.endDate); + } + } + + /** + * Sets given date as the input value on the given element. + */ + setInputValue(el, date) { + if(el.type == 'date') { + this.setDataDate(el, date); + el.value = this.formatDate(date, 'yyyy-mm-dd'); } else { - this.el.value = this.toString(); + el.value = this.toString(date); } this.el.dispatchEvent(new CustomEvent('change', {bubbles:true, cancelable:true, composed:true, detail: {firedBy: this}})); } - _renderDateDisplay() { - let displayDate = Datepicker._isDate(this.date) ? this.date : new Date(); - let i18n = this.options.i18n; - let day = i18n.weekdaysShort[displayDate.getDay()]; - let month = i18n.monthsShort[displayDate.getMonth()]; - let date = displayDate.getDate(); - this.yearTextEl.innerHTML = displayDate.getFullYear().toString(); - this.dateTextEl.innerHTML = `${day}, ${month} ${date}`; + /** + * Renders the date in the modal head section. + */ + _renderDateDisplay(date: Date, endDate: Date = null) { + const displayDate = Datepicker._isDate(date) ? date : new Date(); + // this.yearTextEl.innerHTML = displayDate.getFullYear().toString(); + // @todo should we include an option for date formatting by component options? + if (!this.options.isDateRange) { + this.dateTextEl.innerHTML = this.formatDate(displayDate, 'ddd, mmm d'); + } else { + const displayEndDate = Datepicker._isDate(endDate) ? endDate : new Date(); + this.dateTextEl.innerHTML = `${this.formatDate(displayDate, 'mmm d')} - ${this.formatDate(displayEndDate, 'mmm d')}`; + } } /** @@ -586,11 +672,8 @@ export class Datepicker extends Component { let isWeekSelected = false; for (let i = 0, r = 0; i < cells; i++) { let day = new Date(year, month, 1 + (i - before)), - isSelected = Datepicker._isDate(this.date) - ? Datepicker._compareDates(day, this.date) - : false, isToday = Datepicker._compareDates(day, now), - hasEvent = opts.events.indexOf(day.toDateString()) !== -1 ? true : false, + hasEvent = opts.events.indexOf(day.toDateString()) !== -1, isEmpty = i < before || i >= days + before, dayNumber = 1 + (i - before), monthNumber = month, @@ -603,7 +686,17 @@ export class Datepicker extends Component { (opts.minDate && day < opts.minDate) || (opts.maxDate && day > opts.maxDate) || (opts.disableWeekends && Datepicker._isWeekend(day)) || - (opts.disableDayFn && opts.disableDayFn(day)); + (opts.disableDayFn && opts.disableDayFn(day)), + isDateRange = opts.isDateRange && Datepicker._isDate(this.endDate) && Datepicker._compareWithinRange(day, this.date, this.endDate); + + let isSelected = false; + if (Datepicker._isDate(this.date)) { + isSelected = Datepicker._compareDates(day, this.date); + } + + if (!isSelected && Datepicker._isDate(this.endDate)) { + isSelected = Datepicker._compareDates(day, this.endDate); + } if (isEmpty) { if (i < before) { @@ -629,7 +722,8 @@ export class Datepicker extends Component { isStartRange: isStartRange, isEndRange: isEndRange, isInRange: isInRange, - showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths + showDaysInNextAndPreviousMonths: opts.showDaysInNextAndPreviousMonths, + isDateRange: isDateRange, }; row.push(this.renderDay(dayConfig)); @@ -678,6 +772,9 @@ export class Datepicker extends Component { if (opts.isEndRange) { arr.push('is-endrange'); } + if (opts.isDateRange) { + arr.push('is-daterange'); + } return ( `` + `` + @@ -839,7 +936,11 @@ export class Datepicker extends Component { .substr(0, 2); for (let c = 0; c < 1; c++) { - this._renderDateDisplay(); + if(!this.options.isDateRange) { + this._renderDateDisplay(this.date); + } else { + this._renderDateDisplay(this.date, this.endDate); + } html += this.renderTitle( this, @@ -904,37 +1005,37 @@ export class Datepicker extends Component { this.cancelBtn = this.modalEl.querySelector('.datepicker-cancel'); this.formats = { - d: () => { - return this.date.getDate(); + d: (date: Date) => { + return date.getDate(); }, - dd: () => { - let d = this.date.getDate(); + dd: (date: Date) => { + let d = date.getDate(); return (d < 10 ? '0' : '') + d; }, - ddd: () => { - return this.options.i18n.weekdaysShort[this.date.getDay()]; + ddd: (date: Date) => { + return this.options.i18n.weekdaysShort[date.getDay()]; }, - dddd: () => { - return this.options.i18n.weekdays[this.date.getDay()]; + dddd: (date: Date) => { + return this.options.i18n.weekdays[date.getDay()]; }, - m: () => { - return this.date.getMonth() + 1; + m: (date: Date) => { + return date.getMonth() + 1; }, - mm: () => { - let m = this.date.getMonth() + 1; + mm: (date: Date) => { + let m = date.getMonth() + 1; return (m < 10 ? '0' : '') + m; }, - mmm: () => { - return this.options.i18n.monthsShort[this.date.getMonth()]; + mmm: (date: Date) => { + return this.options.i18n.monthsShort[date.getMonth()]; }, - mmmm: () => { - return this.options.i18n.months[this.date.getMonth()]; + mmmm: (date: Date) => { + return this.options.i18n.months[date.getMonth()]; }, - yy: () => { - return ('' + this.date.getFullYear()).slice(2); + yy: (date: Date) => { + return ('' + date.getFullYear()).slice(2); }, - yyyy: () => { - return this.date.getFullYear(); + yyyy: (date: Date) => { + return date.getFullYear(); } }; } @@ -944,19 +1045,27 @@ export class Datepicker extends Component { this.el.removeEventListener('keydown', this._handleInputKeydown); this.el.removeEventListener('change', this._handleInputChange); this.calendarEl.removeEventListener('click', this._handleCalendarClick); + if (this.options.isDateRange) { + this.endDateEl.removeEventListener('click', this._handleInputClick); + this.endDateEl.removeEventListener('keypress', this._handleInputKeydown); + this.endDateEl.removeEventListener('change', this._handleInputChange); + } } _handleInputClick = (e) => { // Prevents default browser datepicker modal rendering - if(this.el.type == 'date') { + if(e.type == 'date') { e.preventDefault() } + this.setDateFromInput(e.target as HTMLInputElement); this.open(); + this.gotoDate((e.target) === this.el ? this.date : this.endDate); } _handleInputKeydown = (e: KeyboardEvent) => { if (Utils.keys.ENTER.includes(e.key)) { e.preventDefault(); + this.setDateFromInput(e.target as HTMLInputElement); this.open(); } } @@ -970,13 +1079,20 @@ export class Datepicker extends Component { !target.classList.contains('is-empty') && !target.parentElement.classList.contains('is-disabled') ) { - this.setDate( - new Date( - e.target.getAttribute('data-year'), - e.target.getAttribute('data-month'), - e.target.getAttribute('data-day') - ) + const selectedDate = new Date( + e.target.getAttribute('data-year'), + e.target.getAttribute('data-month'), + e.target.getAttribute('data-day') ); + + if (!this.multiple) { + this._handleSingleDateCalendarClick(selectedDate); + } + + if (this.options.isDateRange) { + this._handleDateRangeCalendarClick(selectedDate); + } + if (this.options.autoClose) { this._finishSelection(); } @@ -990,12 +1106,35 @@ export class Datepicker extends Component { } } + _handleSingleDateCalendarClick = (date: Date) => { + this.setDate(date); + } + + _handleDateRangeCalendarClick = (date: Date) => { + if (this.endDate == null || !Datepicker._compareDates(date, this.endDate)) { + if (Datepicker._isDate(this.date) && Datepicker._comparePastDate(date, this.date)) { + return; + } + + this.setDate(date, false, Datepicker._isDate(this.date)); + return; + } + + this._clearDates(); + this.draw(); + } + _handleClearClick = () => { - this.date = null; - this.setInputValue(); + this._clearDates(); + this.setInputValues(); this.close(); } + _clearDates = () => { + this.date = null; + this.endDate = null; + } + _handleMonthChange = (e) => { this.gotoMonth(e.target.value); } @@ -1022,21 +1161,25 @@ export class Datepicker extends Component { _handleInputChange = (e: Event) => { let date; + const el = (e.target as HTMLElement); // Prevent change event from being fired when triggered by the plugin if (e['detail']?.firedBy === this) return; + // Prevent change event from being fired if an end date is set without a start date + if(el == this.endDateEl && !this.date) return; if (this.options.parse) { - date = this.options.parse(this.el.value, + date = this.options.parse((e.target as HTMLInputElement).value, typeof this.options.format === "function" ? this.options.format(new Date(this.el.value)) : this.options.format); } else { - date = new Date(Date.parse(this.el.value)); + date = new Date(Date.parse((e.target as HTMLInputElement).value)); } if (Datepicker._isDate(date)) { - this.setDate(date); - if (this.el.type == 'date') { - this.setDataDate(); + this.setDate(date, false, el == this.endDateEl); + if (e.type == 'date') { + this.setDataDate(e, date); + this.setInputValues(); } } } @@ -1051,7 +1194,7 @@ export class Datepicker extends Component { // Set input value to the selected date and close Datepicker _finishSelection = () => { - this.setInputValue(); + this.setInputValues(); this.close(); }