| @@ -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,75 +47,150 @@ | ||
| 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 | 112 | |
| 55 | - $styleParts = ['position:relative']; | |
| 113 | + $isStickyFirstColCell = | |
| 114 | + $context->stickyFirstCol && $context->col === 0; | |
| 115 | + | |
| 116 | + $styleParts = ['position' => 'relative']; | |
| 56 | 117 | if ($styleValues['paddingTop'] !== '') { |
| 57 | - $styleParts[] = 'padding-top:' . $styleValues['paddingTop']; | |
| 118 | + $styleParts['padding-top'] = $styleValues['paddingTop']; | |
| 58 | 119 | } |
| 59 | 120 | if ($styleValues['paddingRight'] !== '') { |
| 60 | - $styleParts[] = 'padding-right:' . $styleValues['paddingRight']; | |
| 121 | + $styleParts['padding-right'] = $styleValues['paddingRight']; | |
| 61 | 122 | } |
| 62 | 123 | if ($styleValues['paddingBottom'] !== '') { |
| 63 | - $styleParts[] = 'padding-bottom:' . $styleValues['paddingBottom']; | |
| 124 | + $styleParts['padding-bottom'] = $styleValues['paddingBottom']; | |
| 64 | 125 | } |
| 65 | 126 | if ($styleValues['paddingLeft'] !== '') { |
| 66 | - $styleParts[] = 'padding-left:' . $styleValues['paddingLeft']; | |
| 127 | + $styleParts['padding-left'] = $styleValues['paddingLeft']; | |
| 67 | 128 | } |
| 68 | 129 | if ($styleValues['verticalAlign'] !== '') { |
| 69 | - $styleParts[] = 'vertical-align:' . $styleValues['verticalAlign']; | |
| 130 | + $styleParts['vertical-align'] = $styleValues['verticalAlign']; | |
| 70 | 131 | } |
| 71 | 132 | if ($styleValues['backgroundColor'] !== '') { |
| 72 | - $styleParts[] = 'background-color:' . $styleValues['backgroundColor']; | |
| 133 | + $styleParts['background-color'] = $styleValues['backgroundColor']; | |
| 73 | 134 | } |
| 74 | 135 | if ($styleValues['borderTop'] !== '') { |
| 75 | - $styleParts[] = 'border-top:' . $styleValues['borderTop']; | |
| 136 | + $styleParts['border-top'] = $styleValues['borderTop']; | |
| 76 | 137 | } |
| 77 | 138 | if ($styleValues['borderRight'] !== '') { |
| 78 | - $styleParts[] = 'border-right:' . $styleValues['borderRight']; | |
| 139 | + $styleParts['border-right'] = $styleValues['borderRight']; | |
| 79 | 140 | } |
| 80 | 141 | if ($styleValues['borderBottom'] !== '') { |
| 81 | - $styleParts[] = 'border-bottom:' . $styleValues['borderBottom']; | |
| 142 | + $styleParts['border-bottom'] = $styleValues['borderBottom']; | |
| 82 | 143 | } |
| 83 | 144 | if ($styleValues['borderLeft'] !== '') { |
| 84 | - $styleParts[] = 'border-left:' . $styleValues['borderLeft']; | |
| 145 | + $styleParts['border-left'] = $styleValues['borderLeft']; | |
| 85 | 146 | } |
| 86 | 147 | if ($styleValues['borderTopLeftRadius'] !== '') { |
| 87 | - $styleParts[] = 'border-top-left-radius:' . $styleValues['borderTopLeftRadius']; | |
| 148 | + $styleParts['border-top-left-radius'] = $styleValues['borderTopLeftRadius']; | |
| 88 | 149 | } |
| 89 | 150 | if ($styleValues['borderTopRightRadius'] !== '') { |
| 90 | - $styleParts[] = 'border-top-right-radius:' . $styleValues['borderTopRightRadius']; | |
| 151 | + $styleParts['border-top-right-radius'] = $styleValues['borderTopRightRadius']; | |
| 91 | 152 | } |
| 92 | 153 | if ($styleValues['borderBottomRightRadius'] !== '') { |
| 93 | - $styleParts[] = 'border-bottom-right-radius:' . $styleValues['borderBottomRightRadius']; | |
| 154 | + $styleParts['border-bottom-right-radius'] = $styleValues['borderBottomRightRadius']; | |
| 94 | 155 | } |
| 95 | 156 | if ($styleValues['borderBottomLeftRadius'] !== '') { |
| 96 | - $styleParts[] = 'border-bottom-left-radius:' . $styleValues['borderBottomLeftRadius']; | |
| 157 | + $styleParts['border-bottom-left-radius'] = $styleValues['borderBottomLeftRadius']; | |
| 97 | 158 | } |
| 98 | 159 | if ($context->colSpan === 1 && $context->width !== null && $context->width !== '') { |
| 99 | - $styleParts[] = 'width:' . $context->width; | |
| 100 | - $styleParts[] = 'min-width:' . $context->width; | |
| 160 | + $styleParts['width'] = $context->width; | |
| 161 | + $styleParts['min-width'] = $context->width; | |
| 101 | 162 | } |
| 102 | 163 | if ($context->rowSpan === 1 && $context->height !== null && $context->height !== '') { |
| 103 | - $styleParts[] = 'height:' . $context->height; | |
| 104 | - $styleParts[] = 'min-height:' . $context->height; | |
| 164 | + $styleParts['height'] = $context->height; | |
| 165 | + $styleParts['min-height'] = $context->height; | |
| 105 | 166 | } |
| 106 | 167 | |
| 107 | 168 | if ($isStickyHeaderCell) { |
| 108 | - $styleParts[] = 'position:sticky'; | |
| 109 | - $styleParts[] = 'top:0'; | |
| 110 | - $styleParts[] = 'z-index:2'; | |
| 169 | + $styleParts['position'] = 'sticky'; | |
| 170 | + $styleParts['top'] = '0'; | |
| 171 | + $styleParts['z-index'] = $isStickyFirstColCell ? '3' : '2'; | |
| 111 | 172 | |
| 112 | 173 | if ($styleValues['backgroundColor'] === '') { |
| 113 | - $styleParts[] = 'background-color:#fff'; | |
| 174 | + $styleParts['background-color'] = '#fff'; | |
| 114 | 175 | } |
| 115 | 176 | } |
| 116 | 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 | + | |
| 117 | 193 | $tag = $context->tag === 'th' ? 'th' : 'td'; |
| 118 | 194 | $sortableType = getStringOrNull($context->sortableType); |
| 119 | 195 | |
| 120 | 196 | $buttonRenderer = new ButtonRenderer(); |
| @@ -131,9 +207,10 @@ | ||
| 131 | 207 | if (!is_object($element)) { |
| 132 | 208 | continue; |
| 133 | 209 | } |
| 134 | 210 | |
| 135 | - $name = getStringOrNull($element->name->asText()) ?? 'unknown'; | |
| 211 | + $elementName = $element->name->asText(); | |
| 212 | + $name = getStringOrNull($elementName) ?? 'unknown'; | |
| 136 | 213 | $attributes = is_array($element->attributes) |
| 137 | 214 | ? $element->attributes |
| 138 | 215 | : []; |
| 139 | 216 | |
| @@ -262,9 +339,9 @@ | ||
| 262 | 339 | > |
| 263 | 340 | ▲▼ |
| 264 | 341 | </span>" : ''; |
| 265 | 342 | |
| 266 | - $stylesStr = implode(';', $styleParts); | |
| 343 | + $stylesStr = generate_css_string($styleParts); | |
| 267 | 344 | |
| 268 | 345 | $attrsStr = ($tag === 'th' && $sortableType !== null) ? |
| 269 | 346 | "data-sortable='{$sortableType}' |
| 270 | 347 | role='button' |
| @@ -345,9 +422,10 @@ | ||
| 345 | 422 | if (!is_object($element)) { |
| 346 | 423 | continue; |
| 347 | 424 | } |
| 348 | 425 | |
| 349 | - $name = getStringOrNull($element->name->asText()) ?? 'unknown'; | |
| 426 | + $elementName = $element->name->asText(); | |
| 427 | + $name = getStringOrNull($elementName) ?? 'unknown'; | |
| 350 | 428 | $attributes = is_array($element->attributes) |
| 351 | 429 | ? $element->attributes |
| 352 | 430 | : []; |
| 353 | 431 | |
| @@ -392,13 +470,8 @@ | ||
| 392 | 470 | $styleValues['paddingLeft'] = $overridePadding['left']->asAttr(); |
| 393 | 471 | } |
| 394 | 472 | } |
| 395 | 473 | |
| 396 | - if (isset($cellStyleOverride['backgroundColor']) | |
| 397 | - && $cellStyleOverride['backgroundColor'] instanceof StringAttr) { | |
| 398 | - $styleValues['backgroundColor'] = $cellStyleOverride['backgroundColor']->asAttr(); | |
| 399 | - } | |
| 400 | - | |
| 401 | 474 | if (isset($cellStyleOverride['orientation']) |
| 402 | 475 | && $cellStyleOverride['orientation'] instanceof StringAttr) { |
| 403 | 476 | $orientation = $cellStyleOverride['orientation']->asAttr(); |
| 404 | 477 | $styleValues['orientation'] = $orientation; |
| @@ -414,29 +487,17 @@ | ||
| 414 | 487 | && $cellStyleOverride['elementGap'] instanceof StringAttr) { |
| 415 | 488 | $styleValues['elementGap'] = $cellStyleOverride['elementGap']->asAttr(); |
| 416 | 489 | } |
| 417 | 490 | |
| 418 | - if (isset($cellStyleOverride['verticalAlign']) | |
| 419 | - && $cellStyleOverride['verticalAlign'] instanceof StringAttr) { | |
| 420 | - $verticalAlign = $cellStyleOverride['verticalAlign']->asAttr(); | |
| 421 | - $styleValues['verticalAlign'] = $verticalAlign; | |
| 422 | - } | |
| 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. | |
| 423 | 494 | |
| 424 | - $overrideBorder = getArrayOrNull($cellStyleOverride['border']); | |
| 425 | - if (is_array($overrideBorder)) { | |
| 426 | - if (isset($overrideBorder['top']) && $overrideBorder['top'] instanceof StringAttr) { | |
| 427 | - $styleValues['borderTop'] = $overrideBorder['top']->asAttr(); | |
| 428 | - } | |
| 429 | - if (isset($overrideBorder['right']) && $overrideBorder['right'] instanceof StringAttr) { | |
| 430 | - $styleValues['borderRight'] = $overrideBorder['right']->asAttr(); | |
| 431 | - } | |
| 432 | - if (isset($overrideBorder['bottom']) && $overrideBorder['bottom'] instanceof StringAttr) { | |
| 433 | - $styleValues['borderBottom'] = $overrideBorder['bottom']->asAttr(); | |
| 434 | - } | |
| 435 | - if (isset($overrideBorder['left']) && $overrideBorder['left'] instanceof StringAttr) { | |
| 436 | - $styleValues['borderLeft'] = $overrideBorder['left']->asAttr(); | |
| 437 | - } | |
| 438 | - } | |
| 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. | |
| 439 | 500 | |
| 440 | 501 | $overrideBorderRadius = getArrayOrNull($cellStyleOverride['borderRadius']); |
| 441 | 502 | if (is_array($overrideBorderRadius)) { |
| 442 | 503 | if (isset($overrideBorderRadius['topLeft']) |