PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.5
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.5
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 3.10.4 All 148 releases
visualizer / classes / Visualizer / Elementor / Widget.php

Widget.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 4.0.5, at classes/Visualizer/Elementor/Widget.php

465 lines 16.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Copyright 2018 ThemeIsle (email : friends@themeisle.com) |
4 // +----------------------------------------------------------------------+
5 // | This program is free software; you can redistribute it and/or modify |
6 // | it under the terms of the GNU General Public License, version 2, as |
7 // | published by the Free Software Foundation. |
8 // | |
9 // | This program is distributed in the hope that it will be useful, |
10 // | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 // | GNU General Public License for more details. |
13 // | |
14 // | You should have received a copy of the GNU General Public License |
15 // | along with this program; if not, write to the Free Software |
16 // | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, |
17 // | MA 02110-1301 USA |
18 // +----------------------------------------------------------------------+
19 // | Author: Hardeep Asrani <hardeep@themeisle.com> |
20 // +----------------------------------------------------------------------+
21 /**
22 * Elementor widget for displaying Visualizer charts.
23 *
24 * @category Visualizer
25 * @package Elementor
26 *
27 * @since 3.11.16
28 */
29
30 if ( ! defined( 'ABSPATH' ) ) {
31 exit;
32 }
33
34 /**
35 * Visualizer Elementor Widget
36 */
37 class Visualizer_Elementor_Widget extends \Elementor\Widget_Base {
38
39 /**
40 * Register all Elementor-related hooks for the Visualizer widget.
41 *
42 * Called once from index.php on the plugins_loaded action (after Elementor
43 * itself is confirmed present). Each inner hook fires at its normal time in
44 * the WordPress lifecycle, so timing is identical to registering them
45 * directly in visualizer_launch().
46 *
47 * @return void
48 */
49 public static function register_hooks() {
50 // Register the widget with Elementor's widget manager.
51 add_action(
52 'elementor/widgets/register',
53 function ( $widgets_manager ) {
54 $widgets_manager->register( new self() );
55 }
56 );
57
58 // Register the Visualizer icon for the Elementor widget panel.
59 add_action(
60 'elementor/editor/after_enqueue_styles',
61 function () {
62 $icon_url = VISUALIZER_ABSURL . 'images/visualizer-icon.svg';
63 wp_add_inline_style(
64 'elementor-icons',
65 '.visualizer-elementor-icon { display:inline-block; width:1em; height:1em; background:url("' . esc_url( $icon_url ) . '") no-repeat center/contain; }'
66 );
67 }
68 );
69
70 // Enqueue Visualizer scripts inside the Elementor preview iframe.
71 // Elementor serves the preview iframe as a shell page and injects widget HTML via
72 // JavaScript (innerHTML), so wp_enqueue_script calls inside render() never reach the
73 // iframe. We load all chart render libraries here so they are available when
74 // elementor-widget-preview.js triggers visualizer:render:chart:start.
75 add_action(
76 'elementor/preview/enqueue_scripts',
77 function () {
78 do_action( 'visualizer_enqueue_scripts' );
79
80 // ChartJS render library.
81 if ( ! wp_script_is( 'numeral', 'registered' ) ) {
82 wp_register_script( 'numeral', VISUALIZER_ABSURL . 'js/lib/numeral.min.js', array(), Visualizer_Plugin::VERSION, true );
83 }
84 if ( ! wp_script_is( 'chartjs', 'registered' ) ) {
85 wp_register_script( 'chartjs', VISUALIZER_ABSURL . 'js/lib/chartjs.min.js', array( 'numeral' ), null, true );
86 }
87 wp_enqueue_script( 'visualizer-render-chartjs-lib', VISUALIZER_ABSURL . 'js/render-chartjs.js', array( 'chartjs', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true );
88
89 // Google Charts render library.
90 wp_enqueue_script( 'visualizer-google-jsapi', '//www.gstatic.com/charts/loader.js', array(), null, true );
91 wp_enqueue_script( 'visualizer-render-google-lib', VISUALIZER_ABSURL . 'js/render-google.js', array( 'visualizer-google-jsapi', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true );
92
93 // DataTable render library + styles.
94 if ( ! wp_script_is( 'visualizer-datatables', 'registered' ) ) {
95 wp_register_script( 'visualizer-datatables', VISUALIZER_ABSURL . 'js/lib/datatables.min.js', array( 'jquery' ), Visualizer_Plugin::VERSION, true );
96 }
97 wp_enqueue_script( 'visualizer-render-datatables-lib', VISUALIZER_ABSURL . 'js/render-datatables.js', array( 'visualizer-datatables', 'visualizer-customization' ), Visualizer_Plugin::VERSION, true );
98 wp_enqueue_style( 'visualizer-datatables', VISUALIZER_ABSURL . 'css/lib/datatables.min.css', array(), Visualizer_Plugin::VERSION );
99
100 // D3 render library (AI charts).
101 $d3_renderer_asset = VISUALIZER_ABSPATH . '/classes/Visualizer/D3Renderer/build/index.asset.php';
102 if ( file_exists( $d3_renderer_asset ) && ! wp_script_is( 'visualizer-d3-renderer', 'registered' ) ) {
103 /**
104 * Ignore missing build asset in source checkout.
105 *
106 * @phpstan-ignore-next-line
107 */
108 $d3_asset = include $d3_renderer_asset;
109 wp_register_script(
110 'visualizer-d3-renderer',
111 VISUALIZER_ABSURL . 'classes/Visualizer/D3Renderer/build/index.js',
112 array_merge( $d3_asset['dependencies'], array( 'jquery' ) ),
113 $d3_asset['version'],
114 true
115 );
116 }
117 if ( wp_script_is( 'visualizer-d3-renderer', 'registered' ) ) {
118 wp_enqueue_script( 'visualizer-d3-renderer' );
119 wp_localize_script(
120 'visualizer-d3-renderer',
121 'vizD3Renderer',
122 array(
123 'iframeJsUrl' => VISUALIZER_ABSURL . 'classes/Visualizer/D3Renderer/build/iframe.js',
124 )
125 );
126 }
127
128 // Elementor widget preview handler — uses frontend/element_ready hook.
129 wp_enqueue_script( 'visualizer-elementor-preview', VISUALIZER_ABSURL . 'js/elementor-widget-preview.js', array( 'jquery', 'elementor-frontend' ), Visualizer_Plugin::VERSION, true );
130
131 // Prevent Elementor's editor-preview CSS from hiding our widget.
132 // Elementor marks widgets without a content_template() as elementor-widget-empty
133 // and adds display:none to .elementor-widget-empty when the panel is hidden
134 // (.elementor-editor-preview on <body>). Our widget renders async (Google Charts
135 // loads via callback), so the empty class is always present.
136 wp_add_inline_style(
137 'visualizer-datatables',
138 '.elementor-editor-preview .elementor-widget-visualizer-chart.elementor-widget-empty { display: block !important; }'
139 );
140 }
141 );
142 }
143
144 /**
145 * Get widget name.
146 *
147 * @return string Widget name.
148 */
149 public function get_name() {
150 return 'visualizer-chart';
151 }
152
153 /**
154 * Get widget title.
155 *
156 * @return string Widget title.
157 */
158 public function get_title() {
159 return esc_html__( 'Visualizer Chart', 'visualizer' );
160 }
161
162 /**
163 * Get widget icon.
164 *
165 * @return string Widget icon CSS class.
166 */
167 public function get_icon() {
168 return 'visualizer-elementor-icon';
169 }
170
171 /**
172 * Get widget categories.
173 *
174 * @return array<string> Widget categories.
175 */
176 public function get_categories() {
177 return array( 'general' );
178 }
179
180 /**
181 * Get widget keywords.
182 *
183 * @return array<string> Widget keywords.
184 */
185 public function get_keywords() {
186 return array( 'visualizer', 'chart', 'graph', 'table', 'data' );
187 }
188
189 /**
190 * Build the select options from all published Visualizer charts.
191 *
192 * @return array<int|string, string> Associative array of chart ID => label.
193 */
194 private function get_chart_options() {
195 static $options_cache = null;
196 if ( null !== $options_cache ) {
197 return $options_cache;
198 }
199
200 $options = array(
201 '' => esc_html__( '— Select a chart —', 'visualizer' ),
202 );
203
204 $charts = get_posts(
205 array(
206 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
207 'posts_per_page' => -1,
208 'post_status' => 'publish',
209 'orderby' => 'title',
210 'order' => 'ASC',
211 'no_found_rows' => true,
212 )
213 );
214
215 foreach ( $charts as $chart ) {
216 $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS );
217 $title = '#' . $chart->ID;
218 if ( ! empty( $settings[0]['title'] ) ) {
219 $title = $settings[0]['title'];
220 }
221 // ChartJS stores title as an array.
222 if ( is_array( $title ) && isset( $title['text'] ) ) {
223 $title = $title['text'];
224 }
225 if ( ! empty( $settings[0]['backend-title'] ) ) {
226 $title = $settings[0]['backend-title'];
227 }
228 if ( empty( $title ) ) {
229 $title = '#' . $chart->ID;
230 }
231 $options[ $chart->ID ] = $title;
232 }
233
234 $options_cache = $options;
235 return $options_cache;
236 }
237
238 /**
239 * Register widget controls.
240 *
241 * @return void
242 */
243 protected function register_controls() {
244 $this->start_controls_section(
245 'section_chart',
246 array(
247 'label' => esc_html__( 'Chart', 'visualizer' ),
248 'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
249 )
250 );
251
252 $admin_url = admin_url( 'admin.php?page=' . Visualizer_Plugin::NAME );
253 $chart_options = $this->get_chart_options();
254 $has_charts = count( $chart_options ) > 1; // More than just the placeholder option.
255
256 if ( $has_charts ) {
257 $this->add_control(
258 'chart_id',
259 array(
260 'label' => esc_html__( 'Select Chart', 'visualizer' ),
261 'type' => \Elementor\Controls_Manager::SELECT,
262 'options' => $chart_options,
263 'default' => '',
264 )
265 );
266
267 $this->add_control(
268 'chart_notice',
269 array(
270 'type' => \Elementor\Controls_Manager::RAW_HTML,
271 'raw' => sprintf(
272 /* translators: 1: opening anchor tag, 2: closing anchor tag */
273 esc_html__( 'You can create and manage your charts from the %1$sVisualizer dashboard%2$s.', 'visualizer' ),
274 '<a href="' . esc_url( $admin_url ) . '" target="_blank">',
275 '</a>'
276 ),
277 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info',
278 )
279 );
280 } else {
281 $this->add_control(
282 'no_charts_notice',
283 array(
284 'type' => \Elementor\Controls_Manager::RAW_HTML,
285 'raw' => sprintf(
286 /* translators: 1: opening anchor tag, 2: closing anchor tag */
287 esc_html__( 'No charts found. %1$sCreate a chart%2$s in the Visualizer dashboard first.', 'visualizer' ),
288 '<a href="' . esc_url( $admin_url ) . '" target="_blank">',
289 '</a>'
290 ),
291 'content_classes' => 'elementor-panel-alert elementor-panel-alert-warning',
292 )
293 );
294 }
295
296 $this->end_controls_section();
297 }
298
299 /**
300 * Render the widget output on the frontend.
301 *
302 * @return void
303 */
304 protected function render() {
305 $settings = $this->get_settings_for_display();
306 $chart_id = ! empty( $settings['chart_id'] ) ? absint( $settings['chart_id'] ) : 0;
307
308 if ( ! $chart_id ) {
309 if ( \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
310 echo '<p style="text-align:center;padding:20px;color:#888;">' . esc_html__( 'Please select a chart from the widget settings.', 'visualizer' ) . '</p>';
311 }
312 return;
313 }
314
315 // Detect Elementor edit / preview context early — needed before do_shortcode().
316 $is_editor = \Elementor\Plugin::$instance->editor->is_edit_mode() ||
317 \Elementor\Plugin::$instance->preview->is_preview_mode();
318
319 // In the editor, force lazy-loading off so the chart renders immediately in the
320 // preview iframe without requiring a user-interaction event (scroll, hover, etc.).
321 // Also suppress action buttons (edit, export, etc.) — they are meaningless inside
322 // the Elementor preview and the edit link does nothing there.
323 if ( $is_editor ) {
324 add_filter( 'visualizer_lazy_load_chart', '__return_false' );
325 add_filter( 'visualizer_pro_add_actions', '__return_empty_array' );
326 }
327
328 // Ensure visualizer-customization is registered before the shortcode enqueues
329 // visualizer-render-{library} which depends on it. wp_enqueue_scripts never fires
330 // in admin or AJAX contexts (Elementor editor / AJAX re-render), so we trigger the
331 // action manually. It is a no-op when already registered.
332 do_action( 'visualizer_enqueue_scripts' );
333
334 // Capture the shortcode output so we can parse the generated element ID.
335 $html = do_shortcode( '[visualizer id="' . $chart_id . '"]' );
336
337 if ( $is_editor ) {
338 remove_filter( 'visualizer_lazy_load_chart', '__return_false' );
339 remove_filter( 'visualizer_pro_add_actions', '__return_empty_array' );
340
341 // The shortcode enqueues visualizer-render-{library} (render-facade.js).
342 // Dequeue it so Elementor's AJAX response doesn't inject it into the preview
343 // iframe. The preview page already loads render-google.js / render-chartjs.js
344 // via elementor/preview/enqueue_scripts; injecting render-facade.js would add
345 // a second visualizer:render:chart:start trigger causing duplicate renders.
346 foreach ( wp_scripts()->queue as $handle ) {
347 if ( ( 0 === strpos( $handle, 'visualizer-render-' ) || 'visualizer-d3-renderer' === $handle )
348 && 'visualizer-render-google-lib' !== $handle
349 && 'visualizer-render-chartjs-lib' !== $handle
350 && 'visualizer-render-datatables-lib' !== $handle ) {
351 wp_dequeue_script( $handle );
352 }
353 }
354 }
355
356 echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
357
358 if ( ! $is_editor ) {
359 return;
360 }
361
362 // Extract the element ID generated by the shortcode (visualizer-{id}-{rand}).
363 if ( ! preg_match( '/\bid="(visualizer-' . $chart_id . '-\d+)"/', $html, $matches ) ) {
364 return;
365 }
366 $element_id = $matches[1];
367
368 $chart = get_post( $chart_id );
369 if ( ! $chart || Visualizer_Plugin::CPT_VISUALIZER !== $chart->post_type ) {
370 return;
371 }
372
373 $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true );
374 $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
375 $chart_settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
376 $chart_data = Visualizer_Module::get_chart_data( $chart, $type );
377
378 if ( empty( $chart_settings['height'] ) ) {
379 $chart_settings['height'] = '400';
380 }
381
382 // Read library from meta and normalise to the lowercase slugs that
383 // render-google.js / render-chartjs.js / render-datatables.js and
384 // elementor-widget-preview.js expect.
385 $library = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
386 $library_map = array(
387 'GoogleCharts' => 'google',
388 'ChartJS' => 'chartjs',
389 'DataTable' => 'datatables',
390 );
391 if ( isset( $library_map[ $library ] ) ) {
392 $library = $library_map[ $library ];
393 } elseif ( ! $library ) {
394 $library = 'google';
395 }
396
397 $series = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SERIES, $series, $chart_id, $type );
398 $chart_settings = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, $chart_settings, $chart_id, $type );
399 $chart_settings = $this->apply_custom_css_class_names( $chart_settings, $chart_id );
400
401 $chart_entry = array(
402 'type' => $type,
403 'series' => $series,
404 'settings' => $chart_settings,
405 'data' => $chart_data,
406 'library' => $library,
407 );
408
409 // D3/AI charts store their rendering code in post meta — include it so
410 // elementor-widget-preview.js can pass it to the D3 renderer.
411 if ( 'd3' === $library ) {
412 $chart_entry['code'] = get_post_meta( $chart_id, Visualizer_Module_AIBuilder::CF_D3_CODE, true );
413 }
414
415 // Elementor injects widget HTML via innerHTML, so <script type="text/javascript">
416 // tags never execute in the preview iframe. Instead embed the chart data in a
417 // JSON script element — it is preserved through innerHTML but not executed.
418 // elementor-widget-preview.js reads it via the frontend/element_ready hook.
419 printf(
420 '<script type="application/json" class="visualizer-chart-data" data-element-id="%s">%s</script>',
421 esc_attr( $element_id ),
422 wp_json_encode( $chart_entry ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
423 );
424 }
425
426 /**
427 * Ensure custom CSS class mappings are present in settings for preview rendering.
428 *
429 * @param array<string, mixed> $settings Chart settings.
430 * @param int $chart_id Chart ID.
431 * @return array<string, mixed>
432 */
433 private function apply_custom_css_class_names( $settings, $chart_id ) {
434 if ( empty( $settings['customcss'] ) || ! is_array( $settings['customcss'] ) ) {
435 return $settings;
436 }
437
438 $classes = array();
439 $id = 'visualizer-' . $chart_id;
440
441 foreach ( $settings['customcss'] as $name => $element ) {
442 if ( empty( $name ) || ! is_array( $element ) ) {
443 continue;
444 }
445 $has_properties = false;
446 foreach ( $element as $property => $value ) {
447 if ( '' !== $property && '' !== $value && null !== $value ) {
448 $has_properties = true;
449 break;
450 }
451 }
452 if ( ! $has_properties ) {
453 continue;
454 }
455 $classes[ $name ] = $id . $name;
456 }
457
458 if ( ! empty( $classes ) ) {
459 $settings['cssClassNames'] = $classes;
460 }
461
462 return $settings;
463 }
464 }
465