1use script_bindings::inheritance::Castable;
6
7use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
8use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
9use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
10use crate::dom::bindings::root::DomRoot;
11use crate::dom::bindings::str::DOMString;
12use crate::dom::document::Document;
13use crate::dom::event::Event;
14use crate::dom::event::inputevent::InputEvent;
15use crate::dom::execcommand::basecommand::CommandName;
16use crate::dom::execcommand::commands::fontsize::legacy_font_size_for;
17use crate::dom::html::htmlelement::HTMLElement;
18use crate::dom::node::Node;
19use crate::dom::selection::Selection;
20use crate::script_runtime::CanGc;
21
22fn is_command_listed_in_miscellaneous_section(command_name: CommandName) -> bool {
24 matches!(
25 command_name,
26 CommandName::DefaultParagraphSeparator |
27 CommandName::Redo |
28 CommandName::SelectAll |
29 CommandName::StyleWithCss |
30 CommandName::Undo |
31 CommandName::Usecss
32 )
33}
34
35fn mapped_value_of_command(command: CommandName) -> DOMString {
37 match command {
38 CommandName::BackColor => "formatBackColor",
39 CommandName::Bold => "formatBold",
40 CommandName::CreateLink => "insertLink",
41 CommandName::Cut => "deleteByCut",
42 CommandName::Delete => "deleteContentBackward",
43 CommandName::FontName => "formatFontName",
44 CommandName::ForeColor => "formatFontColor",
45 CommandName::ForwardDelete => "deleteContentForward",
46 CommandName::Indent => "formatIndent",
47 CommandName::InsertHorizontalRule => "insertHorizontalRule",
48 CommandName::InsertLineBreak => "insertLineBreak",
49 CommandName::InsertOrderedList => "insertOrderedList",
50 CommandName::InsertParagraph => "insertParagraph",
51 CommandName::InsertText => "insertText",
52 CommandName::InsertUnorderedList => "insertUnorderedList",
53 CommandName::JustifyCenter => "formatJustifyCenter",
54 CommandName::JustifyFull => "formatJustifyFull",
55 CommandName::JustifyLeft => "formatJustifyLeft",
56 CommandName::JustifyRight => "formatJustifyRight",
57 CommandName::Outdent => "formatOutdent",
58 CommandName::Paste => "insertFromPaste",
59 CommandName::Redo => "historyRedo",
60 CommandName::Strikethrough => "formatStrikeThrough",
61 CommandName::Superscript => "formatSuperscript",
62 CommandName::Undo => "historyUndo",
63 _ => "",
64 }
65 .into()
66}
67
68impl Node {
69 fn is_in_plaintext_only_state(&self) -> bool {
70 self.downcast::<HTMLElement>()
71 .is_some_and(|el| el.ContentEditable().str() == "plaintext-only")
72 }
73}
74
75impl Document {
76 fn selection_if_command_is_enabled(
78 &self,
79 cx: &mut js::context::JSContext,
80 command_name: CommandName,
81 ) -> Option<DomRoot<Selection>> {
82 let selection = self.GetSelection(CanGc::from_cx(cx))?;
83 if is_command_listed_in_miscellaneous_section(command_name) {
88 return Some(selection);
89 }
90 let range = selection.active_range()?;
92 let start_container_editing_host = range.start_container().editing_host_of()?;
94 let end_container_editing_host = range.end_container().editing_host_of()?;
98 if !command_name.is_enabled_in_plaintext_only_state() &&
105 (start_container_editing_host.is_in_plaintext_only_state() ||
106 end_container_editing_host.is_in_plaintext_only_state())
107 {
108 None
109 } else {
110 Some(selection)
111 }
112 }
113
114 fn command_if_command_is_supported(&self, command_id: &DOMString) -> Option<CommandName> {
116 Some(match &*command_id.str().to_lowercase() {
119 "delete" => CommandName::Delete,
120 "defaultparagraphseparator" => CommandName::DefaultParagraphSeparator,
121 "fontsize" => CommandName::FontSize,
122 "stylewithcss" => CommandName::StyleWithCss,
123 _ => return None,
124 })
125 }
126}
127
128pub(crate) trait DocumentExecCommandSupport {
129 fn is_command_supported(&self, command_id: DOMString) -> bool;
130 fn is_command_indeterminate(&self, command_id: DOMString) -> bool;
131 fn command_state_for_command(&self, command_id: DOMString) -> bool;
132 fn command_value_for_command(
133 &self,
134 cx: &mut js::context::JSContext,
135 command_id: DOMString,
136 ) -> DOMString;
137 fn check_support_and_enabled(
138 &self,
139 cx: &mut js::context::JSContext,
140 command_id: &DOMString,
141 ) -> Option<(CommandName, DomRoot<Selection>)>;
142 fn exec_command_for_command_id(
143 &self,
144 cx: &mut js::context::JSContext,
145 command_id: DOMString,
146 value: DOMString,
147 ) -> bool;
148}
149
150impl DocumentExecCommandSupport for Document {
151 fn is_command_supported(&self, command_id: DOMString) -> bool {
153 self.command_if_command_is_supported(&command_id).is_some()
154 }
155
156 fn is_command_indeterminate(&self, command_id: DOMString) -> bool {
158 self.command_if_command_is_supported(&command_id)
161 .is_some_and(|command| command.is_indeterminate())
162 }
163
164 fn command_state_for_command(&self, command_id: DOMString) -> bool {
166 let Some(command) = self.command_if_command_is_supported(&command_id) else {
168 return false;
169 };
170 let Some(state) = command.current_state(self) else {
171 return false;
172 };
173 self.state_override(&command).unwrap_or(state)
176 }
177
178 fn command_value_for_command(
180 &self,
181 cx: &mut js::context::JSContext,
182 command_id: DOMString,
183 ) -> DOMString {
184 let Some(command) = self.command_if_command_is_supported(&command_id) else {
186 return DOMString::new();
187 };
188 let Some(value) = command.current_value(cx, self) else {
189 return DOMString::new();
190 };
191 self.value_override(&command)
193 .map(|value_override| {
194 if command == CommandName::FontSize {
197 value_override
198 .parse::<i32>()
199 .map(|parsed| legacy_font_size_for(parsed as f32, self))
200 .unwrap_or(value_override)
201 } else {
202 value_override
203 }
204 })
205 .unwrap_or(value)
207 }
208
209 fn check_support_and_enabled(
211 &self,
212 cx: &mut js::context::JSContext,
213 command_id: &DOMString,
214 ) -> Option<(CommandName, DomRoot<Selection>)> {
215 let command = self.command_if_command_is_supported(command_id)?;
217 let selection = self.selection_if_command_is_enabled(cx, command)?;
218 Some((command, selection))
219 }
220
221 fn exec_command_for_command_id(
223 &self,
224 cx: &mut js::context::JSContext,
225 command_id: DOMString,
226 value: DOMString,
227 ) -> bool {
228 let window = self.window();
229 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
231 return false;
232 };
233 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
235 let affected_editing_host = selection
239 .active_range()
240 .expect("Must always have an active range")
241 .CommonAncestorContainer()
242 .editing_host_of()
243 .expect("Must always have an editing host if command is enabled");
244
245 let event = InputEvent::new(
248 window,
249 None,
250 atom!("beforeinput"),
251 true,
252 true,
253 Some(window),
254 0,
255 None,
256 false,
257 "".into(),
258 CanGc::from_cx(cx),
259 );
260 let event = event.upcast::<Event>();
261 if !event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx)) {
263 return false;
264 }
265
266 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
268 return false;
269 };
270 selection = new_selection;
271
272 selection
276 .active_range()
277 .expect("Must always have an active range")
278 .CommonAncestorContainer()
279 .editing_host_of()
280 } else {
281 None
282 };
283
284 let result = command.execute(cx, self, &selection, value);
286 if !result {
288 return false;
289 }
290 if let Some(affected_editing_host) = affected_editing_host {
294 let event = InputEvent::new(
295 window,
296 None,
297 atom!("input"),
298 true,
299 false,
300 Some(window),
301 0,
302 None,
303 false,
304 mapped_value_of_command(command),
305 CanGc::from_cx(cx),
306 );
307 let event = event.upcast::<Event>();
308 event.set_trusted(true);
309 event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx));
310 }
311
312 true
314 }
315}