diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index a8bc9b90..f37a8afd4 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -60,6 +60,7 @@ features = [ "FormData", "FileReader", "FileReaderSync", + "DocumentFragment", "Range", # events "EventTarget", diff --git a/crates/web/src/components/styled_rte.rs b/crates/web/src/components/styled_rte.rs index b621eb88..fc510dd7 100644 --- a/crates/web/src/components/styled_rte.rs +++ b/crates/web/src/components/styled_rte.rs @@ -1,6 +1,5 @@ use seed::prelude::*; use seed::*; -use tracing::{debug, info}; use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_icon::{Icon, StyledIcon}; @@ -42,6 +41,31 @@ impl HeadingSize { H6 => "H6", } } + + fn as_u32(&self) -> u32 { + match self { + HeadingSize::Normal => 0, + HeadingSize::H1 => 1, + HeadingSize::H2 => 2, + HeadingSize::H3 => 3, + HeadingSize::H4 => 4, + HeadingSize::H5 => 5, + HeadingSize::H6 => 6, + } + } + + fn from_u32(n: u32) -> Self { + match n { + 0 => HeadingSize::Normal, + 1 => HeadingSize::H1, + 2 => HeadingSize::H2, + 3 => HeadingSize::H3, + 4 => HeadingSize::H4, + 5 => HeadingSize::H5, + 6 => HeadingSize::H6, + _ => HeadingSize::Normal, + } + } } #[derive(Debug)] @@ -55,12 +79,7 @@ pub enum RteMsg { Bold, Italic, Underscore, - Undo, - Redo, Strikethrough, - Copy, - Paste, - Cut, JustifyFull, JustifyCenter, JustifyLeft, @@ -107,15 +126,10 @@ impl<'l> ExecCommand<'l> { impl RteMsg { pub fn to_command(&self) -> Option { match self { - RteMsg::Bold => Some(ExecCommand::new("bold")), - RteMsg::Italic => Some(ExecCommand::new("italic")), - RteMsg::Underscore => Some(ExecCommand::new("underline")), - RteMsg::Undo => Some(ExecCommand::new("undo")), - RteMsg::Redo => Some(ExecCommand::new("redo")), + RteMsg::Bold => None, + RteMsg::Italic => None, + RteMsg::Underscore => None, RteMsg::Strikethrough => Some(ExecCommand::new("strikeThrough")), - RteMsg::Copy => Some(ExecCommand::new("copy")), - RteMsg::Paste => Some(ExecCommand::new("paste")), - RteMsg::Cut => Some(ExecCommand::new("cut")), RteMsg::JustifyFull => Some(ExecCommand::new("justifyFull")), RteMsg::JustifyCenter => Some(ExecCommand::new("justifyCenter")), RteMsg::JustifyLeft => Some(ExecCommand::new("justifyLeft")), @@ -127,17 +141,14 @@ impl RteMsg { | HeadingSize::H3 | HeadingSize::H4 | HeadingSize::H5 - | HeadingSize::H6 => { - // Some(ExecCommand::new_with_param("heading", heading.as_str())) - None - } - HeadingSize::Normal => Some(ExecCommand::new_with_param("formatBlock", "div")), + | HeadingSize::H6 => None, + HeadingSize::Normal => None, }, RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")), RteMsg::InsertOrderedList => Some(ExecCommand::new("insertOrderedList")), - RteMsg::RemoveFormat => Some(ExecCommand::new("removeFormat")), - RteMsg::Subscript => Some(ExecCommand::new("subscript")), - RteMsg::Superscript => Some(ExecCommand::new("superscript")), + RteMsg::RemoveFormat => None, + RteMsg::Subscript => None, + RteMsg::Superscript => None, RteMsg::InsertTable { .. } => None, // code RteMsg::InsertCode(_) => None, @@ -284,12 +295,13 @@ impl StyledRteState { if self.restore_range().is_err() { return; } - self.schedule_focus(orders); } _ => match m { - RteMsg::InsertHeading(heading) => { - wrap_into(heading.as_str()); - } + RteMsg::Subscript => wrap_into("SUB"), + RteMsg::Superscript => wrap_into("SUP"), + RteMsg::Bold => wrap_into("B"), + RteMsg::Underscore => wrap_into("U"), + RteMsg::InsertHeading(heading) => wrap_into(heading.as_str()), // code RteMsg::InsertCode(b) => { if *b { @@ -394,7 +406,7 @@ impl StyledRteState { let sel = sel .unwrap_or(None) .ok_or_else(|| "Restoring selection failed. Unable to obtain select".to_string())?; - sel.collapse_to_start(); + sel.collapse_to_end().ok(); let r = self .range .as_ref() @@ -436,89 +448,6 @@ impl<'component> Default for StyledRte<'component> { impl<'outer> StyledRte<'outer> { pub fn render(self) -> Node { - /*{ - let _brush_button = styled_rte_button( - "Brush", - Icon::Brush, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _color_bucket_button = styled_rte_button( - "Color bucket", - Icon::ColorBucket, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _color_picker_button = styled_rte_button( - "Color picker", - Icon::ColorPicker, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - - let _link_broken_button = styled_rte_button( - "Link broken", - Icon::LinkBroken, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - - let _pin_button = styled_rte_button( - "Pin", - Icon::Pin, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _rotation_button = styled_rte_button( - "Rotation", - Icon::Rotation, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _save_button = styled_rte_button( - "Save", - Icon::Save, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _text_height_button = styled_rte_button( - "Text height", - Icon::TextHeight, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _text_width_button = styled_rte_button( - "Text width", - Icon::TextWidth, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - }*/ - - // let field_id = values.field_id.clone(); - // let capture_change = ev(Ev::Input, |ev| { - // ev.stop_propagation(); - // Some(Msg::StrInputChanged(field_id, "".to_string())) - // }); - let id = self.identifier.unwrap_or_default().to_string(); let click_handler = { @@ -543,18 +472,7 @@ impl<'outer> StyledRte<'outer> { "justifyCenter" => RteMsg::JustifyCenter, "justifyLeft" => RteMsg::JustifyLeft, "justifyRight" => RteMsg::JustifyRight, - "undo" => RteMsg::Undo, - "redo" => RteMsg::Redo, - "removeFormat" => RteMsg::RemoveFormat, - "bold" => RteMsg::Bold, - "italic " => RteMsg::Italic, - "underscore" => RteMsg::Underscore, - "strikethrough" => RteMsg::Strikethrough, - "subscript" => RteMsg::Subscript, - "superscript" => RteMsg::Superscript, - - // "font" => RteMsg::, // Some(RteMsg::Font), "listingDots" => RteMsg::InsertUnorderedList, "listingNumber" => RteMsg::InsertOrderedList, "table" => RteMsg::TableSetVisibility(true), @@ -600,21 +518,12 @@ impl<'outer> StyledRte<'outer> { C!["bar"], Self::first_row(click_handler.clone()), self.second_row(click_handler, change_handler), - /* brush_button, - * color_bucket_button, - * color_picker_button, - * link_broken_button, - * pin_button, - * save_button, - * text_height_button, - * text_width_button, */ ], div![ C!["editorWrapper"], div![ C!["editor", self.field_id.to_str()], attrs![At::ContentEditable => true], - // capture_change ], ] ] @@ -660,50 +569,10 @@ impl<'outer> StyledRte<'outer> { Self::styled_rte_button("Redo", ButtonId::Redo, Icon::Redo, click_handler.clone()); let undo_button = Self::styled_rte_button("Undo", ButtonId::Undo, Icon::Undo, click_handler.clone()); - /*let field_id = values.field_id.clone(); - let clip_board_button = styled_rte_button( - "Paste", - Icon::ClipBoard, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Paste, field_id)) - }), - ); - let field_id = values.field_id.clone(); - let copy_button = styled_rte_button( - "Copy", - Icon::Copy, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Copy, field_id)) - }), - ); - let field_id = values.field_id.clone(); - let cut_button = styled_rte_button( - "Cut", - Icon::Cut, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Cut, field_id)) - }), - );*/ - div![ - C!["group system"], - // clip_board_button, - // copy_button, - // cut_button, - undo_button, - redo_button, - ] + div![C!["group system"], undo_button, redo_button,] }; let formatting = { - let remove_formatting = Self::styled_rte_button( - "Remove format", - ButtonId::RemoveFormat, - Icon::EraserAlt, - click_handler.clone(), - ); let bold_button = Self::styled_rte_button("Bold", ButtonId::Bold, Icon::Bold, click_handler.clone()); let italic_button = Self::styled_rte_button( @@ -749,7 +618,6 @@ impl<'outer> StyledRte<'outer> { strike_through_button, subscript_button, superscript_button, - remove_formatting, ] }; @@ -761,45 +629,7 @@ impl<'outer> StyledRte<'outer> { click_handler: EventHandler, change_handler: EventHandler, ) -> Node { - /*let align_group = { - let field_id = values.field_id.clone(); - let align_center_button = styled_rte_button( - "Align Center", - Icon::AlignCenter, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let field_id = values.field_id.clone(); - let align_left_button = styled_rte_button( - "Align Left", - Icon::AlignLeft, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let field_id = values.field_id.clone(); - let align_right_button = styled_rte_button( - "Align Right", - Icon::AlignRight, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - div![ - C!["group align"], - align_center_button, - align_left_button, - align_right_button, - ] - };*/ - let font_group = { - let _font_button = - Self::styled_rte_button("Font", ButtonId::Font, Icon::Font, click_handler.clone()); let options: Vec> = HeadingSize::all() .iter() .map(|h| { @@ -814,36 +644,30 @@ impl<'outer> StyledRte<'outer> { ..Default::default() } .render(); - span![C!["headingOption"], button] + option![ + attrs!["value" => h.as_u32(), "title" => "Text styles"], + C!["headingOption"], + button + ] }) .collect(); - let heading_button = span![C!["headingList"], options]; - /*let _field_id = values.field_id.clone(); - let _small_cap_button = styled_rte_button( - "Small Cap", - Icon::SmallCap, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - ); - let _field_id = values.field_id.clone(); - let _all_caps_button = styled_rte_button( - "All caps", - Icon::AllCaps, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - );*/ - div![ - C!["group font"], - // font_button, - heading_button, - /* small_cap_button, - * all_caps_button */ - ] + let field_id = self.field_id.clone(); + let on_change = ev("change", |ev| { + let target = ev.target().expect("is selected"); + let select = seed::to_select(&target); + let value: String = select.value(); + + let value = value.parse::().ok().unwrap_or_default(); + Msg::Rte( + field_id, + RteMsg::InsertHeading(HeadingSize::from_u32(value)), + ) + }); + + let heading_button = select![C!["headingList"], options, on_change,]; + + div![C!["group font"], heading_button] }; let insert_group = { @@ -861,16 +685,6 @@ impl<'outer> StyledRte<'outer> { Icon::ListingNumber, click_handler.clone(), ); - /*let field_id = values.field_id.clone(); - let sub_listing_button = styled_rte_button( - "Sub Listing", - Icon::SubListing, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - None as Option - }), - );*/ - let mut table_button = Self::styled_rte_button( "Table", ButtonId::Table, @@ -879,12 +693,6 @@ impl<'outer> StyledRte<'outer> { ); table_button.add_child(table_tooltip); - let paragraph_button = Self::styled_rte_button( - "Paragraph", - ButtonId::Paragraph, - Icon::Paragraph, - click_handler.clone(), - ); let mut code_alt_button = Self::styled_rte_button( "Insert code", ButtonId::CodeAlt, @@ -895,12 +703,10 @@ impl<'outer> StyledRte<'outer> { div![ C!["group insert"], - paragraph_button, table_button, code_alt_button, listing_dots, listing_number, - // sub_listing_button, ] }; @@ -919,7 +725,6 @@ impl<'outer> StyledRte<'outer> { div![ C!["row secondRow"], font_group, - // align_group, insert_group, indent_outdent ] @@ -1125,14 +930,75 @@ impl<'outer> StyledRte<'outer> { } } -fn wrap_into(name: &str) -> Option<()> { +fn wrap_into(name: &str) { + try_wrap_into(name); +} + +fn try_wrap_into(name: &str) -> Option<()> { let sel = document().get_selection().ok()??; let r = sel.get_range_at(0).ok()?; - let el: web_sys::Element = document().create_element(name).unwrap(); - let node = el.dyn_ref::().unwrap(); - if let Err(e) = r.surround_contents(&node) { - error!("{}", e); + let start = r.start_container(); + let end = r.end_container(); + + if let Some(node) = is_wrapped(start.clone().ok(), end.clone().ok(), name) { + let el = node.dyn_ref::()?; + let parent = el.parent_element()?; + let children = el.child_nodes(); + let mut len: u32 = children.length(); + + len = len.checked_sub(1)?; + let last = children.item(len)?; + parent.replace_child(&last, &node).ok(); + r.set_end_after(&last).ok(); + let mut prev = last; + while len > 0 { + len -= 1; + let current = children.item(len)?; + parent.replace_child(¤t, &prev).ok(); + prev = current; + } + sel.collapse_to_end().ok() + } else { + let el: web_sys::Element = document().create_element(name).unwrap(); + let node = el.dyn_ref::().unwrap(); + if let Err(e) = r.surround_contents(&node) { + error!("{}", e); + } + r.set_end_after(node).ok(); + sel.collapse_to_end().ok() + } +} + +fn is_wrapped( + start: Option, + end: Option, + name: &str, +) -> Option { + let start = start?; + let end: web_sys::Node = end?; + + if start == end + && start + .dyn_ref::() + .filter(|x| x.tag_name() == name) + .is_some() + { + return Some( + start + .dyn_into::() + .expect("All HTMLElement are by definition Element"), + ); + } + let start = start.parent_element()?; + let start_parent = start.dyn_ref::()?; + + let end = end.parent_element()?; + let end_parent = end.dyn_ref::()?; + + if start_parent == end_parent && start_parent.tag_name().as_str() == name { + Some(start) + } else { + None } - sel.collapse(Some(node)).ok() }