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