fune/toolkit/content/widgets/marquee.js

417 lines
12 KiB
JavaScript

/* 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>`;
}
};