forked from mirrors/gecko-dev
Bug 1895555 - Text Fragments: Implement same-document navigation. r=farre,dom-core
Same-document navigation follows a different code path than normal navigation and was therefore not covered in the initial implementation for text fragments. Same-document navigation does not set a URI in the `Document`, which is the way cross-document navigation would parse text directives from the URL. Instead, `nsDocShell::ScrollToAnchor()` is called via `nsDocShell::InternalLoad()`-> `nsDocShell::HandleSameDocumentNavigation()`. This code path needs to parse and remove the fragment directive from the new fragment to be able to find text fragments and to allow for element-id fallback. `nsDocShell::ScrollToAnchor()` needs to start an attempt to scroll to the text fragment if it exists. It must not, however, clear the uninvoked text directives, because a same-document navigation could happen before the document is fully loaded, hence the target text might not be part of the DOM tree. As per spec, a second attempt to scroll to the text fragment is done after the load is completed. This is done by `Document::ScrollToRef()`, which is called by `nsDocumentViewer::LoadComplete()` after the load has finished. This call will clear the uninvoked directives. Differential Revision: https://phabricator.services.mozilla.com/D209726
This commit is contained in:
parent
a774f79b6b
commit
8726853107
9 changed files with 91 additions and 44 deletions
|
|
@ -64,6 +64,7 @@
|
|||
#include "mozilla/dom/ContentFrameMessageManager.h"
|
||||
#include "mozilla/dom/DocGroup.h"
|
||||
#include "mozilla/dom/Element.h"
|
||||
#include "mozilla/dom/FragmentDirective.h"
|
||||
#include "mozilla/dom/HTMLAnchorElement.h"
|
||||
#include "mozilla/dom/HTMLIFrameElement.h"
|
||||
#include "mozilla/dom/PerformanceNavigation.h"
|
||||
|
|
@ -8476,6 +8477,17 @@ bool nsDocShell::IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
|
|||
rvURINew = aLoadState->URI()->GetHasRef(&aState.mNewURIHasRef);
|
||||
}
|
||||
|
||||
// A Fragment Directive must be removed from the new hash in order to allow
|
||||
// fallback element id scroll. Additionally, the extracted parsed text
|
||||
// directives need to be stored for further use.
|
||||
nsTArray<TextDirective> textDirectives;
|
||||
if (FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragmentString(
|
||||
aState.mNewHash, &textDirectives)) {
|
||||
if (Document* doc = GetDocument()) {
|
||||
doc->FragmentDirective()->SetTextDirectives(std::move(textDirectives));
|
||||
}
|
||||
}
|
||||
|
||||
if (currentURI && NS_SUCCEEDED(rvURINew)) {
|
||||
nsresult rvURIOld = currentURI->GetRef(aState.mCurrentHash);
|
||||
if (NS_SUCCEEDED(rvURIOld)) {
|
||||
|
|
@ -10701,6 +10713,24 @@ nsresult nsDocShell::ScrollToAnchor(bool aCurHasRef, bool aNewHasRef,
|
|||
rootScroll->ClearDidHistoryRestore();
|
||||
}
|
||||
|
||||
// If it's a load from history, we don't have any anchor jumping to do.
|
||||
// Scrollbar position will be restored by the caller based on positions stored
|
||||
// in session history.
|
||||
bool scroll = aLoadType != LOAD_HISTORY && aLoadType != LOAD_RELOAD_NORMAL;
|
||||
// If the load contains text directives, try to apply them. This may fail if
|
||||
// the load is a same-document load that was initiated before the document was
|
||||
// fully loaded and the target is not yet included in the DOM tree.
|
||||
// For this case, the `uninvokedTextDirectives` are not cleared, so that
|
||||
// `Document::ScrollToRef()` can re-apply the text directive.
|
||||
// `Document::ScrollToRef()` is (presumably) the second "async" call mentioned
|
||||
// in sec. 7.4.2.3.3 in the HTML spec, "Fragment navigations":
|
||||
// https://html.spec.whatwg.org/#scroll-to-fragid:~:text=This%20algorithm%20will%20be%20called%20twice
|
||||
const bool hasScrolledToTextFragment =
|
||||
presShell->HighlightAndGoToTextFragment(scroll);
|
||||
if (hasScrolledToTextFragment) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If we have no new anchor, we do not want to scroll, unless there is a
|
||||
// current anchor and we are doing a history load. So return if we have no
|
||||
// new anchor, and there is no current anchor or the load is not a history
|
||||
|
|
@ -10712,11 +10742,6 @@ nsresult nsDocShell::ScrollToAnchor(bool aCurHasRef, bool aNewHasRef,
|
|||
// Both the new and current URIs refer to the same page. We can now
|
||||
// browse to the hash stored in the new URI.
|
||||
|
||||
// If it's a load from history, we don't have any anchor jumping to do.
|
||||
// Scrollbar position will be restored by the caller based on positions stored
|
||||
// in session history.
|
||||
bool scroll = aLoadType != LOAD_HISTORY && aLoadType != LOAD_RELOAD_NORMAL;
|
||||
|
||||
if (aNewHash.IsEmpty()) {
|
||||
// 2. If fragment is the empty string, then return the special value top of
|
||||
// the document.
|
||||
|
|
|
|||
|
|
@ -13121,6 +13121,8 @@ void Document::ScrollToRef() {
|
|||
const bool didScrollToTextFragment =
|
||||
presShell->HighlightAndGoToTextFragment(true);
|
||||
|
||||
FragmentDirective()->ClearUninvokedDirectives();
|
||||
|
||||
// 2. If fragment is the empty string and no text directives have been
|
||||
// scrolled to, then return the special value top of the document.
|
||||
if (didScrollToTextFragment || mScrollToRef.IsEmpty()) {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,21 @@ JSObject* FragmentDirective::WrapObject(JSContext* aCx,
|
|||
return FragmentDirective_Binding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
bool FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragmentString(
|
||||
nsCString& aFragment, nsTArray<TextDirective>* aTextDirectives) {
|
||||
ParsedFragmentDirectiveResult fragmentDirective;
|
||||
const bool hasRemovedFragmentDirective =
|
||||
StaticPrefs::dom_text_fragments_enabled() &&
|
||||
parse_fragment_directive(&aFragment, &fragmentDirective);
|
||||
if (hasRemovedFragmentDirective) {
|
||||
aFragment = fragmentDirective.url_without_fragment_directive;
|
||||
if (aTextDirectives) {
|
||||
aTextDirectives->SwapElements(fragmentDirective.text_directives);
|
||||
}
|
||||
}
|
||||
return hasRemovedFragmentDirective;
|
||||
}
|
||||
|
||||
void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(
|
||||
nsCOMPtr<nsIURI>& aURI, nsTArray<TextDirective>* aTextDirectives) {
|
||||
if (!aURI || !StaticPrefs::dom_text_fragments_enabled()) {
|
||||
|
|
@ -65,18 +80,12 @@ void FragmentDirective::ParseAndRemoveFragmentDirectiveFromFragment(
|
|||
nsAutoCString hash;
|
||||
aURI->GetRef(hash);
|
||||
|
||||
ParsedFragmentDirectiveResult fragmentDirective;
|
||||
const bool hasRemovedFragmentDirective =
|
||||
parse_fragment_directive(&hash, &fragmentDirective);
|
||||
ParseAndRemoveFragmentDirectiveFromFragmentString(hash, aTextDirectives);
|
||||
if (!hasRemovedFragmentDirective) {
|
||||
return;
|
||||
}
|
||||
Unused << NS_MutateURI(aURI)
|
||||
.SetRef(fragmentDirective.url_without_fragment_directive)
|
||||
.Finalize(aURI);
|
||||
if (aTextDirectives) {
|
||||
aTextDirectives->SwapElements(fragmentDirective.text_directives);
|
||||
}
|
||||
Unused << NS_MutateURI(aURI).SetRef(hash).Finalize(aURI);
|
||||
}
|
||||
|
||||
nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() {
|
||||
|
|
@ -88,7 +97,6 @@ nsTArray<RefPtr<nsRange>> FragmentDirective::FindTextFragmentsInDocument() {
|
|||
textDirectiveRanges.AppendElement(range);
|
||||
}
|
||||
}
|
||||
mUninvokedTextDirectives.Clear();
|
||||
return textDirectiveRanges;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ class FragmentDirective final : public nsISupports, public nsWrapperCache {
|
|||
return !mUninvokedTextDirectives.IsEmpty();
|
||||
};
|
||||
|
||||
/** Clears all uninvoked directives. */
|
||||
void ClearUninvokedDirectives() { mUninvokedTextDirectives.Clear(); }
|
||||
|
||||
/** Searches for the current uninvoked text directives and creates a range for
|
||||
* each one that is found.
|
||||
*
|
||||
|
|
@ -94,6 +97,17 @@ class FragmentDirective final : public nsISupports, public nsWrapperCache {
|
|||
nsCOMPtr<nsIURI>& aURI,
|
||||
nsTArray<TextDirective>* aTextDirectives = nullptr);
|
||||
|
||||
/** Parses the fragment directive and removes it from the hash, given as
|
||||
* string. This operation happens in-place.
|
||||
*
|
||||
* This function is called internally by
|
||||
* `ParseAndRemoveFragmentDirectiveFromFragment()`.
|
||||
*
|
||||
* This function returns true if it modified `aFragment`.
|
||||
*/
|
||||
static bool ParseAndRemoveFragmentDirectiveFromFragmentString(
|
||||
nsCString& aFragment, nsTArray<TextDirective>* aTextDirectives = nullptr);
|
||||
|
||||
private:
|
||||
RefPtr<nsRange> FindRangeForTextDirective(
|
||||
const TextDirective& aTextDirective);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,8 @@
|
|||
[Text fragment specified in iframe.src]
|
||||
expected: FAIL
|
||||
|
||||
[Navigate same-origin iframe via window.location]
|
||||
expected: FAIL
|
||||
|
||||
[Cross-origin with element-id fallback]
|
||||
expected: FAIL
|
||||
|
||||
[Non-matching text with element-id fallback]
|
||||
[Navigate cross-origin iframe via window.location]
|
||||
expected: FAIL
|
||||
|
|
|
|||
|
|
@ -1,18 +1,3 @@
|
|||
[percent-encoding.html]
|
||||
[Test navigation with fragment: Percent char without hex digits is invalid..]
|
||||
expected: FAIL
|
||||
|
||||
[Test navigation with fragment: Percent char followed by percent char is invalid..]
|
||||
expected: FAIL
|
||||
|
||||
[Test navigation with fragment: Single digit percent-encoding is invalid..]
|
||||
expected: FAIL
|
||||
|
||||
[Test navigation with fragment: Percent-encoding limited to two digits..]
|
||||
expected: FAIL
|
||||
|
||||
[Test navigation with fragment: Percent-encoded "%%F".]
|
||||
expected: FAIL
|
||||
|
||||
[Test navigation with fragment: Percent-encoding multibyte codepoint (CHECKMARK)..]
|
||||
expected: FAIL
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
[same-document-tests.html]
|
||||
expected:
|
||||
[OK, TIMEOUT]
|
||||
[Basic element id fallback]
|
||||
expected: FAIL
|
||||
|
||||
[Malformed text directive element id fallback]
|
||||
expected: [FAIL, TIMEOUT]
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[scroll-to-text-fragment-same-doc.html]
|
||||
[Activated for same-document window.location.replace]
|
||||
expected: FAIL
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<title>Same document navigation to text fragment directives before loading the document has finished</title>
|
||||
<meta charset=utf-8>
|
||||
<link rel="help" href="https://wicg.github.io/ScrollToTextFragment/">
|
||||
<script src="/resources/testharness.js"></script>
|
||||
<script src="/resources/testharnessreport.js"></script>
|
||||
<script src="resources/util.js"></script>
|
||||
<script>
|
||||
// Ensure that a same-document text directive navigation works correctly
|
||||
// if the navigation is triggered before the page finishes loading.
|
||||
promise_test(async t => {
|
||||
assert_implements(document.fragmentDirective, 'Text directive not implemented');
|
||||
|
||||
location.hash = ':~:text=line%20of%20text';
|
||||
await t.step_wait(() => window.scrollY > 0, "Wait for scroll");
|
||||
assert_true(isInViewport(document.getElementById('text')), 'Scrolled to text');
|
||||
}, 'Same-document text directive navigation before loading the document has finished');
|
||||
</script>
|
||||
<style>
|
||||
div {
|
||||
margin: 200vh 0 200vh 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="text">
|
||||
This is a line of text.
|
||||
</div>
|
||||
Loading…
Reference in a new issue