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}