|
| 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 | +} |
0 commit comments