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