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 +290 -76 1.0.51.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,13 +274,18 @@
219 274 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
220 275 ],
221 276 ];
222 277
223 - // The table border radius rounds the whole table's outer corners.
224 - // border-radius on collapsed-border tables/cells is ignored by
225 - // browsers, so it is rendered on the wrapper (with overflow:hidden)
226 - // and removed from the individual cells here.
227 - $tableRadius = $globalCellStyles['borderRadius'];
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 + ];
228 288 $globalCellStyles['borderRadius'] = [
229 289 'topLeft' => '',
230 290 'topRight' => '',
231 291 'bottomRight' => '',
@@ -239,9 +299,47 @@
239 299
240 300 for ($row = 0; $row < $rows; $row++) {
241 301 $cellsHtml = '';
242 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 + : '';
243 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 +
244 342 for ($col = 0; $col < $cols; $col++) {
245 343 if (isset($hiddenBySpan[$row][$col])) {
246 344 continue;
247 345 }
@@ -294,9 +392,9 @@
294 392 $col,
295 393 $rowSpan,
296 394 $colSpan,
297 395 $tag,
298 - $globalCellStyles,
396 + $cellStylesForRow,
299 397 $this->getCellElements($cell),
300 398 $this->getCellStyleOverride($cell),
301 399 $this->getCellRibbon($cell),
302 400 $sortableType,
@@ -302,14 +400,41 @@
302 400 $sortableType,
303 401 $columnWidth,
304 402 $rowHeight,
305 403 $stickyHeader,
306 - $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
307 411 )
308 412 );
309 413 }
310 414
311 - $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>";
312 437 }
313 438
314 439 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
315 440 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
@@ -321,40 +446,83 @@
321 446 'sortable' => $sortType,
322 447 ];
323 448 }
324 449
325 - $columnsJson = json_encode($columnsData);
450 + $columnsJson = wp_json_encode($columnsData);
326 451 if (!is_string($columnsJson)) {
327 452 $columnsJson = '{}';
328 453 }
454 + $columnsJson = esc_attr($columnsJson);
329 455
330 - $paginationJson = json_encode($paginationConfig);
456 + $paginationJson = wp_json_encode($paginationConfig);
331 457 if (!is_string($paginationJson)) {
332 458 $paginationJson = '{}';
333 459 }
460 + $paginationJson = esc_attr($paginationJson);
334 461
462 + $searchPlaceholderAttr = esc_attr($searchPlaceholder);
463 + $searchPositionAttr = esc_attr($searchPosition);
464 + $searchHighlightColorAttr = esc_attr($searchHighlightColor);
465 +
335 466 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
336 467
337 - // Round the whole table's outer corners by clipping the wrapper.
338 - $wrapperRadiusStyles = [];
339 - if ($tableRadius['topLeft'] !== '') {
340 - $wrapperRadiusStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
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'];
341 474 }
342 - if ($tableRadius['topRight'] !== '') {
343 - $wrapperRadiusStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
475 + if (!$isZeroRadius($tableRadius['topRight'])) {
476 + $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
344 477 }
345 - if ($tableRadius['bottomRight'] !== '') {
346 - $wrapperRadiusStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
478 + if (!$isZeroRadius($tableRadius['bottomRight'])) {
479 + $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
347 480 }
348 - if ($tableRadius['bottomLeft'] !== '') {
349 - $wrapperRadiusStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
481 + if (!$isZeroRadius($tableRadius['bottomLeft'])) {
482 + $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
350 483 }
351 - $wrapperStyleAttr = '';
352 - if (!empty($wrapperRadiusStyles)) {
353 - $wrapperRadiusStyles[] = 'overflow:hidden';
354 - $wrapperStyleAttr = "style='" . implode(';', $wrapperRadiusStyles) . "'";
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';
355 499 }
356 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 +
357 525 $figureAlignmentClass = '';
358 526 if ($isWideWidth) {
359 527 $figureAlignmentClass = 'alignwide';
360 528 } elseif ($isFullWidth) {
@@ -403,11 +571,11 @@
403 571 data-tableberg-sortable='$sortingBoolAsStr'
404 572 data-tableberg-columns='$columnsJson'
405 573 data-tableberg-pagination='$paginationJson'
406 574 data-tableberg-search-enabled='$searchEnabledAsStr'
407 - data-tableberg-search-placeholder='$searchPlaceholder'
408 - data-tableberg-search-position='$searchPosition'
409 - 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'
410 578 data-tableberg-header='$headerBoolAsStr'
411 579 data-tableberg-footer='$footerBoolAsStr'
412 580 {$responsiveDataAttrs}
413 581 >
@@ -479,8 +647,15 @@
479 647 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
480 648 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
481 649 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
482 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 +
483 658 return "
484 659 data-tableberg-responsive='true'
485 660 data-tableberg-rows='{$rows}'
486 661 data-tableberg-cols='{$cols}'
@@ -488,15 +663,15 @@
488 663 data-tableberg-tablet-width='{$tabletMaxWidth}'
489 664 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
490 665 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
491 666 data-tableberg-tablet-count='{$tabletStackCount}'
492 - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
667 + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
493 668 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
494 669 data-tableberg-mobile-width='{$mobileMaxWidth}'
495 670 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
496 671 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
497 672 data-tableberg-mobile-count='{$mobileStackCount}'
498 - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
673 + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
499 674 ";
500 675 }
501 676
502 677 private function getCell($row, $col, $cells) {
@@ -509,13 +684,31 @@
509 684 return $cells[$key];
510 685 }
511 686
512 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) {
513 706 if (!$cell instanceof CellData) {
514 - return [];
707 + return false;
515 708 }
516 709
517 - return is_array($cell->elements) ? $cell->elements : [];
710 + return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
518 711 }
519 712
520 713 private function getCellStyleOverride($cell) {
521 714 if (!$cell instanceof CellData || !is_array($cell->styles)) {
@@ -593,6 +786,27 @@
593 786 }
594 787
595 788 $height = $rowConfig->height->asAttr();
596 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 : [];
597 811 }
598 812 }