Fix input, fix submit handle, add validations

This commit is contained in:
Adrian Wozniak 2020-04-17 15:31:55 +02:00
parent 2f870a5a0a
commit b22210d55a
10 changed files with 434 additions and 328 deletions

View File

@ -1,9 +1,8 @@
#login > .styledForm { #login > .styledForm {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 124.5px auto 24px; margin: 0 auto 24px;
width: 400px; width: 400px;
/*padding: 32px 40px;*/
background: rgb(255, 255, 255) none repeat scroll 0 0; background: rgb(255, 255, 255) none repeat scroll 0 0;
border-radius: 3px; border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px; box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px;
@ -11,6 +10,23 @@
color: var(--textMedium); color: var(--textMedium);
} }
#login > .styledForm:first-of-type {
margin-top: 124.5px;
margin-bottom: 0;
}
#login > .styledForm:last-of-type {
box-shadow: rgba(0, 0, 0, 0.1) 0 10px 10px;
}
#login > .styledForm:first-of-type > .formElement {
padding-bottom: 0;
}
#login > .styledForm:last-of-type > .formElement {
padding-top: 0;
}
#login > .styledForm > .formElement > .formHeading { #login > .styledForm > .formElement > .formHeading {
color: var(--textMedium); color: var(--textMedium);
font-size: 1em; font-size: 1em;

View File

@ -1,4 +1,9 @@
use std::str::FromStr;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use uuid::Uuid;
use jirs_data::WsMsg;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{LoginPage, Page, PageContent}; use crate::model::{LoginPage, Page, PageContent};
@ -6,12 +11,9 @@ use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_input::StyledInput;
use crate::shared::{outer_layout, write_auth_token, ToNode}; use crate::shared::{outer_layout, write_auth_token, ToNode};
use crate::{model, FieldId, LoginFieldId, Msg}; use crate::{model, FieldId, LoginFieldId, Msg};
use jirs_data::WsMsg;
use std::str::FromStr;
use uuid::Uuid;
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::Login { if model.page != Page::Login {
@ -31,12 +33,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => {
page.username = value; page.username = value;
page.username_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => {
page.email = value; page.email = value;
page.email_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => {
page.token = value; page.token = value;
page.token_touched = true;
} }
Msg::SignInRequest => { Msg::SignInRequest => {
send_ws_msg(WsMsg::AuthenticateRequest( send_ws_msg(WsMsg::AuthenticateRequest(
@ -77,11 +82,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let username = StyledTextarea::build() let username = StyledInput::build(FieldId::Login(LoginFieldId::Username))
.one_line()
.update_on(Ev::Change)
.value(page.username.as_str()) .value(page.username.as_str())
.build(FieldId::Login(LoginFieldId::Username)) .valid(!page.username_touched || page.username.len() > 1)
.build()
.into_node(); .into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
@ -89,11 +93,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledTextarea::build() let email = StyledInput::build(FieldId::Login(LoginFieldId::Email))
.one_line()
.update_on(Ev::Change)
.value(page.email.as_str()) .value(page.email.as_str())
.build(FieldId::Login(LoginFieldId::Email)) .valid(!page.email_touched || is_email(page.email.as_str()))
.build()
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")
@ -102,7 +105,9 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node(); .into_node();
let submit = if page.login_success { let submit = if page.login_success {
StyledButton::build().success().text("") StyledButton::build()
.success()
.text("✓ Please check your mail")
} else { } else {
StyledButton::build() StyledButton::build()
.primary() .primary()
@ -126,11 +131,24 @@ pub fn view(model: &model::Model) -> Node<Msg> {
span!["Why I don't see password?"] span!["Why I don't see password?"]
]; ];
let token = StyledTextarea::build() let sign_in_form = StyledForm::build()
.one_line() .heading("Sign In to your account")
.update_on(Ev::Input) .on_submit(ev(Ev::Submit, |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::SignInRequest
}))
.add_field(username_field)
.add_field(email_field)
.add_field(submit_field)
.add_field(no_pass_section)
.build()
.into_node();
let token = StyledInput::build(FieldId::Login(LoginFieldId::Token))
.value(page.token.as_str()) .value(page.token.as_str())
.build(FieldId::Login(LoginFieldId::Token)) .valid(!page.token_touched || is_token(page.token.as_str()))
.build()
.into_node(); .into_node();
let token_field = StyledField::build() let token_field = StyledField::build()
.label("Single use token") .label("Single use token")
@ -145,17 +163,42 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node(); .into_node();
let submit_token_field = StyledField::build().input(submit_token).build().into_node(); let submit_token_field = StyledField::build().input(submit_token).build().into_node();
let form = StyledForm::build() let bind_token_form = StyledForm::build()
.heading("Sign In to your account") .on_submit(ev(Ev::Submit, |ev| {
.add_field(username_field) ev.stop_propagation();
.add_field(email_field) ev.prevent_default();
.add_field(submit_field) Msg::BindClientRequest
.add_field(no_pass_section) }))
.add_field(token_field) .add_field(token_field)
.add_field(submit_token_field) .add_field(submit_token_field)
.build() .build()
.into_node(); .into_node();
let children = vec![form]; let children = vec![sign_in_form, bind_token_form];
outer_layout(model, "login", children) outer_layout(model, "login", children)
} }
fn is_token(s: &str) -> bool {
uuid::Uuid::from_str(s).is_ok()
}
fn is_email(s: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
for c in s.chars() {
match c {
'\n' | ' ' | '\t' | '\r' => return false,
'@' if !has_at => {
has_at = true;
}
'@' if has_at => return false,
'.' if has_at => {
has_dot = true;
}
_ if has_dot => return true,
_ => (),
}
}
return false;
}

View File

@ -151,9 +151,9 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let description = StyledTextarea::build() let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description))
.height(110) .height(110)
.build(FieldId::AddIssueModal(IssueFieldId::Description)) .build()
.into_node(); .into_node();
let description_field = StyledField::build() let description_field = StyledField::build()
.label("Description") .label("Description")

View File

@ -372,15 +372,15 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let title = StyledTextarea::build() let title = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
.value(payload.title.as_str()) IssueFieldId::Title,
.add_class("textarea") )))
.max_height(48) .value(payload.title.as_str())
.height(0) .add_class("textarea")
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( .max_height(48)
IssueFieldId::Title, .height(0)
))) .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(
@ -468,13 +468,13 @@ fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
)) ))
}); });
let text_area = StyledTextarea::build() let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment(
.value(form.body.as_str()) CommentFieldId::Body,
.placeholder("Add a comment...") )))
.build(FieldId::EditIssueModal(EditIssueModalSection::Comment( .value(form.body.as_str())
CommentFieldId::Body, .placeholder("Add a comment...")
))) .build()
.into_node(); .into_node();
let submit = StyledButton::build() let submit = StyledButton::build()
.variant(ButtonVariant::Primary) .variant(ButtonVariant::Primary)

View File

@ -225,6 +225,10 @@ pub struct LoginPage {
pub token: String, pub token: String,
pub login_success: bool, pub login_success: bool,
pub bad_token: String, pub bad_token: String,
// touched
pub username_touched: bool,
pub email_touched: bool,
pub token_touched: bool,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -84,12 +84,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
PageContent::ProjectSettings(page) => page, PageContent::ProjectSettings(page) => page,
_ => return empty![], _ => return empty![],
}; };
let name = StyledTextarea::build() let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name))
.value(page.payload.name.as_ref().cloned().unwrap_or_default()) .value(page.payload.name.as_ref().cloned().unwrap_or_default())
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
.build(FieldId::ProjectSettings(ProjectFieldId::Name)) .build()
.into_node(); .into_node();
let name_field = StyledField::build() let name_field = StyledField::build()
.label("Name") .label("Name")
@ -98,12 +98,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let url = StyledTextarea::build() let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url))
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default()) .value(page.payload.url.as_ref().cloned().unwrap_or_default())
.build(FieldId::ProjectSettings(ProjectFieldId::Url)) .build()
.into_node(); .into_node();
let url_field = StyledField::build() let url_field = StyledField::build()
.label("Url") .label("Url")

View File

@ -1,185 +1,185 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_textarea::StyledTextarea;
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::{FieldChange, FieldId, Msg}; use crate::{FieldChange, FieldId, Msg};
#[derive(Debug, Clone, PartialOrd, PartialEq, Hash)] #[derive(Debug, Clone, PartialOrd, PartialEq, Hash)]
pub enum Mode { pub enum Mode {
Editor, Editor,
View, View,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledEditorState { pub struct StyledEditorState {
pub mode: Mode, pub mode: Mode,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledEditor { pub struct StyledEditor {
id: FieldId, id: FieldId,
text: String, text: String,
mode: Mode, mode: Mode,
update_event: Ev, update_event: Ev,
} }
impl StyledEditor { impl StyledEditor {
pub fn build(id: FieldId) -> StyledEditorBuilder { pub fn build(id: FieldId) -> StyledEditorBuilder {
StyledEditorBuilder { StyledEditorBuilder {
id, id,
text: String::new(), text: String::new(),
mode: Mode::View, mode: Mode::View,
update_event: None, update_event: None,
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StyledEditorBuilder { pub struct StyledEditorBuilder {
id: FieldId, id: FieldId,
text: String, text: String,
mode: Mode, mode: Mode,
update_event: Option<Ev>, update_event: Option<Ev>,
} }
impl StyledEditorBuilder { impl StyledEditorBuilder {
pub fn text<S>(mut self, text: S) -> Self pub fn text<S>(mut self, text: S) -> Self
where where
S: Into<String>, S: Into<String>,
{ {
self.text = text.into(); self.text = text.into();
self self
} }
pub fn mode(mut self, mode: Mode) -> Self { pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode; self.mode = mode;
self self
} }
pub fn build(self) -> StyledEditor { pub fn build(self) -> StyledEditor {
StyledEditor { StyledEditor {
id: self.id, id: self.id,
text: self.text, text: self.text,
mode: self.mode, mode: self.mode,
update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp), update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp),
} }
} }
pub fn update_on(mut self, ev: Ev) -> Self { pub fn update_on(mut self, ev: Ev) -> Self {
self.update_event = Some(ev); self.update_event = Some(ev);
self self
} }
} }
impl ToNode for StyledEditor { impl ToNode for StyledEditor {
fn into_node(self) -> Node<Msg> { fn into_node(self) -> Node<Msg> {
render(self) render(self)
} }
} }
pub fn render(values: StyledEditor) -> Node<Msg> { pub fn render(values: StyledEditor) -> Node<Msg> {
let StyledEditor { let StyledEditor {
id, id,
text, text,
mode, mode,
update_event, update_event,
} = values; } = values;
let field_id = id.clone(); let field_id = id.clone();
let on_editor_clicked = mouse_ev(Ev::Click, move |ev| { let on_editor_clicked = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::Editor)) Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::Editor))
}); });
let field_id = id.clone(); let field_id = id.clone();
let on_view_clicked = mouse_ev(Ev::Click, move |ev| { let on_view_clicked = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::View)) Msg::ModalChanged(FieldChange::TabChanged(field_id, Mode::View))
}); });
let editor_id = format!("editor-{}", id); let editor_id = format!("editor-{}", id);
let view_id = format!("view-{}", id); let view_id = format!("view-{}", id);
let name = format!("styled-editor-{}", id); let name = format!("styled-editor-{}", id);
let text_area = StyledTextarea::build() let text_area = StyledTextarea::build(id)
.height(40) .height(40)
.update_on(update_event) .update_on(update_event)
.value(text.as_str()) .value(text.as_str())
.build(id) .build()
.into_node(); .into_node();
let parsed = comrak::markdown_to_html( let parsed = comrak::markdown_to_html(
text.as_str(), text.as_str(),
&comrak::ComrakOptions { &comrak::ComrakOptions {
hardbreaks: false, hardbreaks: false,
smart: true, smart: true,
github_pre_lang: true, github_pre_lang: true,
width: 0, width: 0,
default_info_string: None, default_info_string: None,
unsafe_: false, unsafe_: false,
ext_strikethrough: true, ext_strikethrough: true,
ext_tagfilter: true, ext_tagfilter: true,
ext_table: true, ext_table: true,
ext_autolink: true, ext_autolink: true,
ext_tasklist: true, ext_tasklist: true,
ext_superscript: true, ext_superscript: true,
ext_header_ids: None, ext_header_ids: None,
ext_footnotes: true, ext_footnotes: true,
ext_description_lists: true, ext_description_lists: true,
}, },
); );
let parsed_node = Node::from_html(parsed.as_str()); let parsed_node = Node::from_html(parsed.as_str());
let (editor_radio_node, view_radio_node) = match mode { let (editor_radio_node, view_radio_node) = match mode {
Mode::Editor => ( Mode::Editor => (
seed::input![ seed::input![
id![editor_id.as_str()], id![editor_id.as_str()],
attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true], attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio"; At::Checked => true],
], ],
seed::input![ seed::input![
id![view_id.as_str()], id![view_id.as_str()],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";], attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio";],
], ],
), ),
Mode::View => ( Mode::View => (
seed::input![ seed::input![
id![editor_id.as_str()], id![editor_id.as_str()],
class!["editorRadio"], class!["editorRadio"],
attrs![At::Type => "radio"; At::Name => name.as_str();], attrs![At::Type => "radio"; At::Name => name.as_str();],
], ],
seed::input![ seed::input![
id![view_id.as_str()], id![view_id.as_str()],
class!["viewRadio"], class!["viewRadio"],
attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true], attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true],
], ],
), ),
}; };
div![ div![
attrs![At::Class => "styledEditor"], attrs![At::Class => "styledEditor"],
label![ label![
if mode == Mode::Editor { if mode == Mode::Editor {
class!["navbar editorTab activeTab"] class!["navbar editorTab activeTab"]
} else { } else {
class!["navbar editorTab"] class!["navbar editorTab"]
}, },
attrs![At::For => editor_id.as_str()], attrs![At::For => editor_id.as_str()],
"Editor", "Editor",
on_editor_clicked on_editor_clicked
], ],
label![ label![
if mode == Mode::View { if mode == Mode::View {
class!["navbar viewTab activeTab"] class!["navbar viewTab activeTab"]
} else { } else {
class!["navbar viewTab"] class!["navbar viewTab"]
}, },
attrs![At::For => view_id.as_str()], attrs![At::For => view_id.as_str()],
"View", "View",
on_view_clicked on_view_clicked
], ],
editor_radio_node, editor_radio_node,
text_area, text_area,
view_radio_node, view_radio_node,
div![attrs![At::Class => "view"], parsed_node], div![attrs![At::Class => "view"], parsed_node],
] ]
} }

View File

@ -1,62 +1,79 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::Msg; use crate::Msg;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StyledForm { pub struct StyledForm {
heading: String, heading: String,
fields: Vec<Node<Msg>>, fields: Vec<Node<Msg>>,
} on_submit: Option<EventHandler<Msg>>,
}
impl StyledForm {
pub fn build() -> StyledFormBuilder { impl StyledForm {
StyledFormBuilder::default() pub fn build() -> StyledFormBuilder {
} StyledFormBuilder::default()
} }
}
impl ToNode for StyledForm {
fn into_node(self) -> Node<Msg> { impl ToNode for StyledForm {
render(self) fn into_node(self) -> Node<Msg> {
} render(self)
} }
}
#[derive(Debug, Default)]
pub struct StyledFormBuilder { #[derive(Debug, Default)]
fields: Vec<Node<Msg>>, pub struct StyledFormBuilder {
heading: String, fields: Vec<Node<Msg>>,
} heading: String,
on_submit: Option<EventHandler<Msg>>,
impl StyledFormBuilder { }
pub fn add_field(mut self, node: Node<Msg>) -> Self {
self.fields.push(node); impl StyledFormBuilder {
self pub fn add_field(mut self, node: Node<Msg>) -> Self {
} self.fields.push(node);
self
pub fn heading<S>(mut self, heading: S) -> Self }
where
S: Into<String>, pub fn heading<S>(mut self, heading: S) -> Self
{ where
self.heading = heading.into(); S: Into<String>,
self {
} self.heading = heading.into();
self
pub fn build(self) -> StyledForm { }
StyledForm {
heading: self.heading, pub fn on_submit(mut self, on_submit: EventHandler<Msg>) -> Self {
fields: self.fields, self.on_submit = Some(on_submit);
} self
} }
}
pub fn build(self) -> StyledForm {
pub fn render(values: StyledForm) -> Node<Msg> { StyledForm {
let StyledForm { heading, fields } = values; heading: self.heading,
div![ fields: self.fields,
attrs![At::Class => "styledForm"], on_submit: self.on_submit,
div![ }
class!["formElement"], }
div![class!["formHeading"], heading], }
fields
], pub fn render(values: StyledForm) -> Node<Msg> {
] let StyledForm {
} heading,
fields,
on_submit,
} = values;
let handlers = match on_submit {
Some(handler) => vec![handler],
_ => vec![],
};
seed::form![
handlers,
attrs![At::Class => "styledForm"],
div![
class!["formElement"],
div![class!["formHeading"], heading],
fields
],
]
}

View File

@ -94,17 +94,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
_ => empty![], _ => empty![],
}; };
let field_id = id.clone(); let field_id = id.clone();
let change_handler = keyboard_ev(Ev::KeyUp, move |event| { let change_handler = ev(Ev::Input, move |event| {
use wasm_bindgen::JsCast;
event.stop_propagation(); event.stop_propagation();
let value = event let target = event.target().unwrap();
.target() let input = seed::to_input(&target);
.unwrap() let value = input.value();
.dyn_ref::<web_sys::HtmlInputElement>()
.unwrap()
.value();
Msg::InputChanged(field_id, value) Msg::InputChanged(field_id, value)
}); });
let key_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation();
Msg::NoOp
});
div![ div![
attrs!(At::Class => wrapper_class_list.join(" ")), attrs!(At::Class => wrapper_class_list.join(" ")),
@ -116,6 +116,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
At::Type => input_type.unwrap_or_else(|| "text".to_string()), At::Type => input_type.unwrap_or_else(|| "text".to_string()),
], ],
change_handler, change_handler,
key_handler,
], ],
] ]
} }

View File

@ -22,13 +22,24 @@ impl ToNode for StyledTextarea {
} }
impl StyledTextarea { impl StyledTextarea {
pub fn build() -> StyledTextareaBuilder { pub fn build(field_id: FieldId) -> StyledTextareaBuilder {
StyledTextareaBuilder::default() StyledTextareaBuilder {
id: field_id,
height: None,
max_height: None,
on_change: None,
value: "".to_string(),
class_list: vec![],
update_event: None,
placeholder: None,
disable_auto_resize: false,
}
} }
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct StyledTextareaBuilder { pub struct StyledTextareaBuilder {
id: FieldId,
height: Option<usize>, height: Option<usize>,
max_height: Option<usize>, max_height: Option<usize>,
on_change: Option<EventHandler<Msg>>, on_change: Option<EventHandler<Msg>>,
@ -40,11 +51,6 @@ pub struct StyledTextareaBuilder {
} }
impl StyledTextareaBuilder { impl StyledTextareaBuilder {
#[inline]
pub fn one_line(self) -> Self {
self.disable_auto_resize().height(39).max_height(39)
}
#[inline] #[inline]
pub fn height(mut self, height: usize) -> Self { pub fn height(mut self, height: usize) -> Self {
self.height = Some(height); self.height = Some(height);
@ -94,9 +100,9 @@ impl StyledTextareaBuilder {
} }
#[inline] #[inline]
pub fn build(self, id: FieldId) -> StyledTextarea { pub fn build(self) -> StyledTextarea {
StyledTextarea { StyledTextarea {
id, id: self.id,
value: self.value, value: self.value,
height: self.height.unwrap_or(110), height: self.height.unwrap_or(110),
class_list: self.class_list, class_list: self.class_list,
@ -145,34 +151,51 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
if disable_auto_resize { if disable_auto_resize {
style_list.push("resize: none".to_string()); style_list.push("resize: none".to_string());
style_list.push(format!(
"height: {h}px; max-height: {h}px; min-height: {h}px",
h = max_height
));
} }
let mut handlers = vec![];
let handler_disable_auto_resize = disable_auto_resize; let handler_disable_auto_resize = disable_auto_resize;
let resize_handler = ev(Ev::KeyUp, move |event| { let resize_handler = ev(Ev::KeyUp, move |event| {
use wasm_bindgen::JsCast; event.stop_propagation();
if handler_disable_auto_resize {
return Msg::NoOp;
}
let target = match event.target() { let target = event.target().unwrap();
Some(el) => el, let textarea = seed::to_textarea(&target);
_ => return Msg::NoOp, let value = textarea.value();
};
let text_area = target.dyn_ref::<web_sys::HtmlTextAreaElement>().unwrap();
let value: String = text_area.value();
let min_height = get_min_height(value.as_str(), height as f64, handler_disable_auto_resize); let min_height = get_min_height(value.as_str(), height as f64, handler_disable_auto_resize);
text_area textarea
.style() .style()
.set_css_text(format!("height: {min_height}px", min_height = min_height).as_str()); .set_css_text(format!("height: {min_height}px", min_height = min_height).as_str());
Msg::NoOp Msg::NoOp
}); });
handlers.push(resize_handler);
let handler_disable_auto_resize = disable_auto_resize;
let text_input_handler = ev(update_event, move |event| { let text_input_handler = ev(update_event, move |event| {
event.stop_propagation();
let target = event.target().unwrap(); let target = event.target().unwrap();
let text = seed::to_textarea(&target).value(); let textarea = seed::to_textarea(&target);
Msg::InputChanged(id, text) let value = textarea.value();
if handler_disable_auto_resize && value.contains("\n") {
event.prevent_default();
}
Msg::InputChanged(
id,
if handler_disable_auto_resize {
value.trim().to_string()
} else {
value
},
)
}); });
handlers.push(text_input_handler);
class_list.push("textAreaInput".to_string()); class_list.push("textAreaInput".to_string());
@ -185,9 +208,11 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
At::AutoFocus => "true"; At::AutoFocus => "true";
At::Style => style_list.join(";"); At::Style => style_list.join(";");
At::Placeholder => placeholder.unwrap_or_default(); At::Placeholder => placeholder.unwrap_or_default();
At::Rows => if disable_auto_resize { "1" } else { "auto" }
], ],
value, value,
handlers, resize_handler,
text_input_handler,
] ]
] ]
} }