use std::collections::HashMap;
use tablo_ui::{
ButtonSize, ButtonVariant, button, dialog, dialog_content, dialog_description, dialog_footer,
dialog_header, dialog_title,
};
use topcoat::{
context::Cx,
runtime::{Event, Signal, expr},
view::*,
};
use crate::{
form::FieldErrors,
schema::{Schema, Source},
topcoat_compat::async_page,
};
pub(crate) struct InputDialog {
pub(crate) id: String,
pub(crate) title: String,
pub(crate) label: String,
pub(crate) confirm: bool,
pub(crate) input: fn() -> Schema,
pub(crate) options: String,
}
pub(crate) enum Posts {
To(String),
Table {
target: TableTarget,
selection: Signal<String>,
},
}
#[derive(Clone)]
pub(crate) struct TableTarget {
pub(crate) action: Signal<String>,
pub(crate) bulk: Signal<bool>,
}
impl InputDialog {
pub(crate) fn render(self, cx: &Cx, posts: Posts) -> BoxView<'_> {
async_page(async move {
let Self {
id,
title,
label,
confirm,
input,
options,
} = self;
let (values, errors) = (HashMap::new(), FieldErrors::new());
let source = Source::form(&values, &errors)
.scoped(format!("{id}-field"))
.options_at(options);
let fields = input().render(cx, source).await?;
let csrf = crate::csrf::current_token(cx);
let modal = id.clone();
let title_id = format!("{id}-title");
let description_id = format!("{id}-description");
let submit = if confirm {
ButtonVariant::Destructive
} else {
ButtonVariant::Primary
};
let table = matches!(posts, Posts::Table { .. });
let described = confirm.then(|| description_id.clone());
let reset = id.clone();
let attrs = attributes! {
cx =>
id=(id)
aria-labelledby=(title_id.clone())
aria-describedby=(described)
@close=$(|_e: Event| {
raw!(
"((form) => { \
form.reset(); \
for (const control of form.elements) { \
if (control.name) { \
control.dispatchEvent(new Event('change', { bubbles: true })); \
} \
} \
})(document.getElementById(String(${reset})).querySelector('form'))",
(),
);
})
};
let (action, selection, count) = match posts {
Posts::To(url) => (attributes! { cx => action=(url) }, None, None),
Posts::Table { target, selection } => {
let TableTarget { action, bulk } = target;
let count = selected_count(cx, selection.clone());
(
attributes! { cx => :action=$(action.get()) },
Some(selection),
Some((bulk, count)),
)
}
};
let description = match &count {
Some((bulk, _)) if !confirm => {
let bulk = bulk.clone();
attributes! { cx => id=(description_id) :hidden=$(!bulk.get()) }
}
_ => attributes! { cx => id=(description_id) },
};
Ok(view! {
cx =>
dialog(
open: false,
attrs: attrs,
<form method="post" class="contents" (action)>
(crate::csrf::field(cx, &csrf))
if confirm {
<input type="hidden" name="confirm" value="1">
}
if let Some(selection) = selection {
<input type="hidden" name="ids" :value=$(selection.get())>
}
dialog_content(
dialog_header(
dialog_title(attrs: attributes! { id=(title_id) }, (title))
if confirm || table {
dialog_description(
attrs: description,
if confirm {
"This action cannot be undone."
}
if let Some((bulk, count)) = count {
<span :hidden=$(!bulk.get())>
" Selected records: "
(count)
</span>
}
)
}
)
<div class="flex flex-col gap-4">(fields)</div>
dialog_footer(
// Not a submit: Enter in a field submits through the form's
// first submit button, which must be the action's.
button(
variant: ButtonVariant::Outline,
size: ButtonSize::Md,
attrs: attributes! {
type="button"
@click=$(|_e: Event| {
raw!(
"document.getElementById(String(${modal})).close()",
(),
);
})
},
"Cancel"
)
button(
variant: submit,
size: ButtonSize::Md,
attrs: attributes! { type="submit" },
(label)
)
)
)
</form>
)
}
.boxed())
})
}
}
/// The size of the table's `selection`, as `,key,` tokens.
pub(crate) fn selected_count(cx: &Cx, selection: Signal<String>) -> BoxView<'_> {
let count = expr!({
let wire = selection.get();
raw!(
"cx.hydrate(String(String(${wire}).split(',').filter(Boolean).length))",
wire.split(',')
.filter(|key| !key.is_empty())
.count()
.to_string()
)
});
view! { cx => (count) }.boxed()
}
/// A trigger's attributes: it opens the dialog `id`, after pointing a table's dialog at `url`
/// when `table` names its target, on the selection when `bulk`.
pub(crate) fn open_dialog(
cx: &Cx,
id: &str,
table: Option<(TableTarget, String, bool)>,
) -> Attributes {
let modal = id.to_string();
let mut attrs = attributes! {
cx =>
type="button"
aria-haspopup="dialog"
aria-controls=(modal.clone())
};
attrs.extend(match table {
None => attributes! {
cx =>
@click=$(|_e: Event| {
raw!("document.getElementById(String(${modal})).showModal()", ());
})
},
Some((TableTarget { action, bulk }, url, on_selection)) => attributes! {
cx =>
@click=$(|_e: Event| {
bulk.set(on_selection);
action.set(url.clone());
raw!("document.getElementById(String(${modal})).showModal()", ());
})
},
});
attrs
}