blob: e13296c8c4025dc4abdf1848763a284ae8bd859b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer.h"
#include <algorithm>
#include <limits>
#include "base/macros.h"
#include "base/numerics/clamped_math.h"
#include "third_party/blink/public/mojom/web_feature/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_intersection_observer_callback.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_intersection_observer_delegate.h"
#include "third_party/blink/renderer/core/css/parser/css_parser_token_range.h"
#include "third_party/blink/renderer/core/css/parser/css_tokenizer.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/inspector/console_message.h"
#include "third_party/blink/renderer/core/intersection_observer/element_intersection_observer_data.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_controller.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_delegate.h"
#include "third_party/blink/renderer/core/intersection_observer/intersection_observer_init.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/timing/dom_window_performance.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/timer.h"
namespace blink {
namespace {
// Internal implementation of IntersectionObserverDelegate when using
// IntersectionObserver with an EventCallback.
class IntersectionObserverDelegateImpl final
: public IntersectionObserverDelegate {
public:
IntersectionObserverDelegateImpl(
ExecutionContext* context,
IntersectionObserver::EventCallback callback,
IntersectionObserver::DeliveryBehavior delivery_behavior)
: context_(context),
callback_(std::move(callback)),
delivery_behavior_(delivery_behavior) {}
IntersectionObserver::DeliveryBehavior GetDeliveryBehavior() const override {
return delivery_behavior_;
}
void Deliver(const HeapVector<Member<IntersectionObserverEntry>>& entries,
IntersectionObserver& observer) override {
callback_.Run(entries);
}
ExecutionContext* GetExecutionContext() const override { return context_; }
void Trace(blink::Visitor* visitor) override {
IntersectionObserverDelegate::Trace(visitor);
visitor->Trace(context_);
}
private:
WeakMember<ExecutionContext> context_;
IntersectionObserver::EventCallback callback_;
IntersectionObserver::DeliveryBehavior delivery_behavior_;
DISALLOW_COPY_AND_ASSIGN(IntersectionObserverDelegateImpl);
};
void ParseRootMargin(String root_margin_parameter,
Vector<Length>& root_margin,
ExceptionState& exception_state) {
// TODO(szager): Make sure this exact syntax and behavior is spec-ed
// somewhere.
// The root margin argument accepts syntax similar to that for CSS margin:
//
// "1px" = top/right/bottom/left
// "1px 2px" = top/bottom left/right
// "1px 2px 3px" = top left/right bottom
// "1px 2px 3px 4px" = top left right bottom
CSSTokenizer tokenizer(root_margin_parameter);
const auto tokens = tokenizer.TokenizeToEOF();
CSSParserTokenRange token_range(tokens);
while (token_range.Peek().GetType() != kEOFToken &&
!exception_state.HadException()) {
if (root_margin.size() == 4) {
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"Extra text found at the end of rootMargin.");
break;
}
const CSSParserToken& token = token_range.ConsumeIncludingWhitespace();
switch (token.GetType()) {
case kPercentageToken:
root_margin.push_back(Length::Percent(token.NumericValue()));
break;
case kDimensionToken:
switch (token.GetUnitType()) {
case CSSPrimitiveValue::UnitType::kPixels:
root_margin.push_back(
Length::Fixed(static_cast<int>(floor(token.NumericValue()))));
break;
case CSSPrimitiveValue::UnitType::kPercentage:
root_margin.push_back(Length::Percent(token.NumericValue()));
break;
default:
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"rootMargin must be specified in pixels or percent.");
}
break;
default:
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"rootMargin must be specified in pixels or percent.");
}
}
}
void ParseThresholds(const DoubleOrDoubleSequence& threshold_parameter,
Vector<float>& thresholds,
ExceptionState& exception_state) {
if (threshold_parameter.IsDouble()) {
thresholds.push_back(
base::MakeClampedNum<float>(threshold_parameter.GetAsDouble()));
} else {
for (auto threshold_value : threshold_parameter.GetAsDoubleSequence())
thresholds.push_back(base::MakeClampedNum<float>(threshold_value));
}
for (auto threshold_value : thresholds) {
if (std::isnan(threshold_value) || threshold_value < 0.0 ||
threshold_value > 1.0) {
exception_state.ThrowRangeError(
"Threshold values must be numbers between 0 and 1");
break;
}
}
std::sort(thresholds.begin(), thresholds.end());
}
} // anonymous namespace
static bool throttle_delay_enabled = true;
const float IntersectionObserver::kMinimumThreshold =
std::numeric_limits<float>::min();
void IntersectionObserver::SetThrottleDelayEnabledForTesting(bool enabled) {
throttle_delay_enabled = enabled;
}
IntersectionObserver* IntersectionObserver::Create(
const IntersectionObserverInit* observer_init,
IntersectionObserverDelegate& delegate,
ExceptionState& exception_state) {
Element* root = observer_init->root();
DOMHighResTimeStamp delay = 0;
bool track_visibility = false;
if (RuntimeEnabledFeatures::IntersectionObserverV2Enabled()) {
delay = observer_init->delay();
track_visibility = observer_init->trackVisibility();
if (track_visibility && delay < 100) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
"To enable the 'trackVisibility' option, you must also use a "
"'delay' option with a value of at least 100. Visibility is more "
"expensive to compute than the basic intersection; enabling this "
"option may negatively affect your page's performance. Please make "
"sure you *really* need visibility tracking before enabling the "
"'trackVisibility' option.");
return nullptr;
}
}
Vector<Length> root_margin;
ParseRootMargin(observer_init->rootMargin(), root_margin, exception_state);
if (exception_state.HadException())
return nullptr;
Vector<float> thresholds;
ParseThresholds(observer_init->threshold(), thresholds, exception_state);
if (exception_state.HadException())
return nullptr;
return MakeGarbageCollected<IntersectionObserver>(
delegate, root, root_margin, thresholds, kFractionOfTarget, delay,
track_visibility, false);
}
IntersectionObserver* IntersectionObserver::Create(
ScriptState* script_state,
V8IntersectionObserverCallback* callback,
const IntersectionObserverInit* observer_init,
ExceptionState& exception_state) {
V8IntersectionObserverDelegate* delegate =
MakeGarbageCollected<V8IntersectionObserverDelegate>(callback,
script_state);
if (observer_init && observer_init->trackVisibility()) {
UseCounter::Count(delegate->GetExecutionContext(),
WebFeature::kIntersectionObserverV2);
}
return Create(observer_init, *delegate, exception_state);
}
IntersectionObserver* IntersectionObserver::Create(
const Vector<Length>& root_margin,
const Vector<float>& thresholds,
Document* document,
EventCallback callback,
DeliveryBehavior behavior,
ThresholdInterpretation semantics,
DOMHighResTimeStamp delay,
bool track_visibility,
bool always_report_root_bounds,
ExceptionState& exception_state) {
IntersectionObserverDelegateImpl* intersection_observer_delegate =
MakeGarbageCollected<IntersectionObserverDelegateImpl>(
document, std::move(callback), behavior);
return MakeGarbageCollected<IntersectionObserver>(
*intersection_observer_delegate, nullptr, root_margin, thresholds,
semantics, delay, track_visibility, always_report_root_bounds);
}
IntersectionObserver::IntersectionObserver(
IntersectionObserverDelegate& delegate,
Element* root,
const Vector<Length>& root_margin,
const Vector<float>& thresholds,
ThresholdInterpretation semantics,
DOMHighResTimeStamp delay,
bool track_visibility,
bool always_report_root_bounds)
: ContextClient(delegate.GetExecutionContext()),
delegate_(&delegate),
root_(root),
thresholds_(thresholds),
delay_(delay),
root_margin_(4, Length::Fixed(0)),
root_is_implicit_(root ? 0 : 1),
track_visibility_(track_visibility ? 1 : 0),
track_fraction_of_root_(semantics == kFractionOfRoot),
always_report_root_bounds_(always_report_root_bounds ? 1 : 0),
needs_delivery_(0) {
switch (root_margin.size()) {
case 0:
break;
case 1:
root_margin_[0] = root_margin_[1] = root_margin_[2] = root_margin_[3] =
root_margin[0];
break;
case 2:
root_margin_[0] = root_margin_[2] = root_margin[0];
root_margin_[1] = root_margin_[3] = root_margin[1];
break;
case 3:
root_margin_[0] = root_margin[0];
root_margin_[1] = root_margin_[3] = root_margin[1];
root_margin_[2] = root_margin[2];
break;
case 4:
root_margin_[0] = root_margin[0];
root_margin_[1] = root_margin[1];
root_margin_[2] = root_margin[2];
root_margin_[3] = root_margin[3];
break;
default:
NOTREACHED();
break;
}
if (root) {
root->EnsureIntersectionObserverData().AddObserver(*this);
root->GetDocument()
.EnsureIntersectionObserverController()
.AddTrackedElement(*root, track_visibility);
}
}
void IntersectionObserver::ProcessCustomWeakness(const WeakCallbackInfo& info) {
if (RootIsImplicit() || (root() && info.IsHeapObjectAlive(root())))
return;
DummyExceptionStateForTesting exception_state;
disconnect(exception_state);
root_ = nullptr;
}
bool IntersectionObserver::RootIsValid() const {
return RootIsImplicit() || root();
}
void IntersectionObserver::observe(Element* target,
ExceptionState& exception_state) {
if (!RootIsValid())
return;
if (!target || root() == target)
return;
LocalFrame* target_frame = target->GetDocument().GetFrame();
if (!target_frame)
return;
if (target->EnsureIntersectionObserverData().GetObservationFor(*this))
return;
IntersectionObservation* observation =
MakeGarbageCollected<IntersectionObservation>(*this, *target);
target->EnsureIntersectionObserverData().AddObservation(*observation);
observations_.insert(observation);
if (target->isConnected()) {
if (RootIsImplicit()) {
target->GetDocument()
.EnsureIntersectionObserverController()
.AddTrackedElement(*target, track_visibility_);
}
if (LocalFrameView* frame_view = target_frame->View()) {
// The IntersectionObsever spec requires that at least one observation
// be recorded after observe() is called, even if the frame is throttled.
frame_view->SetIntersectionObservationState(LocalFrameView::kRequired);
frame_view->ScheduleAnimation();
}
} else {
// The IntersectionObsever spec requires that at least one observation
// be recorded after observe() is called, even if the target is detached.
observation->ComputeIntersection(
IntersectionObservation::kImplicitRootObserversNeedUpdate |
IntersectionObservation::kExplicitRootObserversNeedUpdate |
IntersectionObservation::kIgnoreDelay);
}
}
void IntersectionObserver::unobserve(Element* target,
ExceptionState& exception_state) {
if (!target || !target->IntersectionObserverData())
return;
IntersectionObservation* observation =
target->IntersectionObserverData()->GetObservationFor(*this);
if (!observation)
return;
observation->Disconnect();
observations_.erase(observation);
}
void IntersectionObserver::disconnect(ExceptionState& exception_state) {
for (auto& observation : observations_)
observation->Disconnect();
observations_.clear();
}
HeapVector<Member<IntersectionObserverEntry>> IntersectionObserver::takeRecords(
ExceptionState& exception_state) {
needs_delivery_ = 0;
HeapVector<Member<IntersectionObserverEntry>> entries;
for (auto& observation : observations_)
observation->TakeRecords(entries);
return entries;
}
static void AppendLength(StringBuilder& string_builder, const Length& length) {
string_builder.AppendNumber(length.IntValue());
if (length.IsPercent())
string_builder.Append('%');
else
string_builder.Append("px", 2);
}
String IntersectionObserver::rootMargin() const {
StringBuilder string_builder;
AppendLength(string_builder, root_margin_[0]);
string_builder.Append(' ');
AppendLength(string_builder, root_margin_[1]);
string_builder.Append(' ');
AppendLength(string_builder, root_margin_[2]);
string_builder.Append(' ');
AppendLength(string_builder, root_margin_[3]);
return string_builder.ToString();
}
DOMHighResTimeStamp IntersectionObserver::GetEffectiveDelay() const {
return throttle_delay_enabled ? delay_ : 0;
}
DOMHighResTimeStamp IntersectionObserver::GetTimeStamp() const {
if (Document* document = To<Document>(delegate_->GetExecutionContext())) {
if (LocalDOMWindow* dom_window = document->domWindow())
return DOMWindowPerformance::performance(*dom_window)->now();
}
return -1;
}
bool IntersectionObserver::ComputeIntersections(unsigned flags) {
DCHECK(!RootIsImplicit());
if (!RootIsValid() || !GetExecutionContext() || observations_.IsEmpty())
return false;
IntersectionGeometry::RootGeometry root_geometry(
IntersectionGeometry::GetRootLayoutObjectForTarget(root(), nullptr),
root_margin_);
HeapVector<Member<IntersectionObservation>> observations_to_process;
// TODO(szager): Is this copy necessary?
CopyToVector(observations_, observations_to_process);
for (auto& observation : observations_to_process) {
observation->ComputeIntersection(root_geometry, flags);
}
return trackVisibility();
}
void IntersectionObserver::SetNeedsDelivery() {
if (needs_delivery_)
return;
needs_delivery_ = 1;
To<Document>(GetExecutionContext())
->EnsureIntersectionObserverController()
.ScheduleIntersectionObserverForDelivery(*this);
}
IntersectionObserver::DeliveryBehavior
IntersectionObserver::GetDeliveryBehavior() const {
return delegate_->GetDeliveryBehavior();
}
void IntersectionObserver::Deliver() {
if (!needs_delivery_)
return;
needs_delivery_ = 0;
HeapVector<Member<IntersectionObserverEntry>> entries;
for (auto& observation : observations_)
observation->TakeRecords(entries);
if (entries.size())
delegate_->Deliver(entries, *this);
}
bool IntersectionObserver::HasPendingActivity() const {
return !observations_.IsEmpty();
}
void IntersectionObserver::Trace(blink::Visitor* visitor) {
visitor->template RegisterWeakCallbackMethod<
IntersectionObserver, &IntersectionObserver::ProcessCustomWeakness>(this);
visitor->Trace(delegate_);
visitor->Trace(observations_);
ScriptWrappable::Trace(visitor);
ContextClient::Trace(visitor);
}
} // namespace blink