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::inheritance::Castable;
7
8use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
9use crate::dom::document::Document;
10use crate::dom::element::Element;
11use crate::dom::execcommand::contenteditable::node::{
12    node_matches_local_name, record_the_values, restore_the_values, split_the_parent,
13};
14use crate::dom::execcommand::contenteditable::selection::SelectionDeleteDirection;
15use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
16use crate::dom::html::htmlbrelement::HTMLBRElement;
17use crate::dom::html::htmlhrelement::HTMLHRElement;
18use crate::dom::html::htmlimageelement::HTMLImageElement;
19use crate::dom::html::htmltableelement::HTMLTableElement;
20use crate::dom::selection::Selection;
21use crate::dom::text::Text;
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            let Some(sibling) = node.GetPreviousSibling() &&
59            sibling.is_editable() &&
60            sibling.is_invisible()
61        {
62            sibling.remove_self(cx);
63            continue;
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            let child = node
69                .children_unrooted(cx.no_gc())
70                .nth(offset as usize - 1)
71                .map(|node| node.as_rooted());
72            if let Some(child) = child &&
73                child.is_editable() &&
74                child.is_invisible()
75            {
76                child.remove_self(cx);
77                offset -= 1;
78                continue;
79            }
80        }
81        // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
82        // set offset to the index of node, then set node to its parent.
83        if (offset == 0 && node.is_inline_node()) || node.is_invisible() {
84            offset = node.index();
85            node = node.GetParentNode().expect("Must always have a parent");
86            continue;
87        }
88        if offset > 0 {
89            let child = node
90                .children_unrooted(cx.no_gc())
91                .nth(offset as usize - 1)
92                .map(|node| node.as_rooted());
93            if let Some(child) = child {
94                // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
95                // remove that child from node, preserving its descendants. Then return true.
96                if child.is_editable() && child.is::<HTMLAnchorElement>() {
97                    child.remove_preserving_its_descendants(cx);
98                    return true;
99                }
100                // 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,
101                // set node to that child, then set offset to the length of node.
102                if !(child.is_block_node() ||
103                    child.is::<HTMLBRElement>() ||
104                    child.is::<HTMLImageElement>())
105                {
106                    node = child;
107                    offset = node.len();
108                    continue;
109                }
110            }
111        }
112        // Step 4.6. Otherwise, break from this loop.
113        break;
114    }
115
116    // Step 5. If node is a Text node and offset is not zero, or if node is
117    // a block node that has a child with index offset − 1 and that child is a br or hr or img:
118    if (node.is::<Text>() && offset != 0) ||
119        (offset > 0 &&
120            node.is_block_node() &&
121            node.children_unrooted(cx.no_gc())
122                .nth(offset as usize - 1)
123                .is_some_and(|child| {
124                    child.is::<HTMLBRElement>() ||
125                        child.is::<HTMLHRElement>() ||
126                        child.is::<HTMLImageElement>()
127                }))
128    {
129        // Step 5.1. Call collapse(node, offset) on the context object's selection.
130        selection.collapse_current_range(&node, offset);
131        // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
132        selection.extend_current_range(&node, offset - 1);
133        // Step 5.3. Delete the selection.
134        selection.delete_the_selection(
135            cx,
136            document,
137            Default::default(),
138            Default::default(),
139            Default::default(),
140        );
141        // Step 5.4. Return true.
142        return true;
143    }
144
145    // Step 6. If node is an inline node, return true.
146    if node.is_inline_node() {
147        return true;
148    }
149
150    // Step 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
151    if node_matches_local_name!(
152        node,
153        local_name!("li") | local_name!("dt") | local_name!("dd")
154    ) && node
155        .GetParentNode()
156        .and_then(|parent| parent.children_unrooted(cx.no_gc()).next())
157        .is_some_and(|first| first == &node) &&
158        offset == 0
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        let values = record_the_values(vec![node.clone()]);
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        restore_the_values(cx, values);
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        if node_matches_local_name!(node, local_name!("dd") | local_name!("dt")) &&
175            node.is_no_allowed_child_in_same_editing_host()
176        {
177            node = node
178                .downcast::<Element>()
179                .expect("Must always be an element")
180                .set_the_tag_name(cx, document.default_single_line_container_name().str());
181        }
182        // Step 7.7. Fix disallowed ancestors of node.
183        node.fix_disallowed_ancestors(cx, document);
184        // Step 7.8. Return true.
185        return true;
186    }
187
188    // Step 8. Let start node equal node and let start offset equal offset.
189    let mut start_node = node.clone();
190    let mut start_offset = offset;
191
192    // Step 9. Repeat the following steps:
193    loop {
194        // Step 9.1. If start offset is zero,
195        // set start offset to the index of start node and then set start node to its parent.
196        if start_offset == 0 {
197            // NOTE: This is not in the spec, but required in case we are traversing out of
198            // an editing host and end up at the root node. Since below we start deleting
199            // backwards and stop at the editing host, it's fine to stop at the root node
200            // as well.
201            let Some(parent) = start_node.GetParentNode() else {
202                break;
203            };
204            start_offset = start_node.index();
205            start_node = parent;
206            continue;
207        }
208        // Step 9.2. Otherwise, if start node has an editable invisible child with index start offset minus one,
209        // remove it from start node and subtract one from start offset.
210        assert!(
211            start_offset > 0,
212            "Must always have a start_offset greater than one"
213        );
214        let child = start_node
215            .children_unrooted(cx.no_gc())
216            .nth(start_offset as usize - 1)
217            .map(|node| node.as_rooted());
218        if let Some(child) = child &&
219            child.is_editable() &&
220            child.is_invisible()
221        {
222            child.remove_self(cx);
223            start_offset -= 1;
224            continue;
225        }
226        // Step 9.3. Otherwise, break from this loop.
227        break;
228    }
229
230    // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
231    // TODO
232
233    // Step 11. If the child of start node with index start offset is a table, return true.
234    if start_node
235        .children_unrooted(cx.no_gc())
236        .nth(start_offset as usize)
237        .is_some_and(|child| child.is::<HTMLTableElement>())
238    {
239        return true;
240    }
241
242    // Step 12. If start node has a child with index start offset − 1, and that child is a table:
243    // TODO
244
245    // Step 13. If offset is zero; and either the child of start node with index start offset
246    // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
247    if offset == 0 &&
248        (start_offset > 0 &&
249            start_node
250                .children_unrooted(cx.no_gc())
251                .nth(start_offset as usize - 1)
252                .is_some_and(|child| {
253                    child.is::<HTMLHRElement>() ||
254                        (child.is::<HTMLBRElement>() &&
255                            child.GetPreviousSibling().is_some_and(|previous| {
256                                previous.is::<HTMLBRElement>() || !previous.is_inline_node()
257                            }))
258                }))
259    {
260        // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
261        selection.collapse_current_range(&start_node, start_offset - 1);
262        // Step 13.2. Call extend(start node, start offset) on the context object's selection.
263        selection.extend_current_range(&start_node, start_offset);
264        // Step 13.3. Delete the selection.
265        selection.delete_the_selection(
266            cx,
267            document,
268            Default::default(),
269            Default::default(),
270            Default::default(),
271        );
272        // Step 13.4. Call collapse(node, offset) on the selection.
273        selection.collapse_current_range(&node, offset);
274        // Step 13.5. Return true.
275        return true;
276    }
277
278    // Step 14. If the child of start node with index start offset is an li or dt or dd, and
279    // that child's firstChild is an inline node, and start offset is not zero:
280    // TODO
281
282    // Step 15. If start node's child with index start offset is an li or dt or dd, and
283    // that child's previousSibling is also an li or dt or dd:
284    // TODO
285
286    // Step 16. While start node has a child with index start offset minus one:
287    loop {
288        if start_offset == 0 {
289            break;
290        }
291        let child = start_node
292            .children_unrooted(cx.no_gc())
293            .nth(start_offset as usize - 1)
294            .map(|node| node.as_rooted());
295        let Some(child) = child else {
296            break;
297        };
298        // Step 16.1. If start node's child with index start offset minus one
299        // is editable and invisible, remove it from start node, then subtract one from start offset.
300        if child.is_editable() && child.is_invisible() {
301            child.remove_self(cx);
302            start_offset -= 1;
303        } else {
304            // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
305            // then set start offset to the length of start node.
306            start_node = child;
307            start_offset = start_node.len();
308        }
309    }
310
311    // Step 17. Call collapse(start node, start offset) on the context object's selection.
312    selection.collapse_current_range(&start_node, start_offset);
313
314    // Step 18. Call extend(node, offset) on the context object's selection.
315    selection.extend_current_range(&node, offset);
316
317    // Step 19. Delete the selection, with direction "backward".
318    selection.delete_the_selection(
319        cx,
320        document,
321        Default::default(),
322        Default::default(),
323        SelectionDeleteDirection::Backward,
324    );
325
326    // Step 20. Return true.
327    true
328}