fix(svg): implement deselectAll

This commit is contained in:
ldm0
2026-09-23 00:11:52 +08:00
parent c83012983b
commit cce5b82de9
5 changed files with 190 additions and 1 deletions
@@ -490,6 +490,13 @@ struct SvgSvgElementTemplateMethodsDeclaration {
)]
create_svg_rect: (),
#[webapi(
method = "deselectAll",
length = 0,
callback = svg_svg_element_deselect_all_callback
)]
deselect_all: (),
#[webapi(
method = "createSVGMatrix",
length = 0,
@@ -1548,6 +1548,56 @@ pub(super) fn svg_svg_element_create_matrix_callback<'s>(
rv.set(super::super::geometry_runtime::build_dom_matrix_identity_object(scope).into());
}
pub(super) fn svg_svg_element_deselect_all_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok((runtime_ptr, handle)) =
crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, args.this())
else {
webidl::throw_type_error(
scope,
"SVGSVGElement.deselectAll called on incompatible receiver.",
);
return;
};
let (is_svg_root, owner_document) = {
let runtime = unsafe { &*runtime_ptr };
let is_svg_root = runtime
.dom_host()
.node(handle)
.and_then(|node| node.as_element())
.is_some_and(|element| element.is_svg_element("svg"));
let owner_document = runtime.dom_host().owner_document_handle(handle);
(is_svg_root, owner_document)
};
if !is_svg_root {
webidl::throw_type_error(
scope,
"SVGSVGElement.deselectAll called on incompatible receiver.",
);
return;
}
let Some(owner_document) = owner_document else {
return;
};
let Some(window) = crate::native_bridge::document::document_associated_window_for_handle(
scope,
runtime_ptr,
owner_document,
) else {
return;
};
let Some(selection) = selection_value_for_window(scope, window) else {
return;
};
if selection_has_range(scope, selection) {
selection_clear(scope, selection);
selection_dispatch_change(scope);
}
}
pub(super) fn svg_svg_element_create_transform_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
@@ -1,4 +1,8 @@
use super::{build_dom_point_object, build_dom_rect_object, optional_dom_point_init_arg};
use super::{
build_dom_point_object, build_dom_rect_object, optional_dom_point_init_arg,
selection::{selection_clear, selection_dispatch_change, selection_has_range},
selection_value_for_window,
};
use crate::{
native_bridge::throw_dom_exception,
util::{callback_data_item, get_private_value, set_private_value, v8_string, v8str},
@@ -1696,6 +1696,30 @@ fn document_associated_window_for_object<'s>(
.map(|context| context.global(scope))
}
pub(crate) fn document_associated_window_for_handle<'s>(
scope: &mut v8::PinScope<'s, '_>,
runtime_ptr: *mut JsContextHost,
handle: DomHandle,
) -> Option<v8::Local<'s, v8::Object>> {
if !node_is_document(unsafe { &*runtime_ptr }, handle) {
return None;
}
let document = unsafe { &mut *runtime_ptr }
.native_bridge_mut()
.wrap_handle(scope, runtime_ptr, handle)?;
if let Some(window) = get_private_value(scope, document, DOCUMENT_ASSOCIATED_WINDOW_SLOT)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
return Some(window);
}
if unsafe { &*runtime_ptr }.dom_host().document_handle() != handle {
return None;
}
document
.get_creation_context(scope)
.map(|context| context.global(scope))
}
pub(in crate::native_bridge) fn set_document_associated_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
document: v8::Local<'s, v8::Object>,
@@ -8659,6 +8659,110 @@ fn reflected_dom_token_list_attributes_are_live_same_object_and_owner_scoped() {
assert_eq!(result, "ok");
}
#[test]
fn svg_svg_element_deselect_all_clears_the_owner_document_selection() {
let mut vm = new_parsed_test_vm(
"https://svg-deselect-all.test/",
"<!doctype html><html><head></head><body></body></html>",
);
let result = vm
.eval(
r#"
(() => {
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const svgNamespace = "http://www.w3.org/2000/svg";
const outer = document.createElementNS(svgNamespace, "svg");
const inner = document.createElementNS(svgNamespace, "svg");
const svgText = document.createElementNS(svgNamespace, "text");
const htmlText = document.createElement("p");
svgText.textContent = "SVG selection";
htmlText.textContent = "HTML selection";
inner.appendChild(svgText);
outer.appendChild(inner);
document.body.append(outer, htmlText);
const descriptor = Object.getOwnPropertyDescriptor(
SVGSVGElement.prototype,
"deselectAll"
);
assert(!!descriptor, "deselectAll descriptor");
assert(typeof descriptor.value === "function", "deselectAll function");
assert(descriptor.value.name === "deselectAll", "deselectAll name");
assert(descriptor.value.length === 0, "deselectAll length");
assert(descriptor.enumerable && descriptor.writable && descriptor.configurable,
"deselectAll descriptor flags");
const selection = window.getSelection();
const select = node => {
const range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
assert(selection.rangeCount === 1, "selection precondition");
};
outer.deselectAll();
assert(selection.rangeCount === 0 && selection.isCollapsed,
"empty selection stays empty");
select(svgText);
outer.deselectAll();
assert(selection.rangeCount === 0 && selection.isCollapsed,
"outer svg clears SVG selection");
select(htmlText);
inner.deselectAll();
assert(selection.rangeCount === 0 && selection.isCollapsed,
"inner svg clears selection outside its subtree");
select(svgText);
const originalDocumentGetSelection = document.getSelection;
const originalRemoveAllRanges = Selection.prototype.removeAllRanges;
document.getSelection = () => { throw new Error("observable getSelection call"); };
Selection.prototype.removeAllRanges = () => {
throw new Error("observable removeAllRanges call");
};
Object.defineProperty(outer, "ownerDocument", {
value: null,
configurable: true
});
Object.defineProperty(document, "defaultView", {
value: null,
configurable: true
});
try {
outer.deselectAll();
} finally {
delete outer.ownerDocument;
delete document.defaultView;
document.getSelection = originalDocumentGetSelection;
Selection.prototype.removeAllRanges = originalRemoveAllRanges;
}
assert(selection.rangeCount === 0, "deselectAll uses internal selection state");
let borrowed = "returned";
try {
descriptor.value.call(document.createElement("div"));
} catch (error) {
borrowed = error && error.name;
}
assert(borrowed === "TypeError", "deselectAll receiver brand");
const detachedDocument = document.implementation.createHTMLDocument("");
const detachedSvg = detachedDocument.createElementNS(svgNamespace, "svg");
detachedSvg.deselectAll();
return "ok";
})()
"#,
)
.expect("SVGSVGElement.deselectAll should evaluate");
assert_eq!(result, "ok");
}
#[test]
fn html_rel_list_supported_tokens_are_ascii_case_insensitive_and_owner_specific() {
let mut vm = new_storage_test_vm("https://rel-list-supports.test/");