1use js::context::JSContext;
6use script_bindings::inheritance::Castable;
7
8use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
9use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
10use crate::dom::bindings::codegen::Bindings::RangeBinding::RangeMethods;
11use crate::dom::bindings::root::DomRoot;
12use crate::dom::bindings::str::DOMString;
13use crate::dom::document::Document;
14use crate::dom::event::Event;
15use crate::dom::event::inputevent::InputEvent;
16use crate::dom::execcommand::basecommand::CommandName;
17use crate::dom::execcommand::commands::fontsize::maybe_normalize_pixels;
18use crate::dom::html::htmlelement::HTMLElement;
19use crate::dom::node::Node;
20use crate::dom::selection::Selection;
21use crate::script_runtime::CanGc;
22
23fn is_command_listed_in_miscellaneous_section(command_name: CommandName) -> bool {
25 matches!(
26 command_name,
27 CommandName::DefaultParagraphSeparator |
28 CommandName::Redo |
29 CommandName::SelectAll |
30 CommandName::StyleWithCss |
31 CommandName::Undo |
32 CommandName::Usecss
33 )
34}
35
36fn mapped_value_of_command(command: CommandName) -> DOMString {
38 match command {
39 CommandName::BackColor => "formatBackColor",
40 CommandName::Bold => "formatBold",
41 CommandName::CreateLink => "insertLink",
42 CommandName::Cut => "deleteByCut",
43 CommandName::Delete => "deleteContentBackward",
44 CommandName::FontName => "formatFontName",
45 CommandName::ForeColor => "formatFontColor",
46 CommandName::ForwardDelete => "deleteContentForward",
47 CommandName::Indent => "formatIndent",
48 CommandName::InsertHorizontalRule => "insertHorizontalRule",
49 CommandName::InsertLineBreak => "insertLineBreak",
50 CommandName::InsertOrderedList => "insertOrderedList",
51 CommandName::InsertParagraph => "insertParagraph",
52 CommandName::InsertText => "insertText",
53 CommandName::InsertUnorderedList => "insertUnorderedList",
54 CommandName::JustifyCenter => "formatJustifyCenter",
55 CommandName::JustifyFull => "formatJustifyFull",
56 CommandName::JustifyLeft => "formatJustifyLeft",
57 CommandName::JustifyRight => "formatJustifyRight",
58 CommandName::Outdent => "formatOutdent",
59 CommandName::Paste => "insertFromPaste",
60 CommandName::Redo => "historyRedo",
61 CommandName::Strikethrough => "formatStrikeThrough",
62 CommandName::Superscript => "formatSuperscript",
63 CommandName::Undo => "historyUndo",
64 _ => "",
65 }
66 .into()
67}
68
69impl Node {
70 fn is_in_plaintext_only_state(&self) -> bool {
71 self.downcast::<HTMLElement>()
72 .is_some_and(|el| el.ContentEditable().str() == "plaintext-only")
73 }
74}
75
76impl Document {
77 fn selection_if_command_is_enabled(
79 &self,
80 cx: &mut JSContext,
81 command_name: CommandName,
82 ) -> Option<DomRoot<Selection>> {
83 let selection = self.GetSelection(cx)?;
84 if is_command_listed_in_miscellaneous_section(command_name) {
89 return Some(selection);
90 }
91 let range = selection.active_range()?;
93 let start_container_editing_host = range.start_container().editing_host_of()?;
95 let end_container_editing_host = range.end_container().editing_host_of()?;
99 if !command_name.is_enabled_in_plaintext_only_state() &&
106 (start_container_editing_host.is_in_plaintext_only_state() ||
107 end_container_editing_host.is_in_plaintext_only_state())
108 {
109 None
110 } else {
111 Some(selection)
112 }
113 }
114
115 fn command_if_command_is_supported(&self, command_id: &DOMString) -> Option<CommandName> {
117 Some(match &*command_id.str().to_lowercase() {
120 "backcolor" => CommandName::BackColor,
121 "bold" => CommandName::Bold,
122 "createlink" => CommandName::CreateLink,
123 "delete" => CommandName::Delete,
124 "defaultparagraphseparator" => CommandName::DefaultParagraphSeparator,
125 "fontname" => CommandName::FontName,
126 "fontsize" => CommandName::FontSize,
127 "forecolor" => CommandName::ForeColor,
128 "hilitecolor" => CommandName::HiliteColor,
129 "insertparagraph" => CommandName::InsertParagraph,
130 "italic" => CommandName::Italic,
131 "removeformat" => CommandName::RemoveFormat,
132 "strikethrough" => CommandName::Strikethrough,
133 "stylewithcss" => CommandName::StyleWithCss,
134 "subscript" => CommandName::Subscript,
135 "superscript" => CommandName::Superscript,
136 "underline" => CommandName::Underline,
137 "unlink" => CommandName::Unlink,
138 _ => return None,
139 })
140 }
141}
142
143pub(crate) trait DocumentExecCommandSupport {
144 fn is_command_supported(&self, command_id: DOMString) -> bool;
145 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
146 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
147 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString;
148 fn check_support_and_enabled(
149 &self,
150 cx: &mut JSContext,
151 command_id: &DOMString,
152 ) -> Option<(CommandName, DomRoot<Selection>)>;
153 fn exec_command_for_command_id(
154 &self,
155 cx: &mut JSContext,
156 command_id: DOMString,
157 value: DOMString,
158 ) -> bool;
159}
160
161impl DocumentExecCommandSupport for Document {
162 fn is_command_supported(&self, command_id: DOMString) -> bool {
164 self.command_if_command_is_supported(&command_id).is_some()
165 }
166
167 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
169 self.command_if_command_is_supported(&command_id)
172 .is_some_and(|command| command.is_indeterminate(cx, self))
173 }
174
175 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
177 let Some(command) = self.command_if_command_is_supported(&command_id) else {
179 return false;
180 };
181 let Some(state) = command.current_state(cx, self) else {
182 return false;
183 };
184 self.state_override(&command).unwrap_or(state)
187 }
188
189 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString {
191 let Some(command) = self.command_if_command_is_supported(&command_id) else {
193 return DOMString::new();
194 };
195 let Some(value) = command.current_value(cx, self) else {
196 return DOMString::new();
197 };
198 self.value_override(&command)
200 .map(|value_override| {
201 if command == CommandName::FontSize {
204 maybe_normalize_pixels(&value_override, self).unwrap_or(value_override)
205 } else {
206 value_override
207 }
208 })
209 .unwrap_or(value)
211 }
212
213 fn check_support_and_enabled(
215 &self,
216 cx: &mut JSContext,
217 command_id: &DOMString,
218 ) -> Option<(CommandName, DomRoot<Selection>)> {
219 let command = self.command_if_command_is_supported(command_id)?;
221 let selection = self.selection_if_command_is_enabled(cx, command)?;
222 Some((command, selection))
223 }
224
225 fn exec_command_for_command_id(
227 &self,
228 cx: &mut JSContext,
229 command_id: DOMString,
230 value: DOMString,
231 ) -> bool {
232 let window = self.window();
233 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
235 return false;
236 };
237 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
239 let Some(affected_editing_host) = selection
243 .active_range()
244 .expect("Must always have an active range")
245 .CommonAncestorContainer()
246 .editing_host_of()
247 else {
248 return false;
249 };
250
251 let event = InputEvent::new(
254 window,
255 None,
256 atom!("beforeinput"),
257 true,
258 true,
259 Some(window),
260 0,
261 None,
262 false,
263 "".into(),
264 CanGc::from_cx(cx),
265 );
266 let event = event.upcast::<Event>();
267 if !event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx)) {
269 return false;
270 }
271
272 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
274 return false;
275 };
276 selection = new_selection;
277
278 selection
282 .active_range()
283 .expect("Must always have an active range")
284 .CommonAncestorContainer()
285 .editing_host_of()
286 } else {
287 None
288 };
289
290 let result = command.execute(cx, self, &selection, value);
292 if !result {
294 return false;
295 }
296 if let Some(affected_editing_host) = affected_editing_host {
300 let event = InputEvent::new(
301 window,
302 None,
303 atom!("input"),
304 true,
305 false,
306 Some(window),
307 0,
308 None,
309 false,
310 mapped_value_of_command(command),
311 CanGc::from_cx(cx),
312 );
313 let event = event.upcast::<Event>();
314 event.set_trusted(true);
315 event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx));
316 }
317
318 true
320 }
321}