<?php
/**
* The public-facing functionality of the plugin.
*
* @link http://example.com
* @since 1.0.0
*
* @package LeadConnector
* @subpackage LeadConnector/public
*/
/**
* The public-facing functionality of the plugin.
*
* Defines the plugin name, version, and two examples hooks for how to
* enqueue the public-facing stylesheet and JavaScript.
*
* @package LeadConnector
* @subpackage LeadConnector/public
* @author Harsh Kurra
*/
class LC_CustomValueStringReplacableObject
{
/**
* @var mixed
*/
private $content;
/**
* @var bool
*/
private $isValid;
/**
* LC_CustomValueStringReplacableObject constructor.
*
* @param mixed $content
* @param bool $isValid
*/
public function __construct($content, $isValid)
{
$this->content = $content;
$this->isValid = (bool) $isValid;
}
/**
* Get the content value.
*/
public function getContent()
{
return $this->content;
}
/**
* Check if the return value is valid.
*/
public function isValid()
{
return $this->isValid ?? false;
}
}
class LeadConnector_Public
{
/**
* The ID of this plugin.
*
* @since 1.0.0
* @access private
* @var string $plugin_name The ID of this plugin.
*/
private $plugin_name;
/**
* The version of this plugin.
*
* @since 1.0.0
* @access private
* @var string $version The current version of this plugin.
*/
private $version;
/**
* Initialize the class and set its properties.
*
* @since 1.0.0
* @param string $plugin_name The name of the plugin.
* @param string $version The version of this plugin.
*/
public function __construct($plugin_name, $version)
{
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/lc-constants.php';
$this->plugin_name = $plugin_name;
$this->version = $version;
// Add filters for custom value placeholders across various content types
// Post/Page Content
add_filter('the_content', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('the_excerpt', array($this, 'replace_custom_value_placeholders'), 20);
// Titles
add_filter('the_title', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('wp_title', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('document_title_parts', array($this, 'replace_custom_value_in_title_parts'), 20);
add_filter('pre_get_document_title', array($this, 'replace_custom_value_placeholders'), 20);
// Widget Content
add_filter('widget_text', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('widget_text_content', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('widget_title', array($this, 'replace_custom_value_placeholders'), 20);
// Meta Description and SEO
add_filter('get_the_excerpt', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('meta_description', array($this, 'replace_custom_value_placeholders'), 20);
// Navigation
add_filter('nav_menu_item_title', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('wp_nav_menu_items', array($this, 'replace_custom_value_placeholders'), 20);
// Block-based Navigation
add_filter('render_block_core/navigation', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('render_block_core/navigation-link', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('render_block_core/page-list', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('render_block', array($this, 'replace_custom_value_in_blocks'), 20, 2);
// Comments
add_filter('comment_text', array($this, 'replace_custom_value_placeholders'), 20);
add_filter('comment_excerpt', array($this, 'replace_custom_value_placeholders'), 20);
// Elementor highlight functionality
add_action('wp_head', array($this, 'inject_elementor_highlight_styles'), 999);
}
/**
* Register the stylesheets for the public-facing side of the site.
*
* @since 1.0.0
*/
public function enqueue_styles()
{
/**
* This function is provided for demonstration purposes only.
*
* An instance of this class should be passed to the run() function
* defined in Plugin_Name_Loader as all of the hooks are defined
* in that particular class.
*
* The Plugin_Name_Loader will then create the relationship
* between the defined hooks and the functions defined in this
* class.
*/
wp_enqueue_style($this->plugin_name, plugin_dir_url(__FILE__) . 'css/lc-public.css', array(), $this->version, 'all');
}
/**
* Validates and sanitizes content before processing
*
* @param mixed $content The content to validate
* @return string Sanitized content
*/
private function sanitize_content($content): LC_CustomValueStringReplacableObject
{
// Handle null or empty content
if ($content === null) {
return new LC_CustomValueStringReplacableObject(null, false);
}
// Handle WP_Post objects
if ($content instanceof WP_Post) {
return new LC_CustomValueStringReplacableObject($content, false);
}
// Handle arrays (like from ACF fields)
if (is_array($content)) {
// return implode(' ', array_map('strval', array_filter($content, 'is_scalar')));
return new LC_CustomValueStringReplacableObject($content, false);
}
// Handle objects that implement __toString()
if (is_object($content) && method_exists($content, '__toString')) {
return new LC_CustomValueStringReplacableObject($content, false);
}
// Handle scalar values (string, int, float, bool)
if (is_scalar($content)) {
return new LC_CustomValueStringReplacableObject($content, true);
}
// Return empty string for unsupported types
return new LC_CustomValueStringReplacableObject('', false);
}
public function replace_custom_value_placeholders($content)
{
$sanitized_content = $this->sanitize_content($content);
if (!$sanitized_content->isValid()) {
return $content;
}
// Initialize CustomValues class
require_once plugin_dir_path(dirname(__FILE__)) . 'includes/CutomValues/CustomValues.php';
$custom_values = new LeadConnector_CustomValues();
try {
// Pattern to match {{ custom_value.key }} with optional spaces
return preg_replace_callback('/\{\{\s*custom_values\.(\w+)\s*\}\}/', function ($matches) use ($custom_values) {
$field_key = trim($matches[1]); // Trim any extra whitespace
if (empty($field_key)) {
return '';
}
$value = $custom_values->getValue($field_key);
return $value !== null ? (string) $value : ''; // Ensure string conversion
}, $content);
} catch (Exception $e) {
// Log error if needed
error_log('LeadConnector: Error in replace_custom_value_placeholders: ' . $e->getMessage());
return $content; // Return original content on error
}
}
/**
* Replace custom value placeholders in document title parts
*
* @param array $title_parts The title parts array
* @return array The processed title parts
*/
public function replace_custom_value_in_title_parts($title_parts)
{
if (!is_array($title_parts)) {
return $title_parts;
}
foreach ($title_parts as $key => $part) {
if (is_string($part)) {
$title_parts[$key] = $this->replace_custom_value_placeholders($part);
}
}
return $title_parts;
}
/**
* Replace custom value placeholders in block content
*
* @param string $block_content The block content about to be rendered
* @param array $block The full block, including name and attributes
* @return string
*/
public function replace_custom_value_in_blocks($block_content, $block)
{
// Only process navigation-related blocks or blocks that might contain navigation
$navigation_blocks = ['core/navigation', 'core/navigation-link', 'core/page-list'];
if (empty($block['blockName'])) {
return $block_content;
}
// Process if it's a navigation block or if content contains custom_values placeholder
if (in_array($block['blockName'], $navigation_blocks) || strpos($block_content, 'custom_values.') !== false) {
return $this->replace_custom_value_placeholders($block_content);
}
return $block_content;
}
public function enqueue_scripts()
{
/**
* This function is provided for demonstration purposes only.
*
* An instance of this class should be passed to the run() function
* defined in LeadConnector_Loader as all of the hooks are defined
* in that particular class.
*
* The LeadConnector_Loader will then create the relationship
* between the defined hooks and the functions defined in this
* class.
*/
$options = get_option(LEAD_CONNECTOR_OPTION_NAME);
$heading = "";
$sub_heading = "";
$enabledTextWidget = 0;
if (isset($options[lead_connector_constants\lc_options_enable_text_widget])) {
$enabledTextWidget = esc_attr($options[lead_connector_constants\lc_options_enable_text_widget]);
}
$text_widget_error = '';
if (isset($options[lead_connector_constants\lc_options_text_widget_error])) {
$text_widget_error = esc_attr($options[lead_connector_constants\lc_options_text_widget_error]);
}
if ($enabledTextWidget == 1 && isset($options[lead_connector_constants\lc_options_location_id])) {
$location_id = $options[lead_connector_constants\lc_options_location_id];
if (isset($options[lead_connector_constants\lc_options_text_widget_heading])) {
$heading = esc_attr($options[lead_connector_constants\lc_options_text_widget_heading]);
}
if (isset($options[lead_connector_constants\lc_options_text_widget_sub_heading])) {
$sub_heading = esc_attr($options[lead_connector_constants\lc_options_text_widget_sub_heading]);
}
$use_email_field = "0";
$chat_widget_settings = null;
$usingOldWidget = true;
if (isset($options[lead_connector_constants\lc_options_text_widget_settings])) {
$chat_widget_settings = json_decode($options[lead_connector_constants\lc_options_text_widget_settings]);
if (!isset($options[lead_connector_constants\lc_options_selected_chat_widget_id])) {
if ($chat_widget_settings && isset($chat_widget_settings->widgetId)) {
// Save Option Here
$options[lead_connector_constants\lc_options_selected_chat_widget_id] = $chat_widget_settings->widgetId;
update_option(LEAD_CONNECTOR_OPTION_NAME, $options);
}
}
}
if (isset($options[lead_connector_constants\lc_options_text_widget_use_email_filed])) {
$use_email_field = esc_attr($options[lead_connector_constants\lc_options_text_widget_use_email_filed]);
}
if (isset($options[lead_connector_constants\lc_options_selected_chat_widget_id])) {
$usingOldWidget = false;
}
if ($usingOldWidget) {
wp_enqueue_script($this->plugin_name . ".lc_text_widget", LEAD_CONNECTOR_CDN_BASE_URL . 'loader.js', '', $this->version, false);
wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/lc-public.js', array('jquery'), $this->version, false);
wp_localize_script($this->plugin_name, 'lc_public_js', array(
'text_widget_location_id' => $location_id,
'text_widget_heading' => $heading,
'text_widget_sub_heading' => $sub_heading,
'text_widget_error' => $text_widget_error,
'text_widget_use_email_field' => $use_email_field,
"text_widget_settings" => $chat_widget_settings,
"text_widget_cdn_base_url" => LEAD_CONNECTOR_CDN_BASE_URL,
));
} else {
if (isset($options[lead_connector_constants\lc_options_selected_chat_widget_id])) {
$widgetId = $options[lead_connector_constants\lc_options_selected_chat_widget_id];
add_action('wp_head', function () use ($widgetId) {
$config = wp_json_encode(array(
'src' => LC_CHAT_WIDGET_SRC,
'resourcesUrl' => LC_CHAT_WIDGET_RESOURCES_URL,
'widgetId' => $widgetId,
'serverUrl' => LC_CHAT_WIDGET_SERVER_URL,
'marketplaceUrl' => LC_CHAT_WIDGET_MARKETPLACE_URL,
));
echo '<script data-no-optimize="1" data-no-minify="1">'
. '(function(){var c=' . $config . ';'
. 'var s=document.createElement("script");'
. 's.src=c.src;'
. 's.setAttribute("data-resources-url",c.resourcesUrl);'
. 's.setAttribute("data-widget-id",c.widgetId);'
. 's.setAttribute("data-server-u-r-l",c.serverUrl);'
. 's.setAttribute("data-marketplace-u-r-l",c.marketplaceUrl);'
. 'document.head.appendChild(s);'
. '})();</script>';
});
}
}
}
}
/**
* Inject Elementor highlight styles when elementor_highlight=true parameter is present
*
* @since 1.0.0
*/
public function inject_elementor_highlight_styles()
{
// Check if elementor_highlight parameter is present and set to 'true'
$elementor_highlight = isset($_GET['elementor_highlight']) && $_GET['elementor_highlight'] === 'true';
if (!$elementor_highlight) {
return;
}
// Magic wand SVG icon (encoded for use in CSS)
// Classic magic wand with star at the end
$magic_wand_svg = urlencode('data:image/svg+xml;charset=utf-8,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%23ef4444\'%3E%3Cpath d=\'M17 5h2v14h-2V5zm-4 1h2v12h-2V6zm-4 1h2v10h-2V7zm-4 1h2v8H5V8zM12 2l2 2-2 2-2-2 2-2zm0 16l2 2-2 2-2-2 2-2z\'/%3E%3C/svg%3E');
?>
<style id="leadconnector-elementor-highlight">
/* Custom Hero Classes - Red highlight */
.hero-heading-class:hover,
.hero-description-class:hover,
.about-us-story-content-class:hover,
.about-us-story-header-class:hover {
outline: 2px dashed #84ADFF !important;
outline-offset: 4px !important;
position: relative !important;
cursor: pointer !important;
}
/* Elementor Heading Widget - Magic wand icon on hover */
/* .elementor-widget-heading:hover::before,
.elementor-element.elementor-widget-heading:hover::before {
content: '';
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
background-image: url('<?php echo $magic_wand_svg; ?>');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
z-index: 9999;
pointer-events: none;
} */
/* Elementor Elements - Blue highlight - COMMENTED OUT: will visit later */
/* .elementor-element:hover:not(.elementor-widget-heading):not(.elementor-element.elementor-widget-heading):not(.elementor-widget-text-editor):not(.elementor-element.elementor-widget-text-editor),
.elementor-section:hover {
outline: 2px dashed #3b82f6 !important;
outline-offset: 4px !important;
position: relative !important;
cursor: pointer !important;
} */
</style>
<script id="leadconnector-elementor-hover-events">
(function() {
'use strict';
// Throttle function to limit event frequency
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Get full text content from element, including nested elements
function getFullText(element) {
// For heading widgets, get direct text content
return (element.textContent || element.innerText || '').trim();
}
// Send message to parent window
function sendToParent(eventType, element, elementType) {
try {
if (window.parent && window.parent !== window) {
const rect = element.getBoundingClientRect();
const elementData = {
type: eventType, // 'elementor-click'
elementType: elementType, // 'heading', 'description', 'about-us-story-header', 'about-us-story-content'
selector: element.className ? element.className.split(' ').find(cls => cls.includes('hero-heading-class') || cls.includes('hero-description-class') || cls.includes('about-us-story-header-class') || cls.includes('about-us-story-content-class')) || '' : '',
classes: element.className || '',
position: {
x: Math.round(rect.left),
y: Math.round(rect.top),
width: Math.round(rect.width),
height: Math.round(rect.height)
},
text: (elementType === 'heading' || elementType === 'description' || elementType === 'about-us-story-header' || elementType === 'about-us-story-content') ? getFullText(element) : null
};
window.parent.postMessage({
source: 'leadconnector-elementor-iframe',
...elementData
}, '*'); // Using '*' for cross-origin - parent should validate origin
}
} catch (e) {
// Silently fail if postMessage fails (e.g., cross-origin restrictions)
console.warn('LeadConnector: Could not send message to parent', e);
}
}
// Send click function (no throttling needed for clicks)
const sendClick = function(element, elementType) {
sendToParent('elementor-click', element, elementType);
};
// Track elements that already have listeners to prevent duplicates
const elementsWithListeners = new WeakSet();
// Helper function to determine element type from class
function getElementType(element) {
if (element.classList.contains('hero-heading-class')) return 'heading';
if (element.classList.contains('hero-description-class')) return 'description';
if (element.classList.contains('about-us-story-header-class')) return 'about-us-story-header';
if (element.classList.contains('about-us-story-content-class')) return 'about-us-story-content';
return null;
}
// Initialize event listeners when DOM is ready
function initEventListeners() {
// All custom classes - click events
const customElements = document.querySelectorAll('.hero-heading-class, .hero-description-class, .about-us-story-content-class, .about-us-story-header-class');
customElements.forEach(function(element) {
// Skip if this element already has a listener
if (elementsWithListeners.has(element)) {
return;
}
// Create the click handler
const clickHandler = function(e) {
// Check if the click is on this element or any child element
const clickedElement = e.target;
const targetElement = element;
// Verify the clicked element is within our target element (handles child clicks)
if (!targetElement.contains(clickedElement) && clickedElement !== targetElement) {
return; // Click was outside our element, ignore
}
// Determine element type based on class
const elementType = getElementType(element);
if (elementType) {
sendClick(element, elementType);
}
// Don't stop propagation to allow normal elementor behavior
};
// Attach the listener with capture: true to catch events early
element.addEventListener('click', clickHandler, { passive: true, capture: true });
// Store the handler reference on the element for potential cleanup
element._leadConnectorClickHandler = clickHandler;
// Mark this element as having a listener
elementsWithListeners.add(element);
});
// Other Elementor elements (not headings or text-editor) - COMMENTED OUT: will visit later
/* const otherElements = document.querySelectorAll('.elementor-element:not(.elementor-widget-heading):not(.elementor-element.elementor-widget-heading):not(.elementor-widget-text-editor):not(.elementor-element.elementor-widget-text-editor):not(.elementor-section)');
otherElements.forEach(function(element) {
element.addEventListener('mouseenter', function(e) {
sendHover(element, 'element');
}, { passive: true });
element.addEventListener('mouseleave', function(e) {
sendUnhover(element, 'element');
}, { passive: true });
}); */
// Elementor sections - COMMENTED OUT: will visit later
/* const sections = document.querySelectorAll('.elementor-section');
sections.forEach(function(element) {
element.addEventListener('mouseenter', function(e) {
sendHover(element, 'section');
}, { passive: true });
element.addEventListener('mouseleave', function(e) {
sendUnhover(element, 'section');
}, { passive: true });
}); */
}
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initEventListeners);
} else {
// DOM already ready, but wait a bit for Elementor elements to be rendered
setTimeout(initEventListeners, 500);
}
// Also try after a longer delay in case elements load dynamically
setTimeout(initEventListeners, 1500);
// Listen for messages from parent window (Vue.js app)
function handleParentMessage(event) {
// Optional: Validate origin for security
// if (event.origin !== 'expected-origin') return;
// Check if message is from our parent app
if (event.data && event.data.source === 'leadconnector-parent-app') {
const messageType = event.data.type;
const messageData = event.data.data;
// Handle updateContent message
if (messageType === 'updateContent') {
updateHeroContent(messageData);
}
}
}
// Helper function to get target class from element type
function getTargetClass(elementType) {
const classMap = {
'heading': 'hero-heading-class',
'description': 'hero-description-class',
'about-us-story-header': 'about-us-story-header-class',
'about-us-story-content': 'about-us-story-content-class'
};
return classMap[elementType] || null;
}
// Function to update element content
function updateHeroContent(data) {
if (!data || !data.elementType || !data.content) {
console.warn('LeadConnector: Invalid updateContent data', data);
return;
}
// Determine which class to target based on elementType
const targetClass = getTargetClass(data.elementType);
if (!targetClass) {
console.warn('LeadConnector: Unknown elementType', data.elementType);
return;
}
// Find all elements with the target class
const elements = document.querySelectorAll('.' + targetClass);
if (elements.length === 0) {
console.warn('LeadConnector: No elements found with class', targetClass);
return;
}
// Update content for each element
elements.forEach(function(element) {
// Try to find the text content container
// For Elementor heading widgets, look for the heading tag (h1, h2, h3, etc.)
let contentElement = element.querySelector('h1, h2, h3, h4, h5, h6, .elementor-heading-title');
// For text-editor/widgets, look for the editor container
if (!contentElement) {
contentElement = element.querySelector('.elementor-widget-container, .elementor-text-editor');
}
// Fallback to the element itself
if (!contentElement) {
contentElement = element;
}
// Update only the text content (preserves styles and HTML structure)
if (data.content) {
// Use textContent to update only text, not HTML
// This preserves existing styles, classes, and structure
contentElement.textContent = data.content;
}
});
// Send confirmation back to parent
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
source: 'leadconnector-elementor-iframe',
type: 'content-updated',
elementType: data.elementType,
success: true
}, '*');
}
} catch (e) {
console.warn('LeadConnector: Could not send confirmation to parent', e);
}
}
// Add message listener
window.addEventListener('message', handleParentMessage);
})();
</script>
<?php
}
}