PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.5
Tableberg – Simple Gutenberg Table Block v1.1.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 / Cell / CellRenderer.php

CellRenderer.php in Tableberg – Simple Gutenberg Table Block 1.1.5, at renderer/Cell/CellRenderer.php

524 lines 19.0 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\Cell;
4
5 use Tableberg\Renderer\Attrs\StringAttr;
6 use Tableberg\Renderer\Button\ButtonRenderer;
7 use Tableberg\Renderer\Image\ImageRenderer;
8 use Tableberg\Renderer\ListEl\ListRenderer;
9 use Tableberg\Renderer\Text\TextRenderer;
10
11 use function apply_filters;
12 use function Tableberg\Renderer\generate_css_string;
13 use function Tableberg\Renderer\getArrayOrNull;
14 use function Tableberg\Renderer\getStringOrNull;
15
16 class CellRenderer {
17 /**
18 * @param CellRenderContext $context
19 * @return string
20 */
21 public function render(CellRenderContext $context) {
22 if (!$context->hasCellCoords) {
23 return '';
24 }
25
26 $styleValues = [
27 'paddingTop' => $context->paddingTop,
28 'paddingRight' => $context->paddingRight,
29 'paddingBottom' => $context->paddingBottom,
30 'paddingLeft' => $context->paddingLeft,
31 'orientation' => $context->orientation,
32 'elementGap' => $context->elementGap,
33 'wrap' => $context->wrap,
34 'verticalAlign' => $context->verticalAlign,
35 'backgroundColor' => $context->backgroundColor,
36 'borderTop' => $context->borderTop,
37 'borderRight' => $context->borderRight,
38 'borderBottom' => $context->borderBottom,
39 'borderLeft' => $context->borderLeft,
40 'borderTopLeftRadius' => $context->borderTopLeftRadius,
41 'borderTopRightRadius' => $context->borderTopRightRadius,
42 'borderBottomRightRadius' => $context->borderBottomRightRadius,
43 'borderBottomLeftRadius' => $context->borderBottomLeftRadius,
44 ];
45
46 $styleValues = $this->overrideStyles(
47 $context->cellStyleOverride,
48 $styleValues
49 );
50
51 // Pro styling of a single cell goes through one filter rather than
52 // one per property, so a new pro style needs no change here. Pro
53 // gets the styles free resolved plus every attribute of the cell,
54 // and returns the ones it wants rendered.
55 //
56 // Applied after the `styles` overrides rather than inside them: a
57 // cell can carry a pro attribute without any `styles` at all.
58 $proStyleValues = apply_filters(
59 'tableberg/cell_styles',
60 $styleValues,
61 $context->cellAttrs,
62 $context
63 );
64 if (is_array($proStyleValues)) {
65 // Merged, not replaced, so a filter that returns a partial set
66 // cannot leave the renderer with missing keys.
67 $styleValues = array_merge($styleValues, $proStyleValues);
68 }
69
70 // Table-wide Pro grid modes win over common and individual cell
71 // borders. They render inner separators only; the separate table
72 // border control owns the outside rectangle.
73 if ($context->innerBorderType === 'row') {
74 $styleValues['borderLeft'] = '';
75 $styleValues['borderRight'] = '';
76 if ($context->row === 0) {
77 $styleValues['borderTop'] = '';
78 }
79 if ($context->row + $context->rowSpan >= $context->totalRows) {
80 $styleValues['borderBottom'] = '';
81 }
82 } elseif ($context->innerBorderType === 'col') {
83 $styleValues['borderTop'] = '';
84 $styleValues['borderBottom'] = '';
85 if ($context->col === 0) {
86 $styleValues['borderLeft'] = '';
87 }
88 if ($context->col + $context->colSpan >= $context->totalCols) {
89 $styleValues['borderRight'] = '';
90 }
91 }
92
93 if ($context->isEmpty) {
94 // An "empty cell" renders as a bare, unstyled cell on the
95 // frontend: no content, and no background/border either, so it
96 // is indistinguishable from a cell that was never styled.
97 $styleValues['backgroundColor'] = '';
98 $styleValues['borderTop'] = '';
99 $styleValues['borderRight'] = '';
100 $styleValues['borderBottom'] = '';
101 $styleValues['borderLeft'] = '';
102 $styleValues['borderTopLeftRadius'] = '';
103 $styleValues['borderTopRightRadius'] = '';
104 $styleValues['borderBottomRightRadius'] = '';
105 $styleValues['borderBottomLeftRadius'] = '';
106 }
107
108 $isStickyHeaderCell =
109 $context->stickyHeader &&
110 $context->row === 0 &&
111 $context->tag === 'th';
112
113 $isStickyFirstColCell =
114 $context->stickyFirstCol && $context->col === 0;
115
116 $styleParts = ['position' => 'relative'];
117 if ($styleValues['paddingTop'] !== '') {
118 $styleParts['padding-top'] = $styleValues['paddingTop'];
119 }
120 if ($styleValues['paddingRight'] !== '') {
121 $styleParts['padding-right'] = $styleValues['paddingRight'];
122 }
123 if ($styleValues['paddingBottom'] !== '') {
124 $styleParts['padding-bottom'] = $styleValues['paddingBottom'];
125 }
126 if ($styleValues['paddingLeft'] !== '') {
127 $styleParts['padding-left'] = $styleValues['paddingLeft'];
128 }
129 if ($styleValues['verticalAlign'] !== '') {
130 $styleParts['vertical-align'] = $styleValues['verticalAlign'];
131 }
132 if ($styleValues['backgroundColor'] !== '') {
133 $styleParts['background-color'] = $styleValues['backgroundColor'];
134 }
135 if ($styleValues['borderTop'] !== '') {
136 $styleParts['border-top'] = $styleValues['borderTop'];
137 }
138 if ($styleValues['borderRight'] !== '') {
139 $styleParts['border-right'] = $styleValues['borderRight'];
140 }
141 if ($styleValues['borderBottom'] !== '') {
142 $styleParts['border-bottom'] = $styleValues['borderBottom'];
143 }
144 if ($styleValues['borderLeft'] !== '') {
145 $styleParts['border-left'] = $styleValues['borderLeft'];
146 }
147 if ($styleValues['borderTopLeftRadius'] !== '') {
148 $styleParts['border-top-left-radius'] = $styleValues['borderTopLeftRadius'];
149 }
150 if ($styleValues['borderTopRightRadius'] !== '') {
151 $styleParts['border-top-right-radius'] = $styleValues['borderTopRightRadius'];
152 }
153 if ($styleValues['borderBottomRightRadius'] !== '') {
154 $styleParts['border-bottom-right-radius'] = $styleValues['borderBottomRightRadius'];
155 }
156 if ($styleValues['borderBottomLeftRadius'] !== '') {
157 $styleParts['border-bottom-left-radius'] = $styleValues['borderBottomLeftRadius'];
158 }
159 if ($context->colSpan === 1 && $context->width !== null && $context->width !== '') {
160 $styleParts['width'] = $context->width;
161 $styleParts['min-width'] = $context->width;
162 }
163 if ($context->rowSpan === 1 && $context->height !== null && $context->height !== '') {
164 $styleParts['height'] = $context->height;
165 $styleParts['min-height'] = $context->height;
166 }
167
168 if ($isStickyHeaderCell) {
169 $styleParts['position'] = 'sticky';
170 $styleParts['top'] = '0';
171 $styleParts['z-index'] = $isStickyFirstColCell ? '3' : '2';
172
173 if ($styleValues['backgroundColor'] === '') {
174 $styleParts['background-color'] = '#fff';
175 }
176 }
177
178 if ($isStickyFirstColCell) {
179 if (!$isStickyHeaderCell) {
180 $styleParts['position'] = 'sticky';
181 $styleParts['z-index'] = '1';
182 }
183 $styleParts['left'] = '0';
184
185 if (
186 !$isStickyHeaderCell &&
187 $styleValues['backgroundColor'] === ''
188 ) {
189 $styleParts['background-color'] = '#fff';
190 }
191 }
192
193 $tag = $context->tag === 'th' ? 'th' : 'td';
194 $sortableType = getStringOrNull($context->sortableType);
195
196 $buttonRenderer = new ButtonRenderer();
197 $imageRenderer = new ImageRenderer();
198 $listRenderer = new ListRenderer();
199 $textRenderer = new TextRenderer();
200
201 $elementsHtml = '';
202
203 $isHorizontal = $styleValues['orientation'] === 'horizontal';
204 $implicitAlignment = $this->getUniformElementsAlignment($context->elements);
205
206 foreach ($context->elements as $element) {
207 if (!is_object($element)) {
208 continue;
209 }
210
211 $elementName = $element->name->asText();
212 $name = getStringOrNull($elementName) ?? 'unknown';
213 $attributes = is_array($element->attributes)
214 ? $element->attributes
215 : [];
216
217 $elementHtml = '';
218
219 if ($name === 'text') {
220 $elementHtml = $textRenderer->render($attributes);
221 }
222 if ($name === 'button') {
223 $elementHtml = $buttonRenderer->render($attributes);
224 }
225 if ($name === 'image') {
226 $elementHtml = $imageRenderer->render($attributes);
227 }
228 if ($name === 'list') {
229 $elementHtml = $listRenderer->render($attributes);
230 }
231
232 if ($elementHtml === '') {
233 $elementHtml = apply_filters(
234 'tableberg/render_cell_element_html',
235 '',
236 $name,
237 $attributes,
238 $context
239 );
240
241 if (!is_string($elementHtml)) {
242 $elementHtml = '';
243 }
244 }
245
246 if ($elementHtml === '') {
247 $elementHtml =
248 "<div
249 class='tableberg-cell-element-placeholder'
250 data-element-name='$name'
251 >
252 $name
253 </div>";
254 }
255
256 $elementAlign = $this->getElementAlignment(
257 $name,
258 $attributes
259 );
260 $elementWrapperStyles = [
261 'display:flex',
262 'justify-content:' . $this->alignmentToJustifyContent($elementAlign),
263 ];
264
265 if (!$isHorizontal) {
266 $elementWrapperStyles[] = 'width:100%';
267 }
268
269 $elementsHtml .=
270 "<div
271 class='tableberg-cell-element'
272 style='" . implode(';', $elementWrapperStyles) . "'
273 >
274 {$elementHtml}
275 </div>";
276 }
277
278 $elementsWrapperClasses = ['tableberg-cell-elements'];
279 $elementsWrapperStyleParts = ['display:flex'];
280
281 if ($isHorizontal) {
282 $elementsWrapperClasses[] = 'tableberg-cell-elements-horizontal';
283 }
284
285 $elementsWrapperStyleParts[] = 'flex-direction:' . ($isHorizontal ? 'row' : 'column');
286
287 $verticalAlignFlex = 'center';
288 if ($styleValues['verticalAlign'] === 'top') {
289 $verticalAlignFlex = 'flex-start';
290 } elseif ($styleValues['verticalAlign'] === 'bottom') {
291 $verticalAlignFlex = 'flex-end';
292 }
293
294 if ($isHorizontal) {
295 $elementsWrapperStyleParts[] =
296 'justify-content:' .
297 $this->alignmentToJustifyContent($implicitAlignment);
298 $elementsWrapperStyleParts[] = 'align-items:' . $verticalAlignFlex;
299 } else {
300 $elementsWrapperStyleParts[] = 'justify-content:' . $verticalAlignFlex;
301 $elementsWrapperStyleParts[] = 'align-items:stretch';
302 }
303
304 $elementsWrapperStyleParts[] =
305 'flex-wrap:' . ($styleValues['wrap'] === 'nowrap' ? 'nowrap' : 'wrap');
306
307 if ($styleValues['elementGap'] !== '') {
308 $elementsWrapperStyleParts[] = 'gap:' . $styleValues['elementGap'];
309 }
310
311 $elementsWrapperClassAttr = implode(' ', $elementsWrapperClasses);
312 $elementsWrapperStyleAttr = '';
313 $elementsWrapperStyleAttr = "style='" . implode(';', $elementsWrapperStyleParts) . "'";
314
315 $elementsWrapperHtml =
316 "<div
317 class='{$elementsWrapperClassAttr}'
318 {$elementsWrapperStyleAttr}
319 >
320 {$elementsHtml}
321 </div>";
322
323 $ribbonHtml = apply_filters(
324 'tableberg/render_cell_ribbon_html',
325 '',
326 $context->ribbon,
327 $context
328 );
329
330 if (!is_string($ribbonHtml)) {
331 $ribbonHtml = '';
332 }
333
334 $cellIsSortableHeader = $tag === 'th' && $sortableType !== null;
335 $sortIndicatorHtml = $cellIsSortableHeader ?
336 "<span
337 class='tableberg-sort-indicator'
338 aria-hidden='true'
339 >
340 &#9650;&#9660;
341 </span>" : '';
342
343 $stylesStr = generate_css_string($styleParts);
344
345 $attrsStr = ($tag === 'th' && $sortableType !== null) ?
346 "data-sortable='{$sortableType}'
347 role='button'
348 tabindex='0'
349 aria-sort='none'" : '';
350
351 $classAttr = $context->className !== ''
352 ? " class='" . \esc_attr($context->className) . "'"
353 : '';
354
355 $html =
356 "<$tag
357 rowspan='{$context->rowSpan}'
358 colspan='{$context->colSpan}'
359 {$classAttr}
360 style='$stylesStr'
361 data-cell-row='{$context->row}'
362 data-cell-col='{$context->col}'
363 {$attrsStr}
364 >
365 $elementsWrapperHtml
366 $sortIndicatorHtml
367 $ribbonHtml
368 </$tag>";
369
370 return $html;
371 }
372
373 /**
374 * @param string $elementName
375 * @param array<string, mixed> $attributes
376 * @return string
377 */
378 private function getElementAlignment($elementName, $attributes) {
379 if ($elementName === 'icon') {
380 $styles = getArrayOrNull($attributes['styles']) ?? [];
381 $styleAlign = getStringOrNull($styles['align']);
382 if (in_array($styleAlign, ['left', 'center', 'right'], true)) {
383 return $styleAlign;
384 }
385
386 return 'left';
387 }
388
389 $topLevelAlign = getStringOrNull($attributes['align']);
390 if (in_array($topLevelAlign, ['left', 'center', 'right'], true)) {
391 return $topLevelAlign;
392 }
393
394 return 'left';
395 }
396
397 /**
398 * @param string $alignment
399 * @return string
400 */
401 private function alignmentToJustifyContent($alignment) {
402 if ($alignment === 'center') {
403 return 'center';
404 }
405
406 if ($alignment === 'right') {
407 return 'flex-end';
408 }
409
410 return 'flex-start';
411 }
412
413 /**
414 * @param array<int, mixed> $elements
415 * @return string
416 */
417 private function getUniformElementsAlignment($elements) {
418 $resolvedAlignment = null;
419 $hasElements = false;
420
421 foreach ($elements as $element) {
422 if (!is_object($element)) {
423 continue;
424 }
425
426 $elementName = $element->name->asText();
427 $name = getStringOrNull($elementName) ?? 'unknown';
428 $attributes = is_array($element->attributes)
429 ? $element->attributes
430 : [];
431
432 $currentAlignment = $this->getElementAlignment($name, $attributes);
433
434 if (!$hasElements) {
435 $resolvedAlignment = $currentAlignment;
436 $hasElements = true;
437 continue;
438 }
439
440 if ($resolvedAlignment !== $currentAlignment) {
441 return 'left';
442 }
443 }
444
445 return $hasElements ? $resolvedAlignment : 'left';
446 }
447
448 /**
449 * @param array<string, StringAttr|array>|null $cellStyleOverride
450 * @param array<string, string> $styleValues
451 * @return array<string, string>
452 */
453 private function overrideStyles($cellStyleOverride, $styleValues) {
454 if (!is_array($cellStyleOverride)) {
455 return $styleValues;
456 }
457
458 $overridePadding = getArrayOrNull($cellStyleOverride['padding']);
459 if (is_array($overridePadding)) {
460 if (isset($overridePadding['top']) && $overridePadding['top'] instanceof StringAttr) {
461 $styleValues['paddingTop'] = $overridePadding['top']->asAttr();
462 }
463 if (isset($overridePadding['right']) && $overridePadding['right'] instanceof StringAttr) {
464 $styleValues['paddingRight'] = $overridePadding['right']->asAttr();
465 }
466 if (isset($overridePadding['bottom']) && $overridePadding['bottom'] instanceof StringAttr) {
467 $styleValues['paddingBottom'] = $overridePadding['bottom']->asAttr();
468 }
469 if (isset($overridePadding['left']) && $overridePadding['left'] instanceof StringAttr) {
470 $styleValues['paddingLeft'] = $overridePadding['left']->asAttr();
471 }
472 }
473
474 if (isset($cellStyleOverride['orientation'])
475 && $cellStyleOverride['orientation'] instanceof StringAttr) {
476 $orientation = $cellStyleOverride['orientation']->asAttr();
477 $styleValues['orientation'] = $orientation;
478 }
479
480 if (isset($cellStyleOverride['wrap'])
481 && $cellStyleOverride['wrap'] instanceof StringAttr) {
482 $wrap = $cellStyleOverride['wrap']->asAttr();
483 $styleValues['wrap'] = $wrap;
484 }
485
486 if (isset($cellStyleOverride['elementGap'])
487 && $cellStyleOverride['elementGap'] instanceof StringAttr) {
488 $styleValues['elementGap'] = $cellStyleOverride['elementGap']->asAttr();
489 }
490
491 // A cell's own vertical alignment is a pro feature, resolved through
492 // the `tableberg/cell_styles` filter below. Free only applies the
493 // table-wide default, so it can't be switched on without a licence.
494
495 // Border is a pro attribute now (moved off this shared `styles`
496 // object, same as backgroundColor before it), resolved instead
497 // through the `tableberg/cell_styles` filter below — not here, or
498 // it would keep rendering a lapsed/legacy border regardless of
499 // licence status.
500
501 $overrideBorderRadius = getArrayOrNull($cellStyleOverride['borderRadius']);
502 if (is_array($overrideBorderRadius)) {
503 if (isset($overrideBorderRadius['topLeft'])
504 && $overrideBorderRadius['topLeft'] instanceof StringAttr) {
505 $styleValues['borderTopLeftRadius'] = $overrideBorderRadius['topLeft']->asAttr();
506 }
507 if (isset($overrideBorderRadius['topRight'])
508 && $overrideBorderRadius['topRight'] instanceof StringAttr) {
509 $styleValues['borderTopRightRadius'] = $overrideBorderRadius['topRight']->asAttr();
510 }
511 if (isset($overrideBorderRadius['bottomRight'])
512 && $overrideBorderRadius['bottomRight'] instanceof StringAttr) {
513 $styleValues['borderBottomRightRadius'] = $overrideBorderRadius['bottomRight']->asAttr();
514 }
515 if (isset($overrideBorderRadius['bottomLeft'])
516 && $overrideBorderRadius['bottomLeft'] instanceof StringAttr) {
517 $styleValues['borderBottomLeftRadius'] = $overrideBorderRadius['bottomLeft']->asAttr();
518 }
519 }
520
521 return $styleValues;
522 }
523 }
524