| @@ -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; |
| @@ -104,11 +131,19 @@ | ||
| 104 | 131 | $canApplyCustomWidth, |
| 105 | 132 | $tableAlignment |
| 106 | 133 | ); |
| 107 | 134 | |
| 108 | - $tableStyles = [ | |
| 109 | - 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'), | |
| 110 | - ]; | |
| 135 | + $tableStyles = array_merge( | |
| 136 | + // Table-wide typography, inherited by everything in the cells that | |
| 137 | + // does not set its own. | |
| 138 | + $attrs->table->typography->asStyles(), | |
| 139 | + [ | |
| 140 | + 'border-collapse: ' . ($hasCellSpacing ? 'separate' : 'collapse'), | |
| 141 | + // Neutralize theme-level table borders. Tableberg owns its | |
| 142 | + // borders through the table/cell controls and rounded wrapper. | |
| 143 | + 'border: 0', | |
| 144 | + ] | |
| 145 | + ); | |
| 111 | 146 | |
| 112 | 147 | if ($hasCellSpacing) { |
| 113 | 148 | $tableStyles[] = "border-spacing: {$cellSpacingHorizontal} {$cellSpacingVertical}"; |
| 114 | 149 | } |
| @@ -119,21 +154,21 @@ | ||
| 119 | 154 | } else { |
| 120 | 155 | $tableStyles[] = 'width: 100%'; |
| 121 | 156 | } |
| 122 | 157 | |
| 123 | - if ($tableBorderTop !== '') { | |
| 158 | + if (!$hasRoundedCorners && $tableBorderTop !== '') { | |
| 124 | 159 | $tableStyles[] = "border-top: {$tableBorderTop}"; |
| 125 | 160 | } |
| 126 | 161 | |
| 127 | - if ($tableBorderRight !== '') { | |
| 162 | + if (!$hasRoundedCorners && $tableBorderRight !== '') { | |
| 128 | 163 | $tableStyles[] = "border-right: {$tableBorderRight}"; |
| 129 | 164 | } |
| 130 | 165 | |
| 131 | - if ($tableBorderBottom !== '') { | |
| 166 | + if (!$hasRoundedCorners && $tableBorderBottom !== '') { | |
| 132 | 167 | $tableStyles[] = "border-bottom: {$tableBorderBottom}"; |
| 133 | 168 | } |
| 134 | 169 | |
| 135 | - if ($tableBorderLeft !== '') { | |
| 170 | + if (!$hasRoundedCorners && $tableBorderLeft !== '') { | |
| 136 | 171 | $tableStyles[] = "border-left: {$tableBorderLeft}"; |
| 137 | 172 | } |
| 138 | 173 | |
| 139 | 174 | $tableStyleAttr = "style='" . implode('; ', $tableStyles) . ";'"; |
| @@ -244,13 +279,18 @@ | ||
| 244 | 279 | 'bottomLeft' => $attrs->cellDefaults->styles->borderRadius->bottomLeft->asAttr(), |
| 245 | 280 | ], |
| 246 | 281 | ]; |
| 247 | 282 | |
| 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']; | |
| 283 | + // A collapsed table cannot reliably round its own explicit border. | |
| 284 | + // The wrapper owns only the table border and clips the cell grid to | |
| 285 | + // it. Cell borders stay on cells; copying them to the wrapper would | |
| 286 | + // add an outline that the user did not configure as a table border. | |
| 287 | + $tableOuterBorder = [ | |
| 288 | + 'top' => $tableBorderTop, | |
| 289 | + 'right' => $tableBorderRight, | |
| 290 | + 'bottom' => $tableBorderBottom, | |
| 291 | + 'left' => $tableBorderLeft, | |
| 292 | + ]; | |
| 253 | 293 | $globalCellStyles['borderRadius'] = [ |
| 254 | 294 | 'topLeft' => '', |
| 255 | 295 | 'topRight' => '', |
| 256 | 296 | 'bottomRight' => '', |
| @@ -269,8 +309,32 @@ | ||
| 269 | 309 | $rowBackgroundColor = isset($rowStyles['backgroundColor']) && is_string($rowStyles['backgroundColor']) |
| 270 | 310 | ? $rowStyles['backgroundColor'] |
| 271 | 311 | : ''; |
| 272 | 312 | |
| 313 | + $cellStylesForRow = $globalCellStyles; | |
| 314 | + | |
| 315 | + // Header/footer/even/odd are table-wide defaults, resolved per | |
| 316 | + // row here the same way `cell/index.tsx`'s | |
| 317 | + // `useRowBackgroundStyleKey` does in the editor — header/footer | |
| 318 | + // don't consume a slot in the even/odd count. | |
| 319 | + $isHeaderRow = $headerEnabled && $row === 0; | |
| 320 | + $isFooterRow = $footerEnabled && $rows > 0 && $row === $rows - 1; | |
| 321 | + | |
| 322 | + if ($isHeaderRow) { | |
| 323 | + $rowPositionBackground = $attrs->cellDefaults->styles->headerBackgroundColor->asAttr(); | |
| 324 | + } elseif ($isFooterRow) { | |
| 325 | + $rowPositionBackground = $attrs->cellDefaults->styles->footerBackgroundColor->asAttr(); | |
| 326 | + } else { | |
| 327 | + $dataRowPosition = $headerEnabled ? $row - 1 : $row; | |
| 328 | + $rowPositionBackground = ($dataRowPosition % 2 === 0) | |
| 329 | + ? $attrs->cellDefaults->styles->oddRowBackgroundColor->asAttr() | |
| 330 | + : $attrs->cellDefaults->styles->evenRowBackgroundColor->asAttr(); | |
| 331 | + } | |
| 332 | + | |
| 333 | + if ($rowPositionBackground !== '') { | |
| 334 | + $cellStylesForRow['backgroundColor'] = $rowPositionBackground; | |
| 335 | + } | |
| 336 | + | |
| 273 | 337 | // A row with its own background would otherwise never show |
| 274 | 338 | // through: every cell paints its own background over the |
| 275 | 339 | // `<tr>`'s, and cells fall back to the table-wide default |
| 276 | 340 | // whenever they carry no colour of their own. So for this row's |
| @@ -275,9 +339,8 @@ | ||
| 275 | 339 | // `<tr>`'s, and cells fall back to the table-wide default |
| 276 | 340 | // whenever they carry no colour of their own. So for this row's |
| 277 | 341 | // cells, that fallback is cleared — a cell/column colour on top |
| 278 | 342 | // of it (applied below by the pro filter) still wins either way. |
| 279 | - $cellStylesForRow = $globalCellStyles; | |
| 280 | 343 | if ($rowBackgroundColor !== '') { |
| 281 | 344 | $cellStylesForRow['backgroundColor'] = ''; |
| 282 | 345 | } |
| 283 | 346 | |
| @@ -345,9 +408,12 @@ | ||
| 345 | 408 | $stickyHeader, |
| 346 | 409 | $stickyFirstCol, |
| 347 | 410 | $this->getCellClassName($cell), |
| 348 | 411 | $cell instanceof CellData ? $cell->attrs : [], |
| 349 | - $this->isCellEmpty($cell) | |
| 412 | + $this->isCellEmpty($cell), | |
| 413 | + $innerBorderType, | |
| 414 | + $rows, | |
| 415 | + $cols | |
| 350 | 416 | ) |
| 351 | 417 | ); |
| 352 | 418 | } |
| 353 | 419 | |
| @@ -355,8 +421,17 @@ | ||
| 355 | 421 | if ($rowBackgroundColor !== '') { |
| 356 | 422 | $rowStyleParts[] = 'background-color:' . esc_attr($rowBackgroundColor); |
| 357 | 423 | } |
| 358 | 424 | |
| 425 | + // Inherited by everything in the row's cells that has no colour | |
| 426 | + // of its own. | |
| 427 | + $rowTextColor = isset($rowStyles['textColor']) && is_string($rowStyles['textColor']) | |
| 428 | + ? $rowStyles['textColor'] | |
| 429 | + : ''; | |
| 430 | + if ($rowTextColor !== '') { | |
| 431 | + $rowStyleParts[] = 'color:' . esc_attr($rowTextColor); | |
| 432 | + } | |
| 433 | + | |
| 359 | 434 | $rowBorder = isset($rowStyles['border']) && is_array($rowStyles['border']) |
| 360 | 435 | ? $rowStyles['border'] |
| 361 | 436 | : []; |
| 362 | 437 | foreach (['top', 'right', 'bottom', 'left'] as $side) { |
| @@ -385,27 +460,29 @@ | ||
| 385 | 460 | 'sortable' => $sortType, |
| 386 | 461 | ]; |
| 387 | 462 | } |
| 388 | 463 | |
| 389 | - $columnsJson = json_encode($columnsData); | |
| 464 | + $columnsJson = wp_json_encode($columnsData); | |
| 390 | 465 | if (!is_string($columnsJson)) { |
| 391 | 466 | $columnsJson = '{}'; |
| 392 | 467 | } |
| 468 | + $columnsJson = esc_attr($columnsJson); | |
| 393 | 469 | |
| 394 | - $paginationJson = json_encode($paginationConfig); | |
| 470 | + $paginationJson = wp_json_encode($paginationConfig); | |
| 395 | 471 | if (!is_string($paginationJson)) { |
| 396 | 472 | $paginationJson = '{}'; |
| 397 | 473 | } |
| 474 | + $paginationJson = esc_attr($paginationJson); | |
| 398 | 475 | |
| 476 | + $searchPlaceholderAttr = esc_attr($searchPlaceholder); | |
| 477 | + $searchPositionAttr = esc_attr($searchPosition); | |
| 478 | + $searchHighlightColorAttr = esc_attr($searchHighlightColor); | |
| 479 | + | |
| 399 | 480 | $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}"); |
| 400 | 481 | |
| 401 | - // Round the whole table's outer corners by clipping the wrapper. | |
| 482 | + // Round the whole table's outer corners on the wrapper. | |
| 402 | 483 | // Zero radii are skipped so a table without any actual rounding does |
| 403 | 484 | // 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 | 485 | $wrapperStyles = []; |
| 409 | 486 | if (!$isZeroRadius($tableRadius['topLeft'])) { |
| 410 | 487 | $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft']; |
| 411 | 488 | } |
| @@ -418,8 +495,24 @@ | ||
| 418 | 495 | if (!$isZeroRadius($tableRadius['bottomLeft'])) { |
| 419 | 496 | $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft']; |
| 420 | 497 | } |
| 421 | 498 | |
| 499 | + if (!empty($wrapperStyles)) { | |
| 500 | + if ($tableOuterBorder['top'] !== '') { | |
| 501 | + $wrapperStyles[] = 'border-top:' . $tableOuterBorder['top']; | |
| 502 | + } | |
| 503 | + if ($tableOuterBorder['right'] !== '') { | |
| 504 | + $wrapperStyles[] = 'border-right:' . $tableOuterBorder['right']; | |
| 505 | + } | |
| 506 | + if ($tableOuterBorder['bottom'] !== '') { | |
| 507 | + $wrapperStyles[] = 'border-bottom:' . $tableOuterBorder['bottom']; | |
| 508 | + } | |
| 509 | + if ($tableOuterBorder['left'] !== '') { | |
| 510 | + $wrapperStyles[] = 'border-left:' . $tableOuterBorder['left']; | |
| 511 | + } | |
| 512 | + $wrapperStyles[] = 'box-sizing:border-box'; | |
| 513 | + } | |
| 514 | + | |
| 422 | 515 | // A table wider than its wrapper must scroll inside the wrapper |
| 423 | 516 | // instead of pushing the whole page sideways, so the wrapper is a |
| 424 | 517 | // scroll container by default. |
| 425 | 518 | // |
| @@ -431,10 +524,8 @@ | ||
| 431 | 524 | // scrolling wins and the header sticks within the wrapper. |
| 432 | 525 | // |
| 433 | 526 | // A rounded wrapper always needs the clip, so it opts in regardless. |
| 434 | 527 | $pageStickyHeader = $stickyHeader && !$stickyFirstCol; |
| 435 | - $hasRoundedCorners = !empty($wrapperStyles); | |
| 436 | - | |
| 437 | 528 | if ($hasRoundedCorners || !$pageStickyHeader) { |
| 438 | 529 | // `auto`, not `hidden`: both clip to the rounded corners, but |
| 439 | 530 | // `hidden` would also swallow a table wider than the wrapper. |
| 440 | 531 | // Inline, so it also wins over the `.tableberg-scroll-x` rule. |
| @@ -494,11 +585,11 @@ | ||
| 494 | 585 | data-tableberg-sortable='$sortingBoolAsStr' |
| 495 | 586 | data-tableberg-columns='$columnsJson' |
| 496 | 587 | data-tableberg-pagination='$paginationJson' |
| 497 | 588 | data-tableberg-search-enabled='$searchEnabledAsStr' |
| 498 | - data-tableberg-search-placeholder='$searchPlaceholder' | |
| 499 | - data-tableberg-search-position='$searchPosition' | |
| 500 | - data-tableberg-search-highlight-color='$searchHighlightColor' | |
| 589 | + data-tableberg-search-placeholder='$searchPlaceholderAttr' | |
| 590 | + data-tableberg-search-position='$searchPositionAttr' | |
| 591 | + data-tableberg-search-highlight-color='$searchHighlightColorAttr' | |
| 501 | 592 | data-tableberg-header='$headerBoolAsStr' |
| 502 | 593 | data-tableberg-footer='$footerBoolAsStr' |
| 503 | 594 | {$responsiveDataAttrs} |
| 504 | 595 | > |
| @@ -570,8 +661,11 @@ | ||
| 570 | 661 | $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value()); |
| 571 | 662 | $tabletStackCount = max(1, (int) $tablet->stackCount->value()); |
| 572 | 663 | $mobileStackCount = max(1, (int) $mobile->stackCount->value()); |
| 573 | 664 | |
| 665 | + $tabletRepeatFirstCol = $tablet->repeatFirstCol->asAttr(); | |
| 666 | + $mobileRepeatFirstCol = $mobile->repeatFirstCol->asAttr(); | |
| 667 | + | |
| 574 | 668 | return " |
| 575 | 669 | data-tableberg-responsive='true' |
| 576 | 670 | data-tableberg-rows='{$rows}' |
| 577 | 671 | data-tableberg-cols='{$cols}' |
| @@ -579,15 +673,15 @@ | ||
| 579 | 673 | data-tableberg-tablet-width='{$tabletMaxWidth}' |
| 580 | 674 | data-tableberg-tablet-mode='{$tablet->mode->asAttr()}' |
| 581 | 675 | data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}' |
| 582 | 676 | data-tableberg-tablet-count='{$tabletStackCount}' |
| 583 | - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}' | |
| 677 | + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}' | |
| 584 | 678 | data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}' |
| 585 | 679 | data-tableberg-mobile-width='{$mobileMaxWidth}' |
| 586 | 680 | data-tableberg-mobile-mode='{$mobile->mode->asAttr()}' |
| 587 | 681 | data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}' |
| 588 | 682 | data-tableberg-mobile-count='{$mobileStackCount}' |
| 589 | - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}' | |
| 683 | + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}' | |
| 590 | 684 | "; |
| 591 | 685 | } |
| 592 | 686 | |
| 593 | 687 | private function getCell($row, $col, $cells) { |