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;
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 JSContext,
80 command_name: CommandName,
81 ) -> Option<DomRoot<Selection>> {
82 let selection = self.GetSelection(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(cx, &range, &start_container_editing_host) {
104 return None;
105 }
106
107 if !command_name.is_enabled_in_plaintext_only_state() &&
109 (start_container_editing_host.is_in_plaintext_only_state() ||
110 end_container_editing_host.is_in_plaintext_only_state())
111 {
112 None
113 } else {
114 Some(selection)
115 }
116 }
117
118 fn command_if_command_is_supported(&self, command_id: &DOMString) -> Option<CommandName> {
120 Some(match &*command_id.str().to_lowercase() {
123 "backcolor" => CommandName::BackColor,
124 "bold" => CommandName::Bold,
125 "createlink" => CommandName::CreateLink,
126 "delete" => CommandName::Delete,
127 "defaultparagraphseparator" => CommandName::DefaultParagraphSeparator,
128 "fontname" => CommandName::FontName,
129 "fontsize" => CommandName::FontSize,
130 "forecolor" => CommandName::ForeColor,
131 "forwarddelete" => CommandName::ForwardDelete,
132 "hilitecolor" => CommandName::HiliteColor,
133 "inserthorizontalrule" => CommandName::InsertHorizontalRule,
134 "insertimage" => CommandName::InsertImage,
135 "insertparagraph" => CommandName::InsertParagraph,
136 "inserttext" => CommandName::InsertText,
137 "italic" => CommandName::Italic,
138 "removeformat" => CommandName::RemoveFormat,
139 "strikethrough" => CommandName::Strikethrough,
140 "stylewithcss" => CommandName::StyleWithCss,
141 "subscript" => CommandName::Subscript,
142 "superscript" => CommandName::Superscript,
143 "underline" => CommandName::Underline,
144 "unlink" => CommandName::Unlink,
145 _ => return None,
146 })
147 }
148}
149
150pub(crate) trait DocumentExecCommandSupport {
151 fn is_command_supported(&self, command_id: DOMString) -> bool;
152 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
153 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
154 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString;
155 fn check_support_and_enabled(
156 &self,
157 cx: &mut JSContext,
158 command_id: &DOMString,
159 ) -> Option<(CommandName, DomRoot<Selection>)>;
160 fn exec_command_for_command_id(
161 &self,
162 cx: &mut JSContext,
163 command_id: DOMString,
164 value: DOMString,
165 ) -> bool;
166}
167
168impl DocumentExecCommandSupport for Document {
169 fn is_command_supported(&self, command_id: DOMString) -> bool {
171 self.command_if_command_is_supported(&command_id).is_some()
172 }
173
174 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
176 self.command_if_command_is_supported(&command_id)
179 .is_some_and(|command| command.is_indeterminate(cx, self))
180 }
181
182 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
184 let Some(command) = self.command_if_command_is_supported(&command_id) else {
186 return false;
187 };
188 let Some(state) = command.current_state(cx, self) else {
189 return false;
190 };
191 self.state_override(&command).unwrap_or(state)
194 }
195
196 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString {
198 let Some(command) = self.command_if_command_is_supported(&command_id) else {
200 return DOMString::new();
201 };
202 let Some(value) = command.current_value(cx, self) else {
203 return DOMString::new();
204 };
205 self.value_override(&command)
207 .map(|value_override| {
208 if command == CommandName::FontSize {
211 maybe_normalize_pixels(&value_override, self).unwrap_or(value_override)
212 } else {
213 value_override
214 }
215 })
216 .unwrap_or(value)
218 }
219
220 fn check_support_and_enabled(
222 &self,
223 cx: &mut JSContext,
224 command_id: &DOMString,
225 ) -> Option<(CommandName, DomRoot<Selection>)> {
226 let command = self.command_if_command_is_supported(command_id)?;
228 let selection = self.selection_if_command_is_enabled(cx, command)?;
229 Some((command, selection))
230 }
231
232 fn exec_command_for_command_id(
234 &self,
235 cx: &mut JSContext,
236 command_id: DOMString,
237 value: DOMString,
238 ) -> bool {
239 let window = self.window();
240 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
242 return false;
243 };
244 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
246 let Some(affected_editing_host) = selection
250 .active_range()
251 .expect("Must always have an active range")
252 .CommonAncestorContainer()
253 .editing_host_of()
254 else {
255 return false;
256 };
257
258 let event = InputEvent::new(
261 cx,
262 window,
263 None,
264 atom!("beforeinput"),
265 true,
266 true,
267 Some(window),
268 0,
269 None,
270 false,
271 "".into(),
272 );
273 let event = event.upcast::<Event>();
274 if !event.fire(cx, affected_editing_host.upcast()) {
276 return false;
277 }
278
279 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
281 return false;
282 };
283 selection = new_selection;
284
285 selection
289 .active_range()
290 .expect("Must always have an active range")
291 .CommonAncestorContainer()
292 .editing_host_of()
293 } else {
294 None
295 };
296
297 let result = command.execute(cx, self, &selection, value);
299 if !result {
301 return false;
302 }
303 if let Some(affected_editing_host) = affected_editing_host {
307 let event = InputEvent::new(
308 cx,
309 window,
310 None,
311 atom!("input"),
312 true,
313 false,
314 Some(window),
315 0,
316 None,
317 false,
318 mapped_value_of_command(command),
319 );
320 let event = event.upcast::<Event>();
321 event.set_trusted(true);
322 event.fire(cx, affected_editing_host.upcast());
323 }
324
325 true
327 }
328}