Styled heading select

This commit is contained in:
Adrian Woźniak 2023-04-07 14:01:46 +02:00
parent 9cc34f16bb
commit 04f0620ba4
4 changed files with 381 additions and 418 deletions

View File

@ -13,3 +13,8 @@ main {
article.inner-layout { article.inner-layout {
width: 100%; width: 100%;
} }
strike {
display: inline;
text-decoration: line-through;
}

View File

@ -152,6 +152,7 @@ fn render_rte(state: &StyledRteState) -> Node<Msg> {
table_tooltip: Some(&state.table_tooltip), table_tooltip: Some(&state.table_tooltip),
code_tooltip: Some(&state.code_tooltip), code_tooltip: Some(&state.code_tooltip),
identifier: Some(state.identifier), identifier: Some(state.identifier),
heading_state: Some(&state.heading_state),
} }
.render() .render()
} }

View File

@ -1,5 +1,6 @@
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
use web_sys::MouseEvent;
use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
@ -80,100 +81,51 @@ pub enum RteMsg {
Italic, Italic,
Underscore, Underscore,
Strikethrough, Strikethrough,
JustifyFull,
JustifyCenter,
JustifyLeft,
JustifyRight,
InsertParagraph,
InsertHeading(HeadingSize),
InsertUnorderedList,
InsertOrderedList,
RemoveFormat,
Subscript, Subscript,
Superscript, Superscript,
InsertUnorderedList,
InsertOrderedList,
// heading
InsertHeading(HeadingSize),
// table // table
TableSetVisibility(bool), TableSetVisibility(bool),
TableSetRows(u16), TableSetRows(u16),
TableSetColumns(u16), TableSetColumns(u16),
InsertTable { rows: u16, cols: u16 }, InsertTable { rows: u16, cols: u16 },
ChangeIndent(RteIndentMsg),
// code // code
InsertCode(bool), InsertCode(bool),
CodeChanged(String), CodeChanged(String),
InjectCode, 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 { impl RteMsg {
pub fn to_command(&self) -> Option<ExecCommand> { fn parse_str(ev: MouseEvent, target: &str, cols: u16, rows: u16) -> Self {
match self { match target {
RteMsg::Bold => None, "bold" => Self::Bold,
RteMsg::Italic => None, "italic" => Self::Italic,
RteMsg::Underscore => None, "underscore" => Self::Underscore,
RteMsg::Strikethrough => Some(ExecCommand::new("strikeThrough")), "strikethrough" => Self::Strikethrough,
RteMsg::JustifyFull => Some(ExecCommand::new("justifyFull")), "listingDots" => Self::InsertUnorderedList,
RteMsg::JustifyCenter => Some(ExecCommand::new("justifyCenter")), "listingNumber" => Self::InsertOrderedList,
RteMsg::JustifyLeft => Some(ExecCommand::new("justifyLeft")), "subscript " => Self::Subscript,
RteMsg::JustifyRight => Some(ExecCommand::new("justifyRight")), "superscript" => Self::Superscript,
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,
// indent // "heading" => Self::InsertHeading(),
RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")), "table" => Self::TableSetVisibility(true),
RteMsg::ChangeIndent(RteIndentMsg::Decrease) => Some(ExecCommand::new("outdent")), "codeAlt" => Self::InsertCode(true),
"closeRteTableTooltip" => Self::TableSetVisibility(false),
"rteInsertCode" => Self::InsertCode(false),
"rteInjectCode" => Self::InjectCode,
"rteInsertTable" => Self::InsertTable { rows, cols },
// outer _ => {
RteMsg::TableSetColumns(..) let target = ev.target().unwrap();
| RteMsg::TableSetRows(..) let h = seed::to_html_el(&target);
| RteMsg::TableSetVisibility(..) => None, error!("unknown rte command for element", h);
unreachable!();
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
} }
} }
} }
@ -257,8 +209,9 @@ pub struct StyledRteState {
pub field_id: FieldId, pub field_id: FieldId,
pub table_tooltip: StyledRteTableState, pub table_tooltip: StyledRteTableState,
pub code_tooltip: StyledRteCodeState, pub code_tooltip: StyledRteCodeState,
range: Option<web_sys::Range>,
pub identifier: uuid::Uuid, pub identifier: uuid::Uuid,
pub heading_state: StyledSelectState,
range: Option<web_sys::Range>,
} }
impl StyledRteState { impl StyledRteState {
@ -271,121 +224,109 @@ impl StyledRteState {
rows: 3, rows: 3,
cols: 3, cols: 3,
}, },
code_tooltip: StyledRteCodeState::new(field_id), code_tooltip: StyledRteCodeState::new(field_id.clone()),
range: None, range: None,
identifier: uuid::Uuid::new_v4(), identifier: uuid::Uuid::new_v4(),
heading_state: StyledSelectState::new(field_id, vec![]),
} }
} }
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.code_tooltip.lang.update(msg, orders); self.code_tooltip.lang.update(msg, orders);
self.heading_state.update(msg, orders);
let m = match msg { let m = match msg {
Msg::Rte(field, m) if field == &self.field_id => m, Msg::Rte(field, m) if field == &self.field_id => m,
_ => return, _ => return,
}; };
match m.to_command() {
Some(ExecCommand { name, param }) => {
self.store_range();
if let Err(e) = match m {
html_document().exec_command_with_show_ui_and_value(name, false, param) RteMsg::Italic => self.wrap_into("I"),
{ RteMsg::Strikethrough => self.wrap_into("STRIKE"),
error!(e) 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() { if self.restore_range().is_err() {
return; 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( // orders.skip().send_msg(Msg::StrInputChanged(
// self.field_id.clone(), // self.field_id.clone(),
@ -419,12 +360,111 @@ impl StyledRteState {
Ok(()) Ok(())
} }
fn schedule_focus(&self, orders: &mut impl Orders<Msg>) { fn wrap_into_all(&self, tags: &[&str]) {
let field_id = self.field_id.clone(); for tag in tags {
let identifier = self.identifier; if self.try_wrap_into(tag).is_none() {
orders.perform_cmd(cmds::timeout(200, move || { return;
Msg::Rte(field_id, RteMsg::RequestFocus(identifier)) }
})); }
}
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(&current, &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 table_tooltip: Option<&'component StyledRteTableState>,
pub identifier: Option<uuid::Uuid>, pub identifier: Option<uuid::Uuid>,
pub code_tooltip: Option<&'component StyledRteCodeState>, pub code_tooltip: Option<&'component StyledRteCodeState>,
pub heading_state: Option<&'component StyledSelectState>,
} }
impl<'component> Default for StyledRte<'component> { impl<'component> Default for StyledRte<'component> {
@ -442,6 +483,7 @@ impl<'component> Default for StyledRte<'component> {
table_tooltip: None, table_tooltip: None,
identifier: None, identifier: None,
code_tooltip: None, code_tooltip: None,
heading_state: None,
} }
} }
} }
@ -467,32 +509,7 @@ impl<'outer> StyledRte<'outer> {
.map(|el| seed::to_html_el(&el).id()) .map(|el| seed::to_html_el(&el).id())
.unwrap_or_default(); .unwrap_or_default();
let rte_msg = match target.as_str() { let rte_msg = RteMsg::parse_str(ev, &target, rows, cols);
"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!();
}
};
Msg::Rte(field_id, rte_msg) 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![ div![
C!["styledRte"], C!["styledRte"],
attrs![At::Id => id], attrs![At::Id => id],
div![ div![C!["bar"], first_row, second_row],
C!["bar"],
Self::first_row(click_handler.clone()),
self.second_row(click_handler, change_handler),
],
div![ div![
C!["editorWrapper"], C!["editorWrapper"],
div![ 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 formatting = {
let bold_button = let bold_button =
Self::styled_rte_button("Bold", ButtonId::Bold, Icon::Bold, click_handler.clone()); 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( fn second_row(
@ -629,46 +637,7 @@ impl<'outer> StyledRte<'outer> {
click_handler: EventHandler<Msg>, click_handler: EventHandler<Msg>,
change_handler: EventHandler<Msg>, change_handler: EventHandler<Msg>,
) -> Node<Msg> { ) -> Node<Msg> {
let font_group = { let font_group = self.font_styles();
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 insert_group = { let insert_group = {
let table_tooltip = self.table_tooltip(click_handler.clone(), change_handler); let table_tooltip = self.table_tooltip(click_handler.clone(), change_handler);
@ -710,24 +679,49 @@ impl<'outer> StyledRte<'outer> {
] ]
}; };
let indent_outdent = { div![C!["row secondRow"], font_group, insert_group,]
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![ fn font_styles(&self) -> Node<Msg> {
C!["row secondRow"], let state = self.heading_state.as_ref().unwrap();
font_group, let selected = if let Some(n) = state.values.get(0) {
insert_group, HeadingSize::from_u32(*n)
indent_outdent } 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( fn table_tooltip(
@ -780,43 +774,58 @@ impl<'outer> StyledRte<'outer> {
visible, visible,
children: vec![ children: vec![
h2![span!["Add table"], close_table_tooltip], 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![ div![
C!["inputs"], C!["inputs"],
span!["Columns"], span!["Rows"],
seed::input![ seed::input![
attrs![At::Type => "range"; At::Step => "1"; At::Min => "1"; At::Max => "10"; At::Value => cols], attrs![
on_cols_change 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) let body: Vec<Node<Msg>> = (0..rows)
.map(|_row| { .map(|_row| {
let tds: Vec<Node<Msg>> = (0..cols) let tds: Vec<Node<Msg>> = (0..cols).map(|_col| td![" "]).collect();
.map(|_col| td![" "])
.collect();
tr![tds] tr![tds]
}) })
.collect(); .collect();
seed::div![ seed::div![
C!["tablePreview"], C!["tablePreview"],
seed::table![tbody![body]], seed::table![tbody![body]],
input![ input![
attrs![ attrs![
At::Type => "button"; At::Type => "button";
At::Id => "rteInsertTable"; At::Id => "rteInsertTable";
At::Value => "Insert" At::Value => "Insert"
], ],
on_submit on_submit
], ],
] ]
} },
], ],
class_list: "", class_list: "",
}.render() }
.render()
} }
fn code_tooltip(&self, click_handler: EventHandler<Msg>) -> Node<Msg> { 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] 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(&current, &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
}
}

View File

@ -5,6 +5,7 @@ use seed::*;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_select_child::*; use crate::components::styled_select_child::*;
use crate::pages::project_page::events::EvHandler;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -192,33 +193,21 @@ where
} }
.render() .render()
} else { } else {
empty![] Node::Empty
}; };
let skip = { let skip = selected
let len = selected.len(); .iter()
selected .fold(HashMap::with_capacity(selected.len()), |mut h, o| {
.iter() h.insert(o.value, true);
.fold(HashMap::with_capacity(len), |mut h, o| { h
h.insert(o.value, true); });
h
}) let children: Vec<Node<Msg>> =
}; Self::render_values(&id, options, text_filter, &on_handler, skip);
let children: Vec<Node<Msg>> = if let Some(options) = options {
options let value_container_content =
.filter(|o| !skip.contains_key(&o.value) && o.match_text(text_filter)) Self::value_container_content(id, is_multi, selected, &children);
.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![]
};
seed::div![ seed::div![
C!["styledSelect", variant.to_str(), IF![!valid => "invalid"]], C!["styledSelect", variant.to_str(), IF![!valid => "invalid"]],
@ -227,21 +216,7 @@ where
div![ div![
C!["valueContainer", variant.to_str()], C!["valueContainer", variant.to_str()],
on_handler, on_handler,
match is_multi { value_container_content,
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>>>(),
},
action_icon, action_icon,
], ],
div![ 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> { fn multi_value(child: StyledSelectOption, id: FieldId) -> Node<Msg> {
let handler = super::events::on_click_change_select_remove_multi(id, child.value()); let handler = super::events::on_click_change_select_remove_multi(id, child.value());