diff --git a/Cargo.lock b/Cargo.lock index 8a1c722d..015737fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1080,8 +1080,8 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2", + "quote", "syn", "synstructure", ] @@ -1790,10 +1790,10 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5" dependencies = [ - "base64 0.10.1", - "email", - "lettre", - "mime", + "base64 0.10.1", + "email", + "lettre", + "mime", "time 0.1.43", "uuid 0.7.4", ] @@ -3078,7 +3078,6 @@ dependencies = [ "serde_derive", "serde_json", "walkdir", - "yaml-rust", ] [[package]] @@ -3090,9 +3089,9 @@ dependencies = [ "cfg-if", "libc", "rand 0.7.3", - "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", ] [[package]] @@ -3768,15 +3767,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" -[[package]] -name = "yaml-rust" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zeroize" version = "1.1.0" diff --git a/README.md b/README.md index 86279b78..3b7298a7 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ https://git.sr.ht/~tsumanu/jirs * [X] Grouping by Epic * [X] Basic Rich Text Editor * [ ] Insert Code in Rich Text Editor +* [X] Code syntax * [ ] Personal settings to choose MDE (Markdown Editor) or RTE * [ ] Issues and filters view * [ ] Issues and filters working filters @@ -180,3 +181,159 @@ sudo nginx -s reload ## Issue trackers https://todo.sr.ht/~tsumanu/JIRS + +## Details + +### Display code syntax + +Custom element glued with WASM + +* `file-path` have connected on attr changed callback and will change displayed path +* `lang` does not have callback and it's used only on `connectedCallback` + +```html + +struct Foo { +} + +``` + +### Supported languages + +* ASP +* AWK +* ActionScript +* Advanced CSV +* AppleScript +* Assembly x86 (NASM) +* Batch File +* BibTeX +* Bourne Again Shell (bash) +* C +* C# +* C++ +* CMake +* CMake C Header +* CMake C++ Header +* CMakeCache +* CMakeCommands +* CSS +* Cargo Build Results +* Clojure +* Crystal +* D +* DMD Output +* Dart +* Diff +* Dockerfile +* Elixir +* Elm +* Elm Compile Messages +* Elm Documentation +* Erlang +* F# +* Fortran (Fixed Form) +* Fortran (Modern) +* Fortran Namelist +* Friendly Interactive Shell (fish) +* GFortran Build Results +* Generic Config +* Git Attributes +* Git Commit +* Git Common +* Git Config +* Git Ignore +* Git Link +* Git Log +* Git Mailmap +* Git Rebase Todo +* Go +* GraphQL +* Graphviz (DOT) +* Groovy +* HTML +* HTML (ASP) +* HTML (EEx) +* HTML (Erlang) +* HTML (Jinja2) +* HTML (Rails) +* HTML (Tcl) +* Handlebars +* Haskell +* JSON +* Java +* Java Properties +* Java Server Page (JSP) +* JavaScript +* JavaScript (Rails) +* Javadoc +* Jinja2 +* Julia +* Kotlin +* LaTeX +* LaTeX Log +* Less +* Linker Script +* Lisp +* Literate Haskell +* Lua +* MATLAB +* Make Output +* Makefile +* Markdown +* MiniZinc (MZN) +* MultiMarkdown +* NAnt Build File +* Nim +* Nix +* OCaml +* OCamllex +* OCamlyacc +* Objective-C +* Objective-C++ +* OpenMP (Fortran) +* PHP +* PHP Source +* Pascal +* Perl +* Plain Text +* PowerShell +* PureScript +* Python +* R +* R Console +* Racket +* Rd (R Documentation) +* Reason +* Regular Expression +* Regular Expressions (Elixir) +* Regular Expressions (Javascript) +* Regular Expressions (PHP) +* Regular Expressions (Python) +* Ruby +* Ruby Haml +* Ruby on Rails +* Rust +* SCSS +* SQL +* SQL (Rails) +* SWI-Prolog +* Sass +* Scala +* Shell-Unix-Generic +* Stylus +* Swift +* TOML +* Tcl +* TeX +* Textile +* TypeScript +* TypeScriptReact +* VimL +* XML +* YAML +* camlp4 +* commands-builtin-shell-bash +* lrc +* reStructuredText +* srt diff --git a/jirs-client/Cargo.toml b/jirs-client/Cargo.toml index 532671a2..693aeae6 100644 --- a/jirs-client/Cargo.toml +++ b/jirs-client/Cargo.toml @@ -33,7 +33,7 @@ comrak = "*" wee_alloc = "*" lazy_static = "*" -syntect = { version = "*", default-features = false, features = ["default-fancy"] } +syntect = { version = "*", default-features = false, features = ["html", "regex-fancy", "dump-load-rs"] } [dependencies.wasm-bindgen] version = "0.2.66" diff --git a/jirs-client/js/css/styledRte.css b/jirs-client/js/css/styledRte.css index 1205e6f0..38f989a5 100644 --- a/jirs-client/js/css/styledRte.css +++ b/jirs-client/js/css/styledRte.css @@ -161,3 +161,54 @@ display: flex; justify-content: space-between; } + +/**********************************************************/ +/* Code tooltip */ +/**********************************************************/ + +.codeTooltip { + min-width: 336px; + padding: 15px; + position: absolute; +} + +@media (min-width: 800px) { + .codeTooltip { + min-width: 636px; + left: calc(100% / 2 - 318px); + } +} + +@media (min-width: 1024px) { + .codeTooltip { + min-width: 836px; + left: calc(100% / 2 - 418px); + } +} + +.codeTooltip > h2 { + display: flex; + justify-content: space-between; + margin-bottom: 15px; +} + +.codeTooltip > select { + width: 100%; + border: 1px solid var(--borderLightest); + margin: var(--rte-indent) 0; + min-height: 1rem; + background: var(--backgroundLightest); +} + +.codeTooltip > select > option { +} + +.codeTooltip > textarea { + border: 1px solid var(--borderLightest); + width: 100%; + min-height: 200px; +} + +.codeTooltip > textarea:focus { + border: 1px solid var(--borderInputFocus); +} diff --git a/jirs-client/js/template.html b/jirs-client/js/template.html index baa452b6..545abe2d 100644 --- a/jirs-client/js/template.html +++ b/jirs-client/js/template.html @@ -7,6 +7,7 @@ JIRS +
diff --git a/jirs-client/src/elements.rs b/jirs-client/src/elements.rs index 652cee40..9fa014f8 100644 --- a/jirs-client/src/elements.rs +++ b/jirs-client/src/elements.rs @@ -1,5 +1,6 @@ use syntect::easy::HighlightLines; use syntect::highlighting::{FontStyle, Style}; +use syntect::parsing::SyntaxReference; use wasm_bindgen::prelude::*; #[wasm_bindgen(final, js_name = JirsCodeBuilder)] @@ -14,13 +15,22 @@ impl JirsCodeBuilder { } #[wasm_bindgen] - pub fn hi(&mut self, lang: &str, line: &str) -> String { + pub fn hi_code(&mut self, lang: &str, code: &str) -> String { let syntax = match crate::hi::SYNTAX_SET.find_syntax_by_name(lang) { Some(s) => s, _ => { - return line.to_string(); + return code.to_string(); } }; + let mut buffer = String::new(); + for line in code.lines() { + buffer.push_str(self.hi(syntax, line).as_str()); + buffer.push_str("
"); + } + buffer + } + + fn hi(&mut self, syntax: &SyntaxReference, line: &str) -> String { let mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github let tokens = h.highlight(line, &crate::hi::SYNTAX_SET); @@ -36,8 +46,10 @@ impl JirsCodeBuilder { "font-weight: bold" } else if font_style == FontStyle::ITALIC { "font-style: italic" + } else if font_style == FontStyle::UNDERLINE { + "text-decoration: underline" } else { - "font-decoration: underline" + "" }; let f = format!("rgba({}, {}, {}, {})", f.r, f.g, f.b, f.a); let b = format!("rgba({}, {}, {}, {})", b.r, b.g, b.b, b.a); @@ -55,71 +67,225 @@ impl JirsCodeBuilder { } pub fn define() { - create_custom_element( - "jirs-code-view", - "JirsCodeView", - r#" - +
- "#, - ); + "#, + ) + .on_connected(FillShadowElement::new(el_name, "#view", "")) + .on_connected( + r#" + const lang = this.getAttribute('lang') || ''; + setTimeout(() => {{ + const code = (this.innerHTML || '').trim(); + shadow.querySelector('#view').innerHTML = runtime.hi_code(lang, code); + }}, 1); + "#, + ) + .on_attr_changed("lang", r#""#) + .on_attr_changed( + "file-path", + r#" + shadow.querySelector('#file-name').innerText = newV; + "#, + ) + .mount(); + }; } -fn create_custom_element(tag: &str, name: &str, html: &str) { - let source = format!( - r#" +trait ToJs { + fn to_js(&self) -> String; +} + +impl ToJs for &str { + fn to_js(&self) -> String { + self.to_string() + } +} + +struct FillShadowElement { + el_name: String, + target: String, + source: String, +} + +impl FillShadowElement { + pub fn new, S: Into, Source: ToJs>( + el_name: N, + target: S, + source: Source, + ) -> Self { + Self { + el_name: el_name.into(), + target: target.into(), + source: source.to_js(), + } + } +} + +impl ToJs for FillShadowElement { + fn to_js(&self) -> String { + let shadow = ElementBuilder::shadow_handle(&self.el_name); + format!( + "{shadow}.querySelector('{selector}').innerHTML = `{content}`;", + shadow = shadow, + selector = self.target, + content = self.source + ) + } +} + +#[derive(Default)] +struct ElementBuilder { + name: String, + tag: String, + body: String, + runtime: String, + on_connected: Vec, + on_attr_changed: std::collections::HashMap>, +} + +impl ToJs for ElementBuilder { + fn to_js(&self) -> String { + let shadow = Self::shadow_handle(&self.name); + let runtime = Self::runtime_handle(&self.name); + let (observe, attr_body) = if self.on_attr_changed.is_empty() { + ("".to_string(), "".to_string()) + } else { + let observe = self + .on_attr_changed + .keys() + .map(|s| format!("'{}'", s)) + .collect::>() + .join(","); + let mut on_changed = "switch (name) {".to_string(); + for (k, v) in self.on_attr_changed.iter() { + let body = v.join(";"); + on_changed.push_str( + format!("case '{attr}': {{ {body}; break; }}", attr = k, body = body).as_str(), + ); + } + on_changed.push_str("}"); + + ( + format!( + "static get observedAttributes() {{ return [{}]; }}", + observe + ), + on_changed, + ) + }; + let on_connected: String = self.on_connected.join(";"); + + format!( + r#" class {name} extends HTMLElement {{ static RUNTIME = Symbol(); static SHADOW = Symbol(); - static get observedAttributes() {{ return ['lang']; }} + {observe} constructor() {{ super(); - this[ {name} . SHADOW] = this.attachShadow({{ 'mode': 'closed' }}); - this[ {name} . SHADOW].innerHTML = `{html}`; + {shadow} = this.attachShadow({{ 'mode': 'closed' }}); + {shadow}.innerHTML = `{html}`; }} connectedCallback() {{ - this[ {name} . RUNTIME] = new JirsCodeBuilder(); - const view = this[ {name} . SHADOW].querySelector('#view'); - view.innerHTML = ''; - const lang = this.getAttribute('lang') || ''; + const runtime = {runtime} = new JirsCodeBuilder(); + const shadow = {shadow}; - setTimeout(() => {{ - const hi = () => {{ - const line = code.shift(); - if (line === undefined) return; - const s = this[ {name} . RUNTIME].hi(lang, line); - view.innerHTML += `${{s}}
`; - setTimeout(() => hi(), 10); - }}; - hi(); - }}, 10); + {on_connected} }} disconnectedCallback() {{ - this[ {name} . RUNTIME].free(); + {runtime}.free(); }} attributeChangedCallback(name, oldV, newV) {{ + const runtime = {runtime}; + const shadow = {shadow}; + {attr_body} }} }} customElements.define( '{tag}', {name}); "#, - name = name, - tag = tag, - html = html, - ); - { + name = self.name, + tag = self.tag, + html = self.body, + shadow = shadow, + runtime = runtime, + observe = observe, + attr_body = attr_body, + on_connected = on_connected, + ) + } +} + +impl ElementBuilder { + pub fn identifier, T: Into>(mut self, name: N, tag: T) -> Self { + self.name = name.into(); + self.tag = tag.into(); + self + } + + pub fn runtime>(mut self, runtime: S) -> Self { + self.runtime = runtime.into(); + self + } + + pub fn body>(mut self, body: S) -> Self { + self.body = body.into(); + self + } + + pub fn on_connected(mut self, c: B) -> Self { + self.on_connected.push(c.to_js()); + self + } + + pub fn on_attr_changed, R: Into>(mut self, attr: N, run: R) -> Self { + let a = attr.into(); + let r = run.into(); + self.on_attr_changed + .entry(a) + .or_insert_with(|| vec![]) + .push(r); + self + } + + pub fn shadow_handle(name: &str) -> String { + format!("this[ {name} . SHADOW]", name = name) + } + + pub fn runtime_handle(name: &str) -> String { + format!("this[ {name} . RUNTIME]", name = name) + } + + pub fn mount(&self) { + let source = self.to_js(); + { + use seed::*; + log!(source); + } use seed::*; match js_sys::eval(source.as_str()) { Ok(_v) => (), Err(e) => error!(e), }; - }; + } } diff --git a/jirs-client/src/fields.rs b/jirs-client/src/fields.rs index 185b6f7a..ccae836e 100644 --- a/jirs-client/src/fields.rs +++ b/jirs-client/src/fields.rs @@ -11,6 +11,11 @@ pub enum EditIssueModalSection { Comment(CommentFieldId), } +#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] +pub enum RteField { + CodeLang(Box), +} + #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] pub enum FieldId { SignIn(SignInFieldId), @@ -26,6 +31,7 @@ pub enum FieldId { CopyButtonLabel, ProjectSettings(ProjectFieldId), + Rte(RteField), } impl std::fmt::Display for FieldId { @@ -120,6 +126,7 @@ impl std::fmt::Display for FieldId { UsersFieldId::Avatar => f.write_str("profile-avatar"), UsersFieldId::CurrentProject => f.write_str("profile-currentProject"), }, + FieldId::Rte(..) => f.write_str("rte"), } } } diff --git a/jirs-client/src/invite.rs b/jirs-client/src/invite.rs index 268ae742..0d081ab1 100644 --- a/jirs-client/src/invite.rs +++ b/jirs-client/src/invite.rs @@ -107,10 +107,10 @@ fn submit(_page: &InvitePage) -> Node { } fn token_field(page: &InvitePage) -> Node { - let token = StyledInput::build(FieldId::Invite(InviteFieldId::Token)) + let token = StyledInput::build() .valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none()) .value(page.token.as_str()) - .build() + .build(FieldId::Invite(InviteFieldId::Token)) .into_node(); StyledField::build() diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index ec4cc359..7ff773a8 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -191,6 +191,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { styled_tooltip::Variant::Messages => { model.messages_tooltip_visible = !model.messages_tooltip_visible; } + styled_tooltip::Variant::CodeBuilder => {} Variant::TableBuilder => {} }, _ => (), diff --git a/jirs-client/src/modal/issues.rs b/jirs-client/src/modal/issues.rs index f7e0aa97..2a98cc78 100644 --- a/jirs-client/src/modal/issues.rs +++ b/jirs-client/src/modal/issues.rs @@ -23,7 +23,7 @@ where .and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId)) .map(|epic| vec![epic.to_child()]) .unwrap_or_default(); - let input = StyledSelect::build(field_id) + let input = StyledSelect::build() .name("epic") .selected(selected) .options(model.epics.iter().map(|epic| epic.to_child()).collect()) @@ -32,7 +32,7 @@ where .text_filter(modal.epic_state().text_filter.as_str()) .opened(modal.epic_state().opened) .valid(true) - .build() + .build(field_id) .into_node(); Some( StyledField::build() diff --git a/jirs-client/src/modal/issues/add_issue.rs b/jirs-client/src/modal/issues/add_issue.rs index ab272848..9dec0d33 100644 --- a/jirs-client/src/modal/issues/add_issue.rs +++ b/jirs-client/src/modal/issues/add_issue.rs @@ -300,7 +300,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { } fn issue_type_field(modal: &AddIssueModal) -> Node { - let select_type = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Type)) + let select_type = StyledSelect::build() .name("type") .normal() .text_filter(modal.type_state.text_filter.as_str()) @@ -317,7 +317,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node { ) .to_child() .name("type")]) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Type)) .into_node(); StyledField::build() .label("Issue Type") @@ -328,9 +328,9 @@ fn issue_type_field(modal: &AddIssueModal) -> Node { } fn short_summary_field(modal: &AddIssueModal) -> Node { - let short_summary = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) + let short_summary = StyledInput::build() .state(&modal.title_state) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Title)) .into_node(); StyledField::build() .label("Short Summary") @@ -359,7 +359,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { .reporter_id .or_else(|| model.user.as_ref().map(|u| u.id)) .unwrap_or_default(); - let reporter = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Reporter)) + let reporter = StyledSelect::build() .normal() .text_filter(modal.reporter_state.text_filter.as_str()) .opened(modal.reporter_state.opened) @@ -384,7 +384,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { .collect(), ) .valid(true) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Reporter)) .into_node(); StyledField::build() .input(reporter) @@ -395,7 +395,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node { } fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { - let assignees = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Assignees)) + let assignees = StyledSelect::build() .normal() .multi() .text_filter(modal.assignees_state.text_filter.as_str()) @@ -421,7 +421,7 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { .collect(), ) .valid(true) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Assignees)) .into_node(); StyledField::build() .input(assignees) @@ -432,7 +432,7 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node { } fn issue_priority_field(modal: &AddIssueModal) -> Node { - let select_priority = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Priority)) + let select_priority = StyledSelect::build() .name("priority") .normal() .text_filter(modal.priority_state.text_filter.as_str()) @@ -445,7 +445,7 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node { .collect(), ) .selected(vec![modal.priority.to_child().name("priority")]) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Priority)) .into_node(); StyledField::build() .label("Issue Type") @@ -456,9 +456,9 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node { } fn name_field(modal: &AddIssueModal) -> Node { - let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) + let name = StyledInput::build() .state(&modal.title_state) - .build() + .build(FieldId::AddIssueModal(IssueFieldId::Title)) .into_node(); StyledField::build() .label("Epic name") diff --git a/jirs-client/src/modal/issues/issue_details.rs b/jirs-client/src/modal/issues/issue_details.rs index db4648cc..e892be00 100644 --- a/jirs-client/src/modal/issues/issue_details.rs +++ b/jirs-client/src/modal/issues/issue_details.rs @@ -426,30 +426,30 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node { .build() .into_node(); - let issue_type_select = StyledSelect::build(FieldId::EditIssueModal( - EditIssueModalSection::Issue(IssueFieldId::Type), - )) - .dropdown_width(150) - .name("type") - .text_filter(top_type_state.text_filter.as_str()) - .opened(top_type_state.opened) - .valid(true) - .options( - IssueType::ordered() - .into_iter() - .map(|t| t.to_child().name("type")) - .collect(), - ) - .selected(vec![{ - let id = modal.id; - let issue_type = &payload.issue_type; - issue_type - .to_child() - .name("type") - .text(format!("{} - {}", issue_type, id)) - }]) - .build() - .into_node(); + let issue_type_select = StyledSelect::build() + .dropdown_width(150) + .name("type") + .text_filter(top_type_state.text_filter.as_str()) + .opened(top_type_state.opened) + .valid(true) + .options( + IssueType::ordered() + .into_iter() + .map(|t| t.to_child().name("type")) + .collect(), + ) + .selected(vec![{ + let id = modal.id; + let issue_type = &payload.issue_type; + issue_type + .to_child() + .name("type") + .text(format!("{} - {}", issue_type, id)) + }]) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Type, + ))) + .into_node(); div![ attrs![At::Class => "topActions"], @@ -471,16 +471,16 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .. } = modal; - let title = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Title, - ))) - .add_input_class("issueSummary") - .add_wrapper_class("issueSummary") - .add_wrapper_class("textarea") - .value(payload.title.as_str()) - .valid(payload.title.len() >= 3) - .build() - .into_node(); + let title = StyledInput::build() + .add_input_class("issueSummary") + .add_wrapper_class("issueSummary") + .add_wrapper_class("textarea") + .value(payload.title.as_str()) + .valid(payload.title.len() >= 3) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Title, + ))) + .into_node(); let description_text = payload.description.as_ref().cloned().unwrap_or_default(); let description = StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( @@ -659,114 +659,114 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .. } = modal; - let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::IssueStatusId, - ))) - .name("status") - .opened(status_state.opened) - .normal() - .text_filter(status_state.text_filter.as_str()) - .options( - model - .issue_statuses - .iter() - .map(|opt| opt.to_child().name("status")) - .collect(), - ) - .selected( - model - .issue_statuses - .iter() - .filter(|is| is.id == payload.issue_status_id) - .map(|is| is.to_child().name("status")) - .collect(), - ) - .valid(true) - .build() - .into_node(); + let status = StyledSelect::build() + .name("status") + .opened(status_state.opened) + .normal() + .text_filter(status_state.text_filter.as_str()) + .options( + model + .issue_statuses + .iter() + .map(|opt| opt.to_child().name("status")) + .collect(), + ) + .selected( + model + .issue_statuses + .iter() + .filter(|is| is.id == payload.issue_status_id) + .map(|is| is.to_child().name("status")) + .collect(), + ) + .valid(true) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::IssueStatusId, + ))) + .into_node(); let status_field = StyledField::build() .input(status) .label("Status") .build() .into_node(); - let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Assignees, - ))) - .name("assignees") - .opened(assignees_state.opened) - .empty() - .multi() - .text_filter(assignees_state.text_filter.as_str()) - .options( - model - .users - .iter() - .map(|user| user.to_child().name("assignees")) - .collect(), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.user_ids.contains(&user.id)) - .map(|user| user.to_child().name("assignees")) - .collect(), - ) - .build() - .into_node(); + let assignees = StyledSelect::build() + .name("assignees") + .opened(assignees_state.opened) + .empty() + .multi() + .text_filter(assignees_state.text_filter.as_str()) + .options( + model + .users + .iter() + .map(|user| user.to_child().name("assignees")) + .collect(), + ) + .selected( + model + .users + .iter() + .filter(|user| payload.user_ids.contains(&user.id)) + .map(|user| user.to_child().name("assignees")) + .collect(), + ) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Assignees, + ))) + .into_node(); let assignees_field = StyledField::build() .input(assignees) .label("Assignees") .build() .into_node(); - let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Reporter, - ))) - .name("reporter") - .opened(reporter_state.opened) - .empty() - .text_filter(reporter_state.text_filter.as_str()) - .options( - model - .users - .iter() - .map(|user| user.to_child().name("reporter")) - .collect(), - ) - .selected( - model - .users - .iter() - .filter(|user| payload.reporter_id == user.id) - .map(|user| user.to_child().name("reporter")) - .collect(), - ) - .build() - .into_node(); + let reporter = StyledSelect::build() + .name("reporter") + .opened(reporter_state.opened) + .empty() + .text_filter(reporter_state.text_filter.as_str()) + .options( + model + .users + .iter() + .map(|user| user.to_child().name("reporter")) + .collect(), + ) + .selected( + model + .users + .iter() + .filter(|user| payload.reporter_id == user.id) + .map(|user| user.to_child().name("reporter")) + .collect(), + ) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Reporter, + ))) + .into_node(); let reporter_field = StyledField::build() .input(reporter) .label("Reporter") .build() .into_node(); - let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( - IssueFieldId::Priority, - ))) - .name("priority") - .opened(priority_state.opened) - .empty() - .text_filter(priority_state.text_filter.as_str()) - .options( - IssuePriority::ordered() - .into_iter() - .map(|p| p.to_child().name("priority")) - .collect(), - ) - .selected(vec![payload.priority.to_child().name("priority")]) - .build() - .into_node(); + let priority = StyledSelect::build() + .name("priority") + .opened(priority_state.opened) + .empty() + .text_filter(priority_state.text_filter.as_str()) + .options( + IssuePriority::ordered() + .into_iter() + .map(|p| p.to_child().name("priority")) + .collect(), + ) + .selected(vec![payload.priority.to_child().name("priority")]) + .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( + IssueFieldId::Priority, + ))) + .into_node(); let priority_field = StyledField::build() .input(priority) .label("Priority") diff --git a/jirs-client/src/modal/time_tracking.rs b/jirs-client/src/modal/time_tracking.rs index 6d258c2a..9ba8d769 100644 --- a/jirs-client/src/modal/time_tracking.rs +++ b/jirs-client/src/modal/time_tracking.rs @@ -91,7 +91,7 @@ pub fn time_tracking_field( ) -> Node { let input = match time_tracking_type { TimeTracking::Untracked => empty![], - TimeTracking::Fibonacci => StyledSelect::build(field_id) + TimeTracking::Fibonacci => StyledSelect::build() .selected( select_state .values @@ -106,12 +106,12 @@ pub fn time_tracking_field( .map(|v| v.to_child()) .collect(), ) - .build() + .build(field_id) .into_node(), - TimeTracking::Hourly => StyledInput::build(field_id) + TimeTracking::Hourly => StyledInput::build() .state(input_state) .valid(true) - .build() + .build(field_id) .into_node(), }; StyledField::build() diff --git a/jirs-client/src/profile/view.rs b/jirs-client/src/profile/view.rs index 00c03554..e5d5ee73 100644 --- a/jirs-client/src/profile/view.rs +++ b/jirs-client/src/profile/view.rs @@ -26,11 +26,11 @@ pub fn view(model: &Model) -> Node { .build() .into_node(); - let username = StyledInput::build(FieldId::Profile(UsersFieldId::Username)) + let username = StyledInput::build() .state(&page.name) .valid(true) .primary() - .build() + .build(FieldId::Profile(UsersFieldId::Username)) .into_node(); let username_field = StyledField::build() .label("Username") @@ -38,11 +38,11 @@ pub fn view(model: &Model) -> Node { .build() .into_node(); - let email = StyledInput::build(FieldId::Profile(UsersFieldId::Username)) + let email = StyledInput::build() .state(&page.email) .valid(true) .primary() - .build() + .build(FieldId::Profile(UsersFieldId::Username)) .into_node(); let email_field = StyledField::build() .label("E-Mail") @@ -97,7 +97,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node { joined_projects.insert(p.project_id, p); } - StyledSelect::build(FieldId::Profile(UsersFieldId::CurrentProject)) + StyledSelect::build() .name("current_project") .normal() .options( @@ -117,7 +117,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node { .collect(), ) .state(&page.current_project) - .build() + .build(FieldId::Profile(UsersFieldId::CurrentProject)) .into_node() }; StyledField::build() diff --git a/jirs-client/src/project/view.rs b/jirs-client/src/project/view.rs index 17110f34..c86194c3 100644 --- a/jirs-client/src/project/view.rs +++ b/jirs-client/src/project/view.rs @@ -61,11 +61,11 @@ fn project_board_filters(model: &Model) -> Node { _ => return empty![], }; - let search_input = StyledInput::build(FieldId::TextFilterBoard) + let search_input = StyledInput::build() .icon(Icon::Search) .valid(true) .value(project_page.text_filter.as_str()) - .build() + .build(FieldId::TextFilterBoard) .into_node(); let only_my = StyledButton::build() diff --git a/jirs-client/src/project_settings/view.rs b/jirs-client/src/project_settings/view.rs index d071c3bb..14b36271 100644 --- a/jirs-client/src/project_settings/view.rs +++ b/jirs-client/src/project_settings/view.rs @@ -44,17 +44,16 @@ pub fn view(model: &model::Model) -> Node { let category_field = category_field(page); - let time_tracking = - StyledCheckbox::build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) - .options(vec![ - TimeTracking::Untracked.to_child(), - TimeTracking::Fibonacci.to_child(), - TimeTracking::Hourly.to_child(), - ]) - .state(&page.time_tracking) - .add_class("timeTracking") - .build() - .into_node(); + let time_tracking = StyledCheckbox::build() + .options(vec![ + TimeTracking::Untracked.to_child(), + TimeTracking::Fibonacci.to_child(), + TimeTracking::Hourly.to_child(), + ]) + .state(&page.time_tracking) + .add_class("timeTracking") + .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) + .into_node(); let time_tracking_type: TimeTracking = page.time_tracking.value.into(); let time_tracking_field = StyledField::build() .input(time_tracking) @@ -162,7 +161,7 @@ fn description_field(page: &ProjectSettingsPage) -> Node { /// Build project category dropdown with styled field wrapper fn category_field(page: &ProjectSettingsPage) -> Node { - let category = StyledSelect::build(FieldId::ProjectSettings(ProjectFieldId::Category)) + let category = StyledSelect::build() .opened(page.project_category_state.opened) .text_filter(page.project_category_state.text_filter.as_str()) .valid(true) @@ -180,7 +179,7 @@ fn category_field(page: &ProjectSettingsPage) -> Node { .cloned() .unwrap_or_default() .to_child()]) - .build() + .build(FieldId::ProjectSettings(ProjectFieldId::Category)) .into_node(); StyledField::build() .label("Project Category") @@ -242,12 +241,12 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node { ))) }); - let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) + let input = StyledInput::build() .state(&page.name) .primary() .auto_focus() .on_input_ev(blur) - .build() + .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) .into_node(); div![ @@ -277,12 +276,12 @@ fn column_preview( ProjectPageChange::EditIssueStatusName(None), )) }); - let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) + let input = StyledInput::build() .state(&page.name) .primary() .auto_focus() .on_input_ev(blur) - .build() + .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) .into_node(); div![class!["columnPreview"], div![class!["columnName"], input]] diff --git a/jirs-client/src/shared/styled_checkbox.rs b/jirs-client/src/shared/styled_checkbox.rs index 68050f72..bbfbf6d8 100644 --- a/jirs-client/src/shared/styled_checkbox.rs +++ b/jirs-client/src/shared/styled_checkbox.rs @@ -140,9 +140,8 @@ impl ToNode for StyledCheckbox { } impl StyledCheckbox { - pub fn build(field_id: FieldId) -> StyledCheckboxBuilder { + pub fn build() -> StyledCheckboxBuilder { StyledCheckboxBuilder { - id: field_id, options: vec![], selected: 0, class_list: vec![], @@ -151,7 +150,6 @@ impl StyledCheckbox { } pub struct StyledCheckboxBuilder { - id: FieldId, options: Vec, selected: u32, class_list: Vec, @@ -176,9 +174,9 @@ impl StyledCheckboxBuilder { self } - pub fn build(self) -> StyledCheckbox { + pub fn build(self, field_id: FieldId) -> StyledCheckbox { StyledCheckbox { - id: self.id, + id: field_id, options: self.options, selected: self.selected, class_list: self.class_list, diff --git a/jirs-client/src/shared/styled_input.rs b/jirs-client/src/shared/styled_input.rs index f6ce8f6f..b3ac6b72 100644 --- a/jirs-client/src/shared/styled_input.rs +++ b/jirs-client/src/shared/styled_input.rs @@ -81,9 +81,8 @@ pub struct StyledInput { } impl StyledInput { - pub fn build(id: FieldId) -> StyledInputBuilder { + pub fn build() -> StyledInputBuilder { StyledInputBuilder { - id, icon: None, valid: None, value: None, @@ -99,7 +98,6 @@ impl StyledInput { #[derive(Debug)] pub struct StyledInputBuilder { - id: FieldId, icon: Option, valid: Option, value: Option, @@ -166,9 +164,9 @@ impl StyledInputBuilder { self } - pub fn build(self) -> StyledInput { + pub fn build(self, id: FieldId) -> StyledInput { StyledInput { - id: self.id, + id, icon: self.icon, valid: self.valid.unwrap_or_default(), value: self.value, diff --git a/jirs-client/src/shared/styled_rte.rs b/jirs-client/src/shared/styled_rte.rs index d238df5f..dafd7947 100644 --- a/jirs-client/src/shared/styled_rte.rs +++ b/jirs-client/src/shared/styled_rte.rs @@ -2,9 +2,10 @@ use seed::{prelude::*, *}; use crate::shared::styled_button::StyledButton; use crate::shared::styled_icon::{Icon, StyledIcon}; +use crate::shared::styled_select::{StyledSelect, StyledSelectState}; use crate::shared::styled_tooltip::StyledTooltip; -use crate::shared::ToNode; -use crate::{FieldId, Msg}; +use crate::shared::{ToChild, ToNode}; +use crate::{FieldId, Msg, RteField}; #[derive(Debug, Clone, Copy)] pub enum HeadingSize { @@ -80,6 +81,7 @@ pub enum RteMsg { // code InsertCode(bool), + CodeSetLang(String), RequestFocus(uuid::Uuid), } @@ -146,6 +148,7 @@ impl RteMsg { RteMsg::InsertTable { .. } => None, // code RteMsg::InsertCode(_) => None, + RteMsg::CodeSetLang(_) => None, // indent RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")), @@ -181,7 +184,7 @@ pub struct StyledRteTableState { #[derive(Debug, Clone)] pub struct StyledRteCodeState { pub visible: bool, - pub lang: String, + pub lang: StyledSelectState, } #[derive(Debug)] @@ -197,7 +200,7 @@ pub struct StyledRteState { impl StyledRteState { pub fn new(field_id: FieldId) -> Self { Self { - field_id, + field_id: field_id.clone(), value: String::new(), table_tooltip: StyledRteTableState { visible: false, @@ -206,7 +209,10 @@ impl StyledRteState { }, code_tooltip: StyledRteCodeState { visible: false, - lang: "".to_string(), + lang: StyledSelectState::new( + FieldId::Rte(RteField::CodeLang(Box::new(field_id.clone()))), + vec![], + ), }, range: None, identifier: uuid::Uuid::new_v4(), @@ -214,6 +220,7 @@ impl StyledRteState { } pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders) { + self.code_tooltip.lang.update(msg, orders); let m = match msg { Msg::Rte(m, field) if field == &self.field_id => m, _ => return, @@ -235,12 +242,14 @@ impl StyledRteState { self.schedule_focus(orders); } _ => match m { + // code RteMsg::InsertCode(b) => { if *b { self.store_range(); } self.code_tooltip.visible = *b; } + // table RteMsg::TableSetRows(n) => { self.table_tooltip.rows = *n; } @@ -329,19 +338,24 @@ pub struct StyledRte { field_id: FieldId, table_tooltip: StyledRteTableState, identifier: Option, + code_tooltip: StyledRteCodeState, // value: String, } impl StyledRte { pub fn build(field_id: FieldId) -> StyledRteBuilder { StyledRteBuilder { - field_id, + field_id: field_id.clone(), value: String::new(), table_tooltip: StyledRteTableState { visible: false, rows: 0, cols: 0, }, + code_tooltip: StyledRteCodeState { + visible: false, + lang: StyledSelectState::new(field_id.clone(), vec![]), + }, identifier: None, } } @@ -357,6 +371,7 @@ pub struct StyledRteBuilder { field_id: FieldId, value: String, table_tooltip: StyledRteTableState, + code_tooltip: StyledRteCodeState, identifier: Option, } @@ -364,7 +379,8 @@ impl StyledRteBuilder { pub fn state(mut self, state: &StyledRteState) -> Self { self.value = state.value.clone(); self.table_tooltip = state.table_tooltip.clone(); - self.identifier = Some(state.identifier.clone()); + self.code_tooltip = state.code_tooltip.clone(); + self.identifier = Some(state.identifier); self } @@ -374,6 +390,7 @@ impl StyledRteBuilder { // value: self.value, table_tooltip: self.table_tooltip, identifier: self.identifier, + code_tooltip: self.code_tooltip, } } } @@ -623,42 +640,55 @@ fn first_row(values: &StyledRte) -> Node { Some(Msg::Rte(RteMsg::Italic, field_id)) }), ); - let field_id = values.field_id.clone(); - let underline_button = styled_rte_button( - "Underline", - Icon::Underline, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Underscore, field_id)) - }), - ); - let field_id = values.field_id.clone(); - let strike_through_button = styled_rte_button( - "StrikeThrough", - Icon::StrikeThrough, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Strikethrough, field_id)) - }), - ); - let field_id = values.field_id.clone(); - let subscript_button = styled_rte_button( - "Subscript", - Icon::Subscript, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Subscript, field_id)) - }), - ); - let field_id = values.field_id.clone(); - let superscript_button = styled_rte_button( - "Superscript", - Icon::Superscript, - mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::Superscript, field_id)) - }), - ); + + let underline_button = { + let field_id = values.field_id.clone(); + styled_rte_button( + "Underline", + Icon::Underline, + mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::Underscore, field_id)) + }), + ) + }; + + let strike_through_button = { + let field_id = values.field_id.clone(); + styled_rte_button( + "StrikeThrough", + Icon::StrikeThrough, + mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::Strikethrough, field_id)) + }), + ) + }; + + let subscript_button = { + let field_id = values.field_id.clone(); + styled_rte_button( + "Subscript", + Icon::Subscript, + mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::Subscript, field_id)) + }), + ) + }; + + let superscript_button = { + let field_id = values.field_id.clone(); + styled_rte_button( + "Superscript", + Icon::Superscript, + mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::Superscript, field_id)) + }), + ) + }; + div![ class!["group formatting"], bold_button, @@ -821,7 +851,7 @@ fn second_row(values: &StyledRte) -> Node { }), ) }; - let code_alt_button = { + let mut code_alt_button = { let field_id = values.field_id.clone(); styled_rte_button( "Insert code", @@ -832,6 +862,7 @@ fn second_row(values: &StyledRte) -> Node { }), ) }; + code_alt_button.add_child(code_tooltip(values)); div![ class!["group insert"], @@ -887,33 +918,46 @@ fn table_tooltip(values: &StyledRte) -> Node { rows, cols, } = values.table_tooltip; - let field_id = values.field_id.clone(); - let on_rows_change = input_ev(Ev::Change, move |v| { - v.parse::() - .ok() - .map(|n| Msg::Rte(RteMsg::TableSetRows(n), field_id)) - }); - let field_id = values.field_id.clone(); - let on_cols_change = input_ev(Ev::Change, move |v| { - v.parse::() - .ok() - .map(|n| Msg::Rte(RteMsg::TableSetColumns(n), field_id)) - }); - let field_id = values.field_id.clone(); - let close_table_tooltip = StyledButton::build() - .empty() - .icon(Icon::Close) - .on_click(mouse_ev(Ev::Click, move |ev| { + + let on_rows_change = { + let field_id = values.field_id.clone(); + input_ev(Ev::Change, move |v| { + v.parse::() + .ok() + .map(|n| Msg::Rte(RteMsg::TableSetRows(n), field_id)) + }) + }; + + let on_cols_change = { + let field_id = values.field_id.clone(); + input_ev(Ev::Change, move |v| { + v.parse::() + .ok() + .map(|n| Msg::Rte(RteMsg::TableSetColumns(n), field_id)) + }) + }; + + let close_table_tooltip = { + let field_id = values.field_id.clone(); + StyledButton::build() + .empty() + .icon(Icon::Close) + .on_click(mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::TableSetVisibility(false), field_id)) + })) + .build() + .into_node() + }; + + let on_submit = { + let field_id = values.field_id.clone(); + mouse_ev(Ev::Click, move |ev| { ev.prevent_default(); - Some(Msg::Rte(RteMsg::TableSetVisibility(false), field_id)) - })) - .build() - .into_node(); - let field_id = values.field_id.clone(); - let on_submit = mouse_ev(Ev::Click, move |ev| { - ev.prevent_default(); - Some(Msg::Rte(RteMsg::InsertTable { rows, cols }, field_id)) - }); + Some(Msg::Rte(RteMsg::InsertTable { rows, cols }, field_id)) + }) + }; + StyledTooltip::build() .table_tooltip() .visible(visible) @@ -963,19 +1007,59 @@ fn styled_rte_button(title: &str, icon: Icon, handler: EventHandler) -> Nod ] } -fn insert_code() -> Node { +fn code_tooltip(values: &StyledRte) -> Node { + let StyledRteCodeState { visible, lang } = &values.code_tooltip; + let mut languages: Vec<&str> = crate::hi::SYNTAX_SET .syntaxes() .iter() .map(|s| s.name.as_str()) .collect(); languages.sort(); - let options: Vec> = languages + + let options: Vec<(String, u32)> = languages .into_iter() - .map(|name| option![attrs![At::Value => name], name]) + .enumerate() + .map(|(idx, label)| (label.to_string(), idx as u32)) .collect(); - seed::select![options] + let select_lang_node = StyledSelect::build() + .state(lang) + .selected( + lang.values + .get(0) + .and_then(|n| options.get(*n as usize)) + .map(|v| vec![v.to_child()]) + .unwrap_or_default(), + ) + .options(options.into_iter().map(|opt| opt.to_child()).collect()) + .normal() + .build(FieldId::Rte(RteField::CodeLang(Box::new( + values.field_id.clone(), + )))) + .into_node(); + + let close_tooltip = { + let field_id = values.field_id.clone(); + StyledButton::build() + .empty() + .icon(Icon::Close) + .on_click(mouse_ev(Ev::Click, move |ev| { + ev.prevent_default(); + Some(Msg::Rte(RteMsg::InsertCode(false), field_id)) + })) + .build() + .into_node() + }; + + StyledTooltip::build() + .code_tooltip() + .visible(*visible) + .add_child(h2!["Insert Code", close_tooltip]) + .add_child(select_lang_node) + .add_child(seed::textarea![]) + .build() + .into_node() } pub fn code_to_tag(code: &str) -> Node { diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs index a9daa09f..e7e45856 100644 --- a/jirs-client/src/shared/styled_select.rs +++ b/jirs-client/src/shared/styled_select.rs @@ -61,9 +61,10 @@ impl StyledSelectState { } pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders) { + let ref id = self.field_id; match msg { Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(b)) - if *field_id == self.field_id => + if field_id == id => { self.opened = *b; if !self.opened { @@ -71,22 +72,22 @@ impl StyledSelectState { } } Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(text)) - if *field_id == self.field_id => + if field_id == id => { self.text_filter = text.clone(); } Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(v))) - if field_id == &self.field_id => + if field_id == id => { self.values = vec![*v]; } Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) - if field_id == &self.field_id => + if field_id == id => { self.values.clear(); } Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v)) - if field_id == &self.field_id => + if field_id == id => { let mut old = vec![]; std::mem::swap(&mut old, &mut self.values); @@ -123,9 +124,8 @@ impl ToNode for StyledSelect { } impl StyledSelect { - pub fn build(id: FieldId) -> StyledSelectBuilder { + pub fn build() -> StyledSelectBuilder { StyledSelectBuilder { - id, variant: None, dropdown_width: None, name: None, @@ -142,7 +142,6 @@ impl StyledSelect { #[derive(Debug)] pub struct StyledSelectBuilder { - id: FieldId, variant: Option, dropdown_width: Option, name: Option, @@ -156,9 +155,9 @@ pub struct StyledSelectBuilder { } impl StyledSelectBuilder { - pub fn build(self) -> StyledSelect { + pub fn build(self, id: FieldId) -> StyledSelect { StyledSelect { - id: self.id, + id, variant: self.variant.unwrap_or_default(), dropdown_width: self.dropdown_width, name: self.name, @@ -278,12 +277,14 @@ pub fn render(values: StyledSelect) -> Node { } let action_icon = if clearable && !selected.is_empty() { - let field_id = id.clone(); - let on_click = mouse_ev(Ev::Click, move |ev| { - ev.stop_propagation(); - ev.prevent_default(); - Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) - }); + let on_click = { + let field_id = id.clone(); + mouse_ev(Ev::Click, move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) + }) + }; StyledIcon::build(Icon::Close) .add_class("chevronIcon") .on_click(on_click) @@ -305,10 +306,13 @@ pub fn render(values: StyledSelect) -> Node { let child = child.build(DisplayType::SelectOption); let value = child.value(); let node = child.into_node(); - let field_id = id.clone(); - let on_change = mouse_ev(Ev::Click, move |_| { - Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value))) - }); + + let on_change = { + let field_id = id.clone(); + mouse_ev(Ev::Click, move |_| { + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value))) + }) + }; div![ attrs![At::Class => "option"], on_change, @@ -386,7 +390,7 @@ fn render_value(mut content: Node) -> Node { content } -fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node { +fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node { let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node(); let child = opt.build(DisplayType::SelectValue); let value = child.value(); @@ -395,10 +399,13 @@ fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node "valueMultiItem"], opt, handler] } diff --git a/jirs-client/src/shared/styled_select_child.rs b/jirs-client/src/shared/styled_select_child.rs index 6825cf79..c87a7771 100644 --- a/jirs-client/src/shared/styled_select_child.rs +++ b/jirs-client/src/shared/styled_select_child.rs @@ -301,3 +301,16 @@ impl ToChild for u32 { .value(*self) } } + +pub type Label = String; +pub type Value = u32; + +impl ToChild for (Label, Value) { + type Builder = StyledSelectChildBuilder; + + fn to_child(&self) -> Self::Builder { + StyledSelectChild::build() + .text(self.0.as_str()) + .value(self.1) + } +} diff --git a/jirs-client/src/shared/styled_tooltip.rs b/jirs-client/src/shared/styled_tooltip.rs index fd17e6c3..762d8b10 100644 --- a/jirs-client/src/shared/styled_tooltip.rs +++ b/jirs-client/src/shared/styled_tooltip.rs @@ -8,6 +8,7 @@ pub enum Variant { About, Messages, TableBuilder, + CodeBuilder, } impl Default for Variant { @@ -22,6 +23,7 @@ impl std::fmt::Display for Variant { Variant::About => f.write_str("about"), Variant::Messages => f.write_str("messages"), Variant::TableBuilder => f.write_str("tableTooltip"), + Variant::CodeBuilder => f.write_str("codeTooltip"), } } } @@ -87,6 +89,11 @@ impl StyledTooltipBuilder { self } + pub fn code_tooltip(mut self) -> Self { + self.variant = Variant::CodeBuilder; + self + } + pub fn build(self) -> StyledTooltip { StyledTooltip { visible: self.visible, diff --git a/jirs-client/src/sign_in.rs b/jirs-client/src/sign_in.rs index 23704712..8ae7da67 100644 --- a/jirs-client/src/sign_in.rs +++ b/jirs-client/src/sign_in.rs @@ -92,10 +92,10 @@ pub fn view(model: &model::Model) -> Node { _ => return empty![], }; - let username = StyledInput::build(FieldId::SignIn(SignInFieldId::Username)) + let username = StyledInput::build() .value(page.username.as_str()) .valid(!page.username_touched || page.username.len() > 1) - .build() + .build(FieldId::SignIn(SignInFieldId::Username)) .into_node(); let username_field = StyledField::build() .label("Username") @@ -103,10 +103,10 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let email = StyledInput::build(FieldId::SignIn(SignInFieldId::Email)) + let email = StyledInput::build() .value(page.email.as_str()) .valid(!page.email_touched || is_email(page.email.as_str())) - .build() + .build(FieldId::SignIn(SignInFieldId::Email)) .into_node(); let email_field = StyledField::build() .label("E-Mail") @@ -164,10 +164,10 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let token = StyledInput::build(FieldId::SignIn(SignInFieldId::Token)) + let token = StyledInput::build() .value(page.token.as_str()) .valid(!page.token_touched || is_token(page.token.as_str())) - .build() + .build(FieldId::SignIn(SignInFieldId::Token)) .into_node(); let token_field = StyledField::build() .label("Single use token") diff --git a/jirs-client/src/sign_up.rs b/jirs-client/src/sign_up.rs index 9650e577..c863a8e4 100644 --- a/jirs-client/src/sign_up.rs +++ b/jirs-client/src/sign_up.rs @@ -68,10 +68,10 @@ pub fn view(model: &model::Model) -> Node { _ => return empty![], }; - let username = StyledInput::build(FieldId::SignUp(SignUpFieldId::Username)) + let username = StyledInput::build() .value(page.username.as_str()) .valid(!page.username_touched || page.username.len() > 1) - .build() + .build(FieldId::SignUp(SignUpFieldId::Username)) .into_node(); let username_field = StyledField::build() .label("Username") @@ -79,10 +79,10 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let email = StyledInput::build(FieldId::SignUp(SignUpFieldId::Email)) + let email = StyledInput::build() .value(page.email.as_str()) .valid(!page.email_touched || is_email(page.email.as_str())) - .build() + .build(FieldId::SignUp(SignUpFieldId::Email)) .into_node(); let email_field = StyledField::build() .label("E-Mail") diff --git a/jirs-client/src/users/view.rs b/jirs-client/src/users/view.rs index 9b131b96..f0f5b20b 100644 --- a/jirs-client/src/users/view.rs +++ b/jirs-client/src/users/view.rs @@ -22,10 +22,10 @@ pub fn view(model: &Model) -> Node { _ => return empty![], }; - let name = StyledInput::build(FieldId::Users(UsersFieldId::Username)) + let name = StyledInput::build() .valid(!page.name_touched || page.name.len() >= 3) .value(page.name.as_str()) - .build() + .build(FieldId::Users(UsersFieldId::Username)) .into_node(); let name_field = StyledField::build() .input(name) @@ -33,10 +33,10 @@ pub fn view(model: &Model) -> Node { .build() .into_node(); - let email = StyledInput::build(FieldId::Users(UsersFieldId::Email)) + let email = StyledInput::build() .valid(!page.email_touched || is_email(page.email.as_str())) .value(page.email.as_str()) - .build() + .build(FieldId::Users(UsersFieldId::Email)) .into_node(); let email_field = StyledField::build() .input(email) @@ -44,7 +44,7 @@ pub fn view(model: &Model) -> Node { .build() .into_node(); - let user_role = StyledSelect::build(FieldId::Users(UsersFieldId::UserRole)) + let user_role = StyledSelect::build() .name("user_role") .valid(true) .normal() @@ -56,7 +56,7 @@ pub fn view(model: &Model) -> Node { .map(|role| role.to_child()) .collect(), ) - .build() + .build(FieldId::Users(UsersFieldId::UserRole)) .into_node(); let user_role_field = StyledField::build() .input(user_role)