diff --git a/moli-dom/src/native/document.rs b/moli-dom/src/native/document.rs index 13f4374f5d..740343bcd9 100644 --- a/moli-dom/src/native/document.rs +++ b/moli-dom/src/native/document.rs @@ -55,6 +55,7 @@ pub struct Document { // override execution, but detached/template documents must retain `false` // for fragment parsing and serialization even without a window. scripting_enabled: bool, + design_mode_enabled: bool, css_target: Option, default_language: Option>, source_last_modified_ms: Option, @@ -79,6 +80,7 @@ impl Document { quirks_mode: QuirksMode::NoQuirks, kind: DocumentKind::Html, scripting_enabled, + design_mode_enabled: false, css_target: None, default_language: None, source_last_modified_ms: None, @@ -94,6 +96,7 @@ impl Document { quirks_mode: QuirksMode::NoQuirks, kind: DocumentKind::Xml, scripting_enabled: true, + design_mode_enabled: false, css_target: None, default_language: None, source_last_modified_ms: None, @@ -144,6 +147,10 @@ impl Document { self.scripting_enabled } + pub fn design_mode_enabled(&self) -> bool { + self.design_mode_enabled + } + pub fn fallback_base_url(&self) -> &Url { self.base_url_state.fallback_base_url() } @@ -181,6 +188,10 @@ impl Document { self.scripting_enabled = scripting_enabled; } + pub fn set_design_mode_enabled(&mut self, design_mode_enabled: bool) { + self.design_mode_enabled = design_mode_enabled; + } + pub fn set_css_target(&mut self, target: Option) -> bool { if self.css_target == target { return false; diff --git a/moli-dom/src/native/host/document.rs b/moli-dom/src/native/host/document.rs index 921c88303f..9343479265 100644 --- a/moli-dom/src/native/host/document.rs +++ b/moli-dom/src/native/host/document.rs @@ -993,6 +993,15 @@ impl DomHost { .map(Document::scripting_enabled) } + pub fn document_design_mode_enabled_for_handle( + &self, + document_handle: DomHandle, + ) -> Option { + self.node(document_handle) + .and_then(Node::as_document) + .map(Document::design_mode_enabled) + } + pub fn document_base_url(&self) -> Option { self.document_base_url_for_handle(self.document_handle()) } @@ -1173,6 +1182,25 @@ impl DomHost { true } + pub fn set_document_design_mode_enabled_for_handle( + &mut self, + document_handle: DomHandle, + design_mode_enabled: bool, + ) -> bool { + let Some(document) = self + .node_mut(document_handle) + .and_then(|node| node.data_mut().as_document_mut()) + else { + return false; + }; + if document.design_mode_enabled() == design_mode_enabled { + return false; + } + document.set_design_mode_enabled(design_mode_enabled); + self.record_mutation(MutationScope::QueryState); + true + } + pub fn set_document_quirks_mode_for_handle( &mut self, document_handle: DomHandle, diff --git a/moli-renderer-v8/src/document_runtime.rs b/moli-renderer-v8/src/document_runtime.rs index 5e2275d6bd..e1cd153ec4 100644 --- a/moli-renderer-v8/src/document_runtime.rs +++ b/moli-renderer-v8/src/document_runtime.rs @@ -753,7 +753,6 @@ pub(super) struct DocumentRuntime { selector_engine: QueryEngine, selector_debug: SelectorDebugCounters, document: HostDocumentState, - design_mode_documents: HashSet, script_execution_control: crate::script_execution_control::RendererScriptExecutionControl, author_styles_disabled: bool, bypass_content_security_policy: bool, diff --git a/moli-renderer-v8/src/document_runtime/dom_facade.rs b/moli-renderer-v8/src/document_runtime/dom_facade.rs index c62ffe55b7..953f52434e 100644 --- a/moli-renderer-v8/src/document_runtime/dom_facade.rs +++ b/moli-renderer-v8/src/document_runtime/dom_facade.rs @@ -13,18 +13,14 @@ use html5ever::tree_builder::QuirksMode; // selector semantics or orchestrate mutation side effects on their own. impl DocumentRuntime { pub(crate) fn document_design_mode_enabled(&self, document: DomHandle) -> bool { - self.design_mode_documents.contains(&document) + self.dom_host + .document_design_mode_enabled_for_handle(document) + .unwrap_or(false) } pub(crate) fn set_document_design_mode_enabled(&mut self, document: DomHandle, enabled: bool) { - if !self.dom_host.node(document).is_some_and(Node::is_document) { - return; - } - if enabled { - self.design_mode_documents.insert(document); - } else { - self.design_mode_documents.remove(&document); - } + self.dom_host + .set_document_design_mode_enabled_for_handle(document, enabled); } pub(crate) fn snapshot_document(&self) -> NativeDom { diff --git a/moli-renderer-v8/src/document_runtime/runtime_core.rs b/moli-renderer-v8/src/document_runtime/runtime_core.rs index 02cc9ae4e2..ed7772edaf 100644 --- a/moli-renderer-v8/src/document_runtime/runtime_core.rs +++ b/moli-renderer-v8/src/document_runtime/runtime_core.rs @@ -73,7 +73,6 @@ impl DocumentRuntime { selector_engine: QueryEngine, selector_debug: SelectorDebugCounters::default(), document, - design_mode_documents: HashSet::new(), script_execution_control: Default::default(), author_styles_disabled: false, bypass_content_security_policy: false, @@ -206,7 +205,6 @@ impl DocumentRuntime { selector_engine: _, selector_debug: _, document: _, - design_mode_documents: _, script_execution_control: _, author_styles_disabled: _, bypass_content_security_policy: _, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs index 1f636adadb..0a1e5a7919 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/shadow_dom.rs @@ -2045,25 +2045,42 @@ fn document_design_mode_controls_content_editable_without_crossing_shadow_roots( const inside = document.createElement('span'); root.appendChild(inside); document.body.append(inherited, disabled); - const before = [document.designMode, inherited.isContentEditable].join(':'); + const before = [ + document.designMode, + inherited.isContentEditable, + inherited.matches(':read-write'), + inherited.matches(':read-only') + ].join(':'); document.designMode = 'ON'; const enabled = [ document.designMode, inherited.isContentEditable, disabled.isContentEditable, - inside.isContentEditable + inside.isContentEditable, + inherited.matches(':read-write'), + inherited.matches(':read-only'), + disabled.matches(':read-write'), + inside.matches(':read-write') ].join(':'); document.designMode = 'invalid'; const afterInvalid = document.designMode; document.designMode = 'off'; - const disabledAgain = [document.designMode, inherited.isContentEditable].join(':'); + const disabledAgain = [ + document.designMode, + inherited.isContentEditable, + inherited.matches(':read-write'), + inherited.matches(':read-only') + ].join(':'); return [before, enabled, afterInvalid, disabledAgain].join('|'); })() "#, ) .expect("Document designMode contenteditable probe should evaluate"); - assert_eq!(result, "off:false|on:true:false:false|on|off:false"); + assert_eq!( + result, + "off:false:false:true|on:true:false:false:true:false:false:false|on|off:false:false:true" + ); } #[test] diff --git a/moli-selector/src/lib.rs b/moli-selector/src/lib.rs index ae9ac3eaa8..717285c79c 100644 --- a/moli-selector/src/lib.rs +++ b/moli-selector/src/lib.rs @@ -1482,6 +1482,33 @@ mod tests { ); } + #[test] + fn dom_api_selectors_design_mode_marks_connected_elements_read_write() { + let url = url::Url::parse("https://example.test/").unwrap(); + let mut host = DomHost::from_dom(NativeDom::new_html(url)); + host.reset_html_document_shell(); + let body = host.document_body_handle().unwrap(); + let paragraph = host.create_element("p"); + let blocked = host.create_element("div"); + assert!(host.set_attribute(blocked, "contenteditable", "false")); + let detached = host.create_element("section"); + assert!(host.append_child(body, paragraph)); + assert!(host.append_child(body, blocked)); + assert!(host.set_document_design_mode_enabled_for_handle(host.document_handle(), true)); + + let engine = QueryEngine; + assert!( + engine + .matches_host(&host, paragraph, ":read-write") + .unwrap() + ); + assert!(!engine.matches_host(&host, paragraph, ":read-only").unwrap()); + assert!(!engine.matches_host(&host, blocked, ":read-write").unwrap()); + assert!(engine.matches_host(&host, blocked, ":read-only").unwrap()); + assert!(!engine.matches_host(&host, detached, ":read-write").unwrap()); + assert!(engine.matches_host(&host, detached, ":read-only").unwrap()); + } + #[test] fn dom_api_selectors_lang_uses_nearest_language_attribute() { let url = url::Url::parse("https://example.test/").unwrap(); diff --git a/moli-selector/src/stylo/query/pseudo.rs b/moli-selector/src/stylo/query/pseudo.rs index 88ff2f493b..e3e05d2c56 100644 --- a/moli-selector/src/stylo/query/pseudo.rs +++ b/moli-selector/src/stylo/query/pseudo.rs @@ -344,13 +344,19 @@ impl<'a> QueryElement<'a> { pub(super) fn is_editable(self) -> bool { let mut current = Some(self.handle); while let Some(handle) = current { - if let Some(element) = self.host.node(handle).and_then(Node::as_element) + let Some(node) = self.host.node(handle) else { + return false; + }; + if let Some(document) = node.as_document() { + return document.design_mode_enabled(); + } + if let Some(element) = node.as_element() && let Some(value) = element.attribute("contenteditable") && let Some(is_editable) = contenteditable_value_is_editable(value) { return is_editable; } - current = self.host.node(handle).and_then(Node::parent_node); + current = node.parent_node(); } false }