perspective_viewer/components/form/
code_editor.rsuse std::rc::Rc;
use perspective_client::ExprValidationError;
use wasm_bindgen::JsCast;
use web_sys::*;
use yew::prelude::*;
use crate::components::form::highlight::highlight;
use crate::components::style::LocalStyle;
use crate::custom_elements::FunctionDropDownElement;
use crate::exprtk::{tokenize, Cursor};
use crate::utils::*;
use crate::*;
#[derive(Properties, PartialEq)]
pub struct CodeEditorProps {
pub expr: Rc<String>,
pub oninput: Callback<Rc<String>>,
pub onsave: Callback<()>,
pub disabled: bool,
#[prop_or_default]
pub autofocus: bool,
#[prop_or_default]
pub wordwrap: bool,
#[prop_or_default]
pub autosuggest: bool,
#[prop_or_default]
pub select_all: Subscriber<()>,
#[prop_or_default]
pub error: Option<ExprValidationError>,
}
fn on_input_callback(
event: InputEvent,
oninput: &Callback<Rc<String>>,
) {
let elem = event
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlTextAreaElement>();
oninput.emit(elem.value().into());
}
fn on_keydown(event: KeyboardEvent, deps: &(UseStateSetter<u32>, Callback<()>)) {
let elem = event
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlTextAreaElement>();
deps.0.set(elem.get_caret_position().unwrap_or_default());
if event.shift_key() && event.key_code() == 13 {
event.prevent_default();
deps.1.emit(())
}
if event.key() == "Tab" {
event.prevent_default();
let caret_pos = elem.selection_start().unwrap().unwrap_or_default() as usize;
let mut initial_text = elem.value();
initial_text.insert(caret_pos, '\t');
elem.set_value(&initial_text);
let input_event = web_sys::InputEvent::new("input").unwrap();
let _ = elem.dispatch_event(&input_event).unwrap();
let new_caret_pos = (caret_pos + 1) as u32;
let _ = elem.set_selection_range(new_caret_pos, new_caret_pos);
elem.focus().unwrap();
}
}
fn on_scroll(scroll: &UseStateSetter<(i32, i32)>, editable: &NodeRef) {
let div = editable.cast::<HtmlElement>().unwrap();
scroll.set((div.scroll_top(), div.scroll_left()));
}
fn scroll_sync(scroll: &UseStateHandle<(i32, i32)>, div: &NodeRef, lineno: &NodeRef) {
if let Some(div) = div.cast::<HtmlElement>() {
div.set_scroll_top(scroll.0);
div.set_scroll_left(scroll.1);
}
if let Some(div) = lineno.cast::<HtmlElement>() {
div.set_scroll_top(scroll.0);
}
}
fn autocomplete(
filter_dropdown: &Rc<FunctionDropDownElement>,
token: &Option<String>,
target: &NodeRef,
) {
if let Some(x) = token {
let elem = target.cast::<HtmlElement>().unwrap();
if elem.is_connected() {
filter_dropdown
.autocomplete(x.clone(), elem, Callback::from(|_| ()))
.unwrap();
} else {
filter_dropdown.hide().unwrap();
}
} else {
filter_dropdown.hide().unwrap();
}
}
#[function_component(CodeEditor)]
pub fn code_editor(props: &CodeEditorProps) -> Html {
let select_all = use_state_eq(|| false);
let caret_position = use_state_eq(|| 0_u32);
let scroll_offset = use_state_eq(|| (0, 0));
let textarea_ref = use_node_ref().tee::<5>();
let content_ref = use_node_ref().tee::<3>();
let lineno_ref = use_node_ref().tee::<2>();
let filter_dropdown = use_memo((), |_| FunctionDropDownElement::default());
let mut cursor = Cursor::new(&props.error);
let terms = tokenize(&props.expr)
.into_iter()
.map(|token| highlight(&mut cursor, token, *caret_position))
.collect::<Html>();
let onkeydown = use_callback((caret_position.setter(), props.onsave.clone()), on_keydown);
let oninput = use_callback(props.oninput.clone(), |event, deps| {
on_input_callback(event, deps)
});
let onscroll = use_callback((scroll_offset.setter(), textarea_ref.0), |_, deps| {
on_scroll(&deps.0, &deps.1)
});
let autofocus = props.autofocus;
use_effect_with((props.expr.clone(), textarea_ref.1), {
move |(expr, textarea_ref)| {
let elem = textarea_ref.cast::<web_sys::HtmlTextAreaElement>().unwrap();
if autofocus {
elem.focus().unwrap();
}
if **expr != elem.value() {
elem.set_value(&format!("{}", expr));
elem.set_scroll_top(0);
elem.set_scroll_left(0);
elem.set_caret_position(0).unwrap();
}
}
});
use_effect_with((select_all.setter(), props.select_all.clone()), {
move |(select_all, props_select_all)| {
clone!(select_all);
let sub = props_select_all.add_listener(move |()| select_all.set(true));
move || drop(sub)
}
});
use_effect_with((select_all, textarea_ref.2), {
move |(select_all, textarea_ref)| {
let elem = textarea_ref.cast::<web_sys::HtmlTextAreaElement>().unwrap();
if **select_all {
elem.focus().unwrap();
elem.select_all().unwrap();
}
select_all.set(false);
}
});
let autofocus = props.autofocus;
use_effect_with((props.error.clone(), textarea_ref.3), {
move |(_, textarea_ref)| {
if autofocus {
let elem = textarea_ref.cast::<web_sys::HtmlTextAreaElement>().unwrap();
elem.focus().unwrap();
}
}
});
use_effect_with((scroll_offset, content_ref.0, lineno_ref.0), |deps| {
scroll_sync(&deps.0, &deps.1, &deps.2)
});
use_effect_with(filter_dropdown.clone(), |filter_dropdown| {
clone!(filter_dropdown);
move || filter_dropdown.hide().unwrap()
});
use_effect_with(
(
props.autosuggest,
filter_dropdown,
cursor.auto.clone(),
cursor.noderef.clone(),
),
|deps| {
if deps.0 {
autocomplete(&deps.1, &deps.2, &deps.3)
}
},
);
let error_line = props.error.as_ref().map(|x| x.line);
let line_numbers = cursor
.map_rows(|x| html!(
<span
class={if Some(x) == error_line {"line_number error_highlight"} else {"line_number"}}
>
{ x + 1 }
</span>
))
.collect::<Html>();
let class = if props.wordwrap { "wordwrap" } else { "" };
clone!(props.disabled);
html! {
<>
<LocalStyle href={css!("form/code-editor")} />
<div id="editor" {class}>
<div id="line_numbers" ref={lineno_ref.1}>{ line_numbers }</div>
<div id="editor-inner" {class}>
<textarea
{disabled}
id="textarea_editable"
ref={textarea_ref.4}
spellcheck="false"
{oninput}
{onscroll}
{onkeydown}
/>
<div id="editor-height-sizer" />
<pre id="content" ref={content_ref.1}>
{ terms }
{ {
" "
} }
</pre>
</div>
</div>
</>
}
}