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 "hilitecolor" => CommandName::HiliteColor,
132 "insertparagraph" => CommandName::InsertParagraph,
133 "italic" => CommandName::Italic,
134 "removeformat" => CommandName::RemoveFormat,
135 "strikethrough" => CommandName::Strikethrough,
136 "stylewithcss" => CommandName::StyleWithCss,
137 "subscript" => CommandName::Subscript,
138 "superscript" => CommandName::Superscript,
139 "underline" => CommandName::Underline,
140 "unlink" => CommandName::Unlink,
141 _ => return None,
142 })
143 }
144}
145
146pub(crate) trait DocumentExecCommandSupport {
147 fn is_command_supported(&self, command_id: DOMString) -> bool;
148 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
149 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool;
150 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString;
151 fn check_support_and_enabled(
152 &self,
153 cx: &mut JSContext,
154 command_id: &DOMString,
155 ) -> Option<(CommandName, DomRoot<Selection>)>;
156 fn exec_command_for_command_id(
157 &self,
158 cx: &mut JSContext,
159 command_id: DOMString,
160 value: DOMString,
161 ) -> bool;
162}
163
164impl DocumentExecCommandSupport for Document {
165 fn is_command_supported(&self, command_id: DOMString) -> bool {
167 self.command_if_command_is_supported(&command_id).is_some()
168 }
169
170 fn is_command_indeterminate(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
172 self.command_if_command_is_supported(&command_id)
175 .is_some_and(|command| command.is_indeterminate(cx, self))
176 }
177
178 fn command_state_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> bool {
180 let Some(command) = self.command_if_command_is_supported(&command_id) else {
182 return false;
183 };
184 let Some(state) = command.current_state(cx, self) else {
185 return false;
186 };
187 self.state_override(&command).unwrap_or(state)
190 }
191
192 fn command_value_for_command(&self, cx: &mut JSContext, command_id: DOMString) -> DOMString {
194 let Some(command) = self.command_if_command_is_supported(&command_id) else {
196 return DOMString::new();
197 };
198 let Some(value) = command.current_value(cx, self) else {
199 return DOMString::new();
200 };
201 self.value_override(&command)
203 .map(|value_override| {
204 if command == CommandName::FontSize {
207 maybe_normalize_pixels(&value_override, self).unwrap_or(value_override)
208 } else {
209 value_override
210 }
211 })
212 .unwrap_or(value)
214 }
215
216 fn check_support_and_enabled(
218 &self,
219 cx: &mut JSContext,
220 command_id: &DOMString,
221 ) -> Option<(CommandName, DomRoot<Selection>)> {
222 let command = self.command_if_command_is_supported(command_id)?;
224 let selection = self.selection_if_command_is_enabled(cx, command)?;
225 Some((command, selection))
226 }
227
228 fn exec_command_for_command_id(
230 &self,
231 cx: &mut JSContext,
232 command_id: DOMString,
233 value: DOMString,
234 ) -> bool {
235 let window = self.window();
236 let Some((command, mut selection)) = self.check_support_and_enabled(cx, &command_id) else {
238 return false;
239 };
240 let affected_editing_host = if !is_command_listed_in_miscellaneous_section(command) {
242 let Some(affected_editing_host) = selection
246 .active_range()
247 .expect("Must always have an active range")
248 .CommonAncestorContainer()
249 .editing_host_of()
250 else {
251 return false;
252 };
253
254 let event = InputEvent::new(
257 cx,
258 window,
259 None,
260 atom!("beforeinput"),
261 true,
262 true,
263 Some(window),
264 0,
265 None,
266 false,
267 "".into(),
268 );
269 let event = event.upcast::<Event>();
270 if !event.fire(cx, affected_editing_host.upcast()) {
272 return false;
273 }
274
275 let Some(new_selection) = self.selection_if_command_is_enabled(cx, command) else {
277 return false;
278 };
279 selection = new_selection;
280
281 selection
285 .active_range()
286 .expect("Must always have an active range")
287 .CommonAncestorContainer()
288 .editing_host_of()
289 } else {
290 None
291 };
292
293 let result = command.execute(cx, self, &selection, value);
295 if !result {
297 return false;
298 }
299 if let Some(affected_editing_host) = affected_editing_host {
303 let event = InputEvent::new(
304 cx,
305 window,
306 None,
307 atom!("input"),
308 true,
309 false,
310 Some(window),
311 0,
312 None,
313 false,
314 mapped_value_of_command(command),
315 );
316 let event = event.upcast::<Event>();
317 event.set_trusted(true);
318 event.fire(cx, affected_editing_host.upcast());
319 }
320
321 true
323 }
324}