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::document::Document;
10use crate::dom::execcommand::contenteditable::node::split_the_parent;
11use crate::dom::execcommand::contenteditable::selection::SelectionDeleteDirection;
12use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
13use crate::dom::html::htmlbrelement::HTMLBRElement;
14use crate::dom::html::htmlhrelement::HTMLHRElement;
15use crate::dom::html::htmlimageelement::HTMLImageElement;
16use crate::dom::html::htmllielement::HTMLLIElement;
17use crate::dom::html::htmltableelement::HTMLTableElement;
18use crate::dom::selection::Selection;
19use crate::dom::text::Text;
20
21/// <https://w3c.github.io/editing/docs/execCommand/#the-delete-command>
22pub(crate) fn execute_delete_command(
23 cx: &mut js::context::JSContext,
24 document: &Document,
25 selection: &Selection,
26) -> bool {
27 let active_range = selection
28 .active_range()
29 .expect("Must always have an active range");
30 // Step 1. If the active range is not collapsed, delete the selection and return true.
31 if !active_range.collapsed() {
32 selection.delete_the_selection(
33 cx,
34 document,
35 Default::default(),
36 Default::default(),
37 Default::default(),
38 );
39 return true;
40 }
41
42 // Step 2. Canonicalize whitespace at the active range's start.
43 active_range
44 .start_container()
45 .canonicalize_whitespace(active_range.start_offset(), true);
46
47 // Step 3. Let node and offset be the active range's start node and offset.
48 let mut node = active_range.start_container();
49 let mut offset = active_range.start_offset();
50
51 // Step 4. Repeat the following steps:
52 loop {
53 // Step 4.1. If offset is zero and node's previousSibling is an editable invisible node,
54 // remove node's previousSibling from its parent.
55 if offset == 0 {
56 if let Some(sibling) = node.GetPreviousSibling() {
57 if sibling.is_editable() && sibling.is_invisible() {
58 sibling.remove_self(cx);
59 continue;
60 }
61 }
62 }
63 // Step 4.2. Otherwise, if node has a child with index offset − 1 and that child is an editable invisible node,
64 // remove that child from node, then subtract one from offset.
65 if offset > 0 {
66 let child = node
67 .children_unrooted(cx.no_gc())
68 .nth(offset as usize - 1)
69 .map(|node| node.as_rooted());
70 if let Some(child) = child {
71 if child.is_editable() && child.is_invisible() {
72 child.remove_self(cx);
73 offset -= 1;
74 continue;
75 }
76 }
77 }
78 // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
79 // set offset to the index of node, then set node to its parent.
80 if (offset == 0 && node.is_inline_node()) || node.is_invisible() {
81 offset = node.index();
82 node = node.GetParentNode().expect("Must always have a parent");
83 continue;
84 }
85 if offset > 0 {
86 let child = node
87 .children_unrooted(cx.no_gc())
88 .nth(offset as usize - 1)
89 .map(|node| node.as_rooted());
90 if let Some(child) = child {
91 // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
92 // remove that child from node, preserving its descendants. Then return true.
93 if child.is_editable() && child.is::<HTMLAnchorElement>() {
94 child.remove_preserving_its_descendants(cx);
95 return true;
96 }
97 // 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,
98 // set node to that child, then set offset to the length of node.
99 if !(child.is_block_node() ||
100 child.is::<HTMLBRElement>() ||
101 child.is::<HTMLImageElement>())
102 {
103 node = child;
104 offset = node.len();
105 continue;
106 }
107 }
108 }
109 // Step 4.6. Otherwise, break from this loop.
110 break;
111 }
112
113 // Step 5. If node is a Text node and offset is not zero, or if node is
114 // a block node that has a child with index offset − 1 and that child is a br or hr or img:
115 if (node.is::<Text>() && offset != 0) ||
116 (offset > 0 &&
117 node.is_block_node() &&
118 node.children_unrooted(cx.no_gc())
119 .nth(offset as usize - 1)
120 .is_some_and(|child| {
121 child.is::<HTMLBRElement>() ||
122 child.is::<HTMLHRElement>() ||
123 child.is::<HTMLImageElement>()
124 }))
125 {
126 // Step 5.1. Call collapse(node, offset) on the context object's selection.
127 if selection.Collapse(cx, Some(&node), offset).is_err() {
128 unreachable!("Must not fail to collapse");
129 }
130 // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
131 if selection.Extend(cx, &node, offset - 1).is_err() {
132 unreachable!("Must not fail to extend");
133 }
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 //
153 // TODO: Handle dt or dd
154 if offset == 0 &&
155 node.is::<HTMLLIElement>() &&
156 node.GetParentNode()
157 .and_then(|parent| parent.children().next())
158 .is_some_and(|first| first == node)
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 // TODO
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 // TODO
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 // TODO
175 // Step 7.7. Fix disallowed ancestors of node.
176 // TODO
177 // Step 7.8. Return true.
178 return true;
179 }
180
181 // Step 8. Let start node equal node and let start offset equal offset.
182 let mut start_node = node.clone();
183 let mut start_offset = offset;
184
185 // Step 9. Repeat the following steps:
186 loop {
187 // Step 9.1. If start offset is zero,
188 // set start offset to the index of start node and then set start node to its parent.
189 if start_offset == 0 {
190 start_offset = start_node.index();
191 start_node = start_node
192 .GetParentNode()
193 .expect("Must always have a parent");
194 continue;
195 }
196 // Step 9.2. Otherwise, if start node has an editable invisible child with index start offset minus one,
197 // remove it from start node and subtract one from start offset.
198 assert!(
199 start_offset > 0,
200 "Must always have a start_offset greater than one"
201 );
202 let child = start_node
203 .children_unrooted(cx.no_gc())
204 .nth(start_offset as usize - 1)
205 .map(|node| node.as_rooted());
206 if let Some(child) = child {
207 if child.is_editable() && child.is_invisible() {
208 child.remove_self(cx);
209 start_offset -= 1;
210 continue;
211 }
212 }
213 // Step 9.3. Otherwise, break from this loop.
214 break;
215 }
216
217 // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
218 // TODO
219
220 // Step 11. If the child of start node with index start offset is a table, return true.
221 if start_node
222 .children_unrooted(cx.no_gc())
223 .nth(start_offset as usize)
224 .is_some_and(|child| child.is::<HTMLTableElement>())
225 {
226 return true;
227 }
228
229 // Step 12. If start node has a child with index start offset − 1, and that child is a table:
230 // TODO
231
232 // Step 13. If offset is zero; and either the child of start node with index start offset
233 // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
234 if offset == 0 &&
235 (start_offset > 0 &&
236 start_node
237 .children_unrooted(cx.no_gc())
238 .nth(start_offset as usize - 1)
239 .is_some_and(|child| {
240 child.is::<HTMLHRElement>() ||
241 (child.is::<HTMLBRElement>() &&
242 child.GetPreviousSibling().is_some_and(|previous| {
243 previous.is::<HTMLBRElement>() || !previous.is_inline_node()
244 }))
245 }))
246 {
247 // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
248 if selection
249 .Collapse(cx, Some(&start_node), start_offset - 1)
250 .is_err()
251 {
252 unreachable!("Must not fail to collapse");
253 }
254 // Step 13.2. Call extend(start node, start offset) on the context object's selection.
255 if selection.Extend(cx, &start_node, start_offset).is_err() {
256 unreachable!("Must not fail to extend");
257 }
258 // Step 13.3. Delete the selection.
259 selection.delete_the_selection(
260 cx,
261 document,
262 Default::default(),
263 Default::default(),
264 Default::default(),
265 );
266 // Step 13.4. Call collapse(node, offset) on the selection.
267 if selection.Collapse(cx, Some(&node), offset).is_err() {
268 unreachable!("Must not fail to collapse");
269 }
270 // Step 13.5. Return true.
271 return true;
272 }
273
274 // Step 14. If the child of start node with index start offset is an li or dt or dd, and
275 // that child's firstChild is an inline node, and start offset is not zero:
276 // TODO
277
278 // Step 15. If start node's child with index start offset is an li or dt or dd, and
279 // that child's previousSibling is also an li or dt or dd:
280 // TODO
281
282 // Step 16. While start node has a child with index start offset minus one:
283 loop {
284 if start_offset == 0 {
285 break;
286 }
287 let child = start_node
288 .children_unrooted(cx.no_gc())
289 .nth(start_offset as usize - 1)
290 .map(|node| node.as_rooted());
291 let Some(child) = child else {
292 break;
293 };
294 // Step 16.1. If start node's child with index start offset minus one
295 // is editable and invisible, remove it from start node, then subtract one from start offset.
296 if child.is_editable() && child.is_invisible() {
297 child.remove_self(cx);
298 start_offset -= 1;
299 } else {
300 // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
301 // then set start offset to the length of start node.
302 start_node = child;
303 start_offset = start_node.len();
304 }
305 }
306
307 // Step 17. Call collapse(start node, start offset) on the context object's selection.
308 if selection
309 .Collapse(cx, Some(&start_node), start_offset)
310 .is_err()
311 {
312 unreachable!("Must not fail to collapse");
313 }
314
315 // Step 18. Call extend(node, offset) on the context object's selection.
316 if selection.Extend(cx, &node, offset).is_err() {
317 unreachable!("Must not fail to extend");
318 }
319
320 // Step 19. Delete the selection, with direction "backward".
321 selection.delete_the_selection(
322 cx,
323 document,
324 Default::default(),
325 Default::default(),
326 SelectionDeleteDirection::Backward,
327 );
328
329 // Step 20. Return true.
330 true
331}