Skip to main content

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