PluginProbe
Tableberg – Simple Gutenberg Table Block / trunk
Tableberg – Simple Gutenberg Table Block vtrunk
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 0.5.8 All 41 releases
tableberg / renderer / Table / TableRenderer.php

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

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