PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.3
Tableberg – Simple Gutenberg Table Block v1.1.3
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.3, at renderer/Table/TableRenderer.php

752 lines 27.7 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 $cellStylesForRow = $globalCellStyles;
274
275 // Header/footer/even/odd are table-wide defaults, resolved per
276 // row here the same way `cell/index.tsx`'s
277 // `useRowBackgroundStyleKey` does in the editor — header/footer
278 // don't consume a slot in the even/odd count.
279 $isHeaderRow = $headerEnabled && $row === 0;
280 $isFooterRow = $footerEnabled && $rows > 0 && $row === $rows - 1;
281
282 if ($isHeaderRow) {
283 $rowPositionBackground = $attrs->cellDefaults->styles->headerBackgroundColor->asAttr();
284 } elseif ($isFooterRow) {
285 $rowPositionBackground = $attrs->cellDefaults->styles->footerBackgroundColor->asAttr();
286 } else {
287 $dataRowPosition = $headerEnabled ? $row - 1 : $row;
288 $rowPositionBackground = ($dataRowPosition % 2 === 0)
289 ? $attrs->cellDefaults->styles->oddRowBackgroundColor->asAttr()
290 : $attrs->cellDefaults->styles->evenRowBackgroundColor->asAttr();
291 }
292
293 if ($rowPositionBackground !== '') {
294 $cellStylesForRow['backgroundColor'] = $rowPositionBackground;
295 }
296
297 // A row with its own background would otherwise never show
298 // through: every cell paints its own background over the
299 // `<tr>`'s, and cells fall back to the table-wide default
300 // whenever they carry no colour of their own. So for this row's
301 // cells, that fallback is cleared — a cell/column colour on top
302 // of it (applied below by the pro filter) still wins either way.
303 if ($rowBackgroundColor !== '') {
304 $cellStylesForRow['backgroundColor'] = '';
305 }
306
307 for ($col = 0; $col < $cols; $col++) {
308 if (isset($hiddenBySpan[$row][$col])) {
309 continue;
310 }
311
312 $cell = $this->getCell($row, $col, $attrs->cells);
313 $span = $this->getCellSpan($cell);
314
315 $rowSpan = is_numeric($span['rowSpan']) ? (int) $span['rowSpan'] : 1;
316 $colSpan = is_numeric($span['colSpan']) ? (int) $span['colSpan'] : 1;
317
318 if ($rowSpan > 1 || $colSpan > 1) {
319 for ($rowOffset = 0; $rowOffset < $rowSpan; $rowOffset++) {
320 for ($colOffset = 0; $colOffset < $colSpan; $colOffset++) {
321 if ($rowOffset === 0 && $colOffset === 0) {
322 continue;
323 }
324
325 $hiddenRow = $row + $rowOffset;
326 $hiddenCol = $col + $colOffset;
327 $hiddenBySpan[$hiddenRow][$hiddenCol] = true;
328 }
329 }
330 }
331
332 $isHeaderRowCell = $headerEnabled && $row === 0;
333 $isFooterCell = $footerEnabled && $rows > 0 && $row === $rows - 1;
334
335 $tag = ($isHeaderRowCell || $isFooterCell) ? 'th' : 'td';
336
337 $sortableType = (
338 $isHeaderRowCell &&
339 array_key_exists($col, $sortableColumns)
340 ) ? $sortableColumns[$col] : null;
341
342 $columnWidth = null;
343 if ($colSpan === 1) {
344 if ($fixedColumnWidths && $cols > 0) {
345 $columnWidth = (string) (100 / $cols) . '%';
346 } else {
347 $columnWidth = $this->getColumnWidth(
348 $col,
349 $attrs->columns
350 );
351 }
352 }
353
354 $cellsHtml .= $cellRenderer->render(
355 CellRenderContext::create(
356 $row,
357 $col,
358 $rowSpan,
359 $colSpan,
360 $tag,
361 $cellStylesForRow,
362 $this->getCellElements($cell),
363 $this->getCellStyleOverride($cell),
364 $this->getCellRibbon($cell),
365 $sortableType,
366 $columnWidth,
367 $rowHeight,
368 $stickyHeader,
369 $stickyFirstCol,
370 $this->getCellClassName($cell),
371 $cell instanceof CellData ? $cell->attrs : [],
372 $this->isCellEmpty($cell)
373 )
374 );
375 }
376
377 $rowStyleParts = [];
378 if ($rowBackgroundColor !== '') {
379 $rowStyleParts[] = 'background-color:' . esc_attr($rowBackgroundColor);
380 }
381
382 $rowBorder = isset($rowStyles['border']) && is_array($rowStyles['border'])
383 ? $rowStyles['border']
384 : [];
385 foreach (['top', 'right', 'bottom', 'left'] as $side) {
386 $value = isset($rowBorder[$side]) && is_string($rowBorder[$side])
387 ? $rowBorder[$side]
388 : '';
389 if ($value !== '') {
390 $rowStyleParts[] = "border-{$side}:" . esc_attr($value);
391 }
392 }
393
394 $rowStyleAttr = !empty($rowStyleParts)
395 ? ' style="' . implode(';', $rowStyleParts) . '"'
396 : '';
397
398 $rowsHtml .= "<tr data-tableberg-row='{$row}'$rowStyleAttr>$cellsHtml</tr>";
399 }
400
401 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
402 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
403 $footerBoolAsStr = $footerEnabled ? 'true' : 'false';
404
405 $columnsData = [];
406 foreach ($sortableColumns as $column => $sortType) {
407 $columnsData[$column] = [
408 'sortable' => $sortType,
409 ];
410 }
411
412 $columnsJson = json_encode($columnsData);
413 if (!is_string($columnsJson)) {
414 $columnsJson = '{}';
415 }
416
417 $paginationJson = json_encode($paginationConfig);
418 if (!is_string($paginationJson)) {
419 $paginationJson = '{}';
420 }
421
422 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
423
424 // Round the whole table's outer corners by clipping the wrapper.
425 // Zero radii are skipped so a table without any actual rounding does
426 // not carry pointless border-radius declarations.
427 $isZeroRadius = function ($value) {
428 $trimmed = trim((string) $value);
429 return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1;
430 };
431 $wrapperStyles = [];
432 if (!$isZeroRadius($tableRadius['topLeft'])) {
433 $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
434 }
435 if (!$isZeroRadius($tableRadius['topRight'])) {
436 $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
437 }
438 if (!$isZeroRadius($tableRadius['bottomRight'])) {
439 $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
440 }
441 if (!$isZeroRadius($tableRadius['bottomLeft'])) {
442 $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
443 }
444
445 // A table wider than its wrapper must scroll inside the wrapper
446 // instead of pushing the whole page sideways, so the wrapper is a
447 // scroll container by default.
448 //
449 // The one exception is a header that sticks to the page: a scroll
450 // container of its own would capture it and it would never stick.
451 // (CSS cannot do both — with overflow-x set, overflow-y can no longer
452 // be visible.) A sticky first column, on the other hand, only works
453 // *because* of the scroll container, so when both are enabled the
454 // scrolling wins and the header sticks within the wrapper.
455 //
456 // A rounded wrapper always needs the clip, so it opts in regardless.
457 $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
458 $hasRoundedCorners = !empty($wrapperStyles);
459
460 if ($hasRoundedCorners || !$pageStickyHeader) {
461 // `auto`, not `hidden`: both clip to the rounded corners, but
462 // `hidden` would also swallow a table wider than the wrapper.
463 // Inline, so it also wins over the `.tableberg-scroll-x` rule.
464 $wrapperStyles[] = 'overflow:auto';
465 }
466
467 $wrapperStyleAttr = !empty($wrapperStyles)
468 ? "style='" . implode(';', $wrapperStyles) . "'"
469 : '';
470
471 $figureAlignmentClass = '';
472 if ($isWideWidth) {
473 $figureAlignmentClass = 'alignwide';
474 } elseif ($isFullWidth) {
475 $figureAlignmentClass = 'alignfull';
476 }
477
478 $figureClass = trim(
479 implode(' ', array_filter([
480 'wp-block-tableberg',
481 $attrs->table->className->asAttr(),
482 $figureAlignmentClass,
483 ]))
484 );
485
486 $figureStyles = [];
487 $spacingSides = [
488 'margin-top' => $attrs->table->margin->top,
489 'margin-right' => $attrs->table->margin->right,
490 'margin-bottom' => $attrs->table->margin->bottom,
491 'margin-left' => $attrs->table->margin->left,
492 'padding-top' => $attrs->table->padding->top,
493 'padding-right' => $attrs->table->padding->right,
494 'padding-bottom' => $attrs->table->padding->bottom,
495 'padding-left' => $attrs->table->padding->left,
496 ];
497 foreach ($spacingSides as $prop => $sideAttr) {
498 if ($sideAttr->isNotEmpty()) {
499 $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
500 }
501 }
502 $figureStyleAttr = !empty($figureStyles)
503 ? "style='" . implode('; ', $figureStyles) . ";'"
504 : '';
505
506 $captionHtml = '';
507 if ($caption->isNotEmpty()) {
508 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
509 }
510
511 $html =
512 "<figure class='{$figureClass}' {$figureStyleAttr}>
513 <div class='{$wrapperClass}' {$wrapperStyleAttr}>
514 <table
515 class='{$tableClassAttr}'
516 {$tableStyleAttr}
517 data-tableberg-sortable='$sortingBoolAsStr'
518 data-tableberg-columns='$columnsJson'
519 data-tableberg-pagination='$paginationJson'
520 data-tableberg-search-enabled='$searchEnabledAsStr'
521 data-tableberg-search-placeholder='$searchPlaceholder'
522 data-tableberg-search-position='$searchPosition'
523 data-tableberg-search-highlight-color='$searchHighlightColor'
524 data-tableberg-header='$headerBoolAsStr'
525 data-tableberg-footer='$footerBoolAsStr'
526 {$responsiveDataAttrs}
527 >
528 <tbody>
529 {$rowsHtml}
530 </tbody>
531 </table>
532 </div>
533 {$captionHtml}
534 </figure>";
535
536 return $html;
537 }
538
539 /**
540 * @param bool $isWideWidth
541 * @param bool $isFullWidth
542 * @param bool $canApplyCustomWidth
543 * @param string $alignment
544 * @return string
545 */
546 private function getWrapperAlignmentClass(
547 $isWideWidth,
548 $isFullWidth,
549 $canApplyCustomWidth,
550 $alignment
551 ) {
552 if ($isWideWidth) {
553 return 'alignwide';
554 }
555
556 if ($isFullWidth) {
557 return 'alignfull';
558 }
559
560 if (!$canApplyCustomWidth) {
561 return '';
562 }
563
564 if ($alignment === 'left' || $alignment === 'right') {
565 return 'justify-table-' . $alignment;
566 }
567
568 if ($alignment === 'center') {
569 return 'justify-table-center';
570 }
571
572 return '';
573 }
574
575 /**
576 * @param TableAttrs $attrs
577 * @param int $rows
578 * @param int $cols
579 * @return string
580 */
581 private function buildResponsiveDataAttrs($attrs, $rows, $cols) {
582 $tablet = $attrs->table->responsive->tablet;
583 $mobile = $attrs->table->responsive->mobile;
584
585 $tabletEnabled = $tablet->enabled->value();
586 $mobileEnabled = $mobile->enabled->value();
587
588 if (!$tabletEnabled && !$mobileEnabled) {
589 return '';
590 }
591
592 $tabletMaxWidth = max(1, (int) $tablet->maxWidth->value());
593 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
594 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
595 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
596
597 return "
598 data-tableberg-responsive='true'
599 data-tableberg-rows='{$rows}'
600 data-tableberg-cols='{$cols}'
601 data-tableberg-tablet-enabled='{$tablet->enabled->asAttr()}'
602 data-tableberg-tablet-width='{$tabletMaxWidth}'
603 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
604 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
605 data-tableberg-tablet-count='{$tabletStackCount}'
606 data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
607 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
608 data-tableberg-mobile-width='{$mobileMaxWidth}'
609 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
610 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
611 data-tableberg-mobile-count='{$mobileStackCount}'
612 data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
613 ";
614 }
615
616 private function getCell($row, $col, $cells) {
617 $key = $row . ',' . $col;
618
619 if (!is_array($cells) || !array_key_exists($key, $cells)) {
620 return null;
621 }
622
623 return $cells[$key];
624 }
625
626 private function getCellElements($cell) {
627 if ($this->isCellEmpty($cell)) {
628 return [];
629 }
630
631 return $cell instanceof CellData && is_array($cell->elements)
632 ? $cell->elements
633 : [];
634 }
635
636 /**
637 * "Empty cell" is a pro feature: the cell renders as if it were a bare,
638 * unstyled table cell — no content, no background, no border. Free's
639 * default is to always show the cell as configured.
640 *
641 * @param mixed $cell
642 * @return bool
643 */
644 private function isCellEmpty($cell) {
645 if (!$cell instanceof CellData) {
646 return false;
647 }
648
649 return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
650 }
651
652 private function getCellStyleOverride($cell) {
653 if (!$cell instanceof CellData || !is_array($cell->styles)) {
654 return null;
655 }
656
657 return StringAttr::fromNestedArray($cell->styles);
658 }
659
660 private function getCellRibbon($cell) {
661 if (!$cell instanceof CellData || !is_array($cell->ribbon)) {
662 return null;
663 }
664
665 return $cell->ribbon;
666 }
667
668 /**
669 * @param CellData|null $cell
670 * @return string|null
671 */
672 private function getCellClassName($cell) {
673 if (!$cell instanceof CellData || !$cell->className instanceof StringAttr) {
674 return null;
675 }
676
677 $className = $cell->className->asAttr();
678
679 return $className === '' ? null : $className;
680 }
681
682 private function getCellSpan($cell) {
683 $span = ['rowSpan' => 1, 'colSpan' => 1];
684
685 if ($cell instanceof CellData && $cell->span instanceof Span) {
686 $span['rowSpan'] = $cell->span->rowSpan->value();
687 $span['colSpan'] = $cell->span->colSpan->value();
688 }
689
690 return $span;
691 }
692
693 /**
694 * @param int $column
695 * @param array<int|string, ColumnConfig> $columns
696 * @return string|null
697 */
698 private function getColumnWidth($column, $columns) {
699 if (!is_array($columns) || !array_key_exists($column, $columns)) {
700 return null;
701 }
702
703 $columnConfig = $columns[$column];
704 if (!$columnConfig instanceof ColumnConfig || $columnConfig->width === null) {
705 return null;
706 }
707
708 $width = $columnConfig->width->asAttr();
709 return $width === '' ? null : $width;
710 }
711
712 /**
713 * @param int $row
714 * @param array<int|string, RowConfig> $rowConfigs
715 * @return string|null
716 */
717 private function getRowHeight($row, $rowConfigs) {
718 if (!is_array($rowConfigs) || !array_key_exists($row, $rowConfigs)) {
719 return null;
720 }
721
722 $rowConfig = $rowConfigs[$row];
723 if (!$rowConfig instanceof RowConfig || $rowConfig->height === null) {
724 return null;
725 }
726
727 $height = $rowConfig->height->asAttr();
728 return $height === '' ? null : $height;
729 }
730
731 /**
732 * Pro styling of a row (e.g. background colour). One filter carries
733 * every pro row style, mirroring `tableberg/cell_styles`, so a new pro
734 * style needs no change here.
735 *
736 * @param int $row
737 * @param array<int|string, RowConfig> $rowConfigs
738 * @return array<string, string>
739 */
740 private function getRowStyles($row, $rowConfigs) {
741 $rowAttrs = (
742 is_array($rowConfigs) &&
743 array_key_exists($row, $rowConfigs) &&
744 $rowConfigs[$row] instanceof RowConfig
745 ) ? $rowConfigs[$row]->attrs : [];
746
747 $styles = apply_filters('tableberg/row_styles', [], $rowAttrs);
748
749 return is_array($styles) ? $styles : [];
750 }
751 }
752