`). Pairs naturally with the existing
* `date` type for booking-window or appointment-preference
* forms. Server-side validator in `api/web_forms.php` extended
* in lock-step.
* - Per-field responsive Column Width control (Desktop / Tablet
* / Mobile) on BOTH the Standard Fields and Custom Fields
* repeaters. Lets the editor lay out e.g. First+Last side-by-
* side on desktop, stacked on mobile. Form switches from a
* single-column flex layout to a 12-column CSS grid; each
* field carries its column span via `--cw-cols-d/-t/-m` CSS
* variables that cascade through the grid via media-query
* fallback (mobile inherits tablet inherits desktop unless
* explicitly set).
* - 2.5.2: full configurability.
* - Standard Fields repeater — toggle/relabel/reorder the 10
* prebuilt fields (First Name, Last Name, Phone, Email,
* Address, City, State, Zip, Commentary, Preferred Contact).
* Can't add to or delete from this list; the 10 fields are the
* canonical lead-intake columns in `crm_web_form_submissions`.
* - Custom Fields repeater — arbitrary user-defined fields with
* types (text, textarea, email, tel, url, number, date, select,
* checkbox, radio). Each row carries id/label/type/required/
* placeholder/options/help. Stored as self-describing JSON in
* the new `custom_fields` column.
* - SMS Consent section — toggle, WYSIWYG consent text with
* `{{privacy_url}}` / `{{sms_terms_url}}` / `{{help_phone}}`
* placeholders, URL fields with site-relative defaults, opt-in
* help phone, "require before submit" gate. Verbatim consent
* text and the checked/unchecked flag both stored as
* first-class columns (legal trail for the downstream Twilio
* pipeline).
* - Anti-bot ("I'm not a robot") simple in-form checkbox gate.
* - Container styling: bg, padding, border (style/width/color),
* border-radius.
* - Label typography: family/size/weight/color.
* - Input typography: family/size + separate placeholder vs
* user-typed color + input bg + input border.
* - Required-asterisk color (default red `#dc2626`).
* - Submit button alignment (left/center/right/full-width).
*
* @package A1Tools
* @since 2.5.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class A1Tools_Web_Form_Elementor_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'a1tools-web-form';
}
public function get_title() {
return __( 'A1 Tools – Web Form', 'a1-tools' );
}
public function get_icon() {
return 'eicon-form-horizontal';
}
public function get_categories() {
return array( 'general' );
}
public function get_keywords() {
return array( 'a1', 'form', 'contact', 'lead', 'crm' );
}
public function get_style_depends() {
return array( 'a1tools-web-form' );
}
public function get_script_depends() {
return array( 'a1tools-web-form' );
}
/**
* Default Standard Fields repeater rows. Order in this array is
* the on-form order shown to first-time visitors; the user can
* drag-reorder via Elementor.
*/
public static function default_standard_fields(): array {
return array(
array( 'enabled' => 'yes', 'field_name' => 'first_name', 'field_label' => __( 'First name', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'last_name', 'field_label' => __( 'Last name', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'phone', 'field_label' => __( 'Phone', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'email', 'field_label' => __( 'Email', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'address', 'field_label' => __( 'Address', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'city', 'field_label' => __( 'City', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'state', 'field_label' => __( 'State', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'zip', 'field_label' => __( 'Zip code', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'commentary', 'field_label' => __( 'Tell us about your project', 'a1-tools' ),'field_placeholder' => '', 'field_required' => 'yes' ),
array( 'enabled' => 'yes', 'field_name' => 'preferred_contact', 'field_label' => __( 'Preferred contact method', 'a1-tools' ), 'field_placeholder' => '', 'field_required' => 'yes' ),
);
}
/**
* Allowed Custom Field types — mirrors the server-side validation
* whitelist in `api/web_forms.php`.
*/
public static function custom_field_types(): array {
return array(
'text' => __( 'Text', 'a1-tools' ),
'textarea' => __( 'Textarea', 'a1-tools' ),
'email' => __( 'Email', 'a1-tools' ),
'tel' => __( 'Phone', 'a1-tools' ),
'url' => __( 'URL', 'a1-tools' ),
'number' => __( 'Number', 'a1-tools' ),
'date' => __( 'Date', 'a1-tools' ),
'time' => __( 'Time', 'a1-tools' ),
'select' => __( 'Dropdown (Select)', 'a1-tools' ),
'checkbox' => __( 'Checkbox', 'a1-tools' ),
'radio' => __( 'Radio group', 'a1-tools' ),
);
}
/**
* Responsive column-width options shown in the per-field width
* dropdown (Standard Fields + Custom Fields repeaters). Values are
* percentages — empty string means "inherit from a larger breakpoint."
* The CSS pairs each percentage with a 12-column grid via
* {@see self::pct_to_cols()}.
*/
public static function column_width_options(): array {
return array(
'' => __( 'Default (inherit)', 'a1-tools' ),
'100' => '100%',
'75' => '75%',
'66' => '66%',
'50' => '50%',
'33' => '33%',
'25' => '25%',
);
}
/**
* Map a user-facing percentage value (e.g. '50') to a CSS-grid
* column span on a 12-column track. Anything outside the
* predefined set returns 12 (full row) so a stray value never
* silently shrinks a field.
*/
public static function pct_to_cols( $pct ): int {
$map = array(
'100' => 12, '75' => 9, '66' => 8,
'50' => 6, '33' => 4, '25' => 3,
);
$key = is_string( $pct ) ? $pct : (string) $pct;
return isset( $map[ $key ] ) ? $map[ $key ] : 12;
}
protected function register_controls() {
$this->register_content_controls();
$this->register_standard_fields_section();
$this->register_custom_fields_section();
$this->register_consent_section();
$this->register_antibot_section();
$this->register_submit_section();
$this->register_style_container();
$this->register_style_title();
$this->register_style_labels();
$this->register_style_inputs();
$this->register_style_asterisk();
$this->register_style_submit();
$this->register_style_consent();
}
// ── Content : Header ────────────────────────────────────────────
private function register_content_controls(): void {
$this->start_controls_section(
'section_header',
array( 'label' => __( 'Header', 'a1-tools' ) )
);
$this->add_control(
'form_title',
array(
'label' => __( 'Form title', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Get a free estimate', 'a1-tools' ),
'label_block' => true,
)
);
$this->add_control(
'form_subtitle',
array(
'label' => __( 'Subtitle', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
'default' => __( 'Tell us what you need and we\'ll be in touch shortly.', 'a1-tools' ),
'label_block' => true,
'rows' => 2,
)
);
$this->end_controls_section();
}
// ── Content : Standard Fields repeater ──────────────────────────
private function register_standard_fields_section(): void {
$this->start_controls_section(
'section_standard_fields',
array( 'label' => __( 'Standard Fields', 'a1-tools' ) )
);
$this->add_control(
'standard_fields_help',
array(
'type' => \Elementor\Controls_Manager::RAW_HTML,
'raw' => __( 'Toggle, relabel, and reorder the ten CRM lead-intake fields. These map directly to columns in crm_web_form_submissions so the CRM\'s standard filters (state, preferred contact, etc.) keep working. To add fields beyond these, use the Custom Fields section below.', 'a1-tools' ),
'content_classes' => 'elementor-descriptor',
)
);
$std_repeater = new \Elementor\Repeater();
$std_repeater->add_control( 'enabled', array(
'label' => __( 'Show this field', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'label_on' => __( 'Show', 'a1-tools' ),
'label_off' => __( 'Hide', 'a1-tools' ),
'return_value' => 'yes',
'default' => 'yes',
) );
$std_repeater->add_control( 'field_name', array(
'label' => __( 'Field key', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'description' => __( 'Read-only. Don\'t change — this is the CRM column name.', 'a1-tools' ),
) );
$std_repeater->add_control( 'field_label', array(
'label' => __( 'Label', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
) );
$std_repeater->add_control( 'field_placeholder', array(
'label' => __( 'Placeholder', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
) );
$std_repeater->add_control( 'field_required', array(
'label' => __( 'Required', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'return_value' => 'yes',
'default' => 'yes',
) );
// 2.5.21 — per-field column width (Desktop / Tablet / Mobile).
// Empty value on tablet/mobile inherits the larger breakpoint
// via the CSS variable fallback chain.
$std_repeater->add_responsive_control( 'field_width', array(
'label' => __( 'Column width', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => self::column_width_options(),
'default' => '100',
'description' => __( 'Width of this field on this device. Smaller breakpoints inherit the desktop value unless explicitly set. Useful for putting First/Last name on one row, etc.', 'a1-tools' ),
'condition' => array( 'enabled' => 'yes' ),
) );
$this->add_control(
'standard_fields',
array(
'label' => __( 'Fields', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => $std_repeater->get_controls(),
'default' => self::default_standard_fields(),
'title_field' => '{{{ field_label }}}',
'prevent_empty' => false,
)
);
$this->end_controls_section();
}
// ── Content : Custom Fields repeater ────────────────────────────
private function register_custom_fields_section(): void {
$this->start_controls_section(
'section_custom_fields',
array( 'label' => __( 'Custom Fields', 'a1-tools' ) )
);
$this->add_control(
'custom_fields_help',
array(
'type' => \Elementor\Controls_Manager::RAW_HTML,
'raw' => __( 'Add arbitrary fields beyond the 10 standard ones. They\'re stored as a self-describing JSON array on the submission row and rendered in the CRM\'s submission detail view alongside the standard fields.', 'a1-tools' ),
'content_classes' => 'elementor-descriptor',
)
);
$cf_repeater = new \Elementor\Repeater();
$cf_repeater->add_control( 'field_id', array(
'label' => __( 'Field ID (slug)', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'description' => __( 'Internal identifier. Letters, numbers, underscores only. Stable across edits.', 'a1-tools' ),
) );
$cf_repeater->add_control( 'field_label', array(
'label' => __( 'Label', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
) );
$cf_repeater->add_control( 'field_type', array(
'label' => __( 'Type', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => self::custom_field_types(),
'default' => 'text',
) );
$cf_repeater->add_control( 'field_placeholder', array(
'label' => __( 'Placeholder', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'condition' => array( 'field_type!' => array( 'checkbox', 'radio', 'select' ) ),
) );
$cf_repeater->add_control( 'field_options', array(
'label' => __( 'Options (one per line)', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
'default' => '',
'description' => __( 'For Dropdown, Checkbox, and Radio types. Use value|Label per line — or just Label to use the label as the value.', 'a1-tools' ),
'rows' => 4,
'condition' => array( 'field_type' => array( 'select', 'radio' ) ),
) );
$cf_repeater->add_control( 'field_help', array(
'label' => __( 'Help text (below field)', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'label_block' => true,
) );
$cf_repeater->add_control( 'field_required', array(
'label' => __( 'Required', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'return_value' => 'yes',
'default' => '',
) );
// 2.5.21 — per-field column width (Desktop / Tablet / Mobile),
// same control as the Standard Fields repeater above.
$cf_repeater->add_responsive_control( 'field_width', array(
'label' => __( 'Column width', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => self::column_width_options(),
'default' => '100',
'description' => __( 'Width of this field on this device. Smaller breakpoints inherit the desktop value unless explicitly set.', 'a1-tools' ),
) );
$this->add_control(
'custom_fields',
array(
'label' => __( 'Custom fields', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::REPEATER,
'fields' => $cf_repeater->get_controls(),
'default' => array(),
'title_field' => '{{{ field_label || field_id || "Custom field" }}}',
'prevent_empty' => false,
)
);
$this->end_controls_section();
}
// ── Content : SMS Consent ───────────────────────────────────────
private function register_consent_section(): void {
$this->start_controls_section(
'section_consent',
array( 'label' => __( 'SMS Consent', 'a1-tools' ) )
);
$this->add_control(
'consent_enabled',
array(
'label' => __( 'Enable SMS consent checkbox', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'return_value' => 'yes',
'default' => '',
'label_on' => __( 'On', 'a1-tools' ),
'label_off' => __( 'Off', 'a1-tools' ),
)
);
$this->add_control(
'consent_required',
array(
'label' => __( 'Require check to submit', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'return_value' => 'yes',
'default' => 'yes',
'condition' => array( 'consent_enabled' => 'yes' ),
)
);
$this->add_control(
'consent_text',
array(
'label' => __( 'Consent text', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::WYSIWYG,
'default' => __( 'By checking this box, I consent to receive text messages related to orders, service appointments, or other relevant information. You can reply STOP at any time to opt out. Message and data rates may apply. Message frequency may vary. For more information, please refer to our Privacy Policy and SMS Terms and Conditions.', 'a1-tools' ),
'description' => __( 'Placeholders: {{privacy_url}}, {{sms_terms_url}}, {{help_phone}} are replaced at render time. Site-relative defaults are used for URLs when the fields below are left empty.', 'a1-tools' ),
'condition' => array( 'consent_enabled' => 'yes' ),
)
);
$this->add_control(
'consent_privacy_url',
array(
'label' => __( 'Privacy Policy URL', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::URL,
'default' => array( 'url' => '' ),
'placeholder' => __( 'Leave empty to use [site]/privacy-policy/', 'a1-tools' ),
'show_external' => false,
'condition' => array( 'consent_enabled' => 'yes' ),
)
);
$this->add_control(
'consent_sms_terms_url',
array(
'label' => __( 'SMS Terms URL', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::URL,
'default' => array( 'url' => '' ),
'placeholder' => __( 'Leave empty to use [site]/terms-and-conditions/', 'a1-tools' ),
'show_external' => false,
'condition' => array( 'consent_enabled' => 'yes' ),
)
);
$this->add_control(
'consent_help_phone',
array(
'label' => __( 'Help phone number (optional)', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => '',
'placeholder' => '(555) 123-4567',
'description' => __( 'Replaces {{help_phone}} in the consent text. Leave empty to omit.', 'a1-tools' ),
'condition' => array( 'consent_enabled' => 'yes' ),
)
);
$this->end_controls_section();
}
// ── Content : Anti-bot ──────────────────────────────────────────
private function register_antibot_section(): void {
$this->start_controls_section(
'section_antibot',
array( 'label' => __( 'Anti-bot', 'a1-tools' ) )
);
$this->add_control(
'antibot_enabled',
array(
'label' => __( 'Show "I\'m not a robot" checkbox', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SWITCHER,
'return_value' => 'yes',
'default' => '',
'description' => __( 'Simple in-form checkbox the visitor ticks before submit. Blocks naive scripts without the friction of an external CAPTCHA service. The form already has an invisible honeypot + minimum-fill-time gate, so this is purely additive.', 'a1-tools' ),
)
);
$this->add_control(
'antibot_label',
array(
'label' => __( 'Checkbox label', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'I\'m not a robot', 'a1-tools' ),
'condition' => array( 'antibot_enabled' => 'yes' ),
)
);
$this->end_controls_section();
}
// ── Content : Submit + Messages ─────────────────────────────────
private function register_submit_section(): void {
$this->start_controls_section(
'section_submit',
array( 'label' => __( 'Submit & Messages', 'a1-tools' ) )
);
$this->add_control(
'submit_label',
array(
'label' => __( 'Submit button label', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXT,
'default' => __( 'Send', 'a1-tools' ),
)
);
$this->add_control(
'success_message',
array(
'label' => __( 'Success message', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
'default' => __( 'Thanks! We received your message and will be in touch shortly.', 'a1-tools' ),
)
);
$this->add_control(
'error_message',
array(
'label' => __( 'Error message', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::TEXTAREA,
'default' => __( 'Sorry, we couldn\'t submit your message right now. Please try again or call us.', 'a1-tools' ),
)
);
$this->end_controls_section();
}
// ── Style : Container ───────────────────────────────────────────
private function register_style_container(): void {
$this->start_controls_section(
'style_container',
array(
'label' => __( 'Container', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'container_bg',
array(
'label' => __( 'Background color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form-wrap' => 'background-color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Border::get_type(),
array(
'name' => 'container_border',
'selector' => '{{WRAPPER}} .a1tools-web-form-wrap',
)
);
// 2.5.23 — hover state for the container border color. Sits
// just under the normal Border group so the editor reads
// "Border (normal) → Border hover color → Radius" top-to-bottom.
// The `transition` declaration on the same selector makes the
// hover swap fade instead of snapping; 180ms matches the
// other interactive transitions on the form (inputs, button).
$this->add_control(
'container_border_color_hover',
array(
'label' => __( 'Border color (hover)', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form-wrap' => 'transition: border-color 180ms ease;',
'{{WRAPPER}} .a1tools-web-form-wrap:hover' => 'border-color: {{VALUE}};',
),
)
);
$this->add_control(
'container_radius',
array(
'label' => __( 'Border radius', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', '%', 'em' ),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form-wrap' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}; overflow: hidden;',
),
)
);
$this->add_responsive_control(
'container_padding',
array(
'label' => __( 'Padding', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', 'em', '%' ),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form-wrap' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Box_Shadow::get_type(),
array(
'name' => 'container_shadow',
'selector' => '{{WRAPPER}} .a1tools-web-form-wrap',
)
);
$this->end_controls_section();
}
// ── Style : Title & Subtitle ────────────────────────────────────
private function register_style_title(): void {
$this->start_controls_section(
'style_title',
array(
'label' => __( 'Title & Subtitle', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'title_color',
array(
'label' => __( 'Title color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__title' => 'color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array(
'name' => 'title_typography',
'selector' => '{{WRAPPER}} .a1tools-web-form__title',
)
);
// 2.5.24 — responsive title alignment. Per-breakpoint CHOOSE
// (left/center/right) so a centered hero title on desktop can
// still go left-aligned on mobile, etc. Default empty inherits
// whatever the theme/Elementor parent column dictates, which
// preserves existing form rendering.
$this->add_responsive_control(
'title_align',
array(
'label' => __( 'Alignment', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::CHOOSE,
'options' => array(
'left' => array( 'title' => __( 'Left', 'a1-tools' ), 'icon' => 'eicon-text-align-left' ),
'center' => array( 'title' => __( 'Center', 'a1-tools' ), 'icon' => 'eicon-text-align-center' ),
'right' => array( 'title' => __( 'Right', 'a1-tools' ), 'icon' => 'eicon-text-align-right' ),
),
'default' => '',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__title' => 'text-align: {{VALUE}};',
),
)
);
$this->add_control(
'subtitle_color',
array(
'label' => __( 'Subtitle color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__subtitle' => 'color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array(
'name' => 'subtitle_typography',
'selector' => '{{WRAPPER}} .a1tools-web-form__subtitle',
)
);
// 2.5.24 — responsive subtitle alignment (mirrors title).
$this->add_responsive_control(
'subtitle_align',
array(
'label' => __( 'Alignment', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::CHOOSE,
'options' => array(
'left' => array( 'title' => __( 'Left', 'a1-tools' ), 'icon' => 'eicon-text-align-left' ),
'center' => array( 'title' => __( 'Center', 'a1-tools' ), 'icon' => 'eicon-text-align-center' ),
'right' => array( 'title' => __( 'Right', 'a1-tools' ), 'icon' => 'eicon-text-align-right' ),
),
'default' => '',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__subtitle' => 'text-align: {{VALUE}};',
),
)
);
$this->end_controls_section();
}
// ── Style : Labels ──────────────────────────────────────────────
private function register_style_labels(): void {
$this->start_controls_section(
'style_labels',
array(
'label' => __( 'Field Labels', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'label_color',
array(
'label' => __( 'Color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__label' => 'color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array(
'name' => 'label_typography',
'selector' => '{{WRAPPER}} .a1tools-web-form__label',
)
);
$this->end_controls_section();
}
// ── Style : Inputs ──────────────────────────────────────────────
private function register_style_inputs(): void {
$this->start_controls_section(
'style_inputs',
array(
'label' => __( 'Inputs', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'input_text_color',
array(
'label' => __( 'Text color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__input, {{WRAPPER}} .a1tools-web-form__select, {{WRAPPER}} .a1tools-web-form__textarea' => 'color: {{VALUE}};',
),
)
);
$this->add_control(
'input_placeholder_color',
array(
'label' => __( 'Placeholder color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__input::placeholder, {{WRAPPER}} .a1tools-web-form__textarea::placeholder' => 'color: {{VALUE}}; opacity: 1;',
),
)
);
$this->add_control(
'input_bg',
array(
'label' => __( 'Background color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__input, {{WRAPPER}} .a1tools-web-form__select, {{WRAPPER}} .a1tools-web-form__textarea' => 'background-color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array(
'name' => 'input_typography',
'selector' => '{{WRAPPER}} .a1tools-web-form__input, {{WRAPPER}} .a1tools-web-form__select, {{WRAPPER}} .a1tools-web-form__textarea',
)
);
$this->add_group_control(
\Elementor\Group_Control_Border::get_type(),
array(
'name' => 'input_border',
'selector' => '{{WRAPPER}} .a1tools-web-form__input, {{WRAPPER}} .a1tools-web-form__select, {{WRAPPER}} .a1tools-web-form__textarea',
)
);
$this->add_control(
'input_radius',
array(
'label' => __( 'Border radius', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', '%', 'em' ),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__input, {{WRAPPER}} .a1tools-web-form__select, {{WRAPPER}} .a1tools-web-form__textarea' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
),
)
);
$this->add_control(
'input_focus_border',
array(
'label' => __( 'Focus border color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__input:focus, {{WRAPPER}} .a1tools-web-form__select:focus, {{WRAPPER}} .a1tools-web-form__textarea:focus' => 'border-color: {{VALUE}};',
),
)
);
$this->end_controls_section();
}
// ── Style : Required Asterisk ───────────────────────────────────
private function register_style_asterisk(): void {
$this->start_controls_section(
'style_asterisk',
array(
'label' => __( 'Required asterisk', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'asterisk_color',
array(
'label' => __( 'Color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'default' => '#dc2626',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__required' => 'color: {{VALUE}};',
),
)
);
$this->add_control(
'asterisk_weight',
array(
'label' => __( 'Weight', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::SELECT,
'options' => array(
'400' => __( 'Normal', 'a1-tools' ),
'600' => __( 'Semi-bold', 'a1-tools' ),
'700' => __( 'Bold', 'a1-tools' ),
),
'default' => '700',
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__required' => 'font-weight: {{VALUE}};',
),
)
);
$this->end_controls_section();
}
// ── Style : Submit Button ───────────────────────────────────────
private function register_style_submit(): void {
$this->start_controls_section(
'style_submit',
array(
'label' => __( 'Submit Button', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
// 2.5.22 — alignment uses CSS variables (`--submit-justify`,
// `--submit-width`) rather than `justify-content: stretch`
// (which is invalid CSS and silently falls back to flex-end)
// or `[data-align="full"]` attribute matching (which doesn't
// update in Elementor's live editor preview until save+reload
// because the editor only re-injects CSS, not HTML). Both
// values land on the `.a1tools-web-form__actions` wrapper and
// cascade into the button via `var()`.
$this->add_control(
'submit_alignment',
array(
'label' => __( 'Alignment', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::CHOOSE,
'options' => array(
'left' => array( 'title' => __( 'Left', 'a1-tools' ), 'icon' => 'eicon-text-align-left' ),
'center' => array( 'title' => __( 'Center', 'a1-tools' ), 'icon' => 'eicon-text-align-center' ),
'right' => array( 'title' => __( 'Right', 'a1-tools' ), 'icon' => 'eicon-text-align-right' ),
'full' => array( 'title' => __( 'Full width', 'a1-tools' ), 'icon' => 'eicon-text-align-justify' ),
),
'default' => 'left',
'selectors_dictionary' => array(
'left' => '--submit-justify: flex-start; --submit-width: auto;',
'center' => '--submit-justify: center; --submit-width: auto;',
'right' => '--submit-justify: flex-end; --submit-width: auto;',
'full' => '--submit-justify: stretch; --submit-width: 100%;',
),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__actions' => '{{VALUE}}',
),
)
);
$this->start_controls_tabs( 'submit_tabs' );
$this->start_controls_tab( 'submit_normal_tab', array( 'label' => __( 'Normal', 'a1-tools' ) ) );
$this->add_control( 'submit_bg', array(
'label' => __( 'Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array( '{{WRAPPER}} .a1tools-web-form__submit' => 'background-color: {{VALUE}};' ),
) );
$this->add_control( 'submit_color', array(
'label' => __( 'Text color', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array( '{{WRAPPER}} .a1tools-web-form__submit' => 'color: {{VALUE}};' ),
) );
$this->end_controls_tab();
$this->start_controls_tab( 'submit_hover_tab', array( 'label' => __( 'Hover', 'a1-tools' ) ) );
$this->add_control( 'submit_bg_hover', array(
'label' => __( 'Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array( '{{WRAPPER}} .a1tools-web-form__submit:hover' => 'background-color: {{VALUE}};' ),
) );
$this->add_control( 'submit_color_hover', array(
'label' => __( 'Text color', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array( '{{WRAPPER}} .a1tools-web-form__submit:hover' => 'color: {{VALUE}};' ),
) );
$this->end_controls_tab();
$this->end_controls_tabs();
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array( 'name' => 'submit_typography', 'selector' => '{{WRAPPER}} .a1tools-web-form__submit' )
);
$this->add_control( 'submit_radius', array(
'label' => __( 'Border radius', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', '%', 'em' ),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__submit' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
),
) );
$this->add_responsive_control( 'submit_padding', array(
'label' => __( 'Padding', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::DIMENSIONS,
'size_units' => array( 'px', 'em' ),
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__submit' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};',
),
) );
$this->end_controls_section();
}
// ── Style : Consent Checkbox ────────────────────────────────────
private function register_style_consent(): void {
$this->start_controls_section(
'style_consent',
array(
'label' => __( 'Consent & Anti-bot blocks', 'a1-tools' ),
'tab' => \Elementor\Controls_Manager::TAB_STYLE,
)
);
$this->add_control(
'consent_text_color',
array(
'label' => __( 'Text color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__consent, {{WRAPPER}} .a1tools-web-form__antibot' => 'color: {{VALUE}};',
),
)
);
$this->add_control(
'consent_link_color',
array(
'label' => __( 'Link color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__consent a' => 'color: {{VALUE}};',
),
)
);
$this->add_group_control(
\Elementor\Group_Control_Typography::get_type(),
array(
'name' => 'consent_typography',
'selector' => '{{WRAPPER}} .a1tools-web-form__consent, {{WRAPPER}} .a1tools-web-form__antibot',
)
);
$this->add_control(
'consent_check_color',
array(
'label' => __( 'Checkbox accent color', 'a1-tools' ),
'type' => \Elementor\Controls_Manager::COLOR,
'selectors' => array(
'{{WRAPPER}} .a1tools-web-form__consent input[type="checkbox"], {{WRAPPER}} .a1tools-web-form__antibot input[type="checkbox"]' => 'accent-color: {{VALUE}};',
),
)
);
$this->end_controls_section();
}
// ────────────────────────────────────────────────────────────────
// Render
// ────────────────────────────────────────────────────────────────
protected function render(): void {
$settings = $this->get_settings_for_display();
// (2.5.6) Auto-resolve company_id from the A1 Tools API when
// the WP-Admin option is still at the default 0 — see
// class-a1-tools-web-chat-widget.php for the rationale.
$company_id = (int) get_option( 'a1tools_form_crm_company_id', 0 );
if ( $company_id === 0 && function_exists( 'a1tools_resolve_crm_company_id' ) ) {
$company_id = a1tools_resolve_crm_company_id();
}
$rest_url = esc_url_raw( rest_url( 'a1-tools/v1/submit-web-form' ) );
$page_url = esc_url( is_singular() ? get_permalink() : home_url( add_query_arg( null, null ) ) );
$form_id = 'a1tools-web-form-' . wp_unique_id();
// Build merged Standard Fields keyed by field_name, defaulting
// to the prebuilt defaults so an empty repeater (i.e. first
// install) still renders all 10 fields.
$std_rows = is_array( $settings['standard_fields'] ?? null ) && ! empty( $settings['standard_fields'] )
? $settings['standard_fields']
: self::default_standard_fields();
$custom_rows = is_array( $settings['custom_fields'] ?? null ) ? $settings['custom_fields'] : array();
$submit_align = $settings['submit_alignment'] ?? 'left';
// Consent
$consent_enabled = ( $settings['consent_enabled'] ?? '' ) === 'yes';
$consent_required = ( $settings['consent_required'] ?? 'yes' ) === 'yes';
$consent_text = '';
if ( $consent_enabled ) {
$privacy_url = ! empty( $settings['consent_privacy_url']['url'] )
? $settings['consent_privacy_url']['url']
: home_url( '/privacy-policy/' );
$sms_terms_url = ! empty( $settings['consent_sms_terms_url']['url'] )
? $settings['consent_sms_terms_url']['url']
: home_url( '/terms-and-conditions/' );
$help_phone = $settings['consent_help_phone'] ?? '';
$raw_consent = wp_kses_post( $settings['consent_text'] ?? '' );
$consent_text = strtr( $raw_consent, array(
'{{privacy_url}}' => esc_url( $privacy_url ),
'{{sms_terms_url}}' => esc_url( $sms_terms_url ),
'{{help_phone}}' => esc_html( $help_phone ),
) );
}
// Anti-bot
$antibot_enabled = ( $settings['antibot_enabled'] ?? '' ) === 'yes';
$antibot_label = $settings['antibot_label'] ?? __( 'I\'m not a robot', 'a1-tools' );
?>