use wasm_bindgen_futures::spawn_local;
use web_sys::*;
use yew::prelude::*;
use super::status_indicator::StatusIndicator;
use super::style::LocalStyle;
use crate::components::containers::select::*;
use crate::components::copy_dropdown::CopyDropDownMenu;
use crate::components::export_dropdown::ExportDropDownMenu;
use crate::components::portal::PortalModal;
use crate::components::status_bar_counter::StatusBarRowsCounter;
use crate::custom_events::CustomEvents;
use crate::js::*;
use crate::presentation::Presentation;
use crate::renderer::*;
use crate::session::*;
use crate::tasks::*;
use crate::utils::*;
use crate::*;
#[derive(Clone, Properties)]
pub struct StatusBarProps {
pub id: String,
pub on_reset: Callback<bool>,
#[prop_or_default]
pub on_settings: Option<Callback<()>>,
pub has_table: Option<TableLoadState>,
pub is_errored: bool,
pub stats: Option<ViewStats>,
pub update_count: u32,
pub error: Option<TableErrorState>,
pub title: Option<String>,
pub is_settings_open: bool,
pub selected_theme: Option<String>,
pub available_themes: PtrEqRc<Vec<String>>,
pub is_workspace: bool,
pub custom_events: CustomEvents,
pub session: Session,
pub renderer: Renderer,
pub presentation: Presentation,
}
impl PartialEq for StatusBarProps {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.has_table == other.has_table
&& self.is_errored == other.is_errored
&& self.stats == other.stats
&& self.update_count == other.update_count
&& self.error == other.error
&& self.title == other.title
&& self.is_settings_open == other.is_settings_open
&& self.selected_theme == other.selected_theme
&& self.available_themes == other.available_themes
&& self.is_workspace == other.is_workspace
}
}
impl HasCustomEvents for StatusBarProps {
fn custom_events(&self) -> &CustomEvents {
&self.custom_events
}
}
impl HasPresentation for StatusBarProps {
fn presentation(&self) -> &Presentation {
&self.presentation
}
}
impl HasRenderer for StatusBarProps {
fn renderer(&self) -> &Renderer {
&self.renderer
}
}
impl HasSession for StatusBarProps {
fn session(&self) -> &Session {
&self.session
}
}
impl StateProvider for StatusBarProps {
type State = StatusBarProps;
fn clone_state(&self) -> Self::State {
self.clone()
}
}
pub enum StatusBarMsg {
Reset(MouseEvent),
Export,
Copy,
CloseExport,
CloseCopy,
Noop,
Eject,
SetTheme(String),
ResetTheme,
PointerEvent(web_sys::PointerEvent),
TitleInputEvent,
TitleChangeEvent,
}
pub struct StatusBar {
copy_ref: NodeRef,
export_ref: NodeRef,
input_ref: NodeRef,
statusbar_ref: NodeRef,
title: Option<String>,
copy_target: Option<HtmlElement>,
export_target: Option<HtmlElement>,
}
impl Component for StatusBar {
type Message = StatusBarMsg;
type Properties = StatusBarProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
copy_ref: NodeRef::default(),
export_ref: NodeRef::default(),
input_ref: NodeRef::default(),
statusbar_ref: NodeRef::default(),
title: ctx.props().title.clone(),
copy_target: None,
export_target: None,
}
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
if ctx.props().title != old_props.title
|| ctx.props().is_settings_open != old_props.is_settings_open
{
self.title = ctx.props().title.clone();
}
true
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
maybe_log_or_default!(Ok(match msg {
StatusBarMsg::Reset(event) => {
let all = event.shift_key();
ctx.props().on_reset.emit(all);
false
},
StatusBarMsg::ResetTheme => {
let presentation = ctx.props().presentation.clone();
let session = ctx.props().session.clone();
let renderer = ctx.props().renderer.clone();
ApiFuture::spawn(async move {
presentation.reset_theme().await?;
let view = session.get_view().into_apierror()?;
renderer.restyle_all(&view).await
});
true
},
StatusBarMsg::SetTheme(theme_name) => {
let presentation = ctx.props().presentation.clone();
let session = ctx.props().session.clone();
let renderer = ctx.props().renderer.clone();
ApiFuture::spawn(async move {
presentation.set_theme_name(Some(&theme_name)).await?;
let view = session.get_view().into_apierror()?;
renderer.restyle_all(&view).await
});
false
},
StatusBarMsg::Export => {
self.export_target = self.export_ref.cast::<HtmlElement>();
true
},
StatusBarMsg::Copy => {
self.copy_target = self.copy_ref.cast::<HtmlElement>();
true
},
StatusBarMsg::CloseExport => {
self.export_target = None;
true
},
StatusBarMsg::CloseCopy => {
self.copy_target = None;
true
},
StatusBarMsg::Eject => {
ctx.props().presentation().on_eject.emit(());
false
},
StatusBarMsg::Noop => {
self.title = ctx.props().title.clone();
true
},
StatusBarMsg::TitleInputEvent => {
let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
let title = elem.value();
let title = if title.trim().is_empty() {
None
} else {
Some(title)
};
self.title = title;
true
},
StatusBarMsg::TitleChangeEvent => {
let elem = self.input_ref.cast::<HtmlInputElement>().into_apierror()?;
let title = elem.value();
let title = if title.trim().is_empty() {
None
} else {
Some(title)
};
ctx.props().session().set_title(title);
false
},
StatusBarMsg::PointerEvent(event) => {
if event.target().map(JsValue::from)
== self.statusbar_ref.cast::<HtmlElement>().map(JsValue::from)
{
ctx.props()
.custom_events()
.dispatch_event(format!("statusbar-{}", event.type_()).as_str(), &event)?;
}
false
},
}))
}
fn view(&self, ctx: &Context<Self>) -> Html {
let Self::Properties {
custom_events,
presentation,
renderer,
session,
..
} = ctx.props();
let has_table = ctx.props().has_table.clone();
let is_errored = ctx.props().is_errored;
let is_settings_open = ctx.props().is_settings_open;
let title = &ctx.props().title;
let mut is_updating_class_name = classes!();
if title.is_some() {
is_updating_class_name.push("titled");
};
if !is_settings_open {
is_updating_class_name.push(["settings-closed", "titled"]);
};
if !matches!(has_table, Some(TableLoadState::Loaded)) {
is_updating_class_name.push("updating");
}
let onblur = ctx.link().callback(|_| StatusBarMsg::Noop);
let onclose = ctx.link().callback(|_| StatusBarMsg::Eject);
let onpointerdown = ctx.link().callback(StatusBarMsg::PointerEvent);
let onexport = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export);
let oncopy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy);
let onreset = ctx.link().callback(StatusBarMsg::Reset);
let onchange = ctx
.link()
.callback(|_: Event| StatusBarMsg::TitleChangeEvent);
let oninput = ctx
.link()
.callback(|_: InputEvent| StatusBarMsg::TitleInputEvent);
let is_menu = matches!(has_table, Some(TableLoadState::Loaded))
&& ctx.props().on_settings.as_ref().is_none();
let is_title = is_menu
|| ctx.props().is_workspace
|| title.is_some()
|| is_errored
|| presentation.is_active(&self.input_ref.cast::<Element>());
let is_settings = title.is_some()
|| ctx.props().is_workspace
|| !matches!(has_table, Some(TableLoadState::Loaded))
|| is_errored
|| is_settings_open
|| presentation.is_active(&self.input_ref.cast::<Element>());
let on_copy_select = {
let props = ctx.props().clone();
let link = ctx.link().clone();
Callback::from(move |x: ExportFile| {
let props = props.clone();
let link = link.clone();
spawn_local(async move {
let mime = x.method.mimetype(x.is_chart);
let task = props.export_method_to_blob(x.method);
let result = copy_to_clipboard(task, mime).await;
crate::maybe_log!({
result?;
link.send_message(StatusBarMsg::CloseCopy);
})
})
})
};
let on_export_select = {
let props = ctx.props().clone();
let link = ctx.link().clone();
Callback::from(move |x: ExportFile| {
if !x.name.is_empty() {
clone!(props, link);
spawn_local(async move {
let val = props.export_method_to_blob(x.method).await.unwrap();
let is_chart = props.renderer().is_chart();
download(&x.as_filename(is_chart), &val).unwrap();
link.send_message(StatusBarMsg::CloseExport);
})
}
})
};
let on_close_copy = ctx.link().callback(|_| StatusBarMsg::CloseCopy);
let on_close_export = ctx.link().callback(|_| StatusBarMsg::CloseExport);
if is_settings {
html! {
<>
<LocalStyle href={css!("status-bar")} />
<div
ref={&self.statusbar_ref}
id={ctx.props().id.clone()}
class={is_updating_class_name}
{onpointerdown}
>
<StatusIndicator
{custom_events}
{renderer}
{session}
update_count={ctx.props().update_count}
error={ctx.props().error.clone()}
has_table={ctx.props().has_table.clone()}
stats={ctx.props().stats.clone()}
/>
if is_title {
<label
class="input-sizer"
data-value={self.title.clone().unwrap_or_default()}
>
<input
ref={&self.input_ref}
placeholder=""
value={self.title.clone().unwrap_or_default()}
size="10"
{onblur}
{onchange}
{oninput}
/>
<span id="status-bar-placeholder" />
</label>
}
if is_title {
<StatusBarRowsCounter stats={ctx.props().stats.clone()} />
}
<div id="spacer" />
if is_menu {
<div id="menu-bar" class="section">
<ThemeSelector
theme={ctx.props().selected_theme.clone()}
themes={ctx.props().available_themes.clone()}
on_change={ctx.link().callback(StatusBarMsg::SetTheme)}
on_reset={ctx.link().callback(|_| StatusBarMsg::ResetTheme)}
/>
<div id="plugin-settings"><slot name="statusbar-extra" /></div>
<span class="hover-target">
<span id="reset" class="button" onmousedown={&onreset}>
<span class="icon" />
<span class="icon-label" />
</span>
</span>
<span
ref={&self.export_ref}
class="hover-target"
onmousedown={onexport}
>
<span id="export" class="button">
<span class="icon" />
<span class="icon-label" />
</span>
</span>
<span
ref={&self.copy_ref}
class="hover-target"
onmousedown={oncopy}
>
<span id="copy" class="button">
<span class="icon" />
<span class="icon-label" />
</span>
</span>
</div>
}
if let Some(x) = ctx.props().on_settings.as_ref() {
<div
id="settings_button"
class="noselect"
onmousedown={x.reform(|_| ())}
>
<span class="icon" />
</div>
<div id="close_button" class="noselect" onmousedown={onclose}>
<span class="icon" />
</div>
}
</div>
<PortalModal
tag_name="perspective-copy-menu"
target={self.copy_target.clone()}
own_focus=true
on_close={on_close_copy}
theme={ctx.props().selected_theme.clone().unwrap_or_default()}
>
<CopyDropDownMenu renderer={renderer.clone()} callback={on_copy_select} />
</PortalModal>
<PortalModal
tag_name="perspective-export-menu"
target={self.export_target.clone()}
own_focus=true
on_close={on_close_export}
theme={ctx.props().selected_theme.clone().unwrap_or_default()}
>
<ExportDropDownMenu
renderer={renderer.clone()}
session={session.clone()}
callback={on_export_select}
/>
</PortalModal>
</>
}
} else if let Some(x) = ctx.props().on_settings.as_ref() {
let class = classes!(is_updating_class_name, "floating");
html! {
<div id={ctx.props().id.clone()} {class}>
<div id="settings_button" class="noselect" onmousedown={x.reform(|_| ())}>
<span class="icon" />
</div>
<div id="close_button" class="noselect" onmousedown={&onclose} />
</div>
}
} else {
html! {}
}
}
}
#[derive(Properties, PartialEq)]
struct ThemeSelectorProps {
pub theme: Option<String>,
pub themes: PtrEqRc<Vec<String>>,
pub on_reset: Callback<()>,
pub on_change: Callback<String>,
}
#[function_component]
fn ThemeSelector(props: &ThemeSelectorProps) -> Html {
let is_first = props
.theme
.as_ref()
.and_then(|x| props.themes.first().map(|y| y == x))
.unwrap_or_default();
let values = use_memo(props.themes.clone(), |themes| {
themes
.iter()
.cloned()
.map(SelectItem::Option)
.collect::<Vec<_>>()
});
match &props.theme {
None => html! {},
Some(selected) => {
html! {
if values.len() > 1 {
<span class="hover-target">
<div
id="theme_icon"
class={if is_first {""} else {"modified"}}
tabindex="0"
onclick={props.on_reset.reform(|_| ())}
/>
<span id="theme" class="button">
<span class="icon" />
<Select<String>
id="theme_selector"
class="invert"
{values}
selected={selected.to_owned()}
on_select={props.on_change.clone()}
/>
</span>
</span>
}
}
},
}
}