Skip to main content

workflow_dom/
inject.rs

1//!
2//! DOM injection utilities, allowing injection of `script` and `style`
3//! elements from Rust buffers using [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
4//! objects.
5//!
6//! This can be used in conjunction with [`include_bytes`] macro to embed
7//! JavaScript scripts, modules and CSS stylesheets directly within WASM
8//! binary.
9//!
10
11use crate::result::*;
12use crate::utils::*;
13use js_sys::{Array, Function, Uint8Array};
14use web_sys::Element;
15use web_sys::{Blob, Url};
16use workflow_core::channel::oneshot;
17use workflow_wasm::callback::*;
18
19/// Callback type invoked on element load events, receiving the
20/// associated [`web_sys::CustomEvent`].
21pub type CustomEventCallback = Callback<CallbackClosureWithoutResult<web_sys::CustomEvent>>;
22
23/// The Content enum specifies the type of the content being injected
24/// Each enum variant contains optional content `id` and `&[u8]` data.
25pub enum Content<'content> {
26    /// This data slice represents a JavaScript script
27    Script(Option<&'content str>, &'content [u8]),
28    /// This data slice represents a JavaScript module
29    Module(Option<&'content str>, &'content [u8]),
30    /// This data slice represents a CSS stylesheet
31    Style(Option<&'content str>, &'content [u8]),
32}
33
34/// Inject CSS stylesheed directly into DOM as a
35/// [`<style>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style)
36/// element using [`Element::set_inner_html`]
37pub fn inject_css(id: Option<&str>, css: &str) -> Result<()> {
38    let doc = document();
39    let head = doc
40        .get_elements_by_tag_name("head")
41        .item(0)
42        .ok_or("Unable to locate head element")?;
43
44    let style_el = if let Some(id) = id {
45        match doc.get_element_by_id(id) {
46            Some(old_el) => old_el,
47            _ => {
48                let style_el = doc.create_element("style")?;
49                style_el.set_attribute("id", id)?;
50                head.append_child(&style_el)?;
51                style_el
52            }
53        }
54    } else {
55        let style_el = doc.create_element("style")?;
56        head.append_child(&style_el)?;
57        style_el
58    };
59
60    style_el.set_inner_html(css);
61    Ok(())
62}
63
64/// Inject a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
65/// into DOM. The `content` argument carries the data buffer and
66/// the content type represented by the [`Content`] struct.
67pub fn inject_blob_nowait(content: Content) -> Result<()> {
68    inject_blob_with_callback::<CustomEventCallback>(content, None)
69}
70
71/// Inject a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
72/// into DOM. The `content` argument carries the data buffer and
73/// the content type represented by the [`Content`] struct. This function
74/// returns a future that completes upon injection completion.
75pub async fn inject_blob(content: Content<'_>) -> Result<()> {
76    let (sender, receiver) = oneshot();
77    let callback = callback!(move |event: web_sys::CustomEvent| {
78        sender
79            .try_send(event)
80            .expect("inject_blob_with_callback(): unable to send load notification");
81    });
82    inject_blob_with_callback(content, Some(&callback))?;
83    let _notification = receiver.recv().await?;
84    Ok(())
85}
86
87/// Inject script as a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) buffer
88/// into DOM. Executes an optional `load` callback when the loading is complete. The load callback
89/// receives [`web_sys::CustomEvent`] struct indicating the load result.
90// pub fn inject_script(root:Element, id : Option<&str>, content:&[u8], content_type:&str, callback : Option<&CustomEventCallback>) -> Result<()> {
91pub fn inject_script<C>(
92    root: Element,
93    id: Option<&str>,
94    content: &[u8],
95    content_type: &str,
96    callback: Option<&C>,
97) -> Result<()>
98where
99    C: AsRef<Function>,
100{
101    let doc = document();
102    let string = String::from_utf8_lossy(content);
103    let regex = regex::Regex::new(r"//# sourceMappingURL.*$").unwrap();
104    let content = regex.replace(&string, "");
105
106    let args = Array::new_with_length(1);
107    args.set(0, unsafe { Uint8Array::view(content.as_bytes()).into() });
108    let options = web_sys::BlobPropertyBag::new();
109    options.set_type("application/javascript");
110    let blob = Blob::new_with_u8_array_sequence_and_options(&args, &options)?;
111    let url = Url::create_object_url_with_blob(&blob)?;
112
113    let script = doc.create_element("script")?;
114    if let Some(callback) = callback {
115        script.add_event_listener_with_callback("load", callback.as_ref())?;
116    }
117    if let Some(id) = id {
118        script.set_attribute("id", id)?;
119    }
120    script.set_attribute("type", content_type)?;
121    script.set_attribute("src", &url)?;
122    root.append_child(&script)?;
123
124    Ok(())
125}
126
127/// Inject a CSS stylesheet contained in a data buffer as a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
128/// into DOM via a `<link rel="stylesheet">` element appended to `root`.
129/// Executes an optional `load` callback when loading is complete.
130pub fn inject_stylesheet<C>(
131    root: Element,
132    id: Option<&str>,
133    content: &[u8],
134    callback: Option<&C>,
135) -> Result<()>
136where
137    C: AsRef<Function>,
138{
139    let args = Array::new_with_length(1);
140    args.set(0, unsafe { Uint8Array::view(content).into() });
141    let blob = Blob::new_with_u8_array_sequence(&args)?;
142    let url = Url::create_object_url_with_blob(&blob)?;
143
144    let style = document().create_element("link")?;
145    if let Some(callback) = callback {
146        style.add_event_listener_with_callback("load", callback.as_ref())?;
147        // closure.forget();
148    }
149    if let Some(id) = id {
150        style.set_attribute("id", id)?;
151    }
152    style.set_attribute("type", "text/css")?;
153    style.set_attribute("rel", "stylesheet")?;
154    style.set_attribute("href", &url)?;
155    root.append_child(&style)?;
156    Ok(())
157}
158
159/// Inject data buffer contained in the [`Content`] struct as a [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob)
160/// into DOM. Executes an optional `load` callback when the loading is complete. The load callback
161/// receives [`web_sys::CustomEvent`] struct indicating the load result.
162pub fn inject_blob_with_callback<C>(content: Content, callback: Option<&C>) -> Result<()>
163// pub fn inject_blob_with_callback(content : Content, callback : Option<&CustomEventCallback>) -> Result<()>
164where
165    C: AsRef<Function>,
166{
167    let doc = document();
168    let root = {
169        let collection = doc.get_elements_by_tag_name("head");
170        if collection.length() > 0 {
171            collection.item(0).unwrap()
172        } else {
173            doc.get_elements_by_tag_name("body").item(0).unwrap()
174        }
175    };
176
177    match content {
178        Content::Script(id, content) => {
179            inject_script(root, id, content, "text/javascript", callback)?;
180        }
181        Content::Module(id, content) => {
182            inject_script(root, id, content, "module", callback)?;
183        }
184        Content::Style(id, content) => {
185            inject_stylesheet(root, id, content, callback)?;
186        }
187    }
188
189    Ok(())
190}