PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.65
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.65
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / extensions / Data_Table_Builder / Data_Table_Builder.php

Data_Table_Builder.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.65, at includes/extensions/Data_Table_Builder/Data_Table_Builder.php

885 lines 32.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Data Table Builder Extension.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 if (!defined('ABSPATH')) {
11 exit;
12 }
13
14 final class Data_Table_Builder
15 {
16 private const OPTION_NAME = 'king_addons_table_builder_options';
17
18 public const POST_TYPE = 'kng_table';
19 public const META_DATA = '_kng_table_data';
20 public const META_CONFIG = '_kng_table_config';
21 public const META_STYLE = '_kng_table_style';
22 public const META_FILTERS = '_kng_table_filters';
23 public const META_RESPONSIVE = '_kng_table_responsive';
24 public const META_VERSION = '_kng_table_schema_version';
25
26 public const SCHEMA_VERSION = 1;
27
28 private static ?Data_Table_Builder $instance = null;
29
30 /**
31 * Cached settings.
32 *
33 * @var array<string, mixed>
34 */
35 private array $options = [];
36
37 public static function instance(): Data_Table_Builder
38 {
39 if (is_null(self::$instance)) {
40 self::$instance = new self();
41 }
42 return self::$instance;
43 }
44
45 private function __construct()
46 {
47 $this->options = $this->get_options();
48
49 add_action('init', [$this, 'register_post_type']);
50 add_action('init', [$this, 'maybe_migrate_tables']);
51 add_action('admin_menu', [$this, 'add_admin_menu']);
52 add_action('admin_init', [$this, 'register_settings']);
53 add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
54
55 add_action('admin_post_kng_table_save', [$this, 'handle_save_table']);
56 add_action('admin_post_kng_table_delete', [$this, 'handle_delete_table']);
57 add_action('admin_post_kng_table_duplicate', [$this, 'handle_duplicate_table']);
58 add_action('admin_post_kng_table_export', [$this, 'handle_export_table']);
59 add_action('admin_post_kng_table_import', [$this, 'handle_import_table']);
60
61 add_shortcode('kng_table', [$this, 'render_shortcode']);
62 add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
63 }
64
65 public function get_default_options(): array
66 {
67 return [
68 'enabled' => true,
69 'default_preset' => 'minimal',
70 'default_theme' => 'dark',
71 'default_rows_per_page' => 10,
72 ];
73 }
74
75 public function get_options(): array
76 {
77 $saved = get_option(self::OPTION_NAME, []);
78 return wp_parse_args($saved, $this->get_default_options());
79 }
80
81 public function register_post_type(): void
82 {
83 $labels = [
84 'name' => __('Tables', 'king-addons'),
85 'singular_name' => __('Table', 'king-addons'),
86 'add_new' => __('Add New', 'king-addons'),
87 'add_new_item' => __('Add New Table', 'king-addons'),
88 'edit_item' => __('Edit Table', 'king-addons'),
89 'new_item' => __('New Table', 'king-addons'),
90 'view_item' => __('View Table', 'king-addons'),
91 'search_items' => __('Search Tables', 'king-addons'),
92 'not_found' => __('No tables found', 'king-addons'),
93 'not_found_in_trash' => __('No tables found in Trash', 'king-addons'),
94 ];
95
96 register_post_type(self::POST_TYPE, [
97 'labels' => $labels,
98 'public' => false,
99 'show_ui' => false,
100 'show_in_menu' => false,
101 'capability_type' => 'post',
102 'supports' => ['title'],
103 'has_archive' => false,
104 'rewrite' => false,
105 ]);
106 }
107
108 public function maybe_migrate_tables(): void
109 {
110 $tables = get_posts([
111 'post_type' => self::POST_TYPE,
112 'post_status' => 'publish',
113 'posts_per_page' => 50,
114 'fields' => 'ids',
115 'meta_query' => [
116 'relation' => 'OR',
117 [
118 'key' => self::META_VERSION,
119 'compare' => 'NOT EXISTS',
120 ],
121 [
122 'key' => self::META_VERSION,
123 'value' => self::SCHEMA_VERSION,
124 'compare' => '<',
125 'type' => 'NUMERIC',
126 ],
127 ],
128 ]);
129
130 foreach ($tables as $table_id) {
131 update_post_meta($table_id, self::META_VERSION, self::SCHEMA_VERSION);
132 $data = get_post_meta($table_id, self::META_DATA, true);
133 if (is_array($data) && empty($data['schema_version'])) {
134 $data['schema_version'] = self::SCHEMA_VERSION;
135 update_post_meta($table_id, self::META_DATA, $data);
136 }
137 }
138 }
139
140 public function add_admin_menu(): void
141 {
142 add_submenu_page(
143 'king-addons',
144 __('Table Builder', 'king-addons'),
145 __('Table Builder', 'king-addons'),
146 'manage_options',
147 'king-addons-table-builder',
148 [$this, 'render_admin_page']
149 );
150 }
151
152 public function register_settings(): void
153 {
154 register_setting(
155 'king_addons_table_builder',
156 self::OPTION_NAME,
157 [
158 'type' => 'array',
159 'sanitize_callback' => [$this, 'sanitize_settings'],
160 'default' => $this->get_default_options(),
161 ]
162 );
163 }
164
165 public function enqueue_admin_assets(string $hook): void
166 {
167 if ($hook !== 'king-addons_page_king-addons-table-builder') {
168 return;
169 }
170
171 $shared_css = KING_ADDONS_URL . 'includes/admin/layouts/shared/admin-v3-styles.css';
172 $shared_path = KING_ADDONS_PATH . 'includes/admin/layouts/shared/admin-v3-styles.css';
173 $shared_version = file_exists($shared_path) ? filemtime($shared_path) : KING_ADDONS_VERSION;
174 wp_enqueue_style('king-addons-admin-v3', $shared_css, [], $shared_version);
175
176 $admin_css = KING_ADDONS_URL . 'includes/extensions/Data_Table_Builder/assets/admin.css';
177 $admin_path = KING_ADDONS_PATH . 'includes/extensions/Data_Table_Builder/assets/admin.css';
178 $admin_version = file_exists($admin_path) ? filemtime($admin_path) : KING_ADDONS_VERSION;
179 wp_enqueue_style('king-addons-table-builder-admin', $admin_css, ['king-addons-admin-v3'], $admin_version);
180
181 $admin_js = KING_ADDONS_URL . 'includes/extensions/Data_Table_Builder/assets/admin.js';
182 $admin_js_path = KING_ADDONS_PATH . 'includes/extensions/Data_Table_Builder/assets/admin.js';
183 $admin_js_version = file_exists($admin_js_path) ? filemtime($admin_js_path) : KING_ADDONS_VERSION;
184 wp_enqueue_script('king-addons-table-builder-admin', $admin_js, ['jquery'], $admin_js_version, true);
185
186 wp_localize_script('king-addons-table-builder-admin', 'KNGTableBuilder', [
187 'ajaxUrl' => admin_url('admin-ajax.php'),
188 'themeNonce' => wp_create_nonce('king_addons_dashboard_ui'),
189 'maxRows' => 200,
190 'maxCols' => 20,
191 'isPro' => function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only(),
192 ]);
193 }
194
195 public function enqueue_frontend_assets(): void
196 {
197 // Assets are loaded on demand.
198 }
199
200 private function enqueue_frontend_assets_if_needed(): void
201 {
202 static $enqueued = false;
203
204 if ($enqueued) {
205 return;
206 }
207
208 wp_enqueue_style(
209 'king-addons-table-builder-frontend',
210 KING_ADDONS_URL . 'includes/extensions/Data_Table_Builder/assets/frontend.css',
211 [],
212 KING_ADDONS_VERSION
213 );
214
215 wp_enqueue_script(
216 'king-addons-table-builder-frontend',
217 KING_ADDONS_URL . 'includes/extensions/Data_Table_Builder/assets/frontend.js',
218 ['jquery'],
219 KING_ADDONS_VERSION,
220 true
221 );
222
223 $enqueued = true;
224 }
225
226 public function render_admin_page(): void
227 {
228 if (!current_user_can('manage_options')) {
229 return;
230 }
231
232 $view = isset($_GET['view']) ? sanitize_key($_GET['view']) : 'dashboard';
233 $is_pro = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
234 $settings = $this->get_options();
235
236 include __DIR__ . '/templates/admin-page.php';
237 }
238
239 public function handle_save_table(): void
240 {
241 if (!current_user_can('manage_options')) {
242 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
243 }
244
245 check_admin_referer('kng_table_save');
246
247 $table_id = isset($_POST['table_id']) ? absint($_POST['table_id']) : 0;
248 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : '';
249
250 $post_args = [
251 'post_title' => $title !== '' ? $title : __('Untitled Table', 'king-addons'),
252 'post_type' => self::POST_TYPE,
253 'post_status' => 'publish',
254 ];
255
256 if ($table_id > 0) {
257 $post_args['ID'] = $table_id;
258 }
259
260 $result = wp_insert_post($post_args, true);
261 if (is_wp_error($result)) {
262 $this->redirect_with_message('add', 'error_save', $table_id);
263 }
264
265 $table_id = (int) $result;
266
267 $data = $this->sanitize_table_data(isset($_POST['kng_table_data']) ? wp_unslash($_POST['kng_table_data']) : '');
268 $config = $this->sanitize_table_config(isset($_POST['kng_table_config']) ? wp_unslash($_POST['kng_table_config']) : '');
269 $style = $this->sanitize_table_style(isset($_POST['kng_table_style']) ? wp_unslash($_POST['kng_table_style']) : '');
270 $filters = $this->sanitize_table_filters(isset($_POST['kng_table_filters']) ? wp_unslash($_POST['kng_table_filters']) : '');
271 $responsive = $this->sanitize_table_responsive(isset($_POST['kng_table_responsive']) ? wp_unslash($_POST['kng_table_responsive']) : '');
272
273 update_post_meta($table_id, self::META_DATA, $data);
274 update_post_meta($table_id, self::META_CONFIG, $config);
275 update_post_meta($table_id, self::META_STYLE, $style);
276 update_post_meta($table_id, self::META_FILTERS, $filters);
277 update_post_meta($table_id, self::META_RESPONSIVE, $responsive);
278 update_post_meta($table_id, self::META_VERSION, self::SCHEMA_VERSION);
279
280 $this->redirect_with_message('add', 'saved', $table_id);
281 }
282
283 public function handle_delete_table(): void
284 {
285 if (!current_user_can('manage_options')) {
286 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
287 }
288
289 $table_id = isset($_GET['table_id']) ? absint($_GET['table_id']) : 0;
290 check_admin_referer('kng_table_delete_' . $table_id);
291
292 if ($table_id > 0) {
293 wp_delete_post($table_id, true);
294 }
295
296 $this->redirect_with_message('tables', 'deleted');
297 }
298
299 public function handle_duplicate_table(): void
300 {
301 if (!current_user_can('manage_options')) {
302 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
303 }
304
305 $table_id = isset($_GET['table_id']) ? absint($_GET['table_id']) : 0;
306 check_admin_referer('kng_table_duplicate_' . $table_id);
307
308 $table = get_post($table_id);
309 if (!$table || $table->post_type !== self::POST_TYPE) {
310 $this->redirect_with_message('tables', 'error_not_found');
311 }
312
313 $new_id = wp_insert_post([
314 'post_title' => $table->post_title . ' (Copy)',
315 'post_type' => self::POST_TYPE,
316 'post_status' => 'publish',
317 ]);
318
319 if (is_wp_error($new_id)) {
320 $this->redirect_with_message('tables', 'error_save');
321 }
322
323 update_post_meta($new_id, self::META_DATA, get_post_meta($table_id, self::META_DATA, true));
324 update_post_meta($new_id, self::META_CONFIG, get_post_meta($table_id, self::META_CONFIG, true));
325 update_post_meta($new_id, self::META_STYLE, get_post_meta($table_id, self::META_STYLE, true));
326 update_post_meta($new_id, self::META_FILTERS, get_post_meta($table_id, self::META_FILTERS, true));
327 update_post_meta($new_id, self::META_RESPONSIVE, get_post_meta($table_id, self::META_RESPONSIVE, true));
328 update_post_meta($new_id, self::META_VERSION, self::SCHEMA_VERSION);
329
330 $this->redirect_with_message('tables', 'duplicated');
331 }
332
333 public function handle_export_table(): void
334 {
335 if (!current_user_can('manage_options')) {
336 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
337 }
338
339 $table_id = isset($_GET['table_id']) ? absint($_GET['table_id']) : 0;
340 check_admin_referer('kng_table_export_' . $table_id);
341
342 $table = get_post($table_id);
343 if (!$table || $table->post_type !== self::POST_TYPE) {
344 $this->redirect_with_message('tables', 'error_not_found');
345 }
346
347 $data = get_post_meta($table_id, self::META_DATA, true);
348 if (!is_array($data)) {
349 $data = [];
350 }
351
352 $rows = $data['rows'] ?? [];
353 $columns = $data['columns'] ?? [];
354
355 header('Content-Type: text/csv; charset=utf-8');
356 header('Content-Disposition: attachment; filename=table-' . $table_id . '-' . gmdate('Y-m-d') . '.csv');
357
358 $output = fopen('php://output', 'w');
359
360 $header = [];
361 foreach ($columns as $column) {
362 $header[] = $this->escape_csv_value($column['label'] ?? '');
363 }
364 if (!empty($header)) {
365 fputcsv($output, $header);
366 }
367
368 foreach ($rows as $row) {
369 $line = [];
370 foreach ($row as $cell) {
371 $value = is_array($cell) ? (string) ($cell['value'] ?? '') : (string) $cell;
372 $line[] = $this->escape_csv_value($value);
373 }
374 fputcsv($output, $line);
375 }
376
377 fclose($output);
378 exit;
379 }
380
381 public function handle_import_table(): void
382 {
383 if (!current_user_can('manage_options')) {
384 wp_die(esc_html__('Unauthorized request.', 'king-addons'));
385 }
386
387 check_admin_referer('kng_table_import');
388
389 if (empty($_FILES['import_file']) || !isset($_FILES['import_file']['tmp_name'])) {
390 $this->redirect_with_message('import-export', 'error_import');
391 }
392
393 $file = $_FILES['import_file'];
394 if (!empty($file['error'])) {
395 $this->redirect_with_message('import-export', 'error_import');
396 }
397
398 $handle = fopen($file['tmp_name'], 'r');
399 if (!$handle) {
400 $this->redirect_with_message('import-export', 'error_import');
401 }
402
403 $rows = [];
404 while (($row = fgetcsv($handle)) !== false) {
405 $rows[] = $row;
406 }
407 fclose($handle);
408
409 if (empty($rows)) {
410 $this->redirect_with_message('import-export', 'error_import');
411 }
412
413 $header = array_shift($rows);
414 $columns = [];
415 $is_pro = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
416 $max_cols = $is_pro ? count($header) : min(20, count($header));
417 foreach (array_slice($header, 0, $max_cols) as $label) {
418 $columns[] = [
419 'label' => sanitize_text_field((string) $label),
420 'type' => 'text',
421 'sortable' => true,
422 'hide_mobile' => false,
423 'align' => 'left',
424 ];
425 }
426
427 $data_rows = [];
428 $rows = $is_pro ? $rows : array_slice($rows, 0, 200);
429 foreach ($rows as $row) {
430 $cells = [];
431 foreach (array_slice($row, 0, $max_cols) as $cell) {
432 $cells[] = [
433 'value' => sanitize_text_field((string) $cell),
434 'tooltip' => '',
435 'rowspan' => 1,
436 'colspan' => 1,
437 ];
438 }
439 $data_rows[] = $cells;
440 }
441
442 $data = [
443 'columns' => $columns,
444 'rows' => $data_rows,
445 'schema_version' => self::SCHEMA_VERSION,
446 ];
447
448 $table_id = wp_insert_post([
449 'post_title' => __('Imported Table', 'king-addons'),
450 'post_type' => self::POST_TYPE,
451 'post_status' => 'publish',
452 ]);
453
454 if (is_wp_error($table_id)) {
455 $this->redirect_with_message('import-export', 'error_import');
456 }
457
458 update_post_meta($table_id, self::META_DATA, $data);
459 update_post_meta($table_id, self::META_CONFIG, $this->get_default_table_config());
460 update_post_meta($table_id, self::META_STYLE, $this->get_default_table_style());
461 update_post_meta($table_id, self::META_FILTERS, []);
462 update_post_meta($table_id, self::META_RESPONSIVE, $this->get_default_responsive());
463 update_post_meta($table_id, self::META_VERSION, self::SCHEMA_VERSION);
464
465 $this->redirect_with_message('import-export', 'imported');
466 }
467
468 public function render_shortcode($atts): string
469 {
470 $atts = shortcode_atts([
471 'id' => '',
472 'slug' => '',
473 'theme' => '',
474 'search' => '',
475 'pagination' => '',
476 'sort' => '',
477 'class' => '',
478 ], $atts, 'kng_table');
479
480 $table = null;
481 if (!empty($atts['id'])) {
482 $table = get_post(absint($atts['id']));
483 } elseif (!empty($atts['slug'])) {
484 $query = new \WP_Query([
485 'post_type' => self::POST_TYPE,
486 'post_status' => 'publish',
487 'name' => sanitize_title_with_dashes($atts['slug']),
488 'posts_per_page' => 1,
489 ]);
490 if (!empty($query->posts)) {
491 $table = $query->posts[0];
492 }
493 }
494
495 if (!$table || $table->post_type !== self::POST_TYPE) {
496 return '';
497 }
498
499 $data = get_post_meta($table->ID, self::META_DATA, true);
500 $config = get_post_meta($table->ID, self::META_CONFIG, true);
501 $style = get_post_meta($table->ID, self::META_STYLE, true);
502 $responsive = get_post_meta($table->ID, self::META_RESPONSIVE, true);
503
504 if (!is_array($data)) {
505 $data = [];
506 }
507 if (!is_array($config)) {
508 $config = $this->get_default_table_config();
509 }
510 if (!is_array($style)) {
511 $style = $this->get_default_table_style();
512 }
513 if (!is_array($responsive)) {
514 $responsive = $this->get_default_responsive();
515 }
516
517 $config = $this->apply_shortcode_overrides($config, $atts);
518 if (!empty($atts['theme'])) {
519 $style['theme'] = sanitize_key($atts['theme']);
520 }
521
522 $this->enqueue_frontend_assets_if_needed();
523
524 return $this->render_table_markup($table, $data, $config, $style, $responsive, $atts['class']);
525 }
526
527 private function render_table_markup($table, array $data, array $config, array $style, array $responsive, string $extra_class): string
528 {
529 $columns = $data['columns'] ?? [];
530 $rows = $data['rows'] ?? [];
531
532 if (empty($columns)) {
533 $columns = [];
534 $max_cols = 0;
535 foreach ($rows as $row) {
536 $max_cols = max($max_cols, count($row));
537 }
538 for ($i = 0; $i < $max_cols; $i++) {
539 $columns[] = [
540 'label' => sprintf(__('Column %d', 'king-addons'), $i + 1),
541 'type' => 'text',
542 'sortable' => true,
543 'hide_mobile' => false,
544 'align' => 'left',
545 ];
546 }
547 }
548
549 $table_id = 'kng-table-' . $table->ID;
550 $preset = sanitize_key($style['preset'] ?? $this->options['default_preset']);
551 $theme = sanitize_key($style['theme'] ?? $this->options['default_theme']);
552 $scroll_x = !empty($responsive['scroll_x']);
553
554 $classes = [
555 'kng-table-builder',
556 'kng-table-preset-' . $preset,
557 'kng-table-theme-' . ($theme === 'light' ? 'light' : 'dark'),
558 $scroll_x ? 'kng-table-scroll-x' : 'kng-table-no-scroll',
559 ];
560 $extra_class = trim($extra_class);
561 if ($extra_class !== '') {
562 foreach (preg_split('/\\s+/', $extra_class) as $class_name) {
563 $classes[] = sanitize_html_class($class_name);
564 }
565 }
566
567 $config_data = [
568 'search' => !empty($config['search']),
569 'sorting' => !empty($config['sorting']),
570 'pagination' => !empty($config['pagination']),
571 'rowsPerPage' => (int) ($config['rows_per_page'] ?? 10),
572 'showToolbar' => true,
573 ];
574
575 $hide_cols = isset($responsive['hide_columns']) && is_array($responsive['hide_columns']) ? $responsive['hide_columns'] : [];
576
577 ob_start();
578 ?>
579 <div id="<?php echo esc_attr($table_id); ?>" class="<?php echo esc_attr(implode(' ', $classes)); ?>" data-config="<?php echo esc_attr(wp_json_encode($config_data)); ?>">
580 <div class="kng-table-toolbar">
581 <div class="kng-table-title">
582 <span><?php echo esc_html($table->post_title); ?></span>
583 </div>
584 <?php if (!empty($config['search'])) : ?>
585 <div class="kng-table-search">
586 <input type="search" placeholder="<?php esc_attr_e('Search table...', 'king-addons'); ?>" aria-label="<?php esc_attr_e('Search table', 'king-addons'); ?>">
587 </div>
588 <?php endif; ?>
589 </div>
590 <div class="kng-table-wrapper">
591 <table class="kng-table<?php echo !empty($style['zebra']) ? ' zebra' : ''; ?>" role="table">
592 <thead>
593 <tr>
594 <?php foreach ($columns as $index => $column) :
595 $label = $column['label'] ?? '';
596 $type = $column['type'] ?? 'text';
597 $align = $column['align'] ?? 'left';
598 ?>
599 <th data-col="<?php echo esc_attr($index); ?>" data-type="<?php echo esc_attr($type); ?>" class="kng-table-align-<?php echo esc_attr($align); ?>">
600 <span><?php echo esc_html($label); ?></span>
601 </th>
602 <?php endforeach; ?>
603 </tr>
604 </thead>
605 <tbody>
606 <?php
607 $skip_map = [];
608 foreach ($rows as $row_index => $row) :
609 $col_index = 0;
610 ?>
611 <tr>
612 <?php
613 while ($col_index < count($columns)) :
614 if (!empty($skip_map[$col_index])) {
615 $skip_map[$col_index]--;
616 $col_index++;
617 continue;
618 }
619
620 $cell = $row[$col_index] ?? [];
621 $cell_data = is_array($cell) ? $cell : ['value' => (string) $cell];
622 $value = $cell_data['value'] ?? '';
623 $tooltip = $cell_data['tooltip'] ?? '';
624 $rowspan = max(1, (int) ($cell_data['rowspan'] ?? 1));
625 $colspan = max(1, (int) ($cell_data['colspan'] ?? 1));
626
627 if ($rowspan > 1) {
628 for ($span = 0; $span < $colspan; $span++) {
629 $skip_map[$col_index + $span] = max((int) ($skip_map[$col_index + $span] ?? 0), $rowspan - 1);
630 }
631 }
632 ?>
633 <td class="kng-table-align-<?php echo esc_attr($columns[$col_index]['align'] ?? 'left'); ?>"<?php if ($tooltip !== '') : ?> data-tooltip="<?php echo esc_attr($tooltip); ?>"<?php endif; ?><?php if ($rowspan > 1) : ?> rowspan="<?php echo esc_attr($rowspan); ?>"<?php endif; ?><?php if ($colspan > 1) : ?> colspan="<?php echo esc_attr($colspan); ?>"<?php endif; ?>>
634 <?php echo esc_html($value); ?>
635 </td>
636 <?php
637 $col_index += $colspan;
638 endwhile;
639 ?>
640 </tr>
641 <?php endforeach; ?>
642 </tbody>
643 </table>
644 </div>
645 <?php if (!empty($config['pagination'])) : ?>
646 <div class="kng-table-pagination" aria-label="<?php esc_attr_e('Table pagination', 'king-addons'); ?>"></div>
647 <?php endif; ?>
648 </div>
649 <?php
650
651 if (!empty($hide_cols)) :
652 $hide_cols = array_map('absint', $hide_cols);
653 $hide_cols = array_filter($hide_cols, function ($value) {
654 return $value >= 0;
655 });
656 $hide_cols = array_values(array_unique($hide_cols));
657 if (!empty($hide_cols)) :
658 ?>
659 <style>
660 @media (max-width: 768px) {
661 <?php foreach ($hide_cols as $col_index) :
662 $nth = $col_index + 1;
663 ?>
664 #<?php echo esc_attr($table_id); ?> th:nth-child(<?php echo esc_attr($nth); ?>),
665 #<?php echo esc_attr($table_id); ?> td:nth-child(<?php echo esc_attr($nth); ?>) {
666 display: none;
667 }
668 <?php endforeach; ?>
669 }
670 </style>
671 <?php
672 endif;
673 endif;
674
675 return ob_get_clean();
676 }
677
678 private function get_default_table_config(): array
679 {
680 return [
681 'search' => true,
682 'sorting' => true,
683 'pagination' => true,
684 'rows_per_page' => (int) $this->options['default_rows_per_page'],
685 ];
686 }
687
688 private function get_default_table_style(): array
689 {
690 return [
691 'preset' => $this->options['default_preset'],
692 'theme' => $this->options['default_theme'],
693 'zebra' => true,
694 ];
695 }
696
697 private function get_default_responsive(): array
698 {
699 return [
700 'scroll_x' => true,
701 'hide_columns' => [],
702 ];
703 }
704
705 private function sanitize_table_data(string $json): array
706 {
707 $data = json_decode($json, true);
708 if (!is_array($data)) {
709 return [
710 'columns' => [],
711 'rows' => [],
712 'schema_version' => self::SCHEMA_VERSION,
713 ];
714 }
715
716 $columns = $data['columns'] ?? [];
717 $rows = $data['rows'] ?? [];
718
719 $columns = is_array($columns) ? $columns : [];
720 $rows = is_array($rows) ? $rows : [];
721
722 $is_pro = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code__premium_only();
723
724 $max_cols = $is_pro ? count($columns) : min(20, count($columns));
725 $columns = $is_pro ? $columns : array_slice($columns, 0, $max_cols);
726
727 $sanitized_columns = [];
728 foreach ($columns as $column) {
729 $sanitized_columns[] = [
730 'label' => sanitize_text_field($column['label'] ?? ''),
731 'type' => in_array($column['type'] ?? 'text', ['text', 'number', 'link'], true) ? $column['type'] : 'text',
732 'sortable' => !empty($column['sortable']),
733 'hide_mobile' => !empty($column['hide_mobile']),
734 'align' => in_array($column['align'] ?? 'left', ['left', 'center', 'right'], true) ? $column['align'] : 'left',
735 ];
736 }
737
738 $sanitized_rows = [];
739 $rows = $is_pro ? $rows : array_slice($rows, 0, 200);
740 foreach ($rows as $row) {
741 $row = is_array($row) ? $row : [];
742 $row = array_slice($row, 0, $max_cols);
743 $cells = [];
744 foreach ($row as $cell) {
745 $value = is_array($cell) ? ($cell['value'] ?? '') : $cell;
746 $cells[] = [
747 'value' => sanitize_text_field((string) $value),
748 'tooltip' => sanitize_text_field((string) ($cell['tooltip'] ?? '')),
749 'rowspan' => max(1, min(10, absint($cell['rowspan'] ?? 1))),
750 'colspan' => max(1, min(10, absint($cell['colspan'] ?? 1))),
751 ];
752 }
753
754 while (count($cells) < $max_cols) {
755 $cells[] = [
756 'value' => '',
757 'tooltip' => '',
758 'rowspan' => 1,
759 'colspan' => 1,
760 ];
761 }
762
763 $sanitized_rows[] = $cells;
764 }
765
766 return [
767 'columns' => $sanitized_columns,
768 'rows' => $sanitized_rows,
769 'schema_version' => self::SCHEMA_VERSION,
770 ];
771 }
772
773 private function sanitize_table_config(string $json): array
774 {
775 $config = json_decode($json, true);
776 if (!is_array($config)) {
777 $config = [];
778 }
779
780 return [
781 'search' => !empty($config['search']),
782 'sorting' => !empty($config['sorting']),
783 'pagination' => !empty($config['pagination']),
784 'rows_per_page' => max(5, min(100, absint($config['rows_per_page'] ?? $this->options['default_rows_per_page']))),
785 ];
786 }
787
788 private function sanitize_table_style(string $json): array
789 {
790 $style = json_decode($json, true);
791 if (!is_array($style)) {
792 $style = [];
793 }
794
795 $preset = sanitize_key($style['preset'] ?? $this->options['default_preset']);
796 $theme = sanitize_key($style['theme'] ?? $this->options['default_theme']);
797
798 return [
799 'preset' => $preset,
800 'theme' => $theme === 'light' ? 'light' : 'dark',
801 'zebra' => !empty($style['zebra']),
802 ];
803 }
804
805 public function sanitize_settings(array $settings): array
806 {
807 $defaults = $this->get_default_options();
808
809 $preset = sanitize_key($settings['default_preset'] ?? $defaults['default_preset']);
810 $theme = sanitize_key($settings['default_theme'] ?? $defaults['default_theme']);
811
812 return [
813 'enabled' => !empty($settings['enabled']),
814 'default_preset' => $preset !== '' ? $preset : $defaults['default_preset'],
815 'default_theme' => $theme === 'light' ? 'light' : 'dark',
816 'default_rows_per_page' => max(5, min(100, absint($settings['default_rows_per_page'] ?? $defaults['default_rows_per_page']))),
817 ];
818 }
819
820 private function sanitize_table_filters(string $json): array
821 {
822 $filters = json_decode($json, true);
823 return is_array($filters) ? $filters : [];
824 }
825
826 private function sanitize_table_responsive(string $json): array
827 {
828 $responsive = json_decode($json, true);
829 if (!is_array($responsive)) {
830 $responsive = [];
831 }
832
833 $hide_columns = $responsive['hide_columns'] ?? [];
834 if (!is_array($hide_columns)) {
835 $hide_columns = [];
836 }
837
838 $hide_columns = array_values(array_unique(array_map('absint', $hide_columns)));
839
840 return [
841 'scroll_x' => !empty($responsive['scroll_x']),
842 'hide_columns' => array_values($hide_columns),
843 ];
844 }
845
846 private function apply_shortcode_overrides(array $config, array $atts): array
847 {
848 if ($atts['search'] !== '') {
849 $config['search'] = filter_var($atts['search'], FILTER_VALIDATE_BOOLEAN);
850 }
851 if ($atts['pagination'] !== '') {
852 $config['pagination'] = filter_var($atts['pagination'], FILTER_VALIDATE_BOOLEAN);
853 }
854 if ($atts['sort'] !== '') {
855 $config['sorting'] = filter_var($atts['sort'], FILTER_VALIDATE_BOOLEAN);
856 }
857
858 return $config;
859 }
860
861 private function escape_csv_value(string $value): string
862 {
863 if (preg_match('/^[=+\-@]/', $value)) {
864 return "'" . $value;
865 }
866 return $value;
867 }
868
869 private function redirect_with_message(string $view, string $message, int $table_id = 0): void
870 {
871 $args = [
872 'page' => 'king-addons-table-builder',
873 'view' => $view,
874 'message' => $message,
875 ];
876
877 if ($table_id > 0) {
878 $args['table_id'] = $table_id;
879 }
880
881 wp_safe_redirect(add_query_arg($args, admin_url('admin.php')));
882 exit;
883 }
884 }
885