diff --git a/crates/web/assets/styles/css/app.scss b/crates/web/assets/styles/css/app.scss index 20336228..662241f8 100644 --- a/crates/web/assets/styles/css/app.scss +++ b/crates/web/assets/styles/css/app.scss @@ -18,3 +18,7 @@ strike { display: inline; text-decoration: line-through; } + +bq-rte { + display: block; +} diff --git a/crates/web/src/components/events.rs b/crates/web/src/components/events.rs index 2c24c63c..61e88813 100644 --- a/crates/web/src/components/events.rs +++ b/crates/web/src/components/events.rs @@ -40,7 +40,6 @@ pub fn on_click_change_day(field_id: crate::FieldId, date: chrono::NaiveDateTime ev.stop_propagation(); ev.prevent_default(); - // info!("{:?}", date); crate::Msg::StyledDateTimeInputChanged( field_id, StyledDateTimeChanged::DayChanged(Some(date)), @@ -113,6 +112,7 @@ pub fn on_click_change_select_selected(field_id: crate::FieldId, value: Option EvHandler { ev(Ev::Click, move |ev| { ev.stop_propagation(); + crate::Msg::StyledSelectChanged(field_id, StyledSelectChanged::RemoveMulti(value)) }) } @@ -162,6 +163,7 @@ fn noop(event: Ev) -> EvHandler { ev(event, |ev| { ev.stop_propagation(); ev.prevent_default(); + None as Option }) } diff --git a/crates/web/src/components/styled_rte.rs b/crates/web/src/components/styled_rte.rs index 209e013f..6ee2c5ea 100644 --- a/crates/web/src/components/styled_rte.rs +++ b/crates/web/src/components/styled_rte.rs @@ -1,13 +1,14 @@ use seed::prelude::*; use seed::*; -use web_sys::MouseEvent; +use web_sys::{HtmlElement, MouseEvent}; use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_select::{SelectVariant, StyledSelect, StyledSelectState}; use crate::components::styled_select_child::StyledSelectOption; use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant}; -use crate::{ButtonId, FieldId, Msg, RteField}; +use crate::styled_select::StyledSelectChanged; +use crate::{BuildMsg, ButtonId, FieldId, Msg, RteField}; #[derive(Clone, Copy, Debug)] pub enum HeadingSize { @@ -102,18 +103,17 @@ pub enum RteMsg { } impl RteMsg { - fn parse_str(ev: MouseEvent, target: &str, cols: u16, rows: u16) -> Self { - match target { + fn parse_str(ev: MouseEvent, action_name: &str, cols: u16, rows: u16) -> Self { + match action_name.trim() { "bold" => Self::Bold, "italic" => Self::Italic, "underscore" => Self::Underscore, "strikethrough" => Self::Strikethrough, "listingDots" => Self::InsertUnorderedList, "listingNumber" => Self::InsertOrderedList, - "subscript " => Self::Subscript, + "subscript" => Self::Subscript, "superscript" => Self::Superscript, - // "heading" => Self::InsertHeading(), "table" => Self::TableSetVisibility(true), "codeAlt" => Self::InsertCode(true), "closeRteTableTooltip" => Self::TableSetVisibility(false), @@ -124,7 +124,7 @@ impl RteMsg { _ => { let target = ev.target().unwrap(); let h = seed::to_html_el(&target); - error!("unknown rte command for element", h); + error!("unknown rte command for element", action_name, h); unreachable!(); } } @@ -468,6 +468,104 @@ impl StyledRteState { } } +#[derive(Debug)] +pub struct DetectFontStyle(FieldId); + +impl BuildMsg for DetectFontStyle { + fn build(&self, element: &Option) -> Option { + let Some(el) = element else { + return None; + }; + let el = el.dyn_ref::(); + let Some(mut el) = el else { + return None; + }; + let mut parent; + loop { + match el.tag_name().as_str() { + "BQ-RTE" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::Normal.as_u32())), + )) + } + "H1" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H1.as_u32())), + )) + } + "H2" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H2.as_u32())), + )) + } + "H3" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H3.as_u32())), + )) + } + "H4" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H4.as_u32())), + )) + } + "H5" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H5.as_u32())), + )) + } + "H6" => { + return Some(Msg::StyledSelectChanged( + self.0.clone(), + StyledSelectChanged::Changed(Some(HeadingSize::H6.as_u32())), + )) + } + "BODY" => return None, + _ => {} + } + parent = el.parent_element(); + let Some(parent) = &parent else { + return None; + }; + let Some(parent) = parent.dyn_ref::() else { + return None; + }; + el = parent; + } + } + + fn sender_allowed(&self, element: &Option) -> bool { + let Some(el) = element else { + return false; + }; + let el = el.dyn_ref::(); + let Some(mut el) = el else { + return false; + }; + let mut parent; + loop { + match el.tag_name().as_str() { + "BQ-RTE" => return false, + "BODY" => return false, + _ => {} + } + parent = el.parent_element(); + let Some(parent) = &parent else { + return false; + }; + let Some(parent) = parent.dyn_ref::() else { + return false; + }; + el = parent; + } + } +} + pub struct StyledRte<'component> { pub field_id: FieldId, pub table_tooltip: Option<&'component StyledRteTableState>, @@ -514,72 +612,32 @@ impl<'outer> StyledRte<'outer> { }) }; - let change_handler = { - let field_id = self.field_id.clone(); - ev(Ev::Change, move |event| { - event - .target() - .as_ref() - .ok_or("Can't get event target reference") - .and_then(util::get_value) - .ok() - .and_then(|s| s.parse::().ok()) - .map(|n| Msg::Rte(field_id, RteMsg::TableSetRows(n))) - }) - }; + let first_row = self.first_row(click_handler.clone()); - let first_row = Self::first_row(click_handler.clone()); - let second_row = self.second_row(click_handler, change_handler); + let click_detect_font_style = mouse_ev(Ev::Click, |ev| { + let target = ev.target()?; + let el = to_html_el(&target); + tracing::info!("{:?} {:?}", el.tag_name(), el.id()); + None as Option + }); div![ C!["styledRte"], attrs![At::Id => id], - div![C!["bar"], first_row, second_row], + div![C!["bar"], first_row], div![ C!["editorWrapper"], - div![ + custom![ + Tag::from("bq-rte"), C!["editor", self.field_id.to_str()], attrs![At::ContentEditable => true], - ], + click_detect_font_style + ] ] ] } - fn first_row(click_handler: EventHandler) -> Node { - let justify = { - let justify_all_button = Self::styled_rte_button( - "Justify All", - ButtonId::JustifyAll, - Icon::JustifyAll, - click_handler.clone(), - ); - let justify_center_button = Self::styled_rte_button( - "Justify Center", - ButtonId::JustifyCenter, - Icon::JustifyCenter, - click_handler.clone(), - ); - let justify_left_button = Self::styled_rte_button( - "Justify Left", - ButtonId::JustifyLeft, - Icon::JustifyLeft, - click_handler.clone(), - ); - let justify_right_button = Self::styled_rte_button( - "Justify Right", - ButtonId::JustifyRight, - Icon::JustifyRight, - click_handler.clone(), - ); - div![ - C!["group justify"], - justify_all_button, - justify_center_button, - justify_left_button, - justify_right_button - ] - }; - + fn first_row(&self, click_handler: EventHandler) -> Node { let formatting = { let bold_button = Self::styled_rte_button("Bold", ButtonId::Bold, Icon::Bold, click_handler.clone()); @@ -615,7 +673,7 @@ impl<'outer> StyledRte<'outer> { "Superscript", ButtonId::Superscript, Icon::Superscript, - click_handler, + click_handler.clone(), ); div![ @@ -628,16 +686,19 @@ impl<'outer> StyledRte<'outer> { superscript_button, ] }; - - div![C!["row firstRow"], formatting, justify] - } - - fn second_row( - &self, - click_handler: EventHandler, - change_handler: EventHandler, - ) -> Node { - let font_group = self.font_styles(); + let change_handler = { + let field_id = self.field_id.clone(); + ev(Ev::Change, move |event| { + event + .target() + .as_ref() + .ok_or("Can't get event target reference") + .and_then(util::get_value) + .ok() + .and_then(|s| s.parse::().ok()) + .map(|n| Msg::Rte(field_id, RteMsg::TableSetRows(n))) + }) + }; let insert_group = { let table_tooltip = self.table_tooltip(click_handler.clone(), change_handler); @@ -679,7 +740,9 @@ impl<'outer> StyledRte<'outer> { ] }; - div![C!["row secondRow"], font_group, insert_group,] + let font_group = self.font_styles(); + + div![C!["row firstRow"], font_group, formatting, insert_group] } fn font_styles(&self) -> Node { @@ -689,13 +752,14 @@ impl<'outer> StyledRte<'outer> { } else { HeadingSize::Normal }; + let variant = SelectVariant::Empty; 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, + variant, }]; let options = Some(HeadingSize::all().iter().map(|h| StyledSelectOption { @@ -704,12 +768,12 @@ impl<'outer> StyledRte<'outer> { text: Some(h.as_str()), value: h.as_u32(), class_list: "", - variant: SelectVariant::Normal, + variant, })); let font = StyledSelect { id: self.field_id.clone(), - variant: SelectVariant::Normal, + variant, dropdown_width: Some(96), name: "", valid: true, diff --git a/crates/web/src/components/styled_tip.rs b/crates/web/src/components/styled_tip.rs index 5858ea39..3a6fcc4e 100644 --- a/crates/web/src/components/styled_tip.rs +++ b/crates/web/src/components/styled_tip.rs @@ -9,10 +9,8 @@ pub fn styled_tip(letter: BrowserKey, model: &Model, builder: B) -> Node where B: BuildMsg + 'static, { - model - .key_triggers - .borrow_mut() - .insert(letter.clone(), Box::new(builder)); + model.key_triggers.insert(letter.clone(), Box::new(builder)); + div![ C!["proTip"], strong![C!["strong"], "Pro tip: "], diff --git a/crates/web/src/main.rs b/crates/web/src/main.rs index 9d64a4af..53f9f8c5 100644 --- a/crates/web/src/main.rs +++ b/crates/web/src/main.rs @@ -63,10 +63,18 @@ pub enum OperationKind { } pub trait BuildMsg: std::fmt::Debug { - fn build(&self) -> Msg; + fn build(&self, element: &Option) -> Option; - fn sender_allowed(&self, tag_name: Option<&str>) -> bool { - tag_name == Some("BODY") + fn sender_allowed(&self, element: &Option) -> bool { + if element.is_none() { + return false; + } + let tag = element.as_ref().map(|el| el.tag_name()); + matches!(tag.as_deref(), Some("BODY")) + } + + fn handle_element(&self, _el: &web_sys::HtmlElement) -> Option { + None } } @@ -274,7 +282,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } fn view(model: &Model) -> Node { - model.key_triggers.borrow_mut().clear(); + model.key_triggers.clear(); match model.page { Page::Project @@ -390,27 +398,30 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { ); let key_triggers = model.key_triggers.clone(); - let sender_clone = sender.clone(); - shared::on_event::keydown(move |ev: KeyboardEvent| { - let sender = sender_clone.clone(); - let key_triggers = key_triggers.clone(); - let event = seed::to_keyboard_event(&ev); + { + let sender_clone = sender.clone(); + shared::on_event::keydown(move |ev: KeyboardEvent| { + let sender = sender_clone.clone(); + let key_triggers = key_triggers.clone(); + let event = seed::to_keyboard_event(&ev); - let active = seed::document().active_element().map(|el| el.tag_name()); + let active: Option = seed::document().active_element(); - let Ok(key) = event.key().parse::() else { - return; - }; - if let Some(b) = key_triggers.borrow().get(&key) { - if !b.sender_allowed(active.as_deref()) { + let Ok(key) = event.key().parse::() else { return; - } - ev.prevent_default(); - ev.stop_propagation(); - let msg = b.build(); - sender.clone()(Some(msg)); - }; - }); + }; + if let Some(b) = key_triggers.0.borrow().get(&key) { + if !b.sender_allowed(&active) { + return; + } + ev.prevent_default(); + ev.stop_propagation(); + if let Some(msg) = b.build(&active) { + sender.clone()(Some(msg)); + } + }; + }); + } { let sender_clone = sender.clone(); diff --git a/crates/web/src/modals/issues_create/view.rs b/crates/web/src/modals/issues_create/view.rs index 6dbb798d..123a1129 100644 --- a/crates/web/src/modals/issues_create/view.rs +++ b/crates/web/src/modals/issues_create/view.rs @@ -25,17 +25,18 @@ use crate::{BuildMsg, FieldId, Msg}; pub struct CloseCreateIssueModal; impl BuildMsg for CloseCreateIssueModal { - fn build(&self) -> Msg { - Msg::ModalDropped + fn build(&self, element: &Option) -> Option { + Some(Msg::ModalDropped) } - fn sender_allowed(&self, tag_name: Option<&str>) -> bool { - matches!(tag_name, Some("BODY") | Some("TEXTAREA")) + fn sender_allowed(&self, element: &Option) -> bool { + let tag_name = element.as_ref().map(|el| el.tag_name()); + matches!(tag_name.as_deref(), Some("BODY") | Some("TEXTAREA")) } } pub fn view(model: &Model, modal: &AddIssueModal) -> Node { - model.key_triggers.borrow_mut().insert( + model.key_triggers.insert( BrowserKey::UiKey(UiKey::Escape), Box::new(CloseCreateIssueModal), ); diff --git a/crates/web/src/modals/issues_edit/view.rs b/crates/web/src/modals/issues_edit/view.rs index a6788c9b..d54a9ef9 100644 --- a/crates/web/src/modals/issues_edit/view.rs +++ b/crates/web/src/modals/issues_edit/view.rs @@ -31,15 +31,16 @@ mod comments; pub struct CloseAddComment; impl BuildMsg for CloseAddComment { - fn build(&self) -> Msg { - Msg::ModalChanged(FieldChange::ToggleCommentForm( + fn build(&self, _element: &Option) -> Option { + Some(Msg::ModalChanged(FieldChange::ToggleCommentForm( FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), false, - )) + ))) } - fn sender_allowed(&self, tag_name: Option<&str>) -> bool { - matches!(tag_name, Some("TEXTAREA")) + fn sender_allowed(&self, element: &Option) -> bool { + let tag = element.as_ref().map(|el| el.tag_name()); + tag.as_deref() == Some("TEXTAREA") } } @@ -51,11 +52,10 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node { return Node::Empty; }; - { - let mut b = model.key_triggers.borrow_mut(); - b.insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseIssueModal)); - b.insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseAddComment)); - } + model + .key_triggers + .insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseIssueModal)) + .insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseAddComment)); StyledModal { variant: ModalVariant::Center, @@ -294,12 +294,11 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { pub struct EnableCommentBuilder; impl BuildMsg for EnableCommentBuilder { - fn build(&self) -> Msg { - tracing::info!("{self:?}"); - Msg::ModalChanged(FieldChange::ToggleCommentForm( + fn build(&self, _element: &Option) -> Option { + Some(Msg::ModalChanged(FieldChange::ToggleCommentForm( FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), true, - )) + ))) } } @@ -307,9 +306,8 @@ impl BuildMsg for EnableCommentBuilder { pub struct CloseIssueModal; impl BuildMsg for CloseIssueModal { - fn build(&self) -> Msg { - tracing::info!("{self:?}"); - Msg::ModalDropped + fn build(&self, _element: &Option) -> Option { + Some(Msg::ModalDropped) } } diff --git a/crates/web/src/model.rs b/crates/web/src/model.rs index c6afcbae..ed9f441b 100644 --- a/crates/web/src/model.rs +++ b/crates/web/src/model.rs @@ -1,4 +1,6 @@ +use std::cell::RefCell; use std::collections::hash_map::HashMap; +use std::rc::Rc; use bitque_data::*; use seed::app::Orders; @@ -17,8 +19,8 @@ use crate::pages::reports_page::model::ReportsPage; use crate::pages::sign_in_page::model::SignInPage; use crate::pages::sign_up_page::model::SignUpPage; use crate::pages::users_page::model::UsersPage; -use crate::{BuildMsg, Msg}; use crate::shared::keys::BrowserKey; +use crate::{BuildMsg, Msg}; pub trait IssueModal { fn epic_id_value(&self) -> Option; @@ -277,11 +279,31 @@ pub struct Model { pub epic_ids: Vec, pub epics_by_id: HashMap, - pub key_triggers: std::rc::Rc>>>, + pub key_triggers: KeyTriggers, pub distinct_key_up: crate::shared::on_event::Distinct, pub show_extras: bool, } +#[derive(Debug, Clone)] +pub struct KeyTriggers(pub Rc>>>); + +impl Default for KeyTriggers { + fn default() -> Self { + Self(Rc::new(RefCell::new(HashMap::with_capacity(64)))) + } +} + +impl KeyTriggers { + pub fn insert(&self, key: BrowserKey, value: Box) -> &Self { + self.0.borrow_mut().insert(key, value); + self + } + + pub fn clear(&self) { + self.0.borrow_mut().clear(); + } +} + impl Model { pub fn new(host_url: String, ws_url: String, page: Page) -> Self { Self { @@ -319,7 +341,7 @@ impl Model { show_extras: false, modals_stack: vec![], modals: Modals::default(), - key_triggers: std::rc::Rc::new(std::cell::RefCell::new(HashMap::with_capacity(20))), + key_triggers: KeyTriggers::default(), distinct_key_up: crate::shared::on_event::distinct(), } } diff --git a/crates/web/src/pages/project_page/view.rs b/crates/web/src/pages/project_page/view.rs index d2c824e3..e727be29 100644 --- a/crates/web/src/pages/project_page/view.rs +++ b/crates/web/src/pages/project_page/view.rs @@ -16,8 +16,8 @@ mod filters; struct CreateIssueShortcut(i32); impl BuildMsg for CreateIssueShortcut { - fn build(&self) -> Msg { - Msg::ModalOpened(ModalType::AddIssue(None)) + fn build(&self, _element: &Option) -> Option { + Some(Msg::ModalOpened(ModalType::AddIssue(None))) } } @@ -25,7 +25,6 @@ pub fn view(model: &Model) -> Node { { model .key_triggers - .borrow_mut() .insert(BrowserKey::Character('c'), Box::new(CreateIssueShortcut(0))); }