<?xml version="1.0"?>

<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

<!DOCTYPE bindings [
<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
%datetimeboxDTD;
]>

<bindings id="datetimeboxBindings"
   xmlns="http://www.mozilla.org/xbl"
   xmlns:html="http://www.w3.org/1999/xhtml"
   xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
   xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="date-input"
           extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
    <resources>
      <stylesheet src="chrome://global/content/textbox.css"/>
      <stylesheet src="chrome://global/skin/textbox.css"/>
      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
    </resources>

    <implementation>
      <constructor>
      <![CDATA[
        /* eslint-disable no-multi-spaces */
        this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[;
        this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[;
        this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[;

        this.mYearLabel = ]]>"&date.year.label;"<![CDATA[;
        this.mMonthLabel = ]]>"&date.month.label;"<![CDATA[;
        this.mDayLabel = ]]>"&date.day.label;"<![CDATA[;
        /* eslint-enable no-multi-spaces */

        this.mMinMonth = 1;
        this.mMaxMonth = 12;
        this.mMinDay = 1;
        this.mMaxDay = 31;
        this.mMinYear = 1;
        // Maximum year limited by ECMAScript date object range, year <= 275760.
        this.mMaxYear = 275760;
        this.mMonthDayLength = 2;
        this.mYearLength = 4;
        this.mMonthPageUpDownInterval = 3;
        this.mDayPageUpDownInterval = 7;
        this.mYearPageUpDownInterval = 10;

        this.buildEditFields();

        if (this.mInputElement.value) {
          this.setFieldsFromInputValue();
        }
      ]]>
      </constructor>

      <method name="buildEditFields">
        <body>
        <![CDATA[
          const HTML_NS = "http://www.w3.org/1999/xhtml";
          let root =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");

          let yearMaxLength = this.mMaxYear.toString().length;
          this.mYearField = this.createEditField(this.mYearPlaceHolder,
            this.mYearLabel, true, this.mYearLength, yearMaxLength,
            this.mMinYear, this.mMaxYear, this.mYearPageUpDownInterval);
          this.mMonthField = this.createEditField(this.mMonthPlaceHolder,
            this.mMonthLabel, true, this.mMonthDayLength, this.mMonthDayLength,
            this.mMinMonth, this.mMaxMonth, this.mMonthPageUpDownInterval);
          this.mDayField = this.createEditField(this.mDayPlaceHolder,
            this.mDayLabel, true, this.mMonthDayLength, this.mMonthDayLength,
            this.mMinDay, this.mMaxDay, this.mDayPageUpDownInterval);

          let fragment = document.createDocumentFragment();
          let formatter = Intl.DateTimeFormat(this.mLocales, {
            year: "numeric",
            month: "numeric",
            day: "numeric"
          });
          formatter.formatToParts(Date.now()).map(part => {
            switch (part.type) {
              case "year":
                fragment.appendChild(this.mYearField);
                break;
              case "month":
                fragment.appendChild(this.mMonthField);
                break;
              case "day":
                fragment.appendChild(this.mDayField);
                break;
              default:
                let span = document.createElementNS(HTML_NS, "span");
                span.textContent = part.value;
                fragment.appendChild(span);
                break;
            }
          });

          root.appendChild(fragment);
        ]]>
        </body>
      </method>

      <method name="clearInputFields">
        <parameter name="aFromInputElement"/>
        <body>
        <![CDATA[
          this.log("clearInputFields");

          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          if (this.mMonthField && !this.mMonthField.disabled &&
              !this.mMonthField.readOnly) {
            this.clearFieldValue(this.mMonthField);
          }

          if (this.mDayField && !this.mDayField.disabled &&
              !this.mDayField.readOnly) {
            this.clearFieldValue(this.mDayField);
          }

          if (this.mYearField && !this.mYearField.disabled &&
              !this.mYearField.readOnly) {
            this.clearFieldValue(this.mYearField);
          }

          if (!aFromInputElement) {
            if (this.mInputElement.value) {
              this.mInputElement.setUserInput("");
            } else {
              this.mInputElement.updateValidityState();
            }
          }
        ]]>
        </body>
      </method>

      <method name="setFieldsFromInputValue">
        <body>
        <![CDATA[
          let value = this.mInputElement.value;
          if (!value) {
            this.clearInputFields(true);
            return;
          }

          this.log("setFieldsFromInputValue: " + value);
          let [year, month, day] = value.split("-");

          this.setFieldValue(this.mYearField, year);
          this.setFieldValue(this.mMonthField, month);
          this.setFieldValue(this.mDayField, day);

          this.notifyPicker();
        ]]>
        </body>
      </method>

      <method name="setInputValueFromFields">
        <body>
        <![CDATA[
          if (this.isAnyFieldEmpty()) {
            // Clear input element's value if any of the field has been cleared,
            // otherwise update the validity state, since it may become "not"
            // invalid if fields are not complete.
            if (this.mInputElement.value) {
              this.mInputElement.setUserInput("");
            } else {
              this.mInputElement.updateValidityState();
            }
            // We still need to notify picker in case any of the field has
            // changed.
            this.notifyPicker();
            return;
          }

          let { year, month, day } = this.getCurrentValue();

          // Convert to a valid date string according to:
          // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-date-string
          year = year.toString().padStart(this.mYearLength, "0");
          month = (month < 10) ? ("0" + month) : month;
          day = (day < 10) ? ("0" + day) : day;

          let date = [year, month, day].join("-");

          if (date == this.mInputElement.value) {
            return;
          }

          this.log("setInputValueFromFields: " + date);
          this.notifyPicker();
          this.mInputElement.setUserInput(date);
        ]]>
        </body>
      </method>

      <method name="setFieldsFromPicker">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          let year = aValue.year;
          let month = aValue.month;
          let day = aValue.day;

          if (!this.isEmpty(year)) {
            this.setFieldValue(this.mYearField, year);
          }

          if (!this.isEmpty(month)) {
            this.setFieldValue(this.mMonthField, month);
          }

          if (!this.isEmpty(day)) {
            this.setFieldValue(this.mDayField, day);
          }

          // Update input element's .value if needed.
          this.setInputValueFromFields();
        ]]>
        </body>
      </method>

      <method name="handleKeypress">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          let targetField = aEvent.originalTarget;
          let key = aEvent.key;

          if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
            let buffer = targetField.getAttribute("typeBuffer") || "";

            buffer = buffer.concat(key);
            this.setFieldValue(targetField, buffer);

            let n = Number(buffer);
            let max = targetField.getAttribute("max");
            let maxLength = targetField.getAttribute("maxlength");
            if (buffer.length >= maxLength || n * 10 > max) {
              buffer = "";
              this.advanceToNextField();
            }
            targetField.setAttribute("typeBuffer", buffer);
          }
        ]]>
        </body>
      </method>

      <method name="incrementFieldValue">
        <parameter name="aTargetField"/>
        <parameter name="aTimes"/>
        <body>
        <![CDATA[
          let value = this.getFieldValue(aTargetField);

          // Use current date if field is empty.
          if (this.isEmpty(value)) {
            let now = new Date();

            if (aTargetField == this.mYearField) {
              value = now.getFullYear();
            } else if (aTargetField == this.mMonthField) {
              value = now.getMonth() + 1;
            } else if (aTargetField == this.mDayField) {
              value = now.getDate();
            } else {
              this.log("Field not supported in incrementFieldValue.");
              return;
            }
          }

          let min = Number(aTargetField.getAttribute("min"));
          let max = Number(aTargetField.getAttribute("max"));

          value += Number(aTimes);
          if (value > max) {
            value -= (max - min + 1);
          } else if (value < min) {
            value += (max - min + 1);
          }

          this.setFieldValue(aTargetField, value);
        ]]>
        </body>
      </method>

      <method name="handleKeyboardNav">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          let targetField = aEvent.originalTarget;
          let key = aEvent.key;

          // Home/End key does nothing on year field.
          if (targetField == this.mYearField && (key == "Home" ||
                                                 key == "End")) {
            return;
          }

          switch (key) {
            case "ArrowUp":
              this.incrementFieldValue(targetField, 1);
              break;
            case "ArrowDown":
              this.incrementFieldValue(targetField, -1);
              break;
            case "PageUp": {
              let interval = targetField.getAttribute("pginterval");
              this.incrementFieldValue(targetField, interval);
              break;
            }
            case "PageDown": {
              let interval = targetField.getAttribute("pginterval");
              this.incrementFieldValue(targetField, 0 - interval);
              break;
            }
            case "Home":
              let min = targetField.getAttribute("min");
              this.setFieldValue(targetField, min);
              break;
            case "End":
              let max = targetField.getAttribute("max");
              this.setFieldValue(targetField, max);
              break;
          }
          this.setInputValueFromFields();
        ]]>
        </body>
      </method>

      <method name="getCurrentValue">
        <body>
        <![CDATA[
          let year = this.getFieldValue(this.mYearField);
          let month = this.getFieldValue(this.mMonthField);
          let day = this.getFieldValue(this.mDayField);

          let date = { year, month, day };

          this.log("getCurrentValue: " + JSON.stringify(date));
          return date;
        ]]>
        </body>
      </method>

      <method name="setFieldValue">
       <parameter name="aField"/>
       <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (!aField || !aField.classList.contains("numeric")) {
            return;
          }

          let value = Number(aValue);
          if (isNaN(value)) {
            this.log("NaN on setFieldValue!");
            return;
          }

          let maxLength = aField.getAttribute("maxlength");
          if (aValue.length == maxLength) {
            let min = Number(aField.getAttribute("min"));
            let max = Number(aField.getAttribute("max"));

            if (value < min) {
              value = min;
            } else if (value > max) {
              value = max;
            }
          }

          aField.setAttribute("rawValue", value);

          // Display formatted value based on locale.
          let minDigits = aField.getAttribute("mindigits");
          let formatted = value.toLocaleString(this.mLocales, {
            minimumIntegerDigits: minDigits,
            useGrouping: false
          });

          aField.textContent = formatted;
          aField.setAttribute("aria-valuetext", formatted);
          this.updateResetButtonVisibility();
        ]]>
        </body>
      </method>

      <method name="isAnyFieldAvailable">
        <parameter name="aForPicker"/>
        <body>
        <![CDATA[
          let { year, month, day } = this.getCurrentValue();

          return !this.isEmpty(year) || !this.isEmpty(month) ||
                 !this.isEmpty(day);
        ]]>
        </body>
      </method>

      <method name="isAnyFieldEmpty">
        <body>
        <![CDATA[
          let { year, month, day } = this.getCurrentValue();

          return (this.isEmpty(year) || this.isEmpty(month) ||
                  this.isEmpty(day));
        ]]>
        </body>
      </method>

    </implementation>
  </binding>

  <binding id="time-input"
           extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
    <resources>
      <stylesheet src="chrome://global/content/textbox.css"/>
      <stylesheet src="chrome://global/skin/textbox.css"/>
      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
    </resources>

    <implementation>
      <property name="kMsPerSecond" readonly="true" onget="return 1000;" />
      <property name="kMsPerMinute" readonly="true" onget="return (60 * 1000);" />

      <constructor>
      <![CDATA[
        const kDefaultAMString = "AM";
        const kDefaultPMString = "PM";

        let { amString, pmString } =
          this.getStringsForLocale(this.mLocales);

        this.mAMIndicator = amString || kDefaultAMString;
        this.mPMIndicator = pmString || kDefaultPMString;

        /* eslint-disable no-multi-spaces */
        this.mHourPlaceHolder = ]]>"&time.hour.placeholder;"<![CDATA[;
        this.mMinutePlaceHolder = ]]>"&time.minute.placeholder;"<![CDATA[;
        this.mSecondPlaceHolder = ]]>"&time.second.placeholder;"<![CDATA[;
        this.mMillisecPlaceHolder = ]]>"&time.millisecond.placeholder;"<![CDATA[;
        this.mDayPeriodPlaceHolder = ]]>"&time.dayperiod.placeholder;"<![CDATA[;

        this.mHourLabel = ]]>"&time.hour.label;"<![CDATA[;
        this.mMinuteLabel = ]]>"&time.minute.label;"<![CDATA[;
        this.mSecondLabel = ]]>"&time.second.label;"<![CDATA[;
        this.mMillisecLabel = ]]>"&time.millisecond.label;"<![CDATA[;
        this.mDayPeriodLabel = ]]>"&time.dayperiod.label;"<![CDATA[;
        /* eslint-enable no-multi-spaces */

        this.mHour12 = this.is12HourTime(this.mLocales);
        this.mMillisecSeparatorText = ".";
        this.mMaxLength = 2;
        this.mMillisecMaxLength = 3;
        this.mDefaultStep = 60 * 1000; // in milliseconds

        this.mMinHour = this.mHour12 ? 1 : 0;
        this.mMaxHour = this.mHour12 ? 12 : 23;
        this.mMinMinute = 0;
        this.mMaxMinute = 59;
        this.mMinSecond = 0;
        this.mMaxSecond = 59;
        this.mMinMillisecond = 0;
        this.mMaxMillisecond = 999;

        this.mHourPageUpDownInterval = 3;
        this.mMinSecPageUpDownInterval = 10;

        this.buildEditFields();

        if (this.mInputElement.value) {
          this.setFieldsFromInputValue();
        }
        ]]>
      </constructor>

      <method name="getInputElementValues">
        <body>
        <![CDATA[
          let value = this.mInputElement.value;
          if (value.length === 0) {
            return {};
          }

          let hour, minute, second, millisecond;
          [hour, minute, second] = value.split(":");
          if (second) {
            [second, millisecond] = second.split(".");

            // Convert fraction of second to milliseconds.
            if (millisecond && millisecond.length === 1) {
              millisecond *= 100;
            } else if (millisecond && millisecond.length === 2) {
              millisecond *= 10;
            }
          }

          return { hour, minute, second, millisecond };
        ]]>
        </body>
      </method>

      <method name="hasSecondField">
        <body>
        <![CDATA[
          return !!this.mSecondField;
        ]]>
        </body>
      </method>

      <method name="hasMillisecField">
        <body>
        <![CDATA[
          return !!this.mMillisecField;
        ]]>
        </body>
      </method>

      <method name="hasDayPeriodField">
        <body>
        <![CDATA[
          return !!this.mDayPeriodField;
        ]]>
        </body>
      </method>

      <method name="shouldShowSecondField">
        <body>
        <![CDATA[
          let { second } = this.getInputElementValues();
          if (second != undefined) {
            return true;
          }

          let stepBase = this.mInputElement.getStepBase();
          if ((stepBase % this.kMsPerMinute) != 0) {
            return true;
          }

          let step = this.mInputElement.getStep();
          if ((step % this.kMsPerMinute) != 0) {
            return true;
          }

          return false;
        ]]>
        </body>
      </method>

      <method name="shouldShowMillisecField">
        <body>
        <![CDATA[
          let { millisecond } = this.getInputElementValues();
          if (millisecond != undefined) {
            return true;
          }

          let stepBase = this.mInputElement.getStepBase();
          if ((stepBase % this.kMsPerSecond) != 0) {
            return true;
          }

          let step = this.mInputElement.getStep();
          if ((step % this.kMsPerSecond) != 0) {
            return true;
          }

          return false;
        ]]>
        </body>
      </method>

      <method name="rebuildEditFieldsIfNeeded">
        <body>
        <![CDATA[
          if ((this.shouldShowSecondField() == this.hasSecondField()) &&
              (this.shouldShowMillisecField() == this.hasMillisecField())) {
            return;
          }

          let root =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
          while (root.firstChild) {
            root.firstChild.remove();
          }

          this.mHourField = null;
          this.mMinuteField = null;
          this.mSecondField = null;
          this.mMillisecField = null;

          this.buildEditFields();
        ]]>
        </body>
      </method>

      <method name="buildEditFields">
        <body>
        <![CDATA[
          const HTML_NS = "http://www.w3.org/1999/xhtml";
          let root =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");

          let options = {
            hour: "numeric",
            minute: "numeric",
            hour12: this.mHour12
          };

          this.mHourField = this.createEditField(this.mHourPlaceHolder,
            this.mHourLabel, true, this.mMaxLength, this.mMaxLength,
            this.mMinHour, this.mMaxHour, this.mHourPageUpDownInterval);
          this.mMinuteField = this.createEditField(this.mMinutePlaceHolder,
            this.mMinuteLabel, true, this.mMaxLength, this.mMaxLength,
            this.mMinMinute, this.mMaxMinute, this.mMinSecPageUpDownInterval);

          if (this.mHour12) {
            this.mDayPeriodField = this.createEditField(
              this.mDayPeriodPlaceHolder, this.mDayPeriodLabel, false);

            // Give aria autocomplete hint for am/pm
            this.mDayPeriodField.setAttribute("aria-autocomplete", "inline");
          }

          if (this.shouldShowSecondField()) {
            options.second = "numeric";
            this.mSecondField = this.createEditField(this.mSecondPlaceHolder,
              this.mSecondLabel, true, this.mMaxLength, this.mMaxLength,
              this.mMinSecond, this.mMaxSecond, this.mMinSecPageUpDownInterval);

            if (this.shouldShowMillisecField()) {
              this.mMillisecField = this.createEditField(
                this.mMillisecPlaceHolder, this.mMillisecLabel, true,
                this.mMillisecMaxLength, this.mMillisecMaxLength,
                this.mMinMillisecond, this.mMaxMillisecond,
                this.mMinSecPageUpDownInterval);
            }
          }

          let fragment = document.createDocumentFragment();
          let formatter = Intl.DateTimeFormat(this.mLocales, options);
          formatter.formatToParts(Date.now()).map(part => {
            switch (part.type) {
              case "hour":
                fragment.appendChild(this.mHourField);
                break;
              case "minute":
                fragment.appendChild(this.mMinuteField);
                break;
              case "second":
                fragment.appendChild(this.mSecondField);
                if (this.shouldShowMillisecField()) {
                  // Intl.DateTimeFormat does not support millisecond, so we
                  // need to handle this on our own.
                  let span = document.createElementNS(HTML_NS, "span");
                  span.textContent = this.mMillisecSeparatorText;
                  fragment.appendChild(span);
                  fragment.appendChild(this.mMillisecField);
                }
                break;
              case "dayPeriod":
                fragment.appendChild(this.mDayPeriodField);
                break;
              default:
                let span = document.createElementNS(HTML_NS, "span");
                span.textContent = part.value;
                fragment.appendChild(span);
                break;
            }
          });

          root.appendChild(fragment);
        ]]>
        </body>
      </method>

      <method name="getStringsForLocale">
        <parameter name="aLocales"/>
        <body>
        <![CDATA[
          this.log("getStringsForLocale: " + aLocales);

          let intlUtils = window.intlUtils;
          if (!intlUtils) {
            return {};
          }

          let amString, pmString;
          let keys = [ "dates/gregorian/dayperiods/am",
                       "dates/gregorian/dayperiods/pm" ];

          let result = intlUtils.getDisplayNames(this.mLocales, {
            style: "short",
            keys
          });

          [ amString, pmString ] = keys.map(key => result.values[key]);

          return { amString, pmString };
        ]]>
        </body>
      </method>

      <method name="is12HourTime">
        <parameter name="aLocales"/>
          <body>
          <![CDATA[
            let options = (new Intl.DateTimeFormat(aLocales, {
              hour: "numeric"
            })).resolvedOptions();

            return options.hour12;
          ]]>
          </body>
      </method>

      <method name="setFieldsFromInputValue">
        <body>
        <![CDATA[
          let { hour, minute, second, millisecond } =
            this.getInputElementValues();

          if (this.isEmpty(hour) && this.isEmpty(minute)) {
            this.clearInputFields(true);
            return;
          }

          // Second and millisecond part are optional, rebuild edit fields if
          // needed.
          this.rebuildEditFieldsIfNeeded();

          this.setFieldValue(this.mHourField, hour);
          this.setFieldValue(this.mMinuteField, minute);
          if (this.mHour12) {
            this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
                                                         : this.mAMIndicator);
          }

          if (this.hasSecondField()) {
            this.setFieldValue(this.mSecondField,
              (second != undefined) ? second : 0);
          }

          if (this.hasMillisecField()) {
            this.setFieldValue(this.mMillisecField,
              (millisecond != undefined) ? millisecond : 0);
          }

          this.notifyPicker();
        ]]>
        </body>
      </method>

      <method name="setInputValueFromFields">
        <body>
        <![CDATA[
          if (this.isAnyFieldEmpty()) {
            // Clear input element's value if any of the field has been cleared,
            // otherwise update the validity state, since it may become "not"
            // invalid if fields are not complete.
            if (this.mInputElement.value) {
              this.mInputElement.setUserInput("");
            } else {
              this.mInputElement.updateValidityState();
            }
            // We still need to notify picker in case any of the field has
            // changed.
            this.notifyPicker();
            return;
          }

          let { hour, minute, second, millisecond } = this.getCurrentValue();
          let dayPeriod = this.getDayPeriodValue();

          // Convert to a valid time string according to:
          // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string
          if (this.mHour12) {
            if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
              hour += this.mMaxHour;
            } else if (dayPeriod == this.mAMIndicator &&
                       hour == this.mMaxHour) {
              hour = 0;
            }
          }

          hour = (hour < 10) ? ("0" + hour) : hour;
          minute = (minute < 10) ? ("0" + minute) : minute;

          let time = hour + ":" + minute;
          if (second != undefined) {
            second = (second < 10) ? ("0" + second) : second;
            time += ":" + second;
          }

          if (millisecond != undefined) {
            // Convert milliseconds to fraction of second.
            millisecond = millisecond.toString().padStart(
              this.mMillisecMaxLength, "0");
            time += "." + millisecond;
          }

          if (time == this.mInputElement.value) {
            return;
          }

          this.log("setInputValueFromFields: " + time);
          this.notifyPicker();
          this.mInputElement.setUserInput(time);
        ]]>
        </body>
      </method>

      <method name="setFieldsFromPicker">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          let hour = aValue.hour;
          let minute = aValue.minute;
          this.log("setFieldsFromPicker: " + hour + ":" + minute);

          if (!this.isEmpty(hour)) {
            this.setFieldValue(this.mHourField, hour);
            if (this.mHour12) {
              this.setDayPeriodValue(hour >= this.mMaxHour ? this.mPMIndicator
                                                           : this.mAMIndicator);
            }
          }

          if (!this.isEmpty(minute)) {
            this.setFieldValue(this.mMinuteField, minute);
          }

          // Update input element's .value if needed.
          this.setInputValueFromFields();
        ]]>
        </body>
       </method>

      <method name="clearInputFields">
        <parameter name="aFromInputElement"/>
        <body>
        <![CDATA[
          this.log("clearInputFields");

          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          if (this.mHourField && !this.mHourField.disabled &&
              !this.mHourField.readOnly) {
            this.clearFieldValue(this.mHourField);
          }

          if (this.mMinuteField && !this.mMinuteField.disabled &&
              !this.mMinuteField.readOnly) {
            this.clearFieldValue(this.mMinuteField);
          }

          if (this.hasSecondField() && !this.mSecondField.disabled &&
              !this.mSecondField.readOnly) {
            this.clearFieldValue(this.mSecondField);
          }

          if (this.hasMillisecField() && !this.mMillisecField.disabled &&
              !this.mMillisecField.readOnly) {
            this.clearFieldValue(this.mMillisecField);
          }

          if (this.hasDayPeriodField() && !this.mDayPeriodField.disabled &&
              !this.mDayPeriodField.readOnly) {
            this.clearFieldValue(this.mDayPeriodField);
          }

          if (!aFromInputElement) {
            if (this.mInputElement.value) {
              this.mInputElement.setUserInput("");
            } else {
              this.mInputElement.updateValidityState();
            }
          }
        ]]>
        </body>
      </method>

      <method name="notifyMinMaxStepAttrChanged">
        <body>
        <![CDATA[
          // Second and millisecond part are optional, rebuild edit fields if
          // needed.
          this.rebuildEditFieldsIfNeeded();
          // Fill in values again.
          this.setFieldsFromInputValue();
        ]]>
        </body>
      </method>

      <method name="incrementFieldValue">
        <parameter name="aTargetField"/>
        <parameter name="aTimes"/>
        <body>
        <![CDATA[
          let value = this.getFieldValue(aTargetField);

          // Use current time if field is empty.
          if (this.isEmpty(value)) {
            let now = new Date();

            if (aTargetField == this.mHourField) {
              value = now.getHours();
              if (this.mHour12) {
                value = (value % this.mMaxHour) || this.mMaxHour;
              }
            } else if (aTargetField == this.mMinuteField) {
              value = now.getMinutes();
            } else if (aTargetField == this.mSecondField) {
              value = now.getSeconds();
            } else if (aTargetField == this.mMillisecField) {
              value = now.getMilliseconds();
            } else {
              this.log("Field not supported in incrementFieldValue.");
              return;
            }
          }

          let min = aTargetField.getAttribute("min");
          let max = aTargetField.getAttribute("max");

          value += Number(aTimes);
          if (value > max) {
            value -= (max - min + 1);
          } else if (value < min) {
            value += (max - min + 1);
          }

          this.setFieldValue(aTargetField, value);
        ]]>
        </body>
      </method>

      <method name="handleKeyboardNav">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          let targetField = aEvent.originalTarget;
          let key = aEvent.key;

          if (this.hasDayPeriodField() &&
              targetField == this.mDayPeriodField) {
            // Home/End key does nothing on AM/PM field.
            if (key == "Home" || key == "End") {
              return;
            }

            this.setDayPeriodValue(
              this.getDayPeriodValue() == this.mAMIndicator ? this.mPMIndicator
                                                            : this.mAMIndicator);
            this.setInputValueFromFields();
            return;
          }

          switch (key) {
            case "ArrowUp":
              this.incrementFieldValue(targetField, 1);
              break;
            case "ArrowDown":
              this.incrementFieldValue(targetField, -1);
              break;
            case "PageUp": {
              let interval = targetField.getAttribute("pginterval");
              this.incrementFieldValue(targetField, interval);
              break;
            }
            case "PageDown": {
              let interval = targetField.getAttribute("pginterval");
              this.incrementFieldValue(targetField, 0 - interval);
              break;
            }
            case "Home":
              let min = targetField.getAttribute("min");
              this.setFieldValue(targetField, min);
              break;
            case "End":
              let max = targetField.getAttribute("max");
              this.setFieldValue(targetField, max);
              break;
          }
          this.setInputValueFromFields();
        ]]>
        </body>
      </method>

      <method name="handleKeypress">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          if (this.isDisabled() || this.isReadonly()) {
            return;
          }

          let targetField = aEvent.originalTarget;
          let key = aEvent.key;

          if (this.hasDayPeriodField() &&
              targetField == this.mDayPeriodField) {
            if (key == "a" || key == "A") {
              this.setDayPeriodValue(this.mAMIndicator);
            } else if (key == "p" || key == "P") {
              this.setDayPeriodValue(this.mPMIndicator);
            }
            return;
          }

          if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
            let buffer = targetField.getAttribute("typeBuffer") || "";

            buffer = buffer.concat(key);
            this.setFieldValue(targetField, buffer);

            let n = Number(buffer);
            let max = targetField.getAttribute("max");
            let maxLength = targetField.getAttribute("maxLength");
            if (buffer.length >= maxLength || n * 10 > max) {
              buffer = "";
              this.advanceToNextField();
            }
            targetField.setAttribute("typeBuffer", buffer);
          }
        ]]>
        </body>
      </method>

      <method name="setFieldValue">
       <parameter name="aField"/>
       <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (!aField || !aField.classList.contains("numeric")) {
            return;
          }

          let value = Number(aValue);
          if (isNaN(value)) {
            this.log("NaN on setFieldValue!");
            return;
          }

          if (aField == this.mHourField) {
            if (this.mHour12) {
              // Try to change to 12hr format if user input is 0 or greater
              // than 12.
              let maxLength = aField.getAttribute("maxlength");
              if (value == 0 && aValue.length == maxLength) {
                value = this.mMaxHour;
              } else {
                value = (value > this.mMaxHour) ? value % this.mMaxHour : value;
              }
            } else if (value > this.mMaxHour) {
              value = this.mMaxHour;
            }
          }

          aField.setAttribute("rawValue", value);

          let minDigits = aField.getAttribute("mindigits");
          let formatted = value.toLocaleString(this.mLocales, {
            minimumIntegerDigits: minDigits,
            useGrouping: false
          });

          aField.textContent = formatted;
          aField.setAttribute("aria-valuetext", formatted);
          this.updateResetButtonVisibility();
        ]]>
        </body>
      </method>

      <method name="getDayPeriodValue">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (!this.hasDayPeriodField()) {
            return "";
          }

          let placeholder = this.mDayPeriodField.placeholder;
          let value = this.mDayPeriodField.textContent;

          return (value == placeholder ? "" : value);
        ]]>
        </body>
      </method>

      <method name="setDayPeriodValue">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (!this.hasDayPeriodField()) {
            return;
          }

          this.mDayPeriodField.textContent = aValue;
          this.updateResetButtonVisibility();
        ]]>
        </body>
      </method>

      <method name="isAnyFieldAvailable">
        <parameter name="aForPicker"/>
        <body>
        <![CDATA[
          let { hour, minute, second, millisecond } = this.getCurrentValue();
          let dayPeriod = this.getDayPeriodValue();

          let available = !this.isEmpty(hour) || !this.isEmpty(minute);
          if (available) {
            return true;
          }

          // Picker only cares about hour:minute.
          if (aForPicker) {
            return false;
          }

          return (this.hasDayPeriodField() && !this.isEmpty(dayPeriod)) ||
                 (this.hasSecondField() && !this.isEmpty(second)) ||
                 (this.hasMillisecField() && !this.isEmpty(millisecond));
        ]]>
        </body>
      </method>

      <method name="isAnyFieldEmpty">
        <body>
        <![CDATA[
          let { hour, minute, second, millisecond } = this.getCurrentValue();
          let dayPeriod = this.getDayPeriodValue();

          return (this.isEmpty(hour) || this.isEmpty(minute) ||
                  (this.hasDayPeriodField() && this.isEmpty(dayPeriod)) ||
                  (this.hasSecondField() && this.isEmpty(second)) ||
                  (this.hasMillisecField() && this.isEmpty(millisecond)));
        ]]>
        </body>
      </method>

      <method name="getCurrentValue">
        <body>
        <![CDATA[
          let hour = this.getFieldValue(this.mHourField);
          if (!this.isEmpty(hour)) {
            if (this.mHour12) {
              let dayPeriod = this.getDayPeriodValue();
              if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
                hour += this.mMaxHour;
              } else if (dayPeriod == this.mAMIndicator &&
                         hour == this.mMaxHour) {
                hour = 0;
              }
            }
          }

          let minute = this.getFieldValue(this.mMinuteField);
          let second = this.getFieldValue(this.mSecondField);
          let millisecond = this.getFieldValue(this.mMillisecField);

          let time = { hour, minute, second, millisecond };

          this.log("getCurrentValue: " + JSON.stringify(time));
          return time;
        ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="datetime-input-base">
    <resources>
      <stylesheet src="chrome://global/content/textbox.css"/>
      <stylesheet src="chrome://global/skin/textbox.css"/>
      <stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
    </resources>

    <content>
      <html:div class="datetime-input-box-wrapper" anonid="input-box-wrapper"
                xbl:inherits="context,disabled,readonly" role="presentation">
        <html:span class="datetime-input-edit-wrapper"
                   anonid="edit-wrapper">
          <!-- Each of the date/time input types will append their input child
             - elements here -->
        </html:span>

        <html:button class="datetime-reset-button" anonid="reset-button"
                     tabindex="-1" xbl:inherits="disabled" aria-label="&datetime.reset.label;"/>
      </html:div>
    </content>

    <implementation implements="nsIDateTimeInputArea">
      <constructor>
      <![CDATA[
        this.DEBUG = false;
        this.mInputElement = this.parentNode;
        this.mLocales = window.getRegionalPrefsLocales();

        this.mIsRTL = false;
        let intlUtils = window.intlUtils;
        if (intlUtils) {
          this.mIsRTL =
            intlUtils.getLocaleInfo(this.mLocales).direction === "rtl";
        }

        if (this.mIsRTL) {
          let inputBoxWrapper =
            document.getAnonymousElementByAttribute(this, "anonid",
                                                    "input-box-wrapper");
          inputBoxWrapper.dir = "rtl";
        }

        this.mMin = this.mInputElement.min;
        this.mMax = this.mInputElement.max;
        this.mStep = this.mInputElement.step;
        this.mIsPickerOpen = false;

        this.mResetButton =
          document.getAnonymousElementByAttribute(this, "anonid", "reset-button");
        this.mResetButton.style.visibility = "hidden";

        this.EVENTS.forEach((eventName) => {
          this.addEventListener(eventName, this, { mozSystemGroup: true });
        });
        // Handle keypress separately since we need to catch it on capturing.
        this.addEventListener("keypress", this, {
          capture: true,
          mozSystemGroup: true
        });
        // This is to open the picker when input element is clicked (this
        // includes padding area).
        this.mInputElement.addEventListener("click", this,
                                            { mozSystemGroup: true });
      ]]>
      </constructor>

      <destructor>
      <![CDATA[
        this.EVENTS.forEach((eventName) => {
          this.removeEventListener(eventName, this, { mozSystemGroup: true });
        });
        this.removeEventListener("keypress", this, {
          capture: true,
          mozSystemGroup: true
        });
        this.mInputElement.removeEventListener("click", this,
                                               { mozSystemGroup: true });

        this.mInputElement = null;
      ]]>
      </destructor>

      <property name="EVENTS" readonly="true">
        <getter>
        <![CDATA[
          return ["focus", "blur", "copy", "cut", "paste", "mousedown"];
        ]]>
        </getter>
      </property>

      <method name="log">
        <parameter name="aMsg"/>
        <body>
        <![CDATA[
          if (this.DEBUG) {
            dump("[DateTimeBox] " + aMsg + "\n");
          }
        ]]>
        </body>
      </method>

      <method name="createEditField">
        <parameter name="aPlaceHolder"/>
        <parameter name="aLabel"/>
        <parameter name="aIsNumeric"/>
        <parameter name="aMinDigits"/>
        <parameter name="aMaxLength"/>
        <parameter name="aMinValue"/>
        <parameter name="aMaxValue"/>
        <parameter name="aPageUpDownInterval"/>
        <body>
        <![CDATA[
          const HTML_NS = "http://www.w3.org/1999/xhtml";

          let field = document.createElementNS(HTML_NS, "span");
          field.classList.add("datetime-edit-field");
          field.textContent = aPlaceHolder;
          field.placeholder = aPlaceHolder;
          field.tabIndex = this.mInputElement.tabIndex;

          field.setAttribute("readonly", this.mInputElement.readOnly);
          field.setAttribute("disabled", this.mInputElement.disabled);
          // Set property as well for convenience.
          field.disabled = this.mInputElement.disabled;
          field.readOnly = this.mInputElement.readOnly;
          field.setAttribute("aria-label", aLabel);

          if (aIsNumeric) {
            field.classList.add("numeric");
            // Maximum value allowed.
            field.setAttribute("min", aMinValue);
            // Minumim value allowed.
            field.setAttribute("max", aMaxValue);
            // Interval when pressing pageUp/pageDown key.
            field.setAttribute("pginterval", aPageUpDownInterval);
            // Used to store what the user has already typed in the field,
            // cleared when value is cleared and when field is blurred.
            field.setAttribute("typeBuffer", "");
            // Used to store the non-formatted number, clered when value is
            // cleared.
            field.setAttribute("rawValue", "");
            // Minimum digits to display, padded with leading 0s.
            field.setAttribute("mindigits", aMinDigits);
            // Maximum length for the field, will be advance to the next field
            // automatically if exceeded.
            field.setAttribute("maxlength", aMaxLength);
            // Set spinbutton ARIA role
            field.setAttribute("role", "spinbutton");

            if (this.mIsRTL) {
              // Force the direction to be "ltr", so that the field stays in the
              // same order even when it's empty (with placeholder). By using
              // "embed", the text inside the element is still displayed based
              // on its directionality.
              field.style.unicodeBidi = "embed";
              field.style.direction = "ltr";
            }
          } else {
            // Set generic textbox ARIA role
            field.setAttribute("role", "textbox");
          }

          return field;
        ]]>
        </body>
      </method>

      <method name="updateResetButtonVisibility">
        <body>
          <![CDATA[
            if (this.isAnyFieldAvailable(false)) {
              this.mResetButton.style.visibility = "visible";
            } else {
              this.mResetButton.style.visibility = "hidden";
            }
          ]]>
        </body>
      </method>

      <method name="focusInnerTextBox">
        <body>
        <![CDATA[
          this.log("Focus inner editable field.");

          let editRoot =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");

          for (let child = editRoot.firstChild; child; child = child.nextSibling) {
            if ((child instanceof HTMLSpanElement) &&
                child.classList.contains("datetime-edit-field")) {
              this.mLastFocusedField = child;
              child.focus();
              break;
            }
          }
        ]]>
        </body>
      </method>

      <method name="blurInnerTextBox">
        <body>
        <![CDATA[
          this.log("Blur inner editable field.");

          if (this.mLastFocusedField) {
            this.mLastFocusedField.blur();
          } else {
            // If .mLastFocusedField hasn't been set, blur all editable fields,
            // so that the bound element will actually be blurred. Note that
            // blurring on a element that has no focus won't have any effect.
            let editRoot =
              document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
            for (let child = editRoot.firstChild; child; child = child.nextSibling) {
              if ((child instanceof HTMLSpanElement) &&
                  child.classList.contains("datetime-edit-field")) {
                child.blur();
              }
            }
          }
        ]]>
        </body>
      </method>

      <method name="notifyInputElementValueChanged">
        <body>
        <![CDATA[
          this.log("inputElementValueChanged");
          this.setFieldsFromInputValue();
        ]]>
        </body>
      </method>

      <method name="notifyMinMaxStepAttrChanged">
        <body>
        <!-- No operation by default -->
        </body>
      </method>

      <method name="setValueFromPicker">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          this.setFieldsFromPicker(aValue);
        ]]>
        </body>
      </method>

      <method name="hasBadInput">
        <body>
        <![CDATA[
          // Incomplete field does not imply bad input.
          if (this.isAnyFieldEmpty()) {
            return false;
          }

          // All fields are available but input element's value is empty implies
          // it has been sanitized.
          if (!this.mInputElement.value) {
            return true;
          }

          return false;
        ]]>
        </body>
      </method>

      <method name="advanceToNextField">
        <parameter name="aReverse"/>
        <body>
        <![CDATA[
          this.log("advanceToNextField");

          let focusedInput = this.mLastFocusedField;
          let next = aReverse ? focusedInput.previousElementSibling
                              : focusedInput.nextElementSibling;
          if (!next && !aReverse) {
            this.setInputValueFromFields();
            return;
          }

          while (next) {
            if ((next instanceof HTMLSpanElement) &&
                next.classList.contains("datetime-edit-field")) {
              next.focus();
              break;
            }
            next = aReverse ? next.previousElementSibling
                            : next.nextElementSibling;
          }
        ]]>
        </body>
      </method>

      <method name="setPickerState">
        <parameter name="aIsOpen"/>
        <body>
        <![CDATA[
          this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
          this.mIsPickerOpen = aIsOpen;
        ]]>
        </body>
      </method>

      <method name="setEditAttribute">
        <parameter name="aName"/>
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          this.log("setAttribute: " + aName + "=" + aValue);

          if (aName != "tabindex" && aName != "disabled" &&
              aName != "readonly") {
            return;
          }

          let editRoot =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");

          for (let child = editRoot.firstChild; child; child = child.nextSibling) {
            if ((child instanceof HTMLSpanElement) &&
                child.classList.contains("datetime-edit-field")) {

              switch (aName) {
                case "tabindex":
                  child.setAttribute(aName, aValue);
                  break;
                case "disabled": {
                  let value = this.mInputElement.disabled;
                  child.setAttribute("disabled", value);
                  child.disabled = value;
                  break;
                }
                case "readonly": {
                  let value = this.mInputElement.readOnly;
                  child.setAttribute("readonly", value);
                  child.readOnly = value;
                  break;
                }
              }
            }
          }
        ]]>
        </body>
      </method>

      <method name="removeEditAttribute">
        <parameter name="aName"/>
        <body>
        <![CDATA[
          this.log("removeAttribute: " + aName);

          if (aName != "tabindex" && aName != "disabled" &&
              aName != "readonly") {
            return;
          }

          let editRoot =
            document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");

          for (let child = editRoot.firstChild; child; child = child.nextSibling) {
            if ((child instanceof HTMLSpanElement) &&
                child.classList.contains("datetime-edit-field")) {
              child.removeAttribute(aName);
              // Update property as well.
              if (aName == "readonly") {
                child.readOnly = false;
              } else if (aName == "disabled") {
                child.disabled = false;
              }
            }
          }
        ]]>
        </body>
      </method>

      <method name="isEmpty">
        <parameter name="aValue"/>
        <body>
          return (aValue == undefined || 0 === aValue.length);
        </body>
      </method>

      <method name="getFieldValue">
        <parameter name="aField"/>
        <body>
        <![CDATA[
          if (!aField || !aField.classList.contains("numeric")) {
            return undefined;
          }

          let value = aField.getAttribute("rawValue");
          // Avoid returning 0 when field is empty.
          return (this.isEmpty(value) ? undefined : Number(value));
        ]]>
        </body>
      </method>

      <method name="clearFieldValue">
        <parameter name="aField"/>
        <body>
        <![CDATA[
          aField.textContent = aField.placeholder;
          if (aField.classList.contains("numeric")) {
            aField.setAttribute("typeBuffer", "");
            aField.setAttribute("rawValue", "");
          }
          this.updateResetButtonVisibility();
        ]]>
        </body>
      </method>

      <method name="setFieldValue">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="clearInputFields">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="setFieldsFromInputValue">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="setInputValueFromFields">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="setFieldsFromPicker">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="handleKeypress">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="handleKeyboardNav">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="getCurrentValue">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="isAnyFieldAvailable">
        <body>
          throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        </body>
      </method>

      <method name="notifyPicker">
        <body>
        <![CDATA[
          if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) {
            this.mInputElement.updateDateTimePicker(this.getCurrentValue());
          }
        ]]>
        </body>
      </method>

      <method name="isDisabled">
        <body>
        <![CDATA[
          return this.mInputElement.hasAttribute("disabled");
        ]]>
        </body>
      </method>

      <method name="isReadonly">
        <body>
        <![CDATA[
          return this.mInputElement.hasAttribute("readonly");
        ]]>
        </body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          this.log("handleEvent: " + aEvent.type);

          switch (aEvent.type) {
            case "keypress": {
              this.onKeyPress(aEvent);
              break;
            }
            case "click": {
              this.onClick(aEvent);
              break;
            }
            case "focus": {
              this.onFocus(aEvent);
              break;
            }
            case "blur": {
              this.onBlur(aEvent);
              break;
            }
            case "mousedown": {
              if (aEvent.originalTarget == this.mResetButton) {
                aEvent.preventDefault();
              }
              break;
            }
            case "copy":
            case "cut":
            case "paste": {
              aEvent.preventDefault();
              break;
            }
            default:
              break;
          }
        ]]>
        </body>
      </method>

      <method name="onFocus">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          this.log("onFocus originalTarget: " + aEvent.originalTarget);

          if (document.activeElement != this.mInputElement) {
            return;
          }

          let target = aEvent.originalTarget;
          if ((target instanceof HTMLSpanElement) &&
              target.classList.contains("datetime-edit-field")) {
            if (target.disabled) {
              return;
            }
            this.mLastFocusedField = target;
            this.mInputElement.setFocusState(true);
          }
        ]]>
        </body>
      </method>

      <method name="onBlur">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          this.log("onBlur originalTarget: " + aEvent.originalTarget +
            " target: " + aEvent.target);

          let target = aEvent.originalTarget;
          target.setAttribute("typeBuffer", "");
          this.setInputValueFromFields();
          this.mInputElement.setFocusState(false);
        ]]>
        </body>
      </method>

      <method name="onKeyPress">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          this.log("onKeyPress key: " + aEvent.key);

          switch (aEvent.key) {
            // Close picker on Enter, Escape or Space key.
            case "Enter":
            case "Escape":
            case " ": {
              if (this.mIsPickerOpen) {
                this.mInputElement.closeDateTimePicker();
                aEvent.preventDefault();
              }
              break;
            }
            case "Backspace": {
              let targetField = aEvent.originalTarget;
              this.clearFieldValue(targetField);
              this.setInputValueFromFields();
              aEvent.preventDefault();
              break;
            }
            case "ArrowRight":
            case "ArrowLeft": {
              this.advanceToNextField(!(aEvent.key == "ArrowRight"));
              aEvent.preventDefault();
              break;
            }
            case "ArrowUp":
            case "ArrowDown":
            case "PageUp":
            case "PageDown":
            case "Home":
            case "End": {
              this.handleKeyboardNav(aEvent);
              aEvent.preventDefault();
              break;
            }
            default: {
              // printable characters
              if (aEvent.keyCode == 0 &&
                  !(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)) {
                this.handleKeypress(aEvent);
                aEvent.preventDefault();
              }
              break;
            }
          }
        ]]>
        </body>
      </method>

      <method name="onClick">
        <parameter name="aEvent"/>
        <body>
        <![CDATA[
          this.log("onClick originalTarget: " + aEvent.originalTarget +
                   " target: " + aEvent.target);

          if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
            return;
          }

          if (aEvent.originalTarget == this.mResetButton) {
            this.clearInputFields(false);
          } else if (!this.mIsPickerOpen) {
            this.mInputElement.openDateTimePicker(this.getCurrentValue());
          }
        ]]>
        </body>
      </method>
    </implementation>
  </binding>

</bindings>
