blob: 993d6fc537057b4fa9dce771747ade8f0c3552a8 [file] [log] [blame]
// 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