Skip to main content

script/dom/execcommand/commands/
insertlinebreak.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 js::context::JSContext;
6use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
7use script_bindings::codegen::GenericBindings::RangeBinding::RangeMethods;
8use script_bindings::codegen::GenericBindings::SelectionBinding::SelectionMethods;
9use script_bindings::inheritance::Castable;
10
11use crate::dom::bindings::root::DomRoot;
12use crate::dom::document::Document;
13use crate::dom::element::Element;
14use crate::dom::execcommand::contenteditable::node::{NodeOrString, is_allowed_child};
15use crate::dom::execcommand::contenteditable::selection::SelectionDeletionStripWrappers;
16use crate::dom::selection::Selection;
17use crate::dom::text::Text;
18
19/// <https://w3c.github.io/editing/docs/execCommand/#the-insertlinebreak-command>
20pub(crate) fn execute_insert_line_break_command(
21    cx: &mut JSContext,
22    document: &Document,
23    selection: &Selection,
24) -> bool {
25    // Step 1. Delete the selection, with strip wrappers false.
26    selection.delete_the_selection(
27        cx,
28        document,
29        Default::default(),
30        SelectionDeletionStripWrappers::NoStrip,
31        Default::default(),
32    );
33
34    // Step 2. If the active range's start node is neither editable nor an editing host, return
35    //         true.
36    let mut active_range = selection
37        .active_range(cx)
38        .expect("Must always have an active range.");
39    if !active_range.start_container().is_editable_or_editing_host() {
40        return true;
41    }
42
43    // Step 3. If the active range's start node is an Element, and "br" is not an allowed child of
44    //         it, return true.
45    if active_range.start_container().is::<Element>() &&
46        !is_allowed_child(
47            NodeOrString::String("br".to_owned()),
48            NodeOrString::from_node(&active_range.start_container(), cx.no_gc()),
49        )
50    {
51        return false;
52    }
53
54    // Step 4. If the active range's start node is not an Element, and "br" is not an allowed child
55    //         of the active range's start node's parent, return true.
56    if !active_range.start_container().is::<Element>() &&
57        !is_allowed_child(
58            NodeOrString::String("br".to_owned()),
59            NodeOrString::from_node(
60                &active_range
61                    .start_container()
62                    .GetParentNode()
63                    .expect("Must always have a parent."),
64                cx.no_gc(),
65            ),
66        )
67    {
68        return false;
69    }
70
71    // Step 5. If the active range's start node is a Text node and its start offset is zero, call
72    //         collapse() on the context object's selection, with first argument equal to the
73    //         active range's start node's parent and second argument equal to the active range's
74    //         start node's index.
75    if active_range.start_container().is::<Text>() && active_range.start_offset() == 0 {
76        if selection
77            .Collapse(
78                cx,
79                active_range.start_container().GetParentNode().as_deref(),
80                active_range.start_container().index(),
81            )
82            .is_err()
83        {
84            unreachable!("Should always be able to collapse the selection.");
85        }
86        active_range = selection
87            .active_range(cx)
88            .expect("Must always have an active range");
89    }
90
91    // Step 6. If the active range's start node is a Text node and its start offset is the length
92    //         of its start node, call collapse() on the context object's selection, with first
93    //         argument equal to the active range's start node's parent and second argument equal
94    //         to one plus the active range's start node's index.
95    if active_range.start_container().is::<Text>() &&
96        active_range.start_offset() == active_range.start_container().len()
97    {
98        if selection
99            .Collapse(
100                cx,
101                active_range.start_container().GetParentNode().as_deref(),
102                1 + active_range.start_container().index(),
103            )
104            .is_err()
105        {
106            unreachable!("Should always be able to collapse the selection.");
107        }
108        active_range = selection
109            .active_range(cx)
110            .expect("Must always have an active range");
111    }
112
113    // Step 7. Let br be the result of calling createElement("br") on the context object.
114    let br = document.create_element(cx, "br");
115
116    // Step 8. Call insertNode(br) on the active range.
117    let br_node = DomRoot::upcast(br);
118    if active_range.InsertNode(cx, &br_node).is_err() {
119        unreachable!("The node should always be insertable.");
120    }
121
122    // Step 9. Call collapse() on the context object's selection, with br's parent as the first
123    //         argument and one plus br's index as the second argument.
124    if selection
125        .Collapse(cx, br_node.GetParentNode().as_deref(), 1 + br_node.index())
126        .is_err()
127    {
128        unreachable!("Should always be able to collapse the selection.");
129    }
130
131    // Step 10. If br is a collapsed line break, call createElement("br") on the context object and
132    //          let extra br be the result, then call insertNode(extra br) on the active range.
133    // TODO: Implement this.
134
135    // Step 11. Return true.
136    true
137}