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}