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 +338 -62 1.0.2 → 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,8 +279,25 @@
219 279 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
220 280 ],
221 281 ];
222 282
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 + ];
293 + $globalCellStyles['borderRadius'] = [
294 + 'topLeft' => '',
295 + 'topRight' => '',
296 + 'bottomRight' => '',
297 + 'bottomLeft' => '',
298 + ];
299 +
223 300 $cellRenderer = new CellRenderer();
224 301 $hiddenBySpan = [];
225 302
226 303 $rowsHtml = '';
@@ -227,9 +304,47 @@
227 304
228 305 for ($row = 0; $row < $rows; $row++) {
229 306 $cellsHtml = '';
230 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 + : '';
231 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 +
232 347 for ($col = 0; $col < $cols; $col++) {
233 348 if (isset($hiddenBySpan[$row][$col])) {
234 349 continue;
235 350 }
@@ -282,9 +397,9 @@
282 397 $col,
283 398 $rowSpan,
284 399 $colSpan,
285 400 $tag,
286 - $globalCellStyles,
401 + $cellStylesForRow,
287 402 $this->getCellElements($cell),
288 403 $this->getCellStyleOverride($cell),
289 404 $this->getCellRibbon($cell),
290 405 $sortableType,
@@ -290,14 +405,50 @@
290 405 $sortableType,
291 406 $columnWidth,
292 407 $rowHeight,
293 408 $stickyHeader,
294 - $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
295 416 )
296 417 );
297 418 }
298 419
299 - $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>";
300 451 }
301 452
302 453 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
303 454 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
@@ -309,20 +460,83 @@
309 460 'sortable' => $sortType,
310 461 ];
311 462 }
312 463
313 - $columnsJson = json_encode($columnsData);
464 + $columnsJson = wp_json_encode($columnsData);
314 465 if (!is_string($columnsJson)) {
315 466 $columnsJson = '{}';
316 467 }
468 + $columnsJson = esc_attr($columnsJson);
317 469
318 - $paginationJson = json_encode($paginationConfig);
470 + $paginationJson = wp_json_encode($paginationConfig);
319 471 if (!is_string($paginationJson)) {
320 472 $paginationJson = '{}';
321 473 }
474 + $paginationJson = esc_attr($paginationJson);
322 475
476 + $searchPlaceholderAttr = esc_attr($searchPlaceholder);
477 + $searchPositionAttr = esc_attr($searchPosition);
478 + $searchHighlightColorAttr = esc_attr($searchHighlightColor);
479 +
323 480 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
324 481
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'];
488 + }
489 + if (!$isZeroRadius($tableRadius['topRight'])) {
490 + $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
491 + }
492 + if (!$isZeroRadius($tableRadius['bottomRight'])) {
493 + $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
494 + }
495 + if (!$isZeroRadius($tableRadius['bottomLeft'])) {
496 + $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
497 + }
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';
513 + }
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 +
325 539 $figureAlignmentClass = '';
326 540 if ($isWideWidth) {
327 541 $figureAlignmentClass = 'alignwide';
328 542 } elseif ($isFullWidth) {
@@ -336,8 +550,28 @@
336 550 $figureAlignmentClass,
337 551 ]))
338 552 );
339 553
554 + $figureStyles = [];
555 + $spacingSides = [
556 + 'margin-top' => $attrs->table->margin->top,
557 + 'margin-right' => $attrs->table->margin->right,
558 + 'margin-bottom' => $attrs->table->margin->bottom,
559 + 'margin-left' => $attrs->table->margin->left,
560 + 'padding-top' => $attrs->table->padding->top,
561 + 'padding-right' => $attrs->table->padding->right,
562 + 'padding-bottom' => $attrs->table->padding->bottom,
563 + 'padding-left' => $attrs->table->padding->left,
564 + ];
565 + foreach ($spacingSides as $prop => $sideAttr) {
566 + if ($sideAttr->isNotEmpty()) {
567 + $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
568 + }
569 + }
570 + $figureStyleAttr = !empty($figureStyles)
571 + ? "style='" . implode('; ', $figureStyles) . ";'"
572 + : '';
573 +
340 574 $captionHtml = '';
341 575 if ($caption->isNotEmpty()) {
342 576 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
343 577 }
@@ -342,10 +576,10 @@
342 576 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
343 577 }
344 578
345 579 $html =
346 - "<figure class='{$figureClass}'>
347 - <div class='{$wrapperClass}'>
580 + "<figure class='{$figureClass}' {$figureStyleAttr}>
581 + <div class='{$wrapperClass}' {$wrapperStyleAttr}>
348 582 <table
349 583 class='{$tableClassAttr}'
350 584 {$tableStyleAttr}
351 585 data-tableberg-sortable='$sortingBoolAsStr'
@@ -351,11 +585,11 @@
351 585 data-tableberg-sortable='$sortingBoolAsStr'
352 586 data-tableberg-columns='$columnsJson'
353 587 data-tableberg-pagination='$paginationJson'
354 588 data-tableberg-search-enabled='$searchEnabledAsStr'
355 - data-tableberg-search-placeholder='$searchPlaceholder'
356 - data-tableberg-search-position='$searchPosition'
357 - 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'
358 592 data-tableberg-header='$headerBoolAsStr'
359 593 data-tableberg-footer='$footerBoolAsStr'
360 594 {$responsiveDataAttrs}
361 595 >
@@ -427,8 +661,11 @@
427 661 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
428 662 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
429 663 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
430 664
665 + $tabletRepeatFirstCol = $tablet->repeatFirstCol->asAttr();
666 + $mobileRepeatFirstCol = $mobile->repeatFirstCol->asAttr();
667 +
431 668 return "
432 669 data-tableberg-responsive='true'
433 670 data-tableberg-rows='{$rows}'
434 671 data-tableberg-cols='{$cols}'
@@ -436,15 +673,15 @@
436 673 data-tableberg-tablet-width='{$tabletMaxWidth}'
437 674 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
438 675 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
439 676 data-tableberg-tablet-count='{$tabletStackCount}'
440 - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
677 + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
441 678 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
442 679 data-tableberg-mobile-width='{$mobileMaxWidth}'
443 680 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
444 681 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
445 682 data-tableberg-mobile-count='{$mobileStackCount}'
446 - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
683 + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
447 684 ";
448 685 }
449 686
450 687 private function getCell($row, $col, $cells) {
@@ -457,13 +694,31 @@
457 694 return $cells[$key];
458 695 }
459 696
460 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) {
461 716 if (!$cell instanceof CellData) {
462 - return [];
717 + return false;
463 718 }
464 719
465 - return is_array($cell->elements) ? $cell->elements : [];
720 + return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
466 721 }
467 722
468 723 private function getCellStyleOverride($cell) {
469 724 if (!$cell instanceof CellData || !is_array($cell->styles)) {
@@ -541,6 +796,27 @@
541 796 }
542 797
543 798 $height = $rowConfig->height->asAttr();
544 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 : [];
545 821 }
546 822 }