hasCellCoords) {
return '';
}
$styleValues = [
'paddingTop' => $context->paddingTop,
'paddingRight' => $context->paddingRight,
'paddingBottom' => $context->paddingBottom,
'paddingLeft' => $context->paddingLeft,
'orientation' => $context->orientation,
'elementGap' => $context->elementGap,
'wrap' => $context->wrap,
'verticalAlign' => $context->verticalAlign,
'backgroundColor' => $context->backgroundColor,
'borderTop' => $context->borderTop,
'borderRight' => $context->borderRight,
'borderBottom' => $context->borderBottom,
'borderLeft' => $context->borderLeft,
'borderTopLeftRadius' => $context->borderTopLeftRadius,
'borderTopRightRadius' => $context->borderTopRightRadius,
'borderBottomRightRadius' => $context->borderBottomRightRadius,
'borderBottomLeftRadius' => $context->borderBottomLeftRadius,
];
$styleValues = $this->overrideStyles(
$context->cellStyleOverride,
$styleValues
);
// Pro styling of a single cell goes through one filter rather than
// one per property, so a new pro style needs no change here. Pro
// gets the styles free resolved plus every attribute of the cell,
// and returns the ones it wants rendered.
//
// Applied after the `styles` overrides rather than inside them: a
// cell can carry a pro attribute without any `styles` at all.
$proStyleValues = apply_filters(
'tableberg/cell_styles',
$styleValues,
$context->cellAttrs,
$context
);
if (is_array($proStyleValues)) {
// Merged, not replaced, so a filter that returns a partial set
// cannot leave the renderer with missing keys.
$styleValues = array_merge($styleValues, $proStyleValues);
}
// Table-wide Pro grid modes win over common and individual cell
// borders. They render inner separators only; the separate table
// border control owns the outside rectangle.
if ($context->innerBorderType === 'row') {
$styleValues['borderLeft'] = '';
$styleValues['borderRight'] = '';
if ($context->row === 0) {
$styleValues['borderTop'] = '';
}
if ($context->row + $context->rowSpan >= $context->totalRows) {
$styleValues['borderBottom'] = '';
}
} elseif ($context->innerBorderType === 'col') {
$styleValues['borderTop'] = '';
$styleValues['borderBottom'] = '';
if ($context->col === 0) {
$styleValues['borderLeft'] = '';
}
if ($context->col + $context->colSpan >= $context->totalCols) {
$styleValues['borderRight'] = '';
}
}
if ($context->isEmpty) {
// An "empty cell" renders as a bare, unstyled cell on the
// frontend: no content, and no background/border either, so it
// is indistinguishable from a cell that was never styled.
$styleValues['backgroundColor'] = '';
$styleValues['borderTop'] = '';
$styleValues['borderRight'] = '';
$styleValues['borderBottom'] = '';
$styleValues['borderLeft'] = '';
$styleValues['borderTopLeftRadius'] = '';
$styleValues['borderTopRightRadius'] = '';
$styleValues['borderBottomRightRadius'] = '';
$styleValues['borderBottomLeftRadius'] = '';
}
$isStickyHeaderCell =
$context->stickyHeader &&
$context->row === 0 &&
$context->tag === 'th';
$isStickyFirstColCell =
$context->stickyFirstCol && $context->col === 0;
$styleParts = ['position' => 'relative'];
if ($styleValues['paddingTop'] !== '') {
$styleParts['padding-top'] = $styleValues['paddingTop'];
}
if ($styleValues['paddingRight'] !== '') {
$styleParts['padding-right'] = $styleValues['paddingRight'];
}
if ($styleValues['paddingBottom'] !== '') {
$styleParts['padding-bottom'] = $styleValues['paddingBottom'];
}
if ($styleValues['paddingLeft'] !== '') {
$styleParts['padding-left'] = $styleValues['paddingLeft'];
}
if ($styleValues['verticalAlign'] !== '') {
$styleParts['vertical-align'] = $styleValues['verticalAlign'];
}
if ($styleValues['backgroundColor'] !== '') {
$styleParts['background-color'] = $styleValues['backgroundColor'];
}
if ($styleValues['borderTop'] !== '') {
$styleParts['border-top'] = $styleValues['borderTop'];
}
if ($styleValues['borderRight'] !== '') {
$styleParts['border-right'] = $styleValues['borderRight'];
}
if ($styleValues['borderBottom'] !== '') {
$styleParts['border-bottom'] = $styleValues['borderBottom'];
}
if ($styleValues['borderLeft'] !== '') {
$styleParts['border-left'] = $styleValues['borderLeft'];
}
if ($styleValues['borderTopLeftRadius'] !== '') {
$styleParts['border-top-left-radius'] = $styleValues['borderTopLeftRadius'];
}
if ($styleValues['borderTopRightRadius'] !== '') {
$styleParts['border-top-right-radius'] = $styleValues['borderTopRightRadius'];
}
if ($styleValues['borderBottomRightRadius'] !== '') {
$styleParts['border-bottom-right-radius'] = $styleValues['borderBottomRightRadius'];
}
if ($styleValues['borderBottomLeftRadius'] !== '') {
$styleParts['border-bottom-left-radius'] = $styleValues['borderBottomLeftRadius'];
}
if ($context->colSpan === 1 && $context->width !== null && $context->width !== '') {
$styleParts['width'] = $context->width;
$styleParts['min-width'] = $context->width;
}
if ($context->rowSpan === 1 && $context->height !== null && $context->height !== '') {
$styleParts['height'] = $context->height;
$styleParts['min-height'] = $context->height;
}
if ($isStickyHeaderCell) {
$styleParts['position'] = 'sticky';
$styleParts['top'] = '0';
$styleParts['z-index'] = $isStickyFirstColCell ? '3' : '2';
if ($styleValues['backgroundColor'] === '') {
$styleParts['background-color'] = '#fff';
}
}
if ($isStickyFirstColCell) {
if (!$isStickyHeaderCell) {
$styleParts['position'] = 'sticky';
$styleParts['z-index'] = '1';
}
$styleParts['left'] = '0';
if (
!$isStickyHeaderCell &&
$styleValues['backgroundColor'] === ''
) {
$styleParts['background-color'] = '#fff';
}
}
$tag = $context->tag === 'th' ? 'th' : 'td';
$sortableType = getStringOrNull($context->sortableType);
$buttonRenderer = new ButtonRenderer();
$imageRenderer = new ImageRenderer();
$listRenderer = new ListRenderer();
$textRenderer = new TextRenderer();
$elementsHtml = '';
$isHorizontal = $styleValues['orientation'] === 'horizontal';
$implicitAlignment = $this->getUniformElementsAlignment($context->elements);
foreach ($context->elements as $element) {
if (!is_object($element)) {
continue;
}
$elementName = $element->name->asText();
$name = getStringOrNull($elementName) ?? 'unknown';
$attributes = is_array($element->attributes)
? $element->attributes
: [];
$elementHtml = '';
if ($name === 'text') {
$elementHtml = $textRenderer->render($attributes);
}
if ($name === 'button') {
$elementHtml = $buttonRenderer->render($attributes);
}
if ($name === 'image') {
$elementHtml = $imageRenderer->render($attributes);
}
if ($name === 'list') {
$elementHtml = $listRenderer->render($attributes);
}
if ($elementHtml === '') {
$elementHtml = apply_filters(
'tableberg/render_cell_element_html',
'',
$name,
$attributes,
$context
);
if (!is_string($elementHtml)) {
$elementHtml = '';
}
}
if ($elementHtml === '') {
$elementHtml =
"
$name
";
}
$elementAlign = $this->getElementAlignment(
$name,
$attributes
);
$elementWrapperStyles = [
'display:flex',
'justify-content:' . $this->alignmentToJustifyContent($elementAlign),
];
if (!$isHorizontal) {
$elementWrapperStyles[] = 'width:100%';
}
$elementsHtml .=
"
{$elementHtml}
";
}
$elementsWrapperClasses = ['tableberg-cell-elements'];
$elementsWrapperStyleParts = ['display:flex'];
if ($isHorizontal) {
$elementsWrapperClasses[] = 'tableberg-cell-elements-horizontal';
}
$elementsWrapperStyleParts[] = 'flex-direction:' . ($isHorizontal ? 'row' : 'column');
$verticalAlignFlex = 'center';
if ($styleValues['verticalAlign'] === 'top') {
$verticalAlignFlex = 'flex-start';
} elseif ($styleValues['verticalAlign'] === 'bottom') {
$verticalAlignFlex = 'flex-end';
}
if ($isHorizontal) {
$elementsWrapperStyleParts[] =
'justify-content:' .
$this->alignmentToJustifyContent($implicitAlignment);
$elementsWrapperStyleParts[] = 'align-items:' . $verticalAlignFlex;
} else {
$elementsWrapperStyleParts[] = 'justify-content:' . $verticalAlignFlex;
$elementsWrapperStyleParts[] = 'align-items:stretch';
}
$elementsWrapperStyleParts[] =
'flex-wrap:' . ($styleValues['wrap'] === 'nowrap' ? 'nowrap' : 'wrap');
if ($styleValues['elementGap'] !== '') {
$elementsWrapperStyleParts[] = 'gap:' . $styleValues['elementGap'];
}
$elementsWrapperClassAttr = implode(' ', $elementsWrapperClasses);
$elementsWrapperStyleAttr = '';
$elementsWrapperStyleAttr = "style='" . implode(';', $elementsWrapperStyleParts) . "'";
$elementsWrapperHtml =
"
{$elementsHtml}
";
$ribbonHtml = apply_filters(
'tableberg/render_cell_ribbon_html',
'',
$context->ribbon,
$context
);
if (!is_string($ribbonHtml)) {
$ribbonHtml = '';
}
$cellIsSortableHeader = $tag === 'th' && $sortableType !== null;
$sortIndicatorHtml = $cellIsSortableHeader ?
"
▲▼
" : '';
$stylesStr = generate_css_string($styleParts);
$attrsStr = ($tag === 'th' && $sortableType !== null) ?
"data-sortable='{$sortableType}'
role='button'
tabindex='0'
aria-sort='none'" : '';
$classAttr = $context->className !== ''
? " class='" . \esc_attr($context->className) . "'"
: '';
$html =
"<$tag
rowspan='{$context->rowSpan}'
colspan='{$context->colSpan}'
{$classAttr}
style='$stylesStr'
data-cell-row='{$context->row}'
data-cell-col='{$context->col}'
{$attrsStr}
>
$elementsWrapperHtml
$sortIndicatorHtml
$ribbonHtml
$tag>";
return $html;
}
/**
* @param string $elementName
* @param array $attributes
* @return string
*/
private function getElementAlignment($elementName, $attributes) {
if ($elementName === 'icon') {
$styles = getArrayOrNull($attributes['styles']) ?? [];
$styleAlign = getStringOrNull($styles['align']);
if (in_array($styleAlign, ['left', 'center', 'right'], true)) {
return $styleAlign;
}
return 'left';
}
$topLevelAlign = getStringOrNull($attributes['align']);
if (in_array($topLevelAlign, ['left', 'center', 'right'], true)) {
return $topLevelAlign;
}
return 'left';
}
/**
* @param string $alignment
* @return string
*/
private function alignmentToJustifyContent($alignment) {
if ($alignment === 'center') {
return 'center';
}
if ($alignment === 'right') {
return 'flex-end';
}
return 'flex-start';
}
/**
* @param array $elements
* @return string
*/
private function getUniformElementsAlignment($elements) {
$resolvedAlignment = null;
$hasElements = false;
foreach ($elements as $element) {
if (!is_object($element)) {
continue;
}
$elementName = $element->name->asText();
$name = getStringOrNull($elementName) ?? 'unknown';
$attributes = is_array($element->attributes)
? $element->attributes
: [];
$currentAlignment = $this->getElementAlignment($name, $attributes);
if (!$hasElements) {
$resolvedAlignment = $currentAlignment;
$hasElements = true;
continue;
}
if ($resolvedAlignment !== $currentAlignment) {
return 'left';
}
}
return $hasElements ? $resolvedAlignment : 'left';
}
/**
* @param array|null $cellStyleOverride
* @param array $styleValues
* @return array
*/
private function overrideStyles($cellStyleOverride, $styleValues) {
if (!is_array($cellStyleOverride)) {
return $styleValues;
}
$overridePadding = getArrayOrNull($cellStyleOverride['padding']);
if (is_array($overridePadding)) {
if (isset($overridePadding['top']) && $overridePadding['top'] instanceof StringAttr) {
$styleValues['paddingTop'] = $overridePadding['top']->asAttr();
}
if (isset($overridePadding['right']) && $overridePadding['right'] instanceof StringAttr) {
$styleValues['paddingRight'] = $overridePadding['right']->asAttr();
}
if (isset($overridePadding['bottom']) && $overridePadding['bottom'] instanceof StringAttr) {
$styleValues['paddingBottom'] = $overridePadding['bottom']->asAttr();
}
if (isset($overridePadding['left']) && $overridePadding['left'] instanceof StringAttr) {
$styleValues['paddingLeft'] = $overridePadding['left']->asAttr();
}
}
if (isset($cellStyleOverride['orientation'])
&& $cellStyleOverride['orientation'] instanceof StringAttr) {
$orientation = $cellStyleOverride['orientation']->asAttr();
$styleValues['orientation'] = $orientation;
}
if (isset($cellStyleOverride['wrap'])
&& $cellStyleOverride['wrap'] instanceof StringAttr) {
$wrap = $cellStyleOverride['wrap']->asAttr();
$styleValues['wrap'] = $wrap;
}
if (isset($cellStyleOverride['elementGap'])
&& $cellStyleOverride['elementGap'] instanceof StringAttr) {
$styleValues['elementGap'] = $cellStyleOverride['elementGap']->asAttr();
}
// A cell's own vertical alignment is a pro feature, resolved through
// the `tableberg/cell_styles` filter below. Free only applies the
// table-wide default, so it can't be switched on without a licence.
// Border is a pro attribute now (moved off this shared `styles`
// object, same as backgroundColor before it), resolved instead
// through the `tableberg/cell_styles` filter below — not here, or
// it would keep rendering a lapsed/legacy border regardless of
// licence status.
$overrideBorderRadius = getArrayOrNull($cellStyleOverride['borderRadius']);
if (is_array($overrideBorderRadius)) {
if (isset($overrideBorderRadius['topLeft'])
&& $overrideBorderRadius['topLeft'] instanceof StringAttr) {
$styleValues['borderTopLeftRadius'] = $overrideBorderRadius['topLeft']->asAttr();
}
if (isset($overrideBorderRadius['topRight'])
&& $overrideBorderRadius['topRight'] instanceof StringAttr) {
$styleValues['borderTopRightRadius'] = $overrideBorderRadius['topRight']->asAttr();
}
if (isset($overrideBorderRadius['bottomRight'])
&& $overrideBorderRadius['bottomRight'] instanceof StringAttr) {
$styleValues['borderBottomRightRadius'] = $overrideBorderRadius['bottomRight']->asAttr();
}
if (isset($overrideBorderRadius['bottomLeft'])
&& $overrideBorderRadius['bottomLeft'] instanceof StringAttr) {
$styleValues['borderBottomLeftRadius'] = $overrideBorderRadius['bottomLeft']->asAttr();
}
}
return $styleValues;
}
}