start_controls_section( 'mode_section', array( 'label' => __( 'Placement', 'a1-tools' ), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, ) ); $this->add_control( 'placement', array( 'label' => __( 'Placement', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'floating', 'options' => array( 'floating' => __( 'Floating (fixed to corner)', 'a1-tools' ), 'inline' => __( 'Inline (place in layout)', 'a1-tools' ), ), 'description' => __( 'Both modes share the same conversation — drop multiple onto a page if you want a sticky button + an in-content CTA.', 'a1-tools' ), ) ); $this->add_control( 'floating_corner', array( 'label' => __( 'Floating Corner', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SELECT, 'default' => 'br', 'options' => array( 'br' => __( 'Bottom Right', 'a1-tools' ), 'bl' => __( 'Bottom Left', 'a1-tools' ), 'tr' => __( 'Top Right', 'a1-tools' ), 'tl' => __( 'Top Left', 'a1-tools' ), ), 'condition' => array( 'placement' => 'floating' ), ) ); $this->end_controls_section(); // ── Button content ──────────────────────────────────────── $this->start_controls_section( 'content_section', array( 'label' => __( 'Button & Window', 'a1-tools' ), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, ) ); $this->add_control( 'button_label', array( 'label' => __( 'Button Label', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'Live chat', 'a1-tools' ), ) ); $this->add_control( 'show_icon', array( 'label' => __( 'Show Icon', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SWITCHER, 'default' => 'yes', 'return_value' => 'yes', ) ); $this->add_control( 'window_title', array( 'label' => __( 'Window Title', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'Chat with us', 'a1-tools' ), ) ); $this->add_control( 'window_subtitle', array( 'label' => __( 'Window Subtitle', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( "We're online and ready to help.", 'a1-tools' ), ) ); $this->add_control( 'pre_chat_intro', array( 'label' => __( 'Pre-chat Intro', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXTAREA, 'default' => __( 'Quick details first so we can get back to you if we disconnect:', 'a1-tools' ), 'label_block' => true, ) ); $this->add_control( 'greeting_message', array( 'label' => __( 'Greeting (sent after pre-chat)', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXTAREA, 'default' => __( 'Hi! Thanks for reaching out, a team member will be with you as soon as possible.', 'a1-tools' ), 'label_block' => true, ) ); $this->add_control( 'offline_message', array( 'label' => __( 'Offline Subhead', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( "We're offline — leave a note and we'll reply by email or call.", 'a1-tools' ), 'description' => __( 'Shown when no dispatcher is currently connected.', 'a1-tools' ), ) ); $this->end_controls_section(); // ── Pre-chat Form (content) ──────────────────────────────── // (2.5.2) Mirrors the field configurability added to the Web // Form widget. The pre-chat form has only two fields by design // (name + phone-or-email) — both required by default since // CRM dispatchers need *some* way to reconnect if the visitor // closes the tab. Each field's label and required-flag is // editable here. $this->start_controls_section( 'prechat_section', array( 'label' => __( 'Pre-chat Form', 'a1-tools' ), 'tab' => \Elementor\Controls_Manager::TAB_CONTENT, ) ); $this->add_control( 'prechat_name_label', array( 'label' => __( 'Name field label', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'Your name', 'a1-tools' ), ) ); $this->add_control( 'prechat_name_required', array( 'label' => __( 'Name required', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'prechat_contact_label', array( 'label' => __( 'Phone-or-Email field label', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'Phone or email', 'a1-tools' ), 'description' => __( 'Single field accepting either a phone number or an email address.', 'a1-tools' ), ) ); $this->add_control( 'prechat_contact_required', array( 'label' => __( 'Contact required', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'prechat_message_label', array( 'label' => __( 'Question/message field label', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'How can we help?', 'a1-tools' ), 'description' => __( 'Free-form question or message the visitor types before starting the chat. Sent as their first message in the conversation. (Added in 2.5.15.)', 'a1-tools' ), ) ); $this->add_control( 'prechat_message_required', array( 'label' => __( 'Question/message required', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SWITCHER, 'return_value' => 'yes', 'default' => 'yes', ) ); $this->add_control( 'prechat_message_placeholder', array( 'label' => __( 'Question/message placeholder', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( "Tell us what you'd like to know…", 'a1-tools' ), ) ); $this->add_control( 'prechat_submit_label', array( 'label' => __( 'Start-chat button label', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::TEXT, 'default' => __( 'Start chat', 'a1-tools' ), ) ); $this->end_controls_section(); // ── Button Style ──────────────────────────────────────── $this->start_controls_section( 'style_button_section', array( 'label' => __( 'Button Style', 'a1-tools' ), 'tab' => \Elementor\Controls_Manager::TAB_STYLE, ) ); $this->start_controls_tabs( 'button_state_tabs' ); $this->start_controls_tab( 'button_normal_tab', array( 'label' => __( 'Normal', 'a1-tools' ) ) ); $this->add_control( 'button_bg_color', array( 'label' => __( 'Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#f49320', 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'button_text_color', array( 'label' => __( 'Text/Icon', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#ffffff', 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button' => 'color: {{VALUE}};', ), ) ); $this->end_controls_tab(); $this->start_controls_tab( 'button_hover_tab', array( 'label' => __( 'Hover', 'a1-tools' ) ) ); $this->add_control( 'button_bg_hover_color', array( 'label' => __( 'Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#d67d10', 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button:hover' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'button_text_hover_color', array( 'label' => __( 'Text/Icon', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#ffffff', 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button:hover' => 'color: {{VALUE}};', ), ) ); $this->end_controls_tab(); $this->end_controls_tabs(); $this->add_group_control( \Elementor\Group_Control_Typography::get_type(), array( 'name' => 'button_typography', 'selector' => '{{WRAPPER}} .a1tools-web-chat__button', ) ); $this->add_control( 'button_border_radius', array( 'label' => __( 'Border Radius', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::DIMENSIONS, 'size_units' => array( 'px', '%' ), 'default' => array( 'top' => 30, 'right' => 30, 'bottom' => 30, 'left' => 30, 'unit' => 'px', ), 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', ), ) ); $this->add_responsive_control( 'button_padding', array( 'label' => __( 'Padding', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::DIMENSIONS, 'size_units' => array( 'px', 'em', '%' ), 'default' => array( 'top' => 14, 'right' => 24, 'bottom' => 14, 'left' => 24, 'unit' => 'px', ), 'selectors' => array( '{{WRAPPER}} .a1tools-web-chat__button' => 'padding: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}};', ), ) ); $this->add_group_control( \Elementor\Group_Control_Box_Shadow::get_type(), array( 'name' => 'button_box_shadow', 'selector' => '{{WRAPPER}} .a1tools-web-chat__button', ) ); $this->end_controls_section(); // ── Window Style ─────────────────────────────────────── $this->start_controls_section( 'style_window_section', array( 'label' => __( 'Window Style', 'a1-tools' ), 'tab' => \Elementor\Controls_Manager::TAB_STYLE, ) ); $this->add_control( 'window_header_bg', array( 'label' => __( 'Header Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#f49320', 'selectors' => array( '.a1tools-web-chat-window__header' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'window_header_text', array( 'label' => __( 'Header Text', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#ffffff', 'selectors' => array( '.a1tools-web-chat-window__header, .a1tools-web-chat-window__header *' => 'color: {{VALUE}};', ), ) ); $this->add_control( 'window_dispatcher_bubble', array( 'label' => __( 'Dispatcher Bubble', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#eef2ff', 'selectors' => array( '.a1tools-web-chat-window__msg--dispatcher .a1tools-web-chat-window__bubble' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'window_visitor_bubble', array( 'label' => __( 'Visitor Bubble', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#f49320', 'selectors' => array( '.a1tools-web-chat-window__msg--visitor .a1tools-web-chat-window__bubble' => 'background-color: {{VALUE}}; color: #fff;', ), ) ); // (2.5.2) Window container styling — bg, border, radius, shadow. // These keep the chat window visually coherent with the host // page rather than always being the default white card. // Selectors are unscoped (no {{WRAPPER}}) because the window // renders into document.body via the global JS module, not as // a child of the Elementor widget wrapper. $this->add_control( 'window_bg', array( 'label' => __( 'Window Background', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '#ffffff', 'selectors' => array( '.a1tools-web-chat-window' => 'background-color: {{VALUE}};', ), ) ); $this->add_control( 'window_border_color', array( 'label' => __( 'Window Border Color', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::COLOR, 'default' => '', 'selectors' => array( '.a1tools-web-chat-window' => 'border-color: {{VALUE}}; border-style: solid;', ), ) ); $this->add_control( 'window_border_width', array( 'label' => __( 'Window Border Width', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::SLIDER, 'size_units' => array( 'px' ), 'range' => array( 'px' => array( 'min' => 0, 'max' => 6 ) ), 'default' => array( 'size' => 0, 'unit' => 'px' ), 'selectors' => array( '.a1tools-web-chat-window' => 'border-width: {{SIZE}}{{UNIT}}; border-style: solid;', ), ) ); $this->add_control( 'window_radius', array( 'label' => __( 'Window Border Radius', 'a1-tools' ), 'type' => \Elementor\Controls_Manager::DIMENSIONS, 'size_units' => array( 'px', '%' ), 'selectors' => array( '.a1tools-web-chat-window' => 'border-radius: {{TOP}}{{UNIT}} {{RIGHT}}{{UNIT}} {{BOTTOM}}{{UNIT}} {{LEFT}}{{UNIT}}; overflow: hidden;', ), ) ); $this->add_group_control( \Elementor\Group_Control_Box_Shadow::get_type(), array( 'name' => 'window_shadow', 'selector' => '.a1tools-web-chat-window', ) ); $this->add_group_control( \Elementor\Group_Control_Typography::get_type(), array( 'name' => 'window_typography', 'selector' => '.a1tools-web-chat-window, .a1tools-web-chat-window input, .a1tools-web-chat-window textarea, .a1tools-web-chat-window button', ) ); $this->end_controls_section(); } // ──────────────────────────────────────────────────────────────── // Render // ──────────────────────────────────────────────────────────────── /** * Business-hours predicate for the Live Chat widget. * * Staffed Monday – Friday, 9:00 AM – 4:00 PM Miami time * (America/New_York — automatically handles EST/EDT DST). Saturday * and Sunday are fully off regardless of hour. * * Note: as of 2.5.31 this is NOT used to gate render() anymore * (the cache trap made server-side gating impossible to rely on — * a cached page rendered during off-hours had zero widget HTML * and the JS gate had nothing to reveal). The widget HTML is now * always rendered; show/hide is decided client-side by * `isWithinMiamiBusinessHours()` in `assets/web-chat.js`. This * function is kept for diagnostic / debug-tool use and as a * single source of the canonical rule that the JS mirrors. * * @since 2.5.12 (hours) * @since 2.5.27 (weekend gate) * @since 2.5.31 (cutoff rolled back 5 PM → 4 PM; no longer gates render) * @return bool True when the widget should render. */ private function is_within_business_hours() { try { $now = new \DateTimeImmutable( 'now', new \DateTimeZone( 'America/New_York' ) ); } catch ( \Exception $e ) { // Defensive — if PHP can't resolve the timezone we'd rather // fail open (show the chat) than fail closed and lose every // lead in the gap until someone investigates. return true; } // (2.5.27) Weekend gate. `N` returns 1 (Mon) … 7 (Sun); // 6 = Saturday, 7 = Sunday. Both fully suppress the widget, // regardless of hour. $dow = (int) $now->format( 'N' ); if ( $dow >= 6 ) { return false; } $hour = (int) $now->format( 'G' ); // 0-23 // (2.5.31) Cutoff rolled back to 4 PM at user's request — the // 2.5.13 extension to 5 PM was abandoned. 9:00 (inclusive) → // 16:00 (exclusive): 9:00 AM up to 3:59 PM. At 4:00 PM the // widget disappears. return $hour >= 9 && $hour < 16; } protected function render() { // (2.5.31) Architecture change to fix the cache trap. // // Earlier versions early-returned here when off-hours, emitting // zero HTML. Page caching layers (SiteGround SuperCacher, // Cloudflare, WP Rocket, etc.) cached that empty HTML at off- // hours visits and re-served it during the next business-hours // window, so visitors saw no chat button even when the chat WAS // supposed to be live. The JS-side gate (added 2.5.27) couldn't // rescue the situation — it can only hide existing widget HTML, // not materialise widget HTML that PHP never rendered. // // The widget is now ALWAYS rendered. In frontend mode the HTML // carries `style="display:none"` plus `data-bh-gate="1"`; the // JS at init time inspects the gate attribute, runs // `isWithinMiamiBusinessHours()`, and either reveals the // placement (within hours) or removes it from the DOM (outside // hours / weekend). Cached pages now honour the schedule // because the gating decision happens in the visitor's browser, // not at cache time. // // In Elementor editor / preview mode we skip the gate entirely // so admins can style the widget at any time of day. $is_editor = false; if ( class_exists( '\\Elementor\\Plugin' ) && isset( \Elementor\Plugin::$instance ) ) { $inst = \Elementor\Plugin::$instance; $is_editor = ( isset( $inst->editor ) && $inst->editor->is_edit_mode() ) || ( isset( $inst->preview ) && $inst->preview->is_preview_mode() ); } $settings = $this->get_settings_for_display(); // Read the CRM company id from the SAME option key the existing // form pipeline + admin UI use (`a1tools_form_crm_company_id`). // An earlier draft of this widget mistakenly read // `a1tools_form_company_id` (no `_crm_`), which doesn't exist — // every site rendered the widget with `data-company-id="0"` and // the JS bailed at the `if (!config.companyId)` check, so the // button click handler was never attached. (Plugin 2.5.1.) // // (2.5.6) When the option is still 0 (the default — most sites // never get manually configured), fall through to the // auto-resolver which hits `?action=public&url=` // once and back-fills the option from the server's // `wordpress_sites → wordpress_groups.company_id` lookup. // Result is cached in the option, so subsequent renders // skip the API hop. $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(); } $placement = in_array( ( $settings['placement'] ?? '' ), array( 'inline', 'floating' ), true ) ? $settings['placement'] : 'floating'; $corner = $settings['floating_corner'] ?? 'br'; $button_label = $settings['button_label'] ?? __( 'Live chat', 'a1-tools' ); $window_title = $settings['window_title'] ?? __( 'Chat with us', 'a1-tools' ); $window_subtitle = $settings['window_subtitle'] ?? ''; $pre_chat_intro = $settings['pre_chat_intro'] ?? ''; $greeting = $settings['greeting_message'] ?? ''; $offline_msg = $settings['offline_message'] ?? ''; $show_icon = ( $settings['show_icon'] ?? 'yes' ) === 'yes'; // (2.5.2) Pre-chat form labels — passed to the JS module via // data attributes so the same global window honours per-widget // label overrides on the host page. $prechat_name_label = $settings['prechat_name_label'] ?? __( 'Your name', 'a1-tools' ); $prechat_name_required = ( $settings['prechat_name_required'] ?? 'yes' ) === 'yes'; $prechat_contact_label = $settings['prechat_contact_label'] ?? __( 'Phone or email', 'a1-tools' ); $prechat_contact_required = ( $settings['prechat_contact_required'] ?? 'yes' ) === 'yes'; $prechat_submit_label = $settings['prechat_submit_label'] ?? __( 'Start chat', 'a1-tools' ); // (2.5.15) Pre-chat question/message field. $prechat_message_label = $settings['prechat_message_label'] ?? __( 'How can we help?', 'a1-tools' ); $prechat_message_required = ( $settings['prechat_message_required'] ?? 'yes' ) === 'yes'; $prechat_message_placeholder = $settings['prechat_message_placeholder'] ?? __( "Tell us what you'd like to know…", 'a1-tools' ); $api_base = 'https://tools.a-1chimney.com/api/web_chat.php'; $site_url = home_url(); // Per-widget DOM id so multiple placements on one page coexist. $wid_id = 'a1tools-web-chat-' . wp_unique_id(); $wrap_classes = array( 'a1tools-web-chat' ); $wrap_classes[] = 'a1tools-web-chat--' . ( 'inline' === $placement ? 'inline' : 'floating' ); if ( 'floating' === $placement ) { $wrap_classes[] = 'a1tools-web-chat--corner-' . sanitize_key( $corner ); } // (2.5.31) See the long comment in render() above. In frontend // mode the placement is hidden by default + tagged with // `data-bh-gate="1"`. JS at init time looks for that attribute // and decides whether to reveal or remove the widget based on // the visitor's clock (converted to Miami time). In Elementor // editor / preview mode we render without either so admins can // always see + style the widget regardless of the current hour. $initial_style = $is_editor ? '' : 'display:none;'; $gate_attr = $is_editor ? '' : ' data-bh-gate="1"'; ?>
data-endpoint="" data-company-id="" data-site-url="" data-window-title="" data-window-subtitle="" data-pre-chat-intro="" data-greeting="" data-offline="" data-prechat-name-label="" data-prechat-name-required="" data-prechat-contact-label="" data-prechat-contact-required="" data-prechat-submit-label="" data-prechat-message-label="" data-prechat-message-required="" data-prechat-message-placeholder="">