script/dom/execcommand/
execcommands.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
6use crate::dom::bindings::root::DomRoot;
7use crate::dom::bindings::str::DOMString;
8use crate::dom::document::Document;
9use crate::dom::execcommand::basecommand::BaseCommand;
10use crate::dom::execcommand::commands::defaultparagraphseparator::DefaultParagraphSeparatorCommand;
11use crate::dom::execcommand::commands::delete::DeleteCommand;
12use crate::dom::execcommand::commands::stylewithcss::StyleWithCssCommand;
13use crate::dom::selection::Selection;
14use crate::script_runtime::CanGc;
15
16/// <https://w3c.github.io/editing/docs/execCommand/#miscellaneous-commands>
17fn is_command_listed_in_miscellaneous_section(command_id: &str) -> bool {
18    matches!(
19        command_id.to_lowercase().as_str(),
20        "defaultparagraphseparator" | "redo" | "selectall" | "stylewithcss" | "undo" | "usecss"
21    )
22}
23
24impl Document {
25    /// <https://w3c.github.io/editing/docs/execCommand/#enabled>
26    fn selection_if_command_is_enabled(
27        &self,
28        cx: &mut js::context::JSContext,
29        command_id: &DOMString,
30    ) -> Option<DomRoot<Selection>> {
31        let selection = self.GetSelection(CanGc::from_cx(cx))?;
32        // > Among commands defined in this specification, those listed in Miscellaneous commands are always enabled,
33        // > except for the cut command and the paste command.
34        //
35        // Note: cut and paste are listed in the "clipboard commands" section, not the miscellaneous section
36        if is_command_listed_in_miscellaneous_section(&command_id.str()) {
37            return Some(selection);
38        }
39        // > The other commands defined here are enabled if the active range is not null,
40        let range = selection.active_range()?;
41        // > its start node is either editable or an editing host,
42        if !range.start_container().is_editable_or_editing_host() {
43            return None;
44        }
45        // > the editing host of its start node is not an EditContext editing host,
46        // TODO
47        // > its end node is either editable or an editing host,
48        if !range.end_container().is_editable_or_editing_host() {
49            return None;
50        }
51        // > the editing host of its end node is not an EditContext editing host,
52        // TODO
53        // > and there is some editing host that is an inclusive ancestor of both its start node and its end node.
54        // TODO
55        Some(selection)
56    }
57
58    /// <https://w3c.github.io/editing/docs/execCommand/#supported>
59    fn command_if_command_is_supported(
60        &self,
61        command_id: &DOMString,
62    ) -> Option<Box<dyn BaseCommand>> {
63        // https://w3c.github.io/editing/docs/execCommand/#methods-to-query-and-execute-commands
64        // > All of these methods must treat their command argument ASCII case-insensitively.
65        Some(match &*command_id.str().to_lowercase() {
66            "delete" => Box::new(DeleteCommand {}),
67            "defaultparagraphseparator" => Box::new(DefaultParagraphSeparatorCommand {}),
68            "stylewithcss" => Box::new(StyleWithCssCommand {}),
69            _ => return None,
70        })
71    }
72}
73
74pub(crate) trait DocumentExecCommandSupport {
75    fn is_command_supported(&self, command_id: DOMString) -> bool;
76    fn is_command_indeterminate(&self, command_id: DOMString) -> bool;
77    fn command_state_for_command(&self, command_id: DOMString) -> bool;
78    fn command_value_for_command(&self, command_id: DOMString) -> DOMString;
79    fn check_support_and_enabled(
80        &self,
81        cx: &mut js::context::JSContext,
82        command_id: &DOMString,
83    ) -> Option<(Box<dyn BaseCommand>, DomRoot<Selection>)>;
84    fn exec_command_for_command_id(
85        &self,
86        cx: &mut js::context::JSContext,
87        command_id: DOMString,
88        value: DOMString,
89    ) -> bool;
90}
91
92impl DocumentExecCommandSupport for Document {
93    /// <https://w3c.github.io/editing/docs/execCommand/#querycommandsupported()>
94    fn is_command_supported(&self, command_id: DOMString) -> bool {
95        self.command_if_command_is_supported(&command_id).is_some()
96    }
97
98    /// <https://w3c.github.io/editing/docs/execCommand/#querycommandindeterm()>
99    fn is_command_indeterminate(&self, command_id: DOMString) -> bool {
100        // Step 1. If command is not supported or has no indeterminacy, return false.
101        // Step 2. Return true if command is indeterminate, otherwise false.
102        self.command_if_command_is_supported(&command_id)
103            .is_some_and(|command| command.is_indeterminate())
104    }
105
106    /// <https://w3c.github.io/editing/docs/execCommand/#querycommandstate()>
107    fn command_state_for_command(&self, command_id: DOMString) -> bool {
108        // Step 1. If command is not supported or has no state, return false.
109        let Some(command) = self.command_if_command_is_supported(&command_id) else {
110            return false;
111        };
112        let Some(state) = command.current_state(self) else {
113            return false;
114        };
115        // Step 2. If the state override for command is set, return it.
116        if self.state_override() {
117            return true;
118        }
119        // Step 3. Return true if command's state is true, otherwise false.
120        state
121    }
122
123    /// <https://w3c.github.io/editing/docs/execCommand/#querycommandvalue()>
124    fn command_value_for_command(&self, command_id: DOMString) -> DOMString {
125        // Step 1. If command is not supported or has no value, return the empty string.
126        let Some(command) = self.command_if_command_is_supported(&command_id) else {
127            return DOMString::new();
128        };
129        let Some(value) = command.current_value(self) else {
130            return DOMString::new();
131        };
132        // Step 2. If command is "fontSize" and its value override is set,
133        // convert the value override to an integer number of pixels and return the legacy font size for the result.
134        // TODO
135
136        // Step 3. If the value override for command is set, return it.
137        if let Some(value_override) = self.value_override() {
138            return value_override;
139        }
140        // Step 4. Return command's value.
141        value
142    }
143
144    /// <https://w3c.github.io/editing/docs/execCommand/#querycommandenabled()>
145    fn check_support_and_enabled(
146        &self,
147        cx: &mut js::context::JSContext,
148        command_id: &DOMString,
149    ) -> Option<(Box<dyn BaseCommand>, DomRoot<Selection>)> {
150        // Step 2. Return true if command is both supported and enabled, false otherwise.
151        self.command_if_command_is_supported(command_id)
152            .zip(self.selection_if_command_is_enabled(cx, command_id))
153    }
154
155    /// <https://w3c.github.io/editing/docs/execCommand/#execcommand()>
156    fn exec_command_for_command_id(
157        &self,
158        cx: &mut js::context::JSContext,
159        command_id: DOMString,
160        value: DOMString,
161    ) -> bool {
162        // Step 3. If command is not supported or not enabled, return false.
163        let Some((command, selection)) = self.check_support_and_enabled(cx, &command_id) else {
164            return false;
165        };
166        // Step 4. If command is not in the Miscellaneous commands section:
167        // TODO
168
169        // Step 4.1. Let affected editing host be the editing host that is an inclusive ancestor
170        // of the active range's start node and end node, and is not the ancestor of any editing host
171        // that is an inclusive ancestor of the active range's start node and end node.
172        // TODO
173
174        // Step 4.2. Fire an event named "beforeinput" at affected editing host using InputEvent,
175        // with its bubbles and cancelable attributes initialized to true, and its data attribute initialized to null
176        // TODO
177
178        // Step 4.3. If the value returned by the previous step is false, return false.
179        // TODO
180
181        // Step 4.4. If command is not enabled, return false.
182        // TODO
183
184        // Step 4.5. Let affected editing host be the editing host that is an inclusive ancestor
185        // of the active range's start node and end node, and is not the ancestor of any editing host
186        // that is an inclusive ancestor of the active range's start node and end node.
187        // TODO
188
189        // Step 5. Take the action for command, passing value to the instructions as an argument.
190        let result = command.execute(cx, self, &selection, value);
191        // Step 6. If the previous step returned false, return false.
192        if !result {
193            return false;
194        }
195        // Step 7. If the action modified DOM tree, then fire an event named "input" at affected editing
196        // host using InputEvent, with its isTrusted and bubbles attributes initialized to true,
197        // inputType attribute initialized to the mapped value of command, and its data attribute initialized to null.
198        // TODO
199
200        // Step 8. Return true.
201        true
202    }
203}