PluginProbe
Tableberg – Simple Gutenberg Table Block / 1.1.3
Tableberg – Simple Gutenberg Table Block v1.1.3
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.3, at renderer/Cell/CellRenderer.php

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