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 +249 -72 1.1.0 → 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 === '') {
@@ -65,9 +52,8 @@
65 52 );
66 53 }
67 54
68 55 $attrs = TableAttrs::from_array($attributes);
69 - $isProActive = $this->is_pro_active();
70 56
71 57 $rows = $attrs->table->rows->value();
72 58 $cols = $attrs->table->cols->value();
73 59
@@ -72,10 +58,30 @@
72 58 $cols = $attrs->table->cols->value();
73 59
74 60 $headerEnabled = $attrs->table->headerEnabled->value();
75 61 $footerEnabled = $attrs->table->footerEnabled->value();
76 - $stickyHeader = $attrs->table->stickyHeader->value();
77 - $stickyFirstCol = $attrs->table->stickyFirstCol->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 +
78 84 $caption = $attrs->table->caption;
79 85 $tableWidth = trim($attrs->table->tableWidth->asAttr());
80 86 $tableAlignment = $attrs->table->tableAlignment->asAttr();
81 87 $cellSpacingHorizontal = trim($attrs->table->cellSpacing->horizontal->asAttr());
@@ -84,8 +90,24 @@
84 90 $tableBorderRight = trim($attrs->table->tableBorder->right->asAttr());
85 91 $tableBorderBottom = trim($attrs->table->tableBorder->bottom->asAttr());
86 92 $tableBorderLeft = trim($attrs->table->tableBorder->left->asAttr());
87 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;
88 110
89 111 $isHorizontalSpacingZero = $cellSpacingHorizontal === '0';
90 112 $isVerticalSpacingZero = $cellSpacingVertical === '0';
91 113 $hasCellSpacing = !$isHorizontalSpacingZero || !$isVerticalSpacingZero;
@@ -109,11 +131,19 @@
109 131 $canApplyCustomWidth,
110 132 $tableAlignment
111 133 );
112 134
113 - $tableStyles = [
114 - 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
115 - ];
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 + );
116 146
117 147 if ($hasCellSpacing) {
118 148 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
119 149 }
@@ -124,21 +154,21 @@
124 154 } else {
125 155 $tableStyles[] = 'width: 100%';
126 156 }
127 157
128 - if ($tableBorderTop !== '') {
158 + if (!$hasRoundedCorners && $tableBorderTop !== '') {
129 159 $tableStyles[] = "border-top: {$tableBorderTop}";
130 160 }
131 161
132 - if ($tableBorderRight !== '') {
162 + if (!$hasRoundedCorners && $tableBorderRight !== '') {
133 163 $tableStyles[] = "border-right: {$tableBorderRight}";
134 164 }
135 165
136 - if ($tableBorderBottom !== '') {
166 + if (!$hasRoundedCorners && $tableBorderBottom !== '') {
137 167 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
138 168 }
139 169
140 - if ($tableBorderLeft !== '') {
170 + if (!$hasRoundedCorners && $tableBorderLeft !== '') {
141 171 $tableStyles[] = "border-left: {$tableBorderLeft}";
142 172 }
143 173
144 174 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
@@ -174,18 +204,18 @@
174 204 }
175 205
176 206 $tableClassAttr = implode(' ', $tableClasses);
177 207
178 - $sortableColumns = [];
179 -
180 - if ($isProActive) {
181 - foreach ($attrs->columns as $column => $columnConfig) {
182 - if ($columnConfig->sortable === null) {
183 - continue;
184 - }
185 -
186 - $sortableColumns[$column] = $columnConfig->sortable->asAttr();
187 - }
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 = [];
188 218 }
189 219
190 220 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
191 221 if ($paginationPageSize < 1) {
@@ -192,28 +222,39 @@
192 222 $paginationPageSize = 1;
193 223 }
194 224
195 225 $paginationConfig = [
196 - 'enabled' => $isProActive && $attrs->table->pagination->enabled->value(),
226 + 'enabled' => apply_filters('tableberg/pagination_enabled', false, $attrs->attrs),
197 227 'pageSize' => $paginationPageSize,
198 228 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
199 229 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
200 230 ];
201 231
202 - $searchEnabledAsStr = (
203 - $isProActive && $attrs->table->search->enabled->value()
204 - ) ? 'true' : 'false';
205 - $searchPlaceholder = $isProActive
206 - ? $attrs->table->search->placeholder->asAttr()
207 - : '';
208 - $searchPosition = $isProActive
209 - ? $attrs->table->search->position->asAttr()
210 - : 'left';
211 - $searchHighlightColor = $isProActive
212 - ? $attrs->table->search->highlightColor->asAttr()
213 - : '';
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'] ?? '');
214 246 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
215 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 +
216 257 $globalCellStyles = [
217 258 'padding' => [
218 259 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
219 260 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
@@ -219,11 +260,11 @@
219 260 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
220 261 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
221 262 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
222 263 ],
223 - 'orientation' => $attrs->cellDefaults->styles->orientation->asAttr(),
264 + 'orientation' => $cellLayout['orientation'] === 'horizontal' ? 'horizontal' : 'vertical',
224 265 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
225 - 'wrap' => $attrs->cellDefaults->styles->wrap->asAttr(),
266 + 'wrap' => $cellLayout['wrap'] === 'wrap' ? 'wrap' : 'nowrap',
226 267 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
227 268 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
228 269 'border' => [
229 270 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
@@ -238,13 +279,18 @@
238 279 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
239 280 ],
240 281 ];
241 282
242 - // The table border radius rounds the whole table's outer corners.
243 - // border-radius on collapsed-border tables/cells is ignored by
244 - // browsers, so it is rendered on the wrapper (with overflow:hidden)
245 - // and removed from the individual cells here.
246 - $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 + ];
247 293 $globalCellStyles['borderRadius'] = [
248 294 'topLeft' => '',
249 295 'topRight' => '',
250 296 'bottomRight' => '',
@@ -258,9 +304,47 @@
258 304
259 305 for ($row = 0; $row < $rows; $row++) {
260 306 $cellsHtml = '';
261 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 + : '';
262 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 +
263 347 for ($col = 0; $col < $cols; $col++) {
264 348 if (isset($hiddenBySpan[$row][$col])) {
265 349 continue;
266 350 }
@@ -313,9 +397,9 @@
313 397 $col,
314 398 $rowSpan,
315 399 $colSpan,
316 400 $tag,
317 - $globalCellStyles,
401 + $cellStylesForRow,
318 402 $this->getCellElements($cell),
319 403 $this->getCellStyleOverride($cell),
320 404 $this->getCellRibbon($cell),
321 405 $sortableType,
@@ -322,14 +406,49 @@
322 406 $columnWidth,
323 407 $rowHeight,
324 408 $stickyHeader,
325 409 $stickyFirstCol,
326 - $this->getCellClassName($cell)
410 + $this->getCellClassName($cell),
411 + $cell instanceof CellData ? $cell->attrs : [],
412 + $this->isCellEmpty($cell),
413 + $innerBorderType,
414 + $rows,
415 + $cols
327 416 )
328 417 );
329 418 }
330 419
331 - $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>";
332 451 }
333 452
334 453 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
335 454 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
@@ -341,27 +460,29 @@
341 460 'sortable' => $sortType,
342 461 ];
343 462 }
344 463
345 - $columnsJson = json_encode($columnsData);
464 + $columnsJson = wp_json_encode($columnsData);
346 465 if (!is_string($columnsJson)) {
347 466 $columnsJson = '{}';
348 467 }
468 + $columnsJson = esc_attr($columnsJson);
349 469
350 - $paginationJson = json_encode($paginationConfig);
470 + $paginationJson = wp_json_encode($paginationConfig);
351 471 if (!is_string($paginationJson)) {
352 472 $paginationJson = '{}';
353 473 }
474 + $paginationJson = esc_attr($paginationJson);
354 475
476 + $searchPlaceholderAttr = esc_attr($searchPlaceholder);
477 + $searchPositionAttr = esc_attr($searchPosition);
478 + $searchHighlightColorAttr = esc_attr($searchHighlightColor);
479 +
355 480 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
356 481
357 - // Round the whole table's outer corners by clipping the wrapper.
482 + // Round the whole table's outer corners on the wrapper.
358 483 // Zero radii are skipped so a table without any actual rounding does
359 484 // not carry pointless border-radius declarations.
360 - $isZeroRadius = function ($value) {
361 - $trimmed = trim((string) $value);
362 - return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1;
363 - };
364 485 $wrapperStyles = [];
365 486 if (!$isZeroRadius($tableRadius['topLeft'])) {
366 487 $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
367 488 }
@@ -374,8 +495,24 @@
374 495 if (!$isZeroRadius($tableRadius['bottomLeft'])) {
375 496 $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
376 497 }
377 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 +
378 515 // A table wider than its wrapper must scroll inside the wrapper
379 516 // instead of pushing the whole page sideways, so the wrapper is a
380 517 // scroll container by default.
381 518 //
@@ -387,10 +524,8 @@
387 524 // scrolling wins and the header sticks within the wrapper.
388 525 //
389 526 // A rounded wrapper always needs the clip, so it opts in regardless.
390 527 $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
391 - $hasRoundedCorners = !empty($wrapperStyles);
392 -
393 528 if ($hasRoundedCorners || !$pageStickyHeader) {
394 529 // `auto`, not `hidden`: both clip to the rounded corners, but
395 530 // `hidden` would also swallow a table wider than the wrapper.
396 531 // Inline, so it also wins over the `.tableberg-scroll-x` rule.
@@ -450,11 +585,11 @@
450 585 data-tableberg-sortable='$sortingBoolAsStr'
451 586 data-tableberg-columns='$columnsJson'
452 587 data-tableberg-pagination='$paginationJson'
453 588 data-tableberg-search-enabled='$searchEnabledAsStr'
454 - data-tableberg-search-placeholder='$searchPlaceholder'
455 - data-tableberg-search-position='$searchPosition'
456 - 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'
457 592 data-tableberg-header='$headerBoolAsStr'
458 593 data-tableberg-footer='$footerBoolAsStr'
459 594 {$responsiveDataAttrs}
460 595 >
@@ -526,8 +661,11 @@
526 661 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
527 662 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
528 663 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
529 664
665 + $tabletRepeatFirstCol = $tablet->repeatFirstCol->asAttr();
666 + $mobileRepeatFirstCol = $mobile->repeatFirstCol->asAttr();
667 +
530 668 return "
531 669 data-tableberg-responsive='true'
532 670 data-tableberg-rows='{$rows}'
533 671 data-tableberg-cols='{$cols}'
@@ -535,15 +673,15 @@
535 673 data-tableberg-tablet-width='{$tabletMaxWidth}'
536 674 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
537 675 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
538 676 data-tableberg-tablet-count='{$tabletStackCount}'
539 - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
677 + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
540 678 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
541 679 data-tableberg-mobile-width='{$mobileMaxWidth}'
542 680 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
543 681 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
544 682 data-tableberg-mobile-count='{$mobileStackCount}'
545 - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
683 + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
546 684 ";
547 685 }
548 686
549 687 private function getCell($row, $col, $cells) {
@@ -556,13 +694,31 @@
556 694 return $cells[$key];
557 695 }
558 696
559 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) {
560 716 if (!$cell instanceof CellData) {
561 - return [];
717 + return false;
562 718 }
563 719
564 - return is_array($cell->elements) ? $cell->elements : [];
720 + return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
565 721 }
566 722
567 723 private function getCellStyleOverride($cell) {
568 724 if (!$cell instanceof CellData || !is_array($cell->styles)) {
@@ -640,6 +796,27 @@
640 796 }
641 797
642 798 $height = $rowConfig->height->asAttr();
643 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 : [];
644 821 }
645 822 }