1#![deny(unsafe_code)]
17#![forbid(unstable_features)]
18#![warn(missing_docs, rust_2018_idioms)]
19
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22
23use dioxus::prelude::*;
24use wasm_bindgen::prelude::*;
25use wasm_bindgen::JsCast;
26
27pub use taino_edit_core::schema;
29#[doc(no_inline)]
31pub use taino_edit_core::{
32 base_keymap, lift, remove_mark, select_all, set_block_type, set_mark, split_block, toggle_mark,
33 wrap_in, AttrSpec, AttrValue, Attrs, Command, Dispatch, EditorState, KeyPress, Keymap, Mark,
34 MarkSpec, MarkType, Node, NodeSpec, NodeType, Plugin, PluginKey, PluginSet, ResolvedPos,
35 Schema, SchemaBuilder, Selection, Slice, Transaction, Transform,
36};
37#[doc(no_inline)]
39pub use taino_edit_dom::{Decoration, EditorView, ViewAction, ViewDesc, ViewPlugin};
40
41#[derive(Clone, Default)]
58pub struct ViewPlugins(PluginCell);
59
60type PluginCell = Rc<RefCell<Option<Vec<Box<dyn ViewPlugin>>>>>;
62
63impl ViewPlugins {
64 pub fn new(plugins: Vec<Box<dyn ViewPlugin>>) -> Self {
66 Self(Rc::new(RefCell::new(Some(plugins))))
67 }
68
69 fn take(&self) -> Vec<Box<dyn ViewPlugin>> {
72 self.0.borrow_mut().take().unwrap_or_default()
73 }
74}
75
76impl PartialEq for ViewPlugins {
77 fn eq(&self, _other: &Self) -> bool {
80 true
81 }
82}
83
84impl std::fmt::Debug for ViewPlugins {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 let n = self.0.borrow().as_ref().map_or(0, Vec::len);
87 f.debug_struct("ViewPlugins").field("pending", &n).finish()
88 }
89}
90
91#[derive(Clone, Default)]
100pub struct KeymapProp(Rc<RefCell<Option<Keymap>>>);
101
102impl KeymapProp {
103 pub fn new(keymap: Keymap) -> Self {
105 Self(Rc::new(RefCell::new(Some(keymap))))
106 }
107
108 fn take(&self) -> Option<Keymap> {
110 self.0.borrow_mut().take()
111 }
112}
113
114impl PartialEq for KeymapProp {
115 fn eq(&self, _other: &Self) -> bool {
117 true
118 }
119}
120
121impl std::fmt::Debug for KeymapProp {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("KeymapProp").finish_non_exhaustive()
124 }
125}
126
127#[component]
143pub fn TainoEditor(
144 state: Signal<EditorState>,
145 #[props(default)]
150 plugins: ViewPlugins,
151 #[props(default)]
157 keymap: KeymapProp,
158) -> Element {
159 let mut runtime: Signal<Option<EditorRuntime>> = use_signal(|| None);
162
163 use_effect(move || {
165 let snapshot = state.read().clone();
166 if let Some(rt) = runtime.write().as_mut() {
167 rt.view.update(snapshot.doc().clone());
168 let mirrored_from_dom = rt.selection_from_dom.replace(false);
178 if !mirrored_from_dom
179 && rt.view.has_focus()
180 && rt.view.read_selection() != Some(snapshot.selection())
181 {
182 rt.applying_selection.set(true);
183 let _ = rt.view.set_selection(snapshot.selection());
184 rt.applying_selection.set(false);
185 }
186 rt.view.refresh_view_decorations(Some(snapshot.selection()));
189 }
190 });
191
192 let on_mounted = move |evt: Event<MountedData>| {
193 let Some(element) = evt.data().downcast::<web_sys::Element>().cloned() else {
194 return;
195 };
196 let snapshot = state.read().clone();
197 let mut view = EditorView::mount(
198 snapshot.doc().clone(),
199 snapshot.schema().clone(),
200 element.clone(),
201 );
202 view.set_view_plugins(plugins.take());
203 view.refresh_view_decorations(Some(snapshot.selection()));
204 let applying = Rc::new(Cell::new(false));
205 let from_dom = Rc::new(Cell::new(false));
206 let keymap_cell: Rc<RefCell<Option<Keymap>>> = Rc::new(RefCell::new(keymap.take()));
209 let closures = wire_events(
210 &element,
211 runtime,
212 state,
213 applying.clone(),
214 from_dom.clone(),
215 keymap_cell.clone(),
216 );
217 runtime.set(Some(EditorRuntime {
218 view,
219 closures,
220 applying_selection: applying,
221 selection_from_dom: from_dom,
222 keymap: keymap_cell,
223 }));
224 };
225
226 rsx! {
227 div {
228 class: "taino-editor",
229 onmounted: on_mounted,
230 }
231 }
232}
233
234struct EditorRuntime {
237 view: EditorView,
238 #[allow(dead_code)] closures: Vec<EventCloser>,
240 applying_selection: Rc<Cell<bool>>,
243 selection_from_dom: Rc<Cell<bool>>,
248 #[allow(dead_code)] keymap: Rc<RefCell<Option<Keymap>>>,
252}
253
254struct EventCloser {
256 event: &'static str,
257 target: web_sys::EventTarget,
258 closure: Closure<dyn FnMut(web_sys::Event)>,
259 capture: bool,
262}
263
264impl Drop for EventCloser {
265 fn drop(&mut self) {
266 let _ = self.target.remove_event_listener_with_callback_and_bool(
267 self.event,
268 self.closure.as_ref().unchecked_ref(),
269 self.capture,
270 );
271 }
272}
273
274fn push_listener(
275 closers: &mut Vec<EventCloser>,
276 target: web_sys::EventTarget,
277 event: &'static str,
278 closure: Closure<dyn FnMut(web_sys::Event)>,
279) {
280 push_listener_capture(closers, target, event, closure, false);
281}
282
283fn push_listener_capture(
284 closers: &mut Vec<EventCloser>,
285 target: web_sys::EventTarget,
286 event: &'static str,
287 closure: Closure<dyn FnMut(web_sys::Event)>,
288 capture: bool,
289) {
290 if target
291 .add_event_listener_with_callback_and_bool(event, closure.as_ref().unchecked_ref(), capture)
292 .is_ok()
293 {
294 closers.push(EventCloser {
295 event,
296 target,
297 closure,
298 capture,
299 });
300 }
301}
302
303fn wire_events(
304 el: &web_sys::Element,
305 mut runtime: Signal<Option<EditorRuntime>>,
306 mut state: Signal<EditorState>,
307 applying_selection: Rc<Cell<bool>>,
308 selection_from_dom: Rc<Cell<bool>>,
309 keymap_cell: Rc<RefCell<Option<Keymap>>>,
310) -> Vec<EventCloser> {
311 let target: web_sys::EventTarget = el.clone().into();
312 let mut closers: Vec<EventCloser> = Vec::new();
313
314 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
316 if let Some(Some(t)) = with_view(runtime, |v| v.read_dom_changes()) {
317 apply_transform(state, &t);
318 }
319 });
320 push_listener(&mut closers, target.clone(), "input", cb);
321
322 let km_for_keydown = keymap_cell;
328 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |ev: web_sys::Event| {
329 let Ok(kev) = ev.dyn_into::<web_sys::KeyboardEvent>() else {
330 return;
331 };
332 let key = KeyPress {
333 key: kev.key(),
334 ctrl: kev.ctrl_key(),
335 alt: kev.alt_key(),
336 shift: kev.shift_key(),
337 meta: kev.meta_key(),
338 };
339 let mut cur = state.peek().clone();
340 if let Some(Some(live)) = with_view(runtime, |v| v.read_selection()) {
341 if live != cur.selection() {
342 let mut tx = cur.tr();
343 tx.set_selection(live);
344 tx.no_history();
345 cur = cur.apply(tx);
346 }
347 }
348 let mut next = None;
349 let handled = match km_for_keydown.borrow().as_ref() {
350 Some(km) => {
351 let mut d = |t: Transaction| next = Some(cur.apply(t));
352 km.handle(&cur, &key, Some(&mut d))
353 }
354 None => false,
355 };
356 if let Some(n) = next {
357 if let Some(rt) = runtime.write().as_mut() {
359 rt.view.update(n.doc().clone());
360 rt.applying_selection.set(true);
361 let _ = rt.view.set_selection(n.selection());
362 rt.applying_selection.set(false);
363 rt.view.refresh_view_decorations(Some(n.selection()));
364 }
365 state.set(n);
366 }
367 let structural = matches!(key.key.as_str(), "Enter" | "Backspace" | "Delete");
369 if handled || structural {
370 kev.prevent_default();
371 }
372 });
373 push_listener(&mut closers, target.clone(), "keydown", cb);
374
375 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
377 with_view(runtime, |v| v.composition_start());
378 });
379 push_listener(&mut closers, target.clone(), "compositionstart", cb);
380
381 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
382 let t = with_view(runtime, |v| {
383 v.composition_end();
384 v.read_dom_changes()
385 })
386 .flatten();
387 if let Some(t) = t {
388 apply_transform(state, &t);
389 }
390 });
391 push_listener(&mut closers, target.clone(), "compositionend", cb);
392
393 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |ev: web_sys::Event| {
396 let Ok(clip) = ev.dyn_into::<web_sys::ClipboardEvent>() else {
397 return;
398 };
399 clip.prevent_default();
400 let Some(data) = clip.clipboard_data() else {
401 return;
402 };
403 let md = data.get_data("text/markdown").unwrap_or_default();
404 let html = data.get_data("text/html").unwrap_or_default();
405 let text = data.get_data("text/plain").unwrap_or_default();
406 let t = with_view(runtime, |v| {
407 if !md.is_empty() {
408 v.paste_markdown(&md)
409 } else if !html.is_empty() {
410 v.paste_html(&html)
411 } else if !text.is_empty() {
412 v.paste_text(&text)
413 } else {
414 None
415 }
416 })
417 .flatten();
418 if let Some(t) = t {
419 apply_transform(state, &t);
420 }
421 });
422 push_listener(&mut closers, target.clone(), "paste", cb);
423
424 for kind in ["mousedown", "mousemove", "mouseup"] {
428 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |ev: web_sys::Event| {
429 if let Some(Some(action)) = with_view(runtime, |v| v.handle_view_event(&ev)) {
430 apply_view_action(state, action);
431 }
432 });
433 push_listener(&mut closers, target.clone(), kind, cb);
434 }
435
436 if let Some(doc) = web_sys::window().and_then(|w| w.document()) {
440 let doc_target: web_sys::EventTarget = doc.into();
441 let applying = applying_selection;
442 let from_dom = selection_from_dom;
443 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
444 if applying.get() {
445 return;
446 }
447 let Some(Some(sel)) = with_view(runtime, |v| v.read_selection()) else {
448 return;
449 };
450 let cur = state.peek().selection();
451 if sel == cur {
452 return;
453 }
454 from_dom.set(true);
457 let mut s = state;
458 let next = {
459 let snap = s.peek();
460 let mut tx = snap.tr();
461 tx.set_selection(sel);
462 tx.no_history();
463 snap.apply(tx)
464 };
465 s.set(next);
466 });
467 push_listener(&mut closers, doc_target, "selectionchange", cb);
468 }
469
470 if let Some(window) = web_sys::window() {
474 let win_target: web_sys::EventTarget = window.unchecked_into();
475 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
476 with_view(runtime, |v| v.reposition_inline_decorations());
477 });
478 push_listener_capture(&mut closers, win_target.clone(), "scroll", cb, true);
479 let cb = Closure::<dyn FnMut(web_sys::Event)>::new(move |_ev: web_sys::Event| {
480 with_view(runtime, |v| v.reposition_inline_decorations());
481 });
482 push_listener(&mut closers, win_target, "resize", cb);
483 }
484
485 closers
486}
487
488fn with_view<R>(
490 runtime: Signal<Option<EditorRuntime>>,
491 f: impl FnOnce(&EditorView) -> R,
492) -> Option<R> {
493 runtime.peek().as_ref().map(|rt| f(&rt.view))
494}
495
496fn apply_view_action(mut state: Signal<EditorState>, action: ViewAction) {
498 match action {
499 ViewAction::Select(sel) => {
500 let next = {
501 let snap = state.peek();
502 let mut tx = snap.tr();
503 tx.set_selection(sel);
504 tx.no_history();
505 snap.apply(tx)
506 };
507 state.set(next);
508 }
509 ViewAction::Command(cmd) => {
510 let snapshot = state.peek().clone();
511 let mut next = None;
512 {
513 let mut d = |tx: Transaction| next = Some(snapshot.apply(tx));
514 cmd(&snapshot, Some(&mut d));
515 }
516 if let Some(n) = next {
517 state.set(n);
518 }
519 }
520 }
521}
522
523fn apply_transform(mut state: Signal<EditorState>, tr: &Transform) {
525 let next = {
526 let snap = state.peek();
527 let mut tx = snap.tr();
528 let mut ok = true;
529 for step in tr.steps() {
530 if tx.transform().step(step.clone(), snap.schema()).is_err() {
531 ok = false;
532 break;
533 }
534 }
535 if !ok {
536 return;
537 }
538 snap.apply(tx)
539 };
540 state.set(next);
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 struct Dummy;
549 impl ViewPlugin for Dummy {}
550
551 #[test]
552 fn view_plugins_take_is_once() {
553 let p = ViewPlugins::new(vec![Box::new(Dummy), Box::new(Dummy)]);
554 assert_eq!(p.take().len(), 2, "first take yields the installed plugins");
555 assert_eq!(
556 p.take().len(),
557 0,
558 "the container is empty after the first take"
559 );
560 }
561
562 #[test]
563 fn view_plugins_always_compare_equal() {
564 assert_eq!(
567 ViewPlugins::new(vec![Box::new(Dummy)]),
568 ViewPlugins::default()
569 );
570 }
571}