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::{
11 NodeExecCommandSupport, SelectionDeleteDirection, SelectionExecCommandSupport, split_the_parent,
12};
13use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
14use crate::dom::html::htmlbrelement::HTMLBRElement;
15use crate::dom::html::htmlhrelement::HTMLHRElement;
16use crate::dom::html::htmlimageelement::HTMLImageElement;
17use crate::dom::html::htmllielement::HTMLLIElement;
18use crate::dom::html::htmltableelement::HTMLTableElement;
19use crate::dom::selection::Selection;
20use crate::dom::text::Text;
21use crate::script_runtime::CanGc;
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 if let Some(sibling) = node.GetPreviousSibling() {
59 if sibling.is_editable() && sibling.is_invisible() {
60 sibling.remove_self(cx);
61 continue;
62 }
63 }
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 if let Some(child) = node.children().nth(offset as usize - 1) {
69 if child.is_editable() && child.is_invisible() {
70 child.remove_self(cx);
71 offset -= 1;
72 continue;
73 }
74 }
75 }
76 // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
77 // set offset to the index of node, then set node to its parent.
78 if (offset == 0 && node.is_inline_node()) || node.is_invisible() {
79 offset = node.index();
80 node = node.GetParentNode().expect("Must always have a parent");
81 continue;
82 }
83 if offset > 0 {
84 if let Some(child) = node.children().nth(offset as usize - 1) {
85 // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
86 // remove that child from node, preserving its descendants. Then return true.
87 if child.is_editable() && child.is::<HTMLAnchorElement>() {
88 child.remove_preserving_its_descendants(cx);
89 return true;
90 }
91 // 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,
92 // set node to that child, then set offset to the length of node.
93 if !(child.is_block_node() ||
94 child.is::<HTMLBRElement>() ||
95 child.is::<HTMLImageElement>())
96 {
97 node = child;
98 offset = node.len();
99 continue;
100 }
101 }
102 }
103 // Step 4.6. Otherwise, break from this loop.
104 break;
105 }
106
107 // Step 5. If node is a Text node and offset is not zero, or if node is
108 // a block node that has a child with index offset − 1 and that child is a br or hr or img:
109 if (node.is::<Text>() && offset != 0) ||
110 (offset > 0 &&
111 node.is_block_node() &&
112 node.children()
113 .nth(offset as usize - 1)
114 .is_some_and(|child| {
115 child.is::<HTMLBRElement>() ||
116 child.is::<HTMLHRElement>() ||
117 child.is::<HTMLImageElement>()
118 }))
119 {
120 // Step 5.1. Call collapse(node, offset) on the context object's selection.
121 if selection
122 .Collapse(Some(&node), offset, CanGc::from_cx(cx))
123 .is_err()
124 {
125 unreachable!("Must not fail to collapse");
126 }
127 // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
128 if selection
129 .Extend(&node, offset - 1, CanGc::from_cx(cx))
130 .is_err()
131 {
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 if let Some(child) = start_node.children().nth(start_offset as usize - 1) {
203 if child.is_editable() && child.is_invisible() {
204 child.remove_self(cx);
205 start_offset -= 1;
206 continue;
207 }
208 }
209 // Step 9.3. Otherwise, break from this loop.
210 break;
211 }
212
213 // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
214 // TODO
215
216 // Step 11. If the child of start node with index start offset is a table, return true.
217 if start_node
218 .children()
219 .nth(start_offset as usize)
220 .is_some_and(|child| child.is::<HTMLTableElement>())
221 {
222 return true;
223 }
224
225 // Step 12. If start node has a child with index start offset − 1, and that child is a table:
226 // TODO
227
228 // Step 13. If offset is zero; and either the child of start node with index start offset
229 // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
230 if offset == 0 &&
231 (start_offset > 0 &&
232 start_node
233 .children()
234 .nth(start_offset as usize - 1)
235 .is_some_and(|child| {
236 child.is::<HTMLHRElement>() ||
237 (child.is::<HTMLBRElement>() &&
238 child.GetPreviousSibling().is_some_and(|previous| {
239 previous.is::<HTMLBRElement>() || !previous.is_inline_node()
240 }))
241 }))
242 {
243 // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
244 if selection
245 .Collapse(Some(&start_node), start_offset - 1, CanGc::from_cx(cx))
246 .is_err()
247 {
248 unreachable!("Must not fail to collapse");
249 }
250 // Step 13.2. Call extend(start node, start offset) on the context object's selection.
251 if selection
252 .Extend(&start_node, start_offset, CanGc::from_cx(cx))
253 .is_err()
254 {
255 unreachable!("Must not fail to extend");
256 }
257 // Step 13.3. Delete the selection.
258 selection.delete_the_selection(
259 cx,
260 document,
261 Default::default(),
262 Default::default(),
263 Default::default(),
264 );
265 // Step 13.4. Call collapse(node, offset) on the selection.
266 if selection
267 .Collapse(Some(&node), offset, CanGc::from_cx(cx))
268 .is_err()
269 {
270 unreachable!("Must not fail to collapse");
271 }
272 // Step 13.5. Return true.
273 return true;
274 }
275
276 // Step 14. If the child of start node with index start offset is an li or dt or dd, and
277 // that child's firstChild is an inline node, and start offset is not zero:
278 // TODO
279
280 // Step 15. If start node's child with index start offset is an li or dt or dd, and
281 // that child's previousSibling is also an li or dt or dd:
282 // TODO
283
284 // Step 16. While start node has a child with index start offset minus one:
285 loop {
286 if start_offset == 0 {
287 break;
288 }
289 let Some(child) = start_node.children().nth(start_offset as usize - 1) else {
290 break;
291 };
292 // Step 16.1. If start node's child with index start offset minus one
293 // is editable and invisible, remove it from start node, then subtract one from start offset.
294 if child.is_editable() && child.is_invisible() {
295 child.remove_self(cx);
296 start_offset -= 1;
297 } else {
298 // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
299 // then set start offset to the length of start node.
300 start_node = child;
301 start_offset = start_node.len();
302 }
303 }
304
305 // Step 17. Call collapse(start node, start offset) on the context object's selection.
306 if selection
307 .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
308 .is_err()
309 {
310 unreachable!("Must not fail to collapse");
311 }
312
313 // Step 18. Call extend(node, offset) on the context object's selection.
314 if selection.Extend(&node, offset, CanGc::from_cx(cx)).is_err() {
315 unreachable!("Must not fail to extend");
316 }
317
318 // Step 19. Delete the selection, with direction "backward".
319 selection.delete_the_selection(
320 cx,
321 document,
322 Default::default(),
323 Default::default(),
324 SelectionDeleteDirection::Backward,
325 );
326
327 // Step 20. Return true.
328 true
329}