| @@ -69,8 +69,19 @@ | ||
| 69 | 69 | ); |
| 70 | 70 | $stickyHeader = !empty($stickySettings['stickyHeader']); |
| 71 | 71 | $stickyFirstCol = !empty($stickySettings['stickyFirstCol']); |
| 72 | 72 | |
| 73 | + // Row-only/column-only is Pro-owned. Free's empty default preserves | |
| 74 | + // the attribute without enabling its editor or frontend behavior. | |
| 75 | + $innerBorderType = apply_filters( | |
| 76 | + 'tableberg/inner_border_type', | |
| 77 | + '', | |
| 78 | + $attrs->attrs | |
| 79 | + ); | |
| 80 | + if (!in_array($innerBorderType, ['row', 'col'], true)) { | |
| 81 | + $innerBorderType = ''; | |
| 82 | + } | |
| 83 | + | |
| 73 | 84 | $caption = $attrs->table->caption; |
| 74 | 85 | $tableWidth = trim($attrs->table->tableWidth->asAttr()); |
| 75 | 86 | $tableAlignment = $attrs->table->tableAlignment->asAttr(); |
| 76 | 87 | $cellSpacingHorizontal = trim($attrs->table->cellSpacing->horizontal->asAttr()); |
| @@ -79,8 +90,24 @@ | ||
| 79 | 90 | $tableBorderRight = trim($attrs->table->tableBorder->right->asAttr()); |
| 80 | 91 | $tableBorderBottom = trim($attrs->table->tableBorder->bottom->asAttr()); |
| 81 | 92 | $tableBorderLeft = trim($attrs->table->tableBorder->left->asAttr()); |
| 82 | 93 | $fixedColumnWidths = $attrs->table->fixedColumnWidths->value(); |
| 94 | + $tableRadius = [ | |
| 95 | + 'topLeft' => $attrs->cellDefaults->styles->borderRadius->topLeft->asAttr(), | |
| 96 | + 'topRight' => $attrs->cellDefaults->styles->borderRadius->topRight->asAttr(), | |
| 97 | + 'bottomRight' => $attrs->cellDefaults->styles->borderRadius->bottomRight->asAttr(), | |
| 98 | + 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(), | |
| 99 | + ]; | |
| 100 | + $isZeroRadius = function ($value) { | |
| 101 | + $trimmed = trim((string) $value); | |
| 102 | + return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1; | |
| 103 | + }; | |
| 104 | + $hasRoundedCorners = count(array_filter( | |
| 105 | + $tableRadius, | |
| 106 | + function ($value) use ($isZeroRadius) { | |
| 107 | + return !$isZeroRadius($value); | |
| 108 | + } | |
| 109 | + )) > 0; | |
| 83 | 110 | |
| 84 | 111 | $isHorizontalSpacingZero = $cellSpacingHorizontal === '0'; |
| 85 | 112 | $isVerticalSpacingZero = $cellSpacingVertical === '0'; |
| 86 | 113 | $hasCellSpacing = !$isHorizontalSpacingZero || !$isVerticalSpacingZero; |
| @@ -106,8 +133,11 @@ | ||
| 106 | 133 | ); |
| 107 | 134 | |
| 108 | 135 | $tableStyles = [ |
| 109 | 136 | 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'), |
| 137 | + // Neutralize theme-level table borders. Tableberg owns its | |
| 138 | + // borders through the table/cell controls and rounded wrapper. | |
| 139 | + 'border: 0', | |
| 110 | 140 | ]; |
| 111 | 141 | |
| 112 | 142 | if ($hasCellSpacing) { |
| 113 | 143 | $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}"; |
| @@ -119,21 +149,21 @@ | ||
| 119 | 149 | } else { |
| 120 | 150 | $tableStyles[] = 'width: 100%'; |
| 121 | 151 | } |
| 122 | 152 | |
| 123 | - if ($tableBorderTop !== '') { | |
| 153 | + if (!$hasRoundedCorners && $tableBorderTop !== '') { | |
| 124 | 154 | $tableStyles[] = "border-top: {$tableBorderTop}"; |
| 125 | 155 | } |
| 126 | 156 | |
| 127 | - if ($tableBorderRight !== '') { | |
| 157 | + if (!$hasRoundedCorners && $tableBorderRight !== '') { | |
| 128 | 158 | $tableStyles[] = "border-right: {$tableBorderRight}"; |
| 129 | 159 | } |
| 130 | 160 | |
| 131 | - if ($tableBorderBottom !== '') { | |
| 161 | + if (!$hasRoundedCorners && $tableBorderBottom !== '') { | |
| 132 | 162 | $tableStyles[] = "border-bottom: {$tableBorderBottom}"; |
| 133 | 163 | } |
| 134 | 164 | |
| 135 | - if ($tableBorderLeft !== '') { | |
| 165 | + if (!$hasRoundedCorners && $tableBorderLeft !== '') { | |
| 136 | 166 | $tableStyles[] = "border-left: {$tableBorderLeft}"; |
| 137 | 167 | } |
| 138 | 168 | |
| 139 | 169 | $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'"; |
| @@ -244,13 +274,18 @@ | ||
| 244 | 274 | 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(), |
| 245 | 275 | ], |
| 246 | 276 | ]; |
| 247 | 277 | |
| 248 | - // The table border radius rounds the whole table's outer corners. | |
| 249 | - // border-radius on collapsed-border tables/cells is ignored by | |
| 250 | - // browsers, so it is rendered on the wrapper (with overflow:hidden) | |
| 251 | - // and removed from the individual cells here. | |
| 252 | - $tableRadius = $globalCellStyles['borderRadius']; | |
| 278 | + // A collapsed table cannot reliably round its own explicit border. | |
| 279 | + // The wrapper owns only the table border and clips the cell grid to | |
| 280 | + // it. Cell borders stay on cells; copying them to the wrapper would | |
| 281 | + // add an outline that the user did not configure as a table border. | |
| 282 | + $tableOuterBorder = [ | |
| 283 | + 'top' => $tableBorderTop, | |
| 284 | + 'right' => $tableBorderRight, | |
| 285 | + 'bottom' => $tableBorderBottom, | |
| 286 | + 'left' => $tableBorderLeft, | |
| 287 | + ]; | |
| 253 | 288 | $globalCellStyles['borderRadius'] = [ |
| 254 | 289 | 'topLeft' => '', |
| 255 | 290 | 'topRight' => '', |
| 256 | 291 | 'bottomRight' => '', |
| @@ -269,8 +304,32 @@ | ||
| 269 | 304 | $rowBackgroundColor = isset($rowStyles['backgroundColor']) && is_string($rowStyles['backgroundColor']) |
| 270 | 305 | ? $rowStyles['backgroundColor'] |
| 271 | 306 | : ''; |
| 272 | 307 | |
| 308 | + $cellStylesForRow = $globalCellStyles; | |
| 309 | + | |
| 310 | + // Header/footer/even/odd are table-wide defaults, resolved per | |
| 311 | + // row here the same way `cell/index.tsx`'s | |
| 312 | + // `useRowBackgroundStyleKey` does in the editor — header/footer | |
| 313 | + // don't consume a slot in the even/odd count. | |
| 314 | + $isHeaderRow = $headerEnabled && $row === 0; | |
| 315 | + $isFooterRow = $footerEnabled && $rows > 0 && $row === $rows - 1; | |
| 316 | + | |
| 317 | + if ($isHeaderRow) { | |
| 318 | + $rowPositionBackground = $attrs->cellDefaults->styles->headerBackgroundColor->asAttr(); | |
| 319 | + } elseif ($isFooterRow) { | |
| 320 | + $rowPositionBackground = $attrs->cellDefaults->styles->footerBackgroundColor->asAttr(); | |
| 321 | + } else { | |
| 322 | + $dataRowPosition = $headerEnabled ? $row - 1 : $row; | |
| 323 | + $rowPositionBackground = ($dataRowPosition % 2 === 0) | |
| 324 | + ? $attrs->cellDefaults->styles->oddRowBackgroundColor->asAttr() | |
| 325 | + : $attrs->cellDefaults->styles->evenRowBackgroundColor->asAttr(); | |
| 326 | + } | |
| 327 | + | |
| 328 | + if ($rowPositionBackground !== '') { | |
| 329 | + $cellStylesForRow['backgroundColor'] = $rowPositionBackground; | |
| 330 | + } | |
| 331 | + | |
| 273 | 332 | // A row with its own background would otherwise never show |
| 274 | 333 | // through: every cell paints its own background over the |
| 275 | 334 | // `<tr>`'s, and cells fall back to the table-wide default |
| 276 | 335 | // whenever they carry no colour of their own. So for this row's |
| @@ -275,9 +334,8 @@ | ||
| 275 | 334 | // `<tr>`'s, and cells fall back to the table-wide default |
| 276 | 335 | // whenever they carry no colour of their own. So for this row's |
| 277 | 336 | // cells, that fallback is cleared — a cell/column colour on top |
| 278 | 337 | // of it (applied below by the pro filter) still wins either way. |
| 279 | - $cellStylesForRow = $globalCellStyles; | |
| 280 | 338 | if ($rowBackgroundColor !== '') { |
| 281 | 339 | $cellStylesForRow['backgroundColor'] = ''; |
| 282 | 340 | } |
| 283 | 341 | |
| @@ -345,9 +403,12 @@ | ||
| 345 | 403 | $stickyHeader, |
| 346 | 404 | $stickyFirstCol, |
| 347 | 405 | $this->getCellClassName($cell), |
| 348 | 406 | $cell instanceof CellData ? $cell->attrs : [], |
| 349 | - $this->isCellEmpty($cell) | |
| 407 | + $this->isCellEmpty($cell), | |
| 408 | + $innerBorderType, | |
| 409 | + $rows, | |
| 410 | + $cols | |
| 350 | 411 | ) |
| 351 | 412 | ); |
| 352 | 413 | } |
| 353 | 414 | |
| @@ -385,27 +446,29 @@ | ||
| 385 | 446 | 'sortable' => $sortType, |
| 386 | 447 | ]; |
| 387 | 448 | } |
| 388 | 449 | |
| 389 | - $columnsJson = json_encode($columnsData); | |
| 450 | + $columnsJson = wp_json_encode($columnsData); | |
| 390 | 451 | if (!is_string($columnsJson)) { |
| 391 | 452 | $columnsJson = '{}'; |
| 392 | 453 | } |
| 454 | + $columnsJson = esc_attr($columnsJson); | |
| 393 | 455 | |
| 394 | - $paginationJson = json_encode($paginationConfig); | |
| 456 | + $paginationJson = wp_json_encode($paginationConfig); | |
| 395 | 457 | if (!is_string($paginationJson)) { |
| 396 | 458 | $paginationJson = '{}'; |
| 397 | 459 | } |
| 460 | + $paginationJson = esc_attr($paginationJson); | |
| 398 | 461 | |
| 462 | + $searchPlaceholderAttr = esc_attr($searchPlaceholder); | |
| 463 | + $searchPositionAttr = esc_attr($searchPosition); | |
| 464 | + $searchHighlightColorAttr = esc_attr($searchHighlightColor); | |
| 465 | + | |
| 399 | 466 | $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}"); |
| 400 | 467 | |
| 401 | - // Round the whole table's outer corners by clipping the wrapper. | |
| 468 | + // Round the whole table's outer corners on the wrapper. | |
| 402 | 469 | // Zero radii are skipped so a table without any actual rounding does |
| 403 | 470 | // not carry pointless border-radius declarations. |
| 404 | - $isZeroRadius = function ($value) { | |
| 405 | - $trimmed = trim((string) $value); | |
| 406 | - return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1; | |
| 407 | - }; | |
| 408 | 471 | $wrapperStyles = []; |
| 409 | 472 | if (!$isZeroRadius($tableRadius['topLeft'])) { |
| 410 | 473 | $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft']; |
| 411 | 474 | } |
| @@ -418,8 +481,24 @@ | ||
| 418 | 481 | if (!$isZeroRadius($tableRadius['bottomLeft'])) { |
| 419 | 482 | $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft']; |
| 420 | 483 | } |
| 421 | 484 | |
| 485 | + if (!empty($wrapperStyles)) { | |
| 486 | + if ($tableOuterBorder['top'] !== '') { | |
| 487 | + $wrapperStyles[] = 'border-top:' . $tableOuterBorder['top']; | |
| 488 | + } | |
| 489 | + if ($tableOuterBorder['right'] !== '') { | |
| 490 | + $wrapperStyles[] = 'border-right:' . $tableOuterBorder['right']; | |
| 491 | + } | |
| 492 | + if ($tableOuterBorder['bottom'] !== '') { | |
| 493 | + $wrapperStyles[] = 'border-bottom:' . $tableOuterBorder['bottom']; | |
| 494 | + } | |
| 495 | + if ($tableOuterBorder['left'] !== '') { | |
| 496 | + $wrapperStyles[] = 'border-left:' . $tableOuterBorder['left']; | |
| 497 | + } | |
| 498 | + $wrapperStyles[] = 'box-sizing:border-box'; | |
| 499 | + } | |
| 500 | + | |
| 422 | 501 | // A table wider than its wrapper must scroll inside the wrapper |
| 423 | 502 | // instead of pushing the whole page sideways, so the wrapper is a |
| 424 | 503 | // scroll container by default. |
| 425 | 504 | // |
| @@ -431,10 +510,8 @@ | ||
| 431 | 510 | // scrolling wins and the header sticks within the wrapper. |
| 432 | 511 | // |
| 433 | 512 | // A rounded wrapper always needs the clip, so it opts in regardless. |
| 434 | 513 | $pageStickyHeader = $stickyHeader && !$stickyFirstCol; |
| 435 | - $hasRoundedCorners = !empty($wrapperStyles); | |
| 436 | - | |
| 437 | 514 | if ($hasRoundedCorners || !$pageStickyHeader) { |
| 438 | 515 | // `auto`, not `hidden`: both clip to the rounded corners, but |
| 439 | 516 | // `hidden` would also swallow a table wider than the wrapper. |
| 440 | 517 | // Inline, so it also wins over the `.tableberg-scroll-x` rule. |
| @@ -494,11 +571,11 @@ | ||
| 494 | 571 | data-tableberg-sortable='$sortingBoolAsStr' |
| 495 | 572 | data-tableberg-columns='$columnsJson' |
| 496 | 573 | data-tableberg-pagination='$paginationJson' |
| 497 | 574 | data-tableberg-search-enabled='$searchEnabledAsStr' |
| 498 | - data-tableberg-search-placeholder='$searchPlaceholder' | |
| 499 | - data-tableberg-search-position='$searchPosition' | |
| 500 | - data-tableberg-search-highlight-color='$searchHighlightColor' | |
| 575 | + data-tableberg-search-placeholder='$searchPlaceholderAttr' | |
| 576 | + data-tableberg-search-position='$searchPositionAttr' | |
| 577 | + data-tableberg-search-highlight-color='$searchHighlightColorAttr' | |
| 501 | 578 | data-tableberg-header='$headerBoolAsStr' |
| 502 | 579 | data-tableberg-footer='$footerBoolAsStr' |
| 503 | 580 | {$responsiveDataAttrs} |
| 504 | 581 | > |
| @@ -570,8 +647,15 @@ | ||
| 570 | 647 | $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value()); |
| 571 | 648 | $tabletStackCount = max(1, (int) $tablet->stackCount->value()); |
| 572 | 649 | $mobileStackCount = max(1, (int) $mobile->stackCount->value()); |
| 573 | 650 | |
| 651 | + // Repeating the first column in every stack row is a pro option. Only | |
| 652 | + // the licensed pro plugin turns this filter on, so the saved value is | |
| 653 | + // ignored without a valid licence. | |
| 654 | + $proActive = (bool) apply_filters('tableberg/is_pro_runtime_active', false); | |
| 655 | + $tabletRepeatFirstCol = $proActive ? $tablet->repeatFirstCol->asAttr() : ''; | |
| 656 | + $mobileRepeatFirstCol = $proActive ? $mobile->repeatFirstCol->asAttr() : ''; | |
| 657 | + | |
| 574 | 658 | return " |
| 575 | 659 | data-tableberg-responsive='true' |
| 576 | 660 | data-tableberg-rows='{$rows}' |
| 577 | 661 | data-tableberg-cols='{$cols}' |
| @@ -579,15 +663,15 @@ | ||
| 579 | 663 | data-tableberg-tablet-width='{$tabletMaxWidth}' |
| 580 | 664 | data-tableberg-tablet-mode='{$tablet->mode->asAttr()}' |
| 581 | 665 | data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}' |
| 582 | 666 | data-tableberg-tablet-count='{$tabletStackCount}' |
| 583 | - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}' | |
| 667 | + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}' | |
| 584 | 668 | data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}' |
| 585 | 669 | data-tableberg-mobile-width='{$mobileMaxWidth}' |
| 586 | 670 | data-tableberg-mobile-mode='{$mobile->mode->asAttr()}' |
| 587 | 671 | data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}' |
| 588 | 672 | data-tableberg-mobile-count='{$mobileStackCount}' |
| 589 | - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}' | |
| 673 | + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}' | |
| 590 | 674 | "; |
| 591 | 675 | } |
| 592 | 676 | |
| 593 | 677 | private function getCell($row, $col, $cells) { |