2017-08-22 10:53:26 +03:00
|
|
|
import Component from '@ember/component';
|
2022-02-01 12:34:03 +03:00
|
|
|
import classic from 'ember-classic-decorator';
|
2022-09-23 20:15:08 +03:00
|
|
|
import moment from 'moment-timezone';
|
2020-01-10 17:25:59 +03:00
|
|
|
import {action, computed} from '@ember/object';
|
2017-08-22 10:53:26 +03:00
|
|
|
import {isBlank, isEmpty} from '@ember/utils';
|
|
|
|
import {or, reads} from '@ember/object/computed';
|
2017-10-30 12:38:01 +03:00
|
|
|
import {inject as service} from '@ember/service';
|
2022-02-01 12:34:03 +03:00
|
|
|
import {tagName} from '@ember-decorators/component';
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2020-01-10 17:25:59 +03:00
|
|
|
const DATE_FORMAT = 'YYYY-MM-DD';
|
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@classic
|
|
|
|
@tagName('')
|
|
|
|
export default class GhDateTimePicker extends Component {
|
2022-02-01 20:03:45 +03:00
|
|
|
@service settings;
|
2022-02-01 12:34:03 +03:00
|
|
|
|
|
|
|
date = '';
|
|
|
|
dateFormat = DATE_FORMAT;
|
|
|
|
time = '';
|
|
|
|
errors = null;
|
|
|
|
dateErrorProperty = null;
|
|
|
|
timeErrorProperty = null;
|
|
|
|
isActive = true;
|
|
|
|
_time = '';
|
2022-10-07 13:20:06 +03:00
|
|
|
// _date is always a moment object in the blog's timezone
|
2022-02-01 12:34:03 +03:00
|
|
|
_previousTime = '';
|
2022-10-07 13:20:06 +03:00
|
|
|
_minDate = null; // Always set to a Date object
|
|
|
|
_maxDate = null; // Always set to a Date object
|
2022-02-01 12:34:03 +03:00
|
|
|
_scratchDate = null;
|
|
|
|
_scratchDateError = null;
|
2020-01-10 17:25:59 +03:00
|
|
|
|
|
|
|
// actions
|
2022-02-01 12:34:03 +03:00
|
|
|
setTypedDateError() {}
|
|
|
|
|
2022-04-27 20:20:46 +03:00
|
|
|
get renderInPlaceWithFallback() {
|
|
|
|
return this.renderInPlace === undefined ? true : this.renderInPlace;
|
|
|
|
}
|
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@reads('settings.timezone')
|
2022-02-10 13:41:36 +03:00
|
|
|
blogTimezone;
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@or('dateError', 'timeError')
|
2022-02-10 13:41:36 +03:00
|
|
|
hasError;
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@computed('_date', '_scratchDate')
|
|
|
|
get dateValue() {
|
2020-01-10 17:25:59 +03:00
|
|
|
if (this._scratchDate !== null) {
|
|
|
|
return this._scratchDate;
|
|
|
|
} else {
|
2022-10-07 13:20:06 +03:00
|
|
|
return this._date?.format(DATE_FORMAT);
|
2020-01-10 17:25:59 +03:00
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2022-10-07 13:20:06 +03:00
|
|
|
|
|
|
|
@computed('_date')
|
|
|
|
get localDateValue() {
|
|
|
|
// Convert the selected date to a new date in the local timezone, purely to please PowerDatepicker
|
|
|
|
return new Date(this._date.format(DATE_FORMAT));
|
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@computed('blogTimezone')
|
|
|
|
get timezone() {
|
2019-03-06 16:53:54 +03:00
|
|
|
let blogTimezone = this.blogTimezone;
|
2017-04-11 16:39:45 +03:00
|
|
|
return moment.utc().tz(blogTimezone).format('z');
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@computed('errors.[]', 'dateErrorProperty', '_scratchDateError')
|
|
|
|
get dateError() {
|
2020-01-10 17:25:59 +03:00
|
|
|
if (this._scratchDateError) {
|
|
|
|
return this._scratchDateError;
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
let errors = this.errors;
|
|
|
|
let property = this.dateErrorProperty;
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2019-02-22 06:17:33 +03:00
|
|
|
if (errors && !isEmpty(errors.errorsFor(property))) {
|
2017-04-11 16:39:45 +03:00
|
|
|
return errors.errorsFor(property).get('firstObject').message;
|
|
|
|
}
|
2019-06-24 18:33:21 +03:00
|
|
|
|
|
|
|
return '';
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@computed('errors.[]', 'timeErrorProperty')
|
|
|
|
get timeError() {
|
2019-03-06 16:53:54 +03:00
|
|
|
let errors = this.errors;
|
|
|
|
let property = this.timeErrorProperty;
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2019-02-22 06:17:33 +03:00
|
|
|
if (errors && !isEmpty(errors.errorsFor(property))) {
|
2017-04-11 16:39:45 +03:00
|
|
|
return errors.errorsFor(property).get('firstObject').message;
|
|
|
|
}
|
2019-06-24 18:33:21 +03:00
|
|
|
|
|
|
|
return '';
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2017-04-11 16:39:45 +03:00
|
|
|
|
|
|
|
didReceiveAttrs() {
|
2022-02-01 12:34:03 +03:00
|
|
|
super.didReceiveAttrs(...arguments);
|
2021-07-15 17:27:29 +03:00
|
|
|
|
2019-03-06 16:53:54 +03:00
|
|
|
let date = this.date;
|
|
|
|
let time = this.time;
|
|
|
|
let minDate = this.minDate;
|
|
|
|
let maxDate = this.maxDate;
|
|
|
|
let blogTimezone = this.blogTimezone;
|
2017-04-11 16:39:45 +03:00
|
|
|
|
|
|
|
if (!isBlank(date)) {
|
2022-10-07 13:20:06 +03:00
|
|
|
// Note: input date as a string is expected to be in the blog's timezone
|
|
|
|
this.set('_date', moment.tz(date, blogTimezone));
|
2017-04-11 16:39:45 +03:00
|
|
|
} else {
|
|
|
|
this.set('_date', moment().tz(blogTimezone));
|
|
|
|
}
|
|
|
|
|
2020-01-10 17:25:59 +03:00
|
|
|
// reset scratch date if the component becomes inactive
|
|
|
|
// (eg, PSM is closed, or save type is changed away from scheduled)
|
|
|
|
if (!this.isActive && this._lastIsActive) {
|
|
|
|
this._resetScratchDate();
|
|
|
|
}
|
|
|
|
this._lastIsActive = this.isActive;
|
|
|
|
|
|
|
|
// reset scratch date if date is changed externally
|
|
|
|
if ((date && date.valueOf()) !== (this._lastDate && this._lastDate.valueOf())) {
|
|
|
|
this._resetScratchDate();
|
|
|
|
}
|
|
|
|
this._lastDate = this.date;
|
|
|
|
|
2017-04-11 16:39:45 +03:00
|
|
|
if (isBlank(time)) {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.set('_time', this._date.format('HH:mm'));
|
2017-04-11 16:39:45 +03:00
|
|
|
} else {
|
2019-03-06 16:53:54 +03:00
|
|
|
this.set('_time', this.time);
|
2017-04-11 16:39:45 +03:00
|
|
|
}
|
2019-03-06 16:53:54 +03:00
|
|
|
this.set('_previousTime', this._time);
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-07-01 13:20:02 +03:00
|
|
|
// unless min/max date is at midnight moment will disable that day
|
2017-04-11 16:39:45 +03:00
|
|
|
if (minDate === 'now') {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.set('_minDate', moment(moment().tz(blogTimezone).format(DATE_FORMAT)).toDate());
|
2017-04-11 16:39:45 +03:00
|
|
|
} else if (!isBlank(minDate)) {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.set('_minDate', moment(moment.tz(minDate, blogTimezone).format(DATE_FORMAT)).toDate());
|
2017-04-11 16:39:45 +03:00
|
|
|
} else {
|
|
|
|
this.set('_minDate', null);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maxDate === 'now') {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.set('_maxDate', moment(moment().tz(blogTimezone).format(DATE_FORMAT)).toDate());
|
2017-04-11 16:39:45 +03:00
|
|
|
} else if (!isBlank(maxDate)) {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.set('_maxDate', moment(moment.tz(maxDate, blogTimezone).format(DATE_FORMAT)).toDate());
|
2017-04-11 16:39:45 +03:00
|
|
|
} else {
|
|
|
|
this.set('_maxDate', null);
|
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2020-01-10 17:25:59 +03:00
|
|
|
willDestroyElement() {
|
2022-02-01 12:34:03 +03:00
|
|
|
super.willDestroyElement(...arguments);
|
2020-01-10 17:25:59 +03:00
|
|
|
this.setTypedDateError(null);
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// if date or time is set and the other property is blank set that too
|
|
|
|
// so that we don't get "can't be blank" errors
|
|
|
|
@action
|
|
|
|
setDateInternal(date) {
|
|
|
|
if (date !== this._date) {
|
|
|
|
this.setDate(date);
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
if (isBlank(this.time)) {
|
|
|
|
this.setTime(this._time);
|
2017-11-29 21:01:04 +03:00
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-07 13:20:06 +03:00
|
|
|
/**
|
|
|
|
* This method is called by `PowerDatepicker` when a user selected a date. It is constructed like
|
|
|
|
* The difference here is that the Date object that is passed contains the date, but only when viewed in the local timezone.
|
|
|
|
* This timezone can differ between the timezone of the blog. We need to convert the date to a new date in the blog's timezone on the same day that was selected.
|
|
|
|
* Example: new Date('2000-01-01') -> a user selected 2000-01-01. In the blog timezone, this could be 1999-12-31 23:00, which is wrong.
|
|
|
|
*/
|
|
|
|
@action
|
|
|
|
setLocalDate(date) {
|
|
|
|
// Convert to a date string in the local timezone (moment is in local timezone by default)
|
|
|
|
const dateString = moment(date).format(DATE_FORMAT);
|
|
|
|
this._setDate(dateString);
|
|
|
|
}
|
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@action
|
2022-05-12 16:23:48 +03:00
|
|
|
setTimeInternal(time, event) {
|
2022-02-01 12:34:03 +03:00
|
|
|
if (time.match(/^\d:\d\d$/)) {
|
|
|
|
time = `0${time}`;
|
|
|
|
}
|
2017-11-29 21:01:04 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
if (time !== this._previousTime) {
|
2022-05-12 16:23:48 +03:00
|
|
|
this.setTime(time, event);
|
2022-02-01 12:34:03 +03:00
|
|
|
this.set('_previousTime', time);
|
2017-04-11 16:39:45 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
if (isBlank(this.date)) {
|
2022-10-07 13:20:06 +03:00
|
|
|
this.setDate(this._date.toDate());
|
2017-04-11 16:39:45 +03:00
|
|
|
}
|
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
@action
|
|
|
|
updateTimeValue(event) {
|
|
|
|
this.set('_time', event.target.value);
|
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@action
|
|
|
|
registerTimeInput(elem) {
|
2020-01-10 17:25:59 +03:00
|
|
|
this._timeInputElem = elem;
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@action
|
|
|
|
onDateInput(datepicker, event) {
|
2020-01-15 16:53:51 +03:00
|
|
|
let skipFocus = true;
|
|
|
|
datepicker.actions.close(event, skipFocus);
|
2020-01-10 17:25:59 +03:00
|
|
|
this.set('_scratchDate', event.target.value);
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@action
|
|
|
|
onDateBlur(event) {
|
2020-01-10 17:25:59 +03:00
|
|
|
// make sure we're not doing anything just because the calendar dropdown
|
|
|
|
// is opened and clicked
|
2022-10-07 13:20:06 +03:00
|
|
|
if (event.target.value === this._date.format('YYYY-MM-DD')) {
|
2020-01-10 17:25:59 +03:00
|
|
|
this._resetScratchDate();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!event.target.value) {
|
|
|
|
this._resetScratchDate();
|
|
|
|
} else {
|
|
|
|
this._setDate(event.target.value);
|
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
@action
|
|
|
|
onDateKeydown(datepicker, event) {
|
2020-01-10 17:25:59 +03:00
|
|
|
if (event.key === 'Escape') {
|
|
|
|
this._resetScratchDate();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'Enter') {
|
|
|
|
this._setDate(event.target.value);
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
datepicker.actions.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
// close the dropdown and manually focus the time input if necessary
|
|
|
|
// so that keyboard focus behaves as expected
|
|
|
|
if (event.key === 'Tab' && datepicker.isOpen) {
|
|
|
|
datepicker.actions.close();
|
|
|
|
|
|
|
|
// manual focus is required because the dropdown is rendered in place
|
|
|
|
// and the default browser behaviour will move focus to the dropdown
|
|
|
|
// which is then removed from the DOM making it look like focus has
|
|
|
|
// disappeared. Shift+Tab is fine because the DOM is not changing in
|
|
|
|
// that direction
|
|
|
|
if (!event.shiftKey && this._timeInputElem) {
|
|
|
|
event.preventDefault();
|
|
|
|
this._timeInputElem.focus();
|
|
|
|
this._timeInputElem.select();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// capture a Ctrl/Cmd+S combo to make sure that the model value is updated
|
|
|
|
// before the save occurs or we abort the save if the value is invalid
|
|
|
|
if (event.key === 's' && (event.ctrlKey || event.metaKey)) {
|
|
|
|
let wasValid = this._setDate(event.target.value);
|
|
|
|
if (!wasValid) {
|
|
|
|
event.stopImmediatePropagation();
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
|
|
|
// internal methods
|
|
|
|
|
|
|
|
_resetScratchDate() {
|
|
|
|
this.set('_scratchDate', null);
|
|
|
|
this._setScratchDateError(null);
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
|
|
|
_setDate(dateStr) {
|
|
|
|
if (!dateStr.match(/^\d\d\d\d-\d\d-\d\d$/)) {
|
|
|
|
this._setScratchDateError('Invalid date format, must be YYYY-MM-DD');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-10-07 13:20:06 +03:00
|
|
|
let date = moment.tz(dateStr, DATE_FORMAT, this.blogTimezone);
|
2020-01-10 17:25:59 +03:00
|
|
|
if (!date.isValid()) {
|
|
|
|
this._setScratchDateError('Invalid date');
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-02-01 12:34:03 +03:00
|
|
|
this.setDateInternal(date.toDate());
|
2020-01-10 17:25:59 +03:00
|
|
|
this._resetScratchDate();
|
|
|
|
return true;
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|
2020-01-10 17:25:59 +03:00
|
|
|
|
|
|
|
_setScratchDateError(error) {
|
|
|
|
this.set('_scratchDateError', error);
|
|
|
|
this.setTypedDateError(error);
|
2017-04-11 16:39:45 +03:00
|
|
|
}
|
2022-02-01 12:34:03 +03:00
|
|
|
}
|