Bug 1320225 - [DateTimeInput] Integration of input type=date picker with input box (part 2). r=mconley draft
authorScott Wu <scottcwwu@gmail.com>
Wed, 21 Dec 2016 14:51:53 +0800
changeset 460665 9138fba2fa25b9f4f741455df16319ff55a4427c
parent 460664 92d2a754da3a25b57b4446fd7b9e9b00922953f9
child 542106 92ea54d38f59b872208ca0fe6baca6bc843ed48e
push id41453
push userbmo:scwwu@mozilla.com
push dateFri, 13 Jan 2017 15:04:26 +0000
reviewersmconley
bugs1320225
milestone53.0a1
Bug 1320225 - [DateTimeInput] Integration of input type=date picker with input box (part 2). r=mconley MozReview-Commit-ID: DcwvCSa1ofR
toolkit/content/widgets/datekeeper.js
toolkit/content/widgets/datepicker.js
toolkit/content/widgets/datetimepopup.xml
toolkit/content/widgets/timepicker.js
--- a/toolkit/content/widgets/datekeeper.js
+++ b/toolkit/content/widgets/datekeeper.js
@@ -6,35 +6,35 @@
 
 /**
  * DateKeeper keeps track of the date states.
  *
  * @param {Object} date parts
  *        {
  *          {Number} year
  *          {Number} month
- *          {Number} date
+ *          {Number} day
  *        }
  *        {Object} options
  *        {
  *          {Number} firstDayOfWeek [optional]
  *          {Array<Number>} weekends [optional]
  *          {Number} calViewSize [optional]
  *        }
  */
-function DateKeeper({ year, month, date }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
+function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
   this.state = {
     firstDayOfWeek, weekends, calViewSize,
     dateObj: new Date(0),
     years: [],
     months: [],
     days: []
   };
   this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek);
-  this._update(year, month, date);
+  this._update(year, month, day);
 }
 
 {
   const DAYS_IN_A_WEEK = 7,
         MONTHS_IN_A_YEAR = 12,
         YEAR_VIEW_SIZE = 200,
         YEAR_BUFFER_SIZE = 10;
 
@@ -43,68 +43,68 @@ function DateKeeper({ year, month, date 
      * Set new date
      * @param {Object} date parts
      *        {
      *          {Number} year [optional]
      *          {Number} month [optional]
      *          {Number} date [optional]
      *        }
      */
-    set({ year = this.state.year, month = this.state.month, date = this.state.date }) {
-      this._update(year, month, date);
+    set({ year = this.state.year, month = this.state.month, day = this.state.day }) {
+      this._update(year, month, day);
     },
 
     /**
      * Set date with value
      * @param {Number} value: Date value
      */
     setValue(value) {
       const dateObj = new Date(value);
       this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
     },
 
     /**
-     * Set month. Makes sure the date is <= the last day of the month
+     * Set month. Makes sure the day is <= the last day of the month
      * @param {Number} month
      */
     setMonth(month) {
       const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate();
-      this._update(this.state.year, month, Math.min(this.state.date, lastDayOfMonth));
+      this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth));
     },
 
     /**
-     * Set year. Makes sure the date is <= the last day of the month
+     * Set year. Makes sure the day is <= the last day of the month
      * @param {Number} year
      */
     setYear(year) {
       const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate();
-      this._update(year, this.state.month, Math.min(this.state.date, lastDayOfMonth));
+      this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth));
     },
 
     /**
-     * Set month by offset. Makes sure the date is <= the last day of the month
+     * Set month by offset. Makes sure the day is <= the last day of the month
      * @param {Number} offset
      */
     setMonthByOffset(offset) {
       const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate();
-      this._update(this.state.year, this.state.month + offset, Math.min(this.state.date, lastDayOfMonth));
+      this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth));
     },
 
     /**
      * Update the states.
      * @param  {Number} year  [description]
      * @param  {Number} month [description]
-     * @param  {Number} date  [description]
+     * @param  {Number} day  [description]
      */
-    _update(year, month, date) {
+    _update(year, month, day) {
       // Use setUTCFullYear so that year 99 doesn't get parsed as 1999
-      this.state.dateObj.setUTCFullYear(year, month, date);
+      this.state.dateObj.setUTCFullYear(year, month, day);
       this.state.year = this.state.dateObj.getUTCFullYear();
       this.state.month = this.state.dateObj.getUTCMonth();
-      this.state.date = this.state.dateObj.getUTCDate();
+      this.state.day = this.state.dateObj.getUTCDate();
     },
 
     /**
      * Generate the array of months
      * @return {Array<Object>}
      *         {
      *           {Number} value: Month in int
      *           {Boolean} enabled
@@ -196,24 +196,24 @@ function DateKeeper({ year, month, date 
      * @return {Array<Object>}
      *         {
      *           {Number} textContent
      *           {Array<String>} classNames
      *         }
      */
     _getWeekHeaders(firstDayOfWeek) {
       let headers = [];
-      let day = firstDayOfWeek;
+      let dayOfWeek = firstDayOfWeek;
 
       for (let i = 0; i < DAYS_IN_A_WEEK; i++) {
         headers.push({
-          textContent: day % DAYS_IN_A_WEEK,
-          classNames: this.state.weekends.includes(day % DAYS_IN_A_WEEK) ? ["weekend"] : []
+          textContent: dayOfWeek % DAYS_IN_A_WEEK,
+          classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
         });
-        day++;
+        dayOfWeek++;
       }
       return headers;
     },
 
     /**
      * Get the first day on a calendar month
      * @param  {Date} dateObj
      * @param  {Number} firstDayOfWeek
--- a/toolkit/content/widgets/datepicker.js
+++ b/toolkit/content/widgets/datepicker.js
@@ -32,23 +32,23 @@ function DatePicker(context) {
 
     /*
      * Set initial date picker states.
      */
     _setDefaultState() {
       const now = new Date();
       const { year = now.getFullYear(),
               month = now.getMonth(),
-              date = now.getDate(),
+              day = now.getDate(),
               locale } = this.props;
 
       // TODO: Use calendar info API to get first day of week & weekends
       //       (Bug 1287503)
       const dateKeeper = new DateKeeper({
-        year, month, date
+        year, month, day
       }, {
         calViewSize: CAL_VIEW_SIZE,
         firstDayOfWeek: 0,
         weekends: [0]
       });
 
       this.state = {
         dateKeeper,
@@ -63,16 +63,17 @@ function DatePicker(context) {
         setValue: ({ dateValue, selectionValue }) => {
           dateKeeper.setValue(dateValue);
           this.state.selectionValue = selectionValue;
           this.state.isYearSet = true;
           this.state.isMonthSet = true;
           this.state.isDateSet = true;
           this._update();
           this._dispatchState();
+          this._closePopup();
         },
         setYear: year => {
           dateKeeper.setYear(year);
           this.state.isYearSet = true;
           this._update();
           this._dispatchState();
         },
         setMonth: month => {
@@ -144,32 +145,41 @@ function DatePicker(context) {
       });
 
       isMonthPickerVisible ?
         this.context.monthYearView.classList.remove("hidden") :
         this.context.monthYearView.classList.add("hidden");
     },
 
     /**
+     * Use postMessage to close the picker.
+     */
+    _closePopup() {
+      window.postMessage({
+        name: "ClosePopup"
+      }, "*");
+    },
+
+    /**
      * Use postMessage to pass the state of picker to the panel.
      */
     _dispatchState() {
-      const { year, month, date } = this.state.dateKeeper.state;
-      const { isYearSet, isMonthSet, isDateSet } = this.state;
+      const { year, month, day } = this.state.dateKeeper.state;
+      const { isYearSet, isMonthSet, isDaySet } = this.state;
       // The panel is listening to window for postMessage event, so we
       // do postMessage to itself to send data to input boxes.
       window.postMessage({
-        name: "DatePickerPopupChanged",
+        name: "PickerPopupChanged",
         detail: {
           year,
           month,
-          date,
+          day,
           isYearSet,
           isMonthSet,
-          isDateSet
+          isDaySet
         }
       }, "*");
     },
 
     /**
      * Attach event listeners
      */
     _attachEventListeners() {
@@ -216,49 +226,53 @@ function DatePicker(context) {
 
     /**
      * Handle postMessage events.
      *
      * @param {Event} event
      */
     handleMessage(event) {
       switch (event.data.name) {
-        case "DatePickerSetValue": {
+        case "PickerSetValue": {
           this.set(event.data.detail);
           break;
         }
-        case "DatePickerInit": {
+        case "PickerInit": {
           this.init(event.data.detail);
           break;
         }
       }
     },
 
     /**
      * Set the date state and update the components with the new state.
      *
      * @param {Object} dateState
      *        {
      *          {Number} year [optional]
      *          {Number} month [optional]
      *          {Number} date [optional]
      *        }
      */
-    set(dateState) {
-      if (dateState.year != undefined) {
+    set({ year, month, day }) {
+      const { dateKeeper } = this.state;
+
+      if (year != undefined) {
         this.state.isYearSet = true;
       }
-      if (dateState.month != undefined) {
+      if (month != undefined) {
         this.state.isMonthSet = true;
       }
-      if (dateState.date != undefined) {
-        this.state.isDateSet = true;
+      if (day != undefined) {
+        this.state.isDaySet = true;
       }
 
-      this.state.dateKeeper.set(dateState);
+      dateKeeper.set({
+        year, month, day
+      });
       this._update();
     }
   };
 
   /**
    * MonthYear is a component that handles the month & year spinners
    *
    * @param {Object} options
--- a/toolkit/content/widgets/datetimepopup.xml
+++ b/toolkit/content/widgets/datetimepopup.xml
@@ -15,16 +15,18 @@
       <stylesheet src="chrome://global/skin/datetimepopup.css"/>
     </resources>
     <implementation>
       <field name="dateTimePopupFrame">
         this.querySelector("#dateTimePopupFrame");
       </field>
       <field name="TIME_PICKER_WIDTH" readonly="true">"12em"</field>
       <field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
+      <field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field>
+      <field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field>
       <method name="loadPicker">
         <parameter name="type"/>
         <parameter name="detail"/>
         <body><![CDATA[
           this.hidden = false;
           this.type = type;
           this.pickerState = {};
           // TODO: Resize picker according to content zoom level
@@ -33,67 +35,103 @@
             case "time": {
               this.detail = detail;
               this.dateTimePopupFrame.addEventListener("load", this, true);
               this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/timepicker.xhtml");
               this.dateTimePopupFrame.style.width = this.TIME_PICKER_WIDTH;
               this.dateTimePopupFrame.style.height = this.TIME_PICKER_HEIGHT;
               break;
             }
+            case "date": {
+              this.detail = detail;
+              this.dateTimePopupFrame.addEventListener("load", this, true);
+              this.dateTimePopupFrame.setAttribute("src", "chrome://global/content/datepicker.xhtml");
+              this.dateTimePopupFrame.style.width = this.DATE_PICKER_WIDTH;
+              this.dateTimePopupFrame.style.height = this.DATE_PICKER_HEIGHT;
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="closePicker">
         <body><![CDATA[
           this.hidden = true;
           this.setInputBoxValue(true);
           this.pickerState = {};
           this.type = undefined;
           this.dateTimePopupFrame.removeEventListener("load", this, true);
-          this.dateTimePopupFrame.contentDocument.removeEventListener("TimePickerPopupChanged", this, false);
+          this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false);
           this.dateTimePopupFrame.setAttribute("src", "");
         ]]></body>
       </method>
       <method name="setPopupValue">
         <parameter name="data"/>
         <body><![CDATA[
           switch (this.type) {
             case "time": {
               this.postMessageToPicker({
-                name: "TimePickerSetValue",
+                name: "PickerSetValue",
                 detail: data.value
               });
               break;
             }
+            case "date": {
+              const { year, month, day } = data.value;
+              this.postMessageToPicker({
+                name: "PickerSetValue",
+                detail: {
+                  year,
+                  // Month value from input box starts from 1 instead of 0
+                  month: month == undefined ? undefined : month - 1,
+                  day
+                }
+              });
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="initPicker">
         <parameter name="detail"/>
         <body><![CDATA[
+          const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");
+
           switch (this.type) {
             case "time": {
               const { hour, minute } = detail.value;
               const format = detail.format || "12";
-              const locale = Components.classes["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");
 
               this.postMessageToPicker({
-                name: "TimePickerInit",
+                name: "PickerInit",
                 detail: {
                   hour,
                   minute,
                   format,
                   locale,
                   min: detail.min,
                   max: detail.max,
                   step: detail.step,
                 }
               });
               break;
             }
+            case "date": {
+              const { year, month, day } = detail.value;
+              this.postMessageToPicker({
+                name: "PickerInit",
+                detail: {
+                  year,
+                  // Month value from input box starts from 1 instead of 0
+                  month: month == undefined ? undefined : month - 1,
+                  day,
+                  locale
+                }
+              });
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="setInputBoxValue">
         <parameter name="passAllValues"/>
         <body><![CDATA[
           /**
            * @param {Boolean} passAllValues: Pass spinner values regardless if they've been set/changed or not
@@ -107,32 +145,47 @@
               } else {
                 this.sendPickerValueChanged({
                   hour: isHourSet || isDayPeriodSet ? hour : undefined,
                   minute: isMinuteSet ? minute : undefined
                 });
               }
               break;
             }
+            case "date": {
+              this.sendPickerValueChanged(this.pickerState);
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="sendPickerValueChanged">
         <parameter name="value"/>
         <body><![CDATA[
           switch (this.type) {
             case "time": {
               this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", {
                 detail: {
                   hour: value.hour,
                   minute: value.minute
                 }
               }));
               break;
             }
+            case "date": {
+              this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", {
+                detail: {
+                  year: value.year,
+                  // Month value from input box starts from 1 instead of 0
+                  month: value.month == undefined ? undefined : value.month + 1,
+                  day: value.day
+                }
+              }));
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
           switch (aEvent.type) {
             case "load": {
@@ -150,21 +203,25 @@
       <method name="handleMessage">
         <parameter name="aEvent"/>
         <body><![CDATA[
           if (!this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
             return;
           }
 
           switch (aEvent.data.name) {
-            case "TimePickerPopupChanged": {
+            case "PickerPopupChanged": {
               this.pickerState = aEvent.data.detail;
               this.setInputBoxValue();
               break;
             }
+            case "ClosePopup": {
+              this.closePicker();
+              break;
+            }
           }
         ]]></body>
       </method>
       <method name="postMessageToPicker">
         <parameter name="data"/>
         <body><![CDATA[
           if (this.dateTimePopupFrame.contentDocument.nodePrincipal.isSystemPrincipal) {
             this.dateTimePopupFrame.contentWindow.postMessage(data, "*");
--- a/toolkit/content/widgets/timepicker.js
+++ b/toolkit/content/widgets/timepicker.js
@@ -199,17 +199,17 @@ function TimePicker(context) {
      * Dispatch CustomEvent to pass the state of picker to the panel.
      */
     _dispatchState() {
       const { hour, minute } = this.state.timeKeeper;
       const { isHourSet, isMinuteSet, isDayPeriodSet } = this.state;
       // The panel is listening to window for postMessage event, so we
       // do postMessage to itself to send data to input boxes.
       window.postMessage({
-        name: "TimePickerPopupChanged",
+        name: "PickerPopupChanged",
         detail: {
           hour,
           minute,
           isHourSet,
           isMinuteSet,
           isDayPeriodSet
         }
       }, "*");
@@ -241,21 +241,21 @@ function TimePicker(context) {
 
     /**
      * Handle postMessage events.
      *
      * @param {Event} event
      */
     handleMessage(event) {
       switch (event.data.name) {
-        case "TimePickerSetValue": {
+        case "PickerSetValue": {
           this.set(event.data.detail);
           break;
         }
-        case "TimePickerInit": {
+        case "PickerInit": {
           this.init(event.data.detail);
           break;
         }
       }
     },
 
     /**
      * Set the time state and update the components with the new state.