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