| // +----------------------------------------------------------------------+ /** * Elementor widget for displaying Visualizer charts. * * @category Visualizer * @package Elementor * * @since 3.11.16 */ if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Visualizer Elementor Widget */ class Visualizer_Elementor_Widget extends \Elementor\Widget_Base { /** * Register all Elementor-related hooks for the Visualizer widget. * * Called once from index.php on the plugins_loaded action (after Elementor * itself is confirmed present). Each inner hook fires at its normal time in * the WordPress lifecycle, so timing is identical to registering them * directly in visualizer_launch(). * * @return void */ public static function register_hooks() { // Register the widget with Elementor's widget manager. add_action( 'elementor/widgets/register', function ( $widgets_manager ) { $widgets_manager->register( new self() ); } ); // Register the Visualizer icon for the Elementor widget panel. add_action( 'elementor/editor/after_enqueue_styles', function () { $icon_url = VISUALIZER_ABSURL . 'images/visualizer-icon.svg'; wp_add_inline_style( 'elementor-icons', '.visualizer-elementor-icon { display:inline-block; width:1em; height:1em; background:url("' . esc_url( $icon_url ) . '") no-repeat center/contain; }' ); } ); // Enqueue Visualizer scripts inside the Elementor preview iframe. // Elementor serves the preview iframe as a shell page and injects widget HTML via // JavaScript (innerHTML), so wp_enqueue_script calls inside render() never reach the // iframe. We load all chart render libraries here so they are available when // elementor-widget-preview.js triggers visualizer:render:chart:start. add_action( 'elementor/preview/enqueue_scripts', function () { do_action( 'visualizer_enqueue_scripts' ); // ChartJS render library. if ( ! wp_script_is( 'numeral', 'registered' ) ) { wp_register_script( 'numeral', VISUALIZER_ABSURL . 'js/lib/numeral.min.js', array(), Visualizer_Plugin::VERSION, true ); } if ( ! wp_script_is( 'chartjs', 'registered' ) ) { wp_register_script( 'chartjs', VISUALIZER_ABSURL . 'js/lib/chartjs.min.js', array( 'numeral' ), null, true ); } wp_enqueue_script( 'visualizer-render-chartjs-lib', VISUALIZER_ABSURL . 'js/render-chartjs.js', array( 'chartjs', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true ); // Google Charts render library. wp_enqueue_script( 'visualizer-google-jsapi', '//www.gstatic.com/charts/loader.js', array(), null, true ); wp_enqueue_script( 'visualizer-render-google-lib', VISUALIZER_ABSURL . 'js/render-google.js', array( 'visualizer-google-jsapi', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true ); // DataTable render library + styles. if ( ! wp_script_is( 'visualizer-datatables', 'registered' ) ) { wp_register_script( 'visualizer-datatables', VISUALIZER_ABSURL . 'js/lib/datatables.min.js', array( 'jquery' ), Visualizer_Plugin::VERSION, true ); } wp_enqueue_script( 'visualizer-render-datatables-lib', VISUALIZER_ABSURL . 'js/render-datatables.js', array( 'visualizer-datatables', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true ); wp_enqueue_style( 'visualizer-datatables', VISUALIZER_ABSURL . 'css/lib/datatables.min.css', array(), Visualizer_Plugin::VERSION ); // D3 render library (AI charts). $d3_renderer_asset = VISUALIZER_ABSPATH . '/classes/Visualizer/D3Renderer/build/index.asset.php'; if ( file_exists( $d3_renderer_asset ) && ! wp_script_is( 'visualizer-d3-renderer', 'registered' ) ) { /** * Ignore missing build asset in source checkout. * * @phpstan-ignore-next-line */ $d3_asset = include $d3_renderer_asset; wp_register_script( 'visualizer-d3-renderer', VISUALIZER_ABSURL . 'classes/Visualizer/D3Renderer/build/index.js', array_merge( $d3_asset['dependencies'], array( 'jquery' ) ), $d3_asset['version'], true ); } if ( wp_script_is( 'visualizer-d3-renderer', 'registered' ) ) { wp_enqueue_script( 'visualizer-d3-renderer' ); wp_localize_script( 'visualizer-d3-renderer', 'vizD3Renderer', array( 'iframeJsUrl' => VISUALIZER_ABSURL . 'classes/Visualizer/D3Renderer/build/iframe.js', ) ); } // Elementor widget preview handler — uses frontend/element_ready hook. wp_enqueue_script( 'visualizer-elementor-preview', VISUALIZER_ABSURL . 'js/elementor-widget-preview.js', array( 'jquery', 'elementor-frontend' ), Visualizer_Plugin::VERSION, true ); // Prevent Elementor's editor-preview CSS from hiding our widget. // Elementor marks widgets without a content_template() as elementor-widget-empty // and adds display:none to .elementor-widget-empty when the panel is hidden // (.elementor-editor-preview on
). Our widget renders async (Google Charts // loads via callback), so the empty class is always present. wp_add_inline_style( 'visualizer-datatables', '.elementor-editor-preview .elementor-widget-visualizer-chart.elementor-widget-empty { display: block !important; }' ); } ); } /** * Get widget name. * * @return string Widget name. */ public function get_name() { return 'visualizer-chart'; } /** * Get widget title. * * @return string Widget title. */ public function get_title() { return esc_html__( 'Visualizer Chart', 'visualizer' ); } /** * Get widget icon. * * @return string Widget icon CSS class. */ public function get_icon() { return 'visualizer-elementor-icon'; } /** * Get widget categories. * * @return array' . esc_html__( 'Please select a chart from the widget settings.', 'visualizer' ) . '
'; } return; } // Detect Elementor edit / preview context early — needed before do_shortcode(). $is_editor = \Elementor\Plugin::$instance->editor->is_edit_mode() || \Elementor\Plugin::$instance->preview->is_preview_mode(); // In the editor, force lazy-loading off so the chart renders immediately in the // preview iframe without requiring a user-interaction event (scroll, hover, etc.). // Also suppress action buttons (edit, export, etc.) — they are meaningless inside // the Elementor preview and the edit link does nothing there. if ( $is_editor ) { add_filter( 'visualizer_lazy_load_chart', '__return_false' ); add_filter( 'visualizer_pro_add_actions', '__return_empty_array' ); } // Ensure visualizer-customization is registered before the shortcode enqueues // visualizer-render-{library} which depends on it. wp_enqueue_scripts never fires // in admin or AJAX contexts (Elementor editor / AJAX re-render), so we trigger the // action manually. It is a no-op when already registered. do_action( 'visualizer_enqueue_scripts' ); // Capture the shortcode output so we can parse the generated element ID. $html = do_shortcode( '[visualizer id="' . $chart_id . '"]' ); if ( $is_editor ) { remove_filter( 'visualizer_lazy_load_chart', '__return_false' ); remove_filter( 'visualizer_pro_add_actions', '__return_empty_array' ); // The shortcode enqueues visualizer-render-{library} (render-facade.js). // Dequeue it so Elementor's AJAX response doesn't inject it into the preview // iframe. The preview page already loads render-google.js / render-chartjs.js // via elementor/preview/enqueue_scripts; injecting render-facade.js would add // a second visualizer:render:chart:start trigger causing duplicate renders. foreach ( wp_scripts()->queue as $handle ) { if ( ( 0 === strpos( $handle, 'visualizer-render-' ) || 'visualizer-d3-renderer' === $handle ) && 'visualizer-render-google-lib' !== $handle && 'visualizer-render-chartjs-lib' !== $handle && 'visualizer-render-datatables-lib' !== $handle ) { wp_dequeue_script( $handle ); } } } echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ( ! $is_editor ) { return; } // Extract the element ID generated by the shortcode (visualizer-{id}-{rand}). if ( ! preg_match( '/\bid="(visualizer-' . $chart_id . '-\d+)"/', $html, $matches ) ) { return; } $element_id = $matches[1]; $chart = get_post( $chart_id ); if ( ! $chart || Visualizer_Plugin::CPT_VISUALIZER !== $chart->post_type ) { return; } $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true ); $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true ); $chart_settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true ); $chart_data = Visualizer_Module::get_chart_data( $chart, $type ); if ( empty( $chart_settings['height'] ) ) { $chart_settings['height'] = '400'; } // Read library from meta and normalise to the lowercase slugs that // render-google.js / render-chartjs.js / render-datatables.js and // elementor-widget-preview.js expect. $library = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true ); $library_map = array( 'GoogleCharts' => 'google', 'ChartJS' => 'chartjs', 'DataTable' => 'datatables', ); if ( isset( $library_map[ $library ] ) ) { $library = $library_map[ $library ]; } elseif ( ! $library ) { $library = 'google'; } $series = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SERIES, $series, $chart_id, $type ); $chart_settings = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, $chart_settings, $chart_id, $type ); $chart_settings = $this->apply_custom_css_class_names( $chart_settings, $chart_id ); $chart_entry = array( 'type' => $type, 'series' => $series, 'settings' => $chart_settings, 'data' => $chart_data, 'library' => $library, ); // D3/AI charts store their rendering code in post meta — include it so // elementor-widget-preview.js can pass it to the D3 renderer. if ( 'd3' === $library ) { $chart_entry['code'] = get_post_meta( $chart_id, Visualizer_Module_AIBuilder::CF_D3_CODE, true ); } // Elementor injects widget HTML via innerHTML, so ', esc_attr( $element_id ), wp_json_encode( $chart_entry ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ); } /** * Ensure custom CSS class mappings are present in settings for preview rendering. * * @param array