fix(selectors): honor document design mode

This commit is contained in:
ldm0
2026-09-16 22:06:41 +08:00
parent a773c4042c
commit b6fc53e2af
8 changed files with 100 additions and 18 deletions
+11
View File
@@ -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<NativeNodeId>,
default_language: Option<Box<str>>,
source_last_modified_ms: Option<f64>,
@@ -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<NativeNodeId>) -> bool {
if self.css_target == target {
return false;
+28
View File
@@ -993,6 +993,15 @@ impl DomHost {
.map(Document::scripting_enabled)
}
pub fn document_design_mode_enabled_for_handle(
&self,
document_handle: DomHandle,
) -> Option<bool> {
self.node(document_handle)
.and_then(Node::as_document)
.map(Document::design_mode_enabled)
}
pub fn document_base_url(&self) -> Option<Url> {
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,
-1
View File
@@ -753,7 +753,6 @@ pub(super) struct DocumentRuntime {
selector_engine: QueryEngine,
selector_debug: SelectorDebugCounters,
document: HostDocumentState,
design_mode_documents: HashSet<DomHandle>,
script_execution_control: crate::script_execution_control::RendererScriptExecutionControl,
author_styles_disabled: bool,
bypass_content_security_policy: bool,
@@ -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 {
@@ -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: _,
@@ -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]
+27
View File
@@ -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();
+8 -2
View File
@@ -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
}