PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.5
1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 trunk 0.0.2 0.2.1 0.3.2 0.3.3 0.4.1 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 All 42 releases
← All changes | renderer/Table/TableRenderer.php +325 -59 1.0.21.1.5 View file →
@@ -6,21 +6,8 @@
6 6 use Tableberg\Renderer\Cell\CellRenderContext;
7 7 use Tableberg\Renderer\Attrs\StringAttr;
8 8
9 9 class TableRenderer {
10 - /**
11 - * @return bool
12 - */
13 - private function is_pro_active() {
14 - if (!function_exists('tp_fs')) {
15 - return false;
16 - }
17 -
18 - $fs = call_user_func('tp_fs');
19 -
20 - return is_object($fs) && $fs->can_use_premium_code();
21 - }
22 -
23 10 private function has_visible_border($border) {
24 11 $trimmed_border = trim($border);
25 12
26 13 if ($trimmed_border === '') {
@@ -45,11 +32,28 @@
45 32
46 33 return true;
47 34 }
48 35
49 - public function render($attributes) {
36 + public function render($attributes, $content = '', $block = null) {
37 + // v4 native-blocks format: table data lives in the innerBlocks tree
38 + // (rows -> cells -> elements). Adapt it to the v3 attrs shape so the
39 + // renderer below stays format-agnostic.
40 + $version = is_array($attributes) && isset($attributes['version']) && is_numeric($attributes['version'])
41 + ? (int) $attributes['version']
42 + : 0;
43 + if (
44 + $version >= 4 &&
45 + is_object($block) &&
46 + isset($block->parsed_block['innerBlocks']) &&
47 + is_array($block->parsed_block['innerBlocks'])
48 + ) {
49 + $attributes = InnerBlocksAttrsAdapter::to_attrs(
50 + $attributes,
51 + $block->parsed_block['innerBlocks']
52 + );
53 + }
54 +
50 55 $attrs = TableAttrs::from_array($attributes);
51 - $isProActive = $this->is_pro_active();
52 56
53 57 $rows = $attrs->table->rows->value();
54 58 $cols = $attrs->table->cols->value();
55 59
@@ -54,9 +58,30 @@
54 58 $cols = $attrs->table->cols->value();
55 59
56 60 $headerEnabled = $attrs->table->headerEnabled->value();
57 61 $footerEnabled = $attrs->table->footerEnabled->value();
58 - $stickyHeader = $attrs->table->stickyHeader->value();
62 +
63 + // Sticky header/first column are pro features: free never enables
64 + // them on its own, and pro reads the raw table attrs to decide.
65 + $stickySettings = apply_filters(
66 + 'tableberg/table_sticky_settings',
67 + ['stickyHeader' => false, 'stickyFirstCol' => false],
68 + $attrs->attrs
69 + );
70 + $stickyHeader = !empty($stickySettings['stickyHeader']);
71 + $stickyFirstCol = !empty($stickySettings['stickyFirstCol']);
72 +
73 + // Row-only/column-only is Pro-owned. Free's empty default preserves
74 + // the attribute without enabling its editor or frontend behavior.
75 + $innerBorderType = apply_filters(
76 + 'tableberg/inner_border_type',
77 + '',
78 + $attrs->attrs
79 + );
80 + if (!in_array($innerBorderType, ['row', 'col'], true)) {
81 + $innerBorderType = '';
82 + }
83 +
59 84 $caption = $attrs->table->caption;
60 85 $tableWidth = trim($attrs->table->tableWidth->asAttr());
61 86 $tableAlignment = $attrs->table->tableAlignment->asAttr();
62 87 $cellSpacingHorizontal = trim($attrs->table->cellSpacing->horizontal->asAttr());
@@ -65,8 +90,24 @@
65 90 $tableBorderRight = trim($attrs->table->tableBorder->right->asAttr());
66 91 $tableBorderBottom = trim($attrs->table->tableBorder->bottom->asAttr());
67 92 $tableBorderLeft = trim($attrs->table->tableBorder->left->asAttr());
68 93 $fixedColumnWidths = $attrs->table->fixedColumnWidths->value();
94 + $tableRadius = [
95 + 'topLeft' => $attrs->cellDefaults->styles->borderRadius->topLeft->asAttr(),
96 + 'topRight' => $attrs->cellDefaults->styles->borderRadius->topRight->asAttr(),
97 + 'bottomRight' => $attrs->cellDefaults->styles->borderRadius->bottomRight->asAttr(),
98 + 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
99 + ];
100 + $isZeroRadius = function ($value) {
101 + $trimmed = trim((string) $value);
102 + return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1;
103 + };
104 + $hasRoundedCorners = count(array_filter(
105 + $tableRadius,
106 + function ($value) use ($isZeroRadius) {
107 + return !$isZeroRadius($value);
108 + }
109 + )) > 0;
69 110
70 111 $isHorizontalSpacingZero = $cellSpacingHorizontal === '0';
71 112 $isVerticalSpacingZero = $cellSpacingVertical === '0';
72 113 $hasCellSpacing = !$isHorizontalSpacingZero || !$isVerticalSpacingZero;
@@ -92,8 +133,11 @@
92 133 );
93 134
94 135 $tableStyles = [
95 136 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
137 + // Neutralize theme-level table borders. Tableberg owns its
138 + // borders through the table/cell controls and rounded wrapper.
139 + 'border: 0',
96 140 ];
97 141
98 142 if ($hasCellSpacing) {
99 143 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
@@ -105,21 +149,21 @@
105 149 } else {
106 150 $tableStyles[] = 'width: 100%';
107 151 }
108 152
109 - if ($tableBorderTop !== '') {
153 + if (!$hasRoundedCorners && $tableBorderTop !== '') {
110 154 $tableStyles[] = "border-top: {$tableBorderTop}";
111 155 }
112 156
113 - if ($tableBorderRight !== '') {
157 + if (!$hasRoundedCorners && $tableBorderRight !== '') {
114 158 $tableStyles[] = "border-right: {$tableBorderRight}";
115 159 }
116 160
117 - if ($tableBorderBottom !== '') {
161 + if (!$hasRoundedCorners && $tableBorderBottom !== '') {
118 162 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
119 163 }
120 164
121 - if ($tableBorderLeft !== '') {
165 + if (!$hasRoundedCorners && $tableBorderLeft !== '') {
122 166 $tableStyles[] = "border-left: {$tableBorderLeft}";
123 167 }
124 168
125 169 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
@@ -155,18 +199,18 @@
155 199 }
156 200
157 201 $tableClassAttr = implode(' ', $tableClasses);
158 202
159 - $sortableColumns = [];
160 -
161 - if ($isProActive) {
162 - foreach ($attrs->columns as $column => $columnConfig) {
163 - if ($columnConfig->sortable === null) {
164 - continue;
165 - }
166 -
167 - $sortableColumns[$column] = $columnConfig->sortable->asAttr();
168 - }
203 + // Column sorting is a pro feature: free never marks a column
204 + // sortable on its own, and pro reads the table's raw columns to
205 + // decide which ones are.
206 + $sortableColumns = apply_filters(
207 + 'tableberg/sortable_columns',
208 + [],
209 + $attrs->attrs
210 + );
211 + if (!is_array($sortableColumns)) {
212 + $sortableColumns = [];
169 213 }
170 214
171 215 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
172 216 if ($paginationPageSize < 1) {
@@ -173,28 +217,39 @@
173 217 $paginationPageSize = 1;
174 218 }
175 219
176 220 $paginationConfig = [
177 - 'enabled' => $isProActive && $attrs->table->pagination->enabled->value(),
221 + 'enabled' => apply_filters('tableberg/pagination_enabled', false, $attrs->attrs),
178 222 'pageSize' => $paginationPageSize,
179 223 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
180 224 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
181 225 ];
182 226
183 - $searchEnabledAsStr = (
184 - $isProActive && $attrs->table->search->enabled->value()
185 - ) ? 'true' : 'false';
186 - $searchPlaceholder = $isProActive
187 - ? $attrs->table->search->placeholder->asAttr()
188 - : '';
189 - $searchPosition = $isProActive
190 - ? $attrs->table->search->position->asAttr()
191 - : 'left';
192 - $searchHighlightColor = $isProActive
193 - ? $attrs->table->search->highlightColor->asAttr()
194 - : '';
227 + $searchSettings = apply_filters(
228 + 'tableberg/table_search_settings',
229 + [
230 + 'enabled' => false,
231 + 'placeholder' => '',
232 + 'position' => 'left',
233 + 'highlightColor' => '',
234 + ],
235 + $attrs->attrs
236 + );
237 + $searchEnabledAsStr = !empty($searchSettings['enabled']) ? 'true' : 'false';
238 + $searchPlaceholder = (string) ($searchSettings['placeholder'] ?? '');
239 + $searchPosition = (string) ($searchSettings['position'] ?? 'left');
240 + $searchHighlightColor = (string) ($searchSettings['highlightColor'] ?? '');
195 241 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
196 242
243 + // Horizontal cell-element layout (and the wrap toggle that only
244 + // matters with it) is a pro feature; free's default is always the
245 + // vertical stack. Grouped in one filter since they're one decision.
246 + $cellLayout = apply_filters(
247 + 'tableberg/cell_default_layout',
248 + ['orientation' => 'vertical', 'wrap' => 'nowrap'],
249 + $attrs->attrs
250 + );
251 +
197 252 $globalCellStyles = [
198 253 'padding' => [
199 254 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
200 255 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
@@ -200,11 +255,11 @@
200 255 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
201 256 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
202 257 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
203 258 ],
204 - 'orientation' => $attrs->cellDefaults->styles->orientation->asAttr(),
259 + 'orientation' => $cellLayout['orientation'] === 'horizontal' ? 'horizontal' : 'vertical',
205 260 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
206 - 'wrap' => $attrs->cellDefaults->styles->wrap->asAttr(),
261 + 'wrap' => $cellLayout['wrap'] === 'wrap' ? 'wrap' : 'nowrap',
207 262 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
208 263 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
209 264 'border' => [
210 265 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
@@ -219,8 +274,25 @@
219 274 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
220 275 ],
221 276 ];
222 277
278 + // A collapsed table cannot reliably round its own explicit border.
279 + // The wrapper owns only the table border and clips the cell grid to
280 + // it. Cell borders stay on cells; copying them to the wrapper would
281 + // add an outline that the user did not configure as a table border.
282 + $tableOuterBorder = [
283 + 'top' => $tableBorderTop,
284 + 'right' => $tableBorderRight,
285 + 'bottom' => $tableBorderBottom,
286 + 'left' => $tableBorderLeft,
287 + ];
288 + $globalCellStyles['borderRadius'] = [
289 + 'topLeft' => '',
290 + 'topRight' => '',
291 + 'bottomRight' => '',
292 + 'bottomLeft' => '',
293 + ];
294 +
223 295 $cellRenderer = new CellRenderer();
224 296 $hiddenBySpan = [];
225 297
226 298 $rowsHtml = '';
@@ -227,9 +299,47 @@
227 299
228 300 for ($row = 0; $row < $rows; $row++) {
229 301 $cellsHtml = '';
230 302 $rowHeight = $this->getRowHeight($row, $attrs->rows);
303 + $rowStyles = $this->getRowStyles($row, $attrs->rows);
304 + $rowBackgroundColor = isset($rowStyles['backgroundColor']) && is_string($rowStyles['backgroundColor'])
305 + ? $rowStyles['backgroundColor']
306 + : '';
231 307
308 + $cellStylesForRow = $globalCellStyles;
309 +
310 + // Header/footer/even/odd are table-wide defaults, resolved per
311 + // row here the same way `cell/index.tsx`'s
312 + // `useRowBackgroundStyleKey` does in the editor — header/footer
313 + // don't consume a slot in the even/odd count.
314 + $isHeaderRow = $headerEnabled && $row === 0;
315 + $isFooterRow = $footerEnabled && $rows > 0 && $row === $rows - 1;
316 +
317 + if ($isHeaderRow) {
318 + $rowPositionBackground = $attrs->cellDefaults->styles->headerBackgroundColor->asAttr();
319 + } elseif ($isFooterRow) {
320 + $rowPositionBackground = $attrs->cellDefaults->styles->footerBackgroundColor->asAttr();
321 + } else {
322 + $dataRowPosition = $headerEnabled ? $row - 1 : $row;
323 + $rowPositionBackground = ($dataRowPosition % 2 === 0)
324 + ? $attrs->cellDefaults->styles->oddRowBackgroundColor->asAttr()
325 + : $attrs->cellDefaults->styles->evenRowBackgroundColor->asAttr();
326 + }
327 +
328 + if ($rowPositionBackground !== '') {
329 + $cellStylesForRow['backgroundColor'] = $rowPositionBackground;
330 + }
331 +
332 + // A row with its own background would otherwise never show
333 + // through: every cell paints its own background over the
334 + // `<tr>`'s, and cells fall back to the table-wide default
335 + // whenever they carry no colour of their own. So for this row's
336 + // cells, that fallback is cleared — a cell/column colour on top
337 + // of it (applied below by the pro filter) still wins either way.
338 + if ($rowBackgroundColor !== '') {
339 + $cellStylesForRow['backgroundColor'] = '';
340 + }
341 +
232 342 for ($col = 0; $col < $cols; $col++) {
233 343 if (isset($hiddenBySpan[$row][$col])) {
234 344 continue;
235 345 }
@@ -282,9 +392,9 @@
282 392 $col,
283 393 $rowSpan,
284 394 $colSpan,
285 395 $tag,
286 - $globalCellStyles,
396 + $cellStylesForRow,
287 397 $this->getCellElements($cell),
288 398 $this->getCellStyleOverride($cell),
289 399 $this->getCellRibbon($cell),
290 400 $sortableType,
@@ -290,14 +400,41 @@
290 400 $sortableType,
291 401 $columnWidth,
292 402 $rowHeight,
293 403 $stickyHeader,
294 - $this->getCellClassName($cell)
404 + $stickyFirstCol,
405 + $this->getCellClassName($cell),
406 + $cell instanceof CellData ? $cell->attrs : [],
407 + $this->isCellEmpty($cell),
408 + $innerBorderType,
409 + $rows,
410 + $cols
295 411 )
296 412 );
297 413 }
298 414
299 - $rowsHtml .= "<tr data-tableberg-row='{$row}'>$cellsHtml</tr>";
415 + $rowStyleParts = [];
416 + if ($rowBackgroundColor !== '') {
417 + $rowStyleParts[] = 'background-color:' . esc_attr($rowBackgroundColor);
418 + }
419 +
420 + $rowBorder = isset($rowStyles['border']) && is_array($rowStyles['border'])
421 + ? $rowStyles['border']
422 + : [];
423 + foreach (['top', 'right', 'bottom', 'left'] as $side) {
424 + $value = isset($rowBorder[$side]) && is_string($rowBorder[$side])
425 + ? $rowBorder[$side]
426 + : '';
427 + if ($value !== '') {
428 + $rowStyleParts[] = "border-{$side}:" . esc_attr($value);
429 + }
430 + }
431 +
432 + $rowStyleAttr = !empty($rowStyleParts)
433 + ? ' style="' . implode(';', $rowStyleParts) . '"'
434 + : '';
435 +
436 + $rowsHtml .= "<tr data-tableberg-row='{$row}'$rowStyleAttr>$cellsHtml</tr>";
300 437 }
301 438
302 439 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
303 440 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
@@ -309,20 +446,83 @@
309 446 'sortable' => $sortType,
310 447 ];
311 448 }
312 449
313 - $columnsJson = json_encode($columnsData);
450 + $columnsJson = wp_json_encode($columnsData);
314 451 if (!is_string($columnsJson)) {
315 452 $columnsJson = '{}';
316 453 }
454 + $columnsJson = esc_attr($columnsJson);
317 455
318 - $paginationJson = json_encode($paginationConfig);
456 + $paginationJson = wp_json_encode($paginationConfig);
319 457 if (!is_string($paginationJson)) {
320 458 $paginationJson = '{}';
321 459 }
460 + $paginationJson = esc_attr($paginationJson);
322 461
462 + $searchPlaceholderAttr = esc_attr($searchPlaceholder);
463 + $searchPositionAttr = esc_attr($searchPosition);
464 + $searchHighlightColorAttr = esc_attr($searchHighlightColor);
465 +
323 466 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
324 467
468 + // Round the whole table's outer corners on the wrapper.
469 + // Zero radii are skipped so a table without any actual rounding does
470 + // not carry pointless border-radius declarations.
471 + $wrapperStyles = [];
472 + if (!$isZeroRadius($tableRadius['topLeft'])) {
473 + $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
474 + }
475 + if (!$isZeroRadius($tableRadius['topRight'])) {
476 + $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
477 + }
478 + if (!$isZeroRadius($tableRadius['bottomRight'])) {
479 + $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
480 + }
481 + if (!$isZeroRadius($tableRadius['bottomLeft'])) {
482 + $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
483 + }
484 +
485 + if (!empty($wrapperStyles)) {
486 + if ($tableOuterBorder['top'] !== '') {
487 + $wrapperStyles[] = 'border-top:' . $tableOuterBorder['top'];
488 + }
489 + if ($tableOuterBorder['right'] !== '') {
490 + $wrapperStyles[] = 'border-right:' . $tableOuterBorder['right'];
491 + }
492 + if ($tableOuterBorder['bottom'] !== '') {
493 + $wrapperStyles[] = 'border-bottom:' . $tableOuterBorder['bottom'];
494 + }
495 + if ($tableOuterBorder['left'] !== '') {
496 + $wrapperStyles[] = 'border-left:' . $tableOuterBorder['left'];
497 + }
498 + $wrapperStyles[] = 'box-sizing:border-box';
499 + }
500 +
501 + // A table wider than its wrapper must scroll inside the wrapper
502 + // instead of pushing the whole page sideways, so the wrapper is a
503 + // scroll container by default.
504 + //
505 + // The one exception is a header that sticks to the page: a scroll
506 + // container of its own would capture it and it would never stick.
507 + // (CSS cannot do both — with overflow-x set, overflow-y can no longer
508 + // be visible.) A sticky first column, on the other hand, only works
509 + // *because* of the scroll container, so when both are enabled the
510 + // scrolling wins and the header sticks within the wrapper.
511 + //
512 + // A rounded wrapper always needs the clip, so it opts in regardless.
513 + $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
514 + if ($hasRoundedCorners || !$pageStickyHeader) {
515 + // `auto`, not `hidden`: both clip to the rounded corners, but
516 + // `hidden` would also swallow a table wider than the wrapper.
517 + // Inline, so it also wins over the `.tableberg-scroll-x` rule.
518 + $wrapperStyles[] = 'overflow:auto';
519 + }
520 +
521 + $wrapperStyleAttr = !empty($wrapperStyles)
522 + ? "style='" . implode(';', $wrapperStyles) . "'"
523 + : '';
524 +
325 525 $figureAlignmentClass = '';
326 526 if ($isWideWidth) {
327 527 $figureAlignmentClass = 'alignwide';
328 528 } elseif ($isFullWidth) {
@@ -336,8 +536,28 @@
336 536 $figureAlignmentClass,
337 537 ]))
338 538 );
339 539
540 + $figureStyles = [];
541 + $spacingSides = [
542 + 'margin-top' => $attrs->table->margin->top,
543 + 'margin-right' => $attrs->table->margin->right,
544 + 'margin-bottom' => $attrs->table->margin->bottom,
545 + 'margin-left' => $attrs->table->margin->left,
546 + 'padding-top' => $attrs->table->padding->top,
547 + 'padding-right' => $attrs->table->padding->right,
548 + 'padding-bottom' => $attrs->table->padding->bottom,
549 + 'padding-left' => $attrs->table->padding->left,
550 + ];
551 + foreach ($spacingSides as $prop => $sideAttr) {
552 + if ($sideAttr->isNotEmpty()) {
553 + $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
554 + }
555 + }
556 + $figureStyleAttr = !empty($figureStyles)
557 + ? "style='" . implode('; ', $figureStyles) . ";'"
558 + : '';
559 +
340 560 $captionHtml = '';
341 561 if ($caption->isNotEmpty()) {
342 562 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
343 563 }
@@ -342,10 +562,10 @@
342 562 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
343 563 }
344 564
345 565 $html =
346 - "<figure class='{$figureClass}'>
347 - <div class='{$wrapperClass}'>
566 + "<figure class='{$figureClass}' {$figureStyleAttr}>
567 + <div class='{$wrapperClass}' {$wrapperStyleAttr}>
348 568 <table
349 569 class='{$tableClassAttr}'
350 570 {$tableStyleAttr}
351 571 data-tableberg-sortable='$sortingBoolAsStr'
@@ -351,11 +571,11 @@
351 571 data-tableberg-sortable='$sortingBoolAsStr'
352 572 data-tableberg-columns='$columnsJson'
353 573 data-tableberg-pagination='$paginationJson'
354 574 data-tableberg-search-enabled='$searchEnabledAsStr'
355 - data-tableberg-search-placeholder='$searchPlaceholder'
356 - data-tableberg-search-position='$searchPosition'
357 - data-tableberg-search-highlight-color='$searchHighlightColor'
575 + data-tableberg-search-placeholder='$searchPlaceholderAttr'
576 + data-tableberg-search-position='$searchPositionAttr'
577 + data-tableberg-search-highlight-color='$searchHighlightColorAttr'
358 578 data-tableberg-header='$headerBoolAsStr'
359 579 data-tableberg-footer='$footerBoolAsStr'
360 580 {$responsiveDataAttrs}
361 581 >
@@ -427,8 +647,15 @@
427 647 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
428 648 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
429 649 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
430 650
651 + // Repeating the first column in every stack row is a pro option. Only
652 + // the licensed pro plugin turns this filter on, so the saved value is
653 + // ignored without a valid licence.
654 + $proActive = (bool) apply_filters('tableberg/is_pro_runtime_active', false);
655 + $tabletRepeatFirstCol = $proActive ? $tablet->repeatFirstCol->asAttr() : '';
656 + $mobileRepeatFirstCol = $proActive ? $mobile->repeatFirstCol->asAttr() : '';
657 +
431 658 return "
432 659 data-tableberg-responsive='true'
433 660 data-tableberg-rows='{$rows}'
434 661 data-tableberg-cols='{$cols}'
@@ -436,15 +663,15 @@
436 663 data-tableberg-tablet-width='{$tabletMaxWidth}'
437 664 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
438 665 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
439 666 data-tableberg-tablet-count='{$tabletStackCount}'
440 - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
667 + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
441 668 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
442 669 data-tableberg-mobile-width='{$mobileMaxWidth}'
443 670 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
444 671 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
445 672 data-tableberg-mobile-count='{$mobileStackCount}'
446 - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
673 + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
447 674 ";
448 675 }
449 676
450 677 private function getCell($row, $col, $cells) {
@@ -457,13 +684,31 @@
457 684 return $cells[$key];
458 685 }
459 686
460 687 private function getCellElements($cell) {
688 + if ($this->isCellEmpty($cell)) {
689 + return [];
690 + }
691 +
692 + return $cell instanceof CellData && is_array($cell->elements)
693 + ? $cell->elements
694 + : [];
695 + }
696 +
697 + /**
698 + * "Empty cell" is a pro feature: the cell renders as if it were a bare,
699 + * unstyled table cell — no content, no background, no border. Free's
700 + * default is to always show the cell as configured.
701 + *
702 + * @param mixed $cell
703 + * @return bool
704 + */
705 + private function isCellEmpty($cell) {
461 706 if (!$cell instanceof CellData) {
462 - return [];
707 + return false;
463 708 }
464 709
465 - return is_array($cell->elements) ? $cell->elements : [];
710 + return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
466 711 }
467 712
468 713 private function getCellStyleOverride($cell) {
469 714 if (!$cell instanceof CellData || !is_array($cell->styles)) {
@@ -541,6 +786,27 @@
541 786 }
542 787
543 788 $height = $rowConfig->height->asAttr();
544 789 return $height === '' ? null : $height;
790 + }
791 +
792 + /**
793 + * Pro styling of a row (e.g. background colour). One filter carries
794 + * every pro row style, mirroring `tableberg/cell_styles`, so a new pro
795 + * style needs no change here.
796 + *
797 + * @param int $row
798 + * @param array<int|string, RowConfig> $rowConfigs
799 + * @return array<string, string>
800 + */
801 + private function getRowStyles($row, $rowConfigs) {
802 + $rowAttrs = (
803 + is_array($rowConfigs) &&
804 + array_key_exists($row, $rowConfigs) &&
805 + $rowConfigs[$row] instanceof RowConfig
806 + ) ? $rowConfigs[$row]->attrs : [];
807 +
808 + $styles = apply_filters('tableberg/row_styles', [], $rowAttrs);
809 +
810 + return is_array($styles) ? $styles : [];
545 811 }
546 812 }