The implementation previously generated a descriptor with an `initializer()`, and bound the function to the `this` context of the initializer. In native class syntax, the initializer of a descriptor is only called once, with a `this` context of the constructor, not the instance. This commit updates the implementation so that it generates the bound function on-demand using a getter. This is the same strategy employed by ember's built-in `@action` decorator. Unfortunately, this use of a getter means that the `@observes` decorator does not support being directly chained to `@debounce`. It throws the error "`observer must be provided a function or an observer definition`". The workaround is to put the observer on its own function, which then calls the debounced function. Given that we're aiming to reduce our usage of `@observes`, we've accepted the need for this workaround rather than spending the time to patch the implementation of `@observes`.
434 lines
10 KiB
JavaScript
434 lines
10 KiB
JavaScript
/* global Pikaday:true */
|
|
import computed, {
|
|
debounce,
|
|
observes,
|
|
} from "discourse-common/utils/decorators";
|
|
import Component from "@ember/component";
|
|
import EmberObject, { action } from "@ember/object";
|
|
import I18n from "I18n";
|
|
import { INPUT_DELAY } from "discourse-common/config/environment";
|
|
import { Promise } from "rsvp";
|
|
import { cookAsync } from "discourse/lib/text";
|
|
import { isEmpty } from "@ember/utils";
|
|
import loadScript from "discourse/lib/load-script";
|
|
import { notEmpty } from "@ember/object/computed";
|
|
import { propertyNotEqual } from "discourse/lib/computed";
|
|
import { schedule } from "@ember/runloop";
|
|
import { getOwner } from "discourse-common/lib/get-owner";
|
|
import { applyLocalDates } from "discourse/lib/local-dates";
|
|
import generateDateMarkup from "discourse/plugins/discourse-local-dates/lib/local-date-markup-generator";
|
|
|
|
export default Component.extend({
|
|
timeFormat: "HH:mm:ss",
|
|
dateFormat: "YYYY-MM-DD",
|
|
dateTimeFormat: "YYYY-MM-DD HH:mm:ss",
|
|
date: null,
|
|
toDate: null,
|
|
time: null,
|
|
toTime: null,
|
|
format: null,
|
|
formats: null,
|
|
recurring: null,
|
|
advancedMode: false,
|
|
timezone: null,
|
|
fromSelected: null,
|
|
fromFilled: notEmpty("date"),
|
|
toSelected: null,
|
|
toFilled: notEmpty("toDate"),
|
|
|
|
init() {
|
|
this._super(...arguments);
|
|
|
|
this._picker = null;
|
|
|
|
this.setProperties({
|
|
timezones: [],
|
|
formats: (this.siteSettings.discourse_local_dates_default_formats || "")
|
|
.split("|")
|
|
.filter((f) => f),
|
|
timezone: moment.tz.guess(),
|
|
date: moment().format(this.dateFormat),
|
|
});
|
|
},
|
|
|
|
didInsertElement() {
|
|
this._super(...arguments);
|
|
|
|
this._setupPicker().then((picker) => {
|
|
this._picker = picker;
|
|
this.send("focusFrom");
|
|
});
|
|
},
|
|
|
|
@observes("computedConfig.{from,to,options}", "options", "isValid", "isRange")
|
|
configChanged() {
|
|
this._renderPreview();
|
|
},
|
|
|
|
@debounce(INPUT_DELAY)
|
|
async _renderPreview() {
|
|
if (this.markup) {
|
|
const result = await cookAsync(this.markup);
|
|
this.set("currentPreview", result);
|
|
|
|
schedule("afterRender", () => {
|
|
applyLocalDates(
|
|
document.querySelectorAll(".preview .discourse-local-date"),
|
|
this.siteSettings
|
|
);
|
|
});
|
|
}
|
|
},
|
|
|
|
@computed("date", "toDate", "toTime")
|
|
isRange(date, toDate, toTime) {
|
|
return date && (toDate || toTime);
|
|
},
|
|
|
|
@computed("computedConfig", "isRange")
|
|
isValid(config, isRange) {
|
|
const fromConfig = config.from;
|
|
if (!config.from.dateTime || !config.from.dateTime.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
if (isRange) {
|
|
const toConfig = config.to;
|
|
|
|
if (
|
|
!toConfig.dateTime ||
|
|
!toConfig.dateTime.isValid() ||
|
|
toConfig.dateTime.diff(fromConfig.dateTime) < 0
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
@computed("date", "time", "isRange", "options.{format,timezone}")
|
|
fromConfig(date, time, isRange, options = {}) {
|
|
const timeInferred = time ? false : true;
|
|
|
|
let dateTime;
|
|
if (!timeInferred) {
|
|
dateTime = moment.tz(`${date} ${time}`, options.timezone);
|
|
} else {
|
|
dateTime = moment.tz(date, options.timezone);
|
|
}
|
|
|
|
if (!timeInferred) {
|
|
time = dateTime.format(this.timeFormat);
|
|
}
|
|
|
|
let format = options.format;
|
|
if (timeInferred && this.formats.includes(format)) {
|
|
format = "LL";
|
|
}
|
|
|
|
return EmberObject.create({
|
|
date: dateTime.format(this.dateFormat),
|
|
time,
|
|
dateTime,
|
|
format,
|
|
range: isRange ? "start" : false,
|
|
});
|
|
},
|
|
|
|
@computed("toDate", "toTime", "isRange", "options.{timezone,format}")
|
|
toConfig(date, time, isRange, options = {}) {
|
|
const timeInferred = time ? false : true;
|
|
|
|
if (time && !date) {
|
|
date = moment().format(this.dateFormat);
|
|
}
|
|
|
|
let dateTime;
|
|
if (!timeInferred) {
|
|
dateTime = moment.tz(`${date} ${time}`, options.timezone);
|
|
} else {
|
|
dateTime = moment.tz(date, options.timezone).endOf("day");
|
|
}
|
|
|
|
if (!timeInferred) {
|
|
time = dateTime.format(this.timeFormat);
|
|
}
|
|
|
|
let format = options.format;
|
|
if (timeInferred && this.formats.includes(format)) {
|
|
format = "LL";
|
|
}
|
|
|
|
return EmberObject.create({
|
|
date: dateTime.format(this.dateFormat),
|
|
time,
|
|
dateTime,
|
|
format,
|
|
range: isRange ? "end" : false,
|
|
});
|
|
},
|
|
|
|
@computed("recurring", "timezones", "timezone", "format")
|
|
options(recurring, timezones, timezone, format) {
|
|
return EmberObject.create({
|
|
recurring,
|
|
timezones,
|
|
timezone,
|
|
format,
|
|
});
|
|
},
|
|
|
|
@computed(
|
|
"fromConfig.{date}",
|
|
"toConfig.{date}",
|
|
"options.{recurring,timezones,timezone,format}"
|
|
)
|
|
computedConfig(fromConfig, toConfig, options) {
|
|
return EmberObject.create({
|
|
from: fromConfig,
|
|
to: toConfig,
|
|
options,
|
|
});
|
|
},
|
|
|
|
@computed
|
|
currentUserTimezone() {
|
|
return moment.tz.guess();
|
|
},
|
|
|
|
@computed
|
|
allTimezones() {
|
|
return moment.tz.names();
|
|
},
|
|
|
|
timezoneIsDifferentFromUserTimezone: propertyNotEqual(
|
|
"currentUserTimezone",
|
|
"options.timezone"
|
|
),
|
|
|
|
@computed("currentUserTimezone")
|
|
formattedCurrentUserTimezone(timezone) {
|
|
return timezone.replace("_", " ").replace("Etc/", "").replace("/", ", ");
|
|
},
|
|
|
|
@computed("formats")
|
|
previewedFormats(formats) {
|
|
return formats.map((format) => {
|
|
return {
|
|
format,
|
|
preview: moment().format(format),
|
|
};
|
|
});
|
|
},
|
|
|
|
@computed
|
|
recurringOptions() {
|
|
const key = "discourse_local_dates.create.form.recurring";
|
|
|
|
return [
|
|
{
|
|
name: I18n.t(`${key}.every_day`),
|
|
id: "1.days",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_week`),
|
|
id: "1.weeks",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_two_weeks`),
|
|
id: "2.weeks",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_month`),
|
|
id: "1.months",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_two_months`),
|
|
id: "2.months",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_three_months`),
|
|
id: "3.months",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_six_months`),
|
|
id: "6.months",
|
|
},
|
|
{
|
|
name: I18n.t(`${key}.every_year`),
|
|
id: "1.years",
|
|
},
|
|
];
|
|
},
|
|
|
|
_generateDateMarkup(fromDateTime, options, isRange, toDateTime) {
|
|
return generateDateMarkup(fromDateTime, options, isRange, toDateTime);
|
|
},
|
|
|
|
@computed("advancedMode")
|
|
toggleModeBtnLabel(advancedMode) {
|
|
return advancedMode
|
|
? "discourse_local_dates.create.form.simple_mode"
|
|
: "discourse_local_dates.create.form.advanced_mode";
|
|
},
|
|
|
|
@computed("computedConfig.{from,to,options}", "options", "isValid", "isRange")
|
|
markup(config, options, isValid, isRange) {
|
|
let text;
|
|
|
|
if (isValid && config.from) {
|
|
if (config.to && config.to.range) {
|
|
text = this._generateDateMarkup(
|
|
config.from,
|
|
options,
|
|
isRange,
|
|
config.to
|
|
);
|
|
} else {
|
|
text = this._generateDateMarkup(config.from, options, isRange);
|
|
}
|
|
}
|
|
return text;
|
|
},
|
|
|
|
@computed("fromConfig.dateTime")
|
|
formattedFrom(dateTime) {
|
|
return dateTime.format("LLLL");
|
|
},
|
|
|
|
@computed("toConfig.dateTime", "toSelected")
|
|
formattedTo(dateTime, toSelected) {
|
|
const emptyText = toSelected
|
|
? " "
|
|
: I18n.t("discourse_local_dates.create.form.until");
|
|
|
|
return dateTime.isValid() ? dateTime.format("LLLL") : emptyText;
|
|
},
|
|
|
|
@action
|
|
updateFormat(format, event) {
|
|
event?.preventDefault();
|
|
this.set("format", format);
|
|
},
|
|
|
|
actions: {
|
|
setTime(event) {
|
|
this._setTimeIfValid(event.target.value, "time");
|
|
},
|
|
|
|
setToTime(event) {
|
|
this._setTimeIfValid(event.target.value, "toTime");
|
|
},
|
|
|
|
eraseToDateTime() {
|
|
this.setProperties({ toDate: null, toTime: null });
|
|
this._setPickerDate(null);
|
|
},
|
|
|
|
focusFrom() {
|
|
this.setProperties({ fromSelected: true, toSelected: false });
|
|
this._setPickerDate(this.get("fromConfig.date"));
|
|
this._setPickerMinDate(null);
|
|
},
|
|
|
|
focusTo() {
|
|
this.setProperties({ toSelected: true, fromSelected: false });
|
|
this._setPickerDate(this.get("toConfig.date"));
|
|
this._setPickerMinDate(this.get("fromConfig.date"));
|
|
},
|
|
|
|
advancedMode() {
|
|
this.toggleProperty("advancedMode");
|
|
},
|
|
|
|
save() {
|
|
const markup = this.markup;
|
|
|
|
if (markup) {
|
|
this._closeModal();
|
|
this.insertDate(markup);
|
|
}
|
|
},
|
|
|
|
cancel() {
|
|
this._closeModal();
|
|
},
|
|
},
|
|
|
|
_setTimeIfValid(time, key) {
|
|
if (isEmpty(time)) {
|
|
this.set(key, null);
|
|
return;
|
|
}
|
|
|
|
if (/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/.test(time)) {
|
|
this.set(key, time);
|
|
}
|
|
},
|
|
|
|
_setupPicker() {
|
|
return new Promise((resolve) => {
|
|
loadScript("/javascripts/pikaday.js").then(() => {
|
|
const options = {
|
|
field: this.element.querySelector(".fake-input"),
|
|
container: this.element.querySelector(
|
|
`#picker-container-${this.elementId}`
|
|
),
|
|
bound: false,
|
|
format: "YYYY-MM-DD",
|
|
reposition: false,
|
|
firstDay: 1,
|
|
setDefaultDate: true,
|
|
keyboardInput: false,
|
|
i18n: {
|
|
previousMonth: I18n.t("dates.previous_month"),
|
|
nextMonth: I18n.t("dates.next_month"),
|
|
months: moment.months(),
|
|
weekdays: moment.weekdays(),
|
|
weekdaysShort: moment.weekdaysMin(),
|
|
},
|
|
onSelect: (date) => {
|
|
const formattedDate = moment(date).format("YYYY-MM-DD");
|
|
|
|
if (this.fromSelected) {
|
|
this.set("date", formattedDate);
|
|
}
|
|
|
|
if (this.toSelected) {
|
|
this.set("toDate", formattedDate);
|
|
}
|
|
},
|
|
};
|
|
|
|
resolve(new Pikaday(options));
|
|
});
|
|
});
|
|
},
|
|
|
|
_setPickerMinDate(date) {
|
|
schedule("afterRender", () => {
|
|
if (moment(date, this.dateFormat).isValid()) {
|
|
this._picker.setMinDate(moment(date, this.dateFormat).toDate());
|
|
} else {
|
|
this._picker.setMinDate(null);
|
|
}
|
|
});
|
|
},
|
|
|
|
_setPickerDate(date) {
|
|
schedule("afterRender", () => {
|
|
if (moment(date, this.dateFormat).isValid()) {
|
|
this._picker.setDate(moment.utc(date), true);
|
|
} else {
|
|
this._picker.setDate(null);
|
|
}
|
|
});
|
|
},
|
|
|
|
_closeModal() {
|
|
const composer = getOwner(this).lookup("controller:composer");
|
|
composer.send("closeModal");
|
|
},
|
|
});
|