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
tableberg / renderer / Table / TableRenderer.php

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

823 lines 30.9 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 = 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 );
146
147 if ($hasCellSpacing) {
148 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
149 }
150
151 if ($tableWidth !== '') {
152 $tableStyles[] = "width: {$tableWidth}";
153 $tableStyles[] = "max-width: {$tableWidth}";
154 } else {
155 $tableStyles[] = 'width: 100%';
156 }
157
158 if (!$hasRoundedCorners && $tableBorderTop !== '') {
159 $tableStyles[] = "border-top: {$tableBorderTop}";
160 }
161
162 if (!$hasRoundedCorners && $tableBorderRight !== '') {
163 $tableStyles[] = "border-right: {$tableBorderRight}";
164 }
165
166 if (!$hasRoundedCorners && $tableBorderBottom !== '') {
167 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
168 }
169
170 if (!$hasRoundedCorners && $tableBorderLeft !== '') {
171 $tableStyles[] = "border-left: {$tableBorderLeft}";
172 }
173
174 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
175
176 $tableClasses = ['wp-block-tableberg'];
177
178 if ($this->has_visible_border($tableBorderTop)) {
179 $tableClasses[] = 'tableberg-has-table-border-top';
180 }
181
182 if ($this->has_visible_border($tableBorderRight)) {
183 $tableClasses[] = 'tableberg-has-table-border-right';
184 }
185
186 if ($this->has_visible_border($tableBorderBottom)) {
187 $tableClasses[] = 'tableberg-has-table-border-bottom';
188 }
189
190 if ($this->has_visible_border($tableBorderLeft)) {
191 $tableClasses[] = 'tableberg-has-table-border-left';
192 }
193
194 if ($hasCellSpacing) {
195 $tableClasses[] = 'tableberg-has-cell-spacing';
196
197 if ($isHorizontalSpacingZero) {
198 $tableClasses[] = 'tableberg-cell-spacing-horizontal-zero';
199 }
200
201 if ($isVerticalSpacingZero) {
202 $tableClasses[] = 'tableberg-cell-spacing-vertical-zero';
203 }
204 }
205
206 $tableClassAttr = implode(' ', $tableClasses);
207
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 = [];
218 }
219
220 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
221 if ($paginationPageSize < 1) {
222 $paginationPageSize = 1;
223 }
224
225 $paginationConfig = [
226 'enabled' => apply_filters('tableberg/pagination_enabled', false, $attrs->attrs),
227 'pageSize' => $paginationPageSize,
228 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
229 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
230 ];
231
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'] ?? '');
246 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
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
257 $globalCellStyles = [
258 'padding' => [
259 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
260 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
261 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
262 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
263 ],
264 'orientation' => $cellLayout['orientation'] === 'horizontal' ? 'horizontal' : 'vertical',
265 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
266 'wrap' => $cellLayout['wrap'] === 'wrap' ? 'wrap' : 'nowrap',
267 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
268 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
269 'border' => [
270 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
271 'right' => $attrs->cellDefaults->styles->border->right->asAttr(),
272 'bottom' => $attrs->cellDefaults->styles->border->bottom->asAttr(),
273 'left' => $attrs->cellDefaults->styles->border->left->asAttr(),
274 ],
275 'borderRadius' => [
276 'topLeft' => $attrs->cellDefaults->styles->borderRadius->topLeft->asAttr(),
277 'topRight' => $attrs->cellDefaults->styles->borderRadius->topRight->asAttr(),
278 'bottomRight' => $attrs->cellDefaults->styles->borderRadius->bottomRight->asAttr(),
279 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
280 ],
281 ];
282
283 // A collapsed table cannot reliably round its own explicit border.
284 // The wrapper owns only the table border and clips the cell grid to
285 // it. Cell borders stay on cells; copying them to the wrapper would
286 // add an outline that the user did not configure as a table border.
287 $tableOuterBorder = [
288 'top' => $tableBorderTop,
289 'right' => $tableBorderRight,
290 'bottom' => $tableBorderBottom,
291 'left' => $tableBorderLeft,
292 ];
293 $globalCellStyles['borderRadius'] = [
294 'topLeft' => '',
295 'topRight' => '',
296 'bottomRight' => '',
297 'bottomLeft' => '',
298 ];
299
300 $cellRenderer = new CellRenderer();
301 $hiddenBySpan = [];
302
303 $rowsHtml = '';
304
305 for ($row = 0; $row < $rows; $row++) {
306 $cellsHtml = '';
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 : '';
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
347 for ($col = 0; $col < $cols; $col++) {
348 if (isset($hiddenBySpan[$row][$col])) {
349 continue;
350 }
351
352 $cell = $this->getCell($row, $col, $attrs->cells);
353 $span = $this->getCellSpan($cell);
354
355 $rowSpan = is_numeric($span['rowSpan']) ? (int) $span['rowSpan'] : 1;
356 $colSpan = is_numeric($span['colSpan']) ? (int) $span['colSpan'] : 1;
357
358 if ($rowSpan > 1 || $colSpan > 1) {
359 for ($rowOffset = 0; $rowOffset < $rowSpan; $rowOffset++) {
360 for ($colOffset = 0; $colOffset < $colSpan; $colOffset++) {
361 if ($rowOffset === 0 && $colOffset === 0) {
362 continue;
363 }
364
365 $hiddenRow = $row + $rowOffset;
366 $hiddenCol = $col + $colOffset;
367 $hiddenBySpan[$hiddenRow][$hiddenCol] = true;
368 }
369 }
370 }
371
372 $isHeaderRowCell = $headerEnabled && $row === 0;
373 $isFooterCell = $footerEnabled && $rows > 0 && $row === $rows - 1;
374
375 $tag = ($isHeaderRowCell || $isFooterCell) ? 'th' : 'td';
376
377 $sortableType = (
378 $isHeaderRowCell &&
379 array_key_exists($col, $sortableColumns)
380 ) ? $sortableColumns[$col] : null;
381
382 $columnWidth = null;
383 if ($colSpan === 1) {
384 if ($fixedColumnWidths && $cols > 0) {
385 $columnWidth = (string) (100 / $cols) . '%';
386 } else {
387 $columnWidth = $this->getColumnWidth(
388 $col,
389 $attrs->columns
390 );
391 }
392 }
393
394 $cellsHtml .= $cellRenderer->render(
395 CellRenderContext::create(
396 $row,
397 $col,
398 $rowSpan,
399 $colSpan,
400 $tag,
401 $cellStylesForRow,
402 $this->getCellElements($cell),
403 $this->getCellStyleOverride($cell),
404 $this->getCellRibbon($cell),
405 $sortableType,
406 $columnWidth,
407 $rowHeight,
408 $stickyHeader,
409 $stickyFirstCol,
410 $this->getCellClassName($cell),
411 $cell instanceof CellData ? $cell->attrs : [],
412 $this->isCellEmpty($cell),
413 $innerBorderType,
414 $rows,
415 $cols
416 )
417 );
418 }
419
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>";
451 }
452
453 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
454 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
455 $footerBoolAsStr = $footerEnabled ? 'true' : 'false';
456
457 $columnsData = [];
458 foreach ($sortableColumns as $column => $sortType) {
459 $columnsData[$column] = [
460 'sortable' => $sortType,
461 ];
462 }
463
464 $columnsJson = wp_json_encode($columnsData);
465 if (!is_string($columnsJson)) {
466 $columnsJson = '{}';
467 }
468 $columnsJson = esc_attr($columnsJson);
469
470 $paginationJson = wp_json_encode($paginationConfig);
471 if (!is_string($paginationJson)) {
472 $paginationJson = '{}';
473 }
474 $paginationJson = esc_attr($paginationJson);
475
476 $searchPlaceholderAttr = esc_attr($searchPlaceholder);
477 $searchPositionAttr = esc_attr($searchPosition);
478 $searchHighlightColorAttr = esc_attr($searchHighlightColor);
479
480 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
481
482 // Round the whole table's outer corners on the wrapper.
483 // Zero radii are skipped so a table without any actual rounding does
484 // not carry pointless border-radius declarations.
485 $wrapperStyles = [];
486 if (!$isZeroRadius($tableRadius['topLeft'])) {
487 $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
488 }
489 if (!$isZeroRadius($tableRadius['topRight'])) {
490 $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
491 }
492 if (!$isZeroRadius($tableRadius['bottomRight'])) {
493 $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
494 }
495 if (!$isZeroRadius($tableRadius['bottomLeft'])) {
496 $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
497 }
498
499 if (!empty($wrapperStyles)) {
500 if ($tableOuterBorder['top'] !== '') {
501 $wrapperStyles[] = 'border-top:' . $tableOuterBorder['top'];
502 }
503 if ($tableOuterBorder['right'] !== '') {
504 $wrapperStyles[] = 'border-right:' . $tableOuterBorder['right'];
505 }
506 if ($tableOuterBorder['bottom'] !== '') {
507 $wrapperStyles[] = 'border-bottom:' . $tableOuterBorder['bottom'];
508 }
509 if ($tableOuterBorder['left'] !== '') {
510 $wrapperStyles[] = 'border-left:' . $tableOuterBorder['left'];
511 }
512 $wrapperStyles[] = 'box-sizing:border-box';
513 }
514
515 // A table wider than its wrapper must scroll inside the wrapper
516 // instead of pushing the whole page sideways, so the wrapper is a
517 // scroll container by default.
518 //
519 // The one exception is a header that sticks to the page: a scroll
520 // container of its own would capture it and it would never stick.
521 // (CSS cannot do both — with overflow-x set, overflow-y can no longer
522 // be visible.) A sticky first column, on the other hand, only works
523 // *because* of the scroll container, so when both are enabled the
524 // scrolling wins and the header sticks within the wrapper.
525 //
526 // A rounded wrapper always needs the clip, so it opts in regardless.
527 $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
528 if ($hasRoundedCorners || !$pageStickyHeader) {
529 // `auto`, not `hidden`: both clip to the rounded corners, but
530 // `hidden` would also swallow a table wider than the wrapper.
531 // Inline, so it also wins over the `.tableberg-scroll-x` rule.
532 $wrapperStyles[] = 'overflow:auto';
533 }
534
535 $wrapperStyleAttr = !empty($wrapperStyles)
536 ? "style='" . implode(';', $wrapperStyles) . "'"
537 : '';
538
539 $figureAlignmentClass = '';
540 if ($isWideWidth) {
541 $figureAlignmentClass = 'alignwide';
542 } elseif ($isFullWidth) {
543 $figureAlignmentClass = 'alignfull';
544 }
545
546 $figureClass = trim(
547 implode(' ', array_filter([
548 'wp-block-tableberg',
549 $attrs->table->className->asAttr(),
550 $figureAlignmentClass,
551 ]))
552 );
553
554 $figureStyles = [];
555 $spacingSides = [
556 'margin-top' => $attrs->table->margin->top,
557 'margin-right' => $attrs->table->margin->right,
558 'margin-bottom' => $attrs->table->margin->bottom,
559 'margin-left' => $attrs->table->margin->left,
560 'padding-top' => $attrs->table->padding->top,
561 'padding-right' => $attrs->table->padding->right,
562 'padding-bottom' => $attrs->table->padding->bottom,
563 'padding-left' => $attrs->table->padding->left,
564 ];
565 foreach ($spacingSides as $prop => $sideAttr) {
566 if ($sideAttr->isNotEmpty()) {
567 $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
568 }
569 }
570 $figureStyleAttr = !empty($figureStyles)
571 ? "style='" . implode('; ', $figureStyles) . ";'"
572 : '';
573
574 $captionHtml = '';
575 if ($caption->isNotEmpty()) {
576 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
577 }
578
579 $html =
580 "<figure class='{$figureClass}' {$figureStyleAttr}>
581 <div class='{$wrapperClass}' {$wrapperStyleAttr}>
582 <table
583 class='{$tableClassAttr}'
584 {$tableStyleAttr}
585 data-tableberg-sortable='$sortingBoolAsStr'
586 data-tableberg-columns='$columnsJson'
587 data-tableberg-pagination='$paginationJson'
588 data-tableberg-search-enabled='$searchEnabledAsStr'
589 data-tableberg-search-placeholder='$searchPlaceholderAttr'
590 data-tableberg-search-position='$searchPositionAttr'
591 data-tableberg-search-highlight-color='$searchHighlightColorAttr'
592 data-tableberg-header='$headerBoolAsStr'
593 data-tableberg-footer='$footerBoolAsStr'
594 {$responsiveDataAttrs}
595 >
596 <tbody>
597 {$rowsHtml}
598 </tbody>
599 </table>
600 </div>
601 {$captionHtml}
602 </figure>";
603
604 return $html;
605 }
606
607 /**
608 * @param bool $isWideWidth
609 * @param bool $isFullWidth
610 * @param bool $canApplyCustomWidth
611 * @param string $alignment
612 * @return string
613 */
614 private function getWrapperAlignmentClass(
615 $isWideWidth,
616 $isFullWidth,
617 $canApplyCustomWidth,
618 $alignment
619 ) {
620 if ($isWideWidth) {
621 return 'alignwide';
622 }
623
624 if ($isFullWidth) {
625 return 'alignfull';
626 }
627
628 if (!$canApplyCustomWidth) {
629 return '';
630 }
631
632 if ($alignment === 'left' || $alignment === 'right') {
633 return 'justify-table-' . $alignment;
634 }
635
636 if ($alignment === 'center') {
637 return 'justify-table-center';
638 }
639
640 return '';
641 }
642
643 /**
644 * @param TableAttrs $attrs
645 * @param int $rows
646 * @param int $cols
647 * @return string
648 */
649 private function buildResponsiveDataAttrs($attrs, $rows, $cols) {
650 $tablet = $attrs->table->responsive->tablet;
651 $mobile = $attrs->table->responsive->mobile;
652
653 $tabletEnabled = $tablet->enabled->value();
654 $mobileEnabled = $mobile->enabled->value();
655
656 if (!$tabletEnabled && !$mobileEnabled) {
657 return '';
658 }
659
660 $tabletMaxWidth = max(1, (int) $tablet->maxWidth->value());
661 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
662 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
663 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
664
665 $tabletRepeatFirstCol = $tablet->repeatFirstCol->asAttr();
666 $mobileRepeatFirstCol = $mobile->repeatFirstCol->asAttr();
667
668 return "
669 data-tableberg-responsive='true'
670 data-tableberg-rows='{$rows}'
671 data-tableberg-cols='{$cols}'
672 data-tableberg-tablet-enabled='{$tablet->enabled->asAttr()}'
673 data-tableberg-tablet-width='{$tabletMaxWidth}'
674 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
675 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
676 data-tableberg-tablet-count='{$tabletStackCount}'
677 data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}'
678 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
679 data-tableberg-mobile-width='{$mobileMaxWidth}'
680 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
681 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
682 data-tableberg-mobile-count='{$mobileStackCount}'
683 data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}'
684 ";
685 }
686
687 private function getCell($row, $col, $cells) {
688 $key = $row . ',' . $col;
689
690 if (!is_array($cells) || !array_key_exists($key, $cells)) {
691 return null;
692 }
693
694 return $cells[$key];
695 }
696
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) {
716 if (!$cell instanceof CellData) {
717 return false;
718 }
719
720 return (bool) apply_filters('tableberg/cell_is_empty', false, $cell->attrs);
721 }
722
723 private function getCellStyleOverride($cell) {
724 if (!$cell instanceof CellData || !is_array($cell->styles)) {
725 return null;
726 }
727
728 return StringAttr::fromNestedArray($cell->styles);
729 }
730
731 private function getCellRibbon($cell) {
732 if (!$cell instanceof CellData || !is_array($cell->ribbon)) {
733 return null;
734 }
735
736 return $cell->ribbon;
737 }
738
739 /**
740 * @param CellData|null $cell
741 * @return string|null
742 */
743 private function getCellClassName($cell) {
744 if (!$cell instanceof CellData || !$cell->className instanceof StringAttr) {
745 return null;
746 }
747
748 $className = $cell->className->asAttr();
749
750 return $className === '' ? null : $className;
751 }
752
753 private function getCellSpan($cell) {
754 $span = ['rowSpan' => 1, 'colSpan' => 1];
755
756 if ($cell instanceof CellData && $cell->span instanceof Span) {
757 $span['rowSpan'] = $cell->span->rowSpan->value();
758 $span['colSpan'] = $cell->span->colSpan->value();
759 }
760
761 return $span;
762 }
763
764 /**
765 * @param int $column
766 * @param array<int|string, ColumnConfig> $columns
767 * @return string|null
768 */
769 private function getColumnWidth($column, $columns) {
770 if (!is_array($columns) || !array_key_exists($column, $columns)) {
771 return null;
772 }
773
774 $columnConfig = $columns[$column];
775 if (!$columnConfig instanceof ColumnConfig || $columnConfig->width === null) {
776 return null;
777 }
778
779 $width = $columnConfig->width->asAttr();
780 return $width === '' ? null : $width;
781 }
782
783 /**
784 * @param int $row
785 * @param array<int|string, RowConfig> $rowConfigs
786 * @return string|null
787 */
788 private function getRowHeight($row, $rowConfigs) {
789 if (!is_array($rowConfigs) || !array_key_exists($row, $rowConfigs)) {
790 return null;
791 }
792
793 $rowConfig = $rowConfigs[$row];
794 if (!$rowConfig instanceof RowConfig || $rowConfig->height === null) {
795 return null;
796 }
797
798 $height = $rowConfig->height->asAttr();
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 : [];
821 }
822 }
823