PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.2.0
Tableberg – Simple Gutenberg Table Block v1.2.0
1.2.0 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 All 43 releases
← All changes | renderer/Table/TableRenderer.php +303 -79 1.0.5 → 1.2.0 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;
@@ -90,11 +131,19 @@
90 131 $canApplyCustomWidth,
91 132 $tableAlignment
92 133 );
93 134
94 - $tableStyles = [
95 - 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
96 - ];
135 + $tableStyles = array_merge(
136 + // Table-wide typography, inherited by everything in the cells that
137 + // does not set its own.
138 + $attrs->table->typography->asStyles(),
139 + [
140 + 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
141 + // Neutralize theme-level table borders. Tableberg owns its
142 + // borders through the table/cell controls and rounded wrapper.
143 + 'border: 0',
144 + ]
145 + );
97 146
98 147 if ($hasCellSpacing) {
99 148 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
100 149 }
@@ -105,21 +154,21 @@
105 154 } else {
106 155 $tableStyles[] = 'width: 100%';
107 156 }
108 157
109 - if ($tableBorderTop !== '') {
158 + if (!$hasRoundedCorners && $tableBorderTop !== '') {
110 159 $tableStyles[] = "border-top: {$tableBorderTop}";
111 160 }
112 161
113 - if ($tableBorderRight !== '') {
162 + if (!$hasRoundedCorners && $tableBorderRight !== '') {
114 163 $tableStyles[] = "border-right: {$tableBorderRight}";
115 164 }
116 165
117 - if ($tableBorderBottom !== '') {
166 + if (!$hasRoundedCorners && $tableBorderBottom !== '') {
118 167 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
119 168 }
120 169
121 - if ($tableBorderLeft !== '') {
170 + if (!$hasRoundedCorners && $tableBorderLeft !== '') {
122 171 $tableStyles[] = "border-left: {$tableBorderLeft}";
123 172 }
124 173
125 174 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
@@ -155,18 +204,18 @@
155 204 }
156 205
157 206 $tableClassAttr = implode(' ', $tableClasses);
158 207
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 - }
208 + // Column sorting is a pro feature: free never marks a column
209 + // sortable on its own, and pro reads the table's raw columns to
210 + // decide which ones are.
211 + $sortableColumns = apply_filters(
212 + 'tableberg/sortable_columns',
213 + [],
214 + $attrs->attrs
215 + );
216 + if (!is_array($sortableColumns)) {
217 + $sortableColumns = [];
169 218 }
170 219
171 220 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
172 221 if ($paginationPageSize < 1) {
@@ -173,28 +222,39 @@
173 222 $paginationPageSize = 1;
174 223 }
175 224
176 225 $paginationConfig = [
177 - 'enabled' => $isProActive && $attrs->table->pagination->enabled->value(),
226 + 'enabled' => apply_filters('tableberg/pagination_enabled', false, $attrs->attrs),
178 227 'pageSize' => $paginationPageSize,
179 228 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
180 229 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
181 230 ];
182 231
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 - : '';
232 + $searchSettings = apply_filters(
233 + 'tableberg/table_search_settings',
234 + [
235 + 'enabled' => false,
236 + 'placeholder' => '',
237 + 'position' => 'left',
238 + 'highlightColor' => '',
239 + ],
240 + $attrs->attrs
241 + );
242 + $searchEnabledAsStr = !empty($searchSettings['enabled']) ? 'true' : 'false';
243 + $searchPlaceholder = (string) ($searchSettings['placeholder'] ?? '');
244 + $searchPosition = (string) ($searchSettings['position'] ?? 'left');
245 + $searchHighlightColor = (string) ($searchSettings['highlightColor'] ?? '');
195 246 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
196 247
248 + // Horizontal cell-element layout (and the wrap toggle that only
249 + // matters with it) is a pro feature; free's default is always the
250 + // vertical stack. Grouped in one filter since they're one decision.
251 + $cellLayout = apply_filters(
252 + 'tableberg/cell_default_layout',
253 + ['orientation' => 'vertical', 'wrap' => 'nowrap'],
254 + $attrs->attrs
255 + );
256 +
197 257 $globalCellStyles = [
198 258 'padding' => [
199 259 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
200 260 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
@@ -200,11 +260,11 @@
200 260 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
201 261 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
202 262 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
203 263 ],
204 - 'orientation' => $attrs->cellDefaults->styles->orientation->asAttr(),
264 + 'orientation' => $cellLayout['orientation'] === 'horizontal' ? 'horizontal' : 'vertical',
205 265 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
206 - 'wrap' => $attrs->cellDefaults->styles->wrap->asAttr(),
266 + 'wrap' => $cellLayout['wrap'] === 'wrap' ? 'wrap' : 'nowrap',
207 267 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
208 268 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
209 269 'border' => [
210 270 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
@@ -219,13 +279,18 @@
219 279 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
220 280 ],
221 281 ];
222 282
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'];
283 + // A collapsed table cannot reliably round its own explicit border.
284 + // The wrapper owns only the table border and clips the cell grid to
285 + // it. Cell borders stay on cells; copying them to the wrapper would
286 + // add an outline that the user did not configure as a table border.
287 + $tableOuterBorder = [
288 + 'top' => $tableBorderTop,
289 + 'right' => $tableBorderRight,
290 + 'bottom' => $tableBorderBottom,
291 + 'left' => $tableBorderLeft,
292 + ];
228 293 $globalCellStyles['borderRadius'] = [
229 294 'topLeft' => '',
230 295 'topRight' => '',
231 296 'bottomRight' => '',
@@ -239,9 +304,47 @@
239 304
240 305 for ($row = 0; $row < $rows; $row++) {
241 306 $cellsHtml = '';
242 307 $rowHeight = $this->getRowHeight($row, $attrs->rows);
308 + $rowStyles = $this->getRowStyles($row, $attrs->rows);
309 + $rowBackgroundColor = isset($rowStyles['backgroundColor']) && is_string($rowStyles['backgroundColor'])
310 + ? $rowStyles['backgroundColor']
311 + : '';
243 312
313 + $cellStylesForRow = $globalCellStyles;
314 +
315 + // Header/footer/even/odd are table-wide defaults, resolved per
316 + // row here the same way `cell/index.tsx`'s
317 + // `useRowBackgroundStyleKey` does in the editor — header/footer
318 + // don't consume a slot in the even/odd count.
319 + $isHeaderRow = $headerEnabled && $row === 0;
320 + $isFooterRow = $footerEnabled && $rows > 0 && $row === $rows - 1;
321 +
322 + if ($isHeaderRow) {
323 + $rowPositionBackground = $attrs->cellDefaults->styles->headerBackgroundColor->asAttr();
324 + } elseif ($isFooterRow) {
325 + $rowPositionBackground = $attrs->cellDefaults->styles->footerBackgroundColor->asAttr();
326 + } else {
327 + $dataRowPosition = $headerEnabled ? $row - 1 : $row;
328 + $rowPositionBackground = ($dataRowPosition % 2 === 0)
329 + ? $attrs->cellDefaults->styles->oddRowBackgroundColor->asAttr()
330 + : $attrs->cellDefaults->styles->evenRowBackgroundColor->asAttr();
331 + }
332 +
333 + if ($rowPositionBackground !== '') {
334 + $cellStylesForRow['backgroundColor'] = $rowPositionBackground;
335 + }
336 +
337 + // A row with its own background would otherwise never show
338 + // through: every cell paints its own background over the
339 + // `<tr>`'s, and cells fall back to the table-wide default
340 + // whenever they carry no colour of their own. So for this row's
341 + // cells, that fallback is cleared — a cell/column colour on top
342 + // of it (applied below by the pro filter) still wins either way.
343 + if ($rowBackgroundColor !== '') {
344 + $cellStylesForRow['backgroundColor'] = '';
345 + }
346 +
244 347 for ($col = 0; $col < $cols; $col++) {
245 348 if (isset($hiddenBySpan[$row][$col])) {
246 349 continue;
247 350 }
@@ -294,9 +397,9 @@
294 397 $col,
295 398 $rowSpan,
296 399 $colSpan,
297 400 $tag,
298 - $globalCellStyles,
401 + $cellStylesForRow,
299 402 $this->getCellElements($cell),
300 403 $this->getCellStyleOverride($cell),
301 404 $this->getCellRibbon($cell),
302 405 $sortableType,
@@ -302,14 +405,50 @@
302 405 $sortableType,
303 406 $columnWidth,
304 407 $rowHeight,
305 408 $stickyHeader,
306 - $this->getCellClassName($cell)
409 + $stickyFirstCol,
410 + $this->getCellClassName($cell),
411 + $cell instanceof CellData ? $cell->attrs : [],
412 + $this->isCellEmpty($cell),
413 + $innerBorderType,
414 + $rows,
415 + $cols
307 416 )
308 417 );
309 418 }
310 419
311 - $rowsHtml .= "<tr data-tableberg-row='{$row}'>$cellsHtml</tr>";
420 + $rowStyleParts = [];
421 + if ($rowBackgroundColor !== '') {
422 + $rowStyleParts[] = 'background-color:' . esc_attr($rowBackgroundColor);
423 + }
424 +
425 + // Inherited by everything in the row's cells that has no colour
426 + // of its own.
427 + $rowTextColor = isset($rowStyles['textColor']) && is_string($rowStyles['textColor'])
428 + ? $rowStyles['textColor']
429 + : '';
430 + if ($rowTextColor !== '') {
431 + $rowStyleParts[] = 'color:' . esc_attr($rowTextColor);
432 + }
433 +
434 + $rowBorder = isset($rowStyles['border']) && is_array($rowStyles['border'])
435 + ? $rowStyles['border']
436 + : [];
437 + foreach (['top', 'right', 'bottom', 'left'] as $side) {
438 + $value = isset($rowBorder[$side]) && is_string($rowBorder[$side])
439 + ? $rowBorder[$side]
440 + : '';
441 + if ($value !== '') {
442 + $rowStyleParts[] = "border-{$side}:" . esc_attr($value);
443 + }
444 + }
445 +
446 + $rowStyleAttr = !empty($rowStyleParts)
447 + ? ' style="' . implode(';', $rowStyleParts) . '"'
448 + : '';
449 +
450 + $rowsHtml .= "<tr data-tableberg-row='{$row}'$rowStyleAttr>$cellsHtml</tr>";
312 451 }
313 452
314 453 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
315 454 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
@@ -321,40 +460,83 @@
321 460 'sortable' => $sortType,
322 461 ];
323 462 }
324 463
325 - $columnsJson = json_encode($columnsData);
464 + $columnsJson = wp_json_encode($columnsData);
326 465 if (!is_string($columnsJson)) {
327 466 $columnsJson = '{}';
328 467 }
468 + $columnsJson = esc_attr($columnsJson);
329 469
330 - $paginationJson = json_encode($paginationConfig);
470 + $paginationJson = wp_json_encode($paginationConfig);
331 471 if (!is_string($paginationJson)) {
332 472 $paginationJson = '{}';
333 473 }
474 + $paginationJson = esc_attr($paginationJson);
334 475
476 + $searchPlaceholderAttr = esc_attr($searchPlaceholder);
477 + $searchPositionAttr = esc_attr($searchPosition);
478 + $searchHighlightColorAttr = esc_attr($searchHighlightColor);
479 +
335 480 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
336 481
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'];
482 + // Round the whole table's outer corners on the wrapper.
483 + // Zero radii are skipped so a table without any actual rounding does
484 + // not carry pointless border-radius declarations.
485 + $wrapperStyles = [];
486 + if (!$isZeroRadius($tableRadius['topLeft'])) {
487 + $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
341 488 }
342 - if ($tableRadius['topRight'] !== '') {
343 - $wrapperRadiusStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
489 + if (!$isZeroRadius($tableRadius['topRight'])) {
490 + $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
344 491 }
345 - if ($tableRadius['bottomRight'] !== '') {
346 - $wrapperRadiusStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
492 + if (!$isZeroRadius($tableRadius['bottomRight'])) {
493 + $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
347 494 }
348 - if ($tableRadius['bottomLeft'] !== '') {
349 - $wrapperRadiusStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
495 + if (!$isZeroRadius($tableRadius['bottomLeft'])) {
496 + $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
350 497 }
351 - $wrapperStyleAttr = '';
352 - if (!empty($wrapperRadiusStyles)) {
353 - $wrapperRadiusStyles[] = 'overflow:hidden';
354 - $wrapperStyleAttr = "style='" . implode(';', $wrapperRadiusStyles) . "'";
498 +
499 + if (!empty($wrapperStyles)) {
500 + if ($tableOuterBorder['top'] !== '') {
501 + $wrapperStyles[] = 'border-top:' . $tableOuterBorder['top'];
502 + }
503 + if ($tableOuterBorder['right'] !== '') {
504 + $wrapperStyles[] = 'border-right:' . $tableOuterBorder['right'];
505 + }
506 + if ($tableOuterBorder['bottom'] !== '') {
507 + $wrapperStyles[] = 'border-bottom:' . $tableOuterBorder['bottom'];
508 + }
509 + if ($tableOuterBorder['left'] !== '') {
510 + $wrapperStyles[] = 'border-left:' . $tableOuterBorder['left'];
511 + }
512 + $wrapperStyles[] = 'box-sizing:border-box';
355 513 }
356 514
515 + // A table wider than its wrapper must scroll inside the wrapper
516 + // instead of pushing the whole page sideways, so the wrapper is a
517 + // scroll container by default.
518 + //
519 + // The one exception is a header that sticks to the page: a scroll
520 + // container of its own would capture it and it would never stick.
521 + // (CSS cannot do both — with overflow-x set, overflow-y can no longer
522 + // be visible.) A sticky first column, on the other hand, only works
523 + // *because* of the scroll container, so when both are enabled the
524 + // scrolling wins and the header sticks within the wrapper.
525 + //
526 + // A rounded wrapper always needs the clip, so it opts in regardless.
527 + $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
528 + if ($hasRoundedCorners || !$pageStickyHeader) {
529 + // `auto`, not `hidden`: both clip to the rounded corners, but
530 + // `hidden` would also swallow a table wider than the wrapper.
531 + // Inline, so it also wins over the `.tableberg-scroll-x` rule.
532 + $wrapperStyles[] = 'overflow:auto';
533 + }
534 +
535 + $wrapperStyleAttr = !empty($wrapperStyles)
536 + ? "style='" . implode(';', $wrapperStyles) . "'"
537 + : '';
538 +
357 539 $figureAlignmentClass = '';
358 540 if ($isWideWidth) {
359 541 $figureAlignmentClass = 'alignwide';
360 542 } elseif ($isFullWidth) {
@@ -403,11 +585,11 @@
403 585 data-tableberg-sortable='$sortingBoolAsStr'
404 586 data-tableberg-columns='$columnsJson'
405 587 data-tableberg-pagination='$paginationJson'
406 588 data-tableberg-search-enabled='$searchEnabledAsStr'
407 - data-tableberg-search-placeholder='$searchPlaceholder'
408 - data-tableberg-search-position='$searchPosition'
409 - data-tableberg-search-highlight-color='$searchHighlightColor'
589 + data-tableberg-search-placeholder='$searchPlaceholderAttr'
590 + data-tableberg-search-position='$searchPositionAttr'
591 + data-tableberg-search-highlight-color='$searchHighlightColorAttr'
410 592 data-tableberg-header='$headerBoolAsStr'
411 593 data-tableberg-footer='$footerBoolAsStr'
412 594 {$responsiveDataAttrs}
413 595 >
@@ -479,8 +661,11 @@
479 661 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
480 662 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
481 663 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
482 664
665 + $tabletRepeatFirstCol = $tablet->repeatFirstCol->asAttr();
666 + $mobileRepeatFirstCol = $mobile->repeatFirstCol->asAttr();
667 +
483 668 return "
484 669 data-tableberg-responsive='true'
485 670 data-tableberg-rows='{$rows}'
486 671 data-tableberg-cols='{$cols}'
@@ -488,15 +673,15 @@
488 673 data-tableberg-tablet-width='{$tabletMaxWidth}'
489 674 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
490 675 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
491 676 data-tableberg-tablet-count='{$tabletStackCount}'
492 - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
677 + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
493 678 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
494 679 data-tableberg-mobile-width='{$mobileMaxWidth}'
495 680 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
496 681 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
497 682 data-tableberg-mobile-count='{$mobileStackCount}'
498 - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
683 + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
499 684 ";
500 685 }
501 686
502 687 private function getCell($row, $col, $cells) {
@@ -509,13 +694,31 @@
509 694 return $cells[$key];
510 695 }
511 696
512 697 private function getCellElements($cell) {
698 + if ($this->isCellEmpty($cell)) {
699 + return [];
700 + }
701 +
702 + return $cell instanceof CellData && is_array($cell->elements)
703 + ? $cell->elements
704 + : [];
705 + }
706 +
707 + /**
708 + * "Empty cell" is a pro feature: the cell renders as if it were a bare,
709 + * unstyled table cell — no content, no background, no border. Free's
710 + * default is to always show the cell as configured.
711 + *
712 + * @param mixed $cell
713 + * @return bool
714 + */
715 + private function isCellEmpty($cell) {
513 716 if (!$cell instanceof CellData) {
514 - return [];
717 + return false;
515 718 }
516 719
517 - return is_array($cell->elements) ? $cell->elements : [];
720 + return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
518 721 }
519 722
520 723 private function getCellStyleOverride($cell) {
521 724 if (!$cell instanceof CellData || !is_array($cell->styles)) {
@@ -593,6 +796,27 @@
593 796 }
594 797
595 798 $height = $rowConfig->height->asAttr();
596 799 return $height === '' ? null : $height;
800 + }
801 +
802 + /**
803 + * Pro styling of a row (e.g. background colour). One filter carries
804 + * every pro row style, mirroring `tableberg/cell_styles`, so a new pro
805 + * style needs no change here.
806 + *
807 + * @param int $row
808 + * @param array<int|string, RowConfig> $rowConfigs
809 + * @return array<string, string>
810 + */
811 + private function getRowStyles($row, $rowConfigs) {
812 + $rowAttrs = (
813 + is_array($rowConfigs) &&
814 + array_key_exists($row, $rowConfigs) &&
815 + $rowConfigs[$row] instanceof RowConfig
816 + ) ? $rowConfigs[$row]->attrs : [];
817 +
818 + $styles = apply_filters('tableberg/row_styles', [], $rowAttrs);
819 +
820 + return is_array($styles) ? $styles : [];
597 821 }
598 822 }