diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js new file mode 100644 index 00000000000..d5459e82dcf --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/discourse-local-dates.js @@ -0,0 +1,75 @@ +(function($) { + $.fn.applyLocalDates = function(repeat) { + function _formatTimezone(timezone) { + return timezone.replace("_", " ").split("/"); + } + + function processElement($element, options) { + repeat = repeat || true; + + if (this.timeout) { + clearTimeout(this.timeout); + } + + var relativeTime = moment.utc(options.date + " " + options.time, "YYYY-MM-DD HH:mm"); + + if (options.recurring && relativeTime < moment().utc()) { + var parts = options.recurring.split("."); + var count = parseInt(parts[0], 10); + var type = parts[1]; + var diff = moment().diff(relativeTime, type); + var add = Math.ceil(diff + count); + + relativeTime = relativeTime.add(add, type); + } + + var previews = options.timezones.split("|").map(function(timezone) { + var dateTime = relativeTime.tz(timezone).format(options.format); + var timezoneParts = _formatTimezone(timezone); + + if (dateTime.match(/TZ/)) { + return dateTime.replace("TZ", timezoneParts.join(": ")); + } else { + var output = timezoneParts[0]; + if (timezoneParts[1]) { + output += " (" + timezoneParts[1] + ")"; + } + output += " " + dateTime; + return output; + } + }); + + relativeTime = relativeTime.tz(moment.tz.guess()).format(options.format); + + var html = ""; + html += ""; + html += relativeTime.replace("TZ", _formatTimezone(moment.tz.guess()).join(": ")); + html += ""; + + $element + .html(html) + .attr("title", previews.join("\n")) + .attr("onclick", "alert('" + previews.join("\\n") + "');return false;") + .addClass("cooked"); + + if (repeat) { + this.timeout = setTimeout(function() { + processElement($element, options); + }, 10000); + } + } + + return this.each(function() { + var $this = $(this); + + var options = {}; + options.format = $this.attr("data-format"); + options.date = $this.attr("data-date"); + options.time = $this.attr("data-time"); + options.recurring = $this.attr("data-recurring"); + options.timezones = $this.attr("data-timezones") || "Etc/UTC"; + + processElement($this, options); + }); + }; +})(jQuery); diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 new file mode 100644 index 00000000000..9cb7bf5ac1d --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/components/discourse-local-dates-create-form.js.es6 @@ -0,0 +1,116 @@ +import computed from "ember-addons/ember-computed-decorators"; +import { observes } from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + timeFormat: "HH:mm", + dateFormat: "YYYY-MM-DD", + dateTimeFormat: "YYYY-MM-DD HH:mm", + config: null, + date: null, + time: null, + format: null, + formats: null, + recurring: null, + advancedMode: false, + + init() { + this._super(); + + this.set("date", moment().format(this.dateFormat)); + this.set("time", moment().format(this.timeFormat)); + this.set("format", `LLL`); + this.set("timezones", (this.siteSettings.discourse_local_dates_default_timezones || "").split("|").filter(f => f)); + this.set("formats", (this.siteSettings.discourse_local_dates_default_formats || "").split("|")); + }, + + didInsertElement() { + this._super(); + + this._setConfig(); + }, + + @computed + currentUserTimezone() { + return moment.tz.guess(); + }, + + @computed + recurringOptions() { + return [ + { name: "Every day", id: "1.days" }, + { name: "Every week", id: "1.weeks" }, + { name: "Every two weeks", id: "2.weeks" }, + { name: "Every month", id: "1.months" }, + { name: "Every two months", id: "2.months" }, + { name: "Every three months", id: "3.months" }, + { name: "Every six months", id: "6.months" }, + { name: "Every year", id: "1.years" }, + ]; + }, + + @computed() + allTimezones() { + return _.map(moment.tz.names(), (z) => z); + }, + + @observes("date", "time", "recurring", "format", "timezones") + _setConfig() { + const date = this.get("date"); + const time = this.get("time"); + const recurring = this.get("recurring"); + const format = this.get("format"); + const timezones = this.get("timezones"); + const dateTime = moment(`${date} ${time}`, this.dateTimeFormat).utc(); + + this.set("config", { + date: dateTime.format(this.dateFormat), + time: dateTime.format(this.timeFormat), + dateTime, + recurring, + format, + timezones, + }); + }, + + getTextConfig(config) { + let text = `[date=${config.date} `; + if (config.recurring) text += `recurring=${config.recurring} `; + text += `time=${config.time} `; + text += `format=${config.format} `; + text += `timezones="${config.timezones.join("|")}"`; + text += `]`; + return text; + }, + + @computed("config.dateTime") + validDate(dateTime) { + if (!dateTime) return false; + return dateTime.isValid(); + }, + + actions: { + advancedMode() { + this.toggleProperty("advancedMode"); + }, + + save() { + this._closeModal(); + + const textConfig = this.getTextConfig(this.get("config")); + this.get("toolbarEvent").addText(textConfig); + }, + + fillFormat(format) { + this.set("format", format); + }, + + cancel() { + this._closeModal(); + } + }, + + _closeModal() { + const composer = Discourse.__container__.lookup("controller:composer"); + composer.send("closeModal"); + } +}); diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs new file mode 100644 index 00000000000..f9ffba54f25 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/components/discourse-local-dates-create-form.hbs @@ -0,0 +1,77 @@ +{{#d-modal-body + title="discourse_local_dates.create.modal_title" + class="discourse-local-dates-create-modal" + style="overflow: auto"}} + +
+
+
+ {{date-picker-future class="date" value=date defaultDate="DD-MM-YYYY"}} + {{input type="time" value=time class="time"}} + {{currentUserTimezone}} +
+
+ +

{{i18n "discourse_local_dates.create.form.recurring_title"}}

+
+ {{#if advancedMode}} + + {{/if}} +
+ {{combo-box content=recurringOptions value=recurring none="discourse_local_dates.create.form.recurring_none"}} +
+
+ + {{d-button + class="advanced-mode-btn" + action=(action "advancedMode") + icon="cog" + label="discourse_local_dates.create.form.advanced_mode"}} + + {{#if advancedMode}} +
+
+ +
+ {{text-field value=format}} +
+
+
+
    + {{#each formats as |format|}} +
  • + {{format}} +
  • + {{/each}} +
+
+ +

{{i18n "discourse_local_dates.create.form.timezones_title"}}

+
+ +
+ {{multi-select allowAny=false maximum=5 content=allTimezones values=timezones}} +
+
+
+ {{/if}} +
+{{/d-modal-body}} + + diff --git a/plugins/discourse-local-dates/assets/javascripts/discourse/templates/modal/discourse-local-dates-create-modal.hbs b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/modal/discourse-local-dates-create-modal.hbs new file mode 100644 index 00000000000..b5f390f8e59 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/discourse/templates/modal/discourse-local-dates-create-modal.hbs @@ -0,0 +1 @@ +{{discourse-local-dates-create-form config=config toolbarEvent=toolbarEvent}} diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 new file mode 100644 index 00000000000..b79a2a648a5 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 @@ -0,0 +1,37 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; +import showModal from "discourse/lib/show-modal"; + +function initializeDiscourseLocalDates(api) { + api.decorateCooked($elem => { + $(".discourse-local-date", $elem).applyLocalDates(); + }); + + api.addToolbarPopupMenuOptionsCallback(() => { + return { + action: "insertDiscourseLocalDate", + icon: "globe", + label: "discourse_local_dates.title" + }; + }); + + api.modifyClass('controller:composer', { + actions: { + insertDiscourseLocalDate() { + showModal("discourse-local-dates-create-modal").setProperties({ + toolbarEvent: this.get("toolbarEvent") + }); + } + } + }); +} + +export default { + name: "discourse-local-dates", + + initialize(container) { + const siteSettings = container.lookup("site-settings:main"); + if (siteSettings.discourse_local_dates_enabled) { + withPluginApi("0.8.8", initializeDiscourseLocalDates); + } + } +}; diff --git a/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 new file mode 100644 index 00000000000..25e28947ec7 --- /dev/null +++ b/plugins/discourse-local-dates/assets/javascripts/lib/discourse-markdown/discourse-local-dates.js.es6 @@ -0,0 +1,73 @@ +import { parseBBCodeTag } from 'pretty-text/engines/discourse-markdown/bbcode-block'; + +function addLocalDate(buffer, matches, state) { + let token; + + let config = { + date: null, + time: null, + format: "YYYY-MM-DD HH:mm", + timezones: "" + }; + + let parsed = parseBBCodeTag("[date date" + matches[1] + "]", 0, matches[1].length + 11); + + config.date = parsed.attrs.date; + config.time = parsed.attrs.time; + config.format = parsed.attrs.format || config.format; + config.timezones = parsed.attrs.timezones || config.timezones; + + token = new state.Token('a_open', 'a', 1); + token.attrs = [ + ['class', 'discourse-local-date'], + ['data-date', config.date], + ['data-time', config.time], + ['data-recurring', config.recurring], + ['data-format', config.format], + ['data-timezones', config.timezones], + ]; + buffer.push(token); + + const previews = config.timezones.split("|").filter(t => t).map(timezone => { + const dateTime = moment + .utc(`${config.date} ${config.time}`, "YYYY-MM-DD HH:mm") + .tz(timezone) + .format(config.format); + + const formattedTimezone = timezone.replace("/", ": ").replace("_", " "); + + if (dateTime.match(/TZ/)) { + return dateTime.replace("TZ", formattedTimezone); + } else { + return `${dateTime} (${formattedTimezone})`; + } + }); + + token = new state.Token('text', '', 0); + token.content = previews.join(", "); + buffer.push(token); + + token = new state.Token('a_close', 'a', -1); + buffer.push(token); +} + +export function setup(helper) { + helper.whiteList([ + 'a.discourse-local-date', + 'a[data-*]', + 'a[title]' + ]); + + helper.registerOptions((opts, siteSettings) => { + opts.features['discourse-local-dates'] = !!siteSettings.discourse_local_dates_enabled; + }); + + helper.registerPlugin(md => { + const rule = { + matcher: /\[date(.*?)\]/, + onMatch: addLocalDate + }; + + md.core.textPostProcess.ruler.push('discourse-local-dates', rule); + }); +} diff --git a/plugins/discourse-local-dates/assets/stylesheets/discourse-local-dates.scss b/plugins/discourse-local-dates/assets/stylesheets/discourse-local-dates.scss new file mode 100644 index 00000000000..56a3cca43b6 --- /dev/null +++ b/plugins/discourse-local-dates/assets/stylesheets/discourse-local-dates.scss @@ -0,0 +1,83 @@ +.discourse-local-date { + display: inline-block; + vertical-align: top; + + &.cooked { + color: $primary; + font-weight: bold; + cursor: pointer; + + .d-icon-globe { + margin-right: .25em; + color: $primary-medium; + + &:hover { + color: $primary-high; + } + } + + &:hover .d-icon-globe { + color: $primary-high; + } + } + + + .discourse-local-date { + margin-left: .5em; + } +} + +.discourse-local-dates-create-modal-footer { + display: flex; + align-items: center; + justify-content: space-between; + + .validation-error { + color: $danger; + } + + &:before, &:after { + content: none; + } +} + +.discourse-local-dates-create-modal { + min-height: 300px; + display: flex; + flex-direction: row; + + .form { + flex: 1; + .controls { + &.date-time { + display: flex; + justify-content: flex-start; + align-items: center; + margin-bottom: 1em; + + .date { + margin: 0 0.5em 0 0; + } + + .date-picker { + padding-top: 5px; + bottom: 5px; + margin: 0; + } + + .time { + margin: 0 0.5em 0 0; + max-width: 100px; + } + } + } + + .advanced-mode-btn { + margin-top: 2em; + margin-bottom: 1em; + } + } + + .select-kit.multi-select { + width: 90%; + } +}