blob: b3911cc1e6cfe84106f9b57f9047de260d4b9bf0 [file] [log] [blame]
/*
* 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