diff --git a/packages/ilios-common/.lint-todo b/packages/ilios-common/.lint-todo index 7c494831a2..2aceee0c5d 100644 --- a/packages/ilios-common/.lint-todo +++ b/packages/ilios-common/.lint-todo @@ -1,5 +1,3 @@ -add|ember-template-lint|no-at-ember-render-modifiers|5|2|5|2|fb8a149d14413d4dfc84ffd31349ef3f2ac6d17b|1731542400000|1762646400000|1793750400000|addon/components/date-picker.hbs -add|ember-template-lint|no-at-ember-render-modifiers|6|2|6|2|993f1e23f796f19a221eae6e24872755e0436cb4|1731542400000|1762646400000|1793750400000|addon/components/date-picker.hbs add|ember-template-lint|no-at-ember-render-modifiers|6|8|6|8|c628ee621a6e921e369bf6bcb158a5ef932e6741|1731542400000|1762646400000|1793750400000|addon/components/editable-field.hbs add|ember-template-lint|no-at-ember-render-modifiers|2|2|2|2|ad17d66e0fe1720bc8ddedc12dff3a105709765c|1731542400000|1762646400000|1793750400000|addon/components/html-editor.hbs add|ember-template-lint|no-at-ember-render-modifiers|3|2|3|2|d39abab22a3e75d93f69335da422e7ef73b36283|1731542400000|1762646400000|1793750400000|addon/components/html-editor.hbs diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index 7230d46242..d88b4dbe4f 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,7 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{did-insert (perform this.setupPicker)}} - {{did-update (perform this.updatePicker) @value @maxDate @minDate}} + {{date-picker @value minDate=@minDate maxDate=@maxDate locale=this.intl.primaryLocale onChangeHandler=@onChange}} ...attributes -/> +/> \ No newline at end of file diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index cc63ff7f76..780d997abf 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -1,72 +1,6 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import { dropTask, restartableTask, waitForProperty } from 'ember-concurrency'; -import { tracked } from '@glimmer/tracking'; -import flatpickr from 'flatpickr'; -import { later, next } from '@ember/runloop'; -import { isTesting } from '@embroider/macros'; export default class DatePickerComponent extends Component { @service intl; - - @tracked _flatPickerInstance; - @tracked isOpen = false; - - updatePicker = restartableTask(async (element, [value]) => { - await waitForProperty(this, '_flatPickerInstance'); - if (this._flatPickerInstance.selectedDates[0] != value) { - this._flatPickerInstance.setDate(value); - } - }); - - setupPicker = dropTask(async (element) => { - const currentLocale = this.intl.primaryLocale; - let locale; - switch (currentLocale) { - case 'fr': - // eslint-disable-next-line no-case-declarations - const { French } = await import('flatpickr/dist/l10n/fr.js'); - locale = French; - break; - case 'es': - // eslint-disable-next-line no-case-declarations - const { Spanish } = await import('flatpickr/dist/l10n/es.js'); - locale = Spanish; - break; - default: - locale = 'en'; - } - this._flatPickerInstance = flatpickr(element, { - locale, - defaultDate: this.args.value, - formatDate: (dateObj) => - this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), - onChange: (selectedDates) => this.onChange(selectedDates[0]), - onOpen: () => { - // eslint-disable-next-line ember/no-runloop - later(() => { - this.isOpen = true; - }, 250); - }, - onClose: () => { - this.isOpen = false; - }, - maxDate: this.args.maxDate ?? null, - minDate: this.args.minDate ?? null, - disableMobile: isTesting(), - }); - }); - - willDestroy() { - super.willDestroy(...arguments); - if (this._flatPickerInstance) { - this._flatPickerInstance.destroy(); - } - } - - async onChange(date) { - await this.args.onChange(date); - // eslint-disable-next-line ember/no-runloop - await next(() => {}); - } } diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js new file mode 100644 index 0000000000..13d9345fae --- /dev/null +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -0,0 +1,83 @@ +import Modifier from 'ember-modifier'; +import { registerDestructor } from '@ember/destroyable'; +import flatpickr from 'flatpickr'; +import { French } from 'flatpickr/dist/l10n/fr.js'; +import { Spanish } from 'flatpickr/dist/l10n/es.js'; +import { isTesting } from '@embroider/macros'; +import { service } from '@ember/service'; + +export default class DatePickerModifier extends Modifier { + @service intl; + flatpickr = null; + locale = null; + onChangeHandler = null; + + constructor(owner, args) { + super(owner, args); + registerDestructor(this, () => { + this.locale = null; + this.onChangeHandler = null; + if (this.flatpickr) { + this.flatpickr.destroy(); + this.flatpickr = null; + } + }); + } + + modify(element, [value], { minDate, maxDate, locale, onChangeHandler }) { + // We only need to set this once. + if (!this.onChangeHandler) { + this.onChangeHandler = onChangeHandler; + } + if (!this.flatpickr) { + this.locale = locale ?? this.intl.primaryLocale; + this.flatpickr = this.initPicker(element, value, minDate, maxDate, this.locale); + } + + if (this.flatpickr.selectedDates[0] !== value) { + this.flatpickr.setDate(value); + } + if (this.flatpickr.minDate !== minDate) { + this.flatpickr.set('minDate', minDate); + } + if (this.flatpickr.maxDate !== maxDate) { + this.flatpickr.set('maxDate', maxDate); + } + + if (locale && this.locale !== locale) { + this.locale = locale; + this.flatpickr.set('locale', this.getFlatpickrLocale(locale)); + } + } + + // @see https://flatpickr.js.org/localization/ + getFlatpickrLocale(localeIdentifier) { + switch (localeIdentifier) { + case 'fr': + return French; + case 'es': + return Spanish; + default: + return 'en'; + } + } + + initPicker(element, value, minDate, maxDate, locale) { + return flatpickr(element, { + locale: this.getFlatpickrLocale(locale), + defaultDate: value, + formatDate: (dateObj) => + this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), + onChange: (selectedDates) => this.onChange(selectedDates[0]), + maxDate: maxDate ?? null, + minDate: minDate ?? null, + disableMobile: isTesting(), + }); + } + + async onChange(date) { + if (this.onChangeHandler) { + await this.onChangeHandler(date); + } + } +} diff --git a/packages/ilios-common/app/modifiers/date-picker.js b/packages/ilios-common/app/modifiers/date-picker.js new file mode 100644 index 0000000000..663824728e --- /dev/null +++ b/packages/ilios-common/app/modifiers/date-picker.js @@ -0,0 +1 @@ +export { default } from 'ilios-common/modifiers/date-picker'; diff --git a/packages/test-app/tests/integration/modifiers/date-picker-test.js b/packages/test-app/tests/integration/modifiers/date-picker-test.js new file mode 100644 index 0000000000..cdf50f01fe --- /dev/null +++ b/packages/test-app/tests/integration/modifiers/date-picker-test.js @@ -0,0 +1,111 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'test-app/tests/helpers'; +import { render, find } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Modifier | date-picker', function (hooks) { + setupRenderingTest(hooks); + + test('it works with minimal input', async function (assert) { + const value = new Date('2014-03-02'); + this.set('value', value); + await render(hbs`
`); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + assert.notOk(flatpickr.config.minDate); + assert.notOk(flatpickr.config.maxDate); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('January'); + }); + + test('it works with minDate, maxDate, and locale as input', async function (assert) { + const value = new Date('2014-03-02'); + const minDate = new Date('2014-01-12'); + const maxDate = new Date('2014-05-11'); + const locale = 'es'; + this.set('value', value); + this.set('minDate', minDate); + this.set('maxDate', maxDate); + this.set('locale', locale); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + assert.strictEqual(minDate.getTime(), flatpickr.config.minDate.getTime()); + assert.strictEqual(maxDate.getTime(), flatpickr.config.maxDate.getTime()); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('Enero'); + }); + + test('changing value is responsive', async function (assert) { + const value = new Date('2014-03-02'); + this.set('value', value); + await render(hbs`
`); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + const newValue = new Date('2024-03-03'); + this.set('value', newValue); + assert.strictEqual(newValue.getTime(), flatpickr.selectedDates[0].getTime()); + }); + + test('changing minDate is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const minDate = new Date('2014-01-12'); + this.set('value', value); + this.set('minDate', minDate); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(minDate.getTime(), flatpickr.config.minDate.getTime()); + const newMinDate = new Date('2024-03-03'); + this.set('minDate', newMinDate); + assert.strictEqual(newMinDate.getTime(), flatpickr.config.minDate.getTime()); + }); + + test('changing maxDate is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const maxDate = new Date('2014-01-12'); + this.set('value', value); + this.set('maxDate', maxDate); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(maxDate.getTime(), flatpickr.config.maxDate.getTime()); + const newMaxDate = new Date('2024-03-03'); + this.set('maxDate', newMaxDate); + assert.strictEqual(newMaxDate.getTime(), flatpickr.config.maxDate.getTime()); + }); + + test('changing locale is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const locale = 'es'; + this.set('value', value); + this.set('locale', locale); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('Enero'); + this.set('locale', 'fr'); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('janvier'); + }); + + test('onChange callback fires', async function (assert) { + assert.expect(1); + const value = new Date('2014-03-02'); + const newValue = new Date('2022-06-23'); + this.set('value', value); + this.set('onChange', (date) => { + assert.strictEqual(newValue.getTime(), date.getTime()); + }); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + flatpickr.setDate(newValue, true); + }); +});