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