Skip to content

Commit 7cc40d5

Browse files
committed
feat: add Elementor widget for displaying Visualizer charts
- Register Visualizer Chart widget via `elementor/widgets/register` - Widget renders chart via shortcode in editor/preview context with lazy loading disabled and action buttons suppressed - Add 6 Playwright e2e tests covering widget discovery, no-charts notice, chart selection, rendering, hide-panel regression, and mid-session add
1 parent 94f9316 commit 7cc40d5

7 files changed

Lines changed: 901 additions & 0 deletions

File tree

bin/cli-setup.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ wp --allow-root core install --url="http://localhost:8889" --admin_user="admin"
44
mkdir -p /var/www/html/wp-content/uploads
55
chmod -R 777 /var/www/html/wp-content/uploads/*
66
wp --allow-root plugin install classic-editor
7+
wp --allow-root plugin install elementor
78
wp --allow-root theme install twentytwentyone
89

910
# activate
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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+
* Get widget name.
41+
*
42+
* @return string Widget name.
43+
*/
44+
public function get_name() {
45+
return 'visualizer-chart';
46+
}
47+
48+
/**
49+
* Get widget title.
50+
*
51+
* @return string Widget title.
52+
*/
53+
public function get_title() {
54+
return esc_html__( 'Visualizer Chart', 'visualizer' );
55+
}
56+
57+
/**
58+
* Get widget icon.
59+
*
60+
* @return string Widget icon CSS class.
61+
*/
62+
public function get_icon() {
63+
return 'visualizer-elementor-icon';
64+
}
65+
66+
/**
67+
* Get widget categories.
68+
*
69+
* @return array Widget categories.
70+
*/
71+
public function get_categories() {
72+
return array( 'general' );
73+
}
74+
75+
/**
76+
* Get widget keywords.
77+
*
78+
* @return array Widget keywords.
79+
*/
80+
public function get_keywords() {
81+
return array( 'visualizer', 'chart', 'graph', 'table', 'data' );
82+
}
83+
84+
/**
85+
* Build the select options from all published Visualizer charts.
86+
*
87+
* @return array Associative array of chart ID => label.
88+
*/
89+
private function get_chart_options() {
90+
static $options_cache = null;
91+
if ( null !== $options_cache ) {
92+
return $options_cache;
93+
}
94+
95+
$options = array(
96+
'' => esc_html__( '— Select a chart —', 'visualizer' ),
97+
);
98+
99+
$charts = get_posts(
100+
array(
101+
'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
102+
'posts_per_page' => -1,
103+
'post_status' => 'publish',
104+
'orderby' => 'title',
105+
'order' => 'ASC',
106+
'no_found_rows' => true,
107+
)
108+
);
109+
110+
foreach ( $charts as $chart ) {
111+
$settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS );
112+
$title = '#' . $chart->ID;
113+
if ( ! empty( $settings[0]['title'] ) ) {
114+
$title = $settings[0]['title'];
115+
}
116+
// ChartJS stores title as an array.
117+
if ( is_array( $title ) && isset( $title['text'] ) ) {
118+
$title = $title['text'];
119+
}
120+
if ( ! empty( $settings[0]['backend-title'] ) ) {
121+
$title = $settings[0]['backend-title'];
122+
}
123+
if ( empty( $title ) ) {
124+
$title = '#' . $chart->ID;
125+
}
126+
$options[ $chart->ID ] = $title;
127+
}
128+
129+
$options_cache = $options;
130+
return $options_cache;
131+
}
132+
133+
/**
134+
* Register widget controls.
135+
*/
136+
protected function register_controls() {
137+
$this->start_controls_section(
138+
'section_chart',
139+
array(
140+
'label' => esc_html__( 'Chart', 'visualizer' ),
141+
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
142+
)
143+
);
144+
145+
$admin_url = admin_url( 'admin.php?page=' . Visualizer_Plugin::NAME );
146+
$chart_options = $this->get_chart_options();
147+
$has_charts = count( $chart_options ) > 1; // More than just the placeholder option.
148+
149+
if ( $has_charts ) {
150+
$this->add_control(
151+
'chart_id',
152+
array(
153+
'label' => esc_html__( 'Select Chart', 'visualizer' ),
154+
'type' => \Elementor\Controls_Manager::SELECT,
155+
'options' => $chart_options,
156+
'default' => '',
157+
)
158+
);
159+
160+
$this->add_control(
161+
'chart_notice',
162+
array(
163+
'type' => \Elementor\Controls_Manager::RAW_HTML,
164+
'raw' => sprintf(
165+
/* translators: 1: opening anchor tag, 2: closing anchor tag */
166+
esc_html__( 'You can create and manage your charts from the %1$sVisualizer dashboard%2$s.', 'visualizer' ),
167+
'<a href="' . esc_url( $admin_url ) . '" target="_blank">',
168+
'</a>'
169+
),
170+
'content_classes' => 'elementor-panel-alert elementor-panel-alert-info',
171+
)
172+
);
173+
} else {
174+
$this->add_control(
175+
'no_charts_notice',
176+
array(
177+
'type' => \Elementor\Controls_Manager::RAW_HTML,
178+
'raw' => sprintf(
179+
/* translators: 1: opening anchor tag, 2: closing anchor tag */
180+
esc_html__( 'No charts found. %1$sCreate a chart%2$s in the Visualizer dashboard first.', 'visualizer' ),
181+
'<a href="' . esc_url( $admin_url ) . '" target="_blank">',
182+
'</a>'
183+
),
184+
'content_classes' => 'elementor-panel-alert elementor-panel-alert-warning',
185+
)
186+
);
187+
}
188+
189+
$this->end_controls_section();
190+
}
191+
192+
/**
193+
* Render the widget output on the frontend.
194+
*/
195+
protected function render() {
196+
$settings = $this->get_settings_for_display();
197+
$chart_id = ! empty( $settings['chart_id'] ) ? absint( $settings['chart_id'] ) : 0;
198+
199+
if ( ! $chart_id ) {
200+
if ( \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
201+
echo '<p style="text-align:center;padding:20px;color:#888;">' . esc_html__( 'Please select a chart from the widget settings.', 'visualizer' ) . '</p>';
202+
}
203+
return;
204+
}
205+
206+
// Detect Elementor edit / preview context early — needed before do_shortcode().
207+
$is_editor = \Elementor\Plugin::$instance->editor->is_edit_mode() ||
208+
\Elementor\Plugin::$instance->preview->is_preview_mode();
209+
210+
// In the editor, force lazy-loading off so the chart renders immediately in the
211+
// preview iframe without requiring a user-interaction event (scroll, hover, etc.).
212+
// Also suppress action buttons (edit, export, etc.) — they are meaningless inside
213+
// the Elementor preview and the edit link does nothing there.
214+
if ( $is_editor ) {
215+
add_filter( 'visualizer_lazy_load_chart', '__return_false' );
216+
add_filter( 'visualizer_pro_add_actions', '__return_empty_array' );
217+
}
218+
219+
// Ensure visualizer-customization is registered before the shortcode enqueues
220+
// visualizer-render-{library} which depends on it. wp_enqueue_scripts never fires
221+
// in admin or AJAX contexts (Elementor editor / AJAX re-render), so we trigger the
222+
// action manually. It is a no-op when already registered.
223+
do_action( 'visualizer_enqueue_scripts' );
224+
225+
// Capture the shortcode output so we can parse the generated element ID.
226+
$html = do_shortcode( '[visualizer id="' . $chart_id . '"]' );
227+
228+
if ( $is_editor ) {
229+
remove_filter( 'visualizer_lazy_load_chart', '__return_false' );
230+
remove_filter( 'visualizer_pro_add_actions', '__return_empty_array' );
231+
232+
// The shortcode enqueues visualizer-render-{library} (render-facade.js).
233+
// Dequeue it so Elementor's AJAX response doesn't inject it into the preview
234+
// iframe. The preview page already loads render-google.js / render-chartjs.js
235+
// via elementor/preview/enqueue_scripts; injecting render-facade.js would add
236+
// a second visualizer:render:chart:start trigger causing duplicate renders.
237+
foreach ( wp_scripts()->queue as $handle ) {
238+
if ( 0 === strpos( $handle, 'visualizer-render-' )
239+
&& 'visualizer-render-google-lib' !== $handle
240+
&& 'visualizer-render-chartjs-lib' !== $handle
241+
&& 'visualizer-render-datatables-lib' !== $handle ) {
242+
wp_dequeue_script( $handle );
243+
}
244+
}
245+
}
246+
247+
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
248+
249+
if ( ! $is_editor ) {
250+
return;
251+
}
252+
253+
// Extract the element ID generated by the shortcode (visualizer-{id}-{rand}).
254+
if ( ! preg_match( '/\bid="(visualizer-' . $chart_id . '-\d+)"/', $html, $matches ) ) {
255+
return;
256+
}
257+
$element_id = $matches[1];
258+
259+
$chart = get_post( $chart_id );
260+
if ( ! $chart || Visualizer_Plugin::CPT_VISUALIZER !== $chart->post_type ) {
261+
return;
262+
}
263+
264+
$type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true );
265+
$series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
266+
$chart_settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
267+
$chart_data = Visualizer_Module::get_chart_data( $chart, $type );
268+
269+
if ( empty( $chart_settings['height'] ) ) {
270+
$chart_settings['height'] = '400';
271+
}
272+
273+
// Read library from meta and normalise to the lowercase slugs that
274+
// render-google.js / render-chartjs.js / render-datatables.js and
275+
// elementor-widget-preview.js expect.
276+
$library = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
277+
$library_map = array(
278+
'GoogleCharts' => 'google',
279+
'ChartJS' => 'chartjs',
280+
'DataTable' => 'datatables',
281+
);
282+
if ( isset( $library_map[ $library ] ) ) {
283+
$library = $library_map[ $library ];
284+
} elseif ( ! $library ) {
285+
$library = 'google';
286+
}
287+
288+
$series = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SERIES, $series, $chart_id, $type );
289+
$chart_settings = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, $chart_settings, $chart_id, $type );
290+
$chart_settings = $this->apply_custom_css_class_names( $chart_settings, $chart_id );
291+
292+
$chart_entry = array(
293+
'type' => $type,
294+
'series' => $series,
295+
'settings' => $chart_settings,
296+
'data' => $chart_data,
297+
'library' => $library,
298+
);
299+
300+
// Elementor injects widget HTML via innerHTML, so <script type="text/javascript">
301+
// tags never execute in the preview iframe. Instead embed the chart data in a
302+
// JSON script element — it is preserved through innerHTML but not executed.
303+
// elementor-widget-preview.js reads it via the frontend/element_ready hook.
304+
printf(
305+
'<script type="application/json" class="visualizer-chart-data" data-element-id="%s">%s</script>',
306+
esc_attr( $element_id ),
307+
wp_json_encode( $chart_entry ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
308+
);
309+
}
310+
311+
/**
312+
* Ensure custom CSS class mappings are present in settings for preview rendering.
313+
*
314+
* @param array $settings Chart settings.
315+
* @param int $chart_id Chart ID.
316+
* @return array
317+
*/
318+
private function apply_custom_css_class_names( $settings, $chart_id ) {
319+
if ( empty( $settings['customcss'] ) || ! is_array( $settings['customcss'] ) ) {
320+
return $settings;
321+
}
322+
323+
$classes = array();
324+
$id = 'visualizer-' . $chart_id;
325+
326+
foreach ( $settings['customcss'] as $name => $element ) {
327+
if ( empty( $name ) || ! is_array( $element ) ) {
328+
continue;
329+
}
330+
$has_properties = false;
331+
foreach ( $element as $property => $value ) {
332+
if ( '' !== $property && '' !== $value && null !== $value ) {
333+
$has_properties = true;
334+
break;
335+
}
336+
}
337+
if ( ! $has_properties ) {
338+
continue;
339+
}
340+
$classes[ $name ] = $id . $name;
341+
}
342+
343+
if ( ! empty( $classes ) ) {
344+
$settings['cssClassNames'] = $classes;
345+
}
346+
347+
return $settings;
348+
}
349+
}

images/visualizer-icon.svg

Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)