| @@ -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' => '', |
| @@ -368,9 +403,12 @@ | ||
| 368 | 403 | $stickyHeader, |
| 369 | 404 | $stickyFirstCol, |
| 370 | 405 | $this->getCellClassName($cell), |
| 371 | 406 | $cell instanceof CellData ? $cell->attrs : [], |
| 372 | - $this->isCellEmpty($cell) | |
| 407 | + $this->isCellEmpty($cell), | |
| 408 | + $innerBorderType, | |
| 409 | + $rows, | |
| 410 | + $cols | |
| 373 | 411 | ) |
| 374 | 412 | ); |
| 375 | 413 | } |
| 376 | 414 | |
| @@ -408,27 +446,29 @@ | ||
| 408 | 446 | 'sortable' => $sortType, |
| 409 | 447 | ]; |
| 410 | 448 | } |
| 411 | 449 | |
| 412 | - $columnsJson = json_encode($columnsData); | |
| 450 | + $columnsJson = wp_json_encode($columnsData); | |
| 413 | 451 | if (!is_string($columnsJson)) { |
| 414 | 452 | $columnsJson = '{}'; |
| 415 | 453 | } |
| 454 | + $columnsJson = esc_attr($columnsJson); | |
| 416 | 455 | |
| 417 | - $paginationJson = json_encode($paginationConfig); | |
| 456 | + $paginationJson = wp_json_encode($paginationConfig); | |
| 418 | 457 | if (!is_string($paginationJson)) { |
| 419 | 458 | $paginationJson = '{}'; |
| 420 | 459 | } |
| 460 | + $paginationJson = esc_attr($paginationJson); | |
| 421 | 461 | |
| 462 | + $searchPlaceholderAttr = esc_attr($searchPlaceholder); | |
| 463 | + $searchPositionAttr = esc_attr($searchPosition); | |
| 464 | + $searchHighlightColorAttr = esc_attr($searchHighlightColor); | |
| 465 | + | |
| 422 | 466 | $wrapperClass = trim("tableberg-table-wrapper {$wrapperAlignmentClass}"); |
| 423 | 467 | |
| 424 | - // Round the whole table's outer corners by clipping the wrapper. | |
| 468 | + // Round the whole table's outer corners on the wrapper. | |
| 425 | 469 | // Zero radii are skipped so a table without any actual rounding does |
| 426 | 470 | // not carry pointless border-radius declarations. |
| 427 | - $isZeroRadius = function ($value) { | |
| 428 | - $trimmed = trim((string) $value); | |
| 429 | - return $trimmed === '' || preg_match('/^0(?:\.0+)?[a-z%]*$/i', $trimmed) === 1; | |
| 430 | - }; | |
| 431 | 471 | $wrapperStyles = []; |
| 432 | 472 | if (!$isZeroRadius($tableRadius['topLeft'])) { |
| 433 | 473 | $wrapperStyles[] = 'border-top-left-radius:' . $tableRadius['topLeft']; |
| 434 | 474 | } |
| @@ -441,8 +481,24 @@ | ||
| 441 | 481 | if (!$isZeroRadius($tableRadius['bottomLeft'])) { |
| 442 | 482 | $wrapperStyles[] = 'border-bottom-left-radius:' . $tableRadius['bottomLeft']; |
| 443 | 483 | } |
| 444 | 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 | + | |
| 445 | 501 | // A table wider than its wrapper must scroll inside the wrapper |
| 446 | 502 | // instead of pushing the whole page sideways, so the wrapper is a |
| 447 | 503 | // scroll container by default. |
| 448 | 504 | // |
| @@ -454,10 +510,8 @@ | ||
| 454 | 510 | // scrolling wins and the header sticks within the wrapper. |
| 455 | 511 | // |
| 456 | 512 | // A rounded wrapper always needs the clip, so it opts in regardless. |
| 457 | 513 | $pageStickyHeader = $stickyHeader && !$stickyFirstCol; |
| 458 | - $hasRoundedCorners = !empty($wrapperStyles); | |
| 459 | - | |
| 460 | 514 | if ($hasRoundedCorners || !$pageStickyHeader) { |
| 461 | 515 | // `auto`, not `hidden`: both clip to the rounded corners, but |
| 462 | 516 | // `hidden` would also swallow a table wider than the wrapper. |
| 463 | 517 | // Inline, so it also wins over the `.tableberg-scroll-x` rule. |
| @@ -517,11 +571,11 @@ | ||
| 517 | 571 | data-tableberg-sortable='$sortingBoolAsStr' |
| 518 | 572 | data-tableberg-columns='$columnsJson' |
| 519 | 573 | data-tableberg-pagination='$paginationJson' |
| 520 | 574 | data-tableberg-search-enabled='$searchEnabledAsStr' |
| 521 | - data-tableberg-search-placeholder='$searchPlaceholder' | |
| 522 | - data-tableberg-search-position='$searchPosition' | |
| 523 | - 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' | |
| 524 | 578 | data-tableberg-header='$headerBoolAsStr' |
| 525 | 579 | data-tableberg-footer='$footerBoolAsStr' |
| 526 | 580 | {$responsiveDataAttrs} |
| 527 | 581 | > |
| @@ -593,8 +647,15 @@ | ||
| 593 | 647 | $mobileMaxWidth = max(1, (int) $mobile->maxWidth->value()); |
| 594 | 648 | $tabletStackCount = max(1, (int) $tablet->stackCount->value()); |
| 595 | 649 | $mobileStackCount = max(1, (int) $mobile->stackCount->value()); |
| 596 | 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 | + | |
| 597 | 658 | return " |
| 598 | 659 | data-tableberg-responsive='true' |
| 599 | 660 | data-tableberg-rows='{$rows}' |
| 600 | 661 | data-tableberg-cols='{$cols}' |
| @@ -602,15 +663,15 @@ | ||
| 602 | 663 | data-tableberg-tablet-width='{$tabletMaxWidth}' |
| 603 | 664 | data-tableberg-tablet-mode='{$tablet->mode->asAttr()}' |
| 604 | 665 | data-tableberg-tablet-transpose='{$tablet->transpose->asAttr()}' |
| 605 | 666 | data-tableberg-tablet-count='{$tabletStackCount}' |
| 606 | - data-tableberg-tablet-repeat-first-col='{$tablet->repeatFirstCol->asAttr()}' | |
| 667 | + data-tableberg-tablet-repeat-first-col='{$tabletRepeatFirstCol}' | |
| 607 | 668 | data-tableberg-mobile-enabled='{$mobile->enabled->asAttr()}' |
| 608 | 669 | data-tableberg-mobile-width='{$mobileMaxWidth}' |
| 609 | 670 | data-tableberg-mobile-mode='{$mobile->mode->asAttr()}' |
| 610 | 671 | data-tableberg-mobile-transpose='{$mobile->transpose->asAttr()}' |
| 611 | 672 | data-tableberg-mobile-count='{$mobileStackCount}' |
| 612 | - data-tableberg-mobile-repeat-first-col='{$mobile->repeatFirstCol->asAttr()}' | |
| 673 | + data-tableberg-mobile-repeat-first-col='{$mobileRepeatFirstCol}' | |
| 613 | 674 | "; |
| 614 | 675 | } |
| 615 | 676 | |
| 616 | 677 | private function getCell($row, $col, $cells) { |