#![deny(unsafe_code)]
#![forbid(unstable_features)]
#![warn(missing_docs, rust_2018_idioms)]
use std::cell::Cell;
use std::rc::Rc;
use dioxus::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[doc(no_inline)]
pub use taino_edit_core::{
base_keymap, lift, remove_mark, select_all, set_block_type, set_mark, split_block, toggle_mark,
wrap_in, AttrSpec, AttrValue, Attrs, Command, Dispatch, EditorState, KeyPress, Keymap, Mark,
MarkSpec, MarkType, Node, NodeSpec, NodeType, Plugin, PluginKey, PluginSet, ResolvedPos,
Schema, SchemaBuilder, Selection, Slice, Transaction, Transform,
};
#[doc(no_inline)]
pub use taino_edit_dom::{Decoration, EditorView, ViewDesc};
#[component]
pub fn TainoEditor(state: Signal<EditorState>) -> Element {
let mut runtime: Signal<Option<EditorRuntime>> = use_signal(|| None);
use_effect(move || {
let snapshot = state.read().clone();
if let Some(rt) = runtime.write().as_mut() {
rt.view.update(snapshot.doc().clone());
if rt.view.read_selection() != Some(snapshot.selection()) {
rt.applying_selection.set(true);
let _ = rt.view.set_selection(snapshot.selection());
rt.applying_selection.set(false);
}
}
});
let on_mounted = move |evt: Event<MountedData>| {
let Some(element) = evt.data().downcast::<web_sys::Element>().cloned() else {
return;
};
let snapshot = state.read().clone();
let view = EditorView::mount(
snapshot.doc().clone(),
snapshot.schema().clone(),
element.clone(),
);
let applying = Rc::new(Cell::new(false));
let closures = wire_events(&element, runtime, state, applying.clone());
runtime.set(Some(EditorRuntime {
view,
closures,
applying_selection: applying,
}));
};
rsx! {
div {
class: "taino-editor",
onmounted: on_mounted,
}
}
}
struct EditorRuntime {
view: EditorView,
#[allow(dead_code)] closures: Vec<EventCloser>,
applying_selection: Rc<Cell<bool>>,
}
struct EventCloser {
event: &'static str,
target: web_sys::EventTarget,
closure: Closure<dyn FnMut(web_sys::Event)>,
}
impl Drop for EventCloser {
fn drop(&mut self) {
let _ = self
.target
.remove_event_listener_with_callback(self.event, self.closure.as_ref().unchecked_ref());
}
}
fn push_listener(
closers: &mut Vec<EventCloser>,
target: web_sys::EventTarget,
event: &'static str,
closure: Closure<dyn FnMut(web_sys::Event)>,
) {
if target
.add_event_listener_with_callback(event, closure.as_ref().unchecked_ref())
.is_ok()
{
closers.push(EventCloser {
event,
target,
closure,
});
}
}
fn wire_events(
el: &web_sys::Element,
runtime: Signal<Option<EditorRuntime>>,
state: Signal<EditorState>,
applying_selection: Rc<Cell<bool>>,
) -> Vec<EventCloser> {
let target: web_sys::EventTarget = el.clone().into();
let mut closers: Vec<EventCloser> = Vec::new();
let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
if let Some(Some(t)) = with_view(runtime, |v| v.read_dom_changes()) {
apply_transform(state, &t);
}
});
push_listener(&mut closers, target.clone(), "input", cb);
let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
with_view(runtime, |v| v.composition_start());
});
push_listener(&mut closers, target.clone(), "compositionstart", cb);
let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
let t = with_view(runtime, |v| {
v.composition_end();
v.read_dom_changes()
})
.flatten();
if let Some(t) = t {
apply_transform(state, &t);
}
});
push_listener(&mut closers, target.clone(), "compositionend", cb);
let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |ev: web_sys::Event| {
let Ok(clip) = ev.dyn_into::<web_sys::ClipboardEvent>() else {
return;
};
clip.prevent_default();
let Some(data) = clip.clipboard_data() else {
return;
};
let md = data.get_data("text/markdown").unwrap_or_default();
let html = data.get_data("text/html").unwrap_or_default();
let text = data.get_data("text/plain").unwrap_or_default();
let t = with_view(runtime, |v| {
if !md.is_empty() {
v.paste_markdown(&md)
} else if !html.is_empty() {
v.paste_html(&html)
} else if !text.is_empty() {
v.paste_text(&text)
} else {
None
}
})
.flatten();
if let Some(t) = t {
apply_transform(state, &t);
}
});
push_listener(&mut closers, target.clone(), "paste", cb);
if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
let doc_target: web_sys::EventTarget = doc.into();
let applying = applying_selection;
let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
if applying.get() {
return;
}
let Some(Some(sel)) = with_view(runtime, |v| v.read_selection()) else {
return;
};
let cur = state.peek().selection();
if sel == cur {
return;
}
let mut s = state;
let next = {
let snap = s.peek();
let mut tx = snap.tr();
tx.set_selection(sel);
tx.no_history();
snap.apply(tx)
};
s.set(next);
});
push_listener(&mut closers, doc_target, "selectionchange", cb);
}
closers
}
fn with_view<R>(
runtime: Signal<Option<EditorRuntime>>,
f: impl FnOnce(&EditorView) -> R,
) -> Option<R> {
runtime.peek().as_ref().map(|rt| f(&rt.view))
}
fn apply_transform(mut state: Signal<EditorState>, tr: &Transform) {
let next = {
let snap = state.peek();
let mut tx = snap.tr();
let mut ok = true;
for step in tr.steps() {
if tx.transform().step(step.clone(), snap.schema()).is_err() {
ok = false;
break;
}
}
if !ok {
return;
}
snap.apply(tx)
};
state.set(next);
}