use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::JsCast;
use web_sys::*;
use yew::prelude::*;
use super::super::LocalStyle;
use crate::components::form::highlight::highlight;
use crate::custom_elements::FunctionDropDownElement;
use crate::exprtk::{tokenize, Cursor};
use crate::js::PerspectiveValidationError;
use crate::utils::*;
use crate::*;
#[derive(Debug, Properties, PartialEq)]
pub struct CodeEditorProps {
pub expr: Rc<String>,
pub oninput: Callback<Rc<String>>,
pub onsave: Callback<()>,
#[prop_or_default]
pub error: Option<PerspectiveValidationError>,
}
fn on_input_callback(
event: InputEvent,
position: &UseStateSetter<u32>,
oninput: &Callback<Rc<String>>,
) {
let elem = event
.target()
.unwrap()
.unchecked_into::<web_sys::HtmlTextAreaElement>();
oninput.emit(elem.value().into());
position.set(elem.get_caret_position().unwrap_or_default());
}
fn on_keydown(event: KeyboardEvent, onsave: &Callback<()>) {
if event.shift_key() && event.key_code() == 13 {
event.prevent_default();
onsave.emit(())
}
}
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();
}
}
fn set_initial_state(
textarearef: &NodeRef,
contentref: &NodeRef,
connect_state: &Rc<RefCell<bool>>,
expr: &Rc<String>,
) {
let elem = textarearef.cast::<web_sys::HtmlTextAreaElement>().unwrap();
let is_connected = elem.is_connected();
let was_connected = connect_state.replace(is_connected);
if elem.value() != **expr {
elem.set_value(&format!("{}", expr));
}
if is_connected && !was_connected {
js_log_maybe! {
elem.set_caret_position(expr.len())?;
}
let content = contentref.cast::<HtmlElement>().unwrap();
content.set_scroll_top(elem.scroll_top());
content.set_scroll_left(elem.scroll_left());
}
}
#[function_component(CodeEditor)]
pub fn code_editor(props: &CodeEditorProps) -> Html {
let caret_position = use_state_eq(|| props.expr.len() as u32);
let scroll_offset = use_state_eq(|| (0, 0));
let is_connected = use_mut_ref(|| false);
let textarea_ref = use_node_ref().tee::<3>();
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(on_keydown, props.onsave.clone());
let oninput = use_callback(
|event, deps| on_input_callback(event, &deps.0, &deps.1),
(caret_position.setter(), props.oninput.clone()),
);
let onscroll = use_callback(
|_, deps| on_scroll(&deps.0, &deps.1),
(scroll_offset.setter(), textarea_ref.2),
);
use_effect({
clone!(props.expr);
move || set_initial_state(&textarea_ref.0, &content_ref.2, &is_connected, &expr)
});
use_effect_with_deps(
|deps| scroll_sync(&deps.0, &deps.1, &deps.2),
(scroll_offset, content_ref.0, lineno_ref.0),
);
use_effect_with_deps(
|deps| autocomplete(&deps.0, &deps.1, &deps.2),
(filter_dropdown, cursor.auto.clone(), cursor.noderef.clone()),
);
let line_numbers = cursor
.map_rows(|x| html!(<span class="line_number">{ x + 1 }</span>))
.collect::<Html>();
html_template! {
<LocalStyle href={ css!("form/code-editor") } />
<div id="editor">
<textarea
id="textarea_editable"
ref={ textarea_ref.1 }
spellcheck="false"
{ oninput }
{ onscroll }
{ onkeydown }>
</textarea>
<pre id="content" ref={ content_ref.1 }>
{ terms }
{
" "
}
</pre>
<div id="line_numbers" ref={ lineno_ref.1 }>
{ line_numbers }
</div>
</div>
}
}