| @@ -8,8 +8,9 @@ | ||
| 8 | 8 | use Tableberg\Renderer\ListEl\ListRenderer; |
| 9 | 9 | use Tableberg\Renderer\Text\TextRenderer; |
| 10 | 10 | |
| 11 | 11 | use function apply_filters; |
| 12 | +use function Tableberg\Renderer\generate_css_string; | |
| 12 | 13 | use function Tableberg\Renderer\getArrayOrNull; |
| 13 | 14 | use function Tableberg\Renderer\getStringOrNull; |
| 14 | 15 | |
| 15 | 16 | class CellRenderer { |
| @@ -46,8 +47,65 @@ | ||
| 46 | 47 | $context->cellStyleOverride, |
| 47 | 48 | $styleValues |
| 48 | 49 | ); |
| 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 | + | |
| 50 | 108 | $isStickyHeaderCell = |
| 51 | 109 | $context->stickyHeader && |
| 52 | 110 | $context->row === 0 && |
| 53 | 111 | $context->tag === 'th'; |
| @@ -54,82 +112,82 @@ | ||
| 54 | 112 | |
| 55 | 113 | $isStickyFirstColCell = |
| 56 | 114 | $context->stickyFirstCol && $context->col === 0; |
| 57 | 115 | |
| 58 | - $styleParts = ['position:relative']; | |
| 116 | + $styleParts = ['position' => 'relative']; | |
| 59 | 117 | if ($styleValues['paddingTop'] !== '') { |
| 60 | - $styleParts[] = 'padding-top:' . $styleValues['paddingTop']; | |
| 118 | + $styleParts['padding-top'] = $styleValues['paddingTop']; | |
| 61 | 119 | } |
| 62 | 120 | if ($styleValues['paddingRight'] !== '') { |
| 63 | - $styleParts[] = 'padding-right:' . $styleValues['paddingRight']; | |
| 121 | + $styleParts['padding-right'] = $styleValues['paddingRight']; | |
| 64 | 122 | } |
| 65 | 123 | if ($styleValues['paddingBottom'] !== '') { |
| 66 | - $styleParts[] = 'padding-bottom:' . $styleValues['paddingBottom']; | |
| 124 | + $styleParts['padding-bottom'] = $styleValues['paddingBottom']; | |
| 67 | 125 | } |
| 68 | 126 | if ($styleValues['paddingLeft'] !== '') { |
| 69 | - $styleParts[] = 'padding-left:' . $styleValues['paddingLeft']; | |
| 127 | + $styleParts['padding-left'] = $styleValues['paddingLeft']; | |
| 70 | 128 | } |
| 71 | 129 | if ($styleValues['verticalAlign'] !== '') { |
| 72 | - $styleParts[] = 'vertical-align:' . $styleValues['verticalAlign']; | |
| 130 | + $styleParts['vertical-align'] = $styleValues['verticalAlign']; | |
| 73 | 131 | } |
| 74 | 132 | if ($styleValues['backgroundColor'] !== '') { |
| 75 | - $styleParts[] = 'background-color:' . $styleValues['backgroundColor']; | |
| 133 | + $styleParts['background-color'] = $styleValues['backgroundColor']; | |
| 76 | 134 | } |
| 77 | 135 | if ($styleValues['borderTop'] !== '') { |
| 78 | - $styleParts[] = 'border-top:' . $styleValues['borderTop']; | |
| 136 | + $styleParts['border-top'] = $styleValues['borderTop']; | |
| 79 | 137 | } |
| 80 | 138 | if ($styleValues['borderRight'] !== '') { |
| 81 | - $styleParts[] = 'border-right:' . $styleValues['borderRight']; | |
| 139 | + $styleParts['border-right'] = $styleValues['borderRight']; | |
| 82 | 140 | } |
| 83 | 141 | if ($styleValues['borderBottom'] !== '') { |
| 84 | - $styleParts[] = 'border-bottom:' . $styleValues['borderBottom']; | |
| 142 | + $styleParts['border-bottom'] = $styleValues['borderBottom']; | |
| 85 | 143 | } |
| 86 | 144 | if ($styleValues['borderLeft'] !== '') { |
| 87 | - $styleParts[] = 'border-left:' . $styleValues['borderLeft']; | |
| 145 | + $styleParts['border-left'] = $styleValues['borderLeft']; | |
| 88 | 146 | } |
| 89 | 147 | if ($styleValues['borderTopLeftRadius'] !== '') { |
| 90 | - $styleParts[] = 'border-top-left-radius:' . $styleValues['borderTopLeftRadius']; | |
| 148 | + $styleParts['border-top-left-radius'] = $styleValues['borderTopLeftRadius']; | |
| 91 | 149 | } |
| 92 | 150 | if ($styleValues['borderTopRightRadius'] !== '') { |
| 93 | - $styleParts[] = 'border-top-right-radius:' . $styleValues['borderTopRightRadius']; | |
| 151 | + $styleParts['border-top-right-radius'] = $styleValues['borderTopRightRadius']; | |
| 94 | 152 | } |
| 95 | 153 | if ($styleValues['borderBottomRightRadius'] !== '') { |
| 96 | - $styleParts[] = 'border-bottom-right-radius:' . $styleValues['borderBottomRightRadius']; | |
| 154 | + $styleParts['border-bottom-right-radius'] = $styleValues['borderBottomRightRadius']; | |
| 97 | 155 | } |
| 98 | 156 | if ($styleValues['borderBottomLeftRadius'] !== '') { |
| 99 | - $styleParts[] = 'border-bottom-left-radius:' . $styleValues['borderBottomLeftRadius']; | |
| 157 | + $styleParts['border-bottom-left-radius'] = $styleValues['borderBottomLeftRadius']; | |
| 100 | 158 | } |
| 101 | 159 | if ($context->colSpan === 1 && $context->width !== null && $context->width !== '') { |
| 102 | - $styleParts[] = 'width:' . $context->width; | |
| 103 | - $styleParts[] = 'min-width:' . $context->width; | |
| 160 | + $styleParts['width'] = $context->width; | |
| 161 | + $styleParts['min-width'] = $context->width; | |
| 104 | 162 | } |
| 105 | 163 | if ($context->rowSpan === 1 && $context->height !== null && $context->height !== '') { |
| 106 | - $styleParts[] = 'height:' . $context->height; | |
| 107 | - $styleParts[] = 'min-height:' . $context->height; | |
| 164 | + $styleParts['height'] = $context->height; | |
| 165 | + $styleParts['min-height'] = $context->height; | |
| 108 | 166 | } |
| 109 | 167 | |
| 110 | 168 | if ($isStickyHeaderCell) { |
| 111 | - $styleParts[] = 'position:sticky'; | |
| 112 | - $styleParts[] = 'top:0'; | |
| 113 | - $styleParts[] = $isStickyFirstColCell ? 'z-index:3' : 'z-index:2'; | |
| 169 | + $styleParts['position'] = 'sticky'; | |
| 170 | + $styleParts['top'] = '0'; | |
| 171 | + $styleParts['z-index'] = $isStickyFirstColCell ? '3' : '2'; | |
| 114 | 172 | |
| 115 | 173 | if ($styleValues['backgroundColor'] === '') { |
| 116 | - $styleParts[] = 'background-color:#fff'; | |
| 174 | + $styleParts['background-color'] = '#fff'; | |
| 117 | 175 | } |
| 118 | 176 | } |
| 119 | 177 | |
| 120 | 178 | if ($isStickyFirstColCell) { |
| 121 | 179 | if (!$isStickyHeaderCell) { |
| 122 | - $styleParts[] = 'position:sticky'; | |
| 123 | - $styleParts[] = 'z-index:1'; | |
| 180 | + $styleParts['position'] = 'sticky'; | |
| 181 | + $styleParts['z-index'] = '1'; | |
| 124 | 182 | } |
| 125 | - $styleParts[] = 'left:0'; | |
| 183 | + $styleParts['left'] = '0'; | |
| 126 | 184 | |
| 127 | 185 | if ( |
| 128 | 186 | !$isStickyHeaderCell && |
| 129 | 187 | $styleValues['backgroundColor'] === '' |
| 130 | 188 | ) { |
| 131 | - $styleParts[] = 'background-color:#fff'; | |
| 189 | + $styleParts['background-color'] = '#fff'; | |
| 132 | 190 | } |
| 133 | 191 | } |
| 134 | 192 | |
| 135 | 193 | $tag = $context->tag === 'th' ? 'th' : 'td'; |
| @@ -149,9 +207,10 @@ | ||
| 149 | 207 | if (!is_object($element)) { |
| 150 | 208 | continue; |
| 151 | 209 | } |
| 152 | 210 | |
| 153 | - $name = getStringOrNull($element->name->asText()) ?? 'unknown'; | |
| 211 | + $elementName = $element->name->asText(); | |
| 212 | + $name = getStringOrNull($elementName) ?? 'unknown'; | |
| 154 | 213 | $attributes = is_array($element->attributes) |
| 155 | 214 | ? $element->attributes |
| 156 | 215 | : []; |
| 157 | 216 | |
| @@ -280,9 +339,9 @@ | ||
| 280 | 339 | > |
| 281 | 340 | ▲▼ |
| 282 | 341 | </span>" : ''; |
| 283 | 342 | |
| 284 | - $stylesStr = implode(';', $styleParts); | |
| 343 | + $stylesStr = generate_css_string($styleParts); | |
| 285 | 344 | |
| 286 | 345 | $attrsStr = ($tag === 'th' && $sortableType !== null) ? |
| 287 | 346 | "data-sortable='{$sortableType}' |
| 288 | 347 | role='button' |
| @@ -363,9 +422,10 @@ | ||
| 363 | 422 | if (!is_object($element)) { |
| 364 | 423 | continue; |
| 365 | 424 | } |
| 366 | 425 | |
| 367 | - $name = getStringOrNull($element->name->asText()) ?? 'unknown'; | |
| 426 | + $elementName = $element->name->asText(); | |
| 427 | + $name = getStringOrNull($elementName) ?? 'unknown'; | |
| 368 | 428 | $attributes = is_array($element->attributes) |
| 369 | 429 | ? $element->attributes |
| 370 | 430 | : []; |
| 371 | 431 | |
| @@ -410,13 +470,8 @@ | ||
| 410 | 470 | $styleValues['paddingLeft'] = $overridePadding['left']->asAttr(); |
| 411 | 471 | } |
| 412 | 472 | } |
| 413 | 473 | |
| 414 | - if (isset($cellStyleOverride['backgroundColor']) | |
| 415 | - && $cellStyleOverride['backgroundColor'] instanceof StringAttr) { | |
| 416 | - $styleValues['backgroundColor'] = $cellStyleOverride['backgroundColor']->asAttr(); | |
| 417 | - } | |
| 418 | - | |
| 419 | 474 | if (isset($cellStyleOverride['orientation']) |
| 420 | 475 | && $cellStyleOverride['orientation'] instanceof StringAttr) { |
| 421 | 476 | $orientation = $cellStyleOverride['orientation']->asAttr(); |
| 422 | 477 | $styleValues['orientation'] = $orientation; |
| @@ -432,29 +487,17 @@ | ||
| 432 | 487 | && $cellStyleOverride['elementGap'] instanceof StringAttr) { |
| 433 | 488 | $styleValues['elementGap'] = $cellStyleOverride['elementGap']->asAttr(); |
| 434 | 489 | } |
| 435 | 490 | |
| 436 | - if (isset($cellStyleOverride['verticalAlign']) | |
| 437 | - && $cellStyleOverride['verticalAlign'] instanceof StringAttr) { | |
| 438 | - $verticalAlign = $cellStyleOverride['verticalAlign']->asAttr(); | |
| 439 | - $styleValues['verticalAlign'] = $verticalAlign; | |
| 440 | - } | |
| 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. | |
| 441 | 494 | |
| 442 | - $overrideBorder = getArrayOrNull($cellStyleOverride['border']); | |
| 443 | - if (is_array($overrideBorder)) { | |
| 444 | - if (isset($overrideBorder['top']) && $overrideBorder['top'] instanceof StringAttr) { | |
| 445 | - $styleValues['borderTop'] = $overrideBorder['top']->asAttr(); | |
| 446 | - } | |
| 447 | - if (isset($overrideBorder['right']) && $overrideBorder['right'] instanceof StringAttr) { | |
| 448 | - $styleValues['borderRight'] = $overrideBorder['right']->asAttr(); | |
| 449 | - } | |
| 450 | - if (isset($overrideBorder['bottom']) && $overrideBorder['bottom'] instanceof StringAttr) { | |
| 451 | - $styleValues['borderBottom'] = $overrideBorder['bottom']->asAttr(); | |
| 452 | - } | |
| 453 | - if (isset($overrideBorder['left']) && $overrideBorder['left'] instanceof StringAttr) { | |
| 454 | - $styleValues['borderLeft'] = $overrideBorder['left']->asAttr(); | |
| 455 | - } | |
| 456 | - } | |
| 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. | |
| 457 | 500 | |
| 458 | 501 | $overrideBorderRadius = getArrayOrNull($cellStyleOverride['borderRadius']); |
| 459 | 502 | if (is_array($overrideBorderRadius)) { |
| 460 | 503 | if (isset($overrideBorderRadius['topLeft']) |