From 689cc639efdcadb3d6b7a6b3b0414b4c19d7e815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Tue, 9 Apr 2024 09:41:40 +0000 Subject: [PATCH] Bug 306344 - Use transform animations to implement marquee. r=smaug,firefox-animation-reviewers,boris This matches what chromium does, and is both simpler and prettier. The overflow: hidden enforcement also matches chromium / WebKit via: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/css/resolver/style_adjuster.cc;l=482;drc=88c8510c16d44e0dc8c07426db31aa5bb3c90a2b https://searchfox.org/wubkat/rev/473ca5f8512b88edd7e82c8783e7e09158f17ba1/Source/WebCore/style/StyleAdjuster.cpp#581-596 See also https://github.com/whatwg/html/pull/10243 and the resets.html change. Adding white-space: nowrap isn't strictly necessary, but also matches other implementations: https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/html/html_marquee_element.cc;l=66;drc=4aba604f0aae6fb93eab830b68765604e9a2cca0 (and same link as above on WebKit) Differential Revision: https://phabricator.services.mozilla.com/D206357 --- dom/html/HTMLMarqueeElement.cpp | 13 - dom/html/HTMLMarqueeElement.h | 4 - layout/generic/nsGfxScrollFrame.cpp | 12 +- layout/painting/crashtests/crashtests.list | 2 +- layout/reftests/bugs/404553-1-ref.html | 2 +- layout/reftests/bugs/404553-1.html | 2 +- layout/reftests/marquee/336736-1a-ref.html | 5 + .../{336736-1-ref.html => 336736-1b-ref.html} | 0 layout/reftests/marquee/reftest.list | 4 +- layout/style/res/html.css | 10 +- .../marquee-loop.html.ini | 3 - .../marquee-scrollamount.html.ini | 3 - .../marquee-scrolldelay.html.ini | 3 - .../marquee-overflow.html | 18 + .../form-controls/resets.html | 1 + toolkit/content/widgets/marquee.css | 22 +- toolkit/content/widgets/marquee.js | 546 ++++++++---------- 17 files changed, 291 insertions(+), 359 deletions(-) create mode 100644 layout/reftests/marquee/336736-1a-ref.html rename layout/reftests/marquee/{336736-1-ref.html => 336736-1b-ref.html} (100%) delete mode 100644 testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-loop.html.ini delete mode 100644 testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrollamount.html.ini delete mode 100644 testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrolldelay.html.ini create mode 100644 testing/web-platform/tests/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-overflow.html diff --git a/dom/html/HTMLMarqueeElement.cpp b/dom/html/HTMLMarqueeElement.cpp index ff428547d4478..8721086064cf9 100644 --- a/dom/html/HTMLMarqueeElement.cpp +++ b/dom/html/HTMLMarqueeElement.cpp @@ -107,19 +107,6 @@ bool HTMLMarqueeElement::ParseAttribute(int32_t aNamespaceID, aMaybeScriptedPrincipal, aResult); } -void HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, - const nsAttrValue* aValue, - const nsAttrValue* aOldValue, - nsIPrincipal* aMaybeScriptedPrincipal, - bool aNotify) { - if (IsInComposedDoc() && aNameSpaceID == kNameSpaceID_None && - aName == nsGkAtoms::direction) { - NotifyUAWidgetSetupOrChange(); - } - return nsGenericHTMLElement::AfterSetAttr( - aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); -} - void HTMLMarqueeElement::MapAttributesIntoRule( MappedDeclarationsBuilder& aBuilder) { nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder); diff --git a/dom/html/HTMLMarqueeElement.h b/dom/html/HTMLMarqueeElement.h index 6c491c548b56a..2abebdcd9d23c 100644 --- a/dom/html/HTMLMarqueeElement.h +++ b/dom/html/HTMLMarqueeElement.h @@ -102,10 +102,6 @@ class HTMLMarqueeElement final : public nsGenericHTMLElement { const nsAString& aValue, nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) override; - void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, - const nsAttrValue* aValue, const nsAttrValue* aOldValue, - nsIPrincipal* aMaybeScriptedPrincipal, - bool aNotify) override; NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; diff --git a/layout/generic/nsGfxScrollFrame.cpp b/layout/generic/nsGfxScrollFrame.cpp index 6ff392376bed4..2b0c53efe1b06 100644 --- a/layout/generic/nsGfxScrollFrame.cpp +++ b/layout/generic/nsGfxScrollFrame.cpp @@ -1265,17 +1265,7 @@ nsMargin nsHTMLScrollFrame::ComputeStableScrollbarGutter( // Legacy, this sucks! static bool IsMarqueeScrollbox(const nsIFrame& aScrollFrame) { - if (!aScrollFrame.GetContent()) { - return false; - } - if (MOZ_LIKELY(!aScrollFrame.GetContent()->HasBeenInUAWidget())) { - return false; - } - MOZ_ASSERT(aScrollFrame.GetParent() && - aScrollFrame.GetParent()->GetContent()); - return aScrollFrame.GetParent() && - HTMLMarqueeElement::FromNodeOrNull( - aScrollFrame.GetParent()->GetContent()); + return HTMLMarqueeElement::FromNodeOrNull(aScrollFrame.GetContent()); } /* virtual */ diff --git a/layout/painting/crashtests/crashtests.list b/layout/painting/crashtests/crashtests.list index af2df7830cfd8..954e2de6e22d7 100644 --- a/layout/painting/crashtests/crashtests.list +++ b/layout/painting/crashtests/crashtests.list @@ -18,7 +18,7 @@ load 1469472.html load 1477831-1.html load 1504033.html load 1514544-1.html -asserts(0-1) asserts-if(Android,0-2) load 1547420-1.html +asserts(0-5) load 1547420-1.html load 1549909.html load 1551389-1.html asserts(0-2) load 1555819-1.html diff --git a/layout/reftests/bugs/404553-1-ref.html b/layout/reftests/bugs/404553-1-ref.html index cfa0dc8147587..ac35a9e4b5f22 100644 --- a/layout/reftests/bugs/404553-1-ref.html +++ b/layout/reftests/bugs/404553-1-ref.html @@ -1 +1 @@ -
 
\ No newline at end of file +
 
diff --git a/layout/reftests/bugs/404553-1.html b/layout/reftests/bugs/404553-1.html index 692c63c67c1f2..ab63f1396ef68 100644 --- a/layout/reftests/bugs/404553-1.html +++ b/layout/reftests/bugs/404553-1.html @@ -1 +1 @@ -
 
\ No newline at end of file +<table><marquee behavior="alternate" scrollamount="0" style="background-color: lime; height: 50px;"><div style="background: green; width: 50px"> </div></marquee><span><title> diff --git a/layout/reftests/marquee/336736-1a-ref.html b/layout/reftests/marquee/336736-1a-ref.html new file mode 100644 index 0000000000000..690992b8a5849 --- /dev/null +++ b/layout/reftests/marquee/336736-1a-ref.html @@ -0,0 +1,5 @@ +<html> +<body dir="rtl"> +<div style="background: green; width: 50px"> </div> +</body> +</html> diff --git a/layout/reftests/marquee/336736-1-ref.html b/layout/reftests/marquee/336736-1b-ref.html similarity index 100% rename from layout/reftests/marquee/336736-1-ref.html rename to layout/reftests/marquee/336736-1b-ref.html diff --git a/layout/reftests/marquee/reftest.list b/layout/reftests/marquee/reftest.list index ac6772f6c9985..c7d2fafd30907 100644 --- a/layout/reftests/marquee/reftest.list +++ b/layout/reftests/marquee/reftest.list @@ -1,6 +1,6 @@ == 166591-dynamic-1.html 166591-dynamic-1-ref.html -fuzzy-if(Android,0-8,0-50) == 336736-1a.html 336736-1-ref.html -fuzzy-if(Android,0-8,0-50) == 336736-1b.html 336736-1-ref.html +fuzzy-if(Android,0-8,0-50) == 336736-1a.html 336736-1a-ref.html +fuzzy-if(Android,0-8,0-50) == 336736-1b.html 336736-1b-ref.html == 406073-1.html 406073-1-ref.html == 407016-2.html 407016-2-ref.html fuzzy-if(Android,0-8,0-220) == 413027-4.html 413027-4-ref.html diff --git a/layout/style/res/html.css b/layout/style/res/html.css index bb1afcbfd17cd..18dd1c4855e14 100644 --- a/layout/style/res/html.css +++ b/layout/style/res/html.css @@ -816,15 +816,21 @@ dialog::backdrop { background: rgba(0, 0, 0, 0.1); } +/* https://html.spec.whatwg.org/#the-marquee-element-2 */ marquee { - inline-size: -moz-available; display: inline-block; + text-align: initial; + overflow: hidden !important; + + /* See https://github.com/whatwg/html/issues/10249 */ + inline-size: -moz-available; vertical-align: text-bottom; - text-align: start; + white-space: nowrap; } marquee:is([direction="up"], [direction="down"]) { block-size: 200px; + white-space: unset; } /* Ruby */ diff --git a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-loop.html.ini b/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-loop.html.ini deleted file mode 100644 index 0a4bff7fe8657..0000000000000 --- a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-loop.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[marquee-loop.html] - expected: - if (os == "android") and fission: [OK, TIMEOUT] diff --git a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrollamount.html.ini b/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrollamount.html.ini deleted file mode 100644 index 003dfbe1f71ea..0000000000000 --- a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrollamount.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[marquee-scrollamount.html] - expected: - if (os == "android") and fission: [OK, TIMEOUT] diff --git a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrolldelay.html.ini b/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrolldelay.html.ini deleted file mode 100644 index b7ec124418fe1..0000000000000 --- a/testing/web-platform/meta/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-scrolldelay.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[marquee-scrolldelay.html] - expected: - if (os == "android") and fission: [OK, TIMEOUT] diff --git a/testing/web-platform/tests/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-overflow.html b/testing/web-platform/tests/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-overflow.html new file mode 100644 index 0000000000000..44b149952cdc9 --- /dev/null +++ b/testing/web-platform/tests/html/obsolete/requirements-for-implementations/the-marquee-element-0/marquee-overflow.html @@ -0,0 +1,18 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Marquee forces overflow: hidden + + + +  +  +  +  + + diff --git a/testing/web-platform/tests/html/rendering/non-replaced-elements/form-controls/resets.html b/testing/web-platform/tests/html/rendering/non-replaced-elements/form-controls/resets.html index db21188ee37bc..1a17aeac2d993 100644 --- a/testing/web-platform/tests/html/rendering/non-replaced-elements/form-controls/resets.html +++ b/testing/web-platform/tests/html/rendering/non-replaced-elements/form-controls/resets.html @@ -50,6 +50,7 @@ } input[type=hidden i] { display: none !important; } marquee { + overflow: hidden; text-align: initial; } table { display: table; box-sizing: border-box; } diff --git a/toolkit/content/widgets/marquee.css b/toolkit/content/widgets/marquee.css index b898cd0dce607..6cdb52ca0201e 100644 --- a/toolkit/content/widgets/marquee.css +++ b/toolkit/content/widgets/marquee.css @@ -2,25 +2,19 @@ * 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/. */ -.outerDiv { - overflow: hidden; - width: -moz-available; +slot { + display: block; + will-change: translate; } -.horizontal > .innerDiv { - width: max-content; - /* We want to create overflow of twice our available space. */ - padding: 0 100%; -} - -/* disable scrolling in contenteditable */ -:host(:read-write) .innerDiv { - padding: 0 !important; +/* Disable the animation on contenteditable */ +:host(:read-write) > slot { + translate: none !important; } /* When printing or when the user doesn't want movement, we disable scrolling */ @media print, (prefers-reduced-motion) { - .innerDiv { - padding: 0 !important; + slot { + translate: none !important; } } diff --git a/toolkit/content/widgets/marquee.js b/toolkit/content/widgets/marquee.js index 6afdb25def6de..a694ffdca1d0b 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 = ``; 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,255 +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; - } + get slot() { + return this.shadowRoot.lastChild; } - doStop() { - if (this.runId != 0) { - this._deltaStartStop = Date.now() - this._lastMoveDate; - this.window.clearTimeout(this.runId); - } - - this.runId = 0; + /** + * 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; - } - } // 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; + break; + case "scroll": + default: + switch (dir) { + case "up": + case "down": { + start = marqueeHeight; + end = -contentHeight; + if (dir == "down") { + [start, end] = [end, start]; + } + break; } - - break; - - case "slide": - if (this._currentLoop > 1) { - this.newPosition = this.startAt; + 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; - - default: - this.newPosition = this.startAt; + } + break; + } + return { start, end, axis }; + } - if ( - this._currentDirection == "up" || - this._currentDirection == "down" - ) { - this.outerDiv.scrollTop = this.newPosition; - } else { - this.outerDiv.scrollLeft = this.newPosition; - } + /** + * Measures the marquee contents, and starts the marquee animation if needed. + * The translate animation is applied to the 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(); - 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 = `
`; - } -}; - -this.MarqueeVerticalImplWidget = class extends MarqueeBaseImplWidget { - generateContent() { - // White-space isn't allowed because a marquee could be - // inside 'white-space: pre' - this.shadowRoot.innerHTML = `
`; + // 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(); + } + }); } };