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 "bold" => CommandName::Bold,
121 "delete" => CommandName::Delete,
122 "defaultparagraphseparator" => CommandName::DefaultParagraphSeparator,
123 "fontname" => CommandName::FontName,
124 "fontsize" => CommandName::FontSize,
125 "italic" => CommandName::Italic,
126 "strikethrough" => CommandName::Strikethrough,
127 "stylewithcss" => CommandName::StyleWithCss,
128 "underline" => CommandName::Underline,
129 _ => return None,
130 })
131 }
132}
133
134pub(crate) trait DocumentExecCommandSupport {
135 fn is_command_supported(&self, command_id: DOMString) -> bool;
136 fn is_command_indeterminate(&self, command_id: DOMString) -> bool;
137 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
138 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString;
139 fn check_support_and_enabled(
140 &self,
141 cx: &mut JSContext,
142 command_id: &DOMString,
143 ) -> Option<(CommandName, DomRoot<Selection>)>;
144 fn exec_command_for_command_id(
145 &self,
146 cx: &mut JSContext,
147 command_id: DOMString,
148 value: DOMString,
149 ) -> bool;
150}
151
152impl DocumentExecCommandSupport for Document {
153 fn is_command_supported(&self, command_id: DOMString) -> bool {
155 self.command_if_command_is_supported(&command_id).is_some()
156 }
157
158 fn is_command_indeterminate(&self, command_id: DOMString) -> bool {
160 self.command_if_command_is_supported(&command_id)
163 .is_some_and(|command| command.is_indeterminate())
164 }
165
166 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
168 let Some(command) = self.command_if_command_is_supported(&command_id) else {
170 return false;
171 };
172 let Some(state) = command.current_state(cx, self) else {
173 return false;
174 };
175 self.state_override(&command).unwrap_or(state)
178 }
179
180 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString {
182 let Some(command) = self.command_if_command_is_supported(&command_id) else {
184 return DOMString::new();
185 };
186 let Some(value) = command.current_value(cx, self) else {
187 return DOMString::new();
188 };
189 self.value_override(&command)
191 .map(|value_override| {
192 if command == CommandName::FontSize {
195 maybe_normalize_pixels(&value_override, self).unwrap_or(value_override)
196 } else {
197 value_override
198 }
199 })
200 .unwrap_or(value)
202 }
203
204 fn check_support_and_enabled(
206 &self,
207 cx: &mut JSContext,
208 command_id: &DOMString,
209 ) -> Option<(CommandName, DomRoot<Selection>)> {
210 let command = self.command_if_command_is_supported(command_id)?;
212 let selection = self.selection_if_command_is_enabled(cx, command)?;
213 Some((command, selection))
214 }
215
216 fn exec_command_for_command_id(
218 &self,
219 cx: &mut JSContext,
220 command_id: DOMString,
221 value: DOMString,
222 ) -> bool {
223 let window = self.window();
224 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
226 return false;
227 };
228 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
230 let Some(affected_editing_host) = selection
234 .active_range()
235 .expect("Must always have an active range")
236 .CommonAncestorContainer()
237 .editing_host_of()
238 else {
239 return false;
240 };
241
242 let event = InputEvent::new(
245 window,
246 None,
247 atom!("beforeinput"),
248 true,
249 true,
250 Some(window),
251 0,
252 None,
253 false,
254 "".into(),
255 CanGc::from_cx(cx),
256 );
257 let event = event.upcast::<Event>();
258 if !event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx)) {
260 return false;
261 }
262
263 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
265 return false;
266 };
267 selection = new_selection;
268
269 selection
273 .active_range()
274 .expect("Must always have an active range")
275 .CommonAncestorContainer()
276 .editing_host_of()
277 } else {
278 None
279 };
280
281 let result = command.execute(cx, self, &selection, value);
283 if !result {
285 return false;
286 }
287 if let Some(affected_editing_host) = affected_editing_host {
291 let event = InputEvent::new(
292 window,
293 None,
294 atom!("input"),
295 true,
296 false,
297 Some(window),
298 0,
299 None,
300 false,
301 mapped_value_of_command(command),
302 CanGc::from_cx(cx),
303 );
304 let event = event.upcast::<Event>();
305 event.set_trusted(true);
306 event.fire(affected_editing_host.upcast(), CanGc::from_cx(cx));
307 }
308
309 true
311 }
312}