PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.0
Tableberg – Simple Gutenberg Table Block v1.1.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 0.5.7 All 42 releases
tableberg / renderer / Table / TableRenderer.php

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

646 lines 22.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 /**
11 * @return bool
12 */
13 private function is_pro_active() {
14 if (!function_exists('tp_fs')) {
15 return false;
16 }
17
18 $fs = call_user_func('tp_fs');
19
20 return is_object($fs) && $fs->can_use_premium_code();
21 }
22
23 private function has_visible_border($border) {
24 $trimmed_border = trim($border);
25
26 if ($trimmed_border === '') {
27 return false;
28 }
29
30 $parts = preg_split('/\s+/', $trimmed_border, 3);
31 $width = $parts[0] ?? '';
32 $style = $parts[1] ?? '';
33
34 if ($width === 'none' || $width === 'hidden') {
35 return false;
36 }
37
38 if (preg_match('/^0(?:\.0+)?(?:[a-z%]+)?$/i', $width) === 1) {
39 return false;
40 }
41
42 if ($style === 'none' || $style === 'hidden') {
43 return false;
44 }
45
46 return true;
47 }
48
49 public function render($attributes, $content = '', $block = null) {
50 // v4 native-blocks format: table data lives in the innerBlocks tree
51 // (rows -> cells -> elements). Adapt it to the v3 attrs shape so the
52 // renderer below stays format-agnostic.
53 $version = is_array($attributes) && isset($attributes['version']) && is_numeric($attributes['version'])
54 ? (int) $attributes['version']
55 : 0;
56 if (
57 $version >= 4 &&
58 is_object($block) &&
59 isset($block->parsed_block['innerBlocks']) &&
60 is_array($block->parsed_block['innerBlocks'])
61 ) {
62 $attributes = InnerBlocksAttrsAdapter::to_attrs(
63 $attributes,
64 $block->parsed_block['innerBlocks']
65 );
66 }
67
68 $attrs = TableAttrs::from_array($attributes);
69 $isProActive = $this->is_pro_active();
70
71 $rows = $attrs->table->rows->value();
72 $cols = $attrs->table->cols->value();
73
74 $headerEnabled = $attrs->table->headerEnabled->value();
75 $footerEnabled = $attrs->table->footerEnabled->value();
76 $stickyHeader = $attrs->table->stickyHeader->value();
77 $stickyFirstCol = $attrs->table->stickyFirstCol->value();
78 $caption = $attrs->table->caption;
79 $tableWidth = trim($attrs->table->tableWidth->asAttr());
80 $tableAlignment = $attrs->table->tableAlignment->asAttr();
81 $cellSpacingHorizontal = trim($attrs->table->cellSpacing->horizontal->asAttr());
82 $cellSpacingVertical = trim($attrs->table->cellSpacing->vertical->asAttr());
83 $tableBorderTop = trim($attrs->table->tableBorder->top->asAttr());
84 $tableBorderRight = trim($attrs->table->tableBorder->right->asAttr());
85 $tableBorderBottom = trim($attrs->table->tableBorder->bottom->asAttr());
86 $tableBorderLeft = trim($attrs->table->tableBorder->left->asAttr());
87 $fixedColumnWidths = $attrs->table->fixedColumnWidths->value();
88
89 $isHorizontalSpacingZero = $cellSpacingHorizontal === '0';
90 $isVerticalSpacingZero = $cellSpacingVertical === '0';
91 $hasCellSpacing = !$isHorizontalSpacingZero || !$isVerticalSpacingZero;
92
93 $isWideWidth = $tableWidth === 'wide';
94 $isFullWidth = $tableWidth === 'full';
95
96 $canApplyCustomWidth =
97 $tableWidth !== '' &&
98 !$isWideWidth &&
99 !$isFullWidth &&
100 $tableWidth !== 'auto';
101
102 if (!$canApplyCustomWidth) {
103 $tableWidth = '';
104 }
105
106 $wrapperAlignmentClass = $this->getWrapperAlignmentClass(
107 $isWideWidth,
108 $isFullWidth,
109 $canApplyCustomWidth,
110 $tableAlignment
111 );
112
113 $tableStyles = [
114 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'),
115 ];
116
117 if ($hasCellSpacing) {
118 $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}";
119 }
120
121 if ($tableWidth !== '') {
122 $tableStyles[] = "width: {$tableWidth}";
123 $tableStyles[] = "max-width: {$tableWidth}";
124 } else {
125 $tableStyles[] = 'width: 100%';
126 }
127
128 if ($tableBorderTop !== '') {
129 $tableStyles[] = "border-top: {$tableBorderTop}";
130 }
131
132 if ($tableBorderRight !== '') {
133 $tableStyles[] = "border-right: {$tableBorderRight}";
134 }
135
136 if ($tableBorderBottom !== '') {
137 $tableStyles[] = "border-bottom: {$tableBorderBottom}";
138 }
139
140 if ($tableBorderLeft !== '') {
141 $tableStyles[] = "border-left: {$tableBorderLeft}";
142 }
143
144 $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'";
145
146 $tableClasses = ['wp-block-tableberg'];
147
148 if ($this->has_visible_border($tableBorderTop)) {
149 $tableClasses[] = 'tableberg-has-table-border-top';
150 }
151
152 if ($this->has_visible_border($tableBorderRight)) {
153 $tableClasses[] = 'tableberg-has-table-border-right';
154 }
155
156 if ($this->has_visible_border($tableBorderBottom)) {
157 $tableClasses[] = 'tableberg-has-table-border-bottom';
158 }
159
160 if ($this->has_visible_border($tableBorderLeft)) {
161 $tableClasses[] = 'tableberg-has-table-border-left';
162 }
163
164 if ($hasCellSpacing) {
165 $tableClasses[] = 'tableberg-has-cell-spacing';
166
167 if ($isHorizontalSpacingZero) {
168 $tableClasses[] = 'tableberg-cell-spacing-horizontal-zero';
169 }
170
171 if ($isVerticalSpacingZero) {
172 $tableClasses[] = 'tableberg-cell-spacing-vertical-zero';
173 }
174 }
175
176 $tableClassAttr = implode(' ', $tableClasses);
177
178 $sortableColumns = [];
179
180 if ($isProActive) {
181 foreach ($attrs->columns as $column => $columnConfig) {
182 if ($columnConfig->sortable === null) {
183 continue;
184 }
185
186 $sortableColumns[$column] = $columnConfig->sortable->asAttr();
187 }
188 }
189
190 $paginationPageSize = (int) $attrs->table->pagination->pageSize->value();
191 if ($paginationPageSize < 1) {
192 $paginationPageSize = 1;
193 }
194
195 $paginationConfig = [
196 'enabled' => $isProActive && $attrs->table->pagination->enabled->value(),
197 'pageSize' => $paginationPageSize,
198 'showPageNumbers' => $attrs->table->pagination->showPageNumbers->value(),
199 'showPrevNext' => $attrs->table->pagination->showPrevNext->value(),
200 ];
201
202 $searchEnabledAsStr = (
203 $isProActive && $attrs->table->search->enabled->value()
204 ) ? 'true' : 'false';
205 $searchPlaceholder = $isProActive
206 ? $attrs->table->search->placeholder->asAttr()
207 : '';
208 $searchPosition = $isProActive
209 ? $attrs->table->search->position->asAttr()
210 : 'left';
211 $searchHighlightColor = $isProActive
212 ? $attrs->table->search->highlightColor->asAttr()
213 : '';
214 $responsiveDataAttrs = $this->buildResponsiveDataAttrs($attrs, $rows, $cols);
215
216 $globalCellStyles = [
217 'padding' => [
218 'top' => $attrs->cellDefaults->styles->padding->top->asAttr(),
219 'right' => $attrs->cellDefaults->styles->padding->right->asAttr(),
220 'bottom' => $attrs->cellDefaults->styles->padding->bottom->asAttr(),
221 'left' => $attrs->cellDefaults->styles->padding->left->asAttr(),
222 ],
223 'orientation' => $attrs->cellDefaults->styles->orientation->asAttr(),
224 'elementGap' => $attrs->cellDefaults->styles->elementGap->asAttr(),
225 'wrap' => $attrs->cellDefaults->styles->wrap->asAttr(),
226 'verticalAlign' => $attrs->cellDefaults->styles->verticalAlign->asAttr(),
227 'backgroundColor' => $attrs->cellDefaults->styles->backgroundColor->asAttr(),
228 'border' => [
229 'top' => $attrs->cellDefaults->styles->border->top->asAttr(),
230 'right' => $attrs->cellDefaults->styles->border->right->asAttr(),
231 'bottom' => $attrs->cellDefaults->styles->border->bottom->asAttr(),
232 'left' => $attrs->cellDefaults->styles->border->left->asAttr(),
233 ],
234 'borderRadius' => [
235 'topLeft' => $attrs->cellDefaults->styles->borderRadius->topLeft->asAttr(),
236 'topRight' => $attrs->cellDefaults->styles->borderRadius->topRight->asAttr(),
237 'bottomRight' => $attrs->cellDefaults->styles->borderRadius->bottomRight->asAttr(),
238 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(),
239 ],
240 ];
241
242 // The table border radius rounds the whole table's outer corners.
243 // border-radius on collapsed-border tables/cells is ignored by
244 // browsers, so it is rendered on the wrapper (with overflow:hidden)
245 // and removed from the individual cells here.
246 $tableRadius = $globalCellStyles['borderRadius'];
247 $globalCellStyles['borderRadius'] = [
248 'topLeft' => '',
249 'topRight' => '',
250 'bottomRight' => '',
251 'bottomLeft' => '',
252 ];
253
254 $cellRenderer = new CellRenderer();
255 $hiddenBySpan = [];
256
257 $rowsHtml = '';
258
259 for ($row = 0; $row < $rows; $row++) {
260 $cellsHtml = '';
261 $rowHeight = $this->getRowHeight($row, $attrs->rows);
262
263 for ($col = 0; $col < $cols; $col++) {
264 if (isset($hiddenBySpan[$row][$col])) {
265 continue;
266 }
267
268 $cell = $this->getCell($row, $col, $attrs->cells);
269 $span = $this->getCellSpan($cell);
270
271 $rowSpan = is_numeric($span['rowSpan']) ? (int) $span['rowSpan'] : 1;
272 $colSpan = is_numeric($span['colSpan']) ? (int) $span['colSpan'] : 1;
273
274 if ($rowSpan > 1 || $colSpan > 1) {
275 for ($rowOffset = 0; $rowOffset < $rowSpan; $rowOffset++) {
276 for ($colOffset = 0; $colOffset < $colSpan; $colOffset++) {
277 if ($rowOffset === 0 && $colOffset === 0) {
278 continue;
279 }
280
281 $hiddenRow = $row + $rowOffset;
282 $hiddenCol = $col + $colOffset;
283 $hiddenBySpan[$hiddenRow][$hiddenCol] = true;
284 }
285 }
286 }
287
288 $isHeaderRowCell = $headerEnabled && $row === 0;
289 $isFooterCell = $footerEnabled && $rows > 0 && $row === $rows - 1;
290
291 $tag = ($isHeaderRowCell || $isFooterCell) ? 'th' : 'td';
292
293 $sortableType = (
294 $isHeaderRowCell &&
295 array_key_exists($col, $sortableColumns)
296 ) ? $sortableColumns[$col] : null;
297
298 $columnWidth = null;
299 if ($colSpan === 1) {
300 if ($fixedColumnWidths && $cols > 0) {
301 $columnWidth = (string) (100 / $cols) . '%';
302 } else {
303 $columnWidth = $this->getColumnWidth(
304 $col,
305 $attrs->columns
306 );
307 }
308 }
309
310 $cellsHtml .= $cellRenderer->render(
311 CellRenderContext::create(
312 $row,
313 $col,
314 $rowSpan,
315 $colSpan,
316 $tag,
317 $globalCellStyles,
318 $this->getCellElements($cell),
319 $this->getCellStyleOverride($cell),
320 $this->getCellRibbon($cell),
321 $sortableType,
322 $columnWidth,
323 $rowHeight,
324 $stickyHeader,
325 $stickyFirstCol,
326 $this->getCellClassName($cell)
327 )
328 );
329 }
330
331 $rowsHtml .= "<tr data-tableberg-row='{$row}'>$cellsHtml</tr>";
332 }
333
334 $sortingBoolAsStr = !empty($sortableColumns) ? 'true' : 'false';
335 $headerBoolAsStr = $headerEnabled ? 'true' : 'false';
336 $footerBoolAsStr = $footerEnabled ? 'true' : 'false';
337
338 $columnsData = [];
339 foreach ($sortableColumns as $column => $sortType) {
340 $columnsData[$column] = [
341 'sortable' => $sortType,
342 ];
343 }
344
345 $columnsJson = json_encode($columnsData);
346 if (!is_string($columnsJson)) {
347 $columnsJson = '{}';
348 }
349
350 $paginationJson = json_encode($paginationConfig);
351 if (!is_string($paginationJson)) {
352 $paginationJson = '{}';
353 }
354
355 $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}");
356
357 // Round the whole table's outer corners by clipping the wrapper.
358 // Zero radii are skipped so a table without any actual rounding does
359 // not carry pointless border-radius declarations.
360 $isZeroRadius = function ($value) {
361 $trimmed = trim((string) $value);
362 return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1;
363 };
364 $wrapperStyles = [];
365 if (!$isZeroRadius($tableRadius['topLeft'])) {
366 $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft'];
367 }
368 if (!$isZeroRadius($tableRadius['topRight'])) {
369 $wrapperStyles[] = 'border-top-right-radius:' . $tableRadius['topRight'];
370 }
371 if (!$isZeroRadius($tableRadius['bottomRight'])) {
372 $wrapperStyles[] = 'border-bottom-right-radius:' . $tableRadius['bottomRight'];
373 }
374 if (!$isZeroRadius($tableRadius['bottomLeft'])) {
375 $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft'];
376 }
377
378 // A table wider than its wrapper must scroll inside the wrapper
379 // instead of pushing the whole page sideways, so the wrapper is a
380 // scroll container by default.
381 //
382 // The one exception is a header that sticks to the page: a scroll
383 // container of its own would capture it and it would never stick.
384 // (CSS cannot do both — with overflow-x set, overflow-y can no longer
385 // be visible.) A sticky first column, on the other hand, only works
386 // *because* of the scroll container, so when both are enabled the
387 // scrolling wins and the header sticks within the wrapper.
388 //
389 // A rounded wrapper always needs the clip, so it opts in regardless.
390 $pageStickyHeader = $stickyHeader && !$stickyFirstCol;
391 $hasRoundedCorners = !empty($wrapperStyles);
392
393 if ($hasRoundedCorners || !$pageStickyHeader) {
394 // `auto`, not `hidden`: both clip to the rounded corners, but
395 // `hidden` would also swallow a table wider than the wrapper.
396 // Inline, so it also wins over the `.tableberg-scroll-x` rule.
397 $wrapperStyles[] = 'overflow:auto';
398 }
399
400 $wrapperStyleAttr = !empty($wrapperStyles)
401 ? "style='" . implode(';', $wrapperStyles) . "'"
402 : '';
403
404 $figureAlignmentClass = '';
405 if ($isWideWidth) {
406 $figureAlignmentClass = 'alignwide';
407 } elseif ($isFullWidth) {
408 $figureAlignmentClass = 'alignfull';
409 }
410
411 $figureClass = trim(
412 implode(' ', array_filter([
413 'wp-block-tableberg',
414 $attrs->table->className->asAttr(),
415 $figureAlignmentClass,
416 ]))
417 );
418
419 $figureStyles = [];
420 $spacingSides = [
421 'margin-top' => $attrs->table->margin->top,
422 'margin-right' => $attrs->table->margin->right,
423 'margin-bottom' => $attrs->table->margin->bottom,
424 'margin-left' => $attrs->table->margin->left,
425 'padding-top' => $attrs->table->padding->top,
426 'padding-right' => $attrs->table->padding->right,
427 'padding-bottom' => $attrs->table->padding->bottom,
428 'padding-left' => $attrs->table->padding->left,
429 ];
430 foreach ($spacingSides as $prop => $sideAttr) {
431 if ($sideAttr->isNotEmpty()) {
432 $figureStyles[] = $prop . ': ' . $sideAttr->asAttr();
433 }
434 }
435 $figureStyleAttr = !empty($figureStyles)
436 ? "style='" . implode('; ', $figureStyles) . ";'"
437 : '';
438
439 $captionHtml = '';
440 if ($caption->isNotEmpty()) {
441 $captionHtml = "<figcaption class='tableberg-table-caption wp-element-caption'>{$caption->asHtml()}</figcaption>";
442 }
443
444 $html =
445 "<figure class='{$figureClass}' {$figureStyleAttr}>
446 <div class='{$wrapperClass}' {$wrapperStyleAttr}>
447 <table
448 class='{$tableClassAttr}'
449 {$tableStyleAttr}
450 data-tableberg-sortable='$sortingBoolAsStr'
451 data-tableberg-columns='$columnsJson'
452 data-tableberg-pagination='$paginationJson'
453 data-tableberg-search-enabled='$searchEnabledAsStr'
454 data-tableberg-search-placeholder='$searchPlaceholder'
455 data-tableberg-search-position='$searchPosition'
456 data-tableberg-search-highlight-color='$searchHighlightColor'
457 data-tableberg-header='$headerBoolAsStr'
458 data-tableberg-footer='$footerBoolAsStr'
459 {$responsiveDataAttrs}
460 >
461 <tbody>
462 {$rowsHtml}
463 </tbody>
464 </table>
465 </div>
466 {$captionHtml}
467 </figure>";
468
469 return $html;
470 }
471
472 /**
473 * @param bool $isWideWidth
474 * @param bool $isFullWidth
475 * @param bool $canApplyCustomWidth
476 * @param string $alignment
477 * @return string
478 */
479 private function getWrapperAlignmentClass(
480 $isWideWidth,
481 $isFullWidth,
482 $canApplyCustomWidth,
483 $alignment
484 ) {
485 if ($isWideWidth) {
486 return 'alignwide';
487 }
488
489 if ($isFullWidth) {
490 return 'alignfull';
491 }
492
493 if (!$canApplyCustomWidth) {
494 return '';
495 }
496
497 if ($alignment === 'left' || $alignment === 'right') {
498 return 'justify-table-' . $alignment;
499 }
500
501 if ($alignment === 'center') {
502 return 'justify-table-center';
503 }
504
505 return '';
506 }
507
508 /**
509 * @param TableAttrs $attrs
510 * @param int $rows
511 * @param int $cols
512 * @return string
513 */
514 private function buildResponsiveDataAttrs($attrs, $rows, $cols) {
515 $tablet = $attrs->table->responsive->tablet;
516 $mobile = $attrs->table->responsive->mobile;
517
518 $tabletEnabled = $tablet->enabled->value();
519 $mobileEnabled = $mobile->enabled->value();
520
521 if (!$tabletEnabled && !$mobileEnabled) {
522 return '';
523 }
524
525 $tabletMaxWidth = max(1, (int) $tablet->maxWidth->value());
526 $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value());
527 $tabletStackCount = max(1, (int) $tablet->stackCount->value());
528 $mobileStackCount = max(1, (int) $mobile->stackCount->value());
529
530 return "
531 data-tableberg-responsive='true'
532 data-tableberg-rows='{$rows}'
533 data-tableberg-cols='{$cols}'
534 data-tableberg-tablet-enabled='{$tablet->enabled->asAttr()}'
535 data-tableberg-tablet-width='{$tabletMaxWidth}'
536 data-tableberg-tablet-mode='{$tablet->mode->asAttr()}'
537 data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}'
538 data-tableberg-tablet-count='{$tabletStackCount}'
539 data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}'
540 data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}'
541 data-tableberg-mobile-width='{$mobileMaxWidth}'
542 data-tableberg-mobile-mode='{$mobile->mode->asAttr()}'
543 data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}'
544 data-tableberg-mobile-count='{$mobileStackCount}'
545 data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}'
546 ";
547 }
548
549 private function getCell($row, $col, $cells) {
550 $key = $row . ',' . $col;
551
552 if (!is_array($cells) || !array_key_exists($key, $cells)) {
553 return null;
554 }
555
556 return $cells[$key];
557 }
558
559 private function getCellElements($cell) {
560 if (!$cell instanceof CellData) {
561 return [];
562 }
563
564 return is_array($cell->elements) ? $cell->elements : [];
565 }
566
567 private function getCellStyleOverride($cell) {
568 if (!$cell instanceof CellData || !is_array($cell->styles)) {
569 return null;
570 }
571
572 return StringAttr::fromNestedArray($cell->styles);
573 }
574
575 private function getCellRibbon($cell) {
576 if (!$cell instanceof CellData || !is_array($cell->ribbon)) {
577 return null;
578 }
579
580 return $cell->ribbon;
581 }
582
583 /**
584 * @param CellData|null $cell
585 * @return string|null
586 */
587 private function getCellClassName($cell) {
588 if (!$cell instanceof CellData || !$cell->className instanceof StringAttr) {
589 return null;
590 }
591
592 $className = $cell->className->asAttr();
593
594 return $className === '' ? null : $className;
595 }
596
597 private function getCellSpan($cell) {
598 $span = ['rowSpan' => 1, 'colSpan' => 1];
599
600 if ($cell instanceof CellData && $cell->span instanceof Span) {
601 $span['rowSpan'] = $cell->span->rowSpan->value();
602 $span['colSpan'] = $cell->span->colSpan->value();
603 }
604
605 return $span;
606 }
607
608 /**
609 * @param int $column
610 * @param array<int|string, ColumnConfig> $columns
611 * @return string|null
612 */
613 private function getColumnWidth($column, $columns) {
614 if (!is_array($columns) || !array_key_exists($column, $columns)) {
615 return null;
616 }
617
618 $columnConfig = $columns[$column];
619 if (!$columnConfig instanceof ColumnConfig || $columnConfig->width === null) {
620 return null;
621 }
622
623 $width = $columnConfig->width->asAttr();
624 return $width === '' ? null : $width;
625 }
626
627 /**
628 * @param int $row
629 * @param array<int|string, RowConfig> $rowConfigs
630 * @return string|null
631 */
632 private function getRowHeight($row, $rowConfigs) {
633 if (!is_array($rowConfigs) || !array_key_exists($row, $rowConfigs)) {
634 return null;
635 }
636
637 $rowConfig = $rowConfigs[$row];
638 if (!$rowConfig instanceof RowConfig || $rowConfig->height === null) {
639 return null;
640 }
641
642 $height = $rowConfig->height->asAttr();
643 return $height === '' ? null : $height;
644 }
645 }
646