<?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="marqueeBindings"
          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="marquee" bindToUntrustedContent="true">

    <resources>
      <stylesheet src="chrome://xbl-marquee/content/xbl-marquee.css"/>
    </resources>
    <implementation>

      <property name="scrollAmount" exposeToUntrustedContent="true">
        <getter>
          <![CDATA[
          this._mutationActor(this._mutationObserver.takeRecords());
          return this._scrollAmount;
          ]]>
        </getter>
        <setter>
          <![CDATA[
          var val = parseInt(val);
          if (val < 0) {
            return;
          }
          if (isNaN(val)) {
            val = 0;
          }
          this.setAttribute("scrollamount", val);
          ]]>
        </setter>
      </property>

      <property name="scrollDelay" exposeToUntrustedContent="true">
        <getter>
          <![CDATA[
          this._mutationActor(this._mutationObserver.takeRecords());
          var val = parseInt(this.getAttribute("scrolldelay"));

          if (val <= 0 || isNaN(val)) {
            return this._scrollDelay;
          }

          return val;
          ]]>
        </getter>
        <setter>
          var val = parseInt(val);
          if (val > 0 ) {
            this.setAttribute("scrolldelay", val);
          }
        </setter>
      </property>

      <property name="trueSpeed" exposeToUntrustedContent="true">
        <getter>
          <![CDATA[
          if (!this.hasAttribute("truespeed")) {
            return false;
          }

          return true;
          ]]>
        </getter>
        <setter>
          <![CDATA[
          if (val) {
            this.setAttribute("truespeed", "");
          } else {
            this.removeAttribute('truespeed');
          }
          ]]>
        </setter>
      </property>

      <property name="direction" exposeToUntrustedContent="true">
        <getter>
          this._mutationActor(this._mutationObserver.takeRecords());
          return this._direction;
        </getter>
        <setter>
          <![CDATA[
          if (typeof val == 'string') {
            val = val.toLowerCase();
          } else {
            return;
          }
          if (val != 'left' && val != 'right' && val != 'up' && val != 'down') {
            val = 'left';
          }

          this.setAttribute("direction", val);
          ]]>
        </setter>
      </property>

      <property name="behavior" exposeToUntrustedContent="true">
        <getter>
          this._mutationActor(this._mutationObserver.takeRecords());
          return this._behavior;
        </getter>
        <setter>
          if (typeof val == 'string') {
            val = val.toLowerCase();
          }
          if (val == "alternate" || val == "slide" || val == 'scroll') {
            this.setAttribute("behavior", val);
          }
        </setter>
      </property>


      <property name="loop" exposeToUntrustedContent="true">
        <getter>
          <![CDATA[
          this._mutationActor(this._mutationObserver.takeRecords());
          return this._loop;
          ]]>
        </getter>
        <setter>
          <![CDATA[
          var val = parseInt(val);
          if (val == -1 || val > 0) {
            this.setAttribute("loop", val);
          }
          ]]>
        </setter>
      </property>


      <property name="onstart" exposeToUntrustedContent="true">
        <getter>
          return this.getAttribute("onstart");
        </getter>
        <setter>
          this._setEventListener("start", val, true);
          this.setAttribute("onstart", val);
        </setter>
      </property>

      <property name="onfinish" exposeToUntrustedContent="true">
        <getter>
          return this.getAttribute("onfinish");
        </getter>
        <setter>
          this._setEventListener("finish", val, true);
          this.setAttribute("onfinish", val);
        </setter>
      </property>

      <property name="onbounce" exposeToUntrustedContent="true">
        <getter>
          return this.getAttribute("onbounce");
        </getter>
        <setter>
          this._setEventListener("bounce", val, true);
          this.setAttribute("onbounce", val);
        </setter>
      </property>

      <property name="outerDiv"
        onget="return document.getAnonymousNodes(this)[0]"
      />

      <property name="innerDiv"
        onget="return document.getAnonymousElementByAttribute(this, 'class', 'innerDiv');"
      />

      <property name="height" exposeToUntrustedContent="true"
        onget="return this.getAttribute('height');"
        onset="this.setAttribute('height', val);"
      />

      <property name="width" exposeToUntrustedContent="true"
        onget="return this.getAttribute('width');"
        onset="this.setAttribute('width', val);"
      />

      <method name="_set_scrollDelay">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          aValue = parseInt(aValue);
          if (aValue <= 0) {
            return;
          } else if (isNaN(aValue)) {
            this._scrollDelay = 85;
          } else if (aValue < 60) {
            if (this.trueSpeed == true) {
              this._scrollDelay = aValue;
            } else {
              this._scrollDelay = 60;
            }
          } else {
            this._scrollDelay = aValue;
          }
        ]]>
        </body>
      </method>

      <method name="_set_scrollAmount">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          aValue = parseInt(aValue);
          if (isNaN(aValue)) {
            this._scrollAmount = 6;
          } else if (aValue < 0) {
            return;
          } else {
            this._scrollAmount = aValue;
          }
        ]]>
        </body>
      </method>

      <method name="_set_behavior">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (typeof aValue == 'string') {
            aValue = aValue.toLowerCase();
          }
          if (aValue != 'alternate' && aValue != 'slide' && aValue != 'scroll') {
            this._behavior = 'scroll';
          } else {
            this._behavior = aValue;
          }
        ]]>
        </body>
      </method>

      <method name="_set_direction">
        <parameter name="aValue"/>
        <body>
        <![CDATA[
          if (typeof aValue == 'string') {
            aValue = aValue.toLowerCase();
          }
          if (aValue != 'left' && aValue != 'right' && aValue != 'up' && aValue != 'down') {
            aValue = 'left';
          }

          if (aValue != this._direction) {
            this.startNewDirection = true;
          }
          this._direction = aValue;
        ]]>
        </body>
      </method>

      <method name="_set_loop">
        <parameter name="aValue"/>
        <body>
          <![CDATA[
          var aValue = parseInt(aValue);
          if (aValue == 0) {
            return;
          }
          if (isNaN(aValue) || aValue <= -1) {
            aValue = -1;
          }
          this._loop = aValue;
          ]]>
        </body>
      </method>

      <method name="_setEventListener">
        <parameter name="aName"/>
        <parameter name="aValue"/>
        <parameter name="aIgnoreNextCall"/>
        <body>
          <![CDATA[
          // _setEventListener is only used for setting the attribute event
          // handlers, which we want to ignore if our document is sandboxed
          // without the allow-scripts keyword.
          if (document.hasScriptsBlockedBySandbox) {
            return true;
          }

          // attribute event handlers should only be added if the
          // document's CSP allows it.
          if (!document.inlineScriptAllowedByCSP) {
            return true;
          }

          if (this._ignoreNextCall) {
            return this._ignoreNextCall = false;
          }

          if (aIgnoreNextCall) {
            this._ignoreNextCall = true;
          }

          if (typeof this["_on" + aName] == 'function') {
            this.removeEventListener(aName, this["_on" + aName], false);
          }

          switch (typeof aValue)
          {
            case "function":
              this["_on" + aName] = aValue;
              this.addEventListener(aName, this["_on" + aName], false);
            break;

            case "string":
              if (!aIgnoreNextCall) {
                try {
                  // Function Xrays make this simple and safe. \o/
                  this["_on" + aName] = new window.Function("event", aValue);
                }
                catch(e) {
                  return false;
                }
                this.addEventListener(aName, this["_on" + aName], false);
              }
              else {
                this["_on" + aName] = aValue;
              }
            break;

            case "object":
              this["_on" + aName] = aValue;
            break;

            default:
              this._ignoreNextCall = false;
              throw new Error("Invalid argument for Marquee::on" + aName);
          }
          return true;
          ]]>
        </body>
      </method>

      <method name="_fireEvent">
        <parameter name="aName"/>
        <parameter name="aBubbles"/>
        <parameter name="aCancelable"/>
        <body>
        <![CDATA[
          var e = document.createEvent("Events");
          e.initEvent(aName, aBubbles, aCancelable);
          this.dispatchEvent(e);
        ]]>
        </body>
      </method>

      <method name="start" exposeToUntrustedContent="true">
        <body>
        <![CDATA[
          if (this.runId == 0) {
            var myThis = this;
            var lambda = function myTimeOutFunction(){myThis._doMove(false);}
            this.runId = window.setTimeout(lambda, this._scrollDelay - this._deltaStartStop);
            this._deltaStartStop = 0;
          }
        ]]>
        </body>
      </method>

      <method name="stop" exposeToUntrustedContent="true">
        <body>
        <![CDATA[
          if (this.runId != 0) {
            this._deltaStartStop = Date.now()- this._lastMoveDate;
            clearTimeout(this.runId);
          }

          this.runId = 0;
        ]]>
        </body>
      </method>

      <method name="_doMove">
        <parameter name="aResetPosition"/>
        <body>
        <![CDATA[
          this._lastMoveDate = Date.now();

          //startNewDirection is true at first load and whenever the direction is changed
          if (this.startNewDirection) {
            this.startNewDirection = false; //we only want this to run once every scroll direction change

            var corrvalue = 0;

            switch (this._direction)
            {
              case "up":
                var height = document.defaultView.getComputedStyle(this, "").height;
                this.outerDiv.style.height = height;
                if (this.originalHeight > this.outerDiv.offsetHeight) {
                    corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
                }
                this.innerDiv.style.padding = height + " 0";
                this.dirsign = 1;
                this.startAt = (this._behavior == 'alternate') ? (this.originalHeight - corrvalue) : 0;
                this.stopAt  = (this._behavior == 'alternate' || this._behavior == 'slide') ? 
                                (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
              break;

              case "down":
                var height = document.defaultView.getComputedStyle(this, "").height;
                this.outerDiv.style.height = height;
                if (this.originalHeight > this.outerDiv.offsetHeight) {
                    corrvalue = this.originalHeight - this.outerDiv.offsetHeight;
                }
                this.innerDiv.style.padding = height + " 0";
                this.dirsign = -1;
                this.startAt  = (this._behavior == 'alternate') ?
                                (parseInt(height) + corrvalue) : (this.originalHeight + parseInt(height));
                this.stopAt = (this._behavior == 'alternate' || this._behavior == 'slide') ? 
                              (this.originalHeight - corrvalue) : 0;
              break;

              case "right":
                if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
                    corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
                }
                this.dirsign = -1;
                this.stopAt  = (this._behavior == 'alternate' || this._behavior == 'slide') ? 
                               (this.innerDiv.offsetWidth - corrvalue) : 0;
                this.startAt = this.outerDiv.offsetWidth + ((this._behavior == 'alternate') ? 
                               corrvalue : (this.innerDiv.offsetWidth + this.stopAt));   
              break;

              case "left":
              default:
                if (this.innerDiv.offsetWidth > this.outerDiv.offsetWidth) {
                    corrvalue = this.innerDiv.offsetWidth - this.outerDiv.offsetWidth;
                }
                this.dirsign = 1;
                this.startAt = (this._behavior == 'alternate') ? (this.innerDiv.offsetWidth - corrvalue) : 0;
                this.stopAt  = this.outerDiv.offsetWidth + 
                               ((this._behavior == 'alternate' || this._behavior == 'slide') ? 
                               corrvalue : (this.innerDiv.offsetWidth + this.startAt));
            }

            if (aResetPosition) {
              this.newPosition = this.startAt;
              this._fireEvent("start", false, false);
            }
          } //end if

          this.newPosition = this.newPosition + (this.dirsign * this._scrollAmount);

          if ((this.dirsign == 1 && this.newPosition > this.stopAt) ||
              (this.dirsign == -1 && this.newPosition < this.stopAt))
          {
            switch (this._behavior) 
            {
              case 'alternate':
                // lets start afresh
                this.startNewDirection = true;

                // swap direction
                const swap = {left: "right", down: "up", up: "down", right: "left"};
                this._direction = swap[this._direction];
                this.newPosition = this.stopAt;

                if ((this._direction == "up") || (this._direction == "down")) {
                  this.outerDiv.scrollTop = this.newPosition;
                } else {
                  this.outerDiv.scrollLeft = this.newPosition;
                }

                if (this._loop != 1) {
                  this._fireEvent("bounce", false, true);
                }
              break;

              case 'slide':
                if (this._loop > 1) {
                  this.newPosition = this.startAt;
                }
              break;

              default:
                this.newPosition = this.startAt;

                if ((this._direction == "up") || (this._direction == "down")) {
                  this.outerDiv.scrollTop = this.newPosition;
                } else {
                  this.outerDiv.scrollLeft = this.newPosition;
                }

                //dispatch start event, even when this._loop == 1, comp. with IE6
                this._fireEvent("start", false, false);
            }

            if (this._loop > 1) {
              this._loop--;
            } else if (this._loop == 1) {
              if ((this._direction == "up") || (this._direction == "down")) {
                this.outerDiv.scrollTop = this.stopAt;
              } else {
                this.outerDiv.scrollLeft = this.stopAt;
              }
              this.stop();
              this._fireEvent("finish", false, true);
              return;
            }
          }
          else {
            if ((this._direction == "up") || (this._direction == "down")) {
              this.outerDiv.scrollTop = this.newPosition;
            } else {
              this.outerDiv.scrollLeft = this.newPosition;
            }
          }

          var myThis = this;
          var lambda = function myTimeOutFunction(){myThis._doMove(false);}
          this.runId = window.setTimeout(lambda, this._scrollDelay);
        ]]>
        </body>
      </method>

      <method name="init">
        <body>
        <![CDATA[
          this.stop();

          if ((this._direction != "up") && (this._direction != "down")) {
            var width = window.getComputedStyle(this, "").width;
            this.innerDiv.parentNode.style.margin = '0 ' + width;

            //XXX Adding the margin sometimes causes the marquee to widen, 
            // see testcase from bug bug 364434: 
            // https://bugzilla.mozilla.org/attachment.cgi?id=249233
            // Just add a fixed width with current marquee's width for now
            if (width != window.getComputedStyle(this, "").width) {
              var width = window.getComputedStyle(this, "").width;
              this.outerDiv.style.width = width;
              this.innerDiv.parentNode.style.margin = '0 ' + width;
            }
          }
          else {
            // store the original height before we add padding
            this.innerDiv.style.padding = 0;
            this.originalHeight = this.innerDiv.offsetHeight;
          }

          this._doMove(true);
        ]]>
        </body>
      </method>

      <method name="_mutationActor">
        <parameter name="aMutations"/>
        <body>
        <![CDATA[
          while (aMutations.length > 0) {
            var mutation = aMutations.shift();
            var attrName = mutation.attributeName.toLowerCase();
            var oldValue = mutation.oldValue;
            var target = mutation.target;
            var newValue = target.getAttribute(attrName);

            if (oldValue != newValue) {
              switch (attrName) {
                case "loop":
                  target._set_loop(newValue);
                  if (target.rundId == 0) {
                    target.start();
                  }
                  break;
                case "scrollamount":
                  target._set_scrollAmount(newValue);
                  break;
                case "scrolldelay":
                  target._set_scrollDelay(newValue);
                  target.stop();
                  target.start();
                  break;
                case "truespeed":
                  //needed to update target._scrollDelay
                  var myThis = target;
                  var lambda = function() {myThis._set_scrollDelay(myThis.getAttribute('scrolldelay'));}
                  window.setTimeout(lambda, 0);
                  break;
                case "behavior":
                  target._set_behavior(newValue);
                  target.startNewDirection = true;
                  if ((oldValue == "slide" && target.newPosition == target.stopAt) ||
                      newValue == "alternate" || newValue == "slide") {
                    target.stop();
                    target._doMove(true);
                  }
                  break;
                case "direction":
                  if (!newValue) {
                    newValue = "left";
                  }
                  target._set_direction(newValue);
                  break;
                case "width":
                case "height":
                  target.startNewDirection = true;
                  break;
                case "onstart":
                  target._setEventListener("start", newValue);
                  break;
                case "onfinish":
                  target._setEventListener("finish", newValue);
                  break;
                case "onbounce":
                  target._setEventListener("bounce", newValue);
                  break;
              }
            }
          }
        ]]>
        </body>
      </method>

      <constructor>
        <![CDATA[
          // Set up state.
          this._scrollAmount = 6;
          this._scrollDelay = 85;
          this._direction = "left";
          this._behavior = "scroll";
          this._loop = -1;
          this.dirsign = 1;
          this.startAt = 0;
          this.stopAt = 0;
          this.newPosition = 0;
          this.runId = 0;
          this.originalHeight = 0;
          this.startNewDirection = true;

          // hack needed to fix js error, see bug 386470
          var myThis = this;
          var lambda = function myScopeFunction() { if (myThis.init) myThis.init(); }

          this._set_direction(this.getAttribute('direction'));
          this._set_behavior(this.getAttribute('behavior'));
          this._set_scrollDelay(this.getAttribute('scrolldelay'));
          this._set_scrollAmount(this.getAttribute('scrollamount'));
          this._set_loop(this.getAttribute('loop'));
          this._setEventListener("start", this.getAttribute("onstart"));
          this._setEventListener("finish", this.getAttribute("onfinish"));
          this._setEventListener("bounce", this.getAttribute("onbounce"));
          this.startNewDirection = true;

          this._mutationObserver = new MutationObserver(this._mutationActor);
          this._mutationObserver.observe(this, { attributes: true,
            attributeOldValue: true,
            attributeFilter: ['loop', 'scrollamount', 'scrolldelay', '', 'truespeed', 'behavior',
              'direction', 'width', 'height', 'onstart', 'onfinish', 'onbounce'] });

          // init needs to be run after the page has loaded in order to calculate
          // the correct height/width
          if (document.readyState == "complete") {
            lambda();
          } else {
            window.addEventListener("load", lambda, false);
          }
        ]]>
      </constructor>
    </implementation>

  </binding>

  <binding id="marquee-horizontal" bindToUntrustedContent="true"
           extends="chrome://xbl-marquee/content/xbl-marquee.xml#marquee"
           inheritstyle="false">

    <!-- White-space isn't allowed because a marquee could be 
         inside 'white-space: pre' -->
    <content>
      <html:div style="display: -moz-box; overflow: hidden; width: -moz-available;"
        ><html:div style="display: -moz-box;"
          ><html:div class="innerDiv" style="display: table; border-spacing: 0;"
            ><html:div
              ><children
            /></html:div
          ></html:div
        ></html:div
      ></html:div>
    </content>

  </binding>

  <binding id="marquee-vertical" bindToUntrustedContent="true"
           extends="chrome://xbl-marquee/content/xbl-marquee.xml#marquee"
           inheritstyle="false">

    <!-- White-space isn't allowed because a marquee could be 
         inside 'white-space: pre' -->
    <content>
      <html:div style="overflow: hidden; width: -moz-available;"
        ><html:div class="innerDiv"
          ><children
        /></html:div
      ></html:div>
    </content>

  </binding>

  <binding id="marquee-horizontal-editable" bindToUntrustedContent="true"
           inheritstyle="false">

    <!-- White-space isn't allowed because a marquee could be 
         inside 'white-space: pre' -->
    <content>
      <html:div style="display: inline-block; overflow: auto; width: -moz-available;"
        ><children
      /></html:div>
    </content>

  </binding>

  <binding id="marquee-vertical-editable" bindToUntrustedContent="true"
           inheritstyle="false">

    <!-- White-space isn't allowed because a marquee could be 
         inside 'white-space: pre' -->
    <content>
      <html:div style="overflow: auto; height: inherit; width: -moz-available;"
        ><children/></html:div>
    </content>

  </binding>

</bindings>
