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}