blob: b8f65725c831eba381d36ba27686bc61b924e6de [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// 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/numerics/clamped_math.h"
#include "base/time/time.h"
#include "third_party/blink/public/mojom/use_counter/metrics/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/bindings/core/v8/v8_intersection_observer_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_document_element.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_double_doublesequence.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_entry.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,
bool needs_initial_observation_with_detached_target)
: context_(context),
callback_(std::move(callback)),
delivery_behavior_(delivery_behavior),
needs_initial_observation_with_detached_target_(
needs_initial_observation_with_detached_target) {}
IntersectionObserverDelegateImpl(const IntersectionObserverDelegateImpl&) =
delete;
IntersectionObserverDelegateImpl& operator=(
const IntersectionObserverDelegateImpl&) = delete;
IntersectionObserver::DeliveryBehavior GetDeliveryBehavior() const override {
return delivery_behavior_;
}
bool NeedsInitialObservationWithDetachedTarget() const override {
return needs_initial_observation_with_detached_target_;
}
void Deliver(const HeapVector<Member<IntersectionObserverEntry>>& entries,
IntersectionObserver& observer) override {
callback_.Run(entries);
}
ExecutionContext* GetExecutionContext() const override {
return context_.Get();
}
void Trace(Visitor* visitor) const override {
IntersectionObserverDelegate::Trace(visitor);
visitor->Trace(context_);
}
private:
WeakMember<ExecutionContext> context_;
IntersectionObserver::EventCallback callback_;
IntersectionObserver::DeliveryBehavior delivery_behavior_;
bool needs_initial_observation_with_detached_target_;
};
void ParseMargin(const String& margin_parameter,
Vector<Length>& margin,
ExceptionState& exception_state,
const String& marginName) {
// 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(margin_parameter);
const auto tokens = tokenizer.TokenizeToEOF();
CSSParserTokenRange token_range(tokens);
token_range.ConsumeWhitespace();
while (token_range.Peek().GetType() != kEOFToken &&
!exception_state.HadException()) {
if (margin.size() == 4) {
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
"Extra text found at the end of " + marginName + "Margin.");
break;
}
const CSSParserToken& token = token_range.ConsumeIncludingWhitespace();
switch (token.GetType()) {
case kPercentageToken:
margin.push_back(Length::Percent(token.NumericValue()));
break;
case kDimensionToken:
switch (token.GetUnitType()) {
case CSSPrimitiveValue::UnitType::kPixels:
margin.push_back(
Length::Fixed(static_cast<int>(floor(token.NumericValue()))));
break;
case CSSPrimitiveValue::UnitType::kPercentage:
margin.push_back(Length::Percent(token.NumericValue()));
break;
default:
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
marginName + "Margin must be specified in pixels or percent.");
}
break;
default:
exception_state.ThrowDOMException(
DOMExceptionCode::kSyntaxError,
marginName + "Margin must be specified in pixels or percent.");
}
}
}
void ParseThresholds(
const V8UnionDoubleOrDoubleSequence* threshold_parameter,
Vector<float>& thresholds,
ExceptionState& exception_state) {
switch (threshold_parameter->GetContentType()) {
case V8UnionDoubleOrDoubleSequence::ContentType::kDouble:
thresholds.push_back(
base::MakeClampedNum<float>(threshold_parameter->GetAsDouble()));
break;
case V8UnionDoubleOrDoubleSequence::ContentType::kDoubleSequence:
for (auto threshold_value : threshold_parameter->GetAsDoubleSequence())
thresholds.push_back(base::MakeClampedNum<float>(threshold_value));
break;
}
if (thresholds.empty())
thresholds.push_back(0.f);
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());
}
// Returns a Vector of 4 margins (top, right, bottom, left) following
// https://drafts.csswg.org/css-box-4/#margin-shorthand
Vector<Length> NormalizeMargins(const Vector<Length>& margins) {
Vector<Length> normalized_margins(4, Length::Fixed(0));
switch (margins.size()) {
case 0:
break;
case 1:
normalized_margins[0] = normalized_margins[1] = normalized_margins[2] =
normalized_margins[3] = margins[0];
break;
case 2:
normalized_margins[0] = normalized_margins[2] = margins[0];
normalized_margins[1] = normalized_margins[3] = margins[1];
break;
case 3:
normalized_margins[0] = margins[0];
normalized_margins[1] = normalized_margins[3] = margins[1];
normalized_margins[2] = margins[2];
break;
case 4:
normalized_margins[0] = margins[0];
normalized_margins[1] = margins[1];
normalized_margins[2] = margins[2];
normalized_margins[3] = margins[3];
break;
default:
NOTREACHED();
break;
}
return normalized_margins;
}
Vector<Length> NormalizeScrollMargins(const Vector<Length>& margins) {
Vector<Length> normalized_margins = NormalizeMargins(margins);
if (std::all_of(normalized_margins.begin(), normalized_margins.end(),
[](const auto& m) { return m.IsZero(); })) {
return Vector<Length>();
}
return normalized_margins;
}
String StringifyMargin(const Vector<Length>& margin) {
StringBuilder string_builder;
const auto append_length = [&](const Length& length) {
string_builder.AppendNumber(length.IntValue());
if (length.IsPercent()) {
string_builder.Append('%');
} else {
string_builder.Append("px", 2);
}
};
if (margin.empty()) {
string_builder.Append("0px 0px 0px 0px");
} else {
DCHECK_EQ(margin.size(), 4u);
append_length(margin[0]);
string_builder.Append(' ');
append_length(margin[1]);
string_builder.Append(' ');
append_length(margin[2]);
string_builder.Append(' ');
append_length(margin[3]);
}
return string_builder.ToString();
}
} // anonymous namespace
static bool throttle_delay_enabled = true;
void IntersectionObserver::SetThrottleDelayEnabledForTesting(bool enabled) {
throttle_delay_enabled = enabled;
}
IntersectionObserver* IntersectionObserver::Create(
const IntersectionObserverInit* observer_init,
IntersectionObserverDelegate& delegate,
std::optional<LocalFrameUkmAggregator::MetricId> ukm_metric_id,
ExceptionState& exception_state) {
Node* root = nullptr;
if (observer_init->root()) {
switch (observer_init->root()->GetContentType()) {
case V8UnionDocumentOrElement::ContentType::kDocument:
root = observer_init->root()->GetAsDocument();
break;
case V8UnionDocumentOrElement::ContentType::kElement:
root = observer_init->root()->GetAsElement();
break;
}
}
Params params = {
.root = root,
.delay = observer_init->delay(),
.track_visibility = observer_init->trackVisibility(),
};
if (params.track_visibility && params.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;
}
ParseMargin(observer_init->rootMargin(), params.margin, exception_state,
"root");
if (exception_state.HadException()) {
return nullptr;
}
if (RuntimeEnabledFeatures::IntersectionObserverScrollMarginEnabled()) {
ParseMargin(observer_init->scrollMargin(), params.scroll_margin,
exception_state, "scroll");
if (exception_state.HadException()) {
return nullptr;
}
}
ParseThresholds(observer_init->threshold(), params.thresholds,
exception_state);
if (exception_state.HadException()) {
return nullptr;
}
return MakeGarbageCollected<IntersectionObserver>(delegate, ukm_metric_id,
std::move(params));
}
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,
LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
exception_state);
}
IntersectionObserver* IntersectionObserver::Create(
const Document& document,
EventCallback callback,
std::optional<LocalFrameUkmAggregator::MetricId> ukm_metric_id,
Params&& params) {
IntersectionObserverDelegateImpl* intersection_observer_delegate =
MakeGarbageCollected<IntersectionObserverDelegateImpl>(
document.GetExecutionContext(), std::move(callback), params.behavior,
params.needs_initial_observation_with_detached_target);
return MakeGarbageCollected<IntersectionObserver>(
*intersection_observer_delegate, ukm_metric_id, std::move(params));
}
IntersectionObserver::IntersectionObserver(
IntersectionObserverDelegate& delegate,
std::optional<LocalFrameUkmAggregator::MetricId> ukm_metric_id,
Params&& params)
: ActiveScriptWrappable<IntersectionObserver>({}),
ExecutionContextClient(delegate.GetExecutionContext()),
delegate_(&delegate),
ukm_metric_id_(ukm_metric_id),
root_(params.root),
thresholds_(std::move(params.thresholds)),
delay_(params.delay),
margin_(NormalizeMargins(params.margin)),
scroll_margin_(NormalizeScrollMargins(params.scroll_margin)),
margin_target_(params.margin_target),
root_is_implicit_(params.root ? 0 : 1),
track_visibility_(params.track_visibility),
track_fraction_of_root_(params.semantics == kFractionOfRoot),
always_report_root_bounds_(params.always_report_root_bounds),
use_overflow_clip_edge_(params.use_overflow_clip_edge) {
if (params.root) {
if (params.root->IsDocumentNode()) {
To<Document>(params.root)
->EnsureDocumentExplicitRootIntersectionObserverData()
.AddObserver(*this);
} else {
DCHECK(params.root->IsElementNode());
To<Element>(params.root)
->EnsureIntersectionObserverData()
.AddObserver(*this);
}
}
}
void IntersectionObserver::ProcessCustomWeakness(const LivenessBroker& info) {
// For explicit-root observers, if the root element disappears for any reason,
// any remaining obsevations must be dismantled.
if (root() && !info.IsHeapObjectAlive(root()))
root_ = nullptr;
if (!RootIsImplicit() && !root())
disconnect();
}
bool IntersectionObserver::RootIsValid() const {
return RootIsImplicit() || root();
}
void IntersectionObserver::InvalidateCachedRects() {
DCHECK(!RuntimeEnabledFeatures::IntersectionOptimizationEnabled());
for (auto& observation : observations_) {
observation->InvalidateCachedRects();
}
}
void IntersectionObserver::observe(Element* target,
ExceptionState& exception_state) {
if (!RootIsValid() || !target)
return;
if (target->EnsureIntersectionObserverData().GetObservationFor(*this))
return;
IntersectionObservation* observation =
MakeGarbageCollected<IntersectionObservation>(*this, *target);
target->EnsureIntersectionObserverData().AddObservation(*observation);
observations_.insert(observation);
if (root() && root()->isConnected()) {
root()
->GetDocument()
.EnsureIntersectionObserverController()
.AddTrackedObserver(*this);
}
if (target->isConnected()) {
target->GetDocument()
.EnsureIntersectionObserverController()
.AddTrackedObservation(*observation);
if (LocalFrameView* frame_view = target->GetDocument().View()) {
// The IntersectionObserver 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 if (delegate_->NeedsInitialObservationWithDetachedTarget()) {
std::optional<base::TimeTicks> monotonic_time;
std::optional<IntersectionGeometry::RootGeometry> root_geometry;
observation->ComputeIntersection(
IntersectionObservation::kImplicitRootObserversNeedUpdate |
IntersectionObservation::kExplicitRootObserversNeedUpdate |
IntersectionObservation::kIgnoreDelay,
IntersectionGeometry::kInfiniteScrollDelta, monotonic_time,
root_geometry);
}
}
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);
active_observations_.erase(observation);
if (root() && root()->isConnected() && observations_.empty()) {
root()
->GetDocument()
.EnsureIntersectionObserverController()
.RemoveTrackedObserver(*this);
}
}
void IntersectionObserver::disconnect(ExceptionState& exception_state) {
for (auto& observation : observations_)
observation->Disconnect();
observations_.clear();
active_observations_.clear();
if (root() && root()->isConnected()) {
root()
->GetDocument()
.EnsureIntersectionObserverController()
.RemoveTrackedObserver(*this);
}
}
HeapVector<Member<IntersectionObserverEntry>> IntersectionObserver::takeRecords(
ExceptionState& exception_state) {
HeapVector<Member<IntersectionObserverEntry>> entries;
for (auto& observation : observations_)
observation->TakeRecords(entries);
active_observations_.clear();
return entries;
}
String IntersectionObserver::rootMargin() const {
return StringifyMargin(RootMargin());
}
String IntersectionObserver::scrollMargin() const {
return StringifyMargin(ScrollMargin());
}
DOMHighResTimeStamp IntersectionObserver::GetEffectiveDelay() const {
return throttle_delay_enabled ? delay_ : 0;
}
DOMHighResTimeStamp IntersectionObserver::GetTimeStamp(
base::TimeTicks monotonic_time) const {
return DOMWindowPerformance::performance(
*To<LocalDOMWindow>(delegate_->GetExecutionContext()))
->MonotonicTimeToDOMHighResTimeStamp(monotonic_time);
}
int64_t IntersectionObserver::ComputeIntersections(
unsigned flags,
std::optional<base::TimeTicks>& monotonic_time,
gfx::Vector2dF accumulated_scroll_delta_since_last_update) {
DCHECK(!RootIsImplicit());
if (!RootIsValid() || !GetExecutionContext() || observations_.empty())
return 0;
std::optional<IntersectionGeometry::RootGeometry> root_geometry;
int64_t result = 0;
if (RuntimeEnabledFeatures::IntersectionOptimizationEnabled()) {
for (auto& observation : observations_) {
result += observation->ComputeIntersection(
flags, accumulated_scroll_delta_since_last_update, monotonic_time,
root_geometry);
}
} else {
// If we're processing post-layout deliveries only and we're not a
// post-layout delivery observer, then return early. Likewise, return if we
// need to compute non-post-layout-delivery observations but the observer
// behavior is post-layout.
bool post_layout_delivery_only =
flags & IntersectionObservation::kPostLayoutDeliveryOnly;
bool is_post_layout_delivery_observer =
GetDeliveryBehavior() ==
IntersectionObserver::kDeliverDuringPostLayoutSteps;
if (post_layout_delivery_only != is_post_layout_delivery_observer) {
return 0;
}
// TODO(szager): Is this copy necessary?
HeapVector<Member<IntersectionObservation>> observations_to_process(
observations_);
for (auto& observation : observations_to_process) {
result += observation->ComputeIntersection(flags, gfx::Vector2dF(),
monotonic_time, root_geometry);
}
}
return result;
}
bool IntersectionObserver::IsInternal() const {
return !GetUkmMetricId() ||
GetUkmMetricId() !=
LocalFrameUkmAggregator::kJavascriptIntersectionObserver;
}
void IntersectionObserver::ReportUpdates(IntersectionObservation& observation) {
DCHECK_EQ(observation.Observer(), this);
bool needs_scheduling = active_observations_.empty();
active_observations_.insert(&observation);
if (needs_scheduling) {
To<LocalDOMWindow>(GetExecutionContext())
->document()
->EnsureIntersectionObserverController()
.ScheduleIntersectionObserverForDelivery(*this);
}
}
IntersectionObserver::DeliveryBehavior
IntersectionObserver::GetDeliveryBehavior() const {
return delegate_->GetDeliveryBehavior();
}
void IntersectionObserver::Deliver() {
if (!NeedsDelivery())
return;
HeapVector<Member<IntersectionObserverEntry>> entries;
for (auto& observation : observations_)
observation->TakeRecords(entries);
active_observations_.clear();
if (entries.size())
delegate_->Deliver(entries, *this);
}
bool IntersectionObserver::HasPendingActivity() const {
return NeedsDelivery();
}
void IntersectionObserver::Trace(Visitor* visitor) const {
visitor->template RegisterWeakCallbackMethod<
IntersectionObserver, &IntersectionObserver::ProcessCustomWeakness>(this);
visitor->Trace(delegate_);
visitor->Trace(observations_);
visitor->Trace(active_observations_);
ScriptWrappable::Trace(visitor);
ExecutionContextClient::Trace(visitor);
}
} // namespace blink