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.js560
1 files changed, 245 insertions, 315 deletions
diff --git a/toolkit/content/widgets/marquee.js b/toolkit/content/widgets/marquee.js
index 8b18703b92..a694ffdca1 100644
--- a/toolkit/content/widgets/marquee.js
+++ b/toolkit/content/widgets/marquee.js
@@ -4,87 +4,25 @@
"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;
+ // This needed for behavior=alternate, in order to know in which of the two
+ // directions we're going.
+ this.dirsign = 1;
+ this._currentLoop = this.element.loop;
+ this.animation = null;
+ this._restartScheduled = null;
}
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;
+ // White-space isn't allowed because a marquee could be
+ // inside 'white-space: pre'
+ this.shadowRoot.innerHTML = `<link rel="stylesheet" href="chrome://global/content/elements/marquee.css"
+ /><slot></slot>`;
this._mutationObserver = new this.window.MutationObserver(aMutations =>
this._mutationActor(aMutations)
@@ -92,7 +30,7 @@ this.MarqueeBaseImplWidget = class {
this._mutationObserver.observe(this.element, {
attributes: true,
attributeOldValue: true,
- attributeFilter: ["loop", "", "behavior", "direction", "width", "height"],
+ attributeFilter: ["loop", "direction", "behavior"],
});
// init needs to be run after the page has loaded in order to calculate
@@ -108,12 +46,13 @@ this.MarqueeBaseImplWidget = class {
}
teardown() {
+ this.doStop();
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);
+ this.shadowRoot.replaceChildren();
}
handleEvent(aEvent) {
@@ -131,15 +70,26 @@ this.MarqueeBaseImplWidget = class {
case "marquee-stop":
this.doStop();
break;
+ case "finish":
+ this._animationFinished();
+ break;
}
}
- get outerDiv() {
- return this.shadowRoot.firstChild;
- }
-
- get innerDiv() {
- return this.shadowRoot.getElementById("innerDiv");
+ _animationFinished() {
+ let behavior = this.element.behavior;
+ let shouldLoop =
+ this._currentLoop > 1 || (this._currentLoop == -1 && behavior != "slide");
+ if (shouldLoop) {
+ if (this._currentLoop > 0) {
+ this._currentLoop--;
+ }
+ if (behavior == "alternate") {
+ this.dirsign = -this.dirsign;
+ }
+ this.doStop();
+ this.doStart();
+ }
}
get scrollDelayWithTruespeed() {
@@ -149,269 +99,249 @@ this.MarqueeBaseImplWidget = class {
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;
+ get slot() {
+ return this.shadowRoot.lastChild;
}
- _fireEvent(aName, aBubbles, aCancelable) {
- var e = this.document.createEvent("Events");
- e.initEvent(aName, aBubbles, aCancelable);
- this.element.dispatchEvent(e);
+ /**
+ * Computes CSS-derived values needed to compute the transform of the
+ * contents.
+ *
+ * In particular, it measures the auto width and height of the contents,
+ * and the effective width and height of the marquee itself, along with its
+ * css directionality (which affects the effective direction).
+ */
+ getMetrics() {
+ let slot = this.slot;
+ slot.style.width = "max-content";
+ let slotCS = this.window.getComputedStyle(slot);
+ let marqueeCS = this.window.getComputedStyle(this.element);
+ let contentWidth = parseFloat(slotCS.width) || 0;
+ let contentHeight = parseFloat(slotCS.height) || 0;
+ let marqueeWidth = parseFloat(marqueeCS.width) || 0;
+ let marqueeHeight = parseFloat(marqueeCS.height) || 0;
+ slot.style.width = "";
+ return {
+ contentWidth,
+ contentHeight,
+ marqueeWidth,
+ marqueeHeight,
+ cssDirection: marqueeCS.direction,
+ };
}
- _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;
+ /**
+ * Gets the layout metrics from getMetrics(), and returns an object
+ * describing the start, end, and axis of the animation for the given marquee
+ * behavior and direction.
+ */
+ getTransformParameters({
+ contentWidth,
+ contentHeight,
+ marqueeWidth,
+ marqueeHeight,
+ cssDirection,
+ }) {
+ const innerWidth = marqueeWidth - contentWidth;
+ const innerHeight = marqueeHeight - contentHeight;
+ const dir = this.element.direction;
+
+ let start = 0;
+ let end = 0;
+ const axis = dir == "up" || dir == "down" ? "y" : "x";
+ switch (this.element.behavior) {
+ case "alternate":
+ switch (dir) {
+ case "up":
+ case "down": {
+ if (innerHeight >= 0) {
+ start = innerHeight;
+ end = 0;
+ } else {
+ start = 0;
+ end = innerHeight;
+ }
+ if (dir == "down") {
+ [start, end] = [end, start];
+ }
+ if (this.dirsign == -1) {
+ [start, end] = [end, start];
+ }
+ break;
}
- 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;
+ case "right":
+ case "left":
+ default: {
+ if (innerWidth >= 0) {
+ start = innerWidth;
+ end = 0;
+ } else {
+ start = 0;
+ end = innerWidth;
+ }
+ if (dir == "right") {
+ [start, end] = [end, start];
+ }
+ if (cssDirection == "rtl") {
+ [start, end] = [end, start];
+ }
+ if (this.dirsign == -1) {
+ [start, end] = [end, start];
+ }
+ break;
}
- 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;
+ break;
+ case "slide":
+ switch (dir) {
+ case "up": {
+ start = marqueeHeight;
+ end = 0;
+ break;
}
- 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);
+ case "down": {
+ start = -contentHeight;
+ end = innerHeight;
+ break;
}
- if (rtl) {
- this.startAt = -this.startAt;
- this.stopAt = -this.stopAt;
- this.dirsign = -this.dirsign;
+ case "right":
+ default: {
+ let isRight = dir == "right";
+ if (cssDirection == "rtl") {
+ isRight = !isRight;
+ }
+ if (isRight) {
+ start = -contentWidth;
+ end = innerWidth;
+ } else {
+ start = marqueeWidth;
+ end = 0;
+ }
+ break;
}
- 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;
+ case "scroll":
+ default:
+ switch (dir) {
+ case "up":
+ case "down": {
+ start = marqueeHeight;
+ end = -contentHeight;
+ if (dir == "down") {
+ [start, end] = [end, start];
+ }
+ break;
}
- break;
-
- default:
- this.newPosition = this.startAt;
-
- if (
- this._currentDirection == "up" ||
- this._currentDirection == "down"
- ) {
- this.outerDiv.scrollTop = this.newPosition;
- } else {
- this.outerDiv.scrollLeft = this.newPosition;
+ case "right":
+ case "left":
+ default: {
+ start = marqueeWidth;
+ end = -contentWidth;
+ if (dir == "right") {
+ [start, end] = [end, start];
+ }
+ if (cssDirection == "rtl") {
+ [start, end] = [end, start];
+ }
+ break;
}
+ }
+ break;
+ }
+ return { start, end, axis };
+ }
- // dispatch start event, even when this._currentLoop == 1, comp. with IE6
- this._fireEvent("start", false, false);
+ /**
+ * Measures the marquee contents, and starts the marquee animation if needed.
+ * The translate animation is applied to the <slot> element.
+ * Bouncing and looping is implemented in the finish event handler for the
+ * given animation (see _animationFinished()).
+ */
+ doStart() {
+ if (this.animation) {
+ return;
+ }
+ let scrollAmount = this.element.scrollAmount;
+ if (!scrollAmount) {
+ return;
+ }
+ let metrics = this.getMetrics();
+ let { axis, start, end } = this.getTransformParameters(metrics);
+ let duration =
+ (Math.abs(end - start) * this.scrollDelayWithTruespeed) / scrollAmount;
+ let startValue = start + "px";
+ let endValue = end + "px";
+ if (axis == "y") {
+ startValue = "0 " + startValue;
+ endValue = "0 " + endValue;
+ }
+ // NOTE(emilio): It seems tempting to use `iterations` here, but doing so
+ // wouldn't be great because this uses current layout values (via
+ // getMetrics()), so sizes wouldn't update. This way we update once per
+ // animation iteration.
+ //
+ // fill: forwards is needed so that behavior=slide doesn't jump back to the
+ // start after the animation finishes.
+ this.animation = this.slot.animate(
+ {
+ translate: [startValue, endValue],
+ },
+ {
+ duration,
+ easing: "linear",
+ fill: "forwards",
}
+ );
+ this.animation.addEventListener("finish", this, { once: true });
+ }
- 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;
+ doStop() {
+ if (!this.animation) {
+ return;
}
-
- var myThis = this;
- var lambda = function myTimeOutFunction() {
- myThis._doMove(false);
- };
- this.runId = this.window.setTimeout(lambda, this.scrollDelayWithTruespeed);
+ if (this._restartScheduled) {
+ this.window.cancelAnimationFrame(this._restartScheduled);
+ this._restartScheduled = null;
+ }
+ this.animation.removeEventListener("finish", this);
+ this.animation.cancel();
+ this.animation = null;
}
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);
+ this.doStart();
}
_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;
- }
+ let mutation = aMutations.shift();
+ let attrName = mutation.attributeName.toLowerCase();
+ let oldValue = mutation.oldValue;
+ let newValue = this.element.getAttribute(attrName);
+ if (oldValue == newValue) {
+ continue;
+ }
+ if (attrName == "loop") {
+ this._currentLoop = this.element.loop;
+ }
+ if (attrName == "direction" || attrName == "behavior") {
+ this._scheduleRestartIfNeeded();
}
}
}
-};
-
-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>`;
+ // Schedule a restart with the new parameters if we're running.
+ _scheduleRestartIfNeeded() {
+ if (!this.animation || this._restartScheduled != null) {
+ return;
+ }
+ this._restartScheduled = this.window.requestAnimationFrame(() => {
+ if (this.animation) {
+ this.doStop();
+ this.doStart();
+ }
+ });
}
};