Bug 1310898 - Datetime picker supports zooming in/out draft
authorScott Wu <scottcwwu@gmail.com>
Thu, 07 Sep 2017 10:57:03 +0800
changeset 665257 d07d11e9663031ba7bab3eb3a209049c199f5e83
parent 665088 8e818b5e9b6bef0fc1a5c527ecf30b0d56a02f14
child 731721 23e766d3e4ec30d102336e6659f2219d5b533613
push id80000
push userbmo:scwwu@mozilla.com
push dateFri, 15 Sep 2017 07:35:56 +0000
bugs1310898
milestone57.0a1
Bug 1310898 - Datetime picker supports zooming in/out MozReview-Commit-ID: BjZWhb3qX3I
toolkit/content/browser-content.js
toolkit/content/widgets/datepicker.js
toolkit/content/widgets/datetimepopup.xml
toolkit/content/widgets/spinner.js
toolkit/modules/DateTimePickerHelper.jsm
--- a/toolkit/content/browser-content.js
+++ b/toolkit/content/browser-content.js
@@ -1718,28 +1718,32 @@ let DateTimePickerListener = {
   /**
    * Called after picker is opened to start listening for input box update
    * events.
    */
   addListeners() {
     addEventListener("MozUpdateDateTimePicker", this);
     addEventListener("MozCloseDateTimePicker", this);
     addEventListener("pagehide", this);
+    addEventListener("TextZoomChange", this);
+    addEventListener("FullZoomChange", this);
 
     addMessageListener("FormDateTime:PickerValueChanged", this);
     addMessageListener("FormDateTime:PickerClosed", this);
   },
 
   /**
    * Stop listeneing for events when picker is closed.
    */
   removeListeners() {
     removeEventListener("MozUpdateDateTimePicker", this);
     removeEventListener("MozCloseDateTimePicker", this);
     removeEventListener("pagehide", this);
+    removeEventListener("TextZoomChange", this);
+    removeEventListener("FullZoomChange", this);
 
     removeMessageListener("FormDateTime:PickerValueChanged", this);
     removeMessageListener("FormDateTime:PickerClosed", this);
   },
 
   /**
    * Helper function that returns the CSS direction property of the element.
    */
@@ -1826,16 +1830,24 @@ let DateTimePickerListener = {
       case "pagehide": {
         if (this._inputElement &&
             this._inputElement.ownerDocument == aEvent.target) {
           sendAsyncMessage("FormDateTime:ClosePicker");
           this.close();
         }
         break;
       }
+      case "TextZoomChange": {
+        sendAsyncMessage("FormDateTime:SetZoom");
+        break;
+      }
+      case "FullZoomChange": {
+        sendAsyncMessage("FormDateTime:SetZoom");
+        break;
+      }
       default:
         break;
     }
   },
 }
 
 DateTimePickerListener.init();
 
--- a/toolkit/content/widgets/datepicker.js
+++ b/toolkit/content/widgets/datepicker.js
@@ -43,27 +43,29 @@ function DatePicker(context) {
       document.dispatchEvent(new CustomEvent("PickerReady"));
     },
 
     /*
      * Set initial date picker states.
      */
     _setDefaultState() {
       const { year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends,
-              monthStrings, weekdayStrings, locale, dir } = this.props;
+              monthStrings, weekdayStrings, locale, dir, zoom } = this.props;
       const dateKeeper = new DateKeeper({
         year, month, day, min, max, step, stepBase, firstDayOfWeek, weekends,
         calViewSize: CAL_VIEW_SIZE
       });
 
       document.dir = dir;
 
       this.state = {
         dateKeeper,
         locale,
+        zoom,
+        textSize: 10,
         isMonthPickerVisible: false,
         datetimeOrders: new Intl.DateTimeFormat(locale)
                           .formatToParts(new Date(0)).map(part => part.type),
         getDayString: day => day ? new Intl.NumberFormat(locale).format(day) : "",
         getWeekHeaderString: weekday => weekdayStrings[weekday],
         getMonthString: month => monthStrings[month],
         setSelection: date => {
           dateKeeper.setSelection({
@@ -117,43 +119,47 @@ function DatePicker(context) {
           weekHeader: this.context.weekHeader,
           daysView: this.context.daysView
         }),
         monthYear: new MonthYear({
           setYear: this.state.setYear,
           setMonth: this.state.setMonth,
           getMonthString: this.state.getMonthString,
           datetimeOrders: this.state.datetimeOrders,
-          locale: this.state.locale
+          locale: this.state.locale,
+          rootFontSize: this.state.textSize * this.state.zoom,
         }, {
           monthYear: this.context.monthYear,
           monthYearView: this.context.monthYearView
         })
       };
     },
 
     /**
      * Update date picker and its components.
      */
     _update() {
-      const { dateKeeper, isMonthPickerVisible } = this.state;
+      const { dateKeeper, isMonthPickerVisible, zoom } = this.state;
 
       if (isMonthPickerVisible) {
         this.state.months = dateKeeper.getMonths();
         this.state.years = dateKeeper.getYears();
       } else {
         this.state.days = dateKeeper.getDays();
       }
 
+      this.zoom({ zoom });
+
       this.components.monthYear.setProps({
         isVisible: isMonthPickerVisible,
         dateObj: dateKeeper.state.dateObj,
         months: this.state.months,
         years: this.state.years,
-        toggleMonthPicker: this.state.toggleMonthPicker
+        toggleMonthPicker: this.state.toggleMonthPicker,
+        rootFontSize: this.state.zoom * this.state.textSize,
       });
       this.components.calendar.setProps({
         isVisible: !isMonthPickerVisible,
         days: this.state.days,
         weekHeaders: dateKeeper.state.weekHeaders
       });
 
       isMonthPickerVisible ?
@@ -242,16 +248,20 @@ function DatePicker(context) {
         case "PickerSetValue": {
           this.set(event.data.detail);
           break;
         }
         case "PickerInit": {
           this.init(event.data.detail);
           break;
         }
+        case "PickerZoom": {
+          this.zoom(event.data.detail);
+          break;
+        }
       }
     },
 
     /**
      * Set the date state and update the components with the new state.
      *
      * @param {Object} dateState
      *        {
@@ -265,17 +275,22 @@ function DatePicker(context) {
 
       dateKeeper.setCalendarMonth({
         year, month
       });
       dateKeeper.setSelection({
         year, month, day
       });
       this._update();
-    }
+    },
+
+    zoom({ zoom }) {
+      this.state.zoom = zoom;
+      document.documentElement.style.fontSize = this.state.textSize * zoom + "px";
+    },
   };
 
   /**
    * MonthYear is a component that handles the month & year spinners
    *
    * @param {Object} options
    *        {
    *          {String} locale
@@ -305,26 +320,28 @@ function DatePicker(context) {
     this.components = {
       month: new Spinner({
         id: "spinner-month",
         setValue: month => {
           this.state.isMonthSet = true;
           options.setMonth(month);
         },
         getDisplayString: options.getMonthString,
-        viewportSize: spinnerSize
+        viewportSize: spinnerSize,
+        rootFontSize: options.rootFontSize,
       }, context.monthYearView),
       year: new Spinner({
         id: "spinner-year",
         setValue: year => {
           this.state.isYearSet = true;
           options.setYear(year);
         },
         getDisplayString: year => yearFormat(new Date(new Date(0).setUTCFullYear(year))),
-        viewportSize: spinnerSize
+        viewportSize: spinnerSize,
+        rootFontSize: options.rootFontSize,
       }, context.monthYearView)
     };
 
     this._attachEventListeners();
   }
 
   MonthYear.prototype = {
 
@@ -345,23 +362,25 @@ function DatePicker(context) {
 
       if (props.isVisible) {
         this.context.monthYear.classList.add("active");
         this.components.month.setState({
           value: props.dateObj.getUTCMonth(),
           items: props.months,
           isInfiniteScroll: true,
           isValueSet: this.state.isMonthSet,
+          rootFontSize: props.rootFontSize,
           smoothScroll: !this.state.firstOpened
         });
         this.components.year.setState({
           value: props.dateObj.getUTCFullYear(),
           items: props.years,
           isInfiniteScroll: false,
           isValueSet: this.state.isYearSet,
+          rootFontSize: props.rootFontSize,
           smoothScroll: !this.state.firstOpened
         });
         this.state.firstOpened = false;
       } else {
         this.context.monthYear.classList.remove("active");
         this.state.isMonthSet = false;
         this.state.isYearSet = false;
         this.state.firstOpened = true;
--- a/toolkit/content/widgets/datetimepopup.xml
+++ b/toolkit/content/widgets/datetimepopup.xml
@@ -21,35 +21,37 @@
           if (!frame) {
             frame = this.ownerDocument.createElement("iframe");
             frame.id = "dateTimePopupFrame";
             this.appendChild(frame);
           }
           return frame;
         </getter>
       </property>
+      <field name="zoom">1</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>
       <constructor><![CDATA[
         this.mozIntl = Components.classes["@mozilla.org/mozintl;1"]
                                  .getService(Components.interfaces.mozIMozIntl);
         // Notify DateTimePickerHelper.jsm that binding is ready.
         this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady"));
       ]]></constructor>
       <method name="openPicker">
         <parameter name="type"/>
         <parameter name="anchor"/>
         <parameter name="detail"/>
+        <parameter name="zoom"/>
         <body><![CDATA[
           this.type = type;
           this.pickerState = {};
-          // TODO: Resize picker according to content zoom level
-          this.style.fontSize = "10px";
+          this.zoom = zoom;
+          this.style.fontSize = zoom * 10 + "px";
           switch (type) {
             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;
@@ -171,16 +173,17 @@
                   month: month == undefined ? undefined : month - 1,
                   day,
                   firstDayOfWeek,
                   weekends,
                   monthStrings,
                   weekdayStrings,
                   locale,
                   dir,
+                  zoom: this.zoom,
                   min: detail.min,
                   max: detail.max,
                   step: detail.step,
                   stepBase: detail.stepBase,
                 }
               });
               break;
             }
@@ -209,16 +212,29 @@
             }
             case "date": {
               this.sendPickerValueChanged(this.pickerState);
               break;
             }
           }
         ]]></body>
       </method>
+      <method name="setZoom">
+        <parameter name="zoom"/>
+        <body><![CDATA[
+          this.zoom = zoom;
+          this.style.fontSize = zoom * 10 + "px";
+          this.postMessageToPicker({
+            name: "PickerZoom",
+            detail: {
+              zoom,
+            }
+          });
+        ]]></body>
+      </method>
       <method name="sendPickerValueChanged">
         <parameter name="value"/>
         <body><![CDATA[
           switch (this.type) {
             case "time": {
               this.dispatchEvent(new CustomEvent("DateTimePickerValueChanged", {
                 detail: {
                   hour: value.hour,
--- a/toolkit/content/widgets/spinner.js
+++ b/toolkit/content/widgets/spinner.js
@@ -48,20 +48,21 @@ function Spinner(props, context) {
       const spinnerElement = document.importNode(spinnerTemplate.content, true);
 
       // Make sure viewportSize is an odd number because we want to have the selected
       // item in the center. If it's an even number, use the default size instead.
       const viewportSize = props.viewportSize % 2 ? props.viewportSize : VIEWPORT_SIZE;
 
       this.state = {
         items: [],
-        isScrolling: false
+        isScrolling: false,
+        rootFontSize,
       };
       this.props = {
-        setValue, getDisplayString, viewportSize, rootFontSize,
+        setValue, getDisplayString, viewportSize,
         // We can assume that the viewportSize is an odd number. Calculate how many
         // items we need to insert on top of the spinner so that the selected is at
         // the center. Ex: if viewportSize is 5, we need 2 items on top.
         viewportTopOffset: (viewportSize - 1) / 2
       };
       this.elements = {
         container: spinnerElement.querySelector(".spinner-container"),
         spinner: spinnerElement.querySelector(".spinner"),
@@ -358,17 +359,17 @@ function Spinner(props, context) {
     },
 
     /**
      * Find the index by offset
      * @param {Number} offset: Offset value in pixel.
      * @return {Number}  Index number
      */
     _getIndexByOffset(offset) {
-      return Math.round(offset / (ITEM_HEIGHT * this.props.rootFontSize));
+      return Math.round(offset / (ITEM_HEIGHT * this.state.rootFontSize));
     },
 
     /**
      * Find the index of a value that is the closest to the current position.
      * If centering is true, find the index closest to the center.
      *
      * @param {Number/String} value: The value to find
      * @param {Boolean} centering: Whether or not to find the value closest to center
@@ -413,17 +414,17 @@ function Spinner(props, context) {
      * @param  {Number/String} value: Value to scroll to
      * @param  {Boolean} centering: Whether or not to scroll to center location
      */
     _scrollTo(value, centering) {
       const index = this._getScrollIndex(value, centering);
       // Do nothing if the value is not found
       if (index > -1) {
         this.state.index = index;
-        this.elements.spinner.scrollTop = this.state.index * ITEM_HEIGHT * this.props.rootFontSize;
+        this.elements.spinner.scrollTop = this.state.index * ITEM_HEIGHT * this.state.rootFontSize;
       }
     },
 
     /**
      * Smooth scroll to a value.
      *
      * @param  {Number/String} value: Value to scroll to
      */
--- a/toolkit/modules/DateTimePickerHelper.jsm
+++ b/toolkit/modules/DateTimePickerHelper.jsm
@@ -30,17 +30,18 @@ Cu.import("resource://gre/modules/Servic
  */
 this.DateTimePickerHelper = {
   picker: null,
   weakBrowser: null,
 
   MESSAGES: [
     "FormDateTime:OpenPicker",
     "FormDateTime:ClosePicker",
-    "FormDateTime:UpdatePicker"
+    "FormDateTime:UpdatePicker",
+    "FormDateTime:SetZoom",
   ],
 
   init() {
     for (let msg of this.MESSAGES) {
       Services.mm.addMessageListener(msg, this);
     }
   },
 
@@ -67,16 +68,21 @@ this.DateTimePickerHelper = {
       }
       case "FormDateTime:UpdatePicker": {
         if (!this.picker) {
           return;
         }
         this.picker.setPopupValue(aMessage.data);
         break;
       }
+      case "FormDateTime:SetZoom": {
+        debug("zoom: " + this.getZoom(aMessage.target));
+        this.picker.setZoom(this.getZoom(aMessage.target));
+        break;
+      }
       default:
         break;
     }
   },
 
   // nsIDOMEventListener
   handleEvent(aEvent) {
     debug("handleEvent: " + aEvent.type);
@@ -85,16 +91,17 @@ this.DateTimePickerHelper = {
         this.updateInputBoxValue(aEvent);
         break;
       }
       case "popuphidden": {
         let browser = this.weakBrowser ? this.weakBrowser.get() : null;
         if (browser) {
           browser.messageManager.sendAsyncMessage("FormDateTime:PickerClosed");
         }
+        this.picker.closePicker();
         this.close();
         break;
       }
       default:
         break;
     }
   },
 
@@ -145,21 +152,25 @@ this.DateTimePickerHelper = {
         this.picker.addEventListener("DateTimePickerBindingReady",
                                      resolve, {once: true});
       });
       this.picker.setAttribute("active", true);
       await bindingPromise;
     }
     // The arrow panel needs an anchor to work. The popupAnchor (this._anchor)
     // is a transparent div that the arrow can point to.
-    this.picker.openPicker(type, this._anchor, detail);
+    this.picker.openPicker(type, this._anchor, detail, this.getZoom(aBrowser));
 
     this.addPickerListeners();
   },
 
+  getZoom(browser) {
+    return Services.prefs.getBoolPref("browser.zoom.full") ? browser.fullZoom : browser.textZoom;
+  },
+
   // Picker is closed, do some cleanup.
   close() {
     this.removePickerListeners();
     this.picker = null;
     this.weakBrowser = null;
     this._anchor.hidden = true;
   },