FEATURE: new date/time components (#7898)

FEATURE: new date/time components (#7898)

diff --git a/app/assets/javascripts/discourse/components/date-input.js.es6 b/app/assets/javascripts/discourse/components/date-input.js.es6
new file mode 100644
index 0000000..d29962c
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-input.js.es6
@@ -0,0 +1,101 @@
+/* global Pikaday:true */
+import loadScript from "discourse/lib/load-script";
+import {
+  default as computed,
+  on
+} from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+  classNames: ["d-date-input"],
+  date: null,
+  _picker: null,
+
+  @computed("site.mobileView")
+  inputType(mobileView) {
+    return mobileView ? "date" : "text";
+  },
+
+  @on("didInsertElement")
+  _loadDatePicker() {
+    const container = this.element.querySelector(`#${this.containerId}`);
+
+    if (this.site.mobileView) {
+      this._loadNativePicker(container);
+    } else {
+      this._loadPikadayPicker(container);
+    }
+  },
+
+  didUpdateAttrs() {
+    this._super(...arguments);
+
+    if (this._picker) {
+      this._picker.setDate(this.date, true);
+    }
+  },
+
+  _loadPikadayPicker(container) {
+    loadScript("/javascripts/pikaday.js").then(() => {
+      Ember.run.next(() => {
+        const default_opts = {
+          field: this.element.querySelector(".date-picker"),
+          container: container || this.element,
+          bound: container === null,
+          format: "LL",
+          firstDay: 1,
+          i18n: {
+            previousMonth: I18n.t("dates.previous_month"),
+            nextMonth: I18n.t("dates.next_month"),
+            months: moment.months(),
+            weekdays: moment.weekdays(),
+            weekdaysShort: moment.weekdaysShort()
+          },
+          onSelect: date => this._handleSelection(date)
+        };
+
+        this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
+        this._picker.setDate(this.date, true);
+      });
+    });
+  },
+
+  _loadNativePicker(container) {
+    const wrapper = container || this.element;
+    const picker = wrapper.querySelector("input.date-picker");
+    picker.onchange = () => this._handleSelection(picker.value);
+    picker.hide = () => {
+      /* do nothing for native */
+    };
+    picker.destroy = () => {
+      /* do nothing for native */
+    };
+    this._picker = picker;
+  },
+
+  _handleSelection(value) {
+    if (!this.element || this.isDestroying || this.isDestroyed) return;
+
+    this._picker && this._picker.hide();
+
+    if (this.onChange) {
+      this.onChange(moment(value).toDate());
+    }
+  },
+
+  @on("willDestroyElement")
+  _destroy() {
+    if (this._picker) {
+      this._picker.destroy();
+    }
+    this._picker = null;
+  },
+
+  @computed()
+  placeholder() {
+    return I18n.t("dates.placeholder");
+  },
+
+  _opts() {
+    return null;
+  }
+});
diff --git a/app/assets/javascripts/discourse/components/date-time-input-range.js.es6 b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
new file mode 100644
index 0000000..3754be9
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input-range.js.es6
@@ -0,0 +1,51 @@
+export default Ember.Component.extend({
+  classNames: ["d-date-time-input-range"],
+
+  from: null,
+  to: null,
+  onChangeTo: null,
+  onChangeFrom: null,
+  currentPanel: "from",
+  showFromTime: true,
+  showToTime: true,
+  error: null,
+
+  fromPanelActive: Ember.computed.equal("currentPanel", "from"),
+  toPanelActive: Ember.computed.equal("currentPanel", "to"),
+
+  _valid(state) {
+    if (state.to < state.from) {
+      return I18n.t("date_time_picker.errors.to_before_from");
+    }
+
+    return true;
+  },
+
+  actions: {
+    _onChange(options, value) {
+      if (this.onChange) {
+        const state = {
+          from: this.from,
+          to: this.to
+        };
+
+        const diff = {};
+        diff[options.prop] = value;
+
+        const newState = Object.assign(state, diff);
+
+        const validation = this._valid(newState);
+        if (validation === true) {
+          this.set("error", null);
+          this.onChange(newState);
+        } else {
+          this.set("error", validation);
+        }
+      }
+    },
+
+    onChangePanel(panel) {
+      this.set("currentPanel", panel);
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/components/date-time-input.js.es6 b/app/assets/javascripts/discourse/components/date-time-input.js.es6
new file mode 100644
index 0000000..ce173e3
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/date-time-input.js.es6
@@ -0,0 +1,35 @@
+export default Ember.Component.extend({
+  classNames: ["d-date-time-input"],
+  date: null,
+  showTime: true,
+
+  _hours: Ember.computed("date", function() {
+    return this.date ? this.date.getHours() : null;
+  }),
+
+  _minutes: Ember.computed("date", function() {
+    return this.date ? this.date.getMinutes() : null;
+  }),
+
+  actions: {
+    onChangeTime(time) {
+      if (this.onChange) {
+        const year = this.date.getFullYear();
+        const month = this.date.getMonth();
+        const day = this.date.getDate();
+        this.onChange(new Date(year, month, day, time.hours, time.minutes));
+      }
+    },
+
+    onChangeDate(date) {
+      if (this.onChange) {
+        const year = date.getFullYear();
+        const month = date.getMonth();
+        const day = date.getDate();
+        this.onChange(
+          new Date(year, month, day, this._hours || 0, this._minutes || 0)
+        );
+      }
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/components/time-input.js.es6 b/app/assets/javascripts/discourse/components/time-input.js.es6
new file mode 100644
index 0000000..bdeb1b3
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/time-input.js.es6
@@ -0,0 +1,73 @@
+import { isNumeric } from "discourse/lib/utilities";
+
+export default Ember.Component.extend({
+  classNames: ["d-time-input"],
+  hours: null,
+  minutes: null,
+  _hours: Ember.computed.oneWay("hours"),
+  _minutes: Ember.computed.oneWay("minutes"),
+  isSafari: Ember.computed.oneWay("capabilities.isSafari"),
+  isMobile: Ember.computed.oneWay("site.mobileView"),
+  nativePicker: Ember.computed.or("isSafari", "isMobile"),
+
+  actions: {
+    onInput(options, event) {
+      event.preventDefault();
+
+      if (this.onChange) {
+        let value = event.target.value;
+
+        if (!isNumeric(value)) {
+          value = 0;
+        } else {
+          value = parseInt(value, 10);
+        }
+
+        if (options.prop === "hours") {
+          value = Math.max(0, Math.min(value, 23))
+            .toString()
+            .padStart(2, "0");
+          this._processHoursChange(value);
+        } else {
+          value = Math.max(0, Math.min(value, 59))
+            .toString()
+            .padStart(2, "0");
+          this._processMinutesChange(value);
+        }
+
+        Ember.run.schedule("afterRender", () => (event.target.value = value));
+      }
+    },
+
+    onFocusIn(value, event) {
+      if (value && event.target) {
+        event.target.select();
+      }
+    },
+
+    onChangeTime(event) {
+      const time = event.target.value;
+
+      if (time && this.onChange) {
+        this.onChange({
+          hours: time.split(":")[0],
+          minutes: time.split(":")[1]
+        });
+      }
+    }
+  },
+
+  _processHoursChange(hours) {
+    this.onChange({
+      hours,
+      minutes: this._minutes || "00"
+    });
+  },
+
+  _processMinutesChange(minutes) {
+    this.onChange({
+      hours: this._hours || "00",
+      minutes
+    });
+  }
+});
diff --git a/app/assets/javascripts/discourse/templates/components/date-input.hbs b/app/assets/javascripts/discourse/templates/components/date-input.hbs
new file mode 100644
index 0000000..e29eb0e
--- /dev/null
+++ b/app/assets/javascripts/discourse/templates/components/date-input.hbs
@@ -0,0 +1,5 @@
+{{input
+  type=inputType
+  class="date-picker"
+  placeholder=placeholder
+  value=value}}

[... diff too long, it was truncated ...]

GitHub sha: 95ad4f90

1 Like