diff --git a/crates/web/assets/styles/css/app.scss b/crates/web/assets/styles/css/app.scss index 9663e301..20336228 100644 --- a/crates/web/assets/styles/css/app.scss +++ b/crates/web/assets/styles/css/app.scss @@ -13,3 +13,8 @@ main { article.inner-layout { width: 100%; } + +strike { + display: inline; + text-decoration: line-through; +} diff --git a/crates/web/src/components/styled_editor.rs b/crates/web/src/components/styled_editor.rs index 9fb110cf..b09ad5bb 100644 --- a/crates/web/src/components/styled_editor.rs +++ b/crates/web/src/components/styled_editor.rs @@ -152,6 +152,7 @@ fn render_rte(state: &StyledRteState) -> Node { table_tooltip: Some(&state.table_tooltip), code_tooltip: Some(&state.code_tooltip), identifier: Some(state.identifier), + heading_state: Some(&state.heading_state), } .render() } diff --git a/crates/web/src/components/styled_rte.rs b/crates/web/src/components/styled_rte.rs index fc510dd7..209e013f 100644 --- a/crates/web/src/components/styled_rte.rs +++ b/crates/web/src/components/styled_rte.rs @@ -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 { - 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::() { - 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, pub identifier: uuid::Uuid, + pub heading_state: StyledSelectState, + range: Option, } 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) { 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) { - 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::()?; + 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::().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, + 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 + } + } + + fn is_in_rte(&self) -> Option { + 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::().expect("Element must be Node"); + } + None } } @@ -433,6 +473,7 @@ pub struct StyledRte<'component> { pub table_tooltip: Option<&'component StyledRteTableState>, pub identifier: Option, 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, change_handler: EventHandler, ) -> Node { - let font_group = { - let options: Vec> = 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::().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 { + 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> = (0..rows) .map(|_row| { - let tds: Vec> = (0..cols) - .map(|_col| td![" "]) - .collect(); + let tds: Vec> = (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) -> Node { @@ -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::()?; - 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 - } -} diff --git a/crates/web/src/components/styled_select.rs b/crates/web/src/components/styled_select.rs index 0cd91cf4..b8a12f70 100644 --- a/crates/web/src/components/styled_select.rs +++ b/crates/web/src/components/styled_select.rs @@ -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> = 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> = + 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::>>(), - 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::>>(), - }, + value_container_content, action_icon, ], div![ @@ -266,6 +241,52 @@ where ] } + fn value_container_content( + id: FieldId, + is_multi: bool, + selected: Vec, + children: &Vec>, + ) -> Vec> { + if !is_multi { + return selected + .into_iter() + .map(|m| m.render_value()) + .collect::>>(); + } + + vec![div![ + C!["valueMulti"], + selected + .into_iter() + .map(|m| Self::multi_value(m, id.clone())) + .collect::>>(), + 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, + text_filter: &str, + on_handler: &EvHandler, + skip: HashMap, + ) -> Vec> { + 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 { let handler = super::events::on_click_change_select_remove_multi(id, child.value());