mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-02-04 06:23:45 +00:00
Support range mutation across nodes
Range mutation will trigger MutationObserver MutationObserver with characterDataOldValue=true implicitly means characterData=true For MutationObserver-characterData test.
This commit is contained in:
@@ -820,3 +820,137 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id=deleteContents_crossNode>
|
||||||
|
{
|
||||||
|
// Test deleteContents across multiple sibling text nodes
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.appendChild(document.createTextNode('AAAA'));
|
||||||
|
p.appendChild(document.createTextNode('BBBB'));
|
||||||
|
p.appendChild(document.createTextNode('CCCC'));
|
||||||
|
|
||||||
|
testing.expectEqual(3, p.childNodes.length);
|
||||||
|
testing.expectEqual('AAAABBBBCCCC', p.textContent);
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
// Start at position 2 in first text node ("AA|AA")
|
||||||
|
range.setStart(p.childNodes[0], 2);
|
||||||
|
// End at position 2 in third text node ("CC|CC")
|
||||||
|
range.setEnd(p.childNodes[2], 2);
|
||||||
|
|
||||||
|
range.deleteContents();
|
||||||
|
|
||||||
|
// Should have truncated first node to "AA" and third node to "CC"
|
||||||
|
// Middle node should be removed
|
||||||
|
testing.expectEqual(2, p.childNodes.length);
|
||||||
|
testing.expectEqual('AA', p.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('CC', p.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('AACC', p.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=deleteContents_crossNode_partial>
|
||||||
|
{
|
||||||
|
// Test deleteContents where start node is completely preserved
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.appendChild(document.createTextNode('KEEP'));
|
||||||
|
p.appendChild(document.createTextNode('DELETE'));
|
||||||
|
p.appendChild(document.createTextNode('PARTIAL'));
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
// Start at end of first text node
|
||||||
|
range.setStart(p.childNodes[0], 4);
|
||||||
|
// End in middle of third text node
|
||||||
|
range.setEnd(p.childNodes[2], 4);
|
||||||
|
|
||||||
|
range.deleteContents();
|
||||||
|
|
||||||
|
testing.expectEqual(2, p.childNodes.length);
|
||||||
|
testing.expectEqual('KEEP', p.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('IAL', p.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('KEEPIAL', p.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=extractContents_crossNode>
|
||||||
|
{
|
||||||
|
// Test extractContents across multiple sibling text nodes
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.appendChild(document.createTextNode('AAAA'));
|
||||||
|
p.appendChild(document.createTextNode('BBBB'));
|
||||||
|
p.appendChild(document.createTextNode('CCCC'));
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(p.childNodes[0], 2);
|
||||||
|
range.setEnd(p.childNodes[2], 2);
|
||||||
|
|
||||||
|
const fragment = range.extractContents();
|
||||||
|
|
||||||
|
// Original should be truncated
|
||||||
|
testing.expectEqual(2, p.childNodes.length);
|
||||||
|
testing.expectEqual('AA', p.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('CC', p.childNodes[1].textContent);
|
||||||
|
|
||||||
|
// Fragment should contain extracted content
|
||||||
|
testing.expectEqual(3, fragment.childNodes.length);
|
||||||
|
testing.expectEqual('AA', fragment.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('CC', fragment.childNodes[2].textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=cloneContents_crossNode>
|
||||||
|
{
|
||||||
|
// Test cloneContents across multiple sibling text nodes
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.appendChild(document.createTextNode('AAAA'));
|
||||||
|
p.appendChild(document.createTextNode('BBBB'));
|
||||||
|
p.appendChild(document.createTextNode('CCCC'));
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
range.setStart(p.childNodes[0], 2);
|
||||||
|
range.setEnd(p.childNodes[2], 2);
|
||||||
|
|
||||||
|
const fragment = range.cloneContents();
|
||||||
|
|
||||||
|
// Original should be unchanged
|
||||||
|
testing.expectEqual(3, p.childNodes.length);
|
||||||
|
testing.expectEqual('AAAA', p.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('BBBB', p.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('CCCC', p.childNodes[2].textContent);
|
||||||
|
|
||||||
|
// Fragment should contain cloned content
|
||||||
|
testing.expectEqual(3, fragment.childNodes.length);
|
||||||
|
testing.expectEqual('AA', fragment.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('BBBB', fragment.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('CC', fragment.childNodes[2].textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=deleteContents_crossNode_withElements>
|
||||||
|
{
|
||||||
|
// Test deleteContents with mixed text and element nodes
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.appendChild(document.createTextNode('Start'));
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = 'Middle';
|
||||||
|
div.appendChild(span);
|
||||||
|
div.appendChild(document.createTextNode('End'));
|
||||||
|
|
||||||
|
testing.expectEqual(3, div.childNodes.length);
|
||||||
|
|
||||||
|
const range = document.createRange();
|
||||||
|
// Start in middle of first text node
|
||||||
|
range.setStart(div.childNodes[0], 2);
|
||||||
|
// End in middle of last text node
|
||||||
|
range.setEnd(div.childNodes[2], 1);
|
||||||
|
|
||||||
|
range.deleteContents();
|
||||||
|
|
||||||
|
// Should keep "St" from start, remove span, keep "nd" from end
|
||||||
|
testing.expectEqual(2, div.childNodes.length);
|
||||||
|
testing.expectEqual('St', div.childNodes[0].textContent);
|
||||||
|
testing.expectEqual('nd', div.childNodes[1].textContent);
|
||||||
|
testing.expectEqual('Stnd', div.textContent);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
|
|||||||
copied_options.attributeFilter = filter_copy;
|
copied_options.attributeFilter = filter_copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.characterDataOldValue) {
|
||||||
|
copied_options.characterData = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if already observing this target
|
// Check if already observing this target
|
||||||
for (self._observing.items) |*obs| {
|
for (self._observing.items) |*obs| {
|
||||||
if (obs.target == target) {
|
if (obs.target == target) {
|
||||||
|
|||||||
@@ -652,9 +652,9 @@ pub fn getData(self: *const Node) []const u8 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setData(self: *Node, data: []const u8) void {
|
pub fn setData(self: *Node, data: []const u8, page: *Page) !void {
|
||||||
switch (self._type) {
|
switch (self._type) {
|
||||||
.cdata => |c| c._data = data,
|
.cdata => |c| try c.setData(data, page),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
|||||||
u8,
|
u8,
|
||||||
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
&.{ text_data[0..self._proto._start_offset], text_data[self._proto._end_offset..] },
|
||||||
);
|
);
|
||||||
self._proto._start_container.setData(new_text);
|
try self._proto._start_container.setData(new_text, page);
|
||||||
} else {
|
} else {
|
||||||
// Delete child nodes in range
|
// Delete child nodes in range
|
||||||
var offset = self._proto._start_offset;
|
var offset = self._proto._start_offset;
|
||||||
@@ -371,8 +371,43 @@ pub fn deleteContents(self: *Range, page: *Page) !void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex case: different containers - simplified implementation
|
// Complex case: different containers
|
||||||
// Just collapse the range for now
|
// Handle start container - if it's a text node, truncate it
|
||||||
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||||
|
const text_data = self._proto._start_container.getData();
|
||||||
|
if (self._proto._start_offset < text_data.len) {
|
||||||
|
// Keep only the part before start_offset
|
||||||
|
const new_text = text_data[0..self._proto._start_offset];
|
||||||
|
try self._proto._start_container.setData(new_text, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle end container - if it's a text node, truncate it
|
||||||
|
if (self._proto._end_container.is(Node.CData)) |_| {
|
||||||
|
const text_data = self._proto._end_container.getData();
|
||||||
|
if (self._proto._end_offset < text_data.len) {
|
||||||
|
// Keep only the part from end_offset onwards
|
||||||
|
const new_text = text_data[self._proto._end_offset..];
|
||||||
|
try self._proto._end_container.setData(new_text, page);
|
||||||
|
} else if (self._proto._end_offset == text_data.len) {
|
||||||
|
// If we're at the end, set to empty (will be removed if needed)
|
||||||
|
try self._proto._end_container.setData("", page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove nodes between start and end containers
|
||||||
|
// For now, handle the common case where they're siblings
|
||||||
|
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
||||||
|
var current = self._proto._start_container.nextSibling();
|
||||||
|
while (current != null and current != self._proto._end_container) {
|
||||||
|
const next = current.?.nextSibling();
|
||||||
|
if (current.?.parentNode()) |parent| {
|
||||||
|
_ = try parent.removeChild(current.?, page);
|
||||||
|
}
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.collapse(true);
|
self.collapse(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +436,39 @@ pub fn cloneContents(self: *const Range, page: *Page) !*DocumentFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Complex case: different containers
|
||||||
|
// Clone partial start container
|
||||||
|
if (self._proto._start_container.is(Node.CData)) |_| {
|
||||||
|
const text_data = self._proto._start_container.getData();
|
||||||
|
if (self._proto._start_offset < text_data.len) {
|
||||||
|
// Clone from start_offset to end of text
|
||||||
|
const cloned_text = text_data[self._proto._start_offset..];
|
||||||
|
const text_node = try page.createTextNode(cloned_text);
|
||||||
|
_ = try fragment.asNode().appendChild(text_node, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone nodes between start and end containers (siblings case)
|
||||||
|
if (self._proto._start_container.parentNode() == self._proto._end_container.parentNode()) {
|
||||||
|
var current = self._proto._start_container.nextSibling();
|
||||||
|
while (current != null and current != self._proto._end_container) {
|
||||||
|
const cloned = try current.?.cloneNode(true, page);
|
||||||
|
_ = try fragment.asNode().appendChild(cloned, page);
|
||||||
|
current = current.?.nextSibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone partial end container
|
||||||
|
if (self._proto._end_container.is(Node.CData)) |_| {
|
||||||
|
const text_data = self._proto._end_container.getData();
|
||||||
|
if (self._proto._end_offset > 0 and self._proto._end_offset <= text_data.len) {
|
||||||
|
// Clone from start to end_offset
|
||||||
|
const cloned_text = text_data[0..self._proto._end_offset];
|
||||||
|
const text_node = try page.createTextNode(cloned_text);
|
||||||
|
_ = try fragment.asNode().appendChild(text_node, page);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
|
|||||||
Reference in New Issue
Block a user