script/dom/execcommand/commands/delete.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use script_bindings::inheritance::Castable;
6
7use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
8use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
9use crate::dom::bindings::str::DOMString;
10use crate::dom::document::Document;
11use crate::dom::execcommand::basecommand::BaseCommand;
12use crate::dom::execcommand::contenteditable::{
13 NodeExecCommandSupport, SelectionDeleteDirection, SelectionExecCommandSupport, split_the_parent,
14};
15use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
16use crate::dom::html::htmlbrelement::HTMLBRElement;
17use crate::dom::html::htmlhrelement::HTMLHRElement;
18use crate::dom::html::htmlimageelement::HTMLImageElement;
19use crate::dom::html::htmllielement::HTMLLIElement;
20use crate::dom::html::htmltableelement::HTMLTableElement;
21use crate::dom::selection::Selection;
22use crate::dom::text::Text;
23use crate::script_runtime::CanGc;
24
25pub(crate) struct DeleteCommand {}
26
27impl BaseCommand for DeleteCommand {
28 /// <https://w3c.github.io/editing/docs/execCommand/#the-delete-command>
29 fn execute(
30 &self,
31 cx: &mut js::context::JSContext,
32 document: &Document,
33 selection: &Selection,
34 _value: DOMString,
35 ) -> bool {
36 let active_range = selection
37 .active_range()
38 .expect("Must always have an active range");
39 // Step 1. If the active range is not collapsed, delete the selection and return true.
40 if !active_range.collapsed() {
41 selection.delete_the_selection(
42 cx,
43 document,
44 Default::default(),
45 Default::default(),
46 Default::default(),
47 );
48 return true;
49 }
50
51 // Step 2. Canonicalize whitespace at the active range's start.
52 active_range
53 .start_container()
54 .canonicalize_whitespace(active_range.start_offset(), true);
55
56 // Step 3. Let node and offset be the active range's start node and offset.
57 let mut node = active_range.start_container();
58 let mut offset = active_range.start_offset();
59
60 // Step 4. Repeat the following steps:
61 loop {
62 // Step 4.1. If offset is zero and node's previousSibling is an editable invisible node,
63 // remove node's previousSibling from its parent.
64 if offset == 0 {
65 if let Some(sibling) = node.GetPreviousSibling() {
66 if sibling.is_editable() && sibling.is_invisible() {
67 sibling.remove_self(CanGc::from_cx(cx));
68 continue;
69 }
70 }
71 }
72 // Step 4.2. Otherwise, if node has a child with index offset − 1 and that child is an editable invisible node,
73 // remove that child from node, then subtract one from offset.
74 if offset > 0 {
75 if let Some(child) = node.children().nth(offset as usize - 1) {
76 if child.is_editable() && child.is_invisible() {
77 child.remove_self(CanGc::from_cx(cx));
78 offset -= 1;
79 continue;
80 }
81 }
82 }
83 // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
84 // set offset to the index of node, then set node to its parent.
85 if (offset == 0 && node.is_inline_node()) || node.is_invisible() {
86 offset = node.index();
87 node = node.GetParentNode().expect("Must always have a parent");
88 continue;
89 }
90 if offset > 0 {
91 if let Some(child) = node.children().nth(offset as usize - 1) {
92 // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
93 // remove that child from node, preserving its descendants. Then return true.
94 if child.is_editable() && child.is::<HTMLAnchorElement>() {
95 child.remove_preserving_its_descendants(cx);
96 return true;
97 }
98 // Step 4.5. Otherwise, if node has a child with index offset − 1 and that child is not a block node or a br or an img,
99 // set node to that child, then set offset to the length of node.
100 if !(child.is_block_node() ||
101 child.is::<HTMLBRElement>() ||
102 child.is::<HTMLImageElement>())
103 {
104 node = child;
105 offset = node.len();
106 continue;
107 }
108 }
109 }
110 // Step 4.6. Otherwise, break from this loop.
111 break;
112 }
113
114 // Step 5. If node is a Text node and offset is not zero, or if node is
115 // a block node that has a child with index offset − 1 and that child is a br or hr or img:
116 if (node.is::<Text>() && offset != 0) ||
117 (offset > 0 &&
118 node.is_block_node() &&
119 node.children()
120 .nth(offset as usize - 1)
121 .is_some_and(|child| {
122 child.is::<HTMLBRElement>() ||
123 child.is::<HTMLHRElement>() ||
124 child.is::<HTMLImageElement>()
125 }))
126 {
127 // Step 5.1. Call collapse(node, offset) on the context object's selection.
128 if selection
129 .Collapse(Some(&node), offset, CanGc::from_cx(cx))
130 .is_err()
131 {
132 unreachable!("Must not fail to collapse");
133 }
134 // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
135 if selection
136 .Extend(&node, offset - 1, CanGc::from_cx(cx))
137 .is_err()
138 {
139 unreachable!("Must not fail to extend");
140 }
141 // Step 5.3. Delete the selection.
142 selection.delete_the_selection(
143 cx,
144 document,
145 Default::default(),
146 Default::default(),
147 Default::default(),
148 );
149 // Step 5.4. Return true.
150 return true;
151 }
152
153 // Step 6. If node is an inline node, return true.
154 if node.is_inline_node() {
155 return true;
156 }
157
158 // Step 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
159 //
160 // TODO: Handle dt or dd
161 if offset == 0 &&
162 node.is::<HTMLLIElement>() &&
163 node.GetParentNode()
164 .and_then(|parent| parent.children().next())
165 .is_some_and(|first| first == node)
166 {
167 // Step 7.1. Let items be a list of all lis that are ancestors of node.
168 // TODO
169 // Step 7.2. Normalize sublists of each item in items.
170 // TODO
171 // Step 7.3. Record the values of the one-node list consisting of node, and let values be the result.
172 // TODO
173 // Step 7.4. Split the parent of the one-node list consisting of node.
174 split_the_parent(cx, &[&node]);
175 // Step 7.5. Restore the values from values.
176 // TODO
177 // Step 7.6. If node is a dd or dt, and it is not an allowed child of
178 // any of its ancestors in the same editing host,
179 // set the tag name of node to the default single-line container name
180 // and let node be the result.
181 // TODO
182 // Step 7.7. Fix disallowed ancestors of node.
183 // TODO
184 // Step 7.8. Return true.
185 return true;
186 }
187
188 // Step 8. Let start node equal node and let start offset equal offset.
189 let mut start_node = node.clone();
190 let mut start_offset = offset;
191
192 // Step 9. Repeat the following steps:
193 loop {
194 // Step 9.1. If start offset is zero,
195 // set start offset to the index of start node and then set start node to its parent.
196 if start_offset == 0 {
197 start_offset = start_node.index();
198 start_node = start_node
199 .GetParentNode()
200 .expect("Must always have a parent");
201 continue;
202 }
203 // Step 9.2. Otherwise, if start node has an editable invisible child with index start offset minus one,
204 // remove it from start node and subtract one from start offset.
205 assert!(
206 start_offset > 0,
207 "Must always have a start_offset greater than one"
208 );
209 if let Some(child) = start_node.children().nth(start_offset as usize - 1) {
210 if child.is_editable() && child.is_invisible() {
211 child.remove_self(CanGc::from_cx(cx));
212 start_offset -= 1;
213 continue;
214 }
215 }
216 // Step 9.3. Otherwise, break from this loop.
217 break;
218 }
219
220 // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
221 // TODO
222
223 // Step 11. If the child of start node with index start offset is a table, return true.
224 if start_node
225 .children()
226 .nth(start_offset as usize)
227 .is_some_and(|child| child.is::<HTMLTableElement>())
228 {
229 return true;
230 }
231
232 // Step 12. If start node has a child with index start offset − 1, and that child is a table:
233 // TODO
234
235 // Step 13. If offset is zero; and either the child of start node with index start offset
236 // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
237 if offset == 0 &&
238 (start_offset > 0 &&
239 start_node
240 .children()
241 .nth(start_offset as usize - 1)
242 .is_some_and(|child| {
243 child.is::<HTMLHRElement>() ||
244 (child.is::<HTMLBRElement>() &&
245 child.GetPreviousSibling().is_some_and(|previous| {
246 previous.is::<HTMLBRElement>() || !previous.is_inline_node()
247 }))
248 }))
249 {
250 // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
251 if selection
252 .Collapse(Some(&start_node), start_offset - 1, CanGc::from_cx(cx))
253 .is_err()
254 {
255 unreachable!("Must not fail to collapse");
256 }
257 // Step 13.2. Call extend(start node, start offset) on the context object's selection.
258 if selection
259 .Extend(&start_node, start_offset, CanGc::from_cx(cx))
260 .is_err()
261 {
262 unreachable!("Must not fail to extend");
263 }
264 // Step 13.3. Delete the selection.
265 selection.delete_the_selection(
266 cx,
267 document,
268 Default::default(),
269 Default::default(),
270 Default::default(),
271 );
272 // Step 13.4. Call collapse(node, offset) on the selection.
273 if selection
274 .Collapse(Some(&node), offset, CanGc::from_cx(cx))
275 .is_err()
276 {
277 unreachable!("Must not fail to collapse");
278 }
279 // Step 13.5. Return true.
280 return true;
281 }
282
283 // Step 14. If the child of start node with index start offset is an li or dt or dd, and
284 // that child's firstChild is an inline node, and start offset is not zero:
285 // TODO
286
287 // Step 15. If start node's child with index start offset is an li or dt or dd, and
288 // that child's previousSibling is also an li or dt or dd:
289 // TODO
290
291 // Step 16. While start node has a child with index start offset minus one:
292 loop {
293 if start_offset == 0 {
294 break;
295 }
296 let Some(child) = start_node.children().nth(start_offset as usize - 1) else {
297 break;
298 };
299 // Step 16.1. If start node's child with index start offset minus one
300 // is editable and invisible, remove it from start node, then subtract one from start offset.
301 if child.is_editable() && child.is_invisible() {
302 child.remove_self(CanGc::from_cx(cx));
303 start_offset -= 1;
304 } else {
305 // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
306 // then set start offset to the length of start node.
307 start_node = child;
308 start_offset = start_node.len();
309 }
310 }
311
312 // Step 17. Call collapse(start node, start offset) on the context object's selection.
313 if selection
314 .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
315 .is_err()
316 {
317 unreachable!("Must not fail to collapse");
318 }
319
320 // Step 18. Call extend(node, offset) on the context object's selection.
321 if selection.Extend(&node, offset, CanGc::from_cx(cx)).is_err() {
322 unreachable!("Must not fail to extend");
323 }
324
325 // Step 19. Delete the selection, with direction "backward".
326 selection.delete_the_selection(
327 cx,
328 document,
329 Default::default(),
330 Default::default(),
331 SelectionDeleteDirection::Backward,
332 );
333
334 // Step 20. Return true.
335 true
336 }
337}