| // Copyright 2019 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/page/scrolling/fragment_anchor.h" |
| |
| #include "third_party/blink/renderer/core/accessibility/ax_object_cache.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/scroll_into_view_options.h" |
| #include "third_party/blink/renderer/core/svg/svg_svg_element.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| constexpr char kCssFragmentIdentifierPrefix[] = "targetElement="; |
| constexpr size_t kCssFragmentIdentifierPrefixLength = |
| base::size(kCssFragmentIdentifierPrefix); |
| |
| bool ParseCSSFragmentIdentifier(const String& fragment, String* selector) { |
| size_t pos = fragment.Find(kCssFragmentIdentifierPrefix); |
| if (pos == 0) { |
| *selector = fragment.Substring(kCssFragmentIdentifierPrefixLength - 1); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| Element* FindCSSFragmentAnchor(const AtomicString& selector, |
| Document& document) { |
| DummyExceptionStateForTesting exception_state; |
| return document.QuerySelector(selector, exception_state); |
| } |
| |
| Node* FindAnchorFromFragment(const String& fragment, Document& doc) { |
| Element* anchor_node; |
| String selector; |
| if (RuntimeEnabledFeatures::CSSFragmentIdentifiersEnabled() && |
| ParseCSSFragmentIdentifier(fragment, &selector)) { |
| anchor_node = FindCSSFragmentAnchor(AtomicString(selector), doc); |
| } else { |
| anchor_node = doc.FindAnchor(fragment); |
| } |
| |
| // Implement the rule that "" and "top" both mean top of page as in other |
| // browsers. |
| if (!anchor_node && |
| (fragment.IsEmpty() || DeprecatedEqualIgnoringCase(fragment, "top"))) |
| return &doc; |
| |
| return anchor_node; |
| } |
| |
| } // namespace |
| |
| FragmentAnchor* FragmentAnchor::TryCreate(const KURL& url, |
| bool needs_invoke, |
| LocalFrame& frame) { |
| DCHECK(frame.GetDocument()); |
| Document& doc = *frame.GetDocument(); |
| |
| // If our URL has no ref, then we have no place we need to jump to. |
| // OTOH If CSS target was set previously, we want to set it to 0, recalc |
| // and possibly paint invalidation because :target pseudo class may have been |
| // set (see bug 11321). |
| // Similarly for svg, if we had a previous svgView() then we need to reset |
| // the initial view if we don't have a fragment. |
| if (!url.HasFragmentIdentifier() && !doc.CssTarget() && !doc.IsSVGDocument()) |
| return nullptr; |
| |
| String fragment = url.FragmentIdentifier(); |
| |
| Node* anchor_node = nullptr; |
| |
| // Try the raw fragment for HTML documents, but skip it for `svgView()`: |
| if (!doc.IsSVGDocument()) |
| anchor_node = FindAnchorFromFragment(fragment, doc); |
| |
| // https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-indicated-part-of-the-document |
| // 5. Let decodedFragment be the result of running UTF-8 decode without BOM |
| // on fragmentBytes. |
| if (!anchor_node) { |
| fragment = DecodeURLEscapeSequences(fragment, DecodeURLMode::kUTF8); |
| anchor_node = FindAnchorFromFragment(fragment, doc); |
| } |
| |
| // Setting to null will clear the current target. |
| Element* target = anchor_node && anchor_node->IsElementNode() |
| ? ToElement(anchor_node) |
| : nullptr; |
| doc.SetCSSTarget(target); |
| |
| if (doc.IsSVGDocument()) { |
| if (SVGSVGElement* svg = ToSVGSVGElementOrNull(doc.documentElement())) |
| svg->SetupInitialView(fragment, target); |
| } |
| |
| if (target) |
| target->DispatchActivateInvisibleEventIfNeeded(); |
| |
| if (doc.IsSVGDocument() && !frame.IsMainFrame()) |
| return nullptr; |
| |
| if (!anchor_node || !needs_invoke) |
| return nullptr; |
| |
| auto* anchor = MakeGarbageCollected<FragmentAnchor>(*anchor_node, frame); |
| |
| // If rendering isn't ready yet, we'll focus and scroll as part of the |
| // document lifecycle. |
| if (doc.IsRenderingReady()) { |
| anchor->ApplyFocusIfNeeded(); |
| |
| // Layout needs to be clean for scrolling but if layout is needed, we'll |
| // invoke after layout is completed so no need to do it here. Note, the |
| // view may have been detached by script run during focus() call. |
| if (frame.View() && !frame.View()->NeedsLayout()) |
| anchor->Invoke(); |
| } |
| |
| return anchor; |
| } |
| |
| FragmentAnchor::FragmentAnchor(Node& anchor_node, LocalFrame& frame) |
| : anchor_node_(&anchor_node), |
| frame_(&frame), |
| needs_focus_(!anchor_node.IsDocumentNode()) { |
| DCHECK(frame_->View()); |
| } |
| |
| bool FragmentAnchor::Invoke() { |
| if (!frame_ || !anchor_node_) |
| return false; |
| |
| // Don't remove the fragment anchor until focus has been applied. |
| if (!needs_invoke_) |
| return needs_focus_; |
| |
| Document& doc = *frame_->GetDocument(); |
| |
| if (!doc.IsRenderingReady() || !frame_->View()) |
| return true; |
| |
| Frame* boundary_frame = frame_->FindUnsafeParentScrollPropagationBoundary(); |
| |
| // FIXME: Handle RemoteFrames |
| if (boundary_frame && boundary_frame->IsLocalFrame()) { |
| ToLocalFrame(boundary_frame) |
| ->View() |
| ->SetSafeToPropagateScrollToParent(false); |
| } |
| |
| Element* element_to_scroll = anchor_node_->IsElementNode() |
| ? ToElement(anchor_node_) |
| : doc.documentElement(); |
| if (element_to_scroll) { |
| ScrollIntoViewOptions* options = ScrollIntoViewOptions::Create(); |
| options->setBlock("start"); |
| options->setInlinePosition("nearest"); |
| element_to_scroll->ScrollIntoViewNoVisualUpdate(options); |
| } |
| |
| if (boundary_frame && boundary_frame->IsLocalFrame()) { |
| ToLocalFrame(boundary_frame) |
| ->View() |
| ->SetSafeToPropagateScrollToParent(true); |
| } |
| |
| if (AXObjectCache* cache = doc.ExistingAXObjectCache()) |
| cache->HandleScrolledToAnchor(anchor_node_); |
| |
| // Scroll into view above will cause us to clear needs_invoke_ via the |
| // DidScroll so recompute it here. |
| needs_invoke_ = !doc.IsLoadCompleted() || needs_focus_; |
| |
| return needs_invoke_; |
| } |
| |
| void FragmentAnchor::DidScroll(ScrollType type) { |
| if (!IsExplicitScrollType(type)) |
| return; |
| |
| // If the user/page scrolled, avoid clobbering the scroll offset by removing |
| // the anchor on the next invocation. Note: we may get here as a result of |
| // calling Invoke() because of the ScrollIntoView but that's ok because |
| // needs_invoke_ is recomputed at the end of that method. |
| needs_invoke_ = false; |
| } |
| |
| void FragmentAnchor::DidCompleteLoad() { |
| DCHECK(frame_); |
| DCHECK(frame_->View()); |
| |
| // If there is a pending layout, the fragment anchor will be cleared when it |
| // finishes. |
| if (!frame_->View()->NeedsLayout()) |
| needs_invoke_ = false; |
| } |
| |
| void FragmentAnchor::Trace(blink::Visitor* visitor) { |
| visitor->Trace(anchor_node_); |
| visitor->Trace(frame_); |
| } |
| |
| void FragmentAnchor::PerformPreRafActions() { |
| ApplyFocusIfNeeded(); |
| } |
| |
| void FragmentAnchor::ApplyFocusIfNeeded() { |
| // SVG images can load synchronously during style recalc but it's ok to focus |
| // since we disallow scripting. For everything else, focus() could run script |
| // so make sure we're at a valid point to do so. |
| DCHECK(frame_->GetDocument()->IsSVGDocument() || |
| !ScriptForbiddenScope::IsScriptForbidden()); |
| |
| if (!needs_focus_) |
| return; |
| |
| if (!frame_->GetDocument()->IsRenderingReady()) |
| return; |
| |
| // If the anchor accepts keyboard focus and fragment scrolling is allowed, |
| // move focus there to aid users relying on keyboard navigation. |
| // If anchorNode is not focusable or fragment scrolling is not allowed, |
| // clear focus, which matches the behavior of other browsers. |
| frame_->GetDocument()->UpdateStyleAndLayoutTree(); |
| if (anchor_node_->IsElementNode() && ToElement(anchor_node_)->IsFocusable()) { |
| ToElement(anchor_node_)->focus(); |
| } else { |
| frame_->GetDocument()->SetSequentialFocusNavigationStartingPoint( |
| anchor_node_); |
| frame_->GetDocument()->ClearFocusedElement(); |
| } |
| needs_focus_ = false; |
| } |
| |
| } // namespace blink |