$options key:value options (min, max, class, ...). * @param string[] $flags Bare word options (placeholder, multiple, ...). * @param string[] $values Quoted values (choices, labels, placeholder text). */ public function __construct( public string $type, public string $name, public bool $required, public array $options, public array $flags, public array $values, ) {} /** * Whether the field collects data (not a submit button or [response] marker). */ public function is_input(): bool { return ! in_array( $this->type, array( 'submit', 'response' ), true ); } /** * Whether the field offers a fixed list of choices. */ public function has_choices(): bool { return in_array( $this->type, array( 'select', 'checkbox', 'radio' ), true ); } /** * Whether more than one value may be submitted. */ public function is_multiple(): bool { return 'checkbox' === $this->type || ( 'select' === $this->type && $this->has_flag( 'multiple' ) ); } /** * Returns an option value or null. * * @param string $key Option key. */ public function option( string $key ): ?string { return $this->options[ $key ] ?? null; } /** * Whether a bare-word flag is present. * * @param string $flag Flag name. */ public function has_flag( string $flag ): bool { return in_array( $flag, $this->flags, true ); } /** * Allowed choices for select/checkbox/radio fields, as shown to visitors. * With pipes ("Sales|sales@example.com") only the part before "|" is shown. * * @return string[] */ public function choices(): array { if ( ! $this->has_choices() ) { return array(); } $values = $this->values; if ( 'select' === $this->type && $this->has_flag( 'first_as_label' ) ) { array_shift( $values ); // The first value is only the placeholder option. } return array_map( static fn( string $v ): string => trim( explode( '|', $v, 2 )[0] ), $values ); } /** * Placeholder option text for a select with "first_as_label". */ public function first_label(): string { return 'select' === $this->type && $this->has_flag( 'first_as_label' ) ? trim( explode( '|', $this->values[0] ?? '', 2 )[0] ) : ''; } /** * The choice that reveals an "Other" text box ("free_text" flag): the last one. */ public function free_text_choice(): string { $choices = in_array( $this->type, array( 'checkbox', 'radio' ), true ) && $this->has_flag( 'free_text' ) ? $this->choices() : array(); return array() !== $choices ? (string) end( $choices ) : ''; } /** * Quiz questions: each quoted value is "Question|answer" or * "Question|answer|other accepted answer". * * @return array */ public function quiz_questions(): array { if ( 'quiz' !== $this->type ) { return array(); } $out = array(); foreach ( $this->values as $raw ) { $parts = array_map( 'trim', explode( '|', $raw ) ); $question = (string) array_shift( $parts ); $answers = array_values( array_filter( $parts, 'strlen' ) ); if ( '' !== $question && array() !== $answers ) { $out[] = array( 'question' => $question, 'answers' => $answers, ); } } return $out; } /** * Whether any choice uses a pipe to map a visible label to a hidden value. */ public function has_pipes(): bool { return $this->has_choices() && array() !== array_filter( $this->values, static fn( string $v ): bool => str_contains( $v, '|' ) ); } /** * The admin-defined value behind a visible choice label (the label itself * when that choice has no pipe). Never comes from visitor input. * * @param string $label Choice label. */ public function piped_value( string $label ): string { foreach ( $this->values as $raw ) { $parts = explode( '|', $raw, 2 ); if ( trim( $parts[0] ) === $label ) { return isset( $parts[1] ) ? trim( $parts[1] ) : $label; } } return $label; } /** * Placeholder text: the first quoted value when the "placeholder" flag is set. */ public function placeholder(): string { return $this->has_flag( 'placeholder' ) && isset( $this->values[0] ) ? $this->values[0] : ''; } /** * Static default value for text-like and hidden fields: the first quoted * value, or the second one when the first is the placeholder. */ public function default_value(): string { if ( $this->has_choices() || in_array( $this->type, array( 'acceptance', 'submit' ), true ) ) { return ''; } return $this->values[ $this->has_flag( 'placeholder' ) ? 1 : 0 ] ?? ''; } /** Length cap for each part of a name or address field. */ public const PART_MAX = 200; /** * Whether the field has several inputs (a split name or an address). */ public function is_composite(): bool { return array() !== $this->parts(); } /** * Inputs of a composite field, in order: part key => label, autocomplete * token and whether the part must be filled when the field is required. * Empty for every other field. * * @return array */ public function parts(): array { $part = static fn( string $label, string $autocomplete, bool $required ): array => array( 'label' => $label, 'autocomplete' => $autocomplete, 'required' => $required, ); if ( 'name' === $this->type && $this->has_flag( 'first_last' ) ) { return array( 'first' => $part( __( 'First name', 'flexsolutions-form-builder' ), 'given-name', true ), 'last' => $part( __( 'Last name', 'flexsolutions-form-builder' ), 'family-name', true ), ); } if ( 'address' === $this->type ) { return array( 'street' => $part( __( 'Street address', 'flexsolutions-form-builder' ), 'street-address', true ), 'city' => $part( __( 'City', 'flexsolutions-form-builder' ), 'address-level2', true ), 'region' => $part( __( 'State / Province / Region', 'flexsolutions-form-builder' ), 'address-level1', false ), 'postcode' => $part( __( 'ZIP / Postal code', 'flexsolutions-form-builder' ), 'postal-code', false ), 'country' => $part( __( 'Country', 'flexsolutions-form-builder' ), 'country-name', true ), ); } return array(); } /** * The single text stored, mailed and exported for a composite field: a * split name as "First Last", an address with one filled part per line. * * @param array $values Part key => sanitized text. */ public function join_parts( array $values ): string { $filled = array_filter( array_map( static fn( string $key ): string => (string) ( $values[ $key ] ?? '' ), array_keys( $this->parts() ) ), static fn( string $text ): bool => '' !== $text ); return implode( 'name' === $this->type ? ' ' : "\n", $filled ); } /** * Label for submit buttons and acceptance checkboxes. */ public function label(): string { return $this->values[0] ?? ''; } }