diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/page/scrolling/fragment_anchor.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/core/page/scrolling/fragment_anchor.cc | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/chromium/third_party/blink/renderer/core/page/scrolling/fragment_anchor.cc b/chromium/third_party/blink/renderer/core/page/scrolling/fragment_anchor.cc new file mode 100644 index 00000000000..993d6fc5370 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/page/scrolling/fragment_anchor.cc @@ -0,0 +1,242 @@ +// 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 |