diff options
Diffstat (limited to 'toolkit/content/widgets/marquee.js')
-rw-r--r-- | toolkit/content/widgets/marquee.js | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/toolkit/content/widgets/marquee.js b/toolkit/content/widgets/marquee.js new file mode 100644 index 0000000000..8b18703b92 --- /dev/null +++ b/toolkit/content/widgets/marquee.js @@ -0,0 +1,417 @@ +/* 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/. */ + +"use strict"; + +/* + * This is the class of entry. It will construct the actual implementation + * according to the value of the "direction" property. + */ +this.MarqueeWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + } + + /* + * Callback called by UAWidgets right after constructor. + */ + onsetup() { + this.switchImpl(); + } + + /* + * Callback called by UAWidgetsChild wheen the direction property + * changes. + */ + onchange() { + this.switchImpl(); + } + + switchImpl() { + let newImpl; + switch (this.element.direction) { + case "up": + case "down": + newImpl = MarqueeVerticalImplWidget; + break; + case "left": + case "right": + newImpl = MarqueeHorizontalImplWidget; + break; + } + + // Skip if we are asked to load the same implementation. + // This can happen if the property is set again w/o value change. + if (this.impl && this.impl.constructor == newImpl) { + return; + } + this.teardown(); + if (newImpl) { + this.impl = new newImpl(this.shadowRoot); + this.impl.onsetup(); + } + } + + teardown() { + if (!this.impl) { + return; + } + this.impl.teardown(); + this.shadowRoot.firstChild.remove(); + delete this.impl; + } +}; + +this.MarqueeBaseImplWidget = class { + constructor(shadowRoot) { + this.shadowRoot = shadowRoot; + this.element = shadowRoot.host; + this.document = this.element.ownerDocument; + this.window = this.document.defaultView; + } + + onsetup() { + this.generateContent(); + + // Set up state. + this._currentDirection = this.element.direction || "left"; + this._currentLoop = this.element.loop; + this.dirsign = 1; + this.startAt = 0; + this.stopAt = 0; + this.newPosition = 0; + this.runId = 0; + this.originalHeight = 0; + this.invalidateCache = true; + + this._mutationObserver = new this.window.MutationObserver(aMutations => + this._mutationActor(aMutations) + ); + this._mutationObserver.observe(this.element, { + attributes: true, + attributeOldValue: true, + attributeFilter: ["loop", "", "behavior", "direction", "width", "height"], + }); + + // init needs to be run after the page has loaded in order to calculate + // the correct height/width + if (this.document.readyState == "complete") { + this.init(); + } else { + this.window.addEventListener("load", this, { once: true }); + } + + this.shadowRoot.addEventListener("marquee-start", this); + this.shadowRoot.addEventListener("marquee-stop", this); + } + + teardown() { + this._mutationObserver.disconnect(); + this.window.clearTimeout(this.runId); + + this.window.removeEventListener("load", this); + this.shadowRoot.removeEventListener("marquee-start", this); + this.shadowRoot.removeEventListener("marquee-stop", this); + } + + handleEvent(aEvent) { + if (!aEvent.isTrusted) { + return; + } + + switch (aEvent.type) { + case "load": + this.init(); + break; + case "marquee-start": + this.doStart(); + break; + case "marquee-stop": + this.doStop(); + break; + } + } + + get outerDiv() { + return this.shadowRoot.firstChild; + } + + get innerDiv() { + return this.shadowRoot.getElementById("innerDiv"); + } + + get scrollDelayWithTruespeed() { + if (this.element.scrollDelay < 60 && !this.element.trueSpeed) { + return 60; + } + return this.element.scrollDelay; + } + + doStart() { + if (this.runId == 0) { + var lambda = () => this._doMove(false); + this.runId = this.window.setTimeout( + lambda, + this.scrollDelayWithTruespeed - this._deltaStartStop + ); + this._deltaStartStop = 0; + } + } + + doStop() { + if (this.runId != 0) { + this._deltaStartStop = Date.now() - this._lastMoveDate; + this.window.clearTimeout(this.runId); + } + + this.runId = 0; + } + + _fireEvent(aName, aBubbles, aCancelable) { + var e = this.document.createEvent("Events"); + e.initEvent(aName, aBubbles, aCancelable); + this.element.dispatchEvent(e); + } + + _doMove(aResetPosition) { + this._lastMoveDate = Date.now(); + + // invalidateCache is true at first load and whenever an attribute + // is changed + if (this.invalidateCache) { + this.invalidateCache = false; // we only want this to run once every scroll direction change + + var corrvalue = 0; + + switch (this._currentDirection) { + case "up": + case "down": { + let height = this.window.getComputedStyle(this.element).height; + this.outerDiv.style.height = height; + if (this.originalHeight > this.outerDiv.offsetHeight) { + corrvalue = this.originalHeight - this.outerDiv.offsetHeight; + } + this.innerDiv.style.padding = height + " 0"; + let isUp = this._currentDirection == "up"; + if (isUp) { + this.dirsign = 1; + this.startAt = + this.element.behavior == "alternate" + ? this.originalHeight - corrvalue + : 0; + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? parseInt(height) + corrvalue + : this.originalHeight + parseInt(height); + } else { + this.dirsign = -1; + this.startAt = + this.element.behavior == "alternate" + ? parseInt(height) + corrvalue + : this.originalHeight + parseInt(height); + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? this.originalHeight - corrvalue + : 0; + } + break; + } + case "left": + case "right": + default: { + let isRight = this._currentDirection == "right"; + // NOTE: It's important to use getComputedStyle() to not account for the padding. + let innerWidth = parseInt( + this.window.getComputedStyle(this.innerDiv).width + ); + if (innerWidth > this.outerDiv.offsetWidth) { + corrvalue = innerWidth - this.outerDiv.offsetWidth; + } + let rtl = + this.window.getComputedStyle(this.element).direction == "rtl"; + if (isRight != rtl) { + this.dirsign = -1; + this.stopAt = + this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? innerWidth - corrvalue + : 0; + this.startAt = + this.outerDiv.offsetWidth + + (this.element.behavior == "alternate" + ? corrvalue + : innerWidth + this.stopAt); + } else { + this.dirsign = 1; + this.startAt = + this.element.behavior == "alternate" ? innerWidth - corrvalue : 0; + this.stopAt = + this.outerDiv.offsetWidth + + (this.element.behavior == "alternate" || + this.element.behavior == "slide" + ? corrvalue + : innerWidth + this.startAt); + } + if (rtl) { + this.startAt = -this.startAt; + this.stopAt = -this.stopAt; + this.dirsign = -this.dirsign; + } + break; + } + } + + if (aResetPosition) { + this.newPosition = this.startAt; + this._fireEvent("start", false, false); + } + } // end if + + this.newPosition = + this.newPosition + this.dirsign * this.element.scrollAmount; + + if ( + (this.dirsign == 1 && this.newPosition > this.stopAt) || + (this.dirsign == -1 && this.newPosition < this.stopAt) + ) { + switch (this.element.behavior) { + case "alternate": + // lets start afresh + this.invalidateCache = true; + + // swap direction + const swap = { left: "right", down: "up", up: "down", right: "left" }; + this._currentDirection = swap[this._currentDirection] || "left"; + this.newPosition = this.stopAt; + + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + if (this._currentLoop != 1) { + this._fireEvent("bounce", false, true); + } + break; + + case "slide": + if (this._currentLoop > 1) { + this.newPosition = this.startAt; + } + break; + + default: + this.newPosition = this.startAt; + + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + // dispatch start event, even when this._currentLoop == 1, comp. with IE6 + this._fireEvent("start", false, false); + } + + if (this._currentLoop > 1) { + this._currentLoop--; + } else if (this._currentLoop == 1) { + if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.stopAt; + } else { + this.outerDiv.scrollLeft = this.stopAt; + } + this.element.stop(); + this._fireEvent("finish", false, true); + return; + } + } else if ( + this._currentDirection == "up" || + this._currentDirection == "down" + ) { + this.outerDiv.scrollTop = this.newPosition; + } else { + this.outerDiv.scrollLeft = this.newPosition; + } + + var myThis = this; + var lambda = function myTimeOutFunction() { + myThis._doMove(false); + }; + this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed); + } + + init() { + this.element.stop(); + + if (this._currentDirection == "up" || this._currentDirection == "down") { + // store the original height before we add padding + this.innerDiv.style.padding = 0; + this.originalHeight = this.innerDiv.offsetHeight; + } + + this._doMove(true); + } + + _mutationActor(aMutations) { + while (aMutations.length) { + 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) { + this.invalidateCache = true; + switch (attrName) { + case "loop": + this._currentLoop = target.loop; + break; + case "direction": + this._currentDirection = target.direction; + break; + } + } + } + } +}; + +this.MarqueeHorizontalImplWidget = class extends MarqueeBaseImplWidget { + generateContent() { + // White-space isn't allowed because a marquee could be + // inside 'white-space: pre' + this.shadowRoot.innerHTML = `<div class="outerDiv horizontal" + ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" + /><div class="innerDiv" id="innerDiv" + ><slot + /></div + ></div>`; + } +}; + +this.MarqueeVerticalImplWidget = class extends MarqueeBaseImplWidget { + generateContent() { + // White-space isn't allowed because a marquee could be + // inside 'white-space: pre' + this.shadowRoot.innerHTML = `<div class="outerDiv vertical" + ><link rel="stylesheet" href="chrome://global/content/elements/marquee.css" + /><div class="innerDiv" id="innerDiv" + ><slot + /></div + ></div>`; + } +}; |