<?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/. -->

<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="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>
      <constructor>
      <![CDATA[
        // TODO: Bug 1301312 - localization for input type=time input.
        this.mHour12 = true;
        this.mAMIndicator = "AM";
        this.mPMIndicator = "PM";
        this.mPlaceHolder = "--";
        this.mSeparatorText = ":";
        this.mMillisecSeparatorText = ".";
        this.mMaxLength = 2;
        this.mMillisecMaxLength = 3;
        this.mDefaultStep = 60 * 1000; // in milliseconds

        this.mMinHourInHour12 = 1;
        this.mMaxHourInHour12 = 12;
        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.mHourField =
          document.getAnonymousElementByAttribute(this, "anonid", "input-one");
        this.mHourField.setAttribute("typeBuffer", "");
        this.mMinuteField =
          document.getAnonymousElementByAttribute(this, "anonid", "input-two");
        this.mMinuteField.setAttribute("typeBuffer", "");
        this.mDayPeriodField =
          document.getAnonymousElementByAttribute(this, "anonid", "input-three");
        this.mDayPeriodField.classList.remove("numeric");

        this.mHourField.placeholder = this.mPlaceHolder;
        this.mMinuteField.placeholder = this.mPlaceHolder;
        this.mDayPeriodField.placeholder = this.mPlaceHolder;

        this.mHourField.setAttribute("min", this.mMinHourInHour12);
        this.mHourField.setAttribute("max", this.mMaxHourInHour12);
        this.mMinuteField.setAttribute("min", this.mMinMinute);
        this.mMinuteField.setAttribute("max", this.mMaxMinute);

        this.mMinuteSeparator =
           document.getAnonymousElementByAttribute(this, "anonid", "sep-first");
        this.mMinuteSeparator.textContent = this.mSeparatorText;
        this.mSpaceSeparator =
          document.getAnonymousElementByAttribute(this, "anonid", "sep-second");
        // space between time and am/pm field
        this.mSpaceSeparator.textContent = " ";

        this.mSecondSeparator = null;
        this.mSecondField = null;
        this.mMillisecSeparator = null;
        this.mMillisecField = null;

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

      <method name="insertSeparator">
        <parameter name="aSeparatorText"/>
        <body>
        <![CDATA[
          let container = this.mHourField.parentNode;
          const HTML_NS = "http://www.w3.org/1999/xhtml";

          let separator = document.createElementNS(HTML_NS, "span");
          separator.textContent = aSeparatorText;
          separator.setAttribute("class", "datetime-separator");
          container.insertBefore(separator, this.mSpaceSeparator);

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

      <method name="insertAdditionalField">
        <parameter name="aPlaceHolder"/>
        <parameter name="aMin"/>
        <parameter name="aMax"/>
        <parameter name="aSize"/>
        <parameter name="aMaxLength"/>
        <body>
        <![CDATA[
          let container = this.mHourField.parentNode;
          const HTML_NS = "http://www.w3.org/1999/xhtml";

          let field = document.createElementNS(HTML_NS, "input");
          field.classList.add("textbox-input", "datetime-input", "numeric");
          field.setAttribute("size", aSize);
          field.setAttribute("maxlength", aMaxLength);
          field.setAttribute("min", aMin);
          field.setAttribute("max", aMax);
          field.setAttribute("typeBuffer", "");
          field.disabled = this.mInputElement.disabled;
          field.readOnly = this.mInputElement.readOnly;
          field.tabIndex = this.mInputElement.tabIndex;
          field.placeholder = aPlaceHolder;
          container.insertBefore(field, this.mSpaceSeparator);

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

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

          this.log("setFieldsFromInputValue: " + value);
          let [hour, minute, second] = value.split(':');

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

          if (!this.isEmpty(second)) {
            let index = second.indexOf(".");
            let millisecond;
            if (index != -1) {
              millisecond = second.substring(index + 1);
              second = second.substring(0, index);
            }

            if (!this.mSecondField) {
              this.mSecondSeparator = this.insertSeparator(this.mSeparatorText);
              this.mSecondField = this.insertAdditionalField(this.mPlaceHolder,
                this.mMinSecond, this.mMaxSecond, this.mMaxLength,
                this.mMaxLength);
            }
            this.setFieldValue(this.mSecondField, second);

            if (!this.isEmpty(millisecond)) {
              if (!this.mMillisecField) {
                this.mMillisecSeparator = this.insertSeparator(
                  this.mMillisecSeparatorText);
                this.mMillisecField = this.insertAdditionalField(
                  this.mPlaceHolder, this.mMinMillisecond, this.mMaxMillisecond,
                  this.mMillisecMaxLength, this.mMillisecMaxLength);
              }
              this.setFieldValue(this.mMillisecField, millisecond);
            } else if (this.mMillisecField) {
              this.mMillisecField.remove();
              this.mMillisecField = null;

              this.mMillisecSeparator.remove();
              this.mMillisecSeparator = null;
            }
          } else {
            if (this.mSecondField) {
              this.mSecondField.remove();
              this.mSecondField = null;

              this.mSecondSeparator.remove();
              this.mSecondSeparator = null;
            }

            if (this.mMillisecField) {
              this.mMillisecField.remove();
              this.mMillisecField = null;

              this.mMillisecSeparator.remove();
              this.mMillisecSeparator = null;
            }
          }
          this.notifyPicker();
        ]]>
        </body>
      </method>

      <method name="setInputValueFromFields">
        <body>
        <![CDATA[
          if (this.isEmpty(this.mHourField.value) ||
              this.isEmpty(this.mMinuteField.value) ||
              (this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
              (this.mSecondField && this.isEmpty(this.mSecondField.value)) ||
              (this.mMillisecField && this.isEmpty(this.mMillisecField.value))) {
            // We still need to notify picker in case any of the field has
            // changed. If we can set input element value, then notifyPicker
            // will be called in setFieldsFromInputValue().
            this.notifyPicker();
            return;
          }

          let hour = Number(this.mHourField.value);
          if (this.mHour12) {
            let dayPeriod = this.mDayPeriodField.value;
            if (dayPeriod == this.mPMIndicator &&
                hour < this.mMaxHourInHour12) {
              hour += this.mMaxHourInHour12;
            } else if (dayPeriod == this.mAMIndicator &&
                       hour == this.mMaxHourInHour12) {
              hour = 0;
            }
          }

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

          let time = hour + ":" + this.mMinuteField.value;
          if (this.mSecondField) {
            time += ":" + this.mSecondField.value;
          }

          if (this.mMillisecField) {
            time += "." + this.mMillisecField.value;
          }

          this.log("setInputValueFromFields: " + time);
          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.mDayPeriodField.value =
                (hour >= this.mMaxHourInHour12) ? this.mPMIndicator
                                                : this.mAMIndicator;
            }
          }

          if (!this.isEmpty(minute)) {
            this.setFieldValue(this.mMinuteField, minute);
          }
        ]]>
        </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.mHourField.value = "";
          }

          if (this.mMinuteField && !this.mMinuteField.disabled &&
              !this.mMinuteField.readOnly) {
            this.mMinuteField.value = "";
          }

          if (this.mSecondField && !this.mSecondField.disabled &&
              !this.mSecondField.readOnly) {
            this.mSecondField.value = "";
          }

          if (this.mMillisecField && !this.mMillisecField.disabled &&
              !this.mMillisecField.readOnly) {
            this.mMillisecField.value = "";
          }

          if (this.mDayPeriodField && !this.mDayPeriodField.disabled &&
              !this.mDayPeriodField.readOnly) {
            this.mDayPeriodField.value = "";
          }

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

      <method name="incrementFieldValue">
        <parameter name="aTargetField"/>
        <parameter name="aTimes"/>
        <body>
        <![CDATA[
          let value;

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

            if (aTargetField == this.mHourField) {
              value = now.getHours() % this.mMaxHourInHour12 ||
                this.mMaxHourInHour12;
            } 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;
            }
          } else {
            value = Number(aTargetField.value);
          }

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

          value += aTimes;
          if (value > max) {
            value -= (max - min + 1);
          } else if (value < min) {
            value += (max - min + 1);
          }
          this.setFieldValue(aTargetField, value);
          aTargetField.select();
        ]]>
        </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.mDayPeriodField &&
              targetField == this.mDayPeriodField) {
            // Home/End key does nothing on AM/PM field.
            if (key == "Home" || key == "End") {
              return;
            }

            this.mDayPeriodField.value =
              this.mDayPeriodField.value == this.mAMIndicator ?
                this.mPMIndicator : this.mAMIndicator;
            this.mDayPeriodField.select();
            this.setInputValueFromFields();
            return;
          }

          switch (key) {
            case "ArrowUp":
              this.incrementFieldValue(targetField, 1);
              break;
            case "ArrowDown":
              this.incrementFieldValue(targetField, -1);
              break;
            case "PageUp":
              this.incrementFieldValue(targetField,
                targetField == this.mHourField ? this.mHourPageUpDownInterval
                                               : this.mMinSecPageUpDownInterval);
              break;
            case "PageDown":
              this.incrementFieldValue(targetField,
                targetField == this.mHourField ? (0 - this.mHourPageUpDownInterval)
                                               : (0 - this.mMinSecPageUpDownInterval));
              break;
            case "Home":
              let min = targetField.getAttribute("min");
              this.setFieldValue(targetField, min);
              targetField.select();
              break;
            case "End":
              let max = targetField.getAttribute("max");
              this.setFieldValue(targetField, max);
              targetField.select();
              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.mDayPeriodField &&
              targetField == this.mDayPeriodField) {
            if (key == "a" || key == "A") {
              this.mDayPeriodField.value = this.mAMIndicator;
              this.mDayPeriodField.select();
            } else if (key == "p" || key == "P") {
              this.mDayPeriodField.value = this.mPMIndicator;
              this.mDayPeriodField.select();
            }
            return;
          }

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

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

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

      <method name="setFieldValue">
       <parameter name="aField"/>
       <parameter name="aValue"/>
        <body>
        <![CDATA[
          let value = Number(aValue);
          if (isNaN(value)) {
            this.log("NaN on setFieldValue!");
            return;
          }

          if (aField.maxLength == this.mMaxLength) { // For hour, minute and second
            if (aField == this.mHourField && this.mHour12) {
              value = (value > this.mMaxHourInHour12) ?
                value - this.mMaxHourInHour12 : value;
              if (aValue == "00") {
                value = this.mMaxHourInHour12;
              }
            }
            // prepend zero
            if (value < 10) {
              value = "0" + value;
            }
          } else if (aField.maxLength == this.mMillisecMaxLength) {
            // prepend zeroes
            if (value < 10) {
              value = "00" + value;
            } else if (value < 100) {
              value = "0" + value;
            }
          }

          aField.value = value;
        ]]>
        </body>
      </method>

      <method name="isValueAvailable">
        <body>
        <![CDATA[
          // Picker only cares about hour:minute.
          return !this.isEmpty(this.mHourField.value) ||
                 !this.isEmpty(this.mMinuteField.value);
        ]]>
        </body>
      </method>

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

          let minute;
          if (!this.isEmpty(this.mMinuteField.value)) {
            minute = Number(this.mMinuteField.value);
          }

          // Picker only needs hour/minute.
          let time = { hour, minute };

          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"
                xbl:inherits="context,disabled,readonly">
        <html:span>
          <html:input anonid="input-one"
                      class="textbox-input datetime-input numeric"
                      size="2" maxlength="2"
                      xbl:inherits="disabled,readonly,tabindex"/>
          <html:span anonid="sep-first" class="datetime-separator"></html:span>
          <html:input anonid="input-two"
                      class="textbox-input datetime-input numeric"
                      size="2" maxlength="2"
                      xbl:inherits="disabled,readonly,tabindex"/>
          <html:span anonid="sep-second" class="datetime-separator"></html:span>
          <html:input anonid="input-three"
                      class="textbox-input datetime-input numeric"
                      size="2" maxlength="2"
                      xbl:inherits="disabled,readonly,tabindex"/>
        </html:span>

        <html:button class="datetime-reset-button" anoid="reset-button"
                     tabindex="-1" xbl:inherits="disabled"
                     onclick="document.getBindingParent(this).clearInputFields(false);"/>
      </html:div>
    </content>

    <implementation implements="nsIDateTimeInputArea">
      <constructor>
      <![CDATA[
        this.DEBUG = false;
        this.mInputElement = this.parentNode;

        this.mMin = this.mInputElement.min;
        this.mMax = this.mInputElement.max;
        this.mStep = this.mInputElement.step;
        this.mIsPickerOpen = false;
      ]]>
      </constructor>

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

      <method name="focusInnerTextBox">
        <body>
        <![CDATA[
          this.log("focusInnerTextBox");
          document.getAnonymousElementByAttribute(this, "anonid", "input-one").focus();
        ]]>
        </body>
      </method>

      <method name="blurInnerTextBox">
        <body>
        <![CDATA[
          this.log("blurInnerTextBox");
          if (this.mLastFocusedField) {
            this.mLastFocusedField.blur();
          }
        ]]>
        </body>
      </method>

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

      <method name="setValueFromPicker">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          this.setFieldsFromPicker(aValue);
        ]]>
        </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.type == "text" && !next.disabled) {
              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="isEmpty">
        <parameter name="aValue"/>
        <body>
          return (aValue == undefined || 0 === aValue.length);
        </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="notifyPicker">
        <body>
        <![CDATA[
          if (this.mIsPickerOpen && this.isValueAvailable()) {
            this.mInputElement.updateDateTimePicker(this.getCurrentValue());
          }
        ]]>
        </body>
      </method>

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

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

    </implementation>

    <handlers>
      <handler event="focus">
      <![CDATA[
        this.log("focus on: " + event.originalTarget);

        let target = event.originalTarget;
        if (target.type == "text") {
          this.mLastFocusedField = target;
          target.select();
        }
      ]]>
      </handler>

      <handler event="blur">
      <![CDATA[
        this.setInputValueFromFields();
      ]]>
      </handler>

      <handler event="click">
      <![CDATA[
        // XXX: .originalTarget is not expected.
        // When clicking on one of the inner text boxes, the .originalTarget is
        // a HTMLDivElement and when clicking on the reset button, it's a
        // HTMLButtonElement but it's not equal to our reset-button.
        this.log("click on: " + event.originalTarget);
        if (event.defaultPrevented || this.isDisabled() || this.isReadonly()) {
          return;
        }

        if (!(event.originalTarget instanceof HTMLButtonElement)) {
          this.mInputElement.openDateTimePicker(this.getCurrentValue());
        }
      ]]>
      </handler>

      <handler event="keypress" phase="capturing">
      <![CDATA[
        let key = event.key;
        this.log("keypress: " + key);

        if (key == "Backspace" || key == "Tab") {
          return;
        }

        if (key == "Enter" || key == " ") {
          // Close picker on Enter and Space.
          this.mInputElement.closeDateTimePicker();
        }

        if (key == "ArrowUp" || key == "ArrowDown" ||
            key == "PageUp" || key == "PageDown" ||
            key == "Home" || key == "End") {
          this.handleKeyboardNav(event);
        } else if (key == "ArrowRight" || key == "ArrowLeft") {
          this.advanceToNextField((key == "ArrowRight" ? false : true));
        } else {
          this.handleKeypress(event);
        }

        event.preventDefault();
      ]]>
      </handler>
    </handlers>
  </binding>

</bindings>
