| /* |
| * Copyright (C) 2016-2022 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| |
| #include "IntersectionObserver.h" |
| |
| #include "CSSParserTokenRange.h" |
| #include "CSSPropertyParserHelpers.h" |
| #include "CSSTokenizer.h" |
| #include "DOMWindow.h" |
| #include "Element.h" |
| #include "FrameDestructionObserverInlines.h" |
| #include "InspectorInstrumentation.h" |
| #include "IntersectionObserverCallback.h" |
| #include "IntersectionObserverEntry.h" |
| #include "JSNodeCustom.h" |
| #include "Performance.h" |
| #include "WebCoreOpaqueRoot.h" |
| #include <JavaScriptCore/AbstractSlotVisitorInlines.h> |
| #include <wtf/Vector.h> |
| |
| namespace WebCore { |
| |
| static ExceptionOr<LengthBox> parseRootMargin(String& rootMargin) |
| { |
| CSSTokenizer tokenizer(rootMargin); |
| auto tokenRange = tokenizer.tokenRange(); |
| Vector<Length, 4> margins; |
| while (!tokenRange.atEnd()) { |
| if (margins.size() == 4) |
| return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': Extra text found at the end of rootMargin."_s }; |
| RefPtr<CSSPrimitiveValue> parsedValue = CSSPropertyParserHelpers::consumeLengthOrPercent(tokenRange, HTMLStandardMode, ValueRange::All); |
| if (!parsedValue || parsedValue->isCalculated()) |
| return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': rootMargin must be specified in pixels or percent."_s }; |
| if (parsedValue->isPercentage()) |
| margins.append(Length(parsedValue->doubleValue(), LengthType::Percent)); |
| else if (parsedValue->isPx()) |
| margins.append(Length(parsedValue->intValue(), LengthType::Fixed)); |
| else |
| return Exception { SyntaxError, "Failed to construct 'IntersectionObserver': rootMargin must be specified in pixels or percent."_s }; |
| } |
| switch (margins.size()) { |
| case 0: |
| for (unsigned i = 0; i < 4; ++i) |
| margins.append(Length(0, LengthType::Fixed)); |
| break; |
| case 1: |
| for (unsigned i = 0; i < 3; ++i) |
| margins.append(margins[0]); |
| break; |
| case 2: |
| margins.append(margins[0]); |
| margins.append(margins[1]); |
| break; |
| case 3: |
| margins.append(margins[1]); |
| break; |
| case 4: |
| break; |
| default: |
| ASSERT_NOT_REACHED(); |
| } |
| |
| return LengthBox(WTFMove(margins[0]), WTFMove(margins[1]), WTFMove(margins[2]), WTFMove(margins[3])); |
| } |
| |
| ExceptionOr<Ref<IntersectionObserver>> IntersectionObserver::create(Document& document, Ref<IntersectionObserverCallback>&& callback, IntersectionObserver::Init&& init) |
| { |
| RefPtr<ContainerNode> root; |
| if (init.root) { |
| WTF::switchOn(*init.root, [&root] (RefPtr<Element> element) { |
| root = element.get(); |
| }, [&root] (RefPtr<Document> document) { |
| root = document.get(); |
| }); |
| } |
| |
| auto rootMarginOrException = parseRootMargin(init.rootMargin); |
| if (rootMarginOrException.hasException()) |
| return rootMarginOrException.releaseException(); |
| |
| Vector<double> thresholds; |
| WTF::switchOn(init.threshold, [&thresholds] (double initThreshold) { |
| thresholds.append(initThreshold); |
| }, [&thresholds] (Vector<double>& initThresholds) { |
| thresholds = WTFMove(initThresholds); |
| }); |
| |
| for (auto threshold : thresholds) { |
| if (!(threshold >= 0 && threshold <= 1)) |
| return Exception { RangeError, "Failed to construct 'IntersectionObserver': all thresholds must lie in the range [0.0, 1.0]."_s }; |
| } |
| |
| return adoptRef(*new IntersectionObserver(document, WTFMove(callback), root.get(), rootMarginOrException.releaseReturnValue(), WTFMove(thresholds))); |
| } |
| |
| IntersectionObserver::IntersectionObserver(Document& document, Ref<IntersectionObserverCallback>&& callback, ContainerNode* root, LengthBox&& parsedRootMargin, Vector<double>&& thresholds) |
| : m_root(root) |
| , m_rootMargin(WTFMove(parsedRootMargin)) |
| , m_thresholds(WTFMove(thresholds)) |
| , m_callback(WTFMove(callback)) |
| { |
| if (is<Document>(root)) { |
| auto& observerData = downcast<Document>(*root).ensureIntersectionObserverData(); |
| observerData.observers.append(*this); |
| } else if (root) { |
| auto& observerData = downcast<Element>(*root).ensureIntersectionObserverData(); |
| observerData.observers.append(*this); |
| } else if (auto* frame = document.frame()) |
| m_implicitRootDocument = frame->mainFrame().document(); |
| |
| std::sort(m_thresholds.begin(), m_thresholds.end()); |
| } |
| |
| IntersectionObserver::~IntersectionObserver() |
| { |
| RefPtr root = m_root.get(); |
| if (is<Document>(root)) |
| downcast<Document>(*root).intersectionObserverDataIfExists()->observers.removeFirst(this); |
| else if (root) |
| downcast<Element>(*root).intersectionObserverDataIfExists()->observers.removeFirst(this); |
| disconnect(); |
| } |
| |
| String IntersectionObserver::rootMargin() const |
| { |
| StringBuilder stringBuilder; |
| for (auto side : allBoxSides) { |
| auto& length = m_rootMargin.at(side); |
| stringBuilder.append(length.intValue(), length.isPercent() ? "%" : "px", side != BoxSide::Left ? " " : ""); |
| } |
| return stringBuilder.toString(); |
| } |
| |
| bool IntersectionObserver::isObserving(const Element& element) const |
| { |
| return m_observationTargets.findIf([&](auto& target) { |
| return target.get() == &element; |
| }) != notFound; |
| } |
| |
| void IntersectionObserver::observe(Element& target) |
| { |
| if (!trackingDocument() || !m_callback || isObserving(target)) |
| return; |
| |
| target.ensureIntersectionObserverData().registrations.append({ *this, std::nullopt }); |
| bool hadObservationTargets = hasObservationTargets(); |
| m_observationTargets.append(target); |
| |
| // Per the specification, we should dispatch at least one observation for the target. For this reason, we make sure to keep the |
| // target alive until this first observation. This, in turn, will keep the IntersectionObserver's JS wrapper alive via |
| // isReachableFromOpaqueRoots(), so the callback stays alive. |
| m_targetsWaitingForFirstObservation.append(target); |
| |
| auto* document = trackingDocument(); |
| if (!hadObservationTargets) |
| document->addIntersectionObserver(*this); |
| document->scheduleInitialIntersectionObservationUpdate(); |
| } |
| |
| void IntersectionObserver::unobserve(Element& target) |
| { |
| if (!removeTargetRegistration(target)) |
| return; |
| |
| bool removed = m_observationTargets.removeFirst(&target); |
| ASSERT_UNUSED(removed, removed); |
| m_targetsWaitingForFirstObservation.removeFirstMatching([&](auto& pendingTarget) { return pendingTarget.ptr() == ⌖ }); |
| |
| if (!hasObservationTargets()) { |
| if (auto* document = trackingDocument()) |
| document->removeIntersectionObserver(*this); |
| } |
| } |
| |
| void IntersectionObserver::disconnect() |
| { |
| if (!hasObservationTargets()) { |
| ASSERT(m_targetsWaitingForFirstObservation.isEmpty()); |
| return; |
| } |
| |
| removeAllTargets(); |
| if (auto* document = trackingDocument()) |
| document->removeIntersectionObserver(*this); |
| } |
| |
| auto IntersectionObserver::takeRecords() -> TakenRecords |
| { |
| return { WTFMove(m_queuedEntries), WTFMove(m_pendingTargets) }; |
| } |
| |
| void IntersectionObserver::targetDestroyed(Element& target) |
| { |
| m_observationTargets.removeFirst(&target); |
| m_targetsWaitingForFirstObservation.removeFirstMatching([&](auto& pendingTarget) { return pendingTarget.ptr() == ⌖ }); |
| if (!hasObservationTargets()) { |
| if (auto* document = trackingDocument()) |
| document->removeIntersectionObserver(*this); |
| } |
| } |
| |
| bool IntersectionObserver::removeTargetRegistration(Element& target) |
| { |
| auto* observerData = target.intersectionObserverDataIfExists(); |
| if (!observerData) |
| return false; |
| |
| auto& registrations = observerData->registrations; |
| return registrations.removeFirstMatching([this](auto& registration) { |
| return registration.observer.get() == this; |
| }); |
| } |
| |
| void IntersectionObserver::removeAllTargets() |
| { |
| for (auto& target : m_observationTargets) { |
| bool removed = removeTargetRegistration(*target); |
| ASSERT_UNUSED(removed, removed); |
| } |
| m_observationTargets.clear(); |
| m_targetsWaitingForFirstObservation.clear(); |
| } |
| |
| void IntersectionObserver::rootDestroyed() |
| { |
| ASSERT(m_root); |
| disconnect(); |
| m_root = nullptr; |
| } |
| |
| std::optional<ReducedResolutionSeconds> IntersectionObserver::nowTimestamp() const |
| { |
| if (!m_callback) |
| return std::nullopt; |
| |
| auto* context = m_callback->scriptExecutionContext(); |
| if (!context) |
| return std::nullopt; |
| |
| ASSERT(context->isDocument()); |
| auto& document = downcast<Document>(*context); |
| if (auto* window = document.domWindow()) |
| return window->frozenNowTimestamp(); |
| |
| return std::nullopt; |
| } |
| |
| void IntersectionObserver::appendQueuedEntry(Ref<IntersectionObserverEntry>&& entry) |
| { |
| ASSERT(entry->target()); |
| m_pendingTargets.append(*entry->target()); |
| m_queuedEntries.append(WTFMove(entry)); |
| } |
| |
| void IntersectionObserver::notify() |
| { |
| if (m_queuedEntries.isEmpty()) { |
| ASSERT(m_pendingTargets.isEmpty()); |
| return; |
| } |
| |
| auto takenRecords = takeRecords(); |
| auto targetsWaitingForFirstObservation = std::exchange(m_targetsWaitingForFirstObservation, { }); |
| |
| // FIXME: The JSIntersectionObserver wrapper should be kept alive as long as the intersection observer can fire events. |
| ASSERT(m_callback->hasCallback()); |
| if (!m_callback->hasCallback()) |
| return; |
| |
| auto* context = m_callback->scriptExecutionContext(); |
| if (!context) |
| return; |
| |
| InspectorInstrumentation::willFireObserverCallback(*context, "IntersectionObserver"_s); |
| m_callback->handleEvent(*this, WTFMove(takenRecords.records), *this); |
| InspectorInstrumentation::didFireObserverCallback(*context); |
| } |
| |
| bool IntersectionObserver::isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor& visitor) const |
| { |
| for (auto& target : m_observationTargets) { |
| if (auto* element = target.get(); containsWebCoreOpaqueRoot(visitor, element)) |
| return true; |
| } |
| for (auto& target : m_pendingTargets) { |
| if (containsWebCoreOpaqueRoot(visitor, target.get())) |
| return true; |
| } |
| return !m_targetsWaitingForFirstObservation.isEmpty(); |
| } |
| |
| } // namespace WebCore |