From cce5b82de96f6159bc60367b64bfd34ddacb1e0e Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 30 Aug 2026 23:06:11 +0800 Subject: [PATCH] fix(svg): implement deselectAll --- .../context_bootstrap/svg_runtime/bindings.rs | 7 ++ .../svg_runtime/callbacks.rs | 50 +++++++++ .../src/context_bootstrap/svg_runtime/mod.rs | 6 +- .../src/native_bridge/document.rs | 24 ++++ .../tests/dom_elements/dom_surface.rs | 104 ++++++++++++++++++ 5 files changed, 190 insertions(+), 1 deletion(-) diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs index 080a68a00f..594eaabd8a 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/bindings.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs index 3da34a838a..c44351b042 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/callbacks.rs @@ -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>, diff --git a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs index fe7f0123aa..3c6f4175f5 100644 --- a/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/svg_runtime/mod.rs @@ -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}, diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index e6cdc0ce70..4bda2a5fa2 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -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> { + 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::::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>, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs index 54cf42d10c..cfa2d7574d 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface.rs @@ -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/", + "", + ); + + 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/");