summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/marquee.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/widgets/marquee.js')
-rw-r--r--toolkit/content/widgets/marquee.js417
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>`;
+ }
+};