| /* |
| * Copyright (C) 2021-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 "ModalContainerObserver.h" |
| |
| #include "AccessibilityObject.h" |
| #include "Chrome.h" |
| #include "ChromeClient.h" |
| #include "Document.h" |
| #include "DocumentLoader.h" |
| #include "ElementIterator.h" |
| #include "EventHandler.h" |
| #include "EventLoop.h" |
| #include "EventNames.h" |
| #include "Frame.h" |
| #include "FrameView.h" |
| #include "HTMLAnchorElement.h" |
| #include "HTMLBodyElement.h" |
| #include "HTMLDocument.h" |
| #include "HTMLElement.h" |
| #include "HTMLImageElement.h" |
| #include "HTMLInputElement.h" |
| #include "HitTestResult.h" |
| #include "ModalContainerTypes.h" |
| #include "Page.h" |
| #include "RenderDescendantIterator.h" |
| #include "RenderText.h" |
| #include "RenderView.h" |
| #include "SimulatedClickOptions.h" |
| #include "Text.h" |
| #include <wtf/Noncopyable.h> |
| #include <wtf/RobinHoodHashSet.h> |
| #include <wtf/Scope.h> |
| #include <wtf/URL.h> |
| |
| namespace WebCore { |
| |
| static constexpr size_t maxLengthForClickableElementText = 100; |
| static constexpr double maxWidthForElementsThatLookClickable = 200; |
| static constexpr double maxHeightForElementsThatLookClickable = 100; |
| |
| bool ModalContainerObserver::isNeededFor(const Document& document) |
| { |
| RefPtr topDocumentLoader = document.topDocument().loader(); |
| if (!topDocumentLoader || topDocumentLoader->modalContainerObservationPolicy() == ModalContainerObservationPolicy::Disabled) |
| return false; |
| |
| if (!document.topDocument().url().protocolIsInHTTPFamily()) |
| return false; |
| |
| if (document.inDesignMode() || !is<HTMLDocument>(document)) |
| return false; |
| |
| auto* frame = document.frame(); |
| if (!frame) |
| return false; |
| |
| auto* page = frame->page(); |
| if (!page || page->isEditable()) |
| return false; |
| |
| if (RefPtr owner = document.ownerElement()) { |
| auto observer = owner->document().modalContainerObserverIfExists(); |
| return observer && observer->m_frameOwnersAndContainersToSearchAgain.contains(*owner); |
| } |
| |
| return true; |
| } |
| |
| ModalContainerObserver::ModalContainerObserver() |
| : m_collectClickableElementsTimer(*this, &ModalContainerObserver::collectClickableElementsTimerFired) |
| { |
| } |
| |
| ModalContainerObserver::~ModalContainerObserver() = default; |
| |
| static bool matchesSearchTerm(const Text& textNode, const AtomString& searchTerm) |
| { |
| RefPtr parent = textNode.parentElementInComposedTree(); |
| |
| static NeverDestroyed tagNamesToSearch = [] { |
| static constexpr std::array tags { |
| &HTMLNames::aTag, |
| &HTMLNames::divTag, |
| &HTMLNames::pTag, |
| &HTMLNames::spanTag, |
| &HTMLNames::sectionTag, |
| &HTMLNames::bTag, |
| &HTMLNames::iTag, |
| &HTMLNames::uTag, |
| &HTMLNames::liTag, |
| &HTMLNames::h1Tag, |
| &HTMLNames::h2Tag, |
| &HTMLNames::h3Tag, |
| &HTMLNames::h4Tag, |
| &HTMLNames::h5Tag, |
| &HTMLNames::h6Tag, |
| }; |
| MemoryCompactLookupOnlyRobinHoodHashSet<AtomString> set; |
| set.reserveInitialCapacity(std::size(tags)); |
| for (auto& tag : tags) |
| set.add(tag->get().localName()); |
| return set; |
| }(); |
| |
| if (!is<HTMLElement>(parent.get()) || !tagNamesToSearch.get().contains(downcast<HTMLElement>(*parent).localName())) |
| return false; |
| |
| if (LIKELY(!textNode.data().containsIgnoringASCIICase(searchTerm))) |
| return false; |
| |
| return true; |
| } |
| |
| static AccessibilityRole accessibilityRole(const HTMLElement& element) |
| { |
| return AccessibilityObject::ariaRoleToWebCoreRole(element.attributeWithoutSynchronization(HTMLNames::roleAttr)); |
| } |
| |
| static bool isInsideNavigationElement(const Text& textNode) |
| { |
| for (auto& parent : ancestorsOfType<HTMLElement>(textNode)) { |
| if (parent.hasTagName(HTMLNames::navTag) || accessibilityRole(parent) == AccessibilityRole::LandmarkNavigation) |
| return true; |
| } |
| return false; |
| } |
| |
| struct TextSearchResult { |
| bool foundMatch { false }; |
| bool containsAnyText { false }; |
| }; |
| |
| static TextSearchResult searchForMatch(RenderLayerModelObject& renderer, const AtomString& searchTerm) |
| { |
| TextSearchResult result; |
| for (auto& textRenderer : descendantsOfType<RenderText>(renderer)) { |
| result.containsAnyText = true; |
| if (RefPtr textNode = textRenderer.textNode(); textNode && matchesSearchTerm(*textNode, searchTerm)) { |
| result.foundMatch = !isInsideNavigationElement(*textNode); |
| return result; |
| } |
| } |
| return result; |
| } |
| |
| void ModalContainerObserver::searchForModalContainerOnBehalfOfFrameOwnerIfNeeded(HTMLFrameOwnerElement& owner) |
| { |
| auto containerToSearchAgain = m_frameOwnersAndContainersToSearchAgain.take(owner); |
| if (!containerToSearchAgain) |
| return; |
| |
| if (!m_elementsToIgnoreWhenSearching.remove(*containerToSearchAgain)) |
| return; |
| |
| if (RefPtr view = owner.document().view()) |
| updateModalContainerIfNeeded(*view); |
| } |
| |
| void ModalContainerObserver::updateModalContainerIfNeeded(const FrameView& view) |
| { |
| if (container()) |
| return; |
| |
| if (m_hasAttemptedToFulfillPolicy) |
| return; |
| |
| if (RefPtr owner = view.frame().ownerElement()) { |
| if (auto parentObserver = owner->document().modalContainerObserverIfExists()) |
| parentObserver->searchForModalContainerOnBehalfOfFrameOwnerIfNeeded(*owner); |
| return; |
| } |
| |
| if (!view.frame().isMainFrame()) |
| return; |
| |
| if (!view.hasViewportConstrainedObjects()) |
| return; |
| |
| auto searchTerm = ([&]() -> AtomString { |
| if (UNLIKELY(!m_overrideSearchTermForTesting.isNull())) |
| return m_overrideSearchTermForTesting; |
| |
| if (auto* page = view.frame().page()) |
| return page->chrome().client().searchStringForModalContainerObserver(); |
| |
| return nullAtom(); |
| })(); |
| |
| if (searchTerm.isNull()) |
| return; |
| |
| for (auto& renderer : *view.viewportConstrainedObjects()) { |
| if (renderer.isDocumentElementRenderer()) |
| continue; |
| |
| if (renderer.style().visibility() == Visibility::Hidden) |
| continue; |
| |
| RefPtr element = renderer.element(); |
| if (!element || is<HTMLBodyElement>(*element) || element->isDocumentNode()) |
| continue; |
| |
| if (m_elementsToIgnoreWhenSearching.contains(*element)) |
| continue; |
| |
| auto [foundMatch, containsAnyText] = searchForMatch(renderer, searchTerm); |
| |
| if (containsAnyText) |
| m_elementsToIgnoreWhenSearching.add(*element); |
| |
| if (foundMatch) { |
| setContainer(*element); |
| return; |
| } |
| |
| for (auto& frameOwner : descendantsOfType<HTMLFrameOwnerElement>(*element)) { |
| RefPtr contentFrame = frameOwner.contentFrame(); |
| if (!contentFrame) |
| continue; |
| |
| auto renderView = contentFrame->contentRenderer(); |
| if (!renderView) |
| continue; |
| |
| if (searchForMatch(*renderView, searchTerm).foundMatch) { |
| setContainer(*element, &frameOwner); |
| return; |
| } |
| |
| if (auto frameView = contentFrame->view(); frameView && !frameView->isVisuallyNonEmpty()) { |
| // If the subframe content has not become visually non-empty yet, search the subframe again later. |
| m_frameOwnersAndContainersToSearchAgain.add(frameOwner, *element); |
| } |
| } |
| } |
| } |
| |
| void ModalContainerObserver::setContainer(Element& newContainer, HTMLFrameOwnerElement* frameOwner) |
| { |
| if (container()) |
| container()->invalidateStyle(); |
| |
| if (m_userInteractionBlockingElement) |
| m_userInteractionBlockingElement->invalidateStyle(); |
| |
| m_userInteractionBlockingElement = { }; |
| m_containerAndFrameOwnerForControls = { { newContainer }, { frameOwner } }; |
| |
| newContainer.invalidateStyle(); |
| scheduleClickableElementCollection(); |
| |
| newContainer.document().eventLoop().queueTask(TaskSource::InternalAsyncTask, [weakContainer = WeakPtr { newContainer }]() mutable { |
| RefPtr container = weakContainer.get(); |
| if (!container) |
| return; |
| |
| auto observer = container->document().modalContainerObserverIfExists(); |
| if (!observer || container != observer->container()) |
| return; |
| |
| observer->hideUserInteractionBlockingElementIfNeeded(); |
| observer->makeBodyAndDocumentElementScrollableIfNeeded(); |
| }); |
| } |
| |
| Element* ModalContainerObserver::container() const |
| { |
| return m_containerAndFrameOwnerForControls.first.get(); |
| } |
| |
| HTMLFrameOwnerElement* ModalContainerObserver::frameOwnerForControls() const |
| { |
| return m_containerAndFrameOwnerForControls.second.get(); |
| } |
| |
| static bool listensForUserActivation(const Element& element) |
| { |
| return element.hasEventListeners(eventNames().clickEvent) || element.hasEventListeners(eventNames().mousedownEvent) || element.hasEventListeners(eventNames().mouseupEvent) |
| || element.hasEventListeners(eventNames().touchstartEvent) || element.hasEventListeners(eventNames().touchendEvent) |
| || element.hasEventListeners(eventNames().pointerdownEvent) || element.hasEventListeners(eventNames().pointerupEvent); |
| } |
| |
| enum class ContainerListensForUserActivation : bool { No, Yes }; |
| static bool isClickableControl(const HTMLElement& element, ContainerListensForUserActivation containerListensForUserActivation) |
| { |
| if (element.isActuallyDisabled()) |
| return false; |
| |
| if (!element.renderer()) |
| return false; |
| |
| if (element.hasTagName(HTMLNames::buttonTag)) |
| return true; |
| |
| if (is<HTMLInputElement>(element) && downcast<HTMLInputElement>(element).isTextButton()) |
| return true; |
| |
| switch (accessibilityRole(element)) { |
| case AccessibilityRole::Button: |
| return true; |
| case AccessibilityRole::CheckBox: |
| case AccessibilityRole::Switch: |
| return false; |
| default: |
| break; |
| } |
| |
| if (is<HTMLAnchorElement>(element)) { |
| // FIXME: We might need a more comprehensive policy here that attempts to click the link to detect navigation, |
| // but then immediately cancels the navigation. |
| auto href = downcast<HTMLAnchorElement>(element).href(); |
| return equalIgnoringFragmentIdentifier(element.document().url(), href) || !href.protocolIsInHTTPFamily(); |
| } |
| |
| if (listensForUserActivation(element)) |
| return true; |
| |
| if (containerListensForUserActivation == ContainerListensForUserActivation::No) |
| return false; |
| |
| auto rendererAndRect = element.boundingAbsoluteRectWithoutLayout(); |
| if (!rendererAndRect) |
| return false; |
| |
| auto [renderer, rect] = *rendererAndRect; |
| if (!renderer || rect.isEmpty()) |
| return false; |
| |
| // If the modal container itself has event listeners for user activation, continue looking for elements that look like |
| // clickable elements (e.g. small nodes with pointer-style cursor). |
| if (renderer->style().cursor() == CursorType::Pointer) { |
| if (rect.width() <= maxWidthForElementsThatLookClickable && rect.height() <= maxHeightForElementsThatLookClickable) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| static void removeParentOrChildElements(Vector<Ref<HTMLElement>>& elements) |
| { |
| HashSet<Ref<HTMLElement>> elementsToRemove; |
| for (auto& outer : elements) { |
| if (elementsToRemove.contains(outer)) |
| continue; |
| |
| for (auto& inner : elements) { |
| if (elementsToRemove.contains(inner) || outer.ptr() == inner.ptr() || !outer->contains(inner)) |
| continue; |
| |
| if (accessibilityRole(outer) == AccessibilityRole::Button) { |
| elementsToRemove.add(inner); |
| continue; |
| } |
| |
| if (accessibilityRole(inner) == AccessibilityRole::Button) { |
| elementsToRemove.add(outer); |
| continue; |
| } |
| |
| if (outer->hasTagName(HTMLNames::divTag) || outer->hasTagName(HTMLNames::spanTag) || outer->hasTagName(HTMLNames::pTag) || outer->hasTagName(HTMLNames::sectionTag)) |
| elementsToRemove.add(outer); |
| else |
| elementsToRemove.add(inner); |
| } |
| } |
| |
| elements.removeAllMatching([&] (auto& control) { |
| return elementsToRemove.contains(control); |
| }); |
| } |
| |
| static void removeElementsWithEmptyBounds(Vector<Ref<HTMLElement>>& elements) |
| { |
| elements.removeAllMatching([&] (auto& element) { |
| return element->boundingClientRect().isEmpty(); |
| }); |
| } |
| |
| static String textForControl(HTMLElement& control) |
| { |
| auto ariaLabel = control.attributeWithoutSynchronization(HTMLNames::aria_labelAttr); |
| if (!ariaLabel.isEmpty()) |
| return ariaLabel; |
| |
| if (is<HTMLInputElement>(control)) |
| return downcast<HTMLInputElement>(control).value(); |
| |
| auto title = control.title(); |
| if (!title.isEmpty()) |
| return title; |
| |
| if (is<HTMLImageElement>(control)) { |
| auto altText = downcast<HTMLImageElement>(control).altText(); |
| if (!altText.isEmpty()) |
| return altText; |
| } |
| |
| return control.outerText(); |
| } |
| |
| void ModalContainerObserver::scheduleClickableElementCollection() |
| { |
| m_collectClickableElementsTimer.startOneShot(200_ms); |
| } |
| |
| class ModalContainerPolicyDecisionScope { |
| WTF_MAKE_NONCOPYABLE(ModalContainerPolicyDecisionScope); |
| WTF_MAKE_FAST_ALLOCATED; |
| public: |
| ModalContainerPolicyDecisionScope(Document& document) |
| : m_document { document } |
| { |
| } |
| |
| ModalContainerPolicyDecisionScope(ModalContainerPolicyDecisionScope&&) = default; |
| |
| ~ModalContainerPolicyDecisionScope() |
| { |
| if (m_continueHidingModalContainerAfterScope || !m_document) |
| return; |
| |
| if (auto observer = m_document->modalContainerObserverIfExists()) |
| observer->revealModalContainer(); |
| } |
| |
| void continueHidingModalContainerAfterScope() { m_continueHidingModalContainerAfterScope = true; } |
| Document* document() const { return m_document.get(); } |
| |
| private: |
| WeakPtr<Document> m_document; |
| bool m_continueHidingModalContainerAfterScope { false }; |
| }; |
| |
| void ModalContainerObserver::collectClickableElementsTimerFired() |
| { |
| if (!container()) |
| return; |
| |
| container()->document().eventLoop().queueTask(TaskSource::InternalAsyncTask, [observer = this, decisionScope = ModalContainerPolicyDecisionScope { container()->document() }]() mutable { |
| RefPtr document = decisionScope.document(); |
| if (!document) |
| return; |
| |
| if (observer != document->modalContainerObserverIfExists() || !observer->container()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| auto [classifiableControls, controlTextsToClassify] = observer->collectClickableElements(); |
| if (classifiableControls.isEmpty()) |
| return; |
| |
| auto* page = document->page(); |
| if (!page) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| page->chrome().client().classifyModalContainerControls(WTFMove(controlTextsToClassify), [decisionScope = WTFMove(decisionScope), observer, controls = WTFMove(classifiableControls)] (auto&& types) mutable { |
| RefPtr document = decisionScope.document(); |
| if (!document) |
| return; |
| |
| RefPtr documentLoader = document->loader(); |
| if (!documentLoader) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (observer != document->modalContainerObserverIfExists()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (types.size() != controls.size()) |
| return; |
| |
| struct ClassifiedControls { |
| Vector<WeakPtr<HTMLElement>> positive; |
| Vector<WeakPtr<HTMLElement>> neutral; |
| Vector<WeakPtr<HTMLElement>> negative; |
| |
| HTMLElement* controlToClick(ModalContainerDecision decision) const |
| { |
| auto matchNonNull = [&](const WeakPtr<HTMLElement>& element) { |
| return !!element; |
| }; |
| |
| switch (decision) { |
| case ModalContainerDecision::Show: |
| case ModalContainerDecision::HideAndIgnore: |
| break; |
| case ModalContainerDecision::HideAndAllow: |
| if (auto index = positive.findIf(matchNonNull); index != notFound) |
| return positive[index].get(); |
| if (auto index = neutral.findIf(matchNonNull); index != notFound) |
| return neutral[index].get(); |
| break; |
| case ModalContainerDecision::HideAndDisallow: |
| if (auto index = negative.findIf(matchNonNull); index != notFound) |
| return negative[index].get(); |
| break; |
| } |
| return nullptr; |
| } |
| |
| OptionSet<ModalContainerControlType> types() const |
| { |
| OptionSet<ModalContainerControlType> availableTypesIgnoringOther; |
| if (!positive.isEmpty()) |
| availableTypesIgnoringOther.add(ModalContainerControlType::Positive); |
| if (!negative.isEmpty()) |
| availableTypesIgnoringOther.add(ModalContainerControlType::Negative); |
| if (!neutral.isEmpty()) |
| availableTypesIgnoringOther.add(ModalContainerControlType::Neutral); |
| return availableTypesIgnoringOther; |
| } |
| }; |
| |
| ClassifiedControls classifiedControls; |
| for (size_t index = 0; index < types.size(); ++index) { |
| auto control = controls[index]; |
| if (!control) |
| continue; |
| |
| switch (types[index]) { |
| case ModalContainerControlType::Positive: |
| classifiedControls.positive.append(control); |
| break; |
| case ModalContainerControlType::Negative: |
| classifiedControls.negative.append(control); |
| break; |
| case ModalContainerControlType::Neutral: |
| classifiedControls.neutral.append(control); |
| break; |
| case ModalContainerControlType::Other: |
| break; |
| } |
| } |
| |
| observer->m_hasAttemptedToFulfillPolicy = true; |
| |
| auto* page = document->page(); |
| if (!page) |
| return; |
| |
| auto clickableControlTypes = classifiedControls.types(); |
| if (clickableControlTypes.isEmpty()) |
| return; |
| |
| page->chrome().client().decidePolicyForModalContainer(clickableControlTypes, [decisionScope = WTFMove(decisionScope), observer, classifiedControls = WTFMove(classifiedControls)](auto decision) mutable { |
| RefPtr document = decisionScope.document(); |
| if (!document) |
| return; |
| |
| if (observer != document->modalContainerObserverIfExists()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| if (decision == ModalContainerDecision::Show) |
| return; |
| |
| if (RefPtr controlToClick = classifiedControls.controlToClick(decision)) { |
| observer->clearScrollabilityOverrides(*document); |
| controlToClick->dispatchSimulatedClick(nullptr, SendMouseUpDownEvents, DoNotShowPressedLook); |
| } |
| |
| decisionScope.continueHidingModalContainerAfterScope(); |
| }); |
| }); |
| }); |
| } |
| |
| void ModalContainerObserver::makeBodyAndDocumentElementScrollableIfNeeded() |
| { |
| if (!container()) |
| return; |
| |
| Ref document = container()->document(); |
| RefPtr view = document->view(); |
| if (!view || view->isScrollable()) |
| return; |
| |
| document->updateLayoutIgnorePendingStylesheets(); |
| |
| auto visibleHeight = view->visibleSize().height(); |
| auto shouldMakeElementScrollable = [visibleHeight] (Element* element) { |
| if (!element) |
| return false; |
| |
| auto renderer = element->renderer(); |
| if (!renderer || renderer->style().overflowY() != Overflow::Hidden) |
| return false; |
| |
| return element->boundingClientRect().height() > visibleHeight; |
| }; |
| |
| if (!m_makeBodyElementScrollable) { |
| if (RefPtr body = document->body(); shouldMakeElementScrollable(body.get())) { |
| m_makeBodyElementScrollable = true; |
| body->invalidateStyle(); |
| } |
| } |
| |
| if (!m_makeDocumentElementScrollable) { |
| if (RefPtr documentElement = document->documentElement(); shouldMakeElementScrollable(documentElement.get())) { |
| m_makeDocumentElementScrollable = true; |
| documentElement->invalidateStyle(); |
| } |
| } |
| } |
| |
| void ModalContainerObserver::clearScrollabilityOverrides(Document& document) |
| { |
| if (std::exchange(m_makeBodyElementScrollable, false)) { |
| if (auto element = document.body()) |
| element->invalidateStyle(); |
| } |
| |
| if (std::exchange(m_makeDocumentElementScrollable, false)) { |
| if (auto element = document.documentElement()) |
| element->invalidateStyle(); |
| } |
| } |
| |
| void ModalContainerObserver::hideUserInteractionBlockingElementIfNeeded() |
| { |
| if (m_userInteractionBlockingElement) |
| return; |
| |
| RefPtr container = this->container(); |
| if (!container) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| RefPtr view = container->document().view(); |
| if (!view) |
| return; |
| |
| auto fixedPositionRect = view->rectForFixedPositionLayout(); |
| if (fixedPositionRect.isEmpty()) |
| return; |
| |
| FixedVector locationsToHitTest { |
| fixedPositionRect.center(), |
| fixedPositionRect.minXMinYCorner() + LayoutSize { 1, 1 }, |
| fixedPositionRect.maxXMinYCorner() + LayoutSize { -1, 1 }, |
| fixedPositionRect.minXMaxYCorner() + LayoutSize { 1, -1 }, |
| fixedPositionRect.maxXMaxYCorner() + LayoutSize { -1, -1 } |
| }; |
| |
| constexpr OptionSet hitTestTypes { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::DisallowUserAgentShadowContent }; |
| |
| RefPtr<Element> foundElement; |
| for (auto& location : locationsToHitTest) { |
| auto hitTestResult = view->frame().eventHandler().hitTestResultAtPoint(location, hitTestTypes); |
| auto target = hitTestResult.targetElement(); |
| if (!target || is<HTMLBodyElement>(*target) || target->isDocumentNode()) |
| return; |
| |
| if (foundElement && foundElement != target) |
| return; |
| |
| auto renderer = target->renderer(); |
| if (!renderer || renderer->firstChild() || !renderer->style().hasViewportConstrainedPosition() || renderer->isDocumentElementRenderer()) |
| return; |
| |
| if (!foundElement) |
| foundElement = target; |
| } |
| |
| m_userInteractionBlockingElement = foundElement.get(); |
| foundElement->invalidateStyle(); |
| } |
| |
| void ModalContainerObserver::revealModalContainer() |
| { |
| auto [container, frameOwner] = std::exchange(m_containerAndFrameOwnerForControls, { }); |
| if (container) { |
| container->invalidateStyle(); |
| clearScrollabilityOverrides(container->document()); |
| } |
| |
| if (auto element = std::exchange(m_userInteractionBlockingElement, { })) |
| element->invalidateStyle(); |
| } |
| |
| std::pair<Vector<WeakPtr<HTMLElement>>, Vector<String>> ModalContainerObserver::collectClickableElements() |
| { |
| Ref container = *this->container(); |
| m_collectingClickableElements = true; |
| auto exitCollectClickableElementsScope = makeScopeExit([&] { |
| m_collectingClickableElements = false; |
| container->invalidateStyle(); |
| }); |
| |
| container->invalidateStyle(); |
| container->document().updateLayoutIgnorePendingStylesheets(); |
| |
| auto containerForControls = ([&]() -> RefPtr<Element> { |
| auto frameOwner = frameOwnerForControls(); |
| if (!frameOwner) |
| return container.ptr(); |
| |
| auto contentDocument = frameOwner->contentDocument(); |
| if (!contentDocument) |
| return { }; |
| |
| return contentDocument->documentElement(); |
| })(); |
| |
| if (!containerForControls) |
| return { }; |
| |
| auto containerListensForUserActivation = listensForUserActivation(*containerForControls) ? ContainerListensForUserActivation::Yes : ContainerListensForUserActivation::No; |
| Vector<Ref<HTMLElement>> clickableControls; |
| for (auto& child : descendantsOfType<HTMLElement>(*containerForControls)) { |
| if (isClickableControl(child, containerListensForUserActivation)) |
| clickableControls.append(child); |
| } |
| |
| removeElementsWithEmptyBounds(clickableControls); |
| removeParentOrChildElements(clickableControls); |
| |
| Vector<WeakPtr<HTMLElement>> classifiableControls; |
| Vector<String> controlTextsToClassify; |
| classifiableControls.reserveInitialCapacity(clickableControls.size()); |
| controlTextsToClassify.reserveInitialCapacity(clickableControls.size()); |
| for (auto& control : clickableControls) { |
| auto text = textForControl(control).stripWhiteSpace(); |
| if (!text.isEmpty() && text.length() < maxLengthForClickableElementText) { |
| classifiableControls.uncheckedAppend({ control }); |
| controlTextsToClassify.uncheckedAppend(WTFMove(text)); |
| } |
| } |
| return { WTFMove(classifiableControls), WTFMove(controlTextsToClassify) }; |
| } |
| |
| bool ModalContainerObserver::shouldMakeVerticallyScrollable(const Element& element) const |
| { |
| if (m_makeBodyElementScrollable && element.document().body() == &element) |
| return true; |
| |
| if (m_makeDocumentElementScrollable && element.document().documentElement() == &element) |
| return true; |
| |
| return false; |
| } |
| |
| } // namespace WebCore |