diff --git a/cot/src/form/fields.rs b/cot/src/form/fields.rs index 920e19b1..771ae1b5 100644 --- a/cot/src/form/fields.rs +++ b/cot/src/form/fields.rs @@ -10,7 +10,7 @@ use std::num::{ }; use askama::filters::HtmlSafe; -pub use attrs::Step; +pub use attrs::{AutoCapitalize, AutoComplete, Dir, List, Step}; pub use chrono::{ DateField, DateFieldOptions, DateTimeField, DateTimeFieldOptions, DateTimeWithTimezoneField, DateTimeWithTimezoneFieldOptions, TimeField, TimeFieldOptions, @@ -72,11 +72,39 @@ pub(crate) use impl_form_field; impl_form_field!(StringField, StringFieldOptions, "a string"); /// Custom options for a [`StringField`]. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct StringFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. + pub min_length: Option, + /// The size of the field. Used to set the `size` attribute in the HTML + /// input element. + pub size: Option, + /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input + /// element. + pub autocapitalize: Option, + /// Corresponds to the [`AutoComplete`] attribute in the HTML input element. + pub autocomplete: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname + pub dirname: Option, + /// A [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. + pub list: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, } impl Display for StringField { @@ -90,6 +118,42 @@ impl Display for StringField { if let Some(max_length) = self.custom_options.max_length { tag.attr("maxlength", max_length.to_string()); } + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + let data_list = HtmlTag::data_list(list.clone(), &list_id); + tag.push_tag(data_list); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -142,11 +206,26 @@ impl AsFormField for LimitedString { impl_form_field!(PasswordField, PasswordFieldOptions, "a password"); /// Custom options for a [`PasswordField`]. -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct PasswordFieldOptions { /// The maximum length of the field. Used to set the `maxlength` attribute /// in the HTML input element. pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. + pub min_length: Option, + /// The size of the field. Used to set the [`size`] attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size + pub size: Option, + /// Corresponds to the [`AutoComplete`] attribute in the HTML input element. + pub autocomplete: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, } impl Display for PasswordField { @@ -160,6 +239,26 @@ impl Display for PasswordField { if let Some(max_length) = self.custom_options.max_length { tag.attr("maxlength", max_length.to_string()); } + + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + // we don't set the value attribute for password fields // to avoid leaking the password in the HTML @@ -217,7 +316,7 @@ impl AsFormField for PasswordHash { impl_form_field!(EmailField, EmailFieldOptions, "an email"); /// Custom options for [`EmailField`] -#[derive(Debug, Default, Copy, Clone)] +#[derive(Debug, Default, Clone)] pub struct EmailFieldOptions { /// The maximum length of the field used to set the `maxlength` attribute /// in the HTML input element. @@ -225,11 +324,38 @@ pub struct EmailFieldOptions { /// The minimum length of the field used to set the `minlength` attribute /// in the HTML input element. pub min_length: Option, + /// The size of the field used to set the [`size`] attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size + pub size: Option, + /// Corresponds to the [`AutoCapitalize`] attribute in the HTML input + /// element. + pub autocomplete: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname + pub dirname: Option, + /// A [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. + pub list: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, } impl Display for EmailField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag = HtmlTag::input("email"); + let mut data_list: Option = None; + tag.attr("name", self.id()); tag.attr("id", self.id()); if self.options.required { @@ -241,10 +367,49 @@ impl Display for EmailField { if let Some(min_length) = self.custom_options.min_length { tag.attr("minlength", min_length.to_string()); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + data_list = Some(HtmlTag::data_list(list.clone(), &list_id)); + } + if let Some(value) = &self.value { tag.attr("value", value); } + if let Some(data_list) = data_list { + let mut wrapper = HtmlTag::new("div"); + wrapper + .attr("id", format!("__{}_datalist_wrapper", self.id())) + .push_tag(tag) + .push_tag(data_list); + return write!(f, "{}", wrapper.render()); + } write!(f, "{}", tag.render()) } } @@ -289,10 +454,10 @@ impl AsFormField for Email { impl HtmlSafe for EmailField {} -impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer); +impl_form_field!(IntegerField, IntegerFieldOptions, "an integer", T: Integer + Display); /// Custom options for a [`IntegerField`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct IntegerFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -300,6 +465,14 @@ pub struct IntegerFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, + /// The step size for the field. Used to set the [`Step`] attribute in the + /// HTML input element. + pub step: Option>, } impl Default for IntegerFieldOptions { @@ -307,11 +480,14 @@ impl Default for IntegerFieldOptions { Self { min: T::MIN, max: T::MAX, + placeholder: None, + readonly: None, + step: None, } } } -impl Display for IntegerField { +impl Display for IntegerField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag = HtmlTag::input("number"); tag.attr("name", self.id()); @@ -325,6 +501,19 @@ impl Display for IntegerField { if let Some(max) = &self.custom_options.max { tag.attr("max", max.to_string()); } + + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(step) = &self.custom_options.step { + tag.attr("step", step.to_string()); + } + if let Some(value) = &self.value { tag.attr("value", value); } @@ -333,7 +522,7 @@ impl Display for IntegerField { } } -impl HtmlSafe for IntegerField {} +impl HtmlSafe for IntegerField {} /// A trait for numerical types that optionally have minimum and maximum values. /// @@ -646,10 +835,10 @@ pub(crate) fn check_required(field: &T) -> Result<&str, FormFieldV } } -impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float); +impl_form_field!(FloatField, FloatFieldOptions, "a float", T: Float + Display); /// Custom options for a [`FloatField`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct FloatFieldOptions { /// The minimum value of the field. Used to set the `min` attribute in the /// HTML input element. @@ -657,6 +846,14 @@ pub struct FloatFieldOptions { /// The maximum value of the field. Used to set the `max` attribute in the /// HTML input element. pub max: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, + /// The step size for the field. Used to set the [`Step`] attribute in the + /// HTML input element. + pub step: Option>, } impl Default for FloatFieldOptions { @@ -664,11 +861,14 @@ impl Default for FloatFieldOptions { Self { min: T::MIN, max: T::MAX, + placeholder: None, + readonly: None, + step: None, } } } -impl Display for FloatField { +impl Display for FloatField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut tag: HtmlTag = HtmlTag::input("number"); tag.attr("name", self.id()); @@ -683,6 +883,17 @@ impl Display for FloatField { if let Some(max) = &self.custom_options.max { tag.attr("max", max.to_string()); } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(step) = &self.custom_options.step { + tag.attr("step", step.to_string()); + } if let Some(value) = &self.value { tag.attr("value", value); } @@ -691,7 +902,7 @@ impl Display for FloatField { } } -impl HtmlSafe for FloatField {} +impl HtmlSafe for FloatField {} /// A trait for types that can be represented as a float. /// @@ -776,19 +987,86 @@ impl_float_as_form_field!(f64); impl_form_field!(UrlField, UrlFieldOptions, "a URL"); /// Custom options for a [`UrlField`]. -#[derive(Debug, Default, Copy, Clone)] -pub struct UrlFieldOptions; +#[derive(Debug, Default, Clone)] +pub struct UrlFieldOptions { + /// The maximum length of the field. Used to set the `maxlength` attribute + /// in the HTML input element. + pub max_length: Option, + /// The minimum length of the field. Used to set the `minlength` attribute + /// in the HTML input element. + pub min_length: Option, + /// The size of the field. Used to set the [`size`]attribute in the HTML + /// input element. + /// + /// [`size`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#size + pub size: Option, + /// The [`List`] of options for the `datalist` element, which can be used to + /// provide predefined options for the input. + pub list: Option, + /// The direction of the text input, which can be set to `ltr` + /// (left-to-right) or `rtl` (right-to-left). This corresponds to the + /// [`Dir`] attribute in the HTML input element. + pub dir: Option, + /// The [`dirname`] attribute in the HTML input element, which is used to + /// specify the direction of the text input. + /// + /// [`dirname`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input#dirname + pub dirname: Option, + /// The [`AutoComplete`] attribute in the HTML input element, which is used + /// to specify how the browser should handle autocomplete for the input. + pub autocomplete: Option, + /// The placeholder text for the input field, which is displayed when the + /// field is empty. + pub placeholder: Option, + /// If `true`, the field is read-only and cannot be modified by the user. + pub readonly: Option, +} impl Display for UrlField { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - // no custom options - let _ = self.custom_options; let mut tag = HtmlTag::input("url"); tag.attr("name", self.id()); tag.attr("id", self.id()); if self.options.required { tag.bool_attr("required"); } + if let Some(max_length) = self.custom_options.max_length { + tag.attr("maxlength", max_length.to_string()); + } + if let Some(min_length) = self.custom_options.min_length { + tag.attr("minlength", min_length.to_string()); + } + if let Some(placeholder) = &self.custom_options.placeholder { + tag.attr("placeholder", placeholder); + } + if let Some(readonly) = self.custom_options.readonly { + if readonly { + tag.bool_attr("readonly"); + } + } + if let Some(autocomplete) = &self.custom_options.autocomplete { + tag.attr("autocomplete", autocomplete.to_string()); + } + + if let Some(size) = self.custom_options.size { + tag.attr("size", size.to_string()); + } + + if let Some(dir) = &self.custom_options.dir { + tag.attr("dir", dir.as_str()); + } + + if let Some(dirname) = &self.custom_options.dirname { + tag.attr("dirname", dirname); + } + + if let Some(list) = &self.custom_options.list { + let list_id = format!("__{}_datalist", self.id()); + tag.attr("list", &list_id); + + let data_list = HtmlTag::data_list(list.clone(), &list_id); + tag.push_tag(data_list); + } if let Some(value) = &self.value { tag.attr("value", value); } @@ -831,12 +1109,30 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocapitalize: Some(AutoCapitalize::Words), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), + dirname: Some("dir".to_string()), + list: Some(List::new(["bar", "baz"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); let html = field.to_string(); assert!(html.contains("type=\"text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); assert!(html.contains("required")); assert!(html.contains("maxlength=\"10\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("size=\"15\"")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dir=\"ltr\"")); + assert!(html.contains("dirname=\"dir\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("readonly")); } #[cot::test] @@ -849,6 +1145,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -869,6 +1166,7 @@ mod tests { }, StringFieldOptions { max_length: Some(10), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -886,13 +1184,22 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + placeholder: Some("Enter password".to_string()), + readonly: Some(false), }, ); let html = field.to_string(); assert!(html.contains("type=\"password\"")); assert!(html.contains("required")); assert!(html.contains("maxlength=\"10\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("placeholder=\"Enter password\"")); } + #[cot::test] async fn password_field_clean_value() { let mut field = PasswordField::with_options( @@ -903,6 +1210,7 @@ mod tests { }, PasswordFieldOptions { max_length: Some(10), + ..Default::default() }, ); field @@ -922,18 +1230,33 @@ mod tests { required: true, }, EmailFieldOptions { - min_length: Some(10), - max_length: Some(50), + max_length: Some(10), + min_length: Some(5), + size: Some(15), + autocomplete: Some(AutoComplete::Value("foo bar".to_string())), + dir: Some(Dir::Ltr), + dirname: Some("dir".to_string()), + list: Some(List::new(["foo@example.com", "baz@example.com"])), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), }, ); let html = field.to_string(); + assert!(html.contains("type=\"email\"")); assert!(html.contains("required")); - assert!(html.contains("minlength=\"10\"")); - assert!(html.contains("maxlength=\"50\"")); + assert!(html.contains("minlength=\"5\"")); + assert!(html.contains("maxlength=\"10\"")); assert!(html.contains("name=\"test_id\"")); assert!(html.contains("id=\"test_id\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("readonly")); + assert!(html.contains("autocomplete=\"foo bar\"")); + assert!(html.contains("dir=\"ltr\"")); + assert!(html.contains("dirname=\"dir\"")); + assert!(html.contains("list=\"__test_id_datalist\"")); + assert!(html.contains(r#""#)); } #[cot::test] @@ -947,6 +1270,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -970,6 +1294,7 @@ mod tests { EmailFieldOptions { min_length: Some(10), max_length: Some(50), + ..Default::default() }, ); @@ -993,6 +1318,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1019,6 +1345,7 @@ mod tests { EmailFieldOptions { min_length: Some(5), max_length: Some(10), + ..Default::default() }, ); @@ -1045,6 +1372,7 @@ mod tests { EmailFieldOptions { min_length: Some(50), max_length: Some(10), + ..Default::default() }, ); @@ -1072,13 +1400,22 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + placeholder: Some("Enter text".to_string()), + readonly: Some(false), + step: Some(Step::Value(10)), }, ); let html = field.to_string(); + assert!(html.contains("type=\"number\"")); assert!(html.contains("required")); assert!(html.contains("min=\"1\"")); assert!(html.contains("max=\"10\"")); + assert!(html.contains("step=\"10\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); + assert!(!html.contains("readonly")); } #[cot::test] @@ -1092,6 +1429,7 @@ mod tests { IntegerFieldOptions { min: Some(1), max: Some(10), + ..Default::default() }, ); field @@ -1113,6 +1451,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1137,6 +1476,7 @@ mod tests { IntegerFieldOptions { min: Some(10), max: Some(50), + ..Default::default() }, ); field @@ -1217,13 +1557,22 @@ mod tests { FloatFieldOptions { min: Some(1.5), max: Some(10.7), + placeholder: Some("Enter text".to_string()), + readonly: Some(true), + step: Some(Step::Any), }, ); let html = field.to_string(); + assert!(html.contains("type=\"number\"")); assert!(html.contains("required")); assert!(html.contains("min=\"1.5\"")); assert!(html.contains("max=\"10.7\"")); + assert!(html.contains("step=\"any\"")); + assert!(html.contains("placeholder=\"Enter text\"")); + assert!(html.contains("name=\"test\"")); + assert!(html.contains("id=\"test\"")); + assert!(html.contains("readonly")); } #[cot::test] @@ -1238,6 +1587,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1259,6 +1609,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1283,6 +1634,7 @@ mod tests { FloatFieldOptions { min: Some(5.0), max: Some(10.0), + ..Default::default() }, ); field @@ -1307,6 +1659,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); let bad_inputs = ["NaN", "inf"]; @@ -1337,6 +1690,7 @@ mod tests { FloatFieldOptions { min: Some(1.0), max: Some(10.0), + ..Default::default() }, ); field.set_value(FormFieldValue::new_text("")).await.unwrap(); @@ -1352,12 +1706,24 @@ mod tests { name: "test".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(100), + min_length: Some(5), + size: Some(30), + list: Some(List::new(["https://example.com"])), + dir: Some(Dir::Ltr), + dirname: Some("dir".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Enter URL".to_owned()), + readonly: Some(true), + }, ); + field .set_value(FormFieldValue::new_text("https://example.com")) .await .unwrap(); + let value = Url::clean_value(&field).unwrap(); assert_eq!( value.as_str(), @@ -1373,16 +1739,40 @@ mod tests { name: "url".to_owned(), required: true, }, - UrlFieldOptions, + UrlFieldOptions { + max_length: Some(120), + min_length: Some(10), + size: Some(40), + list: Some(List::new(["https://one.com", "https://two.com"])), + dir: Some(Dir::Ltr), + dirname: Some("lang".to_owned()), + autocomplete: Some(AutoComplete::Value("url".to_owned())), + placeholder: Some("Paste link".to_owned()), + readonly: Some(true), + }, ); + field .set_value(FormFieldValue::new_text("http://example.com")) .await .unwrap(); + let html = field.to_string(); + assert!(html.contains("type=\"url\"")); + assert!(html.contains("id=\"id_url\"")); + assert!(html.contains("name=\"id_url\"")); assert!(html.contains("required")); assert!(html.contains("value=\"http://example.com\"")); + assert!(html.contains("maxlength=\"120\"")); + assert!(html.contains("minlength=\"10\"")); + assert!(html.contains("size=\"40\"")); + assert!(html.contains("placeholder=\"Paste link\"")); + assert!(html.contains("autocomplete=\"url\"")); + assert!(html.contains("dir=\"ltr\"")); + assert!(html.contains("dirname=\"lang\"")); + assert!(html.contains("readonly")); + assert!(html.contains("` elements: +/// Represents the HTML [`step`] attribute for `` elements: /// - `Any` → `step="any"` /// - `Value(T)` → `step=""` where `T` is converted appropriately +/// +/// [`step`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/step #[derive(Debug, Copy, Clone)] pub enum Step { /// Indicates that the user may enter any value (no fixed “step” interval). @@ -26,3 +28,151 @@ impl Display for Step { } } } + +/// Represents the HTML [`list`] attribute for `` elements. +/// Used to provide a set of predefined options for the input. +/// +/// [`list`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#list +#[derive(Debug, Clone, Default)] +pub struct List(Vec); + +impl List { + /// Creates a new `List` from any iterator of string-like items. + pub fn new(iter: I) -> Self + where + I: IntoIterator, + S: AsRef, + { + let v = iter.into_iter().map(|s| s.as_ref().to_string()).collect(); + Self(v) + } +} + +impl From for Vec { + fn from(value: List) -> Self { + value.0 + } +} + +/// Represents the HTML [`autocomplete`] attribute for form fields. +/// +/// [`autocomplete`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete +#[derive(Debug, Clone)] +pub enum AutoComplete { + /// Enables autocomplete. + On, + /// Disables autocomplete. + Off, + /// Custom autocomplete value. + Value(String), +} + +impl AutoComplete { + /// Returns the string representation for use in HTML. + #[must_use] + pub fn as_str(&self) -> &str { + match self { + Self::Off => "off", + Self::On => "on", + Self::Value(value) => value.as_str(), + } + } +} + +impl Display for AutoComplete { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Represents the HTML [`autocapitalize`] attribute for form fields. +/// +/// [`autocapitalize`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/autocapitalize +#[derive(Debug, Clone, Copy)] +pub enum AutoCapitalize { + /// No capitalization. + Off, + /// Capitalize all letters. + On, + /// Capitalize the first letter of each word. + Words, + /// Capitalize all characters. + Characters, +} + +impl AutoCapitalize { + /// Returns the string representation for use in HTML. + fn as_str(self) -> &'static str { + match self { + AutoCapitalize::Off => "off", + AutoCapitalize::On => "on", + AutoCapitalize::Words => "words", + AutoCapitalize::Characters => "characters", + } + } +} + +impl Display for AutoCapitalize { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Represents the HTML [`dir`] attribute for text direction. +/// +/// [`dir`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/dir +#[derive(Debug, Clone, Copy)] +pub enum Dir { + /// Right-to-left text direction. + Rtl, + /// Left-to-right text direction. + Ltr, + /// User agent auto-detects the text direction. + Auto, +} + +impl Dir { + /// Returns the string representation for use in HTML. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Rtl => "rtl", + Self::Ltr => "ltr", + Self::Auto => "auto", + } + } +} + +impl Display for Dir { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Represents the HTML [`capture`] attribute for file inputs. +/// Used to specify the preferred source for file capture. +/// +/// [`capture`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#capture +#[derive(Debug, Clone, Copy)] +pub enum Capture { + /// Use the user-facing camera or microphone. + User, + /// Use the environment-facing camera or microphone. + Environment, +} + +impl Capture { + /// Returns the string representation for use in HTML. + pub fn as_str(self) -> &'static str { + match self { + Self::User => "user", + Self::Environment => "environment", + } + } +} + +impl Display for Capture { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} diff --git a/cot/src/form/fields/files.rs b/cot/src/form/fields/files.rs index fb107280..131080c4 100644 --- a/cot/src/form/fields/files.rs +++ b/cot/src/form/fields/files.rs @@ -5,6 +5,7 @@ use bytes::Bytes; use cot::form::{AsFormField, FormFieldValidationError}; use cot::html::HtmlTag; +use crate::form::fields::attrs::Capture; use crate::form::{FormField, FormFieldOptions, FormFieldValue, FormFieldValueError}; #[derive(Debug)] @@ -64,6 +65,8 @@ pub struct FileFieldOptions { /// /// [`accept` attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#limiting_accepted_file_types pub accept: Option>, + /// The [`Capture`] attribute specifies the source of the file input. + pub capture: Option, } impl Display for FileField { @@ -78,6 +81,10 @@ impl Display for FileField { tag.attr("accept", accept.join(",")); } + if let Some(capture) = self.custom_options.capture { + tag.attr("capture", capture.to_string()); + } + write!(f, "{}", tag.render()) } } @@ -161,6 +168,7 @@ mod tests { }, FileFieldOptions { accept: Some(vec!["image/*".to_string(), ".pdf".to_string()]), + capture: Some(Capture::Environment), }, ); @@ -169,6 +177,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(html.contains("accept=\"image/*,.pdf\"")); + assert!(html.contains("capture=\"environment\"")); } #[test] @@ -179,7 +188,10 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + accept: None, + capture: Some(Capture::User), + }, ); let html = field.to_string(); @@ -187,6 +199,7 @@ mod tests { assert!(html.contains("type=\"file\"")); assert!(html.contains("required")); assert!(!html.contains("accept=")); + assert!(html.contains("capture=\"user\"")); } #[cot::test] @@ -197,7 +210,9 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + ..Default::default() + }, ); let boundary = "boundary"; @@ -232,7 +247,9 @@ mod tests { name: "test".to_owned(), required: true, }, - FileFieldOptions { accept: None }, + FileFieldOptions { + ..Default::default() + }, ); let value = InMemoryUploadedFile::clean_value(&field); diff --git a/cot/src/html.rs b/cot/src/html.rs index 00066911..a07ec51a 100644 --- a/cot/src/html.rs +++ b/cot/src/html.rs @@ -189,6 +189,33 @@ impl HtmlTag { input } + /// Creates a new `HtmlTag` instance for a datalist element. + /// + /// # Examples + /// ``` + /// use cot::html::HtmlTag; + /// let data_list = HtmlTag::data_list(vec!["Option 1", "Option 2"], "my-datalist"); + /// let rendered = data_list.render(); + /// assert_eq!(rendered.as_str(), ""); + /// ``` + #[must_use] + pub fn data_list>>(list: L, id: &str) -> Self { + let mut data_list = Self::new("datalist"); + data_list.attr("id", id); + + let mut options: Vec = Vec::new(); + + for l in list.into() { + let mut option = HtmlTag::new("option"); + option.attr("value", &l); + option.push_str(&l); + options.push(HtmlNode::Tag(option)); + } + + data_list.children = options; + data_list + } + /// Adds an attribute to the HTML tag. /// /// # Safety