| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * Copyright (C) 2003, 2007, 2010 Apple Inc. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| * |
| */ |
| |
| #include "core/html/HTMLMarqueeElement.h" |
| |
| #include "bindings/core/v8/ExceptionStatePlaceholder.h" |
| #include "bindings/core/v8/V8HTMLMarqueeElement.h" |
| #include "core/CSSPropertyNames.h" |
| #include "core/HTMLNames.h" |
| #include "core/animation/DocumentTimeline.h" |
| #include "core/animation/KeyframeEffect.h" |
| #include "core/animation/KeyframeEffectModel.h" |
| #include "core/animation/KeyframeEffectOptions.h" |
| #include "core/animation/StringKeyframe.h" |
| #include "core/animation/TimingInput.h" |
| #include "core/css/CSSStyleDeclaration.h" |
| #include "core/css/StylePropertySet.h" |
| #include "core/dom/Document.h" |
| #include "core/dom/FrameRequestCallback.h" |
| #include "core/dom/shadow/ShadowRoot.h" |
| #include "core/frame/LocalDOMWindow.h" |
| #include "core/frame/UseCounter.h" |
| #include "core/html/HTMLContentElement.h" |
| #include "core/html/HTMLDivElement.h" |
| #include "core/html/HTMLStyleElement.h" |
| #include "wtf/Noncopyable.h" |
| #include <cstdlib> |
| |
| namespace blink { |
| |
| inline HTMLMarqueeElement::HTMLMarqueeElement(Document& document) |
| : HTMLElement(HTMLNames::marqueeTag, document) { |
| UseCounter::count(document, UseCounter::HTMLMarqueeElement); |
| } |
| |
| HTMLMarqueeElement* HTMLMarqueeElement::create(Document& document) { |
| HTMLMarqueeElement* marqueeElement = new HTMLMarqueeElement(document); |
| marqueeElement->ensureUserAgentShadowRoot(); |
| return marqueeElement; |
| } |
| |
| void HTMLMarqueeElement::didAddUserAgentShadowRoot(ShadowRoot& shadowRoot) { |
| Element* style = HTMLStyleElement::create(document(), false); |
| style->setTextContent( |
| ":host { display: inline-block; overflow: hidden;" |
| "text-align: initial; white-space: nowrap; }" |
| ":host([direction=\"up\"]), :host([direction=\"down\"]) { overflow: " |
| "initial; overflow-y: hidden; white-space: initial; }" |
| ":host > div { will-change: transform; }"); |
| shadowRoot.appendChild(style); |
| |
| Element* mover = HTMLDivElement::create(document()); |
| shadowRoot.appendChild(mover); |
| |
| mover->appendChild(HTMLContentElement::create(document())); |
| m_mover = mover; |
| } |
| |
| class HTMLMarqueeElement::RequestAnimationFrameCallback final |
| : public FrameRequestCallback { |
| WTF_MAKE_NONCOPYABLE(RequestAnimationFrameCallback); |
| |
| public: |
| explicit RequestAnimationFrameCallback(HTMLMarqueeElement* marquee) |
| : m_marquee(marquee) {} |
| |
| void handleEvent(double) override { |
| m_marquee->m_continueCallbackRequestId = 0; |
| m_marquee->continueAnimation(); |
| } |
| |
| DEFINE_INLINE_VIRTUAL_TRACE() { |
| visitor->trace(m_marquee); |
| FrameRequestCallback::trace(visitor); |
| } |
| |
| private: |
| Member<HTMLMarqueeElement> m_marquee; |
| }; |
| |
| class HTMLMarqueeElement::AnimationFinished final : public EventListener { |
| WTF_MAKE_NONCOPYABLE(AnimationFinished); |
| |
| public: |
| explicit AnimationFinished(HTMLMarqueeElement* marquee) |
| : EventListener(CPPEventListenerType), m_marquee(marquee) {} |
| |
| bool operator==(const EventListener& that) const override { |
| return this == &that; |
| } |
| |
| void handleEvent(ExecutionContext*, Event*) override { |
| ++m_marquee->m_loopCount; |
| m_marquee->start(); |
| } |
| |
| DEFINE_INLINE_VIRTUAL_TRACE() { |
| visitor->trace(m_marquee); |
| EventListener::trace(visitor); |
| } |
| |
| private: |
| Member<HTMLMarqueeElement> m_marquee; |
| }; |
| |
| Node::InsertionNotificationRequest HTMLMarqueeElement::insertedInto( |
| ContainerNode* insertionPoint) { |
| HTMLElement::insertedInto(insertionPoint); |
| |
| if (isConnected()) |
| start(); |
| |
| return InsertionDone; |
| } |
| |
| void HTMLMarqueeElement::removedFrom(ContainerNode* insertionPoint) { |
| HTMLElement::removedFrom(insertionPoint); |
| if (insertionPoint->isConnected()) { |
| stop(); |
| } |
| } |
| |
| bool HTMLMarqueeElement::isHorizontal() const { |
| Direction direction = getDirection(); |
| return direction != kUp && direction != kDown; |
| } |
| |
| int HTMLMarqueeElement::scrollAmount() const { |
| bool ok; |
| int scrollAmount = fastGetAttribute(HTMLNames::scrollamountAttr).toInt(&ok); |
| if (!ok || scrollAmount < 0) |
| return kDefaultScrollAmount; |
| return scrollAmount; |
| } |
| |
| void HTMLMarqueeElement::setScrollAmount(int value, |
| ExceptionState& exceptionState) { |
| if (value < 0) { |
| exceptionState.throwDOMException( |
| IndexSizeError, |
| "The provided value (" + String::number(value) + ") is negative."); |
| return; |
| } |
| setIntegralAttribute(HTMLNames::scrollamountAttr, value); |
| } |
| |
| int HTMLMarqueeElement::scrollDelay() const { |
| bool ok; |
| int scrollDelay = fastGetAttribute(HTMLNames::scrolldelayAttr).toInt(&ok); |
| if (!ok || scrollDelay < 0) |
| return kDefaultScrollDelayMS; |
| return scrollDelay; |
| } |
| |
| void HTMLMarqueeElement::setScrollDelay(int value, |
| ExceptionState& exceptionState) { |
| if (value < 0) { |
| exceptionState.throwDOMException( |
| IndexSizeError, |
| "The provided value (" + String::number(value) + ") is negative."); |
| return; |
| } |
| setIntegralAttribute(HTMLNames::scrolldelayAttr, value); |
| } |
| |
| int HTMLMarqueeElement::loop() const { |
| bool ok; |
| int loop = fastGetAttribute(HTMLNames::loopAttr).toInt(&ok); |
| if (!ok || loop <= 0) |
| return kDefaultLoopLimit; |
| return loop; |
| } |
| |
| void HTMLMarqueeElement::setLoop(int value, ExceptionState& exceptionState) { |
| if (value <= 0 && value != -1) { |
| exceptionState.throwDOMException( |
| IndexSizeError, "The provided value (" + String::number(value) + |
| ") is neither positive nor -1."); |
| return; |
| } |
| setIntegralAttribute(HTMLNames::loopAttr, value); |
| } |
| |
| void HTMLMarqueeElement::start() { |
| if (m_continueCallbackRequestId) |
| return; |
| |
| RequestAnimationFrameCallback* callback = |
| new RequestAnimationFrameCallback(this); |
| m_continueCallbackRequestId = document().requestAnimationFrame(callback); |
| } |
| |
| void HTMLMarqueeElement::stop() { |
| if (m_continueCallbackRequestId) { |
| document().cancelAnimationFrame(m_continueCallbackRequestId); |
| m_continueCallbackRequestId = 0; |
| return; |
| } |
| |
| if (m_player) |
| m_player->pause(); |
| } |
| |
| bool HTMLMarqueeElement::isPresentationAttribute( |
| const QualifiedName& attr) const { |
| if (attr == HTMLNames::bgcolorAttr || attr == HTMLNames::heightAttr || |
| attr == HTMLNames::hspaceAttr || attr == HTMLNames::vspaceAttr || |
| attr == HTMLNames::widthAttr) { |
| return true; |
| } |
| return HTMLElement::isPresentationAttribute(attr); |
| } |
| |
| void HTMLMarqueeElement::collectStyleForPresentationAttribute( |
| const QualifiedName& attr, |
| const AtomicString& value, |
| MutableStylePropertySet* style) { |
| if (attr == HTMLNames::bgcolorAttr) { |
| addHTMLColorToStyle(style, CSSPropertyBackgroundColor, value); |
| } else if (attr == HTMLNames::heightAttr) { |
| addHTMLLengthToStyle(style, CSSPropertyHeight, value); |
| } else if (attr == HTMLNames::hspaceAttr) { |
| addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value); |
| addHTMLLengthToStyle(style, CSSPropertyMarginRight, value); |
| } else if (attr == HTMLNames::vspaceAttr) { |
| addHTMLLengthToStyle(style, CSSPropertyMarginTop, value); |
| addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value); |
| } else if (attr == HTMLNames::widthAttr) { |
| addHTMLLengthToStyle(style, CSSPropertyWidth, value); |
| } else { |
| HTMLElement::collectStyleForPresentationAttribute(attr, value, style); |
| } |
| } |
| |
| StringKeyframeEffectModel* HTMLMarqueeElement::createEffectModel( |
| const AnimationParameters& parameters) { |
| StyleSheetContents* styleSheetContents = |
| m_mover->document().elementSheet().contents(); |
| MutableStylePropertySet::SetResult setResult; |
| |
| RefPtr<StringKeyframe> keyframe1 = StringKeyframe::create(); |
| setResult = keyframe1->setCSSPropertyValue( |
| CSSPropertyTransform, parameters.transformBegin, styleSheetContents); |
| DCHECK(setResult.didParse); |
| |
| RefPtr<StringKeyframe> keyframe2 = StringKeyframe::create(); |
| setResult = keyframe2->setCSSPropertyValue( |
| CSSPropertyTransform, parameters.transformEnd, styleSheetContents); |
| DCHECK(setResult.didParse); |
| |
| return StringKeyframeEffectModel::create( |
| {std::move(keyframe1), std::move(keyframe2)}, |
| LinearTimingFunction::shared()); |
| } |
| |
| void HTMLMarqueeElement::continueAnimation() { |
| if (!shouldContinue()) |
| return; |
| |
| if (m_player && m_player->playState() == "paused") { |
| m_player->play(); |
| return; |
| } |
| |
| AnimationParameters parameters = getAnimationParameters(); |
| int scrollDelay = this->scrollDelay(); |
| int scrollAmount = this->scrollAmount(); |
| |
| if (scrollDelay < kMinimumScrollDelayMS && |
| !fastHasAttribute(HTMLNames::truespeedAttr)) |
| scrollDelay = kDefaultScrollDelayMS; |
| double duration = 0; |
| if (scrollAmount) |
| duration = parameters.distance * scrollDelay / scrollAmount; |
| if (!duration) |
| return; |
| |
| StringKeyframeEffectModel* effectModel = createEffectModel(parameters); |
| Timing timing; |
| timing.fillMode = Timing::FillMode::FORWARDS; |
| TimingInput::setIterationDuration( |
| timing, UnrestrictedDoubleOrString::fromUnrestrictedDouble(duration), |
| ASSERT_NO_EXCEPTION); |
| |
| KeyframeEffect* keyframeEffect = |
| KeyframeEffect::create(m_mover, effectModel, timing); |
| Animation* player = m_mover->document().timeline().play(keyframeEffect); |
| player->setId(emptyString()); |
| player->setOnfinish(new AnimationFinished(this)); |
| |
| m_player = player; |
| } |
| |
| bool HTMLMarqueeElement::shouldContinue() { |
| int loopCount = loop(); |
| |
| // By default, slide loops only once. |
| if (loopCount <= 0 && getBehavior() == kSlide) |
| loopCount = 1; |
| |
| if (loopCount <= 0) |
| return true; |
| return m_loopCount < loopCount; |
| } |
| |
| HTMLMarqueeElement::Behavior HTMLMarqueeElement::getBehavior() const { |
| const AtomicString& behavior = fastGetAttribute(HTMLNames::behaviorAttr); |
| if (equalIgnoringASCIICase(behavior, "alternate")) |
| return kAlternate; |
| if (equalIgnoringASCIICase(behavior, "slide")) |
| return kSlide; |
| return kScroll; |
| } |
| |
| HTMLMarqueeElement::Direction HTMLMarqueeElement::getDirection() const { |
| const AtomicString& direction = fastGetAttribute(HTMLNames::directionAttr); |
| if (equalIgnoringASCIICase(direction, "down")) |
| return kDown; |
| if (equalIgnoringASCIICase(direction, "up")) |
| return kUp; |
| if (equalIgnoringASCIICase(direction, "right")) |
| return kRight; |
| return kLeft; |
| } |
| |
| HTMLMarqueeElement::Metrics HTMLMarqueeElement::getMetrics() { |
| Metrics metrics; |
| CSSStyleDeclaration* marqueeStyle = |
| document().domWindow()->getComputedStyle(this, String()); |
| // For marquees that are declared inline, getComputedStyle returns "auto" for |
| // width and height. Setting all the metrics to zero disables animation for |
| // inline marquees. |
| if (marqueeStyle->getPropertyValue("width") == "auto" && |
| marqueeStyle->getPropertyValue("height") == "auto") { |
| metrics.contentHeight = 0; |
| metrics.contentWidth = 0; |
| metrics.marqueeWidth = 0; |
| metrics.marqueeHeight = 0; |
| return metrics; |
| } |
| |
| if (isHorizontal()) { |
| m_mover->style()->setProperty(nullptr, "width", "-webkit-max-content", |
| "important", ASSERT_NO_EXCEPTION); |
| } else { |
| m_mover->style()->setProperty(nullptr, "height", "-webkit-max-content", |
| "important", ASSERT_NO_EXCEPTION); |
| } |
| CSSStyleDeclaration* moverStyle = |
| document().domWindow()->getComputedStyle(m_mover, String()); |
| |
| metrics.contentWidth = moverStyle->getPropertyValue("width").toDouble(); |
| metrics.contentHeight = moverStyle->getPropertyValue("height").toDouble(); |
| metrics.marqueeWidth = marqueeStyle->getPropertyValue("width").toDouble(); |
| metrics.marqueeHeight = marqueeStyle->getPropertyValue("height").toDouble(); |
| |
| if (isHorizontal()) { |
| m_mover->style()->removeProperty("width", ASSERT_NO_EXCEPTION); |
| } else { |
| m_mover->style()->removeProperty("height", ASSERT_NO_EXCEPTION); |
| } |
| |
| return metrics; |
| } |
| |
| HTMLMarqueeElement::AnimationParameters |
| HTMLMarqueeElement::getAnimationParameters() { |
| AnimationParameters parameters; |
| Metrics metrics = getMetrics(); |
| |
| double totalWidth = metrics.marqueeWidth + metrics.contentWidth; |
| double totalHeight = metrics.marqueeHeight + metrics.contentHeight; |
| |
| double innerWidth = metrics.marqueeWidth - metrics.contentWidth; |
| double innerHeight = metrics.marqueeHeight - metrics.contentHeight; |
| |
| switch (getBehavior()) { |
| case kAlternate: |
| switch (getDirection()) { |
| case kRight: |
| parameters.transformBegin = |
| createTransform(innerWidth >= 0 ? 0 : innerWidth); |
| parameters.transformEnd = |
| createTransform(innerWidth >= 0 ? innerWidth : 0); |
| parameters.distance = std::abs(innerWidth); |
| break; |
| case kUp: |
| parameters.transformBegin = |
| createTransform(innerHeight >= 0 ? innerHeight : 0); |
| parameters.transformEnd = |
| createTransform(innerHeight >= 0 ? 0 : innerHeight); |
| parameters.distance = std::abs(innerHeight); |
| break; |
| case kDown: |
| parameters.transformBegin = |
| createTransform(innerHeight >= 0 ? 0 : innerHeight); |
| parameters.transformEnd = |
| createTransform(innerHeight >= 0 ? innerHeight : 0); |
| parameters.distance = std::abs(innerHeight); |
| break; |
| case kLeft: |
| default: |
| parameters.transformBegin = |
| createTransform(innerWidth >= 0 ? innerWidth : 0); |
| parameters.transformEnd = |
| createTransform(innerWidth >= 0 ? 0 : innerWidth); |
| parameters.distance = std::abs(innerWidth); |
| } |
| |
| if (m_loopCount % 2) |
| std::swap(parameters.transformBegin, parameters.transformEnd); |
| break; |
| case kSlide: |
| switch (getDirection()) { |
| case kRight: |
| parameters.transformBegin = createTransform(-metrics.contentWidth); |
| parameters.transformEnd = createTransform(innerWidth); |
| parameters.distance = metrics.marqueeWidth; |
| break; |
| case kUp: |
| parameters.transformBegin = createTransform(metrics.marqueeHeight); |
| parameters.transformEnd = "translateY(0)"; |
| parameters.distance = metrics.marqueeHeight; |
| break; |
| case kDown: |
| parameters.transformBegin = createTransform(-metrics.contentHeight); |
| parameters.transformEnd = createTransform(innerHeight); |
| parameters.distance = metrics.marqueeHeight; |
| break; |
| case kLeft: |
| default: |
| parameters.transformBegin = createTransform(metrics.marqueeWidth); |
| parameters.transformEnd = "translateX(0)"; |
| parameters.distance = metrics.marqueeWidth; |
| } |
| break; |
| case kScroll: |
| default: |
| switch (getDirection()) { |
| case kRight: |
| parameters.transformBegin = createTransform(-metrics.contentWidth); |
| parameters.transformEnd = createTransform(metrics.marqueeWidth); |
| parameters.distance = totalWidth; |
| break; |
| case kUp: |
| parameters.transformBegin = createTransform(metrics.marqueeHeight); |
| parameters.transformEnd = createTransform(-metrics.contentHeight); |
| parameters.distance = totalHeight; |
| break; |
| case kDown: |
| parameters.transformBegin = createTransform(-metrics.contentHeight); |
| parameters.transformEnd = createTransform(metrics.marqueeHeight); |
| parameters.distance = totalHeight; |
| break; |
| case kLeft: |
| default: |
| parameters.transformBegin = createTransform(metrics.marqueeWidth); |
| parameters.transformEnd = createTransform(-metrics.contentWidth); |
| parameters.distance = totalWidth; |
| } |
| break; |
| } |
| |
| return parameters; |
| } |
| |
| AtomicString HTMLMarqueeElement::createTransform(double value) const { |
| char axis = isHorizontal() ? 'X' : 'Y'; |
| return String::format("translate%c(", axis) + |
| String::numberToStringECMAScript(value) + "px)"; |
| } |
| |
| DEFINE_TRACE(HTMLMarqueeElement) { |
| visitor->trace(m_mover); |
| visitor->trace(m_player); |
| HTMLElement::trace(visitor); |
| } |
| |
| } // namespace blink |