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
text/typescript
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);
}
});