Skip to content

Commit 70427dd

Browse files
Merge branch 'development' into feat/cron
2 parents 5eaa6a1 + da64108 commit 70427dd

87 files changed

Lines changed: 2057 additions & 23943 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.distignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ AGENTS.md
3333
.wp-env.json
3434
.claude
3535
skills
36-
36+
classes/Visualizer/Gutenberg/src

.github/workflows/build-dev-artifacts.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
- name: Create zip
3131
run: |
3232
npm ci
33+
npm run gutenberg:build
3334
CURRENT_VERSION=$(node -p -e "require('./package.json').version")
3435
COMMIT_HASH=$(git rev-parse --short HEAD)
3536
DEV_VERSION="${CURRENT_VERSION}-dev.${COMMIT_HASH}"

.github/workflows/deploy-wporg.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ jobs:
1515
- name: Build
1616
run: |
1717
npm ci
18+
npm run gutenberg:build
1819
composer install --no-dev --prefer-dist --no-progress --no-suggest
1920
- name: WordPress Plugin Deploy
2021
uses: 10up/action-wordpress-plugin-deploy@master

.github/workflows/test-e2e.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- name: Install npm deps
2525
run: |
2626
npm ci
27+
npm run gutenberg:build
2728
npm install -g playwright-cli
2829
npx playwright install --with-deps chromium
2930
- name: Install composer deps

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ vendor
77
.DS_Store
88
artifacts
99
.phpunit.result.cache
10+
classes/Visualizer/Gutenberg/build

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

0 commit comments

Comments
 (0)