diff options
Diffstat (limited to 'toolkit/content/widgets/marquee.js')
-rw-r--r-- | toolkit/content/widgets/marquee.js | 560 |
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(); + } + }); } }; |