1use web_sys::{
4 CssStyleDeclaration, Document, Element, HtmlElement, Node, ShadowRoot, Window, css,
5 js_sys::Object, wasm_bindgen::JsCast, window,
6};
7
8use crate::ElementOrWindow;
9
10#[derive(Clone, Debug)]
11pub enum DomNodeOrWindow<'a> {
12 Node(&'a Node),
13 Window(&'a Window),
14}
15
16impl<'a> From<&'a Node> for DomNodeOrWindow<'a> {
17 fn from(value: &'a Node) -> Self {
18 DomNodeOrWindow::Node(value)
19 }
20}
21
22impl<'a> From<&'a Element> for DomNodeOrWindow<'a> {
23 fn from(value: &'a Element) -> Self {
24 DomNodeOrWindow::Node(value)
25 }
26}
27
28impl<'a> From<&'a Window> for DomNodeOrWindow<'a> {
29 fn from(value: &'a Window) -> Self {
30 DomNodeOrWindow::Window(value)
31 }
32}
33
34impl<'a> From<ElementOrWindow<'a, Element, Window>> for DomNodeOrWindow<'a> {
35 fn from(value: ElementOrWindow<'a, Element, Window>) -> Self {
36 match value {
37 ElementOrWindow::Element(element) => DomNodeOrWindow::Node(element),
38 ElementOrWindow::Window(window) => DomNodeOrWindow::Window(window),
39 }
40 }
41}
42
43impl<'a> From<&ElementOrWindow<'a, Element, Window>> for DomNodeOrWindow<'a> {
44 fn from(value: &ElementOrWindow<'a, Element, Window>) -> Self {
45 match value {
46 ElementOrWindow::Element(element) => DomNodeOrWindow::Node(element),
47 ElementOrWindow::Window(window) => DomNodeOrWindow::Window(window),
48 }
49 }
50}
51
52impl<'a> From<&'a DomElementOrWindow<'a>> for DomNodeOrWindow<'a> {
53 fn from(value: &'a DomElementOrWindow) -> Self {
54 match value {
55 DomElementOrWindow::Element(element) => DomNodeOrWindow::Node(element),
56 DomElementOrWindow::Window(window) => DomNodeOrWindow::Window(window),
57 }
58 }
59}
60
61#[derive(Clone, Debug)]
62pub enum DomElementOrWindow<'a> {
63 Element(&'a Element),
64 Window(&'a Window),
65}
66
67impl<'a> From<&'a Element> for DomElementOrWindow<'a> {
68 fn from(value: &'a Element) -> Self {
69 DomElementOrWindow::Element(value)
70 }
71}
72
73impl<'a> From<&'a Window> for DomElementOrWindow<'a> {
74 fn from(value: &'a Window) -> Self {
75 DomElementOrWindow::Window(value)
76 }
77}
78
79impl<'a> From<ElementOrWindow<'a, Element, Window>> for DomElementOrWindow<'a> {
80 fn from(value: ElementOrWindow<'a, Element, Window>) -> Self {
81 match value {
82 ElementOrWindow::Element(element) => DomElementOrWindow::Element(element),
83 ElementOrWindow::Window(window) => DomElementOrWindow::Window(window),
84 }
85 }
86}
87
88impl<'a> From<&ElementOrWindow<'a, Element, Window>> for DomElementOrWindow<'a> {
89 fn from(value: &ElementOrWindow<'a, Element, Window>) -> Self {
90 match value {
91 ElementOrWindow::Element(element) => DomElementOrWindow::Element(element),
92 ElementOrWindow::Window(window) => DomElementOrWindow::Window(window),
93 }
94 }
95}
96
97pub fn get_node_name(node_or_window: DomNodeOrWindow) -> String {
98 match node_or_window {
99 DomNodeOrWindow::Node(node) => node.node_name().to_lowercase(),
100 DomNodeOrWindow::Window(_) => "#document".into(),
101 }
102}
103
104pub fn get_window(node: Option<&Node>) -> Window {
105 match node {
106 Some(node) => match node.owner_document() {
107 Some(document) => document.default_view(),
108 None => window(),
109 },
110 None => window(),
111 }
112 .expect("Window should exist.")
113}
114
115pub fn get_document_element(node_or_window: Option<DomNodeOrWindow>) -> Element {
116 let document = match node_or_window {
117 Some(DomNodeOrWindow::Node(node)) => node.owner_document(),
118 Some(DomNodeOrWindow::Window(window)) => window.document(),
119 None => get_window(None).document(),
120 }
121 .expect("Node or window should have document.");
122
123 document
124 .document_element()
125 .expect("Document should have document element.")
126}
127
128pub fn is_element(node: &Node) -> bool {
129 node.is_instance_of::<Element>()
130}
131
132pub fn is_html_element(node: &Node) -> bool {
133 node.is_instance_of::<HtmlElement>()
134}
135
136const OVERFLOW_VALUES: [&str; 5] = ["auto", "scroll", "overlay", "hidden", "clip"];
137const INVALID_OVERFLOW_DISPLAY_VALUES: [&str; 2] = ["inline", "contents"];
138
139pub fn is_overflow_element(element: &Element) -> bool {
140 let style = get_computed_style(element);
141 let overflow = style.get_property_value("overflow").unwrap_or_default();
142 let overflow_x = style.get_property_value("overflow-x").unwrap_or_default();
143 let overflow_y = style.get_property_value("overflow-y").unwrap_or_default();
144 let display = style.get_property_value("display").unwrap_or_default();
145
146 let overflow_combined = format!("{overflow}{overflow_x}{overflow_y}");
147
148 OVERFLOW_VALUES
149 .into_iter()
150 .any(|s| overflow_combined.contains(s))
151 && !INVALID_OVERFLOW_DISPLAY_VALUES
152 .into_iter()
153 .any(|s| display == s)
154}
155
156const TABLE_ELEMENTS: [&str; 3] = ["table", "td", "th"];
157
158pub fn is_table_element(element: &Element) -> bool {
159 let node_name = get_node_name(element.into());
160 TABLE_ELEMENTS.into_iter().any(|s| node_name == s)
161}
162
163const TOP_LAYER_SELECTORS: [&str; 2] = [":popover-open", ":modal"];
164
165pub fn is_top_layer(element: &Element) -> bool {
166 TOP_LAYER_SELECTORS
167 .into_iter()
168 .any(|selector| element.matches(selector).unwrap_or(false))
169}
170
171const TRANSFORM_PROPERTIES: [&str; 5] =
172 ["transform", "translate", "scale", "rotate", "perspective"];
173
174const WILL_CHANGE_VALUES: [&str; 6] = [
175 "transform",
176 "translate",
177 "scale",
178 "rotate",
179 "perspective",
180 "filter",
181];
182
183const CONTAIN_VALUES: [&str; 4] = ["paint", "layout", "strict", "content"];
184
185pub enum ElementOrCss<'a> {
186 Element(&'a Element),
187 Css(CssStyleDeclaration),
188}
189
190impl<'a> From<&'a Element> for ElementOrCss<'a> {
191 fn from(value: &'a Element) -> Self {
192 ElementOrCss::Element(value)
193 }
194}
195
196impl<'a> From<&'a HtmlElement> for ElementOrCss<'a> {
197 fn from(value: &'a HtmlElement) -> Self {
198 ElementOrCss::Element(value)
199 }
200}
201
202impl From<CssStyleDeclaration> for ElementOrCss<'_> {
203 fn from(value: CssStyleDeclaration) -> Self {
204 ElementOrCss::Css(value)
205 }
206}
207
208pub fn is_containing_block(element: ElementOrCss) -> bool {
209 let webkit = is_web_kit();
210 let css = match element {
211 ElementOrCss::Element(element) => get_computed_style(element),
212 ElementOrCss::Css(css) => css,
213 };
214
215 TRANSFORM_PROPERTIES.into_iter().any(|property| {
218 css.get_property_value(property)
219 .map(|value| value != "none")
220 .unwrap_or(false)
221 }) || css
222 .get_property_value("container-type")
223 .map(|value| value != "normal")
224 .unwrap_or(false)
225 || (!webkit
226 && css
227 .get_property_value("backdrop-filter")
228 .map(|value| value != "none")
229 .unwrap_or(false))
230 || (!webkit
231 && css
232 .get_property_value("filter")
233 .map(|value| value != "none")
234 .unwrap_or(false))
235 || css
236 .get_property_value("will-change")
237 .map(|value| WILL_CHANGE_VALUES.into_iter().any(|v| v == value))
238 .unwrap_or(false)
239 || css
240 .get_property_value("contain")
241 .map(|value| CONTAIN_VALUES.into_iter().any(|v| v == value))
242 .unwrap_or(false)
243}
244
245pub fn get_containing_block(element: &Element) -> Option<HtmlElement> {
246 let mut current_node = get_parent_node(element);
247
248 while !is_last_traversable_node(¤t_node) {
249 match current_node.dyn_into::<HtmlElement>() {
250 Ok(element) => {
251 if is_containing_block((&element).into()) {
252 return Some(element);
253 } else if is_top_layer(&element) {
254 return None;
255 }
256
257 current_node = get_parent_node(&element);
258 }
259 _ => {
260 break;
261 }
262 }
263 }
264
265 None
266}
267
268pub fn is_web_kit() -> bool {
269 css::supports_with_value("-webkit-backdrop-filter", "none").unwrap_or(false)
270}
271
272const LAST_TRAVERSABLE_NODE_NAMES: [&str; 3] = ["html", "body", "#document"];
273
274pub fn is_last_traversable_node(node: &Node) -> bool {
275 let node_name = get_node_name(node.into());
276 LAST_TRAVERSABLE_NODE_NAMES
277 .into_iter()
278 .any(|s| node_name == s)
279}
280
281pub fn get_computed_style(element: &Element) -> CssStyleDeclaration {
282 get_window(Some(element))
283 .get_computed_style(element)
284 .expect("Valid element.")
285 .expect("Element should have computed style.")
286}
287
288#[derive(Clone, Debug)]
289pub struct NodeScroll {
290 pub scroll_left: f64,
291 pub scroll_top: f64,
292}
293
294impl NodeScroll {
295 pub fn new(value: f64) -> Self {
296 Self {
297 scroll_left: value,
298 scroll_top: value,
299 }
300 }
301}
302
303pub fn get_node_scroll(element_or_window: DomElementOrWindow) -> NodeScroll {
304 match element_or_window {
305 DomElementOrWindow::Element(element) => NodeScroll {
306 scroll_left: element.scroll_left() as f64,
307 scroll_top: element.scroll_top() as f64,
308 },
309 DomElementOrWindow::Window(window) => NodeScroll {
310 scroll_left: window.scroll_x().expect("Window should have scroll x."),
311 scroll_top: window.scroll_y().expect("Window should have scroll y."),
312 },
313 }
314}
315
316pub fn get_parent_node(node: &Node) -> Node {
317 if get_node_name(node.into()) == "html" {
318 return node.clone();
319 }
320
321 let element = node.dyn_ref::<Element>();
322
323 let result: Node;
324 match element.and_then(|element| element.assigned_slot()) {
325 Some(slot) => {
326 result = slot.into();
328 }
329 _ => {
330 match node.parent_node() {
331 Some(parent_node) => {
332 result = parent_node;
334 }
335 _ => {
336 if let Some(shadow_root) = node.dyn_ref::<ShadowRoot>() {
337 result = shadow_root.host().into();
339 } else {
340 result = get_document_element(Some(node.into())).into();
342 }
343 }
344 }
345 }
346 }
347
348 match node.dyn_ref::<ShadowRoot>() {
349 Some(shadow_root) => shadow_root.host().into(),
350 None => result,
351 }
352}
353
354pub fn get_nearest_overflow_ancestor(node: &Node) -> HtmlElement {
355 let parent_node = get_parent_node(node);
356
357 if is_last_traversable_node(&parent_node) {
358 node.owner_document()
359 .as_ref()
360 .or(node.dyn_ref::<Document>())
361 .expect("Node should be document or have owner document.")
362 .body()
363 .expect("Document should have body.")
364 } else if is_html_element(&parent_node)
365 && is_overflow_element(parent_node.unchecked_ref::<Element>())
366 {
367 parent_node.unchecked_into()
368 } else {
369 get_nearest_overflow_ancestor(&parent_node)
370 }
371}
372
373#[derive(Clone, Debug, PartialEq)]
374pub enum OverflowAncestor {
375 Element(Element),
376 Window(Window),
377 }
380
381pub fn get_overflow_ancestors(
382 node: &Node,
383 mut list: Vec<OverflowAncestor>,
384 traverse_iframe: bool,
385) -> Vec<OverflowAncestor> {
386 let scrollable_ancestor = get_nearest_overflow_ancestor(node);
387 let is_body = node
388 .owner_document()
389 .and_then(|document| document.body())
390 .is_some_and(|body| scrollable_ancestor == body);
391 let window = get_window(Some(&scrollable_ancestor));
392
393 if is_body {
394 let frame_element = get_frame_element(&window);
395
396 list.push(OverflowAncestor::Window(window));
397 if is_overflow_element(&scrollable_ancestor) {
400 list.push(OverflowAncestor::Element(scrollable_ancestor.into()));
401 }
402
403 if let Some(frame_element) = frame_element
404 && traverse_iframe
405 {
406 list.append(&mut get_overflow_ancestors(&frame_element, vec![], true))
407 }
408
409 list
410 } else {
411 let mut other_list = get_overflow_ancestors(&scrollable_ancestor, vec![], traverse_iframe);
412
413 list.push(OverflowAncestor::Element(scrollable_ancestor.into()));
414 list.append(&mut other_list);
415
416 list
417 }
418}
419
420pub fn get_frame_element(window: &Window) -> Option<Element> {
421 window
422 .parent()
423 .ok()
424 .flatten()
425 .and_then(|_| {
426 window
427 .frame_element()
428 .expect("Window should have frame element option.")
429 })
430 .and_then(|frame_element| {
431 Object::get_prototype_of(&frame_element)
432 .is_truthy()
433 .then_some(frame_element)
434 })
435}