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

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