Bug 1363672 - Add step support to date picker. r=mconley draft
authorScott Wu <scottcwwu@gmail.com>
Thu, 11 May 2017 12:16:18 +0800
changeset 608859 8e4b38577fcd99e66e831a971d9993cafef17a5b
parent 606556 0e41d07a703f19224f60b01577b2cbb5708046c9
child 637430 ad7160de19cfc2d56b0322227340cd9ba8b10fe3
push id68423
push userbmo:scwwu@mozilla.com
push dateFri, 14 Jul 2017 07:49:25 +0000
reviewersmconley
bugs1363672
milestone56.0a1
Bug 1363672 - Add step support to date picker. r=mconley MozReview-Commit-ID: 62IfiKArN34
toolkit/content/browser-content.js
toolkit/content/widgets/datekeeper.js
toolkit/content/widgets/datepicker.js
toolkit/content/widgets/datetimepopup.xml
toolkit/themes/shared/datetimeinputpickers.css
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1743,19 +1743,20 @@ let DateTimePickerListener = {
           rect: this.getBoundingContentRect(this._inputElement),
           dir: this.getComputedDirection(this._inputElement),
           type: this._inputElement.type,
           detail: {
             // Pass partial value if it's available, otherwise pass input
             // element's value.
             value: Object.keys(value).length > 0 ? value
                                                  : this._inputElement.value,
-            step: this._inputElement.step,
             min: this._inputElement.min,
             max: this._inputElement.max,
+            step: this._inputElement.getStep(),
+            stepBase: this._inputElement.getStepBase(),
           },
         });
         break;
       }
       case "MozUpdateDateTimePicker": {
         let value = this._inputElement.getDateTimeInputBoxValue();
         value.type = this._inputElement.type;
         sendAsyncMessage("FormDateTime:UpdatePicker", { value });
--- a/toolkit/content/widgets/datekeeper.js
+++ b/toolkit/content/widgets/datekeeper.js
@@ -40,28 +40,31 @@ function DateKeeper(props) {
 
     /**
      * Initialize DateKeeper
      * @param  {Number} year
      * @param  {Number} month
      * @param  {Number} day
      * @param  {String} min
      * @param  {String} max
+     * @param  {Number} step
+     * @param  {Number} stepBase
      * @param  {Number} firstDayOfWeek
      * @param  {Array<Number>} weekends
      * @param  {Number} calViewSize
      */
-    init({ year, month, day, min, max, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
+    init({ year, month, day, min, max, step, stepBase, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
       const today = new Date();
       const isDateSet = year != undefined && month != undefined && day != undefined;
 
       this.state = {
-        firstDayOfWeek, weekends, calViewSize,
+        step, firstDayOfWeek, weekends, calViewSize,
         min: new Date(min != undefined ? min : MIN_DATE),
         max: new Date(max != undefined ? max : MAX_DATE),
+        stepBase: new Date(stepBase),
         today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
         weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
         years: [],
         months: [],
         days: [],
         selection: { year, month, day },
       };
 
@@ -187,53 +190,88 @@ function DateKeeper(props) {
      * @return {Array<Object>}
      *         {
      *           {Date} dateObj
      *           {Array<String>} classNames
      *           {Boolean} enabled
      *         }
      */
     getDays() {
-      // TODO: add step support
       const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
       const month = this.month;
       let days = [];
 
       for (let i = 0; i < this.state.calViewSize; i++) {
-        const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i);
+        const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(),
+                                         firstDayOfMonth.getUTCMonth(),
+                                         firstDayOfMonth.getUTCDate() + i);
         let classNames = [];
         let enabled = true;
-        if (this.state.weekends.includes(dateObj.getUTCDay())) {
+
+        const isWeekend = this.state.weekends.includes(dateObj.getUTCDay());
+        const isCurrentMonth = month == dateObj.getUTCMonth();
+        const isSelection = this.state.selection.year == dateObj.getUTCFullYear() &&
+                            this.state.selection.month == dateObj.getUTCMonth() &&
+                            this.state.selection.day == dateObj.getUTCDate();
+        const isOutOfRange = dateObj.getTime() < this.state.min.getTime() ||
+                             dateObj.getTime() > this.state.max.getTime();
+        const isToday = this.state.today.getTime() == dateObj.getTime();
+        const isOffStep = this._checkIsOffStep(dateObj,
+                                               this._newUTCDate(dateObj.getUTCFullYear(),
+                                                                dateObj.getUTCMonth(),
+                                                                dateObj.getUTCDate() + 1));
+
+        if (isWeekend) {
           classNames.push("weekend");
         }
-        if (month != dateObj.getUTCMonth()) {
+        if (!isCurrentMonth) {
           classNames.push("outside");
         }
-        if (this.state.selection.year == dateObj.getUTCFullYear() &&
-            this.state.selection.month == dateObj.getUTCMonth() &&
-            this.state.selection.day == dateObj.getUTCDate()) {
+        if (isSelection && !isOutOfRange && !isOffStep) {
           classNames.push("selection");
         }
-        if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) {
+        if (isOutOfRange) {
           classNames.push("out-of-range");
           enabled = false;
         }
-        if (this.state.today.getTime() == dateObj.getTime()) {
+        if (isToday) {
           classNames.push("today");
         }
+        if (isOffStep) {
+          classNames.push("off-step");
+          enabled = false;
+        }
         days.push({
           dateObj,
           classNames,
           enabled,
         });
       }
       return days;
     },
 
     /**
+     * Check if a date is off step given a starting point and the next increment
+     * @param  {Date} start
+     * @param  {Date} next
+     * @return {Boolean}
+     */
+    _checkIsOffStep(start, next) {
+      // If the increment is larger or equal to the step, it must not be off-step.
+      if (next - start >= this.state.step) {
+        return false;
+      }
+      // Calculate the last valid date
+      const lastValidStep = Math.floor((next - 1 - this.state.stepBase) / this.state.step);
+      const lastValidTimeInMs = lastValidStep * this.state.step + this.state.stepBase.getTime();
+      // The date is off-step if the last valid date is smaller than the start date
+      return lastValidTimeInMs < start.getTime();
+    },
+
+    /**
      * Get week headers for calendar
      * @param  {Number} firstDayOfWeek
      * @param  {Array<Number>} weekends
      * @return {Array<Object>}
      *         {
      *           {Number} textContent
      *           {Array<String>} classNames
      *         }
--- a/toolkit/content/widgets/datepicker.js
+++ b/toolkit/content/widgets/datepicker.js
@@ -21,16 +21,18 @@ function DatePicker(context) {
      * Initializes the date picker. Set the default states and properties.
      * @param  {Object} props
      *         {
      *           {Number} year [optional]
      *           {Number} month [optional]
      *           {Number} date [optional]
      *           {String} min
      *           {String} max
+     *           {Number} step
+     *           {Number} stepBase
      *           {Number} firstDayOfWeek
      *           {Array<Number>} weekends
      *           {Array<String>} monthStrings
      *           {Array<String>} weekdayStrings
      *           {String} locale [optional]: User preferred locale
      *         }
      */
     init(props = {}) {
@@ -39,20 +41,20 @@ function DatePicker(context) {
       this._createComponents();
       this._update();
     },
 
     /*
      * Set initial date picker states.
      */
     _setDefaultState() {
-      const { year, month, day, min, max, firstDayOfWeek, weekends,
+      const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends,
               monthStrings, weekdayStrings, locale, dir } = this.props;
       const dateKeeper = new DateKeeper({
-        year, month, day, min, max, firstDayOfWeek, weekends,
+        year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends,
         calViewSize: CAL_VIEW_SIZE
       });
 
       document.dir = dir;
 
       this.state = {
         dateKeeper,
         locale,
--- a/toolkit/content/widgets/datetimepopup.xml
+++ b/toolkit/content/widgets/datetimepopup.xml
@@ -171,16 +171,18 @@
                   firstDayOfWeek,
                   weekends,
                   monthStrings,
                   weekdayStrings,
                   locale,
                   dir,
                   min: detail.min,
                   max: detail.max,
+                  step: detail.step,
+                  stepBase: detail.stepBase,
                 }
               });
               break;
             }
           }
         ]]></body>
       </method>
       <method name="setInputBoxValue">
--- a/toolkit/themes/shared/datetimeinputpickers.css
+++ b/toolkit/themes/shared/datetimeinputpickers.css
@@ -245,26 +245,29 @@ button.month-year.active::after {
 .days-view > .weekend {
   color: var(--weekend-font-color);
 }
 
 .days-view > .weekend.outside {
   color: var(--weekend-outside-font-color);
 }
 
-.days-view > .out-of-range {
+.days-view > .out-of-range,
+.days-view > .off-step {
   color: var(--weekday-disabled-font-color);
   background: var(--disabled-fill-color);
 }
 
-.days-view > .out-of-range.weekend {
+.days-view > .out-of-range.weekend,
+.days-view > .off-step.weekend {
   color: var(--weekend-disabled-font-color);
 }
 
-.days-view > .out-of-range::before {
+.days-view > .out-of-range::before,
+.days-view > .off-step::before {
   display: none;
 }
 
 .days-view > div:hover::before,
 .days-view > .select::before,
 .days-view > .today::before {
   top: 5%;
   bottom: 5%;
@@ -346,17 +349,17 @@ button.month-year.active::after {
 .spinner-container > .spinner > div:hover::before,
 .calendar-container .days-view > div:hover::before {
   background: var(--fill-color);
   border: var(--border);
   content: "";
 }
 
 .spinner-container > .spinner:not(.scrolling) > div.selection,
-.calendar-container .days-view > div.selection:not(.out-of-range) {
+.calendar-container .days-view > div.selection {
   color: var(--selected-font-color);
 }
 
 .spinner-container > .spinner > div.selection::before,
 .calendar-container .days-view > div.selection::before {
   background: var(--selected-fill-color);
   border: none;
   content: "";