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)