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 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        && !DISPLAY_VALUES.into_iter().any(|s| display == s)
152}
153
154pub fn is_table_element(element: &Element) -> bool {
155    let node_name = get_node_name(element.into());
156    ["table", "td", "th"].into_iter().any(|s| node_name == s)
157}
158
159pub fn is_top_layer(element: &Element) -> bool {
160    [":popover-open", ":modal"]
161        .into_iter()
162        .any(|selector| element.matches(selector).unwrap_or(false))
163}
164
165const WILL_CHANGE_VALUES: [&str; 6] = [
166    "transform",
167    "translate",
168    "scale",
169    "rotate",
170    "perspective",
171    "filter",
172];
173const CONTAIN_VALUES: [&str; 4] = ["paint", "layout", "strict", "content"];
174
175pub enum ElementOrCss<'a> {
176    Element(&'a Element),
177    Css(CssStyleDeclaration),
178}
179
180impl<'a> From<&'a Element> for ElementOrCss<'a> {
181    fn from(value: &'a Element) -> Self {
182        ElementOrCss::Element(value)
183    }
184}
185
186impl<'a> From<&'a HtmlElement> for ElementOrCss<'a> {
187    fn from(value: &'a HtmlElement) -> Self {
188        ElementOrCss::Element(value)
189    }
190}
191
192impl From<CssStyleDeclaration> for ElementOrCss<'_> {
193    fn from(value: CssStyleDeclaration) -> Self {
194        ElementOrCss::Css(value)
195    }
196}
197
198pub fn is_containing_block(element: ElementOrCss) -> bool {
199    let webkit = is_web_kit();
200    let css = match element {
201        ElementOrCss::Element(element) => get_computed_style(element),
202        ElementOrCss::Css(css) => css,
203    };
204
205    // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
206    // https://drafts.csswg.org/css-transforms-2/#individual-transforms
207    ["transform", "translate", "scale", "rotate", "perspective"]
208        .into_iter()
209        .any(|property| {
210            css.get_property_value(property)
211                .map(|value| value != "none")
212                .unwrap_or(false)
213        })
214        || css
215            .get_property_value("container-type")
216            .map(|value| value != "normal")
217            .unwrap_or(false)
218        || (!webkit
219            && css
220                .get_property_value("backdrop-filter")
221                .map(|value| value != "none")
222                .unwrap_or(false))
223        || (!webkit
224            && css
225                .get_property_value("filter")
226                .map(|value| value != "none")
227                .unwrap_or(false))
228        || css
229            .get_property_value("will-change")
230            .map(|value| WILL_CHANGE_VALUES.into_iter().any(|v| v == value))
231            .unwrap_or(false)
232        || css
233            .get_property_value("contain")
234            .map(|value| CONTAIN_VALUES.into_iter().any(|v| v == value))
235            .unwrap_or(false)
236}
237
238pub fn get_containing_block(element: &Element) -> Option<HtmlElement> {
239    let mut current_node = get_parent_node(element);
240
241    while !is_last_traversable_node(&current_node) {
242        match current_node.dyn_into::<HtmlElement>() {
243            Ok(element) => {
244                if is_containing_block((&element).into()) {
245                    return Some(element);
246                } else if is_top_layer(&element) {
247                    return None;
248                }
249
250                current_node = get_parent_node(&element);
251            }
252            _ => {
253                break;
254            }
255        }
256    }
257
258    None
259}
260
261pub fn is_web_kit() -> bool {
262    css::supports_with_value("-webkit-backdrop-filter", "none").unwrap_or(false)
263}
264
265pub fn is_last_traversable_node(node: &Node) -> bool {
266    let node_name = get_node_name(node.into());
267    ["html", "body", "#document"]
268        .into_iter()
269        .any(|s| node_name == s)
270}
271
272pub fn get_computed_style(element: &Element) -> CssStyleDeclaration {
273    get_window(Some(element))
274        .get_computed_style(element)
275        .expect("Valid element.")
276        .expect("Element should have computed style.")
277}
278
279#[derive(Clone, Debug)]
280pub struct NodeScroll {
281    pub scroll_left: f64,
282    pub scroll_top: f64,
283}
284
285impl NodeScroll {
286    pub fn new(value: f64) -> Self {
287        Self {
288            scroll_left: value,
289            scroll_top: value,
290        }
291    }
292}
293
294pub fn get_node_scroll(element_or_window: DomElementOrWindow) -> NodeScroll {
295    match element_or_window {
296        DomElementOrWindow::Element(element) => NodeScroll {
297            scroll_left: element.scroll_left() as f64,
298            scroll_top: element.scroll_top() as f64,
299        },
300        DomElementOrWindow::Window(window) => NodeScroll {
301            scroll_left: window.scroll_x().expect("Window should have scroll x."),
302            scroll_top: window.scroll_y().expect("Window should have scroll y."),
303        },
304    }
305}
306
307pub fn get_parent_node(node: &Node) -> Node {
308    if get_node_name(node.into()) == "html" {
309        return node.clone();
310    }
311
312    let element = node.dyn_ref::<Element>();
313
314    let result: Node;
315    match element.and_then(|element| element.assigned_slot()) {
316        Some(slot) => {
317            // Step into the shadow DOM of the parent of a slotted node.
318            result = slot.into();
319        }
320        _ => {
321            match node.parent_node() {
322                Some(parent_node) => {
323                    // DOM Element detected.
324                    result = parent_node;
325                }
326                _ => {
327                    if let Some(shadow_root) = node.dyn_ref::<ShadowRoot>() {
328                        // ShadowRoot detected.
329                        result = shadow_root.host().into();
330                    } else {
331                        // Fallback.
332                        result = get_document_element(Some(node.into())).into();
333                    }
334                }
335            }
336        }
337    }
338
339    match node.dyn_ref::<ShadowRoot>() {
340        Some(shadow_root) => shadow_root.host().into(),
341        None => result,
342    }
343}
344
345pub fn get_nearest_overflow_ancestor(node: &Node) -> HtmlElement {
346    let parent_node = get_parent_node(node);
347
348    if is_last_traversable_node(&parent_node) {
349        node.owner_document()
350            .as_ref()
351            .or(node.dyn_ref::<Document>())
352            .expect("Node should be document or have owner document.")
353            .body()
354            .expect("Document should have body.")
355    } else if is_html_element(&parent_node)
356        && is_overflow_element(parent_node.unchecked_ref::<Element>())
357    {
358        parent_node.unchecked_into()
359    } else {
360        get_nearest_overflow_ancestor(&parent_node)
361    }
362}
363
364#[derive(Clone, Debug, PartialEq)]
365pub enum OverflowAncestor {
366    Element(Element),
367    Window(Window),
368    // TODO
369    // VisualViewport(VisualViewport)
370}
371
372pub fn get_overflow_ancestors(
373    node: &Node,
374    mut list: Vec<OverflowAncestor>,
375    traverse_iframe: bool,
376) -> Vec<OverflowAncestor> {
377    let scrollable_ancestor = get_nearest_overflow_ancestor(node);
378    let is_body = node
379        .owner_document()
380        .and_then(|document| document.body())
381        .is_some_and(|body| scrollable_ancestor == body);
382    let window = get_window(Some(&scrollable_ancestor));
383
384    if is_body {
385        let frame_element = get_frame_element(&window);
386
387        list.push(OverflowAncestor::Window(window));
388        // TODO: visual viewport
389
390        if is_overflow_element(&scrollable_ancestor) {
391            list.push(OverflowAncestor::Element(scrollable_ancestor.into()));
392        }
393
394        if let Some(frame_element) = frame_element {
395            if traverse_iframe {
396                list.append(&mut get_overflow_ancestors(&frame_element, vec![], true))
397            }
398        }
399
400        list
401    } else {
402        let mut other_list = get_overflow_ancestors(&scrollable_ancestor, vec![], traverse_iframe);
403
404        list.push(OverflowAncestor::Element(scrollable_ancestor.into()));
405        list.append(&mut other_list);
406
407        list
408    }
409}
410
411pub fn get_frame_element(window: &Window) -> Option<Element> {
412    window
413        .parent()
414        .ok()
415        .flatten()
416        .and_then(|_| {
417            window
418                .frame_element()
419                .expect("Window should have frame element option.")
420        })
421        .and_then(|frame_element| {
422            Object::get_prototype_of(&frame_element)
423                .is_truthy()
424                .then_some(frame_element)
425        })
426}