vertigo-forms 0.1.3

Building block for forms in vertigo
Documentation
use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD as BASE_64};
use std::rc::Rc;
use vertigo::{
    AttrGroup, Computed, Css, DropFileEvent, DropFileItem, Value, bind, component, computed_tuple,
    css, dom,
};

/// Box that allows to accept image files on it, connected to `Value<Option<DropFileItem>>`.
#[component]
pub fn DropImageFile(
    original_link: Computed<Option<Rc<String>>>,
    item: Value<Option<DropFileItem>>,
    params: DropImageFileParams,
    /// Any additional attributes for the dropzone
    zone: AttrGroup,
) {
    let base64_data = item.to_computed().map(|item| match item {
        Some(item) => image_as_uri(&item),
        None => "".to_string(),
    });

    let view_deps = computed_tuple!(
        a => original_link,
        b => item,
        c => base64_data
    );
    let item_clone = item.clone();
    let params = params.clone();
    let callback = params.callback.clone();
    let image_view = view_deps.render_value(move |(original, item, base64_date)| match item {
        Some(item) => {
            let message = format_line(&item);
            let image_css = css! {"
                display: flex;
                flex-flow: column;
            "};
            let restore = bind!(item_clone, callback, |_| {
                if let Some(callback) = &callback {
                    callback(None);
                } else {
                    item_clone.set(None);
                }
            });
            let restore_text = if original.is_some() {
                &params.revert_label
            } else {
                &params.cancel_label
            };
            dom! {
                <div css={image_css}>
                    <button on_click={restore}>{restore_text}</button>
                    <img css={&params.img_css} src={base64_date} />
                    { message }
                </div>
            }
        }
        None => match original {
            Some(original) => {
                dom! { <div><img css={&params.img_css} src={original} /></div> }
            }
            None => dom! { <div>{&params.no_image_text}</div> },
        },
    });

    let item = item.clone();
    let callback = params.callback.clone();
    let on_dropfile = move |event: DropFileEvent| {
        for file in event.items.into_iter() {
            if let Some(callback) = callback.as_deref() {
                callback(Some(file));
            } else {
                item.set(Some(file));
            }
        }
    };

    let dropzone_css = &params.dropzone_css + &params.dropzone_add_css;

    dom! {
        <div css={dropzone_css} on_dropfile={on_dropfile} {..zone}>
            { image_view }
        </div>
    }
}

#[derive(Clone)]
pub struct DropImageFileParams {
    /// Custom callback when new image dropped, leave empty to automatically set/unset `item`
    pub callback: Option<Rc<dyn Fn(Option<DropFileItem>)>>,
    pub revert_label: String,
    pub cancel_label: String,
    pub no_image_text: String,
    pub dropzone_css: Css,
    pub dropzone_add_css: Css,
    pub img_css: Css,
}

impl Default for DropImageFileParams {
    fn default() -> Self {
        Self {
            callback: None,
            revert_label: "Revert".to_string(),
            cancel_label: "Cancel".to_string(),
            no_image_text: "No image".to_string(),
            dropzone_css: css! {"
                width: 400px;
                height: 400px;

                display: flex;
                align-items: center;
                justify-content: center;

                padding: 10px;
            "},
            dropzone_add_css: css!(""),
            img_css: css!(""),
        }
    }
}

fn format_line(item: &DropFileItem) -> String {
    let file_name = &item.name;
    let size = item.data.len();
    format!("{file_name} ({size})")
}

pub fn name_to_mime(name: &str) -> &'static str {
    use std::{ffi::OsStr, path::Path};

    let extension = Path::new(name)
        .extension()
        .and_then(OsStr::to_str)
        .unwrap_or_default();

    match extension {
        "jpg" | "jpeg" | "jpe" => "image/jpeg",
        "png" => "image/png",
        "svg" => "image/svg+xml",
        "gif" => "image/gif",
        "bmp" => "image/bmp",
        "ico" => "image/ico",
        _ => "application/octet-stream",
    }
}

pub fn image_as_uri(item: &DropFileItem) -> String {
    let mime = name_to_mime(&item.name);
    let data = BASE_64.encode(&*item.data);
    format!("data:{mime};base64,{data}")
}