Styled heading select
This commit is contained in:
parent
9cc34f16bb
commit
04f0620ba4
@ -13,3 +13,8 @@ main {
|
||||
article.inner-layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
strike {
|
||||
display: inline;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
@ -152,6 +152,7 @@ fn render_rte(state: &StyledRteState) -> Node<Msg> {
|
||||
table_tooltip: Some(&state.table_tooltip),
|
||||
code_tooltip: Some(&state.code_tooltip),
|
||||
identifier: Some(state.identifier),
|
||||
heading_state: Some(&state.heading_state),
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use seed::prelude::*;
|
||||
use seed::*;
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
use crate::components::styled_button::{ButtonVariant, StyledButton};
|
||||
use crate::components::styled_icon::{Icon, StyledIcon};
|
||||
@ -80,100 +81,51 @@ pub enum RteMsg {
|
||||
Italic,
|
||||
Underscore,
|
||||
Strikethrough,
|
||||
JustifyFull,
|
||||
JustifyCenter,
|
||||
JustifyLeft,
|
||||
JustifyRight,
|
||||
InsertParagraph,
|
||||
InsertHeading(HeadingSize),
|
||||
InsertUnorderedList,
|
||||
InsertOrderedList,
|
||||
RemoveFormat,
|
||||
Subscript,
|
||||
Superscript,
|
||||
InsertUnorderedList,
|
||||
InsertOrderedList,
|
||||
|
||||
// heading
|
||||
InsertHeading(HeadingSize),
|
||||
|
||||
// table
|
||||
TableSetVisibility(bool),
|
||||
TableSetRows(u16),
|
||||
TableSetColumns(u16),
|
||||
InsertTable { rows: u16, cols: u16 },
|
||||
ChangeIndent(RteIndentMsg),
|
||||
|
||||
// code
|
||||
InsertCode(bool),
|
||||
CodeChanged(String),
|
||||
InjectCode,
|
||||
|
||||
RequestFocus(uuid::Uuid),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExecCommand<'l> {
|
||||
pub(crate) name: &'l str,
|
||||
pub(crate) param: &'l str,
|
||||
}
|
||||
|
||||
impl<'l> ExecCommand<'l> {
|
||||
pub fn new(name: &'l str) -> Self {
|
||||
Self::new_with_param(name, "")
|
||||
}
|
||||
|
||||
pub fn new_with_param(name: &'l str, param: &'l str) -> Self {
|
||||
Self { name, param }
|
||||
}
|
||||
}
|
||||
|
||||
impl RteMsg {
|
||||
pub fn to_command(&self) -> Option<ExecCommand> {
|
||||
match self {
|
||||
RteMsg::Bold => None,
|
||||
RteMsg::Italic => None,
|
||||
RteMsg::Underscore => None,
|
||||
RteMsg::Strikethrough => Some(ExecCommand::new("strikeThrough")),
|
||||
RteMsg::JustifyFull => Some(ExecCommand::new("justifyFull")),
|
||||
RteMsg::JustifyCenter => Some(ExecCommand::new("justifyCenter")),
|
||||
RteMsg::JustifyLeft => Some(ExecCommand::new("justifyLeft")),
|
||||
RteMsg::JustifyRight => Some(ExecCommand::new("justifyRight")),
|
||||
RteMsg::InsertParagraph => Some(ExecCommand::new("insertParagraph")),
|
||||
RteMsg::InsertHeading(heading) => match heading {
|
||||
HeadingSize::H1
|
||||
| HeadingSize::H2
|
||||
| HeadingSize::H3
|
||||
| HeadingSize::H4
|
||||
| HeadingSize::H5
|
||||
| HeadingSize::H6 => None,
|
||||
HeadingSize::Normal => None,
|
||||
},
|
||||
RteMsg::InsertUnorderedList => Some(ExecCommand::new("insertUnorderedList")),
|
||||
RteMsg::InsertOrderedList => Some(ExecCommand::new("insertOrderedList")),
|
||||
RteMsg::RemoveFormat => None,
|
||||
RteMsg::Subscript => None,
|
||||
RteMsg::Superscript => None,
|
||||
RteMsg::InsertTable { .. } => None,
|
||||
// code
|
||||
RteMsg::InsertCode(_) => None,
|
||||
RteMsg::CodeChanged(_) => None,
|
||||
RteMsg::InjectCode => None,
|
||||
fn parse_str(ev: MouseEvent, target: &str, cols: u16, rows: u16) -> Self {
|
||||
match target {
|
||||
"bold" => Self::Bold,
|
||||
"italic" => Self::Italic,
|
||||
"underscore" => Self::Underscore,
|
||||
"strikethrough" => Self::Strikethrough,
|
||||
"listingDots" => Self::InsertUnorderedList,
|
||||
"listingNumber" => Self::InsertOrderedList,
|
||||
"subscript " => Self::Subscript,
|
||||
"superscript" => Self::Superscript,
|
||||
|
||||
// indent
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")),
|
||||
RteMsg::ChangeIndent(RteIndentMsg::Decrease) => Some(ExecCommand::new("outdent")),
|
||||
// "heading" => Self::InsertHeading(),
|
||||
"table" => Self::TableSetVisibility(true),
|
||||
"codeAlt" => Self::InsertCode(true),
|
||||
"closeRteTableTooltip" => Self::TableSetVisibility(false),
|
||||
"rteInsertCode" => Self::InsertCode(false),
|
||||
"rteInjectCode" => Self::InjectCode,
|
||||
"rteInsertTable" => Self::InsertTable { rows, cols },
|
||||
|
||||
// outer
|
||||
RteMsg::TableSetColumns(..)
|
||||
| RteMsg::TableSetRows(..)
|
||||
| RteMsg::TableSetVisibility(..) => None,
|
||||
|
||||
RteMsg::RequestFocus(identifier) => {
|
||||
let res = document().query_selector(format!("#{}", identifier).as_str());
|
||||
if let Ok(Some(el)) = res {
|
||||
if let Ok(el) = el.dyn_into::<web_sys::HtmlElement>() {
|
||||
if let Err(e) = el.focus() {
|
||||
error!(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
_ => {
|
||||
let target = ev.target().unwrap();
|
||||
let h = seed::to_html_el(&target);
|
||||
error!("unknown rte command for element", h);
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -257,8 +209,9 @@ pub struct StyledRteState {
|
||||
pub field_id: FieldId,
|
||||
pub table_tooltip: StyledRteTableState,
|
||||
pub code_tooltip: StyledRteCodeState,
|
||||
range: Option<web_sys::Range>,
|
||||
pub identifier: uuid::Uuid,
|
||||
pub heading_state: StyledSelectState,
|
||||
range: Option<web_sys::Range>,
|
||||
}
|
||||
|
||||
impl StyledRteState {
|
||||
@ -271,121 +224,109 @@ impl StyledRteState {
|
||||
rows: 3,
|
||||
cols: 3,
|
||||
},
|
||||
code_tooltip: StyledRteCodeState::new(field_id),
|
||||
code_tooltip: StyledRteCodeState::new(field_id.clone()),
|
||||
range: None,
|
||||
identifier: uuid::Uuid::new_v4(),
|
||||
heading_state: StyledSelectState::new(field_id, vec![]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
|
||||
self.code_tooltip.lang.update(msg, orders);
|
||||
self.heading_state.update(msg, orders);
|
||||
|
||||
let m = match msg {
|
||||
Msg::Rte(field, m) if field == &self.field_id => m,
|
||||
_ => return,
|
||||
};
|
||||
match m.to_command() {
|
||||
Some(ExecCommand { name, param }) => {
|
||||
self.store_range();
|
||||
|
||||
if let Err(e) =
|
||||
html_document().exec_command_with_show_ui_and_value(name, false, param)
|
||||
{
|
||||
error!(e)
|
||||
match m {
|
||||
RteMsg::Italic => self.wrap_into("I"),
|
||||
RteMsg::Strikethrough => self.wrap_into("STRIKE"),
|
||||
RteMsg::InsertUnorderedList => self.wrap_into_all(&["UL", "LI"]),
|
||||
RteMsg::InsertOrderedList => self.wrap_into_all(&["OL", "LI"]),
|
||||
RteMsg::Subscript => self.wrap_into("SUB"),
|
||||
RteMsg::Superscript => self.wrap_into("SUP"),
|
||||
RteMsg::Bold => self.wrap_into("B"),
|
||||
RteMsg::Underscore => self.wrap_into("U"),
|
||||
RteMsg::InsertHeading(heading) => self.wrap_into(heading.as_str()),
|
||||
// code
|
||||
RteMsg::InsertCode(b) => {
|
||||
if *b {
|
||||
self.store_range();
|
||||
} else {
|
||||
self.code_tooltip.reset();
|
||||
}
|
||||
self.code_tooltip.visible = *b;
|
||||
}
|
||||
RteMsg::CodeChanged(s) => {
|
||||
self.code_tooltip.code = s.to_string();
|
||||
}
|
||||
RteMsg::InjectCode => {
|
||||
let lang = match self
|
||||
.code_tooltip
|
||||
.lang
|
||||
.values
|
||||
.get(0)
|
||||
.and_then(|idx| self.code_tooltip.languages.get(*idx as usize))
|
||||
{
|
||||
Some(v) => v.to_string(),
|
||||
_ => return,
|
||||
};
|
||||
let doc = seed::html_document();
|
||||
let r = match self.range.as_ref() {
|
||||
Some(r) => r,
|
||||
_ => return,
|
||||
};
|
||||
let code = self.code_tooltip.code.to_string();
|
||||
let view = match doc.create_element("bitque-code-view") {
|
||||
Ok(t) => t,
|
||||
_ => return,
|
||||
};
|
||||
if let Err(err) = view.set_attribute("lang", lang.as_str()) {
|
||||
error!(err);
|
||||
}
|
||||
view.set_inner_html(code.as_str());
|
||||
if let Err(e) = r.insert_node(&view) {
|
||||
error!(e);
|
||||
}
|
||||
|
||||
self.code_tooltip.reset();
|
||||
}
|
||||
// table
|
||||
RteMsg::TableSetRows(n) => {
|
||||
self.table_tooltip.rows = *n;
|
||||
}
|
||||
RteMsg::TableSetColumns(n) => {
|
||||
self.table_tooltip.cols = *n;
|
||||
}
|
||||
RteMsg::TableSetVisibility(b) => {
|
||||
if *b {
|
||||
self.store_range();
|
||||
}
|
||||
self.table_tooltip.visible = *b;
|
||||
}
|
||||
RteMsg::InsertTable { rows, cols } => {
|
||||
self.table_tooltip.visible = false;
|
||||
self.table_tooltip.cols = 3;
|
||||
self.table_tooltip.rows = 3;
|
||||
if self.restore_range().is_err() {
|
||||
return;
|
||||
}
|
||||
let doc = seed::html_document();
|
||||
let r = match self.range.as_ref() {
|
||||
Some(r) => r,
|
||||
_ => return,
|
||||
};
|
||||
let table = match doc.create_element("table") {
|
||||
Ok(t) => t,
|
||||
_ => return,
|
||||
};
|
||||
table.set_inner_html(RteTableBodyBuilder::new(*cols, *rows).to_string().as_str());
|
||||
if let Err(e) = r.insert_node(&table) {
|
||||
error!(e);
|
||||
}
|
||||
}
|
||||
_ => match m {
|
||||
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 {
|
||||
self.store_range();
|
||||
} else {
|
||||
self.code_tooltip.reset();
|
||||
}
|
||||
self.code_tooltip.visible = *b;
|
||||
}
|
||||
RteMsg::CodeChanged(s) => {
|
||||
self.code_tooltip.code = s.to_string();
|
||||
}
|
||||
RteMsg::InjectCode => {
|
||||
let lang = match self
|
||||
.code_tooltip
|
||||
.lang
|
||||
.values
|
||||
.get(0)
|
||||
.and_then(|idx| self.code_tooltip.languages.get(*idx as usize))
|
||||
{
|
||||
Some(v) => v.to_string(),
|
||||
_ => return,
|
||||
};
|
||||
let doc = seed::html_document();
|
||||
let r = match self.range.as_ref() {
|
||||
Some(r) => r,
|
||||
_ => return,
|
||||
};
|
||||
let code = self.code_tooltip.code.to_string();
|
||||
let view = match doc.create_element("bitque-code-view") {
|
||||
Ok(t) => t,
|
||||
_ => return,
|
||||
};
|
||||
if let Err(err) = view.set_attribute("lang", lang.as_str()) {
|
||||
error!(err);
|
||||
}
|
||||
view.set_inner_html(code.as_str());
|
||||
if let Err(e) = r.insert_node(&view) {
|
||||
error!(e);
|
||||
}
|
||||
|
||||
self.code_tooltip.reset();
|
||||
|
||||
self.schedule_focus(orders);
|
||||
}
|
||||
// table
|
||||
RteMsg::TableSetRows(n) => {
|
||||
self.table_tooltip.rows = *n;
|
||||
}
|
||||
RteMsg::TableSetColumns(n) => {
|
||||
self.table_tooltip.cols = *n;
|
||||
}
|
||||
RteMsg::TableSetVisibility(b) => {
|
||||
if *b {
|
||||
self.store_range();
|
||||
}
|
||||
self.table_tooltip.visible = *b;
|
||||
}
|
||||
RteMsg::InsertTable { rows, cols } => {
|
||||
self.table_tooltip.visible = false;
|
||||
self.table_tooltip.cols = 3;
|
||||
self.table_tooltip.rows = 3;
|
||||
if self.restore_range().is_err() {
|
||||
return;
|
||||
}
|
||||
let doc = seed::html_document();
|
||||
let r = match self.range.as_ref() {
|
||||
Some(r) => r,
|
||||
_ => return,
|
||||
};
|
||||
let table = match doc.create_element("table") {
|
||||
Ok(t) => t,
|
||||
_ => return,
|
||||
};
|
||||
table.set_inner_html(
|
||||
RteTableBodyBuilder::new(*cols, *rows).to_string().as_str(),
|
||||
);
|
||||
if let Err(e) = r.insert_node(&table) {
|
||||
error!(e);
|
||||
}
|
||||
self.schedule_focus(orders);
|
||||
}
|
||||
_ => error!("unknown rte command {:?}", m),
|
||||
},
|
||||
};
|
||||
// orders.skip().send_msg(Msg::StrInputChanged(
|
||||
// self.field_id.clone(),
|
||||
@ -419,12 +360,111 @@ impl StyledRteState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schedule_focus(&self, orders: &mut impl Orders<Msg>) {
|
||||
let field_id = self.field_id.clone();
|
||||
let identifier = self.identifier;
|
||||
orders.perform_cmd(cmds::timeout(200, move || {
|
||||
Msg::Rte(field_id, RteMsg::RequestFocus(identifier))
|
||||
}));
|
||||
fn wrap_into_all(&self, tags: &[&str]) {
|
||||
for tag in tags {
|
||||
if self.try_wrap_into(tag).is_none() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_into(&self, name: &str) {
|
||||
if self.is_in_rte() != Some(true) {
|
||||
return;
|
||||
}
|
||||
self.try_wrap_into(name);
|
||||
}
|
||||
|
||||
fn try_wrap_into(&self, name: &str) -> Option<()> {
|
||||
let sel = document().get_selection().ok()??;
|
||||
let r = sel.get_range_at(0).ok()?;
|
||||
|
||||
let start = r.start_container();
|
||||
let end = r.end_container();
|
||||
|
||||
if let Some(node) = self.is_wrapped(start.clone().ok(), end.clone().ok(), name) {
|
||||
let el = node.dyn_ref::<web_sys::HtmlElement>()?;
|
||||
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;
|
||||
}
|
||||
r.set_start_before(&prev).ok();
|
||||
sel.collapse_to_end().ok()
|
||||
} else {
|
||||
let offset = r.end_offset().ok()?;
|
||||
let doc = r.extract_contents().ok()?;
|
||||
let el: web_sys::Element = document().create_element(name).unwrap();
|
||||
el.append_child(&doc).ok()?;
|
||||
r.insert_node(&el).ok()?;
|
||||
|
||||
let node = el.dyn_ref::<web_sys::Node>().unwrap();
|
||||
r.set_start_before(node).ok();
|
||||
r.set_end_after(node).ok();
|
||||
|
||||
sel.collapse_with_offset(Some(node), offset).ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_wrapped(
|
||||
&self,
|
||||
start: Option<web_sys::Node>,
|
||||
end: Option<web_sys::Node>,
|
||||
name: &str,
|
||||
) -> Option<web_sys::Element> {
|
||||
let start = start?;
|
||||
let end: web_sys::Node = end?;
|
||||
|
||||
if start == end
|
||||
&& start
|
||||
.dyn_ref::<web_sys::HtmlElement>()
|
||||
.filter(|x| x.tag_name() == name)
|
||||
.is_some()
|
||||
{
|
||||
return Some(
|
||||
start
|
||||
.dyn_into::<web_sys::Element>()
|
||||
.expect("All HTMLElement are by definition Element"),
|
||||
);
|
||||
}
|
||||
let start = start.parent_element()?;
|
||||
let start_parent = start.dyn_ref::<web_sys::HtmlElement>()?;
|
||||
|
||||
let end = end.parent_element()?;
|
||||
let end_parent = end.dyn_ref::<web_sys::HtmlElement>()?;
|
||||
|
||||
if start_parent == end_parent && start_parent.tag_name().as_str() == name {
|
||||
Some(start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_in_rte(&self) -> Option<bool> {
|
||||
let id = self.identifier.to_string();
|
||||
let sel = document().get_selection().ok()??;
|
||||
let r = sel.get_range_at(0).ok()?;
|
||||
let mut current: web_sys::Node = r.start_container().ok()?;
|
||||
while let Some(c) = current.parent_element() {
|
||||
if c.id() == id {
|
||||
return Some(true);
|
||||
}
|
||||
if c.tag_name() == "BODY" {
|
||||
return None;
|
||||
}
|
||||
current = c.dyn_into::<web_sys::Node>().expect("Element must be Node");
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@ -433,6 +473,7 @@ pub struct StyledRte<'component> {
|
||||
pub table_tooltip: Option<&'component StyledRteTableState>,
|
||||
pub identifier: Option<uuid::Uuid>,
|
||||
pub code_tooltip: Option<&'component StyledRteCodeState>,
|
||||
pub heading_state: Option<&'component StyledSelectState>,
|
||||
}
|
||||
|
||||
impl<'component> Default for StyledRte<'component> {
|
||||
@ -442,6 +483,7 @@ impl<'component> Default for StyledRte<'component> {
|
||||
table_tooltip: None,
|
||||
identifier: None,
|
||||
code_tooltip: None,
|
||||
heading_state: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -467,32 +509,7 @@ impl<'outer> StyledRte<'outer> {
|
||||
.map(|el| seed::to_html_el(&el).id())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rte_msg = match target.as_str() {
|
||||
"justifyAll" => RteMsg::JustifyFull,
|
||||
"justifyCenter" => RteMsg::JustifyCenter,
|
||||
"justifyLeft" => RteMsg::JustifyLeft,
|
||||
"justifyRight" => RteMsg::JustifyRight,
|
||||
|
||||
"listingDots" => RteMsg::InsertUnorderedList,
|
||||
"listingNumber" => RteMsg::InsertOrderedList,
|
||||
"table" => RteMsg::TableSetVisibility(true),
|
||||
"paragraph" => RteMsg::InsertParagraph,
|
||||
"codeAlt" => RteMsg::InsertCode(true),
|
||||
"indent" => RteMsg::ChangeIndent(RteIndentMsg::Increase),
|
||||
"outdent" => RteMsg::ChangeIndent(RteIndentMsg::Decrease),
|
||||
|
||||
"closeRteTableTooltip" => RteMsg::TableSetVisibility(false),
|
||||
"rteInsertCode" => RteMsg::InsertCode(false),
|
||||
"rteInjectCode" => RteMsg::InjectCode,
|
||||
"rteInsertTable" => RteMsg::InsertTable { rows, cols },
|
||||
|
||||
_ => {
|
||||
let target = ev.target().unwrap();
|
||||
let h = seed::to_html_el(&target);
|
||||
error!("unknown rte command for element", h);
|
||||
unreachable!();
|
||||
}
|
||||
};
|
||||
let rte_msg = RteMsg::parse_str(ev, &target, rows, cols);
|
||||
Msg::Rte(field_id, rte_msg)
|
||||
})
|
||||
};
|
||||
@ -511,14 +528,13 @@ impl<'outer> StyledRte<'outer> {
|
||||
})
|
||||
};
|
||||
|
||||
let first_row = Self::first_row(click_handler.clone());
|
||||
let second_row = self.second_row(click_handler, change_handler);
|
||||
|
||||
div![
|
||||
C!["styledRte"],
|
||||
attrs![At::Id => id],
|
||||
div![
|
||||
C!["bar"],
|
||||
Self::first_row(click_handler.clone()),
|
||||
self.second_row(click_handler, change_handler),
|
||||
],
|
||||
div![C!["bar"], first_row, second_row],
|
||||
div![
|
||||
C!["editorWrapper"],
|
||||
div![
|
||||
@ -564,14 +580,6 @@ impl<'outer> StyledRte<'outer> {
|
||||
]
|
||||
};
|
||||
|
||||
let system = {
|
||||
let redo_button =
|
||||
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());
|
||||
div![C!["group system"], undo_button, redo_button,]
|
||||
};
|
||||
|
||||
let formatting = {
|
||||
let bold_button =
|
||||
Self::styled_rte_button("Bold", ButtonId::Bold, Icon::Bold, click_handler.clone());
|
||||
@ -621,7 +629,7 @@ impl<'outer> StyledRte<'outer> {
|
||||
]
|
||||
};
|
||||
|
||||
div![C!["row firstRow"], system, formatting, justify]
|
||||
div![C!["row firstRow"], formatting, justify]
|
||||
}
|
||||
|
||||
fn second_row(
|
||||
@ -629,46 +637,7 @@ impl<'outer> StyledRte<'outer> {
|
||||
click_handler: EventHandler<Msg>,
|
||||
change_handler: EventHandler<Msg>,
|
||||
) -> Node<Msg> {
|
||||
let font_group = {
|
||||
let options: Vec<Node<Msg>> = HeadingSize::all()
|
||||
.iter()
|
||||
.map(|h| {
|
||||
let field_id = self.field_id.clone();
|
||||
let button = StyledButton {
|
||||
text: Some(h.as_str()),
|
||||
on_click: Some(mouse_ev(Ev::Click, move |ev| {
|
||||
ev.prevent_default();
|
||||
Some(Msg::Rte(field_id, RteMsg::InsertHeading(*h)))
|
||||
})),
|
||||
variant: ButtonVariant::Empty,
|
||||
..Default::default()
|
||||
}
|
||||
.render();
|
||||
option![
|
||||
attrs!["value" => h.as_u32(), "title" => "Text styles"],
|
||||
C!["headingOption"],
|
||||
button
|
||||
]
|
||||
})
|
||||
.collect();
|
||||
|
||||
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::<u32>().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 font_group = self.font_styles();
|
||||
|
||||
let insert_group = {
|
||||
let table_tooltip = self.table_tooltip(click_handler.clone(), change_handler);
|
||||
@ -710,24 +679,49 @@ impl<'outer> StyledRte<'outer> {
|
||||
]
|
||||
};
|
||||
|
||||
let indent_outdent = {
|
||||
let indent_button = Self::styled_rte_button(
|
||||
"Indent",
|
||||
ButtonId::Indent,
|
||||
Icon::Indent,
|
||||
click_handler.clone(),
|
||||
);
|
||||
let outdent_button =
|
||||
Self::styled_rte_button("Outdent", ButtonId::Outdent, Icon::Outdent, click_handler);
|
||||
div![C!["group indentOutdent"], indent_button, outdent_button]
|
||||
};
|
||||
div![C!["row secondRow"], font_group, insert_group,]
|
||||
}
|
||||
|
||||
div![
|
||||
C!["row secondRow"],
|
||||
font_group,
|
||||
insert_group,
|
||||
indent_outdent
|
||||
]
|
||||
fn font_styles(&self) -> Node<Msg> {
|
||||
let state = self.heading_state.as_ref().unwrap();
|
||||
let selected = if let Some(n) = state.values.get(0) {
|
||||
HeadingSize::from_u32(*n)
|
||||
} else {
|
||||
HeadingSize::Normal
|
||||
};
|
||||
let selected = vec![StyledSelectOption {
|
||||
name: Some(selected.as_str()),
|
||||
icon: None,
|
||||
text: Some(selected.as_str()),
|
||||
value: selected.as_u32(),
|
||||
class_list: "",
|
||||
variant: SelectVariant::Normal,
|
||||
}];
|
||||
|
||||
let options = Some(HeadingSize::all().iter().map(|h| StyledSelectOption {
|
||||
name: Some(h.as_str()),
|
||||
icon: None,
|
||||
text: Some(h.as_str()),
|
||||
value: h.as_u32(),
|
||||
class_list: "",
|
||||
variant: SelectVariant::Normal,
|
||||
}));
|
||||
|
||||
let font = StyledSelect {
|
||||
id: self.field_id.clone(),
|
||||
variant: SelectVariant::Normal,
|
||||
dropdown_width: Some(96),
|
||||
name: "",
|
||||
valid: true,
|
||||
is_multi: false,
|
||||
options,
|
||||
selected,
|
||||
text_filter: state.text_filter.as_str(),
|
||||
opened: state.opened,
|
||||
clearable: false,
|
||||
}
|
||||
.render();
|
||||
div![C!["group font"], font]
|
||||
}
|
||||
|
||||
fn table_tooltip(
|
||||
@ -780,43 +774,58 @@ impl<'outer> StyledRte<'outer> {
|
||||
visible,
|
||||
children: vec![
|
||||
h2![span!["Add table"], close_table_tooltip],
|
||||
div![C!["inputs"], span!["Rows"], seed::input![
|
||||
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => rows],
|
||||
on_rows_change
|
||||
]],
|
||||
div![
|
||||
C!["inputs"],
|
||||
span!["Columns"],
|
||||
seed::input![
|
||||
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => cols],
|
||||
on_cols_change
|
||||
]
|
||||
],
|
||||
C!["inputs"],
|
||||
span!["Rows"],
|
||||
seed::input![
|
||||
attrs![
|
||||
At::Type => "range";
|
||||
At::Step => "1";
|
||||
At::Min => "1";
|
||||
At::Max => "10";
|
||||
At::Value => rows
|
||||
],
|
||||
on_rows_change
|
||||
]
|
||||
],
|
||||
div![
|
||||
C!["inputs"],
|
||||
span!["Columns"],
|
||||
seed::input![
|
||||
attrs![
|
||||
At::Type => "range";
|
||||
At::Step => "1";
|
||||
At::Min => "1";
|
||||
At::Max => "10";
|
||||
At::Value => cols
|
||||
],
|
||||
on_cols_change
|
||||
]
|
||||
],
|
||||
{
|
||||
let body: Vec<Node<Msg>> = (0..rows)
|
||||
.map(|_row| {
|
||||
let tds: Vec<Node<Msg>> = (0..cols)
|
||||
.map(|_col| td![" "])
|
||||
.collect();
|
||||
let tds: Vec<Node<Msg>> = (0..cols).map(|_col| td![" "]).collect();
|
||||
tr![tds]
|
||||
})
|
||||
.collect();
|
||||
seed::div![
|
||||
C!["tablePreview"],
|
||||
seed::table![tbody![body]],
|
||||
input![
|
||||
attrs![
|
||||
At::Type => "button";
|
||||
At::Id => "rteInsertTable";
|
||||
At::Value => "Insert"
|
||||
],
|
||||
on_submit
|
||||
],
|
||||
]
|
||||
}
|
||||
C!["tablePreview"],
|
||||
seed::table![tbody![body]],
|
||||
input![
|
||||
attrs![
|
||||
At::Type => "button";
|
||||
At::Id => "rteInsertTable";
|
||||
At::Value => "Insert"
|
||||
],
|
||||
on_submit
|
||||
],
|
||||
]
|
||||
},
|
||||
],
|
||||
class_list: "",
|
||||
}.render()
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
fn code_tooltip(&self, click_handler: EventHandler<Msg>) -> Node<Msg> {
|
||||
@ -929,76 +938,3 @@ impl<'outer> StyledRte<'outer> {
|
||||
span![C!["styledRteButton"], attrs![At::Title => title], button]
|
||||
}
|
||||
}
|
||||
|
||||
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 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::<web_sys::HtmlElement>()?;
|
||||
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::<web_sys::Node>().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<web_sys::Node>,
|
||||
end: Option<web_sys::Node>,
|
||||
name: &str,
|
||||
) -> Option<web_sys::Element> {
|
||||
let start = start?;
|
||||
let end: web_sys::Node = end?;
|
||||
|
||||
if start == end
|
||||
&& start
|
||||
.dyn_ref::<web_sys::HtmlElement>()
|
||||
.filter(|x| x.tag_name() == name)
|
||||
.is_some()
|
||||
{
|
||||
return Some(
|
||||
start
|
||||
.dyn_into::<web_sys::Element>()
|
||||
.expect("All HTMLElement are by definition Element"),
|
||||
);
|
||||
}
|
||||
let start = start.parent_element()?;
|
||||
let start_parent = start.dyn_ref::<web_sys::HtmlElement>()?;
|
||||
|
||||
let end = end.parent_element()?;
|
||||
let end_parent = end.dyn_ref::<web_sys::HtmlElement>()?;
|
||||
|
||||
if start_parent == end_parent && start_parent.tag_name().as_str() == name {
|
||||
Some(start)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ use seed::*;
|
||||
|
||||
use crate::components::styled_icon::{Icon, StyledIcon};
|
||||
use crate::components::styled_select_child::*;
|
||||
use crate::pages::project_page::events::EvHandler;
|
||||
use crate::{FieldId, Msg};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@ -192,33 +193,21 @@ where
|
||||
}
|
||||
.render()
|
||||
} else {
|
||||
empty![]
|
||||
Node::Empty
|
||||
};
|
||||
|
||||
let skip = {
|
||||
let len = selected.len();
|
||||
selected
|
||||
.iter()
|
||||
.fold(HashMap::with_capacity(len), |mut h, o| {
|
||||
h.insert(o.value, true);
|
||||
h
|
||||
})
|
||||
};
|
||||
let children: Vec<Node<Msg>> = if let Some(options) = options {
|
||||
options
|
||||
.filter(|o| !skip.contains_key(&o.value) && o.match_text(text_filter))
|
||||
.map(|child| {
|
||||
let on_change = super::events::on_click_change_select_selected(
|
||||
id.clone(),
|
||||
Some(child.value()),
|
||||
);
|
||||
let node = child.render_option();
|
||||
div![C!["option"], on_change, on_handler.clone(), node]
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let skip = selected
|
||||
.iter()
|
||||
.fold(HashMap::with_capacity(selected.len()), |mut h, o| {
|
||||
h.insert(o.value, true);
|
||||
h
|
||||
});
|
||||
|
||||
let children: Vec<Node<Msg>> =
|
||||
Self::render_values(&id, options, text_filter, &on_handler, skip);
|
||||
|
||||
let value_container_content =
|
||||
Self::value_container_content(id, is_multi, selected, &children);
|
||||
|
||||
seed::div![
|
||||
C!["styledSelect", variant.to_str(), IF![!valid => "invalid"]],
|
||||
@ -227,21 +216,7 @@ where
|
||||
div![
|
||||
C!["valueContainer", variant.to_str()],
|
||||
on_handler,
|
||||
match is_multi {
|
||||
true => vec![div![
|
||||
C!["valueMulti"],
|
||||
selected
|
||||
.into_iter()
|
||||
.map(|m| Self::multi_value(m, id.clone()))
|
||||
.collect::<Vec<Node<Msg>>>(),
|
||||
IF![children.is_empty() => div![C!["placeholder"], "Select"]],
|
||||
IF![!children.is_empty() => div![C!["addMore"], StyledIcon::from(Icon::Plus).render(), "Add more"]],
|
||||
]],
|
||||
false => selected
|
||||
.into_iter()
|
||||
.map(|m| m.render_value())
|
||||
.collect::<Vec<Node<Msg>>>(),
|
||||
},
|
||||
value_container_content,
|
||||
action_icon,
|
||||
],
|
||||
div![
|
||||
@ -266,6 +241,52 @@ where
|
||||
]
|
||||
}
|
||||
|
||||
fn value_container_content(
|
||||
id: FieldId,
|
||||
is_multi: bool,
|
||||
selected: Vec<StyledSelectOption>,
|
||||
children: &Vec<Node<Msg>>,
|
||||
) -> Vec<Node<Msg>> {
|
||||
if !is_multi {
|
||||
return selected
|
||||
.into_iter()
|
||||
.map(|m| m.render_value())
|
||||
.collect::<Vec<Node<Msg>>>();
|
||||
}
|
||||
|
||||
vec![div![
|
||||
C!["valueMulti"],
|
||||
selected
|
||||
.into_iter()
|
||||
.map(|m| Self::multi_value(m, id.clone()))
|
||||
.collect::<Vec<Node<Msg>>>(),
|
||||
IF![children.is_empty() => div![C!["placeholder"], "Select"]],
|
||||
IF![!children.is_empty() => div![C!["addMore"], StyledIcon::from(Icon::Plus).render(), "Add more"]],
|
||||
]]
|
||||
}
|
||||
|
||||
fn render_values(
|
||||
id: &FieldId,
|
||||
options: Option<Options>,
|
||||
text_filter: &str,
|
||||
on_handler: &EvHandler,
|
||||
skip: HashMap<u32, bool>,
|
||||
) -> Vec<Node<Msg>> {
|
||||
let Some(options) = options else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
options
|
||||
.filter(|o| !skip.contains_key(&o.value) && o.match_text(text_filter))
|
||||
.map(|child| {
|
||||
let on_change =
|
||||
super::events::on_click_change_select_selected(id.clone(), Some(child.value()));
|
||||
let node = child.render_option();
|
||||
div![C!["option"], on_change, on_handler.clone(), node]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn multi_value(child: StyledSelectOption, id: FieldId) -> Node<Msg> {
|
||||
let handler = super::events::on_click_change_select_remove_multi(id, child.value());
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user