Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Table/Table.razor
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@
{
if (col.Visible)
{
<col style="@GetColWidthString(col.Width)" />
<col style="@GetColWidthString(GetColWidth(col))" />
}
}
@if (ShowExtendButtons && !IsExtendButtonsInRowHeader)
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Table/Table.razor.Sort.cs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ string GetFixedHeaderStyleString() => IsFixedHeader
private string? GetLeftStyle(ITableColumn col)
{
var columns = GetVisibleColumns();
var defaultWidth = 200;
var defaultWidth = DefaultFixedColumnWidth;
var width = 0;
var start = 0;
var index = columns.IndexOf(col);
Expand Down
48 changes: 47 additions & 1 deletion src/BootstrapBlazor/Components/Table/Table.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName(

private static string? GetColWidthString(int? width) => (width.HasValue && width.Value > 0) ? $"width: {width.Value}px;" : null;

private int? GetColWidth(TableColumnState state)
{
// 列状态中的宽度在首次构建列时生成,运行时动态设置 Fixed 后状态中的宽度可能为空
// 固定列未设置宽度时使用默认固定列宽,保证固定列偏移量与实际列宽一致
var col = Columns.Find(i => i.GetFieldName() == state.Name);
var width = state.Width ?? col?.Width;
return width ?? (col is { Fixed: true } ? DefaultFixedColumnWidth : null);
}
Comment thread
Copilot marked this conversation as resolved.

/// <summary>
/// <para lang="zh">获得/设置 滚动条宽度 默认 null 未设置使用 <see cref="ScrollOptions"/> 配置类中的 <see cref="ScrollOptions.ScrollWidth"/></para>
/// <para lang="en">Gets or sets Scroll Width. Default null (Use <see cref="ScrollOptions.ScrollWidth"/>)</para>
Expand Down Expand Up @@ -1155,6 +1164,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
var measureColumnMinWidth = _measureColumnMinWidth;
var updateSortTooltip = _updateSortTooltip;
var scrollToTop = _shouldScrollTop;
var fixColumnOffset = _fixColumnOffset;

_invoke = false;
_resetColumnListPopover = false;
Expand All @@ -1163,6 +1173,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
_measureColumnMinWidth = false;
_updateSortTooltip = false;
_shouldScrollTop = false;
_fixColumnOffset = false;

await InvokeVoidAsync("updateTableState", Id, new
{
Expand All @@ -1177,7 +1188,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
AutoScrollLastSelectedRowToView,
AutoScrollVerticalAlign = AutoScrollVerticalAlign.ToDescriptionString(),
ScrollIntoViewBehavior = ScrollIntoViewBehavior.ToDescriptionString(),
ScrollToTop = scrollToTop
ScrollToTop = scrollToTop,
FixColumnOffset = fixColumnOffset
});
}

Expand Down Expand Up @@ -1322,6 +1334,10 @@ private List<ITableColumn> GetTableColumns()

private async Task BuildTableColumnsAsync()
{
// 缓存运行时动态设置的固定列状态
// 自动生成列模式下列实例每次渲染均会重新创建,重建后回放固定状态防止丢失
var fixedColumns = Columns.Where(i => i.Fixed).Select(i => i.GetFieldName()).ToHashSet(StringComparer.Ordinal);

// 构建列信息
var cols = GetTableColumns();

Expand All @@ -1335,10 +1351,35 @@ private async Task BuildTableColumnsAsync()
Columns.AddRange(cols.OrderFunc());
EnsureTemplateColumnFieldNames();

// 回放运行时动态设置的固定列状态
if (fixedColumns.Count != 0)
{
foreach (var col in Columns)
{
if (fixedColumns.Contains(col.GetFieldName()))
{
col.Fixed = true;
}
}
}

// 加载客户端持久化列状态
ResetTableColumns();

// 固定列集合发生变化时通知客户端脚本按实际渲染宽度修正固定列偏移量
var fixedColumnNames = Columns.Where(i => i.Fixed).Select(i => i.GetFieldName()).ToHashSet(StringComparer.Ordinal);
if (!fixedColumnNames.SetEquals(_lastFixedColumnNames))
{
_lastFixedColumnNames = fixedColumnNames;
_fixColumnOffset = true;
_invoke = true;
}
}

private HashSet<string> _lastFixedColumnNames = new(StringComparer.Ordinal);

private bool _fixColumnOffset;

private void EnsureTemplateColumnFieldNames()
{
const string prefix = "bb_template_column_";
Expand Down Expand Up @@ -1404,6 +1445,11 @@ private void ResetTableColumns()
{
_visibleColumnsCache.Clear();

// 固定列状态可能在运行时发生改变且自动生成列模式下列实例每次渲染均会重新创建
// 清空缓存重新计算固定列边界,同时防止缓存按引用累积过期列实例
FirstFixedColumnCache.Clear();
LastFixedColumnCache.Clear();

if (_tableColumnStates.Count == 0)
{
// 重建缓存
Expand Down
99 changes: 98 additions & 1 deletion src/BootstrapBlazor/Components/Table/Table.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ export async function reset(id) {
cancelAnimationFrame(table.minWidthRaf);
table.minWidthRaf = null;
}
if (table.fixedOffsetObserver) {
table.fixedOffsetObserver.disconnect();
table.fixedOffsetObserver = null;
}
if (table.fixedOffsetRaf) {
cancelAnimationFrame(table.fixedOffsetRaf);
table.fixedOffsetRaf = null;
}
table.autoColumns = [];

table.columns = [];
Expand Down Expand Up @@ -118,6 +126,10 @@ export async function reset(id) {
if (table.thead) {
setColSize(table, table.options);
}

// 修正固定列 sticky 偏移量并监听表格尺寸变化
fixColumnOffset(table);
setFixedOffsetObserver(table);
}

export async function switchCardView(id) {
Expand Down Expand Up @@ -147,6 +159,14 @@ const destroyTable = table => {
if (table.minWidthRaf) {
cancelAnimationFrame(table.minWidthRaf);
}
if (table.fixedOffsetObserver) {
table.fixedOffsetObserver.disconnect();
table.fixedOffsetObserver = null;
}
if (table.fixedOffsetRaf) {
cancelAnimationFrame(table.fixedOffsetRaf);
table.fixedOffsetRaf = null;
}
if (table.thead) {
EventHandler.off(table.body, 'scroll')
}
Expand Down Expand Up @@ -1107,6 +1127,10 @@ export async function updateTableState(id, options) {
setColSize(table, options);
}

if (options.fixColumnOffset) {
fixColumnOffset(table);
}

if (options.resetColumnListPopover) {
resetColumnListPopover(table);
}
Expand Down Expand Up @@ -1176,7 +1200,8 @@ const setColSize = (table, options) => {
return;
}
const th = headerCollection.find(i => i.getAttribute('data-bb-field') === col.name);
if (th === void 0) {
if (th === void 0 || th.classList.contains('fixed')) {
// skip fixed column which width is controlled by colgroup
return;
}
const colIndex = headerCollection.indexOf(th);
Expand Down Expand Up @@ -1206,6 +1231,78 @@ const applyColumnMinWidth = table => {
}
}

const setFixedOffsetObserver = table => {
if (!window.ResizeObserver || !table.tables || table.tables.length === 0) {
return;
}
table.fixedOffsetObserver = new ResizeObserver(() => {
if (table.fixedOffsetRaf) {
cancelAnimationFrame(table.fixedOffsetRaf);
}
table.fixedOffsetRaf = requestAnimationFrame(() => {
table.fixedOffsetRaf = null;
fixColumnOffset(table);
});
});
table.tables.forEach(t => table.fixedOffsetObserver.observe(t));
}

const fixColumnOffset = table => {
if (!table.tables || table.tables.length === 0) {
return;
}

table.tables.forEach(t => {
if (isVisible(t) === false || t.querySelector('.fixed') === null) {
return;
}

// 以首个完整行单元格的实际渲染宽度为基准重新计算固定列 sticky 偏移量
// 自动布局下列实际渲染宽度可能大于服务端声明宽度,导致偏移量与实际列宽错位
const cells = getOffsetBaseCells(t);
if (cells.length === 0) {
return;
}

const leftOffsets = [];
const rightOffsets = [];
let left = 0;
cells.forEach((cell, index) => {
leftOffsets[index] = left;
left += getWidth(cell);
});
let right = 0;
for (let index = cells.length - 1; index >= 0; index--) {
rightOffsets[index] = right;
right += getWidth(cells[index]);
}

[...t.querySelectorAll('tr')].forEach(row => {
[...row.children].forEach((cell, index) => {
if (cell.classList.contains('fixed')) {
if (cell.classList.contains('fixed-right')) {
if (rightOffsets[index] !== void 0) {
cell.style.right = `${rightOffsets[index]}px`;
}
}
else if (leftOffsets[index] !== void 0) {
cell.style.left = `${leftOffsets[index]}px`;
}
}
});
});
});
}

const getOffsetBaseCells = t => {
const headerRow = t.querySelector('thead > tr');
if (headerRow) {
return [...headerRow.children];
}
const bodyRow = [...t.querySelectorAll('tbody > tr')].find(r => !r.classList.contains('is-detail'));
return bodyRow ? [...bodyRow.children] : [];
}

const setAutoColWidths = (table, apply) => {
const colgroups = table.tables.map(t => t.querySelectorAll('colgroup col'));
table.autoColumns.forEach(({ colIndex, minWidth }) => {
Expand Down
99 changes: 99 additions & 0 deletions test/UnitTest/Components/TableTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,105 @@
}
}

[Fact]
public void ColumnFixed_Dynamic_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 2));
pb.Add(a => a.DefaultFixedColumnWidth, 200);
pb.Add(a => a.TableColumns, foo => builder =>
{
builder.OpenComponent<TableColumn<Foo, string>>(0);
builder.AddAttribute(1, "Field", foo.Name);
builder.AddAttribute(2, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Name), typeof(string)));
builder.CloseComponent();

builder.OpenComponent<TableColumn<Foo, string>>(3);
builder.AddAttribute(4, "Field", foo.Address);
builder.AddAttribute(5, "FieldExpression", Utility.GenerateValueExpression(foo, nameof(Foo.Address), typeof(string)));
builder.CloseComponent();
});
});
});

var table = cut.FindComponent<Table<Foo>>();

// 未固定列时 col 元素不输出宽度
cut.DoesNotContain("width: 200px;");

// 运行时动态设置前两列为固定列
table.Instance.Columns[0].Fixed = true;
table.Instance.Columns[1].Fixed = true;
cut.Render();

// 固定列未设置宽度时使用默认固定列宽渲染 col 元素
var cols = cut.FindAll("colgroup col");
Assert.Equal(2, cols.Count);
Assert.All(cols, col => Assert.Contains("width: 200px;", col.GetAttribute("style")));

// 固定列单元格输出固定样式与偏移量
cut.Contains("left: 0px;");
cut.Contains("left: 200px;");

// 取消固定后恢复
table.Instance.Columns[0].Fixed = false;
table.Instance.Columns[1].Fixed = false;
cut.Render();

cut.DoesNotContain("width: 200px;");
cut.DoesNotContain("left: 200px;");
}

[Fact]
public void ColumnFixed_DynamicAutoGenerateColumns_Ok()
{
var localizer = Context.Services.GetRequiredService<IStringLocalizer<Foo>>();
var cut = Context.Render<BootstrapBlazorRoot>(pb =>
{
pb.AddChildContent<Table<Foo>>(pb =>
{
pb.Add(a => a.RenderMode, TableRenderMode.Table);
pb.Add(a => a.Items, Foo.GenerateFoo(localizer, 2));
pb.Add(a => a.AutoGenerateColumns, true);
pb.Add(a => a.DefaultFixedColumnWidth, 200);
});
});

var table = cut.FindComponent<Table<Foo>>();

// 运行时动态设置前两列为固定列
table.Instance.Columns[0].Fixed = true;
table.Instance.Columns[1].Fixed = true;
cut.Render();

// 自动生成列模式下列实例每次渲染均会重新创建,固定状态应保留
Assert.True(table.Instance.Columns[0].Fixed);
Assert.True(table.Instance.Columns[1].Fixed);

// 未设置宽度的固定列使用默认固定列宽渲染 col 元素,已设置宽度的固定列保持原宽度
var cols = cut.FindAll("colgroup col");
Assert.Single(cols.Where(c => (c.GetAttribute("style") ?? "").Contains("width: 200px;")));

Check warning on line 1974 in test/UnitTest/Components/TableTest.cs

View workflow job for this annotation

GitHub Actions / run test

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)
Assert.Single(cols.Where(c => (c.GetAttribute("style") ?? "").Contains("width: 180px;")));

Check warning on line 1975 in test/UnitTest/Components/TableTest.cs

View workflow job for this annotation

GitHub Actions / run test

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)

// 固定列单元格输出固定样式与偏移量
cut.Contains("left: 0px;");
cut.Contains("left: 180px;");

// 取消固定后恢复
table.Instance.Columns[0].Fixed = false;
table.Instance.Columns[1].Fixed = false;
cut.Render();

Assert.False(table.Instance.Columns[0].Fixed);
cut.DoesNotContain("width: 200px;");
cut.DoesNotContain("left: 180px;");
}

[Fact]
public void ColumnFixed_TailColumn_Ok()
{
Expand Down
Loading