use std::cell::RefCell;
use std::rc::Rc;
use perspective_client::config::{COMPLETIONS, CompletionItemSuggestion};
use perspective_js::utils::ApiResult;
use web_sys::*;
use yew::html::ImplicitClone;
use yew::prelude::*;
use super::portal::PortalModal;
use crate::utils::*;
static CSS: &str = include_str!(concat!(env!("OUT_DIR"), "/css/function-dropdown.css"));
#[derive(Default)]
struct FunctionDropDownState {
values: Vec<CompletionItemSuggestion>,
selected: usize,
on_select: Option<Callback<CompletionItemSuggestion>>,
target: Option<HtmlElement>,
}
#[derive(Clone, Default)]
pub struct FunctionDropDownElement {
state: Rc<RefCell<FunctionDropDownState>>,
notify: Rc<PubSub<()>>,
}
impl PartialEq for FunctionDropDownElement {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.state, &other.state)
}
}
impl ImplicitClone for FunctionDropDownElement {}
impl FunctionDropDownElement {
pub fn reautocomplete(&self) {
self.notify.emit(());
}
pub fn autocomplete(
&self,
input: String,
target: HtmlElement,
callback: Callback<CompletionItemSuggestion>,
) -> ApiResult<()> {
let values = filter_values(&input);
if values.is_empty() {
self.hide()?;
} else {
let mut s = self.state.borrow_mut();
s.values = values;
s.selected = 0;
s.on_select = Some(callback);
s.target = Some(target);
drop(s);
self.notify.emit(());
}
Ok(())
}
pub fn item_select(&self) {
let state = self.state.borrow();
if let Some(value) = state.values.get(state.selected)
&& let Some(ref cb) = state.on_select
{
cb.emit(*value);
}
}
pub fn item_down(&self) {
let mut state = self.state.borrow_mut();
state.selected += 1;
if state.selected >= state.values.len() {
state.selected = 0;
}
drop(state);
self.notify.emit(());
}
pub fn item_up(&self) {
let mut state = self.state.borrow_mut();
if state.selected < 1 {
state.selected = state.values.len();
}
state.selected -= 1;
drop(state);
self.notify.emit(());
}
pub fn hide(&self) -> ApiResult<()> {
self.state.borrow_mut().target = None;
self.notify.emit(());
Ok(())
}
}
#[derive(Properties, PartialEq)]
pub struct FunctionDropDownPortalProps {
pub element: FunctionDropDownElement,
pub theme: String,
}
pub struct FunctionDropDownPortal {
_sub: Subscription,
}
impl Component for FunctionDropDownPortal {
type Message = ();
type Properties = FunctionDropDownPortalProps;
fn create(ctx: &Context<Self>) -> Self {
let link = ctx.link().clone();
let sub = ctx
.props()
.element
.notify
.add_listener(move |()| link.send_message(()));
Self { _sub: sub }
}
fn update(&mut self, _ctx: &Context<Self>, _msg: ()) -> bool {
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let state = ctx.props().element.state.borrow();
let target = state.target.clone();
let on_close = {
let element = ctx.props().element.clone();
Callback::from(move |()| {
let _ = element.hide();
})
};
if target.is_some() {
let values = state.values.clone();
let selected = state.selected;
let on_select = state.on_select.clone();
drop(state);
html! {
<PortalModal
tag_name="perspective-dropdown"
{target}
own_focus=false
{on_close}
theme={ctx.props().theme.clone()}
>
<FunctionDropDownView {values} {selected} {on_select} />
</PortalModal>
}
} else {
html! {}
}
}
}
#[derive(Properties, PartialEq)]
struct FunctionDropDownViewProps {
values: Vec<CompletionItemSuggestion>,
selected: usize,
on_select: Option<Callback<CompletionItemSuggestion>>,
}
#[function_component]
fn FunctionDropDownView(props: &FunctionDropDownViewProps) -> Html {
let body = html! {
if !props.values.is_empty() {
{ for props.values
.iter()
.enumerate()
.map(|(idx, value)| {
let click = props.on_select.as_ref().unwrap().reform({
let value = *value;
move |_: MouseEvent| value
});
html! {
if idx == props.selected {
<div onmousedown={click} class="selected">
<span style="font-weight:500">{ value.label }</span>
<br/>
<span style="padding-left:12px">{ value.documentation }</span>
</div>
} else {
<div onmousedown={click}>
<span style="font-weight:500">{ value.label }</span>
<br/>
<span style="padding-left:12px">{ value.documentation }</span>
</div>
}
}
}) }
}
};
html! { <><style>{ CSS }</style>{ body }</> }
}
fn filter_values(input: &str) -> Vec<CompletionItemSuggestion> {
let input = input.to_lowercase();
COMPLETIONS
.iter()
.filter(|x| x.label.to_lowercase().starts_with(&input))
.cloned()
.collect::<Vec<_>>()
}