use std::cell::Cell;
use taino_edit_core::{Command, DomSpec, Fragment, Node, Schema, Selection, Slice, Transform};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{Document, Element};
use crate::decoration::Decoration;
use crate::desc::ViewDesc;
use crate::position_map::{doc_pos_to_dom, dom_to_doc_pos};
pub enum ViewAction {
Select(Selection),
Command(Command),
}
pub trait ViewPlugin {
fn handle_event(&self, _view: &EditorView, _event: &web_sys::Event) -> Option<ViewAction> {
None
}
fn decorations(&self, _view: &EditorView, _selection: Option<Selection>) -> Vec<Decoration> {
Vec::new()
}
}
pub struct EditorView {
root: Element,
schema: Schema,
doc: Node,
children: Vec<ViewDesc>,
composing: Cell<bool>,
decorations: Vec<Decoration>,
plugins: Vec<Box<dyn ViewPlugin>>,
overlay: Option<Element>,
}
impl std::fmt::Debug for EditorView {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EditorView")
.field("doc", &self.doc)
.field("children", &self.children)
.field("decorations", &self.decorations)
.field("plugins", &self.plugins.len())
.finish_non_exhaustive()
}
}
impl Drop for EditorView {
fn drop(&mut self) {
if let Some(layer) = &self.overlay {
if let Some(parent) = layer.parent_element() {
let _ = parent.remove_child(layer);
}
}
}
}
impl EditorView {
pub fn mount(doc: Node, schema: Schema, root: Element) -> Self {
let _ = root.set_attribute("contenteditable", "true");
if !root.has_attribute("tabindex") {
let _ = root.set_attribute("tabindex", "0");
}
let document = root
.owner_document()
.expect("root element has an owner Document");
while let Some(child) = root.first_child() {
if root.remove_child(&child).is_err() {
break; }
}
let mut children = Vec::with_capacity(doc.child_count());
for child in doc.content().iter() {
let desc = render(child, &document);
let _ = root.append_child(&desc.dom_node());
children.push(desc);
}
EditorView {
root,
schema,
doc,
children,
composing: Cell::new(false),
decorations: Vec::new(),
plugins: Vec::new(),
overlay: None,
}
}
pub fn set_view_plugins(&mut self, plugins: Vec<Box<dyn ViewPlugin>>) {
self.plugins = plugins;
}
pub fn handle_view_event(&self, event: &web_sys::Event) -> Option<ViewAction> {
for p in &self.plugins {
if let Some(action) = p.handle_event(self, event) {
return Some(action);
}
}
None
}
pub fn refresh_view_decorations(&mut self, selection: Option<Selection>) {
let decos: Vec<Decoration> = self
.plugins
.iter()
.flat_map(|p| p.decorations(self, selection))
.collect();
self.set_decorations(decos);
}
pub fn pos_at_point(&self, x: f32, y: f32) -> Option<usize> {
let document = web_sys::window()?.document()?;
let mut el = document.element_from_point(x, y)?;
loop {
if let Some(pos) = pos_before_element(&self.children, 0, &el) {
return Some(pos);
}
let parent = el.parent_element()?;
let same_root = parent.is_same_node(Some(self.root.as_ref()));
if !same_root && !self.root.contains(Some(parent.as_ref())) {
return None;
}
el = parent;
}
}
pub fn node_dom_at(&self, pos: usize) -> Option<Element> {
dom_element_at(&self.children, 0, pos).cloned()
}
pub fn set_decorations(&mut self, decorations: Vec<Decoration>) {
for d in &self.decorations {
if matches!(d, Decoration::Node { .. }) {
apply_decoration(&self.children, d, false);
}
}
for d in &decorations {
if matches!(d, Decoration::Node { .. }) {
apply_decoration(&self.children, d, true);
}
}
self.decorations = decorations;
let any_inline = self
.decorations
.iter()
.any(|d| matches!(d, Decoration::Inline { .. }));
let _ = self.ensure_overlay(any_inline);
self.paint_inline_overlay();
}
pub fn decorations(&self) -> &[Decoration] {
&self.decorations
}
pub fn reposition_inline_decorations(&self) {
self.paint_inline_overlay();
}
fn paint_inline_overlay(&self) {
let Some(layer) = &self.overlay else {
return;
};
layer.set_inner_html("");
let Some(document) = self.root.owner_document() else {
return;
};
let origin = layer.get_bounding_client_rect();
for d in &self.decorations {
let Decoration::Inline { from, to, class } = d else {
continue;
};
let (Some((sn, so)), Some((en, eo))) = (
doc_pos_to_dom(&self.root, &self.children, *from),
doc_pos_to_dom(&self.root, &self.children, *to),
) else {
continue;
};
let Ok(range) = document.create_range() else {
continue;
};
if range.set_start(&sn, so).is_err() || range.set_end(&en, eo).is_err() {
continue;
}
let Some(rects) = range.get_client_rects() else {
continue;
};
for i in 0..rects.length() {
let Some(r) = rects.get(i) else { continue };
if r.width() <= 0.0 && r.height() <= 0.0 {
continue;
}
let Ok(box_el) = document.create_element("span") else {
continue;
};
let _ = box_el.set_attribute("class", class);
let style = format!(
"position:absolute;left:{:.2}px;top:{:.2}px;width:{:.2}px;\
height:{:.2}px;pointer-events:none;",
r.left() - origin.left(),
r.top() - origin.top(),
r.width(),
r.height(),
);
let _ = box_el.set_attribute("style", &style);
let _ = layer.append_child(&box_el);
}
}
}
fn ensure_overlay(&mut self, want: bool) -> Option<Element> {
if let Some(layer) = &self.overlay {
return Some(layer.clone());
}
if !want {
return None;
}
let parent = self.root.parent_element()?;
let document = self.root.owner_document()?;
let layer = document.create_element("div").ok()?;
let _ = layer.set_attribute("class", "taino-deco-layer");
let _ = layer.set_attribute(
"style",
"position:absolute;left:0;top:0;width:0;height:0;pointer-events:none;",
);
let _ = parent.append_child(&layer);
self.overlay = Some(layer.clone());
Some(layer)
}
pub fn focus(&self) -> Result<(), JsValue> {
let el: web_sys::HtmlElement = self.root.clone().dyn_into()?;
el.focus()
}
pub fn has_focus(&self) -> bool {
let Some(document) = self.root.owner_document() else {
return false;
};
let Some(active) = document.active_element() else {
return false;
};
wasm_bindgen::JsValue::from(active) == wasm_bindgen::JsValue::from(&self.root)
}
pub fn set_tabindex(&self, n: i32) {
let _ = self.root.set_attribute("tabindex", &n.to_string());
}
pub fn composition_start(&self) {
self.composing.set(true);
}
pub fn composition_end(&self) {
self.composing.set(false);
}
pub fn is_composing(&self) -> bool {
self.composing.get()
}
pub fn root(&self) -> &Element {
&self.root
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn doc(&self) -> &Node {
&self.doc
}
pub fn children(&self) -> &[ViewDesc] {
&self.children
}
pub fn set_selection(&self, sel: Selection) -> Result<(), JsValue> {
let window = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?;
let selection = window
.get_selection()?
.ok_or_else(|| JsValue::from_str("no Selection api"))?;
let (anchor_pos, head_pos) = match sel {
Selection::Text { anchor, head } => (anchor, head),
Selection::Node { pos } => {
let len = self.doc.node_at(pos).map(|n| n.node_size()).unwrap_or(0);
(pos, pos + len)
}
Selection::Cell { anchor, head } => {
let lo = anchor.min(head);
let hi = anchor.max(head);
let hi_end = self
.doc
.node_at(hi)
.map(|n| hi + n.node_size())
.unwrap_or(hi);
(lo, hi_end)
}
Selection::All => (0, self.doc.content().size()),
};
let (anchor_node, anchor_off) = doc_pos_to_dom(&self.root, &self.children, anchor_pos)
.ok_or_else(|| JsValue::from_str("anchor out of range"))?;
let (focus_node, focus_off) = doc_pos_to_dom(&self.root, &self.children, head_pos)
.ok_or_else(|| JsValue::from_str("head out of range"))?;
selection.remove_all_ranges()?;
selection.set_base_and_extent(&anchor_node, anchor_off, &focus_node, focus_off)
}
pub fn read_selection(&self) -> Option<Selection> {
let window = web_sys::window()?;
let selection = window.get_selection().ok().flatten()?;
let anchor_node = selection.anchor_node()?;
let focus_node = selection.focus_node()?;
let anchor = dom_to_doc_pos(
&self.root,
&self.children,
&anchor_node,
selection.anchor_offset(),
)?;
let head = dom_to_doc_pos(
&self.root,
&self.children,
&focus_node,
selection.focus_offset(),
)?;
Some(Selection::Text { anchor, head })
}
pub fn read_dom_changes(&self) -> Option<Transform> {
if self.composing.get() {
return None;
}
let mut found = None;
collect_text_changes(&self.children, 0, &mut |desc, doc_pos| {
if found.is_some() {
return;
}
if let ViewDesc::Text { node, text, .. } = desc {
let dom_data = text.data();
let doc_text = node.text().unwrap_or("");
if dom_data != doc_text {
if let Some((offset, old_len, new_part)) = find_diff(doc_text, &dom_data) {
found = Some((doc_pos + offset, old_len, new_part, node.clone()));
}
}
}
});
if let Some((pos, old_len, new_text, prev_text_node)) = found {
let mut transform = Transform::new(self.doc.clone());
let replacement = if new_text.is_empty() {
Slice::empty()
} else {
let new_node = self
.schema
.text(&new_text, prev_text_node.marks().to_vec())
.ok()?;
Slice::new(Fragment::from_node(new_node), 0, 0)
};
transform
.replace(pos, pos + old_len, replacement, &self.schema)
.ok()?;
return Some(transform);
}
if let Some((pos, text)) = find_empty_block_text(&self.children, 0) {
let new_node = self.schema.text(&text, vec![]).ok()?;
let mut transform = Transform::new(self.doc.clone());
transform
.replace(
pos,
pos,
Slice::new(Fragment::from_node(new_node), 0, 0),
&self.schema,
)
.ok()?;
return Some(transform);
}
None
}
fn paste_range(&self) -> Option<(usize, usize)> {
let sel = self.read_selection()?;
Some(match sel {
Selection::Text { anchor, head } => (anchor.min(head), anchor.max(head)),
Selection::Node { pos } => {
let len = self.doc.node_at(pos).map(|n| n.node_size()).unwrap_or(0);
(pos, pos + len)
}
Selection::Cell { anchor, head } => {
let lo = anchor.min(head);
let hi = anchor.max(head);
let hi_end = self
.doc
.node_at(hi)
.map(|n| hi + n.node_size())
.unwrap_or(hi);
(lo, hi_end)
}
Selection::All => (0, self.doc.content().size()),
})
}
pub fn paste_text(&self, text: &str) -> Option<Transform> {
let (from, to) = self.paste_range()?;
let mut transform = Transform::new(self.doc.clone());
let slice = if text.is_empty() {
Slice::empty()
} else {
let node = self.schema.text(text, vec![]).ok()?;
Slice::new(Fragment::from_node(node), 0, 0)
};
transform.replace(from, to, slice, &self.schema).ok()?;
Some(transform)
}
pub fn paste_html(&self, html: &str) -> Option<Transform> {
let parsed = self.schema.parse_html(html).ok()?;
let (from, to) = self.paste_range()?;
let slice = Slice::new(parsed.content().clone(), 0, 0);
let mut transform = Transform::new(self.doc.clone());
transform.replace(from, to, slice, &self.schema).ok()?;
Some(transform)
}
pub fn paste_markdown(&self, md: &str) -> Option<Transform> {
let parsed = taino_edit_core::markdown::parse_markdown(&self.schema, md).ok()?;
let (from, to) = self.paste_range()?;
let slice = Slice::new(parsed.content().clone(), 0, 0);
let mut transform = Transform::new(self.doc.clone());
transform.replace(from, to, slice, &self.schema).ok()?;
Some(transform)
}
pub fn extract_slice(&self, from: usize, to: usize) -> Option<Slice> {
self.doc.slice(from, to).ok()
}
pub fn drop_slice(&self, slice: &Slice, at: usize) -> Option<Transform> {
let mut transform = Transform::new(self.doc.clone());
transform
.replace(at, at, slice.clone(), &self.schema)
.ok()?;
Some(transform)
}
pub fn update(&mut self, new_doc: Node) {
let document = self
.root
.owner_document()
.expect("root element has an owner Document");
let new_kids: Vec<Node> = new_doc.content().iter().cloned().collect();
let new_descs = patch_children(&document, &self.root, &self.children, &new_kids);
self.children = new_descs;
self.doc = new_doc;
}
}
fn render(node: &Node, document: &Document) -> ViewDesc {
if node.is_text() {
return render_text(node, document);
}
let dom_el = match node.node_type().spec().to_dom {
Some(f) => create_element(document, &f(node)),
None => document
.create_element("span")
.expect("create_element succeeds for `span`"),
};
let mut children = Vec::with_capacity(node.child_count());
for child in node.content().iter() {
let cd = render(child, document);
let _ = dom_el.append_child(&cd.dom_node());
children.push(cd);
}
if children.is_empty() && is_textblock(node) {
append_trailing_break(document, &dom_el);
}
ViewDesc::Element {
node: node.clone(),
dom: dom_el,
children,
}
}
fn render_text(node: &Node, document: &Document) -> ViewDesc {
let text_node = document.create_text_node(node.text().unwrap_or(""));
let mut current: web_sys::Node = text_node.clone().into();
let mut wrapper: Option<Element> = None;
for mark in node.marks() {
let Some(f) = mark.mark_type().spec().to_dom else {
continue;
};
let el = create_element(document, &f(mark));
let _ = el.append_child(¤t);
current = el.clone().into();
wrapper = Some(el);
}
ViewDesc::Text {
node: node.clone(),
text: text_node,
wrapper,
}
}
const TRAILING_BREAK_ATTR: &str = "data-taino-trailing-break";
fn is_textblock(node: &Node) -> bool {
node.node_type().is_block()
&& node
.node_type()
.spec()
.content
.as_deref()
.is_some_and(|c| c.contains("inline") || c.contains("text"))
}
fn append_trailing_break(document: &Document, el: &Element) {
if let Ok(br) = document.create_element("br") {
let _ = br.set_attribute(TRAILING_BREAK_ATTR, "");
let _ = el.append_child(&br);
}
}
fn direct_trailing_break(el: &Element) -> Option<Element> {
let kids = el.child_nodes();
for i in 0..kids.length() {
if let Some(node) = kids.item(i) {
if let Ok(e) = node.dyn_into::<Element>() {
if e.has_attribute(TRAILING_BREAK_ATTR) {
return Some(e);
}
}
}
}
None
}
fn reconcile_trailing_break(document: &Document, el: &Element, empty: bool) {
let existing = direct_trailing_break(el);
match (empty, existing) {
(true, None) => append_trailing_break(document, el),
(false, Some(br)) => {
if let Some(parent) = br.parent_node() {
let _ = parent.remove_child(&br);
}
}
_ => {}
}
}
fn direct_text(el: &Element) -> String {
let kids = el.child_nodes();
let mut s = String::new();
for i in 0..kids.length() {
if let Some(n) = kids.item(i) {
if n.node_type() == web_sys::Node::TEXT_NODE {
if let Some(d) = n.text_content() {
s.push_str(&d);
}
}
}
}
s
}
fn find_empty_block_text(descs: &[ViewDesc], base: usize) -> Option<(usize, String)> {
let mut pos = base;
for d in descs {
match d {
ViewDesc::Text { node, .. } => pos += node.node_size(),
ViewDesc::Element {
node,
dom,
children,
} => {
let content_start = pos + 1;
if children.is_empty() {
if is_textblock(node) {
let txt = direct_text(dom);
if !txt.is_empty() {
return Some((content_start, txt));
}
}
} else if let Some(found) = find_empty_block_text(children, content_start) {
return Some(found);
}
pos += node.node_size();
}
}
}
None
}
fn create_element(document: &Document, spec: &DomSpec) -> Element {
let el = document
.create_element(spec.tag())
.expect("create_element succeeds for spec tag");
for (name, value) in spec.attrs() {
let _ = el.set_attribute(name, value);
}
el
}
fn collect_text_changes(
descs: &[ViewDesc],
base: usize,
visit: &mut dyn FnMut(&ViewDesc, usize),
) -> usize {
let mut pos = base;
for d in descs {
match d {
ViewDesc::Text { node, .. } => {
visit(d, pos);
pos += node.node_size();
}
ViewDesc::Element { node, children, .. } => {
collect_text_changes(children, pos + 1, visit);
pos += node.node_size();
}
}
}
pos
}
fn apply_decoration(children: &[ViewDesc], deco: &Decoration, add: bool) {
match deco {
Decoration::Node { pos, class } => {
if let Some(dom) = dom_element_at(children, 0, *pos) {
let list = dom.class_list();
if add {
let _ = list.add_1(class);
} else {
let _ = list.remove_1(class);
}
}
}
Decoration::Inline { .. } => {}
}
}
fn pos_before_element(children: &[ViewDesc], base: usize, target: &Element) -> Option<usize> {
let mut pos = base;
for c in children {
match c {
ViewDesc::Text { node, .. } => {
pos += node.node_size();
}
ViewDesc::Element {
node,
dom,
children: kids,
} => {
if dom.is_same_node(Some(target.as_ref())) {
return Some(pos);
}
if let Some(p) = pos_before_element(kids, pos + 1, target) {
return Some(p);
}
pos += node.node_size();
}
}
}
None
}
fn dom_element_at(children: &[ViewDesc], base: usize, target: usize) -> Option<&Element> {
let mut pos = base;
for c in children {
match c {
ViewDesc::Text { node, .. } => {
pos += node.node_size();
}
ViewDesc::Element {
node,
dom,
children: kids,
} => {
if pos == target {
return Some(dom);
}
let size = node.node_size();
if target > pos && target < pos + size {
if let Some(e) = dom_element_at(kids, pos + 1, target) {
return Some(e);
}
}
pos += size;
}
}
}
None
}
fn same_markup(a: &Node, b: &Node) -> bool {
a.node_type() == b.node_type() && a.attrs() == b.attrs() && a.marks() == b.marks()
}
fn patch_children(
document: &Document,
parent_dom: &Element,
old: &[ViewDesc],
new: &[Node],
) -> Vec<ViewDesc> {
let mut result = Vec::with_capacity(new.len());
for (i, new_node) in new.iter().enumerate() {
if let Some(old_desc) = old.get(i) {
if let Some(patched) = try_patch(document, old_desc, new_node) {
result.push(patched);
continue;
}
let fresh = render(new_node, document);
let _ = parent_dom.replace_child(&fresh.dom_node(), &old_desc.dom_node());
result.push(fresh);
} else {
let fresh = render(new_node, document);
let _ = parent_dom.append_child(&fresh.dom_node());
result.push(fresh);
}
}
for stale in old.iter().skip(new.len()) {
let _ = parent_dom.remove_child(&stale.dom_node());
}
result
}
fn try_patch(document: &Document, old: &ViewDesc, new: &Node) -> Option<ViewDesc> {
if old.node() == new {
return Some(old.clone());
}
match old {
ViewDesc::Text {
node,
text,
wrapper,
} => {
if !new.is_text() || !same_markup(node, new) {
return None;
}
let val = new.text().unwrap_or("");
if text.data() != val {
text.set_data(val);
}
Some(ViewDesc::Text {
node: new.clone(),
text: text.clone(),
wrapper: wrapper.clone(),
})
}
ViewDesc::Element {
node,
dom,
children,
} => {
if new.is_text() || node.node_type() != new.node_type() || node.attrs() != new.attrs() {
return None;
}
if children.is_empty() {
while let Some(c) = dom.first_child() {
if dom.remove_child(&c).is_err() {
break; }
}
}
let new_kids: Vec<Node> = new.content().iter().cloned().collect();
let new_children = patch_children(document, dom, children, &new_kids);
if is_textblock(new) {
reconcile_trailing_break(document, dom, new_children.is_empty());
}
Some(ViewDesc::Element {
node: new.clone(),
dom: dom.clone(),
children: new_children,
})
}
}
}
fn find_diff(a: &str, b: &str) -> Option<(usize, usize, String)> {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let a_len = a_chars.len();
let b_len = b_chars.len();
let mut prefix_len = 0;
while prefix_len < a_len && prefix_len < b_len && a_chars[prefix_len] == b_chars[prefix_len] {
prefix_len += 1;
}
if prefix_len == a_len && prefix_len == b_len {
return None;
}
let mut suffix_len = 0;
while suffix_len < a_len - prefix_len
&& suffix_len < b_len - prefix_len
&& a_chars[a_len - 1 - suffix_len] == b_chars[b_len - 1 - suffix_len]
{
suffix_len += 1;
}
let old_start = prefix_len;
let old_end = a_len - suffix_len;
let new_end = b_len - suffix_len;
let new_part: String = b_chars[old_start..new_end].iter().collect();
Some((old_start, old_end - old_start, new_part))
}