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::bindings::str::DOMString;
10use crate::dom::document::Document;
11use crate::dom::execcommand::basecommand::BaseCommand;
12use crate::dom::execcommand::contenteditable::{
13    NodeExecCommandSupport, SelectionDeleteDirection, SelectionExecCommandSupport, split_the_parent,
14};
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::htmllielement::HTMLLIElement;
20use crate::dom::html::htmltableelement::HTMLTableElement;
21use crate::dom::selection::Selection;
22use crate::dom::text::Text;
23use crate::script_runtime::CanGc;
24
25pub(crate) struct DeleteCommand {}
26
27impl BaseCommand for DeleteCommand {
28    /// <https://w3c.github.io/editing/docs/execCommand/#the-delete-command>
29    fn execute(
30        &self,
31        cx: &mut js::context::JSContext,
32        document: &Document,
33        selection: &Selection,
34        _value: DOMString,
35    ) -> bool {
36        let active_range = selection
37            .active_range()
38            .expect("Must always have an active range");
39        // Step 1. If the active range is not collapsed, delete the selection and return true.
40        if !active_range.collapsed() {
41            selection.delete_the_selection(
42                cx,
43                document,
44                Default::default(),
45                Default::default(),
46                Default::default(),
47            );
48            return true;
49        }
50
51        // Step 2. Canonicalize whitespace at the active range's start.
52        active_range
53            .start_container()
54            .canonicalize_whitespace(active_range.start_offset(), true);
55
56        // Step 3. Let node and offset be the active range's start node and offset.
57        let mut node = active_range.start_container();
58        let mut offset = active_range.start_offset();
59
60        // Step 4. Repeat the following steps:
61        loop {
62            // Step 4.1. If offset is zero and node's previousSibling is an editable invisible node,
63            // remove node's previousSibling from its parent.
64            if offset == 0 {
65                if let Some(sibling) = node.GetPreviousSibling() {
66                    if sibling.is_editable() && sibling.is_invisible() {
67                        sibling.remove_self(CanGc::from_cx(cx));
68                        continue;
69                    }
70                }
71            }
72            // Step 4.2. Otherwise, if node has a child with index offset − 1 and that child is an editable invisible node,
73            // remove that child from node, then subtract one from offset.
74            if offset > 0 {
75                if let Some(child) = node.children().nth(offset as usize - 1) {
76                    if child.is_editable() && child.is_invisible() {
77                        child.remove_self(CanGc::from_cx(cx));
78                        offset -= 1;
79                        continue;
80                    }
81                }
82            }
83            // Step 4.3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible node,
84            // set offset to the index of node, then set node to its parent.
85            if (offset == 0 && node.is_inline_node()) || node.is_invisible() {
86                offset = node.index();
87                node = node.GetParentNode().expect("Must always have a parent");
88                continue;
89            }
90            if offset > 0 {
91                if let Some(child) = node.children().nth(offset as usize - 1) {
92                    // Step 4.4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
93                    // remove that child from node, preserving its descendants. Then return true.
94                    if child.is_editable() && child.is::<HTMLAnchorElement>() {
95                        child.remove_preserving_its_descendants(cx);
96                        return true;
97                    }
98                    // 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,
99                    // set node to that child, then set offset to the length of node.
100                    if !(child.is_block_node() ||
101                        child.is::<HTMLBRElement>() ||
102                        child.is::<HTMLImageElement>())
103                    {
104                        node = child;
105                        offset = node.len();
106                        continue;
107                    }
108                }
109            }
110            // Step 4.6. Otherwise, break from this loop.
111            break;
112        }
113
114        // Step 5. If node is a Text node and offset is not zero, or if node is
115        // a block node that has a child with index offset − 1 and that child is a br or hr or img:
116        if (node.is::<Text>() && offset != 0) ||
117            (offset > 0 &&
118                node.is_block_node() &&
119                node.children()
120                    .nth(offset as usize - 1)
121                    .is_some_and(|child| {
122                        child.is::<HTMLBRElement>() ||
123                            child.is::<HTMLHRElement>() ||
124                            child.is::<HTMLImageElement>()
125                    }))
126        {
127            // Step 5.1. Call collapse(node, offset) on the context object's selection.
128            if selection
129                .Collapse(Some(&node), offset, CanGc::from_cx(cx))
130                .is_err()
131            {
132                unreachable!("Must not fail to collapse");
133            }
134            // Step 5.2. Call extend(node, offset − 1) on the context object's selection.
135            if selection
136                .Extend(&node, offset - 1, CanGc::from_cx(cx))
137                .is_err()
138            {
139                unreachable!("Must not fail to extend");
140            }
141            // Step 5.3. Delete the selection.
142            selection.delete_the_selection(
143                cx,
144                document,
145                Default::default(),
146                Default::default(),
147                Default::default(),
148            );
149            // Step 5.4. Return true.
150            return true;
151        }
152
153        // Step 6. If node is an inline node, return true.
154        if node.is_inline_node() {
155            return true;
156        }
157
158        // Step 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
159        //
160        // TODO: Handle dt or dd
161        if offset == 0 &&
162            node.is::<HTMLLIElement>() &&
163            node.GetParentNode()
164                .and_then(|parent| parent.children().next())
165                .is_some_and(|first| first == node)
166        {
167            // Step 7.1. Let items be a list of all lis that are ancestors of node.
168            // TODO
169            // Step 7.2. Normalize sublists of each item in items.
170            // TODO
171            // Step 7.3. Record the values of the one-node list consisting of node, and let values be the result.
172            // TODO
173            // Step 7.4. Split the parent of the one-node list consisting of node.
174            split_the_parent(cx, &[&node]);
175            // Step 7.5. Restore the values from values.
176            // TODO
177            // Step 7.6. If node is a dd or dt, and it is not an allowed child of
178            // any of its ancestors in the same editing host,
179            // set the tag name of node to the default single-line container name
180            // and let node be the result.
181            // TODO
182            // Step 7.7. Fix disallowed ancestors of node.
183            // TODO
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                start_offset = start_node.index();
198                start_node = start_node
199                    .GetParentNode()
200                    .expect("Must always have a parent");
201                continue;
202            }
203            // Step 9.2. Otherwise, if start node has an editable invisible child with index start offset minus one,
204            // remove it from start node and subtract one from start offset.
205            assert!(
206                start_offset > 0,
207                "Must always have a start_offset greater than one"
208            );
209            if let Some(child) = start_node.children().nth(start_offset as usize - 1) {
210                if child.is_editable() && child.is_invisible() {
211                    child.remove_self(CanGc::from_cx(cx));
212                    start_offset -= 1;
213                    continue;
214                }
215            }
216            // Step 9.3. Otherwise, break from this loop.
217            break;
218        }
219
220        // Step 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host that's an indentation element:
221        // TODO
222
223        // Step 11. If the child of start node with index start offset is a table, return true.
224        if start_node
225            .children()
226            .nth(start_offset as usize)
227            .is_some_and(|child| child.is::<HTMLTableElement>())
228        {
229            return true;
230        }
231
232        // Step 12. If start node has a child with index start offset − 1, and that child is a table:
233        // TODO
234
235        // Step 13. If offset is zero; and either the child of start node with index start offset
236        // minus one is an hr, or the child is a br whose previousSibling is either a br or not an inline node:
237        if offset == 0 &&
238            (start_offset > 0 &&
239                start_node
240                    .children()
241                    .nth(start_offset as usize - 1)
242                    .is_some_and(|child| {
243                        child.is::<HTMLHRElement>() ||
244                            (child.is::<HTMLBRElement>() &&
245                                child.GetPreviousSibling().is_some_and(|previous| {
246                                    previous.is::<HTMLBRElement>() || !previous.is_inline_node()
247                                }))
248                    }))
249        {
250            // Step 13.1. Call collapse(start node, start offset − 1) on the context object's selection.
251            if selection
252                .Collapse(Some(&start_node), start_offset - 1, CanGc::from_cx(cx))
253                .is_err()
254            {
255                unreachable!("Must not fail to collapse");
256            }
257            // Step 13.2. Call extend(start node, start offset) on the context object's selection.
258            if selection
259                .Extend(&start_node, start_offset, CanGc::from_cx(cx))
260                .is_err()
261            {
262                unreachable!("Must not fail to extend");
263            }
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            if selection
274                .Collapse(Some(&node), offset, CanGc::from_cx(cx))
275                .is_err()
276            {
277                unreachable!("Must not fail to collapse");
278            }
279            // Step 13.5. Return true.
280            return true;
281        }
282
283        // Step 14. If the child of start node with index start offset is an li or dt or dd, and
284        // that child's firstChild is an inline node, and start offset is not zero:
285        // TODO
286
287        // Step 15. If start node's child with index start offset is an li or dt or dd, and
288        // that child's previousSibling is also an li or dt or dd:
289        // TODO
290
291        // Step 16. While start node has a child with index start offset minus one:
292        loop {
293            if start_offset == 0 {
294                break;
295            }
296            let Some(child) = start_node.children().nth(start_offset as usize - 1) else {
297                break;
298            };
299            // Step 16.1. If start node's child with index start offset minus one
300            // is editable and invisible, remove it from start node, then subtract one from start offset.
301            if child.is_editable() && child.is_invisible() {
302                child.remove_self(CanGc::from_cx(cx));
303                start_offset -= 1;
304            } else {
305                // Step 16.2. Otherwise, set start node to its child with index start offset minus one,
306                // then set start offset to the length of start node.
307                start_node = child;
308                start_offset = start_node.len();
309            }
310        }
311
312        // Step 17. Call collapse(start node, start offset) on the context object's selection.
313        if selection
314            .Collapse(Some(&start_node), start_offset, CanGc::from_cx(cx))
315            .is_err()
316        {
317            unreachable!("Must not fail to collapse");
318        }
319
320        // Step 18. Call extend(node, offset) on the context object's selection.
321        if selection.Extend(&node, offset, CanGc::from_cx(cx)).is_err() {
322            unreachable!("Must not fail to extend");
323        }
324
325        // Step 19. Delete the selection, with direction "backward".
326        selection.delete_the_selection(
327            cx,
328            document,
329            Default::default(),
330            Default::default(),
331            SelectionDeleteDirection::Backward,
332        );
333
334        // Step 20. Return true.
335        true
336    }
337}