Refactor #5678 - TreeTable CSS and responsive structure improvements

pull/5701/head
tugcekucukoglu 2024-05-03 09:20:10 +03:00
parent 2c65e1b3fd
commit 5cb78b9b3c
7 changed files with 93 additions and 67 deletions

View File

@ -210,12 +210,6 @@ const TreeTableProps = [
default: 'false', default: 'false',
description: 'When specified, enables horizontal and/or vertical scrolling.' description: 'When specified, enables horizontal and/or vertical scrolling.'
}, },
{
name: 'scrollDirection',
type: 'string',
default: 'vertical',
description: 'Orientation of the scrolling, options are "vertical", "horizontal" and "both".'
},
{ {
name: 'scrollHeight', name: 'scrollHeight',
type: 'string', type: 'string',

View File

@ -150,22 +150,26 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
scrollDirection: {
type: String,
default: 'vertical'
},
scrollHeight: { scrollHeight: {
type: String, type: String,
default: null default: null
}, },
responsiveLayout: { responsiveLayout: {
type: String, type: String,
default: null default: 'scroll'
}, },
size: { size: {
type: String, type: String,
default: null default: null
}, },
tableStyle: {
type: null,
default: null
},
tableClass: {
type: [String, Object],
default: null
},
tableProps: { tableProps: {
type: Object, type: Object,
default: null default: null

View File

@ -137,7 +137,6 @@ export default {
selected: this.$parent.selected, selected: this.$parent.selected,
frozen: this.columnProp('frozen'), frozen: this.columnProp('frozen'),
scrollable: this.$parentInstance.scrollable, scrollable: this.$parentInstance.scrollable,
scrollDirection: this.$parentInstance.scrollDirection,
showGridlines: this.$parentInstance.showGridlines, showGridlines: this.$parentInstance.showGridlines,
size: this.$parentInstance?.size size: this.$parentInstance?.size
} }

View File

@ -99,7 +99,6 @@ export default {
frozen: this.$parentInstance.scrollable && this.columnProp('frozen'), frozen: this.$parentInstance.scrollable && this.columnProp('frozen'),
resizable: this.resizableColumns, resizable: this.resizableColumns,
scrollable: this.$parentInstance.scrollable, scrollable: this.$parentInstance.scrollable,
scrollDirection: this.$parentInstance.scrollDirection,
showGridlines: this.$parentInstance.showGridlines, showGridlines: this.$parentInstance.showGridlines,
size: this.$parentInstance?.size size: this.$parentInstance?.size
} }

View File

@ -586,20 +586,18 @@ export interface TreeTableProps {
* Height of the scroll viewport in fixed pixels or the 'flex' keyword for a dynamic size. * Height of the scroll viewport in fixed pixels or the 'flex' keyword for a dynamic size.
*/ */
scrollHeight?: HintedString<'flex'> | undefined; scrollHeight?: HintedString<'flex'> | undefined;
/**
* Orientation of the scrolling.
* @defaultValue vertical
*/
scrollDirection?: 'vertical' | 'horizontal' | 'both' | undefined;
/**
* Defines the responsive mode, currently only option is scroll.
* @defaultValue stack
*/
responsiveLayout?: 'stack' | 'scroll' | undefined;
/** /**
* Defines the size of the table. * Defines the size of the table.
*/ */
size?: 'small' | 'large' | undefined; size?: 'small' | 'large' | undefined;
/**
* Inline style of the table element.
*/
tableStyle?: string | object | undefined;
/**
* Style class of the table element.
*/
tableClass?: string | object | undefined;
/** /**
* Props to pass to the table element. * Props to pass to the table element.
*/ */

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="cx('root')" data-scrollselectors=".p-treetable-scrollable-body" role="table" v-bind="ptmi('root')"> <div :class="cx('root')" data-scrollselectors=".p-treetable-scrollable-body" v-bind="ptmi('root')">
<slot></slot> <slot></slot>
<div v-if="loading && loadingMode === 'mask'" :class="cx('loading')" v-bind="ptm('loading')"> <div v-if="loading && loadingMode === 'mask'" :class="cx('loading')" v-bind="ptm('loading')">
<div :class="cx('mask')" v-bind="ptm('mask')"> <div :class="cx('mask')" v-bind="ptm('mask')">
@ -51,9 +51,9 @@
<slot name="paginatorrowsperpagedropdownicon" :class="slotProps.class"></slot> <slot name="paginatorrowsperpagedropdownicon" :class="slotProps.class"></slot>
</template> </template>
</TTPaginator> </TTPaginator>
<div :class="cx('tableContainer')" :style="{ maxHeight: scrollHeight }" v-bind="ptm('tableContainer')"> <div :class="cx('tableContainer')" :style="[sx('tableContainer'), { maxHeight: scrollHeight }]" v-bind="ptm('tableContainer')">
<table ref="table" role="table" v-bind="{ ...tableProps, ...ptm('table') }"> <table ref="table" role="table" :class="[cx('table'), tableClass]" :style="tableStyle" v-bind="{ ...tableProps, ...ptm('table') }">
<thead :class="cx('thead')" role="rowgroup" v-bind="ptm('thead')"> <thead :class="cx('thead')" :style="sx('thead')" role="rowgroup" v-bind="ptm('thead')">
<tr role="row" v-bind="ptm('headerRow')"> <tr role="row" v-bind="ptm('headerRow')">
<template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i"> <template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i">
<TTHeaderCell <TTHeaderCell
@ -111,7 +111,7 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tfoot v-if="hasFooter" :class="cx('tfoot')" role="rowgroup" v-bind="ptm('tfoot')"> <tfoot v-if="hasFooter" :class="cx('tfoot')" :style="sx('tfoot')" role="rowgroup" v-bind="ptm('tfoot')">
<tr role="row" v-bind="ptm('footerRow')"> <tr role="row" v-bind="ptm('footerRow')">
<template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i"> <template v-for="(col, i) of columns" :key="columnProp(col, 'columnKey') || columnProp(col, 'field') || i">
<TTFooterCell v-if="!columnProp(col, 'hidden')" :column="col" :index="i" :unstyled="unstyled" :pt="pt"></TTFooterCell> <TTFooterCell v-if="!columnProp(col, 'hidden')" :column="col" :index="i" :unstyled="unstyled" :pt="pt"></TTFooterCell>
@ -171,7 +171,7 @@
import { FilterService } from 'primevue/api'; import { FilterService } from 'primevue/api';
import SpinnerIcon from 'primevue/icons/spinner'; import SpinnerIcon from 'primevue/icons/spinner';
import Paginator from 'primevue/paginator'; import Paginator from 'primevue/paginator';
import { DomHandler, HelperSet, ObjectUtils } from 'primevue/utils'; import { DomHandler, HelperSet, ObjectUtils, UniqueComponentId } from 'primevue/utils';
import BaseTreeTable from './BaseTreeTable.vue'; import BaseTreeTable from './BaseTreeTable.vue';
import FooterCell from './FooterCell.vue'; import FooterCell from './FooterCell.vue';
import HeaderCell from './HeaderCell.vue'; import HeaderCell from './HeaderCell.vue';
@ -240,16 +240,10 @@ export default {
} }
}, },
mounted() { mounted() {
if (this.scrollable && this.scrollDirection !== 'vertical') { this.$el.setAttribute(this.attributeSelector, '');
this.updateScrollWidth();
}
},
updated() {
if (this.scrollable && this.scrollDirection !== 'vertical') {
this.updateScrollWidth();
}
}, },
beforeUnmount() { beforeUnmount() {
this.destroyStyleElement();
this.d_columns.clear(); this.d_columns.clear();
}, },
methods: { methods: {
@ -649,21 +643,18 @@ export default {
let nextColumnWidth = nextColumn.offsetWidth - delta; let nextColumnWidth = nextColumn.offsetWidth - delta;
if (newColumnWidth > 15 && nextColumnWidth > 15) { if (newColumnWidth > 15 && nextColumnWidth > 15) {
if (!this.scrollable) {
this.resizeColumnElement.style.width = newColumnWidth + 'px';
if (nextColumn) {
nextColumn.style.width = nextColumnWidth + 'px';
}
} else {
this.resizeTableCells(newColumnWidth, nextColumnWidth); this.resizeTableCells(newColumnWidth, nextColumnWidth);
} }
}
} else if (this.columnResizeMode === 'expand') { } else if (this.columnResizeMode === 'expand') {
this.$refs.table.style.width = this.$refs.table.offsetWidth + delta + 'px'; const tableWidth = this.$refs.table.offsetWidth + delta + 'px';
if (!this.scrollable) this.resizeColumnElement.style.width = newColumnWidth + 'px'; const updateTableWidth = (el) => {
else this.resizeTableCells(newColumnWidth); !!el && (el.style.width = el.style.minWidth = tableWidth);
};
// Reasoning: resize table cells before updating the table width so that it can use existing computed cell widths and adjust only the one column.
this.resizeTableCells(newColumnWidth);
updateTableWidth(this.$refs.table);
} }
this.$emit('column-resize-end', { this.$emit('column-resize-end', {
@ -681,23 +672,31 @@ export default {
}, },
resizeTableCells(newColumnWidth, nextColumnWidth) { resizeTableCells(newColumnWidth, nextColumnWidth) {
let colIndex = DomHandler.index(this.resizeColumnElement); let colIndex = DomHandler.index(this.resizeColumnElement);
let children = this.$refs.table.children; let widths = [];
let headers = DomHandler.find(this.$refs.table, 'thead[data-pc-section="thead"] > tr > th');
for (let child of children) { headers.forEach((header) => widths.push(DomHandler.getOuterWidth(header)));
for (let row of child.children) {
let resizeCell = row.children[colIndex];
resizeCell.style.flex = '0 0 ' + newColumnWidth + 'px'; this.destroyStyleElement();
this.createStyleElement();
if (this.columnResizeMode === 'fit') { let innerHTML = '';
let nextCell = resizeCell.nextElementSibling; let selector = `[data-pc-name="treetable"][${this.attributeSelector}] > [data-pc-section="tablecontainer"] > table[data-pc-section="table"]`;
if (nextCell) { widths.forEach((width, index) => {
nextCell.style.flex = '0 0 ' + nextColumnWidth + 'px'; let colWidth = index === colIndex ? newColumnWidth : nextColumnWidth && index === colIndex + 1 ? nextColumnWidth : width;
} let style = `width: ${colWidth}px !important; max-width: ${colWidth}px !important`;
}
} innerHTML += `
${selector} > thead[data-pc-section="thead"] > tr > th:nth-child(${index + 1}),
${selector} > tbody[data-pc-section="tbody"] > tr > td:nth-child(${index + 1}),
${selector} > tfoot[data-pc-section="tfoot"] > tr > td:nth-child(${index + 1}) {
${style}
} }
`;
});
this.styleElement.innerHTML = innerHTML;
}, },
bindColumnResizeEvents() { bindColumnResizeEvents() {
if (!this.documentColumnResizeListener) { if (!this.documentColumnResizeListener) {
@ -750,12 +749,21 @@ export default {
hasGlobalFilter() { hasGlobalFilter() {
return this.filters && Object.prototype.hasOwnProperty.call(this.filters, 'global'); return this.filters && Object.prototype.hasOwnProperty.call(this.filters, 'global');
}, },
updateScrollWidth() {
this.$refs.table.style.width = this.$refs.table.scrollWidth + 'px';
},
getItemLabel(node) { getItemLabel(node) {
return node.data.name; return node.data.name;
}, },
createStyleElement() {
this.styleElement = document.createElement('style');
this.styleElement.type = 'text/css';
DomHandler.setAttribute(this.styleElement, 'nonce', this.$primevue?.config?.csp?.nonce);
document.head.appendChild(this.styleElement);
},
destroyStyleElement() {
if (this.styleElement) {
document.head.removeChild(this.styleElement);
this.styleElement = null;
}
},
setTabindex(node, index) { setTabindex(node, index) {
if (this.isNodeSelected(node)) { if (this.isNodeSelected(node)) {
this.hasASelectedNode = true; this.hasASelectedNode = true;
@ -852,6 +860,9 @@ export default {
return data ? data.length : 0; return data ? data.length : 0;
} }
},
attributeSelector() {
return UniqueComponentId();
} }
}, },
components: { components: {

View File

@ -427,6 +427,7 @@ p-treetable-gridlines .p-treetable-tbody > tr:last-child>td {
transition: background-color ${dt('transition.duration')}, color ${dt('transition.duration')}, border-color ${dt('transition.duration')}, box-shadow ${dt('transition.duration')}, outline-color ${dt('transition.duration')}; transition: background-color ${dt('transition.duration')}, color ${dt('transition.duration')}, border-color ${dt('transition.duration')}, box-shadow ${dt('transition.duration')}, outline-color ${dt('transition.duration')};
outline-color: transparent; outline-color: transparent;
user-select: none; user-select: none;
margin-right: 0.5rem;
} }
.p-treetable-row-toggle-button:enabled:hover { .p-treetable-row-toggle-button:enabled:hover {
@ -443,6 +444,11 @@ p-treetable-gridlines .p-treetable-tbody > tr:last-child>td {
background: ${dt('treetable.row.action.highlight.hover.background')}; background: ${dt('treetable.row.action.highlight.hover.background')};
color: inherit; color: inherit;
} }
.p-treetable .p-treetable-row-checkbox {
vertical-align: middle;
margin-right: 0.5rem;
}
`; `;
const classes = { const classes = {
@ -465,12 +471,20 @@ const classes = {
header: 'p-treetable-header', header: 'p-treetable-header',
paginator: ({ position }) => 'p-treetable-paginator-' + position, paginator: ({ position }) => 'p-treetable-paginator-' + position,
tableContainer: 'p-treetable-table-container', tableContainer: 'p-treetable-table-container',
table: ({ props }) => [
'p-treetable-table',
{
'p-treetable-scrollable-table': props.scrollable,
'p-treetable-resizable-table': props.resizableColumns,
'p-treetable-resizable-table-fit': props.resizableColumns && props.columnResizeMode === 'fit'
}
],
thead: 'p-treetable-thead', thead: 'p-treetable-thead',
headerCell: ({ instance, props }) => [ headerCell: ({ instance, props, context }) => [
{ {
'p-treetable-sortable-column': instance.columnProp('sortable'), 'p-treetable-sortable-column': instance.columnProp('sortable'),
'p-treetable-resizable-column': props.resizableColumns, 'p-treetable-resizable-column': props.resizableColumns,
'p-treetable-column-sorted': instance.isColumnSorted(), 'p-treetable-column-sorted': context?.sorted,
'p-treetable-frozen-column': instance.columnProp('frozen') 'p-treetable-frozen-column': instance.columnProp('frozen')
} }
], ],
@ -503,8 +517,15 @@ const classes = {
columnResizeHelper: 'p-treetable-column-resize-indicator' columnResizeHelper: 'p-treetable-column-resize-indicator'
}; };
const inlineStyles = {
tableContainer: { overflow: 'auto' },
thead: { position: 'sticky' },
tfoot: { position: 'sticky' }
};
export default BaseStyle.extend({ export default BaseStyle.extend({
name: 'treetable', name: 'treetable',
theme, theme,
classes classes,
inlineStyles
}); });