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"
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"

157
README.md
View File

@ -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
<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 = "*"
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"

View File

@ -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);
}

View File

@ -7,6 +7,7 @@
<link href="/logo2.svg" rel="icon">
<title>JIRS</title>
<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>
<body>
<main id="app"></main>

View File

@ -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("<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 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#"
<style>
:host { display: block; border: 1px solid black; background: rgba(43, 48, 59, 255); padding: 1rem; }
{
let el_name = "JirsCodeView";
let tag = "jirs-code-view";
ElementBuilder::default()
.identifier(el_name, tag)
.runtime("JirsCodeBuilder")
.body(
r#"
<style>
:host { display: block; border: 1px solid black; }
: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>
<div id='file-name'></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) {
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<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 {{
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}}<br />`;
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<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::*;
match js_sys::eval(source.as_str()) {
Ok(_v) => (),
Err(e) => error!(e),
};
};
}
}

View File

@ -11,6 +11,11 @@ pub enum EditIssueModalSection {
Comment(CommentFieldId),
}
#[derive(Clone, Debug, PartialOrd, PartialEq, Hash)]
pub enum RteField {
CodeLang(Box<FieldId>),
}
#[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"),
}
}
}

View File

@ -107,10 +107,10 @@ fn submit(_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())
.value(page.token.as_str())
.build()
.build(FieldId::Invite(InviteFieldId::Token))
.into_node();
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 => {
model.messages_tooltip_visible = !model.messages_tooltip_visible;
}
styled_tooltip::Variant::CodeBuilder => {}
Variant::TableBuilder => {}
},
_ => (),

View File

@ -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()

View File

@ -300,7 +300,7 @@ pub fn view(model: &Model, 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")
.normal()
.text_filter(modal.type_state.text_filter.as_str())
@ -317,7 +317,7 @@ fn issue_type_field(modal: &AddIssueModal) -> Node<Msg> {
)
.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<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)
.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<Msg> {
.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<Msg> {
.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<Msg> {
}
fn assignees_field(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
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<Msg> {
.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<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")
.normal()
.text_filter(modal.priority_state.text_filter.as_str())
@ -445,7 +445,7 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node<Msg> {
.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<Msg> {
}
fn name_field(modal: &AddIssueModal) -> Node<Msg> {
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")

View File

@ -426,30 +426,30 @@ fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.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<Msg> {
..
} = 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<Msg> {
..
} = 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")

View File

@ -91,7 +91,7 @@ pub fn time_tracking_field(
) -> Node<Msg> {
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()

View File

@ -26,11 +26,11 @@ pub fn view(model: &Model) -> Node<Msg> {
.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<Msg> {
.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<Msg> {
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<Msg> {
.collect(),
)
.state(&page.current_project)
.build()
.build(FieldId::Profile(UsersFieldId::CurrentProject))
.into_node()
};
StyledField::build()

View File

@ -61,11 +61,11 @@ fn project_board_filters(model: &Model) -> Node<Msg> {
_ => 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()

View File

@ -44,17 +44,16 @@ pub fn view(model: &model::Model) -> Node<Msg> {
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<Msg> {
/// Build project category dropdown with styled field wrapper
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)
.text_filter(page.project_category_state.text_filter.as_str())
.valid(true)
@ -180,7 +179,7 @@ fn category_field(page: &ProjectSettingsPage) -> Node<Msg> {
.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<Msg> {
)))
});
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]]

View File

@ -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<ChildBuilder>,
selected: u32,
class_list: Vec<String>,
@ -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,

View File

@ -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<Icon>,
valid: Option<bool>,
value: Option<String>,
@ -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,

View File

@ -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<Msg>) {
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<uuid::Uuid>,
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<uuid::Uuid>,
}
@ -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<Msg> {
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<Msg> {
}),
)
};
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<Msg> {
}),
)
};
code_alt_button.add_child(code_tooltip(values));
div![
class!["group insert"],
@ -887,33 +918,46 @@ fn table_tooltip(values: &StyledRte) -> Node<Msg> {
rows,
cols,
} = values.table_tooltip;
let field_id = values.field_id.clone();
let on_rows_change = input_ev(Ev::Change, move |v| {
v.parse::<u16>()
.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::<u16>()
.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::<u16>()
.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::<u16>()
.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<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
.syntaxes()
.iter()
.map(|s| s.name.as_str())
.collect();
languages.sort();
let options: Vec<Node<Msg>> = 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<Msg> {

View File

@ -61,9 +61,10 @@ impl StyledSelectState {
}
pub fn update(&mut self, msg: &Msg, _orders: &mut impl Orders<Msg>) {
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<Variant>,
dropdown_width: Option<usize>,
name: Option<String>,
@ -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<Msg> {
}
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<Msg> {
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<Msg>) -> Node<Msg> {
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 child = opt.build(DisplayType::SelectValue);
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_child(close_icon);
let handler = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(value))
});
let handler = {
let field_id = id.clone();
mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(value))
})
};
div![attrs![At::Class => "valueMultiItem"], opt, handler]
}

View File

@ -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)
}
}

View File

@ -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,

View File

@ -92,10 +92,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => 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<Msg> {
.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<Msg> {
.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")

View File

@ -68,10 +68,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => 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<Msg> {
.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")

View File

@ -22,10 +22,10 @@ pub fn view(model: &Model) -> Node<Msg> {
_ => 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<Msg> {
.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<Msg> {
.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<Msg> {
.map(|role| role.to_child())
.collect(),
)
.build()
.build(FieldId::Users(UsersFieldId::UserRole))
.into_node();
let user_role_field = StyledField::build()
.input(user_role)