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 {
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),
code_tooltip: Some(&state.code_tooltip),
identifier: Some(state.identifier),
heading_state: Some(&state.heading_state),
}
.render()
}

View File

@ -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,37 +224,32 @@ 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)
}
if self.restore_range().is_err() {
return;
}
}
_ => 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()),
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 {
@ -344,8 +292,6 @@ impl StyledRteState {
}
self.code_tooltip.reset();
self.schedule_focus(orders);
}
// table
RteMsg::TableSetRows(n) => {
@ -376,16 +322,11 @@ impl StyledRteState {
Ok(t) => t,
_ => return,
};
table.set_inner_html(
RteTableBodyBuilder::new(*cols, *rows).to_string().as_str(),
);
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(&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 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,24 +774,38 @@ 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],
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],
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();
@ -813,10 +821,11 @@ impl<'outer> StyledRte<'outer> {
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(&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_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
let skip = selected
.iter()
.fold(HashMap::with_capacity(len), |mut h, o| {
.fold(HashMap::with_capacity(selected.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 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());