Skip to content

Commit

Permalink
[Selection API] Add composed range endpoints
Browse files Browse the repository at this point in the history
We update Range API to support composed range. Previously, when setting
the start/end range endpoints, if they are in different node trees,
they will be collapsed. Now, if start/end are in different node trees,
but same document, composed_range_ will be set to track those cross
shadow tree endpoints.

We update Selection::GetComposedRanges to use composed start and end
to create a composed StaticRange.

We fix Selection::GetComposedRanges to use
IsShadowIncludingInclusiveAncestorOf for Rescoping and add a test.

Change-Id: Ib46497382fb83809394499182b5c8e4ee08925b7
Bug: 40286116
Fixed: 335245347
Fixed: 355630554
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5744732
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Siye Liu <siliu@microsoft.com>
Commit-Queue: Di Zhang <dizhangg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1337611}
  • Loading branch information
dizhang168 authored and chromium-wpt-export-bot committed Aug 6, 2024
1 parent 7f4f309 commit 82f2066
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 3 deletions.
6 changes: 4 additions & 2 deletions selection/selection-nested-video.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
assert_equals(sel.focusOffset, 0);
t.done();
})
});
}, 'selection have the same anchor and focus nodes because b and c are in different tree scopes.');

</script>
<div id="a">A</div>
<video>
<video id="b">
<video id="b"></video>
</video>
81 changes: 81 additions & 0 deletions shadow-dom/selection-getComposedRanges-collapsed.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html>
<body>
<meta name="author" href="mailto:dizhangg@chromium.org">
<meta name="assert" content="Selection's getComposedRanges should return a sequence of static ranges">
<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getcomposedranges">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>

<div id="container">
<div id="host1">
<template shadowrootmode=open>C</template>
A - not slotted
</div>
<div id="host2">
<template shadowrootmode=open>D</template>
<div id=b>B - not slotted</div>
</div>
</div>

<script>

const c = host1.shadowRoot;
const d = host2.shadowRoot;

test(() => {
const sel = getSelection();
sel.setBaseAndExtent(b, 0, c, 0);
assert_equals(sel.getRangeAt(0).startContainer, b);
assert_equals(sel.getRangeAt(0).startOffset, 0);
assert_equals(sel.getRangeAt(0).endContainer, b);
assert_equals(sel.getRangeAt(0).endOffset, 0);

assert_equals(sel.getComposedRanges()[0].startContainer, b);
assert_equals(sel.getComposedRanges()[0].startOffset, 0);
assert_equals(sel.getComposedRanges()[0].endContainer, b);
assert_equals(sel.getComposedRanges()[0].endOffset, 0);
}, 'Setting the range to nodes that aren\'t in the same tree collapses both composed and non-composed ranges.');

test(() => {
const sel = getSelection();
sel.setBaseAndExtent(c, 0, d, 0);

assert_equals(sel.getRangeAt(0).startContainer, d);
assert_equals(sel.getRangeAt(0).startOffset, 0);
assert_equals(sel.getRangeAt(0).endContainer, d);
assert_equals(sel.getRangeAt(0).endOffset, 0);

assert_equals(sel.getComposedRanges()[0].startContainer, container);
assert_equals(sel.getComposedRanges()[0].startOffset, 1);
assert_equals(sel.getComposedRanges()[0].endContainer, container);
assert_equals(sel.getComposedRanges()[0].endOffset, 4);

assert_equals(sel.getComposedRanges(c)[0].startContainer, c);
assert_equals(sel.getComposedRanges(c)[0].startOffset, 0);
assert_equals(sel.getComposedRanges(c)[0].endContainer, container);
assert_equals(sel.getComposedRanges(c)[0].endOffset, 4);

assert_equals(sel.getComposedRanges(d)[0].startContainer, container);
assert_equals(sel.getComposedRanges(d)[0].startOffset, 1);
assert_equals(sel.getComposedRanges(d)[0].endContainer, d);
assert_equals(sel.getComposedRanges(d)[0].endOffset, 0);

assert_equals(sel.getComposedRanges(c, d)[0].startContainer, c);
assert_equals(sel.getComposedRanges(c, d)[0].startOffset, 0);
assert_equals(sel.getComposedRanges(c, d)[0].endContainer, d);
assert_equals(sel.getComposedRanges(c, d)[0].endOffset, 0);

// Re-setting the same range should never change the output
sel.setBaseAndExtent(c, 0, d, 0);
assert_equals(sel.getRangeAt(0).startContainer, d);
assert_equals(sel.getRangeAt(0).startOffset, 0);
assert_equals(sel.getRangeAt(0).endContainer, d);
assert_equals(sel.getRangeAt(0).endOffset, 0);

assert_equals(sel.getComposedRanges(c, d)[0].startContainer, c);
assert_equals(sel.getComposedRanges(c, d)[0].startOffset, 0);
assert_equals(sel.getComposedRanges(c, d)[0].endContainer, d);
assert_equals(sel.getComposedRanges(c, d)[0].endOffset, 0);
}, 'Setting the range to nodes in different shadow trees collapses ordinary ranges, but does not collapse composed ranges.');
</script>
37 changes: 36 additions & 1 deletion shadow-dom/selection-getComposedRanges.tentative.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<body>
<meta name="author" title="Ryosuke Niwa" href="mailto:rniwa@webkit.org">
<meta name="assert" content="Selection's getComposedRanges should return a sequence of static ranges">
<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getcomposedrange">
<link rel="help" href="https://w3c.github.io/selection-api/#dom-selection-getcomposedranges">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="container"></div>
Expand All @@ -14,6 +14,17 @@
assert_array_equals(getSelection().getComposedRanges(), []);
}, 'getComposedRanges returns an empty sequence when there is no selection');

test(() => {
container.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(container.firstChild, 0, container.firstChild, 5);
try {
getSelection().getComposedRanges(container);
assert_unreached('executed without error; want error');
} catch (e) {
assert_equals(e.message, "Failed to execute 'getComposedRanges' on 'Selection': parameter 1 is not of type 'ShadowRoot'.");
}
}, 'getComposedRanges should fail if argument is not a shadow root');

test(() => {
container.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(container.firstChild, 0, container.firstChild, 5);
Expand All @@ -38,6 +49,7 @@

test(() => {
container.innerHTML = 'a<div id="host"></div>b';
const host = container.querySelector('#host');
const shadowRoot = host.attachShadow({mode: 'closed'});
shadowRoot.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(shadowRoot.firstChild, 0, shadowRoot.firstChild, 5);
Expand All @@ -51,6 +63,7 @@

test(() => {
container.innerHTML = 'a<div id="host"></div>b';
const host = container.querySelector('#host');
const shadowRoot = host.attachShadow({mode: 'closed'});
shadowRoot.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(shadowRoot.firstChild, 0, shadowRoot.firstChild, 5);
Expand All @@ -64,6 +77,7 @@

test(() => {
container.innerHTML = 'a<div id="host"></div>b';
const host = container.querySelector('#host');
const shadowRoot = host.attachShadow({mode: 'closed'});
shadowRoot.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 2);
Expand All @@ -77,6 +91,7 @@

test(() => {
container.innerHTML = 'a<div id="host"></div>b';
const host = container.querySelector('#host');
const shadowRoot = host.attachShadow({mode: 'closed'});
shadowRoot.innerHTML = 'hello, world';
getSelection().setBaseAndExtent(shadowRoot.firstChild, 7, container, 2);
Expand All @@ -90,6 +105,7 @@

test(() => {
container.innerHTML = 'a<div id="outerHost"></div>b';
const outerHost = container.querySelector('#outerHost');
const outerShadowRoot = outerHost.attachShadow({mode: 'closed'});
outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>';
const innerHost = outerShadowRoot.getElementById('innerHost');
Expand All @@ -106,6 +122,7 @@

test(() => {
container.innerHTML = 'a<div id="outerHost"></div>b';
const outerHost = container.querySelector('#outerHost');
const outerShadowRoot = outerHost.attachShadow({mode: 'closed'});
outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>';
const innerHost = outerShadowRoot.getElementById('innerHost');
Expand All @@ -122,6 +139,7 @@

test(() => {
container.innerHTML = 'a<div id="outerHost"></div>b';
const outerHost = container.querySelector('#outerHost');
const outerShadowRoot = outerHost.attachShadow({mode: 'closed'});
outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>';
const innerHost = outerShadowRoot.getElementById('innerHost');
Expand All @@ -136,6 +154,23 @@
assert_equals(ranges[0].endOffset, 1);
}, 'getComposedRanges returns a sequence with a static range pointing to the outer shadow tree when there is a selection in an inner shadow tree and the outer shadow tree is specified as an argument');

test(() => {
container.innerHTML = 'a<div id="outerHost"></div>b';
const outerHost = container.querySelector('#outerHost');
const outerShadowRoot = outerHost.attachShadow({mode: 'closed'});
outerShadowRoot.innerHTML = '<div id="innerHost">hello</div><div>world</div>';
const innerHost = outerShadowRoot.getElementById('innerHost');
const innerShadowRoot = innerHost.attachShadow({mode: 'closed'});
innerShadowRoot.innerHTML = 'some text';
getSelection().setBaseAndExtent(container.firstChild, 0, outerShadowRoot, 0);
const ranges = getSelection().getComposedRanges(innerShadowRoot);
assert_equals(ranges.length, 1);
assert_equals(ranges[0].startContainer, container.firstChild, "A");
assert_equals(ranges[0].startOffset, 0, "B");
assert_equals(ranges[0].endContainer, outerShadowRoot, "C");
assert_equals(ranges[0].endOffset, 0, "D");
}, 'getComposedRanges returns a sequence with a static range without rescoping when there is a selection in an outer shadow tree and the inner shadow tree is specified as an argument');

</script>
</body>
</html>

0 comments on commit 82f2066

Please sign in to comment.