PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.2
Tableberg – Simple Gutenberg Table Block v1.1.2
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
tableberg / renderer / Table / TableRenderer.php

TableRenderer.php in Tableberg – Simple Gutenberg Table Block 1.1.2, at renderer/Table/TableRenderer.php

729 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Tableberg\Renderer\Table;
4
5 use Tableberg\Renderer\Cell\CellRenderer;
6 use Tableberg\Renderer\Cell\CellRenderContext;
7 use Tableberg\Renderer\Attrs\StringAttr;
8
9 class TableRenderer {
10 private function has_visible_border($border) {
11 $trimmed_border = trim($border);
12
13 if ($trimmed_border === '') {
14 return false;
15 }
16
17 $parts = preg_split('/\s+/', $trimmed_border, 3);
18 $width = $parts[0] ?? '';
19 $style = $parts[1] ?? '';
20
21 if ($width === 'none' || $width === 'hidden') {
22 return false;
23 }
24
25 if (preg_match('/^0(?:\.0+)?(?:[a-z%]+)?$/i', $width) === 1) {
26 return false;
27 }
28
29 if ($style === 'none' || $style === 'hidden') {
30 return false;
31 }
32
33 return true;
34 }
35
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
55 $attrs = TableAttrs::from_array($attributes);
56
57 $rows = $attrs->table->rows->value();
58 $cols = $attrs->table->cols->value();
59
60 $headerEnabled = $attrs->table->headerEnabled->value();
61 $footerEnabled = $attrs->table->footerEnabled->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 $caption = $attrs->table->caption;
74 $tableWidth = trim($attrs->table->tableWidth->asAttr());
75 $tableAlignment = $attrs->table->tableAlignment->asAttr();
76 $cellSpacingHorizontal = trim($attrs->table->cellSpacing->horizontal->asAttr());
77 $cellSpacingVertical = trim($attrs->table->cellSpacing->vertical->asAttr());
78 $tableBorderTop = trim($attrs->table->tableBorder->top->asAttr());
79 $tableBorderRight = trim($attrs->table->tableBorder->right->asAttr());
80 $tableBorderBottom = trim($attrs->table->tableBorder->bottom->asAttr());
81 $tableBorderLeft = trim($attrs->table->tableBorder->left->asAttr());
82 $fixedColumnWidths = $attrs->table->fixedColumnWidths->value();
83
84 $isHorizontalSpacingZero = $cellSpacingHorizontal === '0';
85 $isVerticalSpacingZero = $cellSpacingVertical === '0';
86 $hasCellSpacing = !$isHorizontalSpacingZero || !$isVerticalSpacingZero;
87
88 $isWideWidth = $tableWidth === 'wide';
89 $isFullWidth = $tableWidth === 'full';
90
91 $canApplyCustomWidth =
92 $tableWidth !== '' &&
93 !$isWideWidth &&
94 !$isFullWidth &&
95 $tableWidth !== 'auto';
96
97 if (!$canApplyCustomWidth) {
98 $tableWidth = '';
99 }
100
101 $wrapperAlignmentClass = $this->getWrapperAlignmentClass(
102 $isWideWidth,
103 $isFullWidth,
104 $canApplyCustomWidth,
105 $tableAlignment
106 );
107
108 $tableStyles = [
109 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
110 ];
111
112 if ($hasCellSpacing) {
113 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
114 }
115
116 if ($tableWidth !== '') {
117 $tableStyles[] = "width: {$tableWidth}";
118 $tableStyles[] = "max-width: {$tableWidth}";
119 } else {
120 $tableStyles[] = 'width: 100%';
121 }
122
123 if ($tableBorderTop !== '') {
124 $tableStyles[] = "border-top: {$tableBorderTop}";
125 }
126
127 if ($tableBorderRight !== '') {
128 $tableStyles[] = "border-right: {$tableBorderRight}";
129 }
130
131 if ($tableBorderBottom !== '') {
132 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
133 }
134
135 if ($tableBorderLeft !== '') {
136 $tableStyles[] = "border-left: {$tableBorderLeft}";
137 }
138
139 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
140
141 $tableClasses = ['wp-block-tableberg'];
142
143 if ($this->has_visible_border($tableBorderTop)) {
144 $tableClasses[] = 'tableberg-has-table-border-top';
145 }
146
147 if ($this->has_visible_border($tableBorderRight)) {
148 $tableClasses[] = 'tableberg-has-table-border-right';
149 }
150
151 if ($this->has_visible_border($tableBorderBottom)) {
152 $tableClasses[] = 'tableberg-has-table-border-bottom';
153 }
154
155 if ($this->has_visible_border($tableBorderLeft)) {
156 $tableClasses[] = 'tableberg-has-table-border-left';
157 }
158
159 if ($hasCellSpacing) {
160 $tableClasses[] = 'tableberg-has-cell-spacing';
161
162 if ($isHorizontalSpacingZero) {
163 $tableClasses[] = 'tableberg-cell-spacing-horizontal-zero';
164 }
165
166 if ($isVerticalSpacingZero) {
167 $tableClasses[] = 'tableberg-cell-spacing-vertical-zero';
168 }
169 }
170
171 $tableClassAttr = implode(' ', $tableClasses);
172
173 // Column sorting is a pro feature: free never marks a column
174 // sortable on its own, and pro reads the table's raw columns to
175 // decide which ones are.
176 $sortableColumns = apply_filters(
177 'tableberg/sortable_columns',
178 [],
179 $attrs->attrs
180 );
181 if (!is_array($sortableColumns)) {
182 $sortableColumns = [];
183 }
184
185 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
186 if ($paginationPageSize < 1) {
187 $paginationPageSize = 1;
188 }
189
190 $paginationConfig = [
191 'enabled' => apply_filters('tableberg/pagination_enabled', false, $attrs->attrs),
192 'pageSize' => $paginationPageSize,
193 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
194 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
195 ];
196
197 $searchSettings = apply_filters(
198 'tableberg/table_search_settings',
199 [
200 'enabled' => false,
201 'placeholder' => '',
202 'position' => 'left',
203 'highlightColor' => '',
204 ],
205 $attrs->attrs
206 );
207 $searchEnabledAsStr = !empty($searchSettings['enabled']) ? 'true' : 'false';
208 $searchPlaceholder = (string) ($searchSettings['placeholder'] ?? '');
209 $searchPosition = (string) ($searchSettings['position'] ?? 'left');
210 $searchHighlightColor = (string) ($searchSettings['highlightColor'] ?? '');
211 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
212
213 // Horizontal cell-element layout (and the wrap toggle that only
214 // matters with it) is a pro feature; free's default is always the
215 // vertical stack. Grouped in one filter since they're one decision.
216 $cellLayout = apply_filters(
217 'tableberg/cell_default_layout',
218 ['orientation' => 'vertical', 'wrap' => 'nowrap'],
219 $attrs->attrs
220 );
221
222 $globalCellStyles = [
223 'padding' => [
224 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
225 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
226 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
227 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
228 ],
229 'orientation' => $cellLayout['orientation'] === 'horizontal' ? 'horizontal' : 'vertical',
230 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
231 'wrap' => $cellLayout['wrap'] === 'wrap' ? 'wrap' : 'nowrap',
232 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
233 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
234 'border' => [
235 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
236 'right' => $attrs->cellDefaults->styles->border->right->asAttr(),
237 'bottom' => $attrs->cellDefaults->styles->border->bottom->asAttr(),
238 'left' => $attrs->cellDefaults->styles->border->left->asAttr(),
239 ],
240 'borderRadius' => [
241 'topLeft' => $attrs->cellDefaults->styles->borderRadius->topLeft->asAttr(),
242 'topRight' => $attrs->cellDefaults->styles->borderRadius->topRight->asAttr(),
243 'bottomRight' => $attrs->cellDefaults->styles->borderRadius->bottomRight->asAttr(),
244 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
245 ],
246 ];
247
248 // The table border radius rounds the whole table's outer corners.
249 // border-radius on collapsed-border tables/cells is ignored by
250 // browsers, so it is rendered on the wrapper (with overflow:hidden)
251 // and removed from the individual cells here.
252 $tableRadius = $globalCellStyles['borderRadius'];
253 $globalCellStyles['borderRadius'] = [
254 'topLeft' => '',
255 'topRight' => '',
256 'bottomRight' => '',
257 'bottomLeft' => '',
258 ];
259
260 $cellRenderer = new CellRenderer();
261 $hiddenBySpan = [];
262
263 $rowsHtml = '';
264
265 for ($row = 0; $row < $rows; $row++) {
266 $cellsHtml = '';
267 $rowHeight = $this->getRowHeight($row, $attrs->rows);
268 $rowStyles = $this->getRowStyles($row, $attrs->rows);
269 $rowBackgroundColor = isset($rowStyles['backgroundColor']) && is_string($rowStyles['backgroundColor'])
270 ? $rowStyles['backgroundColor']
271 : '';
272
273 // A row with its own background would otherwise never show
274 // through: every cell paints its own background over the
275 // `<tr>`'s, and cells fall back to the table-wide default
276 // whenever they carry no colour of their own. So for this row's
277 // cells, that fallback is cleared — a cell/column colour on top
278 // of it (applied below by the pro filter) still wins either way.
279 $cellStylesForRow = $globalCellStyles;
280 if ($rowBackgroundColor !== '') {
281 $cellStylesForRow['backgroundColor'] = '';
282 }
283
284 for ($col = 0; $col < $cols; $col++) {
285 if (isset($hiddenBySpan[$row][$col])) {
286 continue;
287 }
288
289 $cell = $this->getCell($row, $col, $attrs->cells);
290 $span = $this->getCellSpan($cell);
291
292 $rowSpan = is_numeric($span['rowSpan']) ? (int) $span['rowSpan'] : 1;
293 $colSpan = is_numeric($span['colSpan']) ? (int) $span['colSpan'] : 1;
294
295 if ($rowSpan > 1 || $colSpan > 1) {
296 for ($rowOffset = 0; $rowOffset < $rowSpan; $rowOffset++) {
297 for ($colOffset = 0; $colOffset < $colSpan; $colOffset++) {
298 if ($rowOffset === 0 && $colOffset === 0) {
299 continue;
300 }
301
302 $hiddenRow = $row + $rowOffset;
303 $hiddenCol = $col + $colOffset;
304 $hiddenBySpan[$hiddenRow][$hiddenCol] = true;
305 }
306 }
307 }
308
309 $isHeaderRowCell = $headerEnabled && $row === 0;
310 $isFooterCell = $footerEnabled && $rows > 0 && $row === $rows - 1;
311
312 $tag = ($isHeaderRowCell || $isFooterCell) ? 'th' : 'td';
313
314 $sortableType = (
315 $isHeaderRowCell &&
316 array_key_exists($col, $sortableColumns)
317 ) ? $sortableColumns[$col] : null;
318
319 $columnWidth = null;
320 if ($colSpan === 1) {
321 if ($fixedColumnWidths && $cols > 0) {
322 $columnWidth = (string) (100 / $cols) . '%';
323 } else {
324 $columnWidth = $this->getColumnWidth(
325 $col,
326 $attrs->columns
327 );
328 }
329 }
330
331 $cellsHtml .= $cellRenderer->render(
332 CellRenderContext::create(
333 $row,
334 $col,
335 $rowSpan,
336 $colSpan,
337 $tag,
338 $cellStylesForRow,
339 $this->getCellElements($cell),
340 $this->getCellStyleOverride($cell),
341 $this->getCellRibbon($cell),
342 $sortableType,
343 $columnWidth,
344 $rowHeight,
345 $stickyHeader,
346 $stickyFirstCol,
347 $this->getCellClassName($cell),
348 $cell instanceof CellData ? $cell->attrs : [],
349 $this->isCellEmpty($cell)
350 )
351 );
352 }
353
354 $rowStyleParts = [];
355 if ($rowBackgroundColor !== '') {
356 $rowStyleParts[] = 'background-color:' . esc_attr($rowBackgroundColor);
357 }
358
359 $rowBorder = isset($rowStyles['border']) && is_array($rowStyles['border'])
360 ? $rowStyles['border']
361 : [];
362 foreach (['top', 'right', 'bottom', 'left'] as $side) {
363 $value = isset($rowBorder[$side]) && is_string($rowBorder[$side])
364 ? $rowBorder[$side]
365 : '';
366 if ($value !== '') {
367 $rowStyleParts[] = "border-{$side}:" . esc_attr($value);
368 }
369 }
370
371 $rowStyleAttr = !empty($rowStyleParts)
372 ? ' style="' . implode(';', $rowStyleParts) . '"'
373 : '';
374
375 $rowsHtml .= "<tr data-tableberg-row='{$row}'$rowStyleAttr>$cellsHtml</tr>";
376 }
377
378 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
379 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
380 $footerBoolAsStr = $footerEnabled ? 'true' : 'false';
381
382 $columnsData = [];
383 foreach ($sortableColumns as $column => $sortType) {
384 $columnsData[$column] = [
385 'sortable' => $sortType,
386 ];
387 }
388
389 $columnsJson = json_encode($columnsData);
390 if (!is_string($columnsJson)) {
391 $columnsJson = '{}';
392 }
393
394 $paginationJson = json_encode($paginationConfig);
395 if (!is_string($paginationJson)) {
396 $paginationJson = '{}';
397 }
398
399 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
400
401 // Round the whole table's outer corners by clipping the wrapper.
402 // Zero radii are skipped so a table without any actual rounding does
403 // not carry pointless border-radius declarations.
404 $isZeroRadius = function ($value) {
405 $trimmed = trim((string) $value);
406 return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1;
407 };
408 $wrapperStyles = [];
409 if (!$isZeroRadius($tableRadius['topLeft'])) {
410 $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
411 }
412 if (!$isZeroRadius($tableRadius['topRight'])) {
413 $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
414 }
415 if (!$isZeroRadius($tableRadius['bottomRight'])) {
416 $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
417 }
418 if (!$isZeroRadius($tableRadius['bottomLeft'])) {
419 $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
420 }
421
422 // A table wider than its wrapper must scroll inside the wrapper
423 // instead of pushing the whole page sideways, so the wrapper is a
424 // scroll container by default.
425 //
426 // The one exception is a header that sticks to the page: a scroll
427 // container of its own would capture it and it would never stick.
428 // (CSS cannot do both — with overflow-x set, overflow-y can no longer
429 // be visible.) A sticky first column, on the other hand, only works
430 // *because* of the scroll container, so when both are enabled the
431 // scrolling wins and the header sticks within the wrapper.
432 //
433 // A rounded wrapper always needs the clip, so it opts in regardless.
434 $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
435 $hasRoundedCorners = !empty($wrapperStyles);
436
437 if ($hasRoundedCorners || !$pageStickyHeader) {
438 // `auto`, not `hidden`: both clip to the rounded corners, but
439 // `hidden` would also swallow a table wider than the wrapper.
440 // Inline, so it also wins over the `.tableberg-scroll-x` rule.
441 $wrapperStyles[] = 'overflow:auto';
442 }
443
444 $wrapperStyleAttr = !empty($wrapperStyles)
445 ? "style='" . implode(';', $wrapperStyles) . "'"
446 : '';
447
448 $figureAlignmentClass = '';
449 if ($isWideWidth) {
450 $figureAlignmentClass = 'alignwide';
451 } elseif ($isFullWidth) {
452 $figureAlignmentClass = 'alignfull';
453 }
454
455 $figureClass = trim(
456 implode(' ', array_filter([
457 'wp-block-tableberg',
458 $attrs->table->className->asAttr(),
459 $figureAlignmentClass,
460 ]))
461 );
462
463 $figureStyles = [];
464 $spacingSides = [
465 'margin-top' => $attrs->table->margin->top,
466 'margin-right' => $attrs->table->margin->right,
467 'margin-bottom' => $attrs->table->margin->bottom,
468 'margin-left' => $attrs->table->margin->left,
469 'padding-top' => $attrs->table->padding->top,
470 'padding-right' => $attrs->table->padding->right,
471 'padding-bottom' => $attrs->table->padding->bottom,
472 'padding-left' => $attrs->table->padding->left,
473 ];
474 foreach ($spacingSides as $prop => $sideAttr) {
475 if ($sideAttr->isNotEmpty()) {
476 $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
477 }
478 }
479 $figureStyleAttr = !empty($figureStyles)
480 ? "style='" . implode('; ', $figureStyles) . ";'"
481 : '';
482
483 $captionHtml = '';
484 if ($caption->isNotEmpty()) {
485 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
486 }
487
488 $html =
489 "<figure class='{$figureClass}' {$figureStyleAttr}>
490 <div class='{$wrapperClass}' {$wrapperStyleAttr}>
491 <table
492 class='{$tableClassAttr}'
493 {$tableStyleAttr}
494 data-tableberg-sortable='$sortingBoolAsStr'
495 data-tableberg-columns='$columnsJson'
496 data-tableberg-pagination='$paginationJson'
497 data-tableberg-search-enabled='$searchEnabledAsStr'
498 data-tableberg-search-placeholder='$searchPlaceholder'
499 data-tableberg-search-position='$searchPosition'
500 data-tableberg-search-highlight-color='$searchHighlightColor'
501 data-tableberg-header='$headerBoolAsStr'
502 data-tableberg-footer='$footerBoolAsStr'
503 {$responsiveDataAttrs}
504 >
505 <tbody>
506 {$rowsHtml}
507 </tbody>
508 </table>
509 </div>
510 {$captionHtml}
511 </figure>";
512
513 return $html;
514 }
515
516 /**
517 * @param bool $isWideWidth
518 * @param bool $isFullWidth
519 * @param bool $canApplyCustomWidth
520 * @param string $alignment
521 * @return string
522 */
523 private function getWrapperAlignmentClass(
524 $isWideWidth,
525 $isFullWidth,
526 $canApplyCustomWidth,
527 $alignment
528 ) {
529 if ($isWideWidth) {
530 return 'alignwide';
531 }
532
533 if ($isFullWidth) {
534 return 'alignfull';
535 }
536
537 if (!$canApplyCustomWidth) {
538 return '';
539 }
540
541 if ($alignment === 'left' || $alignment === 'right') {
542 return 'justify-table-' . $alignment;
543 }
544
545 if ($alignment === 'center') {
546 return 'justify-table-center';
547 }
548
549 return '';
550 }
551
552 /**
553 * @param TableAttrs $attrs
554 * @param int $rows
555 * @param int $cols
556 * @return string
557 */
558 private function buildResponsiveDataAttrs($attrs, $rows, $cols) {
559 $tablet = $attrs->table->responsive->tablet;
560 $mobile = $attrs->table->responsive->mobile;
561
562 $tabletEnabled = $tablet->enabled->value();
563 $mobileEnabled = $mobile->enabled->value();
564
565 if (!$tabletEnabled && !$mobileEnabled) {
566 return '';
567 }
568
569 $tabletMaxWidth = max(1, (int) $tablet->maxWidth->value());
570 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
571 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
572 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
573
574 return "
575 data-tableberg-responsive='true'
576 data-tableberg-rows='{$rows}'
577 data-tableberg-cols='{$cols}'
578 data-tableberg-tablet-enabled='{$tablet->enabled->asAttr()}'
579 data-tableberg-tablet-width='{$tabletMaxWidth}'
580 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
581 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
582 data-tableberg-tablet-count='{$tabletStackCount}'
583 data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
584 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
585 data-tableberg-mobile-width='{$mobileMaxWidth}'
586 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
587 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
588 data-tableberg-mobile-count='{$mobileStackCount}'
589 data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
590 ";
591 }
592
593 private function getCell($row, $col, $cells) {
594 $key = $row . ',' . $col;
595
596 if (!is_array($cells) || !array_key_exists($key, $cells)) {
597 return null;
598 }
599
600 return $cells[$key];
601 }
602
603 private function getCellElements($cell) {
604 if ($this->isCellEmpty($cell)) {
605 return [];
606 }
607
608 return $cell instanceof CellData && is_array($cell->elements)
609 ? $cell->elements
610 : [];
611 }
612
613 /**
614 * "Empty cell" is a pro feature: the cell renders as if it were a bare,
615 * unstyled table cell — no content, no background, no border. Free's
616 * default is to always show the cell as configured.
617 *
618 * @param mixed $cell
619 * @return bool
620 */
621 private function isCellEmpty($cell) {
622 if (!$cell instanceof CellData) {
623 return false;
624 }
625
626 return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
627 }
628
629 private function getCellStyleOverride($cell) {
630 if (!$cell instanceof CellData || !is_array($cell->styles)) {
631 return null;
632 }
633
634 return StringAttr::fromNestedArray($cell->styles);
635 }
636
637 private function getCellRibbon($cell) {
638 if (!$cell instanceof CellData || !is_array($cell->ribbon)) {
639 return null;
640 }
641
642 return $cell->ribbon;
643 }
644
645 /**
646 * @param CellData|null $cell
647 * @return string|null
648 */
649 private function getCellClassName($cell) {
650 if (!$cell instanceof CellData || !$cell->className instanceof StringAttr) {
651 return null;
652 }
653
654 $className = $cell->className->asAttr();
655
656 return $className === '' ? null : $className;
657 }
658
659 private function getCellSpan($cell) {
660 $span = ['rowSpan' => 1, 'colSpan' => 1];
661
662 if ($cell instanceof CellData && $cell->span instanceof Span) {
663 $span['rowSpan'] = $cell->span->rowSpan->value();
664 $span['colSpan'] = $cell->span->colSpan->value();
665 }
666
667 return $span;
668 }
669
670 /**
671 * @param int $column
672 * @param array<int|string, ColumnConfig> $columns
673 * @return string|null
674 */
675 private function getColumnWidth($column, $columns) {
676 if (!is_array($columns) || !array_key_exists($column, $columns)) {
677 return null;
678 }
679
680 $columnConfig = $columns[$column];
681 if (!$columnConfig instanceof ColumnConfig || $columnConfig->width === null) {
682 return null;
683 }
684
685 $width = $columnConfig->width->asAttr();
686 return $width === '' ? null : $width;
687 }
688
689 /**
690 * @param int $row
691 * @param array<int|string, RowConfig> $rowConfigs
692 * @return string|null
693 */
694 private function getRowHeight($row, $rowConfigs) {
695 if (!is_array($rowConfigs) || !array_key_exists($row, $rowConfigs)) {
696 return null;
697 }
698
699 $rowConfig = $rowConfigs[$row];
700 if (!$rowConfig instanceof RowConfig || $rowConfig->height === null) {
701 return null;
702 }
703
704 $height = $rowConfig->height->asAttr();
705 return $height === '' ? null : $height;
706 }
707
708 /**
709 * Pro styling of a row (e.g. background colour). One filter carries
710 * every pro row style, mirroring `tableberg/cell_styles`, so a new pro
711 * style needs no change here.
712 *
713 * @param int $row
714 * @param array<int|string, RowConfig> $rowConfigs
715 * @return array<string, string>
716 */
717 private function getRowStyles($row, $rowConfigs) {
718 $rowAttrs = (
719 is_array($rowConfigs) &&
720 array_key_exists($row, $rowConfigs) &&
721 $rowConfigs[$row] instanceof RowConfig
722 ) ? $rowConfigs[$row]->attrs : [];
723
724 $styles = apply_filters('tableberg/row_styles', [], $rowAttrs);
725
726 return is_array($styles) ? $styles : [];
727 }
728 }
729