script/dom/execcommand/contenteditable/
htmlelement.rs1use html5ever::local_name;
6use js::context::JSContext;
7use script_bindings::codegen::GenericBindings::RangeBinding::RangeMethods;
8use script_bindings::inheritance::Castable;
9use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
10use style::properties::{LonghandId, PropertyDeclarationId, ShorthandId};
11
12use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
13use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
14use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
15use crate::dom::bindings::codegen::Bindings::SelectionBinding::SelectionMethods;
16use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
17use crate::dom::bindings::root::DomRoot;
18use crate::dom::element::Element;
19use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
20use crate::dom::execcommand::contenteditable::node::move_preserving_ranges;
21use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
22use crate::dom::html::htmlelement::HTMLElement;
23use crate::dom::html::htmlfontelement::HTMLFontElement;
24use crate::dom::iterators::ShadowIncluding;
25use crate::dom::node::node::{Node, NodeTraits};
26use crate::dom::text::Text;
27
28impl HTMLElement {
29 pub(crate) fn local_name(&self) -> &str {
30 self.upcast::<Element>().local_name()
31 }
32
33 fn remove_value_from_text_decoration(&self, cx: &mut JSContext, value: &str) {
34 let element = self.upcast::<Element>();
35 let mut original_value = String::new();
36 let property;
37
38 {
40 let style_attribute = element.style_attribute().borrow();
41 let Some(declarations) = style_attribute.as_ref() else {
42 return;
43 };
44 let document = element.owner_document();
45 let shared_lock = document.style_shared_author_lock();
46 let read_lock = shared_lock.read();
47 let style = declarations.read_with(&read_lock);
48
49 if style
52 .shorthand_to_css(ShorthandId::TextDecoration, &mut original_value)
53 .is_ok()
54 {
55 property = CssPropertyName::TextDecoration;
56 } else if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
57 LonghandId::TextDecorationLine,
58 )) {
59 if text_decoration.to_css(&mut original_value).is_ok() {
60 property = CssPropertyName::TextDecorationLine;
61 } else {
62 return;
63 }
64 } else {
65 return;
66 }
67 }
68
69 let new_value = original_value
70 .replace(&format!(" {value} "), " ")
71 .replace(&format!(" {value}"), "")
72 .replace(&format!("{value} "), "")
73 .replace(value, "");
74 if new_value.is_empty() {
75 property.remove_from_element(cx, self);
76 } else {
77 property.set_for_element(cx, self, new_value.into());
78 }
79 }
80
81 pub(crate) fn clear_the_value(&self, cx: &mut JSContext, command: &CommandName) {
83 let node = self.upcast::<Node>();
88 let element = self.upcast::<Element>();
89
90 if !node.is_editable() {
92 return;
93 }
94 if element.specified_command_value(command).is_none() {
97 return;
98 }
99 let node_parent = node.GetParentNode().expect("Must always have a parent");
101 if element.is_simple_modifiable_element() {
102 for child in node.children() {
105 move_preserving_ranges(cx, &child, |cx| {
106 node_parent.InsertBefore(cx, &child, Some(node))
107 });
108 }
109 node.remove_self(cx);
111 return;
113 }
114 match command {
115 CommandName::Strikethrough => {
119 self.remove_value_from_text_decoration(cx, "line-through");
120 },
121 CommandName::Underline => {
124 self.remove_value_from_text_decoration(cx, "underline");
125 },
126 _ => {},
127 }
128 if let Some(property) = command.relevant_css_property() {
131 property.remove_from_element(cx, self);
132 }
133 if element.has_empty_style_attribute() {
137 element.remove_attribute_by_name(cx, &local_name!("style"));
138 }
139 if self.is::<HTMLFontElement>() {
141 match command {
142 CommandName::ForeColor => {
144 element.remove_attribute_by_name(cx, &local_name!("color"));
145 },
146 CommandName::FontName => {
148 element.remove_attribute_by_name(cx, &local_name!("face"));
149 },
150 CommandName::FontSize => {
152 element.remove_attribute_by_name(cx, &local_name!("size"));
153 },
154 _ => {},
155 }
156 }
157 if self.is::<HTMLAnchorElement>() &&
160 matches!(command, CommandName::CreateLink | CommandName::Unlink)
161 {
162 element.remove_attribute_by_name(cx, &local_name!("href"));
163 }
164 if element.specified_command_value(command).is_none() {
167 return;
168 }
169 element.set_the_tag_name(cx, "span");
172 }
173
174 pub(crate) fn handle_focus_state_for_contenteditable(&self, cx: &mut JSContext) {
178 if !self.is_editing_host() {
179 return;
180 }
181 let document = self.owner_document();
182 let Some(selection) = document.GetSelection(cx) else {
183 return;
184 };
185 let range = self
186 .upcast::<Element>()
187 .ensure_contenteditable_selection_range(cx, &document);
188 if selection
194 .active_range(cx)
195 .is_some_and(|active| active == range)
196 {
197 return;
198 }
199 let node = self.upcast::<Node>();
200 let mut selected_node = DomRoot::from_ref(node);
201 let mut previous_eligible_node = DomRoot::from_ref(node);
202 let mut previous_node = DomRoot::from_ref(node);
203 let mut selected_offset = 0;
204 for child in node.traverse_preorder(ShadowIncluding::Yes) {
205 if let Some(text) = child.downcast::<Text>() {
206 if !text.is_whitespace_node() {
211 let is_pre_formatted_text_node = child
214 .GetParentElement()
215 .and_then(|parent| parent.style())
216 .is_some_and(|style| {
217 style.get_inherited_text().white_space_collapse ==
218 WhiteSpaceCollapse::Preserve
219 });
220 if !is_pre_formatted_text_node {
221 selected_offset = text
224 .data()
225 .find(|c: char| !c.is_whitespace())
226 .unwrap_or_default() as u32;
227 }
228 selected_node = child;
229 break;
230 }
231 }
232 if matches!(
235 child.type_id(),
236 NodeTypeId::Element(ElementTypeId::HTMLElement(
237 HTMLElementTypeId::HTMLInputElement,
238 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
239 HTMLElementTypeId::HTMLTextAreaElement,
240 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
241 HTMLElementTypeId::HTMLHRElement,
242 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
243 HTMLElementTypeId::HTMLBRElement,
244 ))
245 ) {
246 selected_node = previous_node;
247 break;
248 }
249 if child
252 .downcast::<HTMLElement>()
253 .is_some_and(|el| el.ContentEditable().str() == "false")
254 {
255 selected_node = previous_eligible_node;
256 break;
257 }
258 if child.is_block_node() {
261 previous_eligible_node = child.clone();
262 }
263 previous_node = child;
264 }
265 let _ = range.SetStart(&selected_node, selected_offset);
266 let _ = range.SetEnd(&selected_node, selected_offset);
267 selection.AddRange(&range);
268 }
269}