mod availability;
mod builders;
mod check;
mod choice;
mod chrome;
pub(crate) mod custom;
pub(crate) mod file;
mod placement;
mod read_only;
mod render;
#[cfg(feature = "rich-text")]
mod rich_text;
mod text;
use std::sync::Arc;
pub(crate) use availability::{Disabled, Hidden};
pub use builders::{
ChoiceField, CustomField, FileField, IntoFormValue, IntoOptions, RepeaterField, TextField,
};
pub(crate) use check::Items;
pub(crate) use choice::{ChoiceControl, Relationship, option_view};
pub(crate) use chrome::{FieldChrome, error_kind, render_field};
pub(crate) use custom::{Control, ControlInput};
pub use custom::{Switch, Toggle};
pub(crate) use file::{FileControl, is_linkable, is_rooted, stored_upload};
pub(crate) use placement::{Placement, Posting};
pub(crate) use read_only::{read_only, value_cell};
#[cfg(feature = "rich-text")]
use rich_text::RichTextControl;
#[cfg(feature = "rich-text")]
pub use rich_text::RichTextField;
pub(crate) use text::{Kind, Pattern, TextControl, password_reveal};
use toasty::stmt::{List, Path};
use topcoat::context::Cx;
use super::{condition::Condition, layouts::Span, lenses::Binding, repeater::RepeaterControl};
use crate::{
Text,
schema::FormInput,
values::{FieldError, FormScalar, FormValue},
};
pub struct Field {
binding: Binding,
label: Option<Text>,
required: bool,
control: ControlKind,
condition: Option<Condition>,
span: Option<Span>,
checkbox: bool,
accepted: bool,
help: Option<Text>,
disabled: Disabled,
disabling: Option<Condition>,
hidden: Hidden,
default: Option<FormValue>,
}
pub(crate) enum ControlKind {
Text(TextControl),
#[cfg(feature = "rich-text")]
RichText(RichTextControl),
Choice(ChoiceControl),
File(FileControl),
Custom(Arc<dyn Control>),
Repeater(RepeaterControl),
}
impl std::fmt::Debug for Field {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let control = match &self.control {
ControlKind::Text(_) => "text",
#[cfg(feature = "rich-text")]
ControlKind::RichText(_) => "rich text",
ControlKind::Choice(_) => "choice",
ControlKind::File(_) => "file",
ControlKind::Custom(_) => "custom",
ControlKind::Repeater(_) => "repeater",
};
f.debug_struct("Field")
.field("name", &self.name())
.field("label", &self.label())
.field("required", &self.required)
.field("control", &control)
.field("condition", &self.condition)
.field("help", &self.help)
.field("disabled", &self.disabled)
.field("disabling", &self.disabling)
.field("hidden", &self.hidden)
.field("default", &self.default)
.finish()
}
}
impl Field {
fn bound(binding: Binding, control: ControlKind) -> Self {
Self {
binding,
label: None,
required: false,
control,
condition: None,
span: None,
checkbox: false,
accepted: false,
help: None,
disabled: Disabled::Never,
disabling: None,
hidden: Hidden::default(),
default: None,
}
}
pub fn text<M, T>(lens: impl Into<Path<M, T>>) -> TextField
where
M: toasty::schema::Model,
T: FormScalar + toasty::stmt::IntoExpr<T> + 'static,
{
let path: Path<M, T> = lens.into();
let binding = Binding::of(&path);
let control =
TextControl::new::<M, T>(path, binding.unique(), binding.unique_with().to_vec());
TextField::new(Self::bound(binding, ControlKind::Text(control)))
}
#[doc(hidden)]
pub fn embedded_leaf<M, T>(path: Path<M, T>) -> TextField
where
M: toasty::schema::Model,
T: FormScalar,
{
TextField::new(Self::bound(
Binding::of(&path),
ControlKind::Text(TextControl::leaf::<T>()),
))
}
pub fn choice<M, T>(lens: impl Into<Path<M, T>>) -> ChoiceField
where
M: toasty::schema::Model,
{
ChoiceField::new(Self::bound(
Binding::of::<M, T>(&lens.into()),
ControlKind::Choice(ChoiceControl::default()),
))
}
pub fn file<M>(lens: impl Into<Path<M, String>>) -> FileField
where
M: toasty::schema::Model,
{
FileField::new(Self::bound(
Binding::of::<M, String>(&lens.into()),
ControlKind::File(FileControl::default()),
))
}
pub fn files<M>(lens: impl Into<Path<M, List<String>>>) -> FileField
where
M: toasty::schema::Model,
{
FileField::new(Self::bound(
Binding::of::<M, List<String>>(&lens.into()),
ControlKind::File(FileControl {
multiple: true,
..FileControl::default()
}),
))
}
pub fn toggle<M>(lens: impl Into<Path<M, bool>>) -> CustomField
where
M: toasty::schema::Model,
{
let mut field = Self::custom(lens, Toggle);
field.0.checkbox = true;
field
}
pub fn custom<M, T>(lens: impl Into<Path<M, T>>, control: impl Control + 'static) -> CustomField
where
M: toasty::schema::Model,
T: FormScalar,
{
CustomField::new(Self::bound(
Binding::of::<M, T>(&lens.into()),
ControlKind::Custom(Arc::new(control)),
))
}
pub fn repeater<M, T>(lens: impl Into<Path<M, List<T>>>) -> RepeaterField
where
M: toasty::schema::Model,
T: FormInput,
{
RepeaterField::new(Self::bound(
Binding::of::<M, List<T>>(&lens.into()),
ControlKind::Repeater(RepeaterControl::new::<T>()),
))
}
pub fn text_input<T: FormScalar>(name: impl Into<String>) -> TextField {
TextField::new(Self::bound(
Self::named(name.into()),
ControlKind::Text(TextControl::leaf::<T>()),
))
}
pub fn choice_input(name: impl Into<String>) -> ChoiceField {
ChoiceField::new(Self::bound(
Self::named(name.into()),
ControlKind::Choice(ChoiceControl::default()),
))
}
pub fn toggle_input(name: impl Into<String>) -> CustomField {
let mut field = Self::bound(
Self::named(name.into()),
ControlKind::Custom(Arc::new(Toggle)),
);
field.checkbox = true;
CustomField::new(field)
}
fn named(name: String) -> Binding {
let label = Text::field(&name);
Binding::named(name, label)
}
pub(crate) fn discriminant(binding: Binding, variants: Vec<(String, Text)>) -> Self {
let choice = ChoiceControl {
discriminant: true,
options: variants,
..ChoiceControl::default()
};
Self::bound(binding, ControlKind::Choice(choice))
}
pub(crate) fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
self.binding.misdeclared()
}
pub fn name(&self) -> &str {
self.binding.name()
}
pub(crate) fn label(&self) -> Text {
self.label.clone().unwrap_or_else(|| self.binding.label())
}
pub(crate) fn offers_nothing(&self) -> bool {
self.as_choice().is_some_and(ChoiceControl::offers_nothing)
}
pub(crate) fn is_password(&self) -> bool {
matches!(&self.control, ControlKind::Text(text) if text.password)
}
pub(crate) fn as_repeater(&self) -> Option<&RepeaterControl> {
match &self.control {
ControlKind::Repeater(repeater) => Some(repeater),
_ => None,
}
}
pub(crate) fn prefix(&mut self, prefix: &str) {
let label = self.label();
self.binding = Binding::named(format!("{prefix}{}", self.name()), label);
self.prefix_comparisons(prefix);
}
pub(crate) fn as_choice(&self) -> Option<&ChoiceControl> {
match &self.control {
ControlKind::Choice(choice) => Some(choice),
_ => None,
}
}
pub(crate) fn is_file(&self) -> bool {
matches!(self.control, ControlKind::File(_))
}
pub(crate) fn is_unique(&self) -> bool {
matches!(&self.control, ControlKind::Text(text) if text.unique)
}
pub(crate) fn unique_with(&self) -> Vec<&[String]> {
match &self.control {
ControlKind::Text(text) if text.unique => text.unique_with(),
_ => Vec::new(),
}
}
pub(crate) fn condition(&self) -> Option<&Condition> {
self.condition.as_ref()
}
pub(crate) fn span(&self) -> Option<Span> {
self.span
}
pub(crate) fn set_condition(&mut self, condition: Condition) {
self.condition = Some(condition);
}
pub(crate) fn is_checkbox(&self) -> bool {
self.checkbox
}
pub(crate) fn can_post(&self, value: &str) -> Option<bool> {
if self.checkbox {
return Some(value == "true" || value == "false");
}
match &self.control {
ControlKind::Choice(choice) if choice.is_multiple() => Some(false),
ControlKind::Choice(choice) if !choice.is_relationship() => {
Some(value.is_empty() || choice.label_of(value).is_some())
}
_ => None,
}
}
pub(crate) fn refuses(&self, value: &FormValue) -> bool {
match (&self.control, value) {
(ControlKind::Choice(choice), FormValue::List(items)) if choice.is_multiple() => {
!choice.is_relationship()
&& !choice.tags
&& items.iter().any(|item| choice.label_of(item).is_none())
}
(ControlKind::Choice(choice), _) if choice.is_multiple() => true,
(ControlKind::Text(text), FormValue::Text(value)) => {
!value.trim().is_empty() && !text.parses(value)
}
(_, FormValue::Text(value)) => self.can_post(value) == Some(false),
_ => true,
}
}
pub(crate) fn is_required(&self) -> bool {
self.required
}
pub(crate) fn default_value(&self) -> Option<&FormValue> {
self.default.as_ref()
}
fn chrome(
&self,
cx: &Cx,
id: String,
error: Option<&FieldError>,
fallback: Option<String>,
) -> FieldChrome {
let mut chrome = FieldChrome::new(cx, id, error, &self.label(), fallback);
chrome.help = self.help.clone();
chrome
}
pub(crate) fn set_required(&mut self, required: bool) {
self.required = required;
}
pub(crate) fn is_nullable(&self) -> bool {
self.binding.nullable()
}
pub(crate) fn same_value(&self, a: &str, b: &str) -> bool {
match &self.control {
ControlKind::Text(text) => text.same_value(a, b),
_ => a.trim() == b.trim(),
}
}
pub(crate) fn parent_key(&self) -> Option<&str> {
self.as_choice().and_then(ChoiceControl::parent_key)
}
pub(crate) async fn validate_exists(
&self,
cx: &Cx,
value: &FormValue,
parent: Option<&str>,
stored: Option<&FormValue>,
) -> Vec<crate::Text> {
match &self.control {
ControlKind::Choice(choice) => {
choice
.validate_exists(cx, &self.label(), value, parent, stored)
.await
}
_ => Vec::new(),
}
}
pub(crate) async fn recheck(
&self,
cx: &Cx,
value: &FormValue,
parent: Option<&str>,
stored: Option<&FormValue>,
ex: &mut dyn toasty::Executor,
) -> Vec<crate::Text> {
match &self.control {
ControlKind::Choice(choice) => {
choice
.recheck(cx, &self.label(), value, parent, stored, ex)
.await
}
_ => Vec::new(),
}
}
pub(crate) async fn keep_unseen(
&self,
cx: &Cx,
value: &FormValue,
stored: &FormValue,
ex: &mut dyn toasty::Executor,
) -> Option<FormValue> {
match &self.control {
ControlKind::Choice(choice) => choice.keep_unseen(cx, value, stored, ex).await,
_ => None,
}
}
}
#[cfg(test)]
mod test_support {
pub(super) use crate::test_support::DummyUser;
}
#[cfg(test)]
mod tests;