Better select lang (broken text filter), style for code. Move to and then field_id

This commit is contained in:
Adrian Woźniak 2020-08-15 00:55:40 +02:00
parent 8deab1c2d5
commit 5267ce8a11
25 changed files with 832 additions and 353 deletions

28
Cargo.lock generated
View File

@ -1080,8 +1080,8 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
"synstructure", "synstructure",
] ]
@ -1790,10 +1790,10 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5" checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5"
dependencies = [ dependencies = [
"base64 0.10.1", "base64 0.10.1",
"email", "email",
"lettre", "lettre",
"mime", "mime",
"time 0.1.43", "time 0.1.43",
"uuid 0.7.4", "uuid 0.7.4",
] ]
@ -3078,7 +3078,6 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"walkdir", "walkdir",
"yaml-rust",
] ]
[[package]] [[package]]
@ -3090,9 +3089,9 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"rand 0.7.3", "rand 0.7.3",
"redox_syscall", "redox_syscall",
"remove_dir_all", "remove_dir_all",
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]] [[package]]
@ -3768,15 +3767,6 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" 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]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.1.0" version = "1.1.0"

157
README.md
View File

@ -51,6 +51,7 @@ https://git.sr.ht/~tsumanu/jirs
* [X] Grouping by Epic * [X] Grouping by Epic
* [X] Basic Rich Text Editor * [X] Basic Rich Text Editor
* [ ] Insert Code in Rich Text Editor * [ ] Insert Code in Rich Text Editor
* [X] Code syntax
* [ ] Personal settings to choose MDE (Markdown Editor) or RTE * [ ] Personal settings to choose MDE (Markdown Editor) or RTE
* [ ] Issues and filters view * [ ] Issues and filters view
* [ ] Issues and filters working filters * [ ] Issues and filters working filters
@ -180,3 +181,159 @@ sudo nginx -s reload
## Issue trackers ## Issue trackers
https://todo.sr.ht/~tsumanu/JIRS 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
<jirs-code-view lang="Rust" file-path="/some/path.rs">
struct Foo {
}
</jirs-code-view>
```
### 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

View File

@ -33,7 +33,7 @@ comrak = "*"
wee_alloc = "*" wee_alloc = "*"
lazy_static = "*" 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] [dependencies.wasm-bindgen]
version = "0.2.66" version = "0.2.66"

View File

@ -161,3 +161,54 @@
display: flex; display: flex;
justify-content: space-between; 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);
}

View File

@ -7,6 +7,7 @@
<link href="/logo2.svg" rel="icon"> <link href="/logo2.svg" rel="icon">
<title>JIRS</title> <title>JIRS</title>
<link href="/styles.css" rel="stylesheet" type="text/css"> <link href="/styles.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<main id="app"></main> <main id="app"></main>

View File

@ -1,5 +1,6 @@
use syntect::easy::HighlightLines; use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style}; use syntect::highlighting::{FontStyle, Style};
use syntect::parsing::SyntaxReference;
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen(final, js_name = JirsCodeBuilder)] #[wasm_bindgen(final, js_name = JirsCodeBuilder)]
@ -14,13 +15,22 @@ impl JirsCodeBuilder {
} }
#[wasm_bindgen] #[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) { let syntax = match crate::hi::SYNTAX_SET.find_syntax_by_name(lang) {
Some(s) => s, 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("<br />");
}
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 mut h = HighlightLines::new(syntax, &crate::hi::THEME_SET.themes["base16-ocean-dark"]); // inspired-github
let tokens = h.highlight(line, &crate::hi::SYNTAX_SET); let tokens = h.highlight(line, &crate::hi::SYNTAX_SET);
@ -36,8 +46,10 @@ impl JirsCodeBuilder {
"font-weight: bold" "font-weight: bold"
} else if font_style == FontStyle::ITALIC { } else if font_style == FontStyle::ITALIC {
"font-style: italic" "font-style: italic"
} else if font_style == FontStyle::UNDERLINE {
"text-decoration: underline"
} else { } else {
"font-decoration: underline" ""
}; };
let f = format!("rgba({}, {}, {}, {})", f.r, f.g, f.b, f.a); let f = format!("rgba({}, {}, {}, {})", f.r, f.g, f.b, f.a);
let b = format!("rgba({}, {}, {}, {})", b.r, b.g, b.b, b.a); let b = format!("rgba({}, {}, {}, {})", b.r, b.g, b.b, b.a);
@ -55,71 +67,225 @@ impl JirsCodeBuilder {
} }
pub fn define() { pub fn define() {
create_custom_element( {
"jirs-code-view", let el_name = "JirsCodeView";
"JirsCodeView", let tag = "jirs-code-view";
r#"
<style> ElementBuilder::default()
:host { display: block; border: 1px solid black; background: rgba(43, 48, 59, 255); padding: 1rem; } .identifier(el_name, tag)
.runtime("JirsCodeBuilder")
.body(
r#"
<style>
:host { display: block; border: 1px solid black; }
:host { margin-left: 400px; } :host { margin-left: 400px; }
#view span { white-space: pre; } #view { background: rgba(43, 48, 59, 255); padding: 1rem; }
#view span { white-space: pre; font-family: 'Source Code Pro', monospace; }
</style> </style>
<div id='file-name'></div>
<div id='view'></div> <div id='view'></div>
"#, "#,
); )
.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) { trait ToJs {
let source = format!( fn to_js(&self) -> String;
r#" }
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<N: Into<String>, S: Into<String>, 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<String>,
on_attr_changed: std::collections::HashMap<String, Vec<String>>,
}
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::<Vec<String>>()
.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 {{ class {name} extends HTMLElement {{
static RUNTIME = Symbol(); static RUNTIME = Symbol();
static SHADOW = Symbol(); static SHADOW = Symbol();
static get observedAttributes() {{ return ['lang']; }} {observe}
constructor() {{ constructor() {{
super(); super();
this[ {name} . SHADOW] = this.attachShadow({{ 'mode': 'closed' }}); {shadow} = this.attachShadow({{ 'mode': 'closed' }});
this[ {name} . SHADOW].innerHTML = `{html}`; {shadow}.innerHTML = `{html}`;
}} }}
connectedCallback() {{ connectedCallback() {{
this[ {name} . RUNTIME] = new JirsCodeBuilder(); const runtime = {runtime} = new JirsCodeBuilder();
const view = this[ {name} . SHADOW].querySelector('#view'); const shadow = {shadow};
view.innerHTML = '';
const lang = this.getAttribute('lang') || '';
setTimeout(() => {{ {on_connected}
const hi = () => {{
const line = code.shift();
if (line === undefined) return;
const s = this[ {name} . RUNTIME].hi(lang, line);
view.innerHTML += `${{s}}<br />`;
setTimeout(() => hi(), 10);
}};
hi();
}}, 10);
}} }}
disconnectedCallback() {{ disconnectedCallback() {{
this[ {name} . RUNTIME].free(); {runtime}.free();
}} }}
attributeChangedCallback(name, oldV, newV) {{ attributeChangedCallback(name, oldV, newV) {{
const runtime = {runtime};
const shadow = {shadow};
{attr_body}
}} }}
}} }}
customElements.define( '{tag}', {name}); customElements.define( '{tag}', {name});
"#, "#,
name = name, name = self.name,
tag = tag, tag = self.tag,
html = html, html = self.body,
); shadow = shadow,
{ runtime = runtime,
observe = observe,
attr_body = attr_body,
on_connected = on_connected,
)
}
}
impl ElementBuilder {
pub fn identifier<N: Into<String>, T: Into<String>>(mut self, name: N, tag: T) -> Self {
self.name = name.into();
self.tag = tag.into();
self
}
pub fn runtime<S: Into<String>>(mut self, runtime: S) -> Self {
self.runtime = runtime.into();
self
}
pub fn body<S: Into<String>>(mut self, body: S) -> Self {
self.body = body.into();
self
}
pub fn on_connected<B: ToJs>(mut self, c: B) -> Self {
self.on_connected.push(c.to_js());
self
}
pub fn on_attr_changed<N: Into<String>, R: Into<String>>(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::*; use seed::*;
match js_sys::eval(source.as_str()) { match js_sys::eval(source.as_str()) {
Ok(_v) => (), Ok(_v) => (),
Err(e) => error!(e), Err(e) => error!(e),
}; };
}; }
} }

View File

@ -11,6 +11,11 @@ pub enum EditIssueModalSection {
Comment(CommentFieldId), Comment(CommentFieldId),
} }
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum RteField {
CodeLang(Box<FieldId>),
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)] #[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum FieldId { pub enum FieldId {
SignIn(SignInFieldId), SignIn(SignInFieldId),
@ -26,6 +31,7 @@ pub enum FieldId {
CopyButtonLabel, CopyButtonLabel,
ProjectSettings(ProjectFieldId), ProjectSettings(ProjectFieldId),
Rte(RteField),
} }
impl std::fmt::Display for FieldId { impl std::fmt::Display for FieldId {
@ -120,6 +126,7 @@ impl std::fmt::Display for FieldId {
UsersFieldId::Avatar => f.write_str("profile-avatar"), UsersFieldId::Avatar => f.write_str("profile-avatar"),
UsersFieldId::CurrentProject => f.write_str("profile-currentProject"), UsersFieldId::CurrentProject => f.write_str("profile-currentProject"),
}, },
FieldId::Rte(..) => f.write_str("rte"),
} }
} }
} }

View File

@ -107,10 +107,10 @@ fn submit(_page: &InvitePage) -> Node<Msg> {
} }
fn token_field(page: &InvitePage) -> Node<Msg> { fn token_field(page: &InvitePage) -> Node<Msg> {
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()) .valid(!page.token_touched || is_token(page.token.as_str()) && page.error.is_none())
.value(page.token.as_str()) .value(page.token.as_str())
.build() .build(FieldId::Invite(InviteFieldId::Token))
.into_node(); .into_node();
StyledField::build() StyledField::build()

View File

@ -191,6 +191,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
styled_tooltip::Variant::Messages => { styled_tooltip::Variant::Messages => {
model.messages_tooltip_visible = !model.messages_tooltip_visible; model.messages_tooltip_visible = !model.messages_tooltip_visible;
} }
styled_tooltip::Variant::CodeBuilder => {}
Variant::TableBuilder => {} Variant::TableBuilder => {}
}, },
_ => (), _ => (),

View File

@ -23,7 +23,7 @@ where
.and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId)) .and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId))
.map(|epic| vec![epic.to_child()]) .map(|epic| vec![epic.to_child()])
.unwrap_or_default(); .unwrap_or_default();
let input = StyledSelect::build(field_id) let input = StyledSelect::build()
.name("epic") .name("epic")
.selected(selected) .selected(selected)
.options(model.epics.iter().map(|epic| epic.to_child()).collect()) .options(model.epics.iter().map(|epic| epic.to_child()).collect())
@ -32,7 +32,7 @@ where
.text_filter(modal.epic_state().text_filter.as_str()) .text_filter(modal.epic_state().text_filter.as_str())
.opened(modal.epic_state().opened) .opened(modal.epic_state().opened)
.valid(true) .valid(true)
.build() .build(field_id)
.into_node(); .into_node();
Some( Some(
StyledField::build() StyledField::build()

View File

@ -300,7 +300,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
} }
fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> { fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Type)) let select_type = StyledSelect::build()
.name("type") .name("type")
.normal() .normal()
.text_filter(modal.type_state.text_filter.as_str()) .text_filter(modal.type_state.text_filter.as_str())
@ -317,7 +317,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
) )
.to_child() .to_child()
.name("type")]) .name("type")])
.build() .build(FieldId::AddIssueModal(IssueFieldId::Type))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.label("Issue Type") .label("Issue Type")
@ -328,9 +328,9 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
} }
fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> { fn short_summary_field(modal: &AddIssueModal) -> Node<Msg> {
let short_summary = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) let short_summary = StyledInput::build()
.state(&modal.title_state) .state(&modal.title_state)
.build() .build(FieldId::AddIssueModal(IssueFieldId::Title))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.label("Short Summary") .label("Short Summary")
@ -359,7 +359,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.reporter_id .reporter_id
.or_else(|| model.user.as_ref().map(|u| u.id)) .or_else(|| model.user.as_ref().map(|u| u.id))
.unwrap_or_default(); .unwrap_or_default();
let reporter = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Reporter)) let reporter = StyledSelect::build()
.normal() .normal()
.text_filter(modal.reporter_state.text_filter.as_str()) .text_filter(modal.reporter_state.text_filter.as_str())
.opened(modal.reporter_state.opened) .opened(modal.reporter_state.opened)
@ -384,7 +384,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.collect(), .collect(),
) )
.valid(true) .valid(true)
.build() .build(FieldId::AddIssueModal(IssueFieldId::Reporter))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.input(reporter) .input(reporter)
@ -395,7 +395,7 @@ fn reporter_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
} }
fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> { fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let assignees = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Assignees)) let assignees = StyledSelect::build()
.normal() .normal()
.multi() .multi()
.text_filter(modal.assignees_state.text_filter.as_str()) .text_filter(modal.assignees_state.text_filter.as_str())
@ -421,7 +421,7 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.collect(), .collect(),
) )
.valid(true) .valid(true)
.build() .build(FieldId::AddIssueModal(IssueFieldId::Assignees))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.input(assignees) .input(assignees)
@ -432,7 +432,7 @@ fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
} }
fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> { fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
let select_priority = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Priority)) let select_priority = StyledSelect::build()
.name("priority") .name("priority")
.normal() .normal()
.text_filter(modal.priority_state.text_filter.as_str()) .text_filter(modal.priority_state.text_filter.as_str())
@ -445,7 +445,7 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
.collect(), .collect(),
) )
.selected(vec![modal.priority.to_child().name("priority")]) .selected(vec![modal.priority.to_child().name("priority")])
.build() .build(FieldId::AddIssueModal(IssueFieldId::Priority))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.label("Issue Type") .label("Issue Type")
@ -456,9 +456,9 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
} }
fn name_field(modal: &AddIssueModal) -> Node<Msg> { fn name_field(modal: &AddIssueModal) -> Node<Msg> {
let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) let name = StyledInput::build()
.state(&modal.title_state) .state(&modal.title_state)
.build() .build(FieldId::AddIssueModal(IssueFieldId::Title))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.label("Epic name") .label("Epic name")

View File

@ -426,30 +426,30 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let issue_type_select = StyledSelect::build(FieldId::EditIssueModal( let issue_type_select = StyledSelect::build()
EditIssueModalSection::Issue(IssueFieldId::Type), .dropdown_width(150)
)) .name("type")
.dropdown_width(150) .text_filter(top_type_state.text_filter.as_str())
.name("type") .opened(top_type_state.opened)
.text_filter(top_type_state.text_filter.as_str()) .valid(true)
.opened(top_type_state.opened) .options(
.valid(true) IssueType::ordered()
.options( .into_iter()
IssueType::ordered() .map(|t| t.to_child().name("type"))
.into_iter() .collect(),
.map(|t| t.to_child().name("type")) )
.collect(), .selected(vec![{
) let id = modal.id;
.selected(vec![{ let issue_type = &payload.issue_type;
let id = modal.id; issue_type
let issue_type = &payload.issue_type; .to_child()
issue_type .name("type")
.to_child() .text(format!("{} - {}", issue_type, id))
.name("type") }])
.text(format!("{} - {}", issue_type, id)) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
}]) IssueFieldId::Type,
.build() )))
.into_node(); .into_node();
div![ div![
attrs![At::Class => "topActions"], attrs![At::Class => "topActions"],
@ -471,16 +471,16 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let title = StyledInput::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let title = StyledInput::build()
IssueFieldId::Title, .add_input_class("issueSummary")
))) .add_wrapper_class("issueSummary")
.add_input_class("issueSummary") .add_wrapper_class("textarea")
.add_wrapper_class("issueSummary") .value(payload.title.as_str())
.add_wrapper_class("textarea") .valid(payload.title.len() >= 3)
.value(payload.title.as_str()) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
.valid(payload.title.len() >= 3) IssueFieldId::Title,
.build() )))
.into_node(); .into_node();
let description_text = payload.description.as_ref().cloned().unwrap_or_default(); let description_text = payload.description.as_ref().cloned().unwrap_or_default();
let description = StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let description = StyledEditor::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
@ -659,114 +659,114 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let status = StyledSelect::build()
IssueFieldId::IssueStatusId, .name("status")
))) .opened(status_state.opened)
.name("status") .normal()
.opened(status_state.opened) .text_filter(status_state.text_filter.as_str())
.normal() .options(
.text_filter(status_state.text_filter.as_str()) model
.options( .issue_statuses
model .iter()
.issue_statuses .map(|opt| opt.to_child().name("status"))
.iter() .collect(),
.map(|opt| opt.to_child().name("status")) )
.collect(), .selected(
) model
.selected( .issue_statuses
model .iter()
.issue_statuses .filter(|is| is.id == payload.issue_status_id)
.iter() .map(|is| is.to_child().name("status"))
.filter(|is| is.id == payload.issue_status_id) .collect(),
.map(|is| is.to_child().name("status")) )
.collect(), .valid(true)
) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
.valid(true) IssueFieldId::IssueStatusId,
.build() )))
.into_node(); .into_node();
let status_field = StyledField::build() let status_field = StyledField::build()
.input(status) .input(status)
.label("Status") .label("Status")
.build() .build()
.into_node(); .into_node();
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let assignees = StyledSelect::build()
IssueFieldId::Assignees, .name("assignees")
))) .opened(assignees_state.opened)
.name("assignees") .empty()
.opened(assignees_state.opened) .multi()
.empty() .text_filter(assignees_state.text_filter.as_str())
.multi() .options(
.text_filter(assignees_state.text_filter.as_str()) model
.options( .users
model .iter()
.users .map(|user| user.to_child().name("assignees"))
.iter() .collect(),
.map(|user| user.to_child().name("assignees")) )
.collect(), .selected(
) model
.selected( .users
model .iter()
.users .filter(|user| payload.user_ids.contains(&user.id))
.iter() .map(|user| user.to_child().name("assignees"))
.filter(|user| payload.user_ids.contains(&user.id)) .collect(),
.map(|user| user.to_child().name("assignees")) )
.collect(), .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
) IssueFieldId::Assignees,
.build() )))
.into_node(); .into_node();
let assignees_field = StyledField::build() let assignees_field = StyledField::build()
.input(assignees) .input(assignees)
.label("Assignees") .label("Assignees")
.build() .build()
.into_node(); .into_node();
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let reporter = StyledSelect::build()
IssueFieldId::Reporter, .name("reporter")
))) .opened(reporter_state.opened)
.name("reporter") .empty()
.opened(reporter_state.opened) .text_filter(reporter_state.text_filter.as_str())
.empty() .options(
.text_filter(reporter_state.text_filter.as_str()) model
.options( .users
model .iter()
.users .map(|user| user.to_child().name("reporter"))
.iter() .collect(),
.map(|user| user.to_child().name("reporter")) )
.collect(), .selected(
) model
.selected( .users
model .iter()
.users .filter(|user| payload.reporter_id == user.id)
.iter() .map(|user| user.to_child().name("reporter"))
.filter(|user| payload.reporter_id == user.id) .collect(),
.map(|user| user.to_child().name("reporter")) )
.collect(), .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
) IssueFieldId::Reporter,
.build() )))
.into_node(); .into_node();
let reporter_field = StyledField::build() let reporter_field = StyledField::build()
.input(reporter) .input(reporter)
.label("Reporter") .label("Reporter")
.build() .build()
.into_node(); .into_node();
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalSection::Issue( let priority = StyledSelect::build()
IssueFieldId::Priority, .name("priority")
))) .opened(priority_state.opened)
.name("priority") .empty()
.opened(priority_state.opened) .text_filter(priority_state.text_filter.as_str())
.empty() .options(
.text_filter(priority_state.text_filter.as_str()) IssuePriority::ordered()
.options( .into_iter()
IssuePriority::ordered() .map(|p| p.to_child().name("priority"))
.into_iter() .collect(),
.map(|p| p.to_child().name("priority")) )
.collect(), .selected(vec![payload.priority.to_child().name("priority")])
) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
.selected(vec![payload.priority.to_child().name("priority")]) IssueFieldId::Priority,
.build() )))
.into_node(); .into_node();
let priority_field = StyledField::build() let priority_field = StyledField::build()
.input(priority) .input(priority)
.label("Priority") .label("Priority")

View File

@ -91,7 +91,7 @@ pub fn time_tracking_field(
) -> Node<Msg> { ) -> Node<Msg> {
let input = match time_tracking_type { let input = match time_tracking_type {
TimeTracking::Untracked => empty![], TimeTracking::Untracked => empty![],
TimeTracking::Fibonacci => StyledSelect::build(field_id) TimeTracking::Fibonacci => StyledSelect::build()
.selected( .selected(
select_state select_state
.values .values
@ -106,12 +106,12 @@ pub fn time_tracking_field(
.map(|v| v.to_child()) .map(|v| v.to_child())
.collect(), .collect(),
) )
.build() .build(field_id)
.into_node(), .into_node(),
TimeTracking::Hourly => StyledInput::build(field_id) TimeTracking::Hourly => StyledInput::build()
.state(input_state) .state(input_state)
.valid(true) .valid(true)
.build() .build(field_id)
.into_node(), .into_node(),
}; };
StyledField::build() StyledField::build()

View File

@ -26,11 +26,11 @@ pub fn view(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let username = StyledInput::build(FieldId::Profile(UsersFieldId::Username)) let username = StyledInput::build()
.state(&page.name) .state(&page.name)
.valid(true) .valid(true)
.primary() .primary()
.build() .build(FieldId::Profile(UsersFieldId::Username))
.into_node(); .into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
@ -38,11 +38,11 @@ pub fn view(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::Profile(UsersFieldId::Username)) let email = StyledInput::build()
.state(&page.email) .state(&page.email)
.valid(true) .valid(true)
.primary() .primary()
.build() .build(FieldId::Profile(UsersFieldId::Username))
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")
@ -97,7 +97,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
joined_projects.insert(p.project_id, p); joined_projects.insert(p.project_id, p);
} }
StyledSelect::build(FieldId::Profile(UsersFieldId::CurrentProject)) StyledSelect::build()
.name("current_project") .name("current_project")
.normal() .normal()
.options( .options(
@ -117,7 +117,7 @@ fn build_current_project(model: &Model, page: &ProfilePage) -> Node<Msg> {
.collect(), .collect(),
) )
.state(&page.current_project) .state(&page.current_project)
.build() .build(FieldId::Profile(UsersFieldId::CurrentProject))
.into_node() .into_node()
}; };
StyledField::build() StyledField::build()

View File

@ -61,11 +61,11 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let search_input = StyledInput::build(FieldId::TextFilterBoard) let search_input = StyledInput::build()
.icon(Icon::Search) .icon(Icon::Search)
.valid(true) .valid(true)
.value(project_page.text_filter.as_str()) .value(project_page.text_filter.as_str())
.build() .build(FieldId::TextFilterBoard)
.into_node(); .into_node();
let only_my = StyledButton::build() let only_my = StyledButton::build()

View File

@ -44,17 +44,16 @@ pub fn view(model: &model::Model) -> Node<Msg> {
let category_field = category_field(page); let category_field = category_field(page);
let time_tracking = let time_tracking = StyledCheckbox::build()
StyledCheckbox::build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking)) .options(vec![
.options(vec![ TimeTracking::Untracked.to_child(),
TimeTracking::Untracked.to_child(), TimeTracking::Fibonacci.to_child(),
TimeTracking::Fibonacci.to_child(), TimeTracking::Hourly.to_child(),
TimeTracking::Hourly.to_child(), ])
]) .state(&page.time_tracking)
.state(&page.time_tracking) .add_class("timeTracking")
.add_class("timeTracking") .build(FieldId::ProjectSettings(ProjectFieldId::TimeTracking))
.build() .into_node();
.into_node();
let time_tracking_type: TimeTracking = page.time_tracking.value.into(); let time_tracking_type: TimeTracking = page.time_tracking.value.into();
let time_tracking_field = StyledField::build() let time_tracking_field = StyledField::build()
.input(time_tracking) .input(time_tracking)
@ -162,7 +161,7 @@ fn description_field(page: &ProjectSettingsPage) -> Node<Msg> {
/// Build project category dropdown with styled field wrapper /// Build project category dropdown with styled field wrapper
fn category_field(page: &ProjectSettingsPage) -> Node<Msg> { fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
let category = StyledSelect::build(FieldId::ProjectSettings(ProjectFieldId::Category)) let category = StyledSelect::build()
.opened(page.project_category_state.opened) .opened(page.project_category_state.opened)
.text_filter(page.project_category_state.text_filter.as_str()) .text_filter(page.project_category_state.text_filter.as_str())
.valid(true) .valid(true)
@ -180,7 +179,7 @@ fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
.cloned() .cloned()
.unwrap_or_default() .unwrap_or_default()
.to_child()]) .to_child()])
.build() .build(FieldId::ProjectSettings(ProjectFieldId::Category))
.into_node(); .into_node();
StyledField::build() StyledField::build()
.label("Project Category") .label("Project Category")
@ -242,12 +241,12 @@ fn add_column(page: &ProjectSettingsPage, column_style: &str) -> Node<Msg> {
))) )))
}); });
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) let input = StyledInput::build()
.state(&page.name) .state(&page.name)
.primary() .primary()
.auto_focus() .auto_focus()
.on_input_ev(blur) .on_input_ev(blur)
.build() .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.into_node(); .into_node();
div![ div![
@ -277,12 +276,12 @@ fn column_preview(
ProjectPageChange::EditIssueStatusName(None), ProjectPageChange::EditIssueStatusName(None),
)) ))
}); });
let input = StyledInput::build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName)) let input = StyledInput::build()
.state(&page.name) .state(&page.name)
.primary() .primary()
.auto_focus() .auto_focus()
.on_input_ev(blur) .on_input_ev(blur)
.build() .build(FieldId::ProjectSettings(ProjectFieldId::IssueStatusName))
.into_node(); .into_node();
div![class!["columnPreview"], div![class!["columnName"], input]] div![class!["columnPreview"], div![class!["columnName"], input]]

View File

@ -140,9 +140,8 @@ impl ToNode for StyledCheckbox {
} }
impl StyledCheckbox { impl StyledCheckbox {
pub fn build(field_id: FieldId) -> StyledCheckboxBuilder { pub fn build() -> StyledCheckboxBuilder {
StyledCheckboxBuilder { StyledCheckboxBuilder {
id: field_id,
options: vec![], options: vec![],
selected: 0, selected: 0,
class_list: vec![], class_list: vec![],
@ -151,7 +150,6 @@ impl StyledCheckbox {
} }
pub struct StyledCheckboxBuilder { pub struct StyledCheckboxBuilder {
id: FieldId,
options: Vec<ChildBuilder>, options: Vec<ChildBuilder>,
selected: u32, selected: u32,
class_list: Vec<String>, class_list: Vec<String>,
@ -176,9 +174,9 @@ impl StyledCheckboxBuilder {
self self
} }
pub fn build(self) -> StyledCheckbox { pub fn build(self, field_id: FieldId) -> StyledCheckbox {
StyledCheckbox { StyledCheckbox {
id: self.id, id: field_id,
options: self.options, options: self.options,
selected: self.selected, selected: self.selected,
class_list: self.class_list, class_list: self.class_list,

View File

@ -81,9 +81,8 @@ pub struct StyledInput {
} }
impl StyledInput { impl StyledInput {
pub fn build(id: FieldId) -> StyledInputBuilder { pub fn build() -> StyledInputBuilder {
StyledInputBuilder { StyledInputBuilder {
id,
icon: None, icon: None,
valid: None, valid: None,
value: None, value: None,
@ -99,7 +98,6 @@ impl StyledInput {
#[derive(Debug)] #[derive(Debug)]
pub struct StyledInputBuilder { pub struct StyledInputBuilder {
id: FieldId,
icon: Option<Icon>, icon: Option<Icon>,
valid: Option<bool>, valid: Option<bool>,
value: Option<String>, value: Option<String>,
@ -166,9 +164,9 @@ impl StyledInputBuilder {
self self
} }
pub fn build(self) -> StyledInput { pub fn build(self, id: FieldId) -> StyledInput {
StyledInput { StyledInput {
id: self.id, id,
icon: self.icon, icon: self.icon,
valid: self.valid.unwrap_or_default(), valid: self.valid.unwrap_or_default(),
value: self.value, value: self.value,

View File

@ -2,9 +2,10 @@ use seed::{prelude::*, *};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_select::{StyledSelect, StyledSelectState};
use crate::shared::styled_tooltip::StyledTooltip; use crate::shared::styled_tooltip::StyledTooltip;
use crate::shared::ToNode; use crate::shared::{ToChild, ToNode};
use crate::{FieldId, Msg}; use crate::{FieldId, Msg, RteField};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum HeadingSize { pub enum HeadingSize {
@ -80,6 +81,7 @@ pub enum RteMsg {
// code // code
InsertCode(bool), InsertCode(bool),
CodeSetLang(String),
RequestFocus(uuid::Uuid), RequestFocus(uuid::Uuid),
} }
@ -146,6 +148,7 @@ impl RteMsg {
RteMsg::InsertTable { .. } => None, RteMsg::InsertTable { .. } => None,
// code // code
RteMsg::InsertCode(_) => None, RteMsg::InsertCode(_) => None,
RteMsg::CodeSetLang(_) => None,
// indent // indent
RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")), RteMsg::ChangeIndent(RteIndentMsg::Increase) => Some(ExecCommand::new("indent")),
@ -181,7 +184,7 @@ pub struct StyledRteTableState {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledRteCodeState { pub struct StyledRteCodeState {
pub visible: bool, pub visible: bool,
pub lang: String, pub lang: StyledSelectState,
} }
#[derive(Debug)] #[derive(Debug)]
@ -197,7 +200,7 @@ pub struct StyledRteState {
impl StyledRteState { impl StyledRteState {
pub fn new(field_id: FieldId) -> Self { pub fn new(field_id: FieldId) -> Self {
Self { Self {
field_id, field_id: field_id.clone(),
value: String::new(), value: String::new(),
table_tooltip: StyledRteTableState { table_tooltip: StyledRteTableState {
visible: false, visible: false,
@ -206,7 +209,10 @@ impl StyledRteState {
}, },
code_tooltip: StyledRteCodeState { code_tooltip: StyledRteCodeState {
visible: false, visible: false,
lang: "".to_string(), lang: StyledSelectState::new(
FieldId::Rte(RteField::CodeLang(Box::new(field_id.clone()))),
vec![],
),
}, },
range: None, range: None,
identifier: uuid::Uuid::new_v4(), identifier: uuid::Uuid::new_v4(),
@ -214,6 +220,7 @@ impl StyledRteState {
} }
pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, orders: &mut impl Orders<Msg>) {
self.code_tooltip.lang.update(msg, orders);
let m = match msg { let m = match msg {
Msg::Rte(m, field) if field == &self.field_id => m, Msg::Rte(m, field) if field == &self.field_id => m,
_ => return, _ => return,
@ -235,12 +242,14 @@ impl StyledRteState {
self.schedule_focus(orders); self.schedule_focus(orders);
} }
_ => match m { _ => match m {
// code
RteMsg::InsertCode(b) => { RteMsg::InsertCode(b) => {
if *b { if *b {
self.store_range(); self.store_range();
} }
self.code_tooltip.visible = *b; self.code_tooltip.visible = *b;
} }
// table
RteMsg::TableSetRows(n) => { RteMsg::TableSetRows(n) => {
self.table_tooltip.rows = *n; self.table_tooltip.rows = *n;
} }
@ -329,19 +338,24 @@ pub struct StyledRte {
field_id: FieldId, field_id: FieldId,
table_tooltip: StyledRteTableState, table_tooltip: StyledRteTableState,
identifier: Option<uuid::Uuid>, identifier: Option<uuid::Uuid>,
code_tooltip: StyledRteCodeState,
// value: String, // value: String,
} }
impl StyledRte { impl StyledRte {
pub fn build(field_id: FieldId) -> StyledRteBuilder { pub fn build(field_id: FieldId) -> StyledRteBuilder {
StyledRteBuilder { StyledRteBuilder {
field_id, field_id: field_id.clone(),
value: String::new(), value: String::new(),
table_tooltip: StyledRteTableState { table_tooltip: StyledRteTableState {
visible: false, visible: false,
rows: 0, rows: 0,
cols: 0, cols: 0,
}, },
code_tooltip: StyledRteCodeState {
visible: false,
lang: StyledSelectState::new(field_id.clone(), vec![]),
},
identifier: None, identifier: None,
} }
} }
@ -357,6 +371,7 @@ pub struct StyledRteBuilder {
field_id: FieldId, field_id: FieldId,
value: String, value: String,
table_tooltip: StyledRteTableState, table_tooltip: StyledRteTableState,
code_tooltip: StyledRteCodeState,
identifier: Option<uuid::Uuid>, identifier: Option<uuid::Uuid>,
} }
@ -364,7 +379,8 @@ impl StyledRteBuilder {
pub fn state(mut self, state: &StyledRteState) -> Self { pub fn state(mut self, state: &StyledRteState) -> Self {
self.value = state.value.clone(); self.value = state.value.clone();
self.table_tooltip = state.table_tooltip.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 self
} }
@ -374,6 +390,7 @@ impl StyledRteBuilder {
// value: self.value, // value: self.value,
table_tooltip: self.table_tooltip, table_tooltip: self.table_tooltip,
identifier: self.identifier, identifier: self.identifier,
code_tooltip: self.code_tooltip,
} }
} }
} }
@ -623,42 +640,55 @@ fn first_row(values: &StyledRte) -> Node<Msg> {
Some(Msg::Rte(RteMsg::Italic, field_id)) Some(Msg::Rte(RteMsg::Italic, field_id))
}), }),
); );
let field_id = values.field_id.clone();
let underline_button = styled_rte_button( let underline_button = {
"Underline", let field_id = values.field_id.clone();
Icon::Underline, styled_rte_button(
mouse_ev(Ev::Click, move |ev| { "Underline",
ev.prevent_default(); Icon::Underline,
Some(Msg::Rte(RteMsg::Underscore, field_id)) 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| { let strike_through_button = {
ev.prevent_default(); let field_id = values.field_id.clone();
Some(Msg::Rte(RteMsg::Strikethrough, field_id)) styled_rte_button(
}), "StrikeThrough",
); Icon::StrikeThrough,
let field_id = values.field_id.clone(); mouse_ev(Ev::Click, move |ev| {
let subscript_button = styled_rte_button( ev.prevent_default();
"Subscript", Some(Msg::Rte(RteMsg::Strikethrough, field_id))
Icon::Subscript, }),
mouse_ev(Ev::Click, move |ev| { )
ev.prevent_default(); };
Some(Msg::Rte(RteMsg::Subscript, field_id))
}), let subscript_button = {
); let field_id = values.field_id.clone();
let field_id = values.field_id.clone(); styled_rte_button(
let superscript_button = styled_rte_button( "Subscript",
"Superscript", Icon::Subscript,
Icon::Superscript, mouse_ev(Ev::Click, move |ev| {
mouse_ev(Ev::Click, move |ev| { ev.prevent_default();
ev.prevent_default(); Some(Msg::Rte(RteMsg::Subscript, field_id))
Some(Msg::Rte(RteMsg::Superscript, 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![ div![
class!["group formatting"], class!["group formatting"],
bold_button, bold_button,
@ -821,7 +851,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
}), }),
) )
}; };
let code_alt_button = { let mut code_alt_button = {
let field_id = values.field_id.clone(); let field_id = values.field_id.clone();
styled_rte_button( styled_rte_button(
"Insert code", "Insert code",
@ -832,6 +862,7 @@ fn second_row(values: &StyledRte) -> Node<Msg> {
}), }),
) )
}; };
code_alt_button.add_child(code_tooltip(values));
div![ div![
class!["group insert"], class!["group insert"],
@ -887,33 +918,46 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
rows, rows,
cols, cols,
} = values.table_tooltip; } = values.table_tooltip;
let field_id = values.field_id.clone();
let on_rows_change = input_ev(Ev::Change, move |v| { let on_rows_change = {
v.parse::<u16>() let field_id = values.field_id.clone();
.ok() input_ev(Ev::Change, move |v| {
.map(|n| Msg::Rte(RteMsg::TableSetRows(n), field_id)) v.parse::<u16>()
}); .ok()
let field_id = values.field_id.clone(); .map(|n| Msg::Rte(RteMsg::TableSetRows(n), field_id))
let on_cols_change = input_ev(Ev::Change, move |v| { })
v.parse::<u16>() };
.ok()
.map(|n| Msg::Rte(RteMsg::TableSetColumns(n), field_id)) let on_cols_change = {
}); let field_id = values.field_id.clone();
let field_id = values.field_id.clone(); input_ev(Ev::Change, move |v| {
let close_table_tooltip = StyledButton::build() v.parse::<u16>()
.empty() .ok()
.icon(Icon::Close) .map(|n| Msg::Rte(RteMsg::TableSetColumns(n), field_id))
.on_click(mouse_ev(Ev::Click, move |ev| { })
};
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(); ev.prevent_default();
Some(Msg::Rte(RteMsg::TableSetVisibility(false), field_id)) Some(Msg::Rte(RteMsg::InsertTable { rows, cols }, 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))
});
StyledTooltip::build() StyledTooltip::build()
.table_tooltip() .table_tooltip()
.visible(visible) .visible(visible)
@ -963,19 +1007,59 @@ fn styled_rte_button(title: &str, icon: Icon, handler: EventHandler<Msg>) -> Nod
] ]
} }
fn insert_code() -> Node<Msg> { fn code_tooltip(values: &StyledRte) -> Node<Msg> {
let StyledRteCodeState { visible, lang } = &values.code_tooltip;
let mut languages: Vec<&str> = crate::hi::SYNTAX_SET let mut languages: Vec<&str> = crate::hi::SYNTAX_SET
.syntaxes() .syntaxes()
.iter() .iter()
.map(|s| s.name.as_str()) .map(|s| s.name.as_str())
.collect(); .collect();
languages.sort(); languages.sort();
let options: Vec<Node<Msg>> = languages
let options: Vec<(String, u32)> = languages
.into_iter() .into_iter()
.map(|name| option![attrs![At::Value => name], name]) .enumerate()
.map(|(idx, label)| (label.to_string(), idx as u32))
.collect(); .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<Msg> { pub fn code_to_tag(code: &str) -> Node<Msg> {

View File

@ -61,9 +61,10 @@ impl StyledSelectState {
} }
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) { pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) {
let ref id = self.field_id;
match msg { match msg {
Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(b)) Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(b))
if *field_id == self.field_id => if field_id == id =>
{ {
self.opened = *b; self.opened = *b;
if !self.opened { if !self.opened {
@ -71,22 +72,22 @@ impl StyledSelectState {
} }
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(text)) Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(text))
if *field_id == self.field_id => if field_id == id =>
{ {
self.text_filter = text.clone(); self.text_filter = text.clone();
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(v))) Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(v)))
if field_id == &self.field_id => if field_id == id =>
{ {
self.values = vec![*v]; self.values = vec![*v];
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None))
if field_id == &self.field_id => if field_id == id =>
{ {
self.values.clear(); self.values.clear();
} }
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v)) Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v))
if field_id == &self.field_id => if field_id == id =>
{ {
let mut old = vec![]; let mut old = vec![];
std::mem::swap(&mut old, &mut self.values); std::mem::swap(&mut old, &mut self.values);
@ -123,9 +124,8 @@ impl ToNode for StyledSelect {
} }
impl StyledSelect { impl StyledSelect {
pub fn build(id: FieldId) -> StyledSelectBuilder { pub fn build() -> StyledSelectBuilder {
StyledSelectBuilder { StyledSelectBuilder {
id,
variant: None, variant: None,
dropdown_width: None, dropdown_width: None,
name: None, name: None,
@ -142,7 +142,6 @@ impl StyledSelect {
#[derive(Debug)] #[derive(Debug)]
pub struct StyledSelectBuilder { pub struct StyledSelectBuilder {
id: FieldId,
variant: Option<Variant>, variant: Option<Variant>,
dropdown_width: Option<usize>, dropdown_width: Option<usize>,
name: Option<String>, name: Option<String>,
@ -156,9 +155,9 @@ pub struct StyledSelectBuilder {
} }
impl StyledSelectBuilder { impl StyledSelectBuilder {
pub fn build(self) -> StyledSelect { pub fn build(self, id: FieldId) -> StyledSelect {
StyledSelect { StyledSelect {
id: self.id, id,
variant: self.variant.unwrap_or_default(), variant: self.variant.unwrap_or_default(),
dropdown_width: self.dropdown_width, dropdown_width: self.dropdown_width,
name: self.name, name: self.name,
@ -278,12 +277,14 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
} }
let action_icon = if clearable && !selected.is_empty() { let action_icon = if clearable && !selected.is_empty() {
let field_id = id.clone(); let on_click = {
let on_click = mouse_ev(Ev::Click, move |ev| { let field_id = id.clone();
ev.stop_propagation(); mouse_ev(Ev::Click, move |ev| {
ev.prevent_default(); ev.stop_propagation();
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) ev.prevent_default();
}); Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None))
})
};
StyledIcon::build(Icon::Close) StyledIcon::build(Icon::Close)
.add_class("chevronIcon") .add_class("chevronIcon")
.on_click(on_click) .on_click(on_click)
@ -305,10 +306,13 @@ pub fn render(values: StyledSelect) -> Node<Msg> {
let child = child.build(DisplayType::SelectOption); let child = child.build(DisplayType::SelectOption);
let value = child.value(); let value = child.value();
let node = child.into_node(); let node = child.into_node();
let field_id = id.clone();
let on_change = mouse_ev(Ev::Click, move |_| { let on_change = {
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value))) let field_id = id.clone();
}); mouse_ev(Ev::Click, move |_| {
Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value)))
})
};
div![ div![
attrs![At::Class => "option"], attrs![At::Class => "option"],
on_change, on_change,
@ -386,7 +390,7 @@ fn render_value(mut content: Node<Msg>) -> Node<Msg> {
content content
} }
fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node<Msg> { fn into_multi_value(opt: StyledSelectChildBuilder, id: FieldId) -> Node<Msg> {
let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node(); let close_icon = StyledIcon::build(Icon::Close).size(14).build().into_node();
let child = opt.build(DisplayType::SelectValue); let child = opt.build(DisplayType::SelectValue);
let value = child.value(); let value = child.value();
@ -395,10 +399,13 @@ fn into_multi_value(opt: StyledSelectChildBuilder, field_id: FieldId) -> Node<Ms
opt.add_class("value"); opt.add_class("value");
opt.add_child(close_icon); opt.add_child(close_icon);
let handler = mouse_ev(Ev::Click, move |ev| { let handler = {
ev.stop_propagation(); let field_id = id.clone();
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(value)) mouse_ev(Ev::Click, move |ev| {
}); ev.stop_propagation();
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(value))
})
};
div![attrs![At::Class => "valueMultiItem"], opt, handler] div![attrs![At::Class => "valueMultiItem"], opt, handler]
} }

View File

@ -301,3 +301,16 @@ impl ToChild for u32 {
.value(*self) .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)
}
}

View File

@ -8,6 +8,7 @@ pub enum Variant {
About, About,
Messages, Messages,
TableBuilder, TableBuilder,
CodeBuilder,
} }
impl Default for Variant { impl Default for Variant {
@ -22,6 +23,7 @@ impl std::fmt::Display for Variant {
Variant::About => f.write_str("about"), Variant::About => f.write_str("about"),
Variant::Messages => f.write_str("messages"), Variant::Messages => f.write_str("messages"),
Variant::TableBuilder => f.write_str("tableTooltip"), Variant::TableBuilder => f.write_str("tableTooltip"),
Variant::CodeBuilder => f.write_str("codeTooltip"),
} }
} }
} }
@ -87,6 +89,11 @@ impl StyledTooltipBuilder {
self self
} }
pub fn code_tooltip(mut self) -> Self {
self.variant = Variant::CodeBuilder;
self
}
pub fn build(self) -> StyledTooltip { pub fn build(self) -> StyledTooltip {
StyledTooltip { StyledTooltip {
visible: self.visible, visible: self.visible,

View File

@ -92,10 +92,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let username = StyledInput::build(FieldId::SignIn(SignInFieldId::Username)) let username = StyledInput::build()
.value(page.username.as_str()) .value(page.username.as_str())
.valid(!page.username_touched || page.username.len() > 1) .valid(!page.username_touched || page.username.len() > 1)
.build() .build(FieldId::SignIn(SignInFieldId::Username))
.into_node(); .into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
@ -103,10 +103,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::SignIn(SignInFieldId::Email)) let email = StyledInput::build()
.value(page.email.as_str()) .value(page.email.as_str())
.valid(!page.email_touched || is_email(page.email.as_str())) .valid(!page.email_touched || is_email(page.email.as_str()))
.build() .build(FieldId::SignIn(SignInFieldId::Email))
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")
@ -164,10 +164,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let token = StyledInput::build(FieldId::SignIn(SignInFieldId::Token)) let token = StyledInput::build()
.value(page.token.as_str()) .value(page.token.as_str())
.valid(!page.token_touched || is_token(page.token.as_str())) .valid(!page.token_touched || is_token(page.token.as_str()))
.build() .build(FieldId::SignIn(SignInFieldId::Token))
.into_node(); .into_node();
let token_field = StyledField::build() let token_field = StyledField::build()
.label("Single use token") .label("Single use token")

View File

@ -68,10 +68,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let username = StyledInput::build(FieldId::SignUp(SignUpFieldId::Username)) let username = StyledInput::build()
.value(page.username.as_str()) .value(page.username.as_str())
.valid(!page.username_touched || page.username.len() > 1) .valid(!page.username_touched || page.username.len() > 1)
.build() .build(FieldId::SignUp(SignUpFieldId::Username))
.into_node(); .into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
@ -79,10 +79,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::SignUp(SignUpFieldId::Email)) let email = StyledInput::build()
.value(page.email.as_str()) .value(page.email.as_str())
.valid(!page.email_touched || is_email(page.email.as_str())) .valid(!page.email_touched || is_email(page.email.as_str()))
.build() .build(FieldId::SignUp(SignUpFieldId::Email))
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")

View File

@ -22,10 +22,10 @@ pub fn view(model: &Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let name = StyledInput::build(FieldId::Users(UsersFieldId::Username)) let name = StyledInput::build()
.valid(!page.name_touched || page.name.len() >= 3) .valid(!page.name_touched || page.name.len() >= 3)
.value(page.name.as_str()) .value(page.name.as_str())
.build() .build(FieldId::Users(UsersFieldId::Username))
.into_node(); .into_node();
let name_field = StyledField::build() let name_field = StyledField::build()
.input(name) .input(name)
@ -33,10 +33,10 @@ pub fn view(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledInput::build(FieldId::Users(UsersFieldId::Email)) let email = StyledInput::build()
.valid(!page.email_touched || is_email(page.email.as_str())) .valid(!page.email_touched || is_email(page.email.as_str()))
.value(page.email.as_str()) .value(page.email.as_str())
.build() .build(FieldId::Users(UsersFieldId::Email))
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.input(email) .input(email)
@ -44,7 +44,7 @@ pub fn view(model: &Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let user_role = StyledSelect::build(FieldId::Users(UsersFieldId::UserRole)) let user_role = StyledSelect::build()
.name("user_role") .name("user_role")
.valid(true) .valid(true)
.normal() .normal()
@ -56,7 +56,7 @@ pub fn view(model: &Model) -> Node<Msg> {
.map(|role| role.to_child()) .map(|role| role.to_child())
.collect(), .collect(),
) )
.build() .build(FieldId::Users(UsersFieldId::UserRole))
.into_node(); .into_node();
let user_role_field = StyledField::build() let user_role_field = StyledField::build()
.input(user_role) .input(user_role)