floating_ui_utils/
dom.rs

1//! Utility functions for the DOM. Requires `dom` feature.
2
3use 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    // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
216    // https://drafts.csswg.org/css-transforms-2/#individual-transforms
217    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(&current_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            // Step into the shadow DOM of the parent of a slotted node.
327            result = slot.into();
328        }
329        _ => {
330            match node.parent_node() {
331                Some(parent_node) => {
332                    // DOM Element detected.
333                    result = parent_node;
334                }
335                _ => {
336                    if let Some(shadow_root) = node.dyn_ref::<ShadowRoot>() {
337                        // ShadowRoot detected.
338                        result = shadow_root.host().into();
339                    } else {
340                        // Fallback.
341                        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    // TODO
378    // VisualViewport(VisualViewport)
379}
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        // TODO: visual viewport
398
399        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}