| // 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_stream.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/page/chrome_client.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) {} |
| IntersectionObserverDelegateImpl(const IntersectionObserverDelegateImpl&) = |
| delete; |
| IntersectionObserverDelegateImpl& operator=( |
| const IntersectionObserverDelegateImpl&) = delete; |
| |
| 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_.Get(); |
| } |
| |
| void Trace(Visitor* visitor) const override { |
| IntersectionObserverDelegate::Trace(visitor); |
| visitor->Trace(context_); |
| } |
| |
| private: |
| WeakMember<ExecutionContext> context_; |
| IntersectionObserver::EventCallback callback_; |
| IntersectionObserver::DeliveryBehavior delivery_behavior_; |
| }; |
| |
| void ParseMargin(const String& margin_parameter, |
| Vector<Length>& margin, |
| ExceptionState& exception_state, |
| const char* margin_name) { |
| // 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 |
| |
| CSSParserTokenStream stream(margin_parameter); |
| stream.ConsumeWhitespace(); |
| while (!stream.AtEnd()) { |
| if (margin.size() == 4) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kSyntaxError, |
| StrCat({"Extra text found at the end of ", margin_name, "Margin."})); |
| break; |
| } |
| const CSSParserToken& token = stream.Peek(); |
| if (token.GetType() == kPercentageToken) { |
| margin.push_back(Length::Percent(token.NumericValue())); |
| } else if (token.GetType() == kDimensionToken && |
| token.GetUnitType() == CSSPrimitiveValue::UnitType::kPixels) { |
| margin.push_back( |
| Length::Fixed(static_cast<int>(floor(token.NumericValue())))); |
| } else { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kSyntaxError, |
| StrCat( |
| {margin_name, "Margin must be specified in pixels or percent."})); |
| break; |
| } |
| stream.ConsumeIncludingWhitespace(); |
| } |
| } |
| |
| void ParseThresholds(const V8UnionDoubleOrDoubleSequence* threshold_parameter, |
| Vector<float>& thresholds, |
| ExceptionState& exception_state) { |
| switch (threshold_parameter->GetContentType()) { |
| case V8UnionDoubleOrDoubleSequence::ContentType::kDouble: |
| thresholds.push_back( |
| base::ClampedNumeric<float>(threshold_parameter->GetAsDouble())); |
| break; |
| case V8UnionDoubleOrDoubleSequence::ContentType::kDoubleSequence: |
| for (auto threshold_value : threshold_parameter->GetAsDoubleSequence()) |
| thresholds.push_back(base::ClampedNumeric<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(); |
| } |
| |
| 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) { |
| if (length.IsPercent()) { |
| string_builder.AppendNumber(length.Percent()); |
| string_builder.Append('%'); |
| } else { |
| string_builder.AppendNumber(static_cast<int>(length.Pixels())); |
| string_builder.Append(base::byte_span_from_cstring("px")); |
| } |
| }; |
| |
| 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 = base::Milliseconds(observer_init->delay()), |
| .track_visibility = observer_init->trackVisibility(), |
| }; |
| if (params.track_visibility && params.delay < base::Milliseconds(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; |
| } |
| |
| 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); |
| 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), |
| expose_occluder_id_(params.expose_occluder_id) { |
| 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::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() |
| ->GetDocument() |
| .EnsureIntersectionObserverController() |
| .AddTrackedObserver(*this); |
| } |
| 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(); |
| } |
| } |
| |
| 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() && 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() |
| ->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()); |
| } |
| |
| base::TimeDelta IntersectionObserver::GetEffectiveDelay() const { |
| return throttle_delay_enabled ? delay_ : base::TimeDelta(); |
| } |
| |
| 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 |