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