UNPKG

comindware.core.ui

Version:

Comindware Core UI provides the basic components like editors, lists, dropdowns, popups that we so desperately need while creating Marionette-based single-page applications.

713 lines (624 loc) • 24.5 kB
import template from './templates/dateTimeEditor.hbs'; import DateTimeService from './services/DateTimeService'; import BaseEditorView from './base/BaseEditorView'; import formRepository from '../formRepository'; import iconWrapRemove from './iconsWraps/iconWrapRemove.html'; import DatePanelView from './impl/dateTime/views/DatePanelView'; import DateInputView from './impl/dateTime/views/DateInputView'; import dropdown from 'dropdown'; import { dateHelpers, keyCode } from 'utils'; import GlobalEventService from '../../services/GlobalEventService'; import DurationEditorView from './DurationEditorView'; import { getClasses } from './impl/dateTime/meta'; import MobileService from 'services/MobileService'; const defaultOptions = () => ({ allowEmptyValue: true, dateDisplayFormat: dateHelpers.getFormat('dateISO'), timeDisplayFormat: undefined, showTitle: true, showDate: true, showTime: true, allFocusableParts: { maxLength: 2, text: ':' }, seconds: { text: '' } }); const defaultClasses = 'editor editor_date-time js-dropdown__root'; const clearButtonClassSelector = '.js-clear-button'; const editorTypes = { dateTime: 'dateTime', date: 'date', time: 'time' }; /** * @name DateTimeEditorView * @memberof module:core.form.editors * @class Combined date and time editor. Supported data type: <code>String</code> in ISO8601 format * (for example, '2015-07-20T10:46:37Z'). * @extends module:core.form.editors.base.BaseEditorView * @param {Object} options Options object. * All the properties of {@link module:core.form.editors.base.BaseEditorView BaseEditorView} class are also supported. * @param {Boolean} [options.allowEmptyValue=true] - Whether to display a delete button that sets the value to <code>null</code>. * E.g. for UTC+3 enter <code>180</code>. Negative values allowed. Defaults to browser timezone offset. * @param {String} [options.dateDisplayFormat=null] - A [MomentJS](http://momentjs.com/docs/#/displaying/format/) format string (e.g. 'M/D/YYYY' etc.). * @param {String} [options.timeDisplayFormat=null] - A [MomentJS](http://momentjs.com/docs/#/displaying/format/) format string (e.g. 'LTS' etc.). * @param {Boolean} {options.showTitle=true} Whether to show title attribute. * @param {Object} [allFocusableParts] Params for time editor's part. Like Duration Editor Options. * @param {Object} [days, minutes, hours, seconds] Params for time editor's part. Like Duration Editor Options. * */ export default formRepository.editors.DateTime = BaseEditorView.extend({ initialize(options = {}) { this.__applyOptions(options, defaultOptions); this.value = this.__adjustValue(this.value); this.editorType = this.options.showDate ? this.options.showTime ? editorTypes.dateTime : editorTypes.date : this.options.showTime ? editorTypes.time : console.warn('DateTimeEditor: showDate and showTime is false'); if (this.options.showDate !== false) { this.dateButtonModel = new Backbone.Model({ [this.key]: this.value == null ? '' : moment(this.value).format(this.options.dateDisplayFormat) }); this.listenTo(this.dateButtonModel, `change:${this.key}`, this.__onDateModelChange); } }, ui: { clearButton: clearButtonClassSelector, mobileInput: '.js-mobile-input' }, events() { const events = { 'click @ui.clearButton': '__onClearClickHandler', 'dblclick @ui.clearButton': '__onClearDblclick', mouseenter: '__onMouseenter' }; if (MobileService.isMobile) { const mobileEvents = { 'input @ui.mobileInput': '__onMobileInput', click: '__onClickMobileMode' }; Object.assign(events, mobileEvents); } return events; }, __onClickMobileMode(event, inner) { if (inner || !this.getEditable()) { return; } this.__openNativePicker(); }, regions: { timeDropdownRegion: { el: '.js-time-dropdown-region', replaceElement: true }, dateDropdownRegion: { el: '.js-date-dropdown-region', replaceElement: true } }, className() { this.displayClasses = getClasses(); return this.__getClassName(); }, template: Handlebars.compile(template), templateContext() { const mobileInputType = { [editorTypes.dateTime]: 'datetime-local', [editorTypes.date]: 'date', [editorTypes.time]: 'time' }; return _.defaultsPure( { isMobile: MobileService.isMobile, mobileInputType: mobileInputType[this.editorType], showSeparator: this.options.showTime && this.options.showDate }, this.options ); }, setValue(value: String, { updateButtons = true, triggerChange = false } = {}): void { this.__value(value, updateButtons, triggerChange); }, getValue(): string { return this.__adjustValue(this.value); }, onRender(): void { this.__updateClearButton(); this.__presentView(); }, onAttach() { if (MobileService.isMobile) { this.timeDropdownView?.ui.input[0].setAttribute('readonly', ''); this.calendarDropdownView?.ui.input[0].setAttribute('readonly', ''); this.__setValueToMobileInput(this.value); } }, __setValueToMobileInput(value) { const nativeFormats = { [editorTypes.dateTime]: 'YYYY-MM-DDTHH:mm', [editorTypes.date]: 'YYYY-MM-DD', [editorTypes.time]: 'HH:mm' }; this.ui.mobileInput[0].value = value ? moment(value).format(nativeFormats[this.editorType]) : null; }, focusElement: null, focus(): void { if (this.options.showDate) { this.calendarDropdownView?.focus(); } else if (this.options.showTime) { this.timeDropdownView?.focus(); } }, blur(): void { this.options.showDate && this.__dateBlur(); this.options.showTime && this.__timeBlur(); }, setFormat(newFormat) { if (typeof newFormat !== 'object' || newFormat === null) { return; } this.options.dateDisplayFormat = newFormat.dateDisplayFormat; this.options.timeDisplayFormat = newFormat.timeDisplayFormat; this.options.showDate = !!this.options.dateDisplayFormat; this.options.showTime = !!this.options.timeDisplayFormat; this.__presentView(); this.$editorEl.attr('class', this.__getClassName()); }, checkChange() { if (this.model) { if (this.__adjustValue(this.value) !== this.__adjustValue(this.model.get(this.key))) { this.__triggerChange(); } } }, __onMobileInput(event) { const input = event.target; const value = input.value; this.setValue(this.__mapNativeToValue(value), { updateButtons: true, triggerChange: true }); }, __mapNativeToValue(value) { if (!value) { return null; } if (this.options.showDate) { return moment(value).toISOString(); } else if (this.options.showTime) { const parsedTime = value.split(':'); const hour = parsedTime[0]; const minutes = parsedTime[1]; return moment(this.value || undefined) .hour(hour) .minutes(minutes) .seconds(0) .milliseconds(0) .toISOString(); } }, __dateButtonInputKeydown(buttonView, event) { switch (event.keyCode) { case keyCode.DELETE: case keyCode.BACKSPACE: this.calendarDropdownView.close(); return; case keyCode.F2: this.__toggleCalendarPicker(); return; case keyCode.ENTER: this.__onEnterValueSelect(event); return; case keyCode.TAB: // if tab go to time input(default browser behavior), // not trigger tab on parent grid. if (!event.shiftKey) { event.stopPropagation(); } return; default: break; } if (!this.calendarDropdownView.isOpen) { return; } let newValue; const oldValue = this.value === null ? undefined : this.value; switch (event.keyCode) { case keyCode.UP: newValue = event.shiftKey ? moment(oldValue).add(1, 'years') : moment(oldValue).subtract(1, 'weeks'); break; case keyCode.DOWN: newValue = event.shiftKey ? moment(oldValue).subtract(1, 'years') : moment(oldValue).add(1, 'weeks'); break; case keyCode.LEFT: newValue = event.shiftKey ? moment(oldValue).subtract(1, 'months') : moment(oldValue).subtract(1, 'days'); break; case keyCode.RIGHT: newValue = event.shiftKey ? moment(oldValue).add(1, 'months') : moment(oldValue).add(1, 'days'); break; default: return; } const amountMilliseconds = newValue.valueOf(); this.__value(amountMilliseconds, true, false); this.__updatePickerDate(amountMilliseconds); }, __timeButtonInputKeydown(event) { switch (event.keyCode) { case keyCode.TAB: // if tab go to date input(default browser behavior), // not trigger tab on parent grid. if (event.shiftKey) { event.stopPropagation(); } break; default: break; } }, __updatePickerDate(date) { if (this.calendarDropdownView.isOpen) { this.calendarDropdownView.panelView?.updatePickerDate(date); } }, __toggleCalendarPicker() { if (this.calendarDropdownView.isOpen) { this.calendarDropdownView.close(); } else { this.__openCalendarPicker(); } }, __updateClearButton(): void { if (!this.options.allowEmptyValue || !this.getValue()) { this.ui.clearButton.hide(); } else { this.ui.clearButton.show(); } }, __value(newValue, updateButtons, triggerChange): void { const value = this.__adjustValue(newValue); if (this.value === value) { return; } this.value = value; if (this.options.showTitle) { this.__updateTitle(); } if (updateButtons) { this.options.showDate && this.__setValueToDate(value); this.options.showTime && this.__setValueToTimeButton(value); } if (MobileService.isMobile) { this.__setValueToMobileInput(this.value); } if (triggerChange) { this.__triggerChange(); } }, __setEnabled(enabled: boolean): void { BaseEditorView.prototype.__setEnabled.call(this, enabled); this.enabled = this.getEnabled(); //__setEnabled() from descendants this.calendarDropdownView?.setEnabled(enabled); this.timeDropdownView?.setEnabled(enabled); }, __setReadonly(readonly: boolean): void { BaseEditorView.prototype.__setReadonly.call(this, readonly); this.readonly = this.getReadonly(); //__setReadonly() from descendants this.calendarDropdownView?.setReadonly(readonly); this.timeDropdownView?.setReadonly(readonly); }, __onClearClick() { if (this.__isDoubleClicked) { this.__isDoubleClicked = false; return; } this.__value(null, true, false); this.focus(); }, __onEnterValueSelect(event: KeyboardEvent) { if (this.calendarDropdownView.isOpen) { this.__triggerChange(); this.__toggleCalendarPicker(); event.stopPropagation(); } }, __adjustValue(value: string): string { return value == null ? null : moment(value).toISOString(); }, __updateTitle(): void { const dateDisplayValue = DateTimeService.getDateDisplayValue(this.getValue(), this.options.dateDisplayFormat); const timeDisplayValue = DateTimeService.getTimeDisplayValue(this.getValue(), this.options.timeDisplayFormat); const resultValue = `${dateDisplayValue} ${timeDisplayValue}`; this.$editorEl.prop('title', resultValue); }, __createDateDropdownEditor() { this.calendarDropdownView = dropdown.factory.createDropdown({ buttonView: DateInputView, buttonViewOptions: { model: this.dateButtonModel, key: this.key, autocommit: true, readonly: this.options.readonly, allowEmptyValue: this.options.allowEmptyValue, dateDisplayFormat: this.options.dateDisplayFormat, emptyPlaceholder: Localizer.get('CORE.FORM.EDITORS.DATE.EMPTYPLACEHOLDER'), changeMode: 'blur', showTitle: false, hideClearButton: true }, panelView: DatePanelView, panelViewOptions: { value: this.value, allowEmptyValue: this.options.allowEmptyValue, startDate: this.options.startDate, endDate: this.options.endDate, datesDisabled: this.options.datesDisabled, daysOfWeekDisabled: this.options.daysOfWeekDisabled, calendar: this.options.calendar }, attributes: { tabindex: 0 }, renderAfterClose: false, autoOpen: false, popoutFlow: 'right', panelMinWidth: 'none', class: 'editor_date-time_date', externalBlurHandler: (target: Element) => this.__checkBlur(target) }); this.showChildView('dateDropdownRegion', this.calendarDropdownView); this.listenTo(this.calendarDropdownView, 'focus', this.__onDateButtonFocus); this.listenTo(this.calendarDropdownView, 'blur', this.__onEditorBlur); this.listenTo(this.calendarDropdownView, 'panel:select', this.__onPanelDateChange); this.listenTo(this.calendarDropdownView, 'keydown', this.__dateButtonInputKeydown); }, __onEditorBlur(view: Marionette.View<any>, event: FocusEvent) { if (view.panelView?.el.contains(event.relatedTarget)) { return; } this.onBlur(); }, __updateDateAndValidateToButton(recipientISO, fromFormatted, { includeTime = false }) { const recipientMoment = moment(recipientISO || {}); const fromMoment = DateTimeService.tryGetValidMoment(fromFormatted, this.options.dateDisplayFormat); if (fromMoment) { recipientMoment.year(fromMoment.year()); recipientMoment.month(fromMoment.month()); recipientMoment.date(fromMoment.date()); if (includeTime) { recipientMoment.milliseconds(fromMoment.milliseconds()); recipientMoment.seconds(fromMoment.seconds()); recipientMoment.minutes(fromMoment.minutes()); recipientMoment.hours(fromMoment.hours()); } else if (recipientISO == null) { recipientMoment.milliseconds(0); recipientMoment.seconds(0); recipientMoment.minutes(0); recipientMoment.hours(0); } this.dateButtonModel.set(this.key, recipientMoment.format(this.options.dateDisplayFormat), { inner: true }); } else { this.dateButtonModel.set(this.key, Localizer.get('CORE.FORM.EDITORS.DATE.INVALIDDATE'), { inner: true }); } return recipientMoment.toISOString(); }, __onDateModelChange(model, formattedDate, options) { if (options.inner) { return; } this.__value(this.__updateDateAndValidateToButton(this.value, formattedDate, { includeTime: false }), false, true); }, __setValueToDate(value) { this.dateButtonModel.set(this.key, value == null ? value : moment(value).format(this.options.dateDisplayFormat), { inner: true }); }, __onPanelDateChange(jsDate) { this.__value(this.__updateDateAndValidateToButton(this.value, jsDate, { includeTime: false }), true, true); this.stopListening(GlobalEventService); this.calendarDropdownView?.focus(); this.calendarDropdownView?.close(); }, __onDateButtonFocus() { this.__openCalendarPicker(); this.onFocus(); }, __openCalendarPicker() { if (!this.getEditable()) { return; } if (MobileService.isMobile) { this.__openNativePicker(); } else { this.calendarDropdownView?.open(); this.__updatePickerDate(this.value); this.calendarDropdownView?.adjustPosition(); } }, __openTimePicker() { if (!this.getEditable()) { return; } if (MobileService.isMobile) { this.__openNativePicker(); } else { this.__timeDropdownOpen(); } }, __openNativePicker() { this.ui.mobileInput.trigger('click', true); }, __timeDropdownOpen() { this.timeDropdownView?.open(); const panelView = this.timeDropdownView?.panelView; if (!panelView?.collection.length) { panelView.collection.reset(this.__getTimeCollection()); } }, __getTimeCollection() { const timeArray = []; for (let h = 0; h < 24; h++) { for (let m = 0; m < 60; m += 15) { const val = { hours: h, minutes: m }; const time = moment(val); const formattedTime = dateHelpers.getDisplayTime(time); timeArray.push({ time, formattedTime }); } } return timeArray; }, __dateBlur() { this.calendarDropdownView?.close(); this.calendarDropdownView?.blur(); this.onBlur(); }, __timeBlur() { this.timeDropdownView?.close(); this.timeDropdownView?.blur(); this.onBlur(); }, __createTimeDropdownView() { const model = new Backbone.Model({ [this.key]: this.value == null ? '' : dateHelpers.dateISOToDuration(this.value, { days: false }).toISOString() }); this.listenTo(model, 'change', this.__onTimeModelChange); const isFormatHasSeconds = dateHelpers.isFormatHasSeconds(this.options.timeDisplayFormat); this.timeDropdownView = dropdown.factory.createDropdown({ buttonView: DurationEditorView, buttonViewOptions: _.defaultsPure({ allowDays: false, allowHours: true, allowMinutes: true, allowSeconds: isFormatHasSeconds, allFocusableParts: this.options.allFocusableParts, seconds: this.options.seconds, minutes: { text: this.options.minutes ? this.options.minutes.text : isFormatHasSeconds ? ':' : '' }, showEmptyParts: true, hideClearButton: true, fillZero: true, normalTime: true, showTitle: false, autocommit: true, readonly: this.getReadonly(), editable: this.getEditable(), emptyPlaceholder: Localizer.get('CORE.FORM.EDITORS.DATE.TIMEEMPTYPLACEHOLDER'), key: this.options.key, autocommit: true, readonly: this.getReadonly(), editable: this.getEditable(), model }), panelView: Marionette.CollectionView.extend({ collection: new Backbone.Collection(), tagName: 'ul', className: 'dropdown__wrp dropdown__wrp_time', childViewEvents: { select(time) { this.trigger('select', time); } }, childView: Marionette.View.extend({ tagName: 'li', className: 'time-dropdown__i', events: { click() { this.trigger('select', this.model.get('time')); } }, template: Handlebars.compile('{{formattedTime}}') }) }), attributes: { tabindex: 0 }, renderAfterClose: false, autoOpen: false, alwaysAlignByButton: true, class: 'editor_date-time_time', externalBlurHandler: (target: Element) => this.__checkBlur(target) }); this.listenTo(this.timeDropdownView, 'focus', this.__onTimeButtonFocus); this.listenTo(this.timeDropdownView, 'blur', this.__onEditorBlur); this.listenTo(this.timeDropdownView, 'container:click', this.__onTimeButtonFocus); this.listenTo(this.timeDropdownView, 'panel:select', this.__onTimePanelSelect); this.showChildView('timeDropdownRegion', this.timeDropdownView); this.listenTo(this.timeDropdownView, 'keydown', this.__timeButtonInputKeydown); }, __updateTime(ISOstr) { //replace time of ISO string to time from timebutton const valTimeModel = this.timeDropdownView && this.timeDropdownView.model.get(this.key); if (!valTimeModel) { return; } const dateMoment = moment(ISOstr || {}).clone(); const timeDuration = moment.duration(valTimeModel).clone(); dateMoment.hours(timeDuration.hours()); dateMoment.minutes(timeDuration.minutes()); dateMoment.seconds(timeDuration.seconds()); dateMoment.milliseconds(timeDuration.milliseconds()); return dateMoment.toISOString(); }, __onTimeModelChange(model) { if (model.get(this.key) === null) { return; } this.__value(this.__updateTime(this.value), false, true); }, __setValueToTimeButton(dateISOstring) { const newDuration = dateISOstring && dateHelpers.dateISOToDuration(dateISOstring, { days: false }).toISOString(); this.timeDropdownView && this.timeDropdownView.setValue(newDuration, true); }, __onTimePanelSelect(time) { this.__setValueToTimeButton(time); this.timeDropdownView.focus(); this.timeDropdownView.close(); }, __onTimeButtonFocus() { this.__openTimePicker(); this.onFocus(); }, __onMouseenter() { this.$editorEl.off('mouseenter'); if (!MobileService.isMobile && !this.options.hideClearButton) { this.renderIcons(iconWrapRemove); } }, __presentView() { if (this.options.showTitle) { this.__updateTitle(); } if (this.options.showTime !== false) { this.__createTimeDropdownView(); } else { this.getRegion('timeDropdownRegion').reset(); } if (this.options.showDate !== false) { this.__createDateDropdownEditor(); } else { this.getRegion('dateDropdownRegion').reset(); } }, __getClassName() { return `${defaultClasses} ${this.displayClasses.dateMapClasses[this.options.dateDisplayFormat] || ''} ${this.displayClasses.timeMapClasses[ this.options.timeDisplayFormat ] || ''}`; }, __checkBlur(target: Element) { return target === this.el.querySelector(clearButtonClassSelector); } });