use std::cell::Cell;
use taino_edit_core::{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};
#[derive(Debug)]
pub struct EditorView {
root: Element,
schema: Schema,
doc: Node,
children: Vec<ViewDesc>,
composing: Cell<bool>,
decorations: Vec<Decoration>,
}
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() {
let _ = root.remove_child(&child);
}
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(),
}
}
pub fn set_decorations(&mut self, decorations: Vec<Decoration>) {
for d in self.decorations.clone() {
apply_decoration(&self.children, &d, false);
}
for d in &decorations {
apply_decoration(&self.children, d, true);
}
self.decorations = decorations;
}
pub fn decorations(&self) -> &[Decoration] {
&self.decorations
}
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::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 {
found = Some((doc_pos, doc_text.chars().count(), dom_data, node.clone()));
}
}
});
let (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()?;
Some(transform)
}
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::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);
}
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,
}
}
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(ViewDesc::Element { dom, .. }) = find_block_at(children, *pos) {
let list = dom.class_list();
if add {
let _ = list.add_1(class);
} else {
let _ = list.remove_1(class);
}
}
}
}
}
fn find_block_at(children: &[ViewDesc], pos: usize) -> Option<&ViewDesc> {
let mut cur = 0;
for c in children {
if pos == cur {
return Some(c);
}
cur += c.node().node_size();
if pos < cur {
return None;
}
}
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;
}
text.set_data(new.text().unwrap_or(""));
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;
}
let new_kids: Vec<Node> = new.content().iter().cloned().collect();
let new_children = patch_children(document, dom, children, &new_kids);
Some(ViewDesc::Element {
node: new.clone(),
dom: dom.clone(),
children: new_children,
})
}
}
}