mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Measure auto fit column width against the rendered text
Replaces the character-count width estimate with Excel-faithful auto fit: the displayed text (number format applied to the evaluated value) of the column's populated cells is measured with canvas.measureText using the active theme's cell font, so formulas, number formats, bold and conditional formatting fit correctly in any theme. Empty columns keep their width, merged cells are ignored, hidden rows are included and the width is clamped to Excel's 255 character limit. The behavior was verified against Excel via COM automation. Auto fitted columns are tracked per column and persisted as bestFit in XLSX; manual resize and undo clear the flag.
This commit is contained in:
committed by
Atanas Korchev
parent
e306d13501
commit
da70790c3e
128
Radzen.Blazor.Tests/Spreadsheet/AutoFitXlsxRoundTripTests.cs
Normal file
128
Radzen.Blazor.Tests/Spreadsheet/AutoFitXlsxRoundTripTests.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Radzen.Documents.Spreadsheet;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Spreadsheet.Tests;
|
||||
|
||||
#nullable enable
|
||||
|
||||
// Round-trip the bestFit (auto fit) column flag through XlsxWriter + XlsxReader.
|
||||
public class AutoFitXlsxRoundTripTests
|
||||
{
|
||||
private static readonly XNamespace Ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
|
||||
private static Workbook Build()
|
||||
{
|
||||
var wb = new Workbook();
|
||||
wb.AddSheet("Sheet1", 10, 10);
|
||||
return wb;
|
||||
}
|
||||
|
||||
private static MemoryStream Save(Workbook wb)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
wb.SaveToStream(ms);
|
||||
ms.Position = 0;
|
||||
return ms;
|
||||
}
|
||||
|
||||
private static XDocument ReadSheetXml(MemoryStream ms)
|
||||
{
|
||||
using var zip = new ZipArchive(ms, ZipArchiveMode.Read, leaveOpen: true);
|
||||
using var stream = zip.GetEntry("xl/worksheets/sheet1.xml")!.Open();
|
||||
var doc = XDocument.Load(stream);
|
||||
ms.Position = 0;
|
||||
return doc;
|
||||
}
|
||||
|
||||
private static MemoryStream PatchSheetXml(MemoryStream ms, string colsXml)
|
||||
{
|
||||
using (var zip = new ZipArchive(ms, ZipArchiveMode.Update, leaveOpen: true))
|
||||
{
|
||||
var entry = zip.GetEntry("xl/worksheets/sheet1.xml")!;
|
||||
XDocument doc;
|
||||
using (var stream = entry.Open())
|
||||
{
|
||||
doc = XDocument.Load(stream);
|
||||
}
|
||||
|
||||
doc.Descendants(Ns + "cols").Remove();
|
||||
var cols = XElement.Parse(colsXml);
|
||||
foreach (var element in cols.DescendantsAndSelf())
|
||||
{
|
||||
element.Name = Ns + element.Name.LocalName;
|
||||
}
|
||||
doc.Root!.Element(Ns + "sheetData")!.AddBeforeSelf(cols);
|
||||
|
||||
entry.Delete();
|
||||
var newEntry = zip.CreateEntry("xl/worksheets/sheet1.xml");
|
||||
using var output = newEntry.Open();
|
||||
doc.Save(output);
|
||||
}
|
||||
|
||||
ms.Position = 0;
|
||||
return ms;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_PreservesWidthAndBestFit()
|
||||
{
|
||||
var wb = Build();
|
||||
wb.Sheets[0].Columns[1] = 240;
|
||||
wb.Sheets[0].Columns.SetAutoFit(1);
|
||||
|
||||
using var ms = Save(wb);
|
||||
|
||||
var col = ReadSheetXml(ms).Descendants(Ns + "col").Single();
|
||||
Assert.Equal("2", col.Attribute("min")?.Value);
|
||||
Assert.Equal("1", col.Attribute("bestFit")?.Value);
|
||||
Assert.Equal("1", col.Attribute("customWidth")?.Value);
|
||||
Assert.NotNull(col.Attribute("width"));
|
||||
|
||||
var loaded = Workbook.LoadFromStream(ms).Sheets[0];
|
||||
Assert.True(System.Math.Abs(loaded.Columns[1] - 240) < 0.5);
|
||||
Assert.True(loaded.Columns.IsAutoFit(1));
|
||||
Assert.False(loaded.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_AutoFitAtDefaultWidth_StillEmitsColumn()
|
||||
{
|
||||
var wb = Build();
|
||||
wb.Sheets[0].Columns.SetAutoFit(2);
|
||||
|
||||
using var ms = Save(wb);
|
||||
|
||||
var col = ReadSheetXml(ms).Descendants(Ns + "col").Single();
|
||||
Assert.Equal("3", col.Attribute("min")?.Value);
|
||||
Assert.Equal("1", col.Attribute("bestFit")?.Value);
|
||||
|
||||
var loaded = Workbook.LoadFromStream(ms).Sheets[0];
|
||||
Assert.True(loaded.Columns.IsAutoFit(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_BestFitWithoutWidth_SetsFlagAndKeepsDefaultWidth()
|
||||
{
|
||||
using var ms = PatchSheetXml(Save(Build()), """<cols><col min="2" max="3" bestFit="1"/></cols>""");
|
||||
|
||||
var loaded = Workbook.LoadFromStream(ms).Sheets[0];
|
||||
Assert.Equal(loaded.Columns.Size, loaded.Columns[1]);
|
||||
Assert.True(loaded.Columns.IsAutoFit(1));
|
||||
Assert.True(loaded.Columns.IsAutoFit(2));
|
||||
Assert.False(loaded.Columns.IsAutoFit(3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_BestFitRangeBeyondSheet_IsCapped()
|
||||
{
|
||||
using var ms = PatchSheetXml(Save(Build()), """<cols><col min="1" max="16384" width="12" bestFit="1" customWidth="1"/></cols>""");
|
||||
|
||||
var loaded = Workbook.LoadFromStream(ms).Sheets[0];
|
||||
Assert.True(loaded.Columns.IsAutoFit(0));
|
||||
Assert.True(loaded.Columns.IsAutoFit(loaded.Columns.Count - 1));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -797,62 +798,414 @@ public class SpreadsheetTests
|
||||
Assert.Equal(2, reported[^1]); // the new third sheet becomes active and is reported back
|
||||
}
|
||||
|
||||
// ── Auto fit column width ───────────────────────────────────────────
|
||||
|
||||
// SetupModule intercepts the InvokeAsync<IJSObjectReference> that creates the Spreadsheet JS
|
||||
// object, so measureTexts can return a canned result and its payload can be inspected.
|
||||
private static BunitJSModuleInterop SetupSpreadsheetJs(TestContext ctx, params double[] widths)
|
||||
{
|
||||
var module = ctx.JSInterop.SetupModule("Radzen.createSpreadsheet", _ => true);
|
||||
module.Mode = JSRuntimeMode.Loose;
|
||||
if (widths.Length > 0)
|
||||
{
|
||||
module.Setup<double[]>("measureTexts", _ => true).SetResult(widths);
|
||||
}
|
||||
return module;
|
||||
}
|
||||
|
||||
private static Task AutoFit(IRenderedComponent<RadzenSpreadsheet> c, int column)
|
||||
=> c.InvokeAsync(() => c.Instance.OnColumnResizeDoubleClickAsync(new CellEventArgs
|
||||
{
|
||||
Column = column,
|
||||
Pointer = new Microsoft.AspNetCore.Components.Web.PointerEventArgs()
|
||||
}));
|
||||
|
||||
private static List<TextMeasureItem> MeasuredItems(BunitJSModuleInterop module)
|
||||
{
|
||||
var invocation = Assert.Single(module.Invocations["measureTexts"]);
|
||||
return Assert.IsType<List<TextMeasureItem>>(invocation.Arguments[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ResizesToContent_WhenContentExists()
|
||||
public async Task AutoFitColumn_ResizesToMeasuredWidth()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "Short";
|
||||
sheet.Cells[1, 0].Value = "This is a much longer string";
|
||||
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await c.InvokeAsync(() => c.Instance.OnColumnResizeDoubleClickAsync(new CellEventArgs
|
||||
{
|
||||
Column = 0,
|
||||
Pointer = new Microsoft.AspNetCore.Components.Web.PointerEventArgs()
|
||||
}));
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.True(sheet.Columns[0] > 100);
|
||||
Assert.Equal(300, sheet.Columns[0]);
|
||||
Assert.True(sheet.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ResizesToDefault_WhenColumnIsEmpty()
|
||||
public async Task AutoFitColumn_MeasuresDisplayedValue_NotFormulaText()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Columns[1] = 50;
|
||||
|
||||
sheet.Cells[1, 0].Value = 1d;
|
||||
sheet.Cells[2, 0].Value = 2d;
|
||||
sheet.Cells[0, 0].Formula = "=A2+A3";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await c.InvokeAsync(() => c.Instance.OnColumnResizeDoubleClickAsync(new CellEventArgs
|
||||
{
|
||||
Column = 1,
|
||||
Pointer = new Microsoft.AspNetCore.Components.Web.PointerEventArgs()
|
||||
}));
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Equal(100, sheet.Columns[1]);
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Contains("3", texts);
|
||||
Assert.DoesNotContain("=A2+A3", texts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ClampsTo800_WhenContentIsExtremelyLong()
|
||||
public async Task AutoFitColumn_MeasuresNumberFormattedText()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = new string('A', 500);
|
||||
|
||||
sheet.Cells[0, 0].Value = 39448d;
|
||||
sheet.Cells[0, 0].Format.NumberFormat = "0.00";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await c.InvokeAsync(() => c.Instance.OnColumnResizeDoubleClickAsync(new CellEventArgs
|
||||
{
|
||||
Column = 0,
|
||||
Pointer = new Microsoft.AspNetCore.Components.Web.PointerEventArgs()
|
||||
}));
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Equal(800, sheet.Columns[0]);
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Contains("39448.00", texts);
|
||||
Assert.DoesNotContain("39448", texts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_EmptyColumn_KeepsWidth()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Columns[1] = 50;
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 1);
|
||||
|
||||
Assert.Equal(50, sheet.Columns[1]);
|
||||
Assert.False(sheet.Columns.IsAutoFit(1));
|
||||
module.VerifyNotInvoke("measureTexts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_IgnoresMergedCells()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "extremely long merged content that spans two columns";
|
||||
sheet.MergedCells.Add(new RangeRef(new CellRef(0, 0), new CellRef(0, 1)));
|
||||
sheet.Cells[1, 0].Value = "tiny";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Equal(["tiny"], texts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_IncludesHiddenRows()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "short";
|
||||
sheet.Cells[1, 0].Value = "very long content in a hidden row";
|
||||
sheet.Rows.Hide(1);
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Contains("very long content in a hidden row", texts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ClampsToMaxAndMinWidth()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx, 50000);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
Assert.Equal(ColumnWidthConversion.MaxWidthInPixels, sheet.Columns[0]);
|
||||
|
||||
using var ctx2 = CreateContext();
|
||||
SetupSpreadsheetJs(ctx2, 5);
|
||||
var wb2 = NewWorkbook();
|
||||
var sheet2 = wb2.Sheets[0];
|
||||
sheet2.Cells[0, 0].Value = "content";
|
||||
|
||||
var c2 = ctx2.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb2));
|
||||
|
||||
await AutoFit(c2, 0);
|
||||
Assert.Equal(RadzenSpreadsheet.MinColumnWidth, sheet2.Columns[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_Undo_RestoresWidthAndFlag()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
sheet.Columns[0] = 120;
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
Assert.Equal(300, sheet.Columns[0]);
|
||||
Assert.True(sheet.Columns.IsAutoFit(0));
|
||||
|
||||
await c.InvokeAsync(c.Instance.Undo);
|
||||
|
||||
Assert.Equal(120, sheet.Columns[0]);
|
||||
Assert.False(sheet.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ManualResize_ClearsFlag()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
Assert.True(sheet.Columns.IsAutoFit(0));
|
||||
|
||||
await Run(c, new ResizeColumnCommand(sheet, 0, 300, 150));
|
||||
|
||||
Assert.Equal(150, sheet.Columns[0]);
|
||||
Assert.False(sheet.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ResizingDisallowed_DoesNothing()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p =>
|
||||
{
|
||||
p.Add(x => x.Workbook, wb);
|
||||
p.Add(x => x.AllowResizing, false);
|
||||
});
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Equal(100, sheet.Columns[0]);
|
||||
module.VerifyNotInvoke("measureTexts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_DeduplicatesIdenticalCells()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "Same";
|
||||
sheet.Cells[1, 0].Value = "Same";
|
||||
sheet.Cells[2, 0].Value = "Same";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Single(MeasuredItems(module));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_NullMeasurement_DoesNothing()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Equal(100, sheet.Columns[0]);
|
||||
Assert.False(sheet.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_OutOfRangeOrHiddenColumn_DoesNothing()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 2].Value = "content";
|
||||
sheet.Columns.Hide(2);
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, -1);
|
||||
await AutoFit(c, sheet.Columns.Count);
|
||||
await AutoFit(c, 2);
|
||||
|
||||
Assert.Equal(100, sheet.Columns[2]);
|
||||
Assert.False(sheet.Columns.IsAutoFit(2));
|
||||
module.VerifyNotInvoke("measureTexts");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_ConditionalFormat_AffectsFontAndSkipsDedupe()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = 5d;
|
||||
sheet.Cells[5, 0].Value = 5d; // identical twin outside the rule range
|
||||
sheet.ConditionalFormats.Add(new RangeRef(new CellRef(0, 0), new CellRef(1, 0)),
|
||||
new GreaterThanRule { Value = 0, Format = new Format { Bold = true } });
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var items = MeasuredItems(module);
|
||||
Assert.Contains(items, item => item.Text == "5" && item.Bold);
|
||||
Assert.Contains(items, item => item.Text == "5" && !item.Bold);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_WrapText_MeasuresLongestLine()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "short\nthe longest line here";
|
||||
sheet.Cells[0, 0].Format.WrapText = true;
|
||||
sheet.Cells[1, 0].Value = "a\nbb";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Contains("the longest line here", texts);
|
||||
Assert.Contains("a bb", texts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_TruncatesAndCapsPayload()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = new Workbook();
|
||||
var sheet = wb.AddSheet("Sheet1", 250, 5);
|
||||
sheet.Cells[0, 0].Value = new string('A', 1500);
|
||||
for (var row = 1; row < 220; row++)
|
||||
{
|
||||
sheet.Cells[row, 0].Value = $"item {row}";
|
||||
sheet.Cells[row, 0].Format.FontSize = row + 1d; // distinct font group per cell
|
||||
}
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var items = MeasuredItems(module);
|
||||
Assert.Equal(1000, items.Max(item => item.Text.Length));
|
||||
Assert.True(items.Count <= 200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_PoisonCell_IsSkipped()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
var module = SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = new Workbook();
|
||||
var sheet = wb.AddSheet("Sheet1", 200, 5);
|
||||
sheet.Cells[0, 0].Value = "good";
|
||||
// Out-of-range date serial in a row outside the rendered viewport
|
||||
sheet.Cells[150, 0].Value = 1e10;
|
||||
sheet.Cells[150, 0].Format.NumberFormat = "dddd, mmmm dd, yyyy";
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
var texts = MeasuredItems(module).Select(item => item.Text).ToList();
|
||||
Assert.Contains("good", texts);
|
||||
Assert.Equal(300, sheet.Columns[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_AlreadyAtFitWidth_StillSetsFlag()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = "content";
|
||||
sheet.Columns[0] = 300;
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
Assert.Equal(300, sheet.Columns[0]);
|
||||
Assert.True(sheet.Columns.IsAutoFit(0));
|
||||
|
||||
await c.InvokeAsync(c.Instance.Undo);
|
||||
Assert.False(sheet.Columns.IsAutoFit(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoFitColumn_Top10RuleOverSparseColumn_DoesNotThrow()
|
||||
{
|
||||
using var ctx = CreateContext();
|
||||
SetupSpreadsheetJs(ctx, 300);
|
||||
var wb = NewWorkbook();
|
||||
var sheet = wb.Sheets[0];
|
||||
sheet.Cells[0, 0].Value = 1d;
|
||||
sheet.Cells[1, 0].Value = 2d;
|
||||
sheet.ConditionalFormats.Add(new RangeRef(new CellRef(0, 0), new CellRef(9, 0)),
|
||||
new Top10Rule { Count = 1, Format = new Format { Bold = true } });
|
||||
|
||||
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
|
||||
|
||||
await AutoFit(c, 0);
|
||||
|
||||
Assert.Equal(300, sheet.Columns[0]);
|
||||
}
|
||||
|
||||
private static class EventCallbackFactory
|
||||
|
||||
@@ -46,6 +46,8 @@ public class Axis(double size, int count)
|
||||
|
||||
private readonly HashSet<int> hidden = [];
|
||||
|
||||
private readonly HashSet<int> autoFit = [];
|
||||
|
||||
private bool isUpdating;
|
||||
|
||||
/// <summary>
|
||||
@@ -120,6 +122,25 @@ public class Axis(double size, int count)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the size at the specified index was set by auto fit (persisted as bestFit in XLSX).
|
||||
/// </summary>
|
||||
public bool IsAutoFit(int index)
|
||||
{
|
||||
return autoFit.Contains(index);
|
||||
}
|
||||
|
||||
// No change event: the flag has no visual effect, it only feeds the XLSX writer.
|
||||
internal void SetAutoFit(int index)
|
||||
{
|
||||
autoFit.Add(index);
|
||||
}
|
||||
|
||||
internal void ClearAutoFit(int index)
|
||||
{
|
||||
autoFit.Remove(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size of the axis at the specified index.
|
||||
/// </summary>
|
||||
@@ -167,6 +188,7 @@ public class Axis(double size, int count)
|
||||
|
||||
DictionaryShift.Remap(data, Remap);
|
||||
DictionaryShift.Remap(hidden, Remap);
|
||||
DictionaryShift.Remap(autoFit, Remap);
|
||||
}
|
||||
|
||||
internal void ShiftDown(int fromIndex, int count)
|
||||
@@ -175,6 +197,7 @@ public class Axis(double size, int count)
|
||||
|
||||
DictionaryShift.Remap(data, Remap);
|
||||
DictionaryShift.Remap(hidden, Remap);
|
||||
DictionaryShift.Remap(autoFit, Remap);
|
||||
}
|
||||
|
||||
internal IEnumerable<int> GetCustomSizedIndices() => data.Keys;
|
||||
|
||||
@@ -94,6 +94,16 @@ public class Cell
|
||||
Changed?.Invoke(this);
|
||||
}
|
||||
|
||||
internal Format? FormatOrNull => format;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the text displayed in the cell: the number format applied to the value, or the value as a string.
|
||||
/// </summary>
|
||||
public string? GetDisplayText()
|
||||
{
|
||||
return NumberFormat.Apply(format?.NumberFormat, Value, ValueType) ?? Value?.ToString();
|
||||
}
|
||||
|
||||
internal Format? GetEffectiveFormat()
|
||||
{
|
||||
var effectiveFormat = format;
|
||||
|
||||
16
Radzen.Blazor/Documents/Spreadsheet/ColumnWidthConversion.cs
Normal file
16
Radzen.Blazor/Documents/Spreadsheet/ColumnWidthConversion.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Radzen.Documents.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
// XLSX column widths are in characters of the Normal font's maximum digit width (7px per character).
|
||||
internal static class ColumnWidthConversion
|
||||
{
|
||||
// Excel caps column width at 255 characters; 255 * 7 converts back to exactly 255.
|
||||
public const double MaxWidthInPixels = 255 * 7;
|
||||
|
||||
public static double PixelsToChars(double pixels) => Math.Round(pixels / 7.0, 8);
|
||||
|
||||
public static double CharsToPixels(double chars) => (256 * chars + Math.Truncate(128 / 7.0)) / 256 * 7;
|
||||
}
|
||||
@@ -412,21 +412,29 @@ static class XlsxReader
|
||||
{
|
||||
var min = col.Attribute("min")?.Value;
|
||||
var max = col.Attribute("max")?.Value;
|
||||
var width = col.Attribute("width")?.Value;
|
||||
|
||||
if (min is not null && max is not null && width is not null &&
|
||||
int.TryParse(min, out var minCol) &&
|
||||
int.TryParse(max, out var maxCol) &&
|
||||
double.TryParse(width, NumberStyles.Float, CultureInfo.InvariantCulture, out var colWidth))
|
||||
if (min is null || max is null ||
|
||||
!int.TryParse(min, out var minCol) ||
|
||||
!int.TryParse(max, out var maxCol))
|
||||
{
|
||||
// Excel column width to pixels conversion:
|
||||
// pixels = Truncate(((256 * width + Truncate(128/7)) / 256) * 7)
|
||||
var pixelWidth = (256 * colWidth + Math.Truncate(128 / 7.0)) / 256 * 7;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = minCol - 1; i <= maxCol - 1 && i < sheet.Columns.Count; i++)
|
||||
var bestFit = col.Attribute("bestFit")?.Value is "1" or "true";
|
||||
var hasWidth = double.TryParse(col.Attribute("width")?.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var colWidth);
|
||||
var pixelWidth = ColumnWidthConversion.CharsToPixels(colWidth);
|
||||
|
||||
for (var i = minCol - 1; i <= maxCol - 1 && i < sheet.Columns.Count; i++)
|
||||
{
|
||||
if (hasWidth)
|
||||
{
|
||||
sheet.Columns[i] = pixelWidth;
|
||||
}
|
||||
|
||||
if (bestFit)
|
||||
{
|
||||
sheet.Columns.SetAutoFit(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1175,11 +1175,12 @@ class XlsxWriter(Workbook sourceWorkbook)
|
||||
private static XElement? CreateColumns(Worksheet sheet)
|
||||
{
|
||||
var colElements = Enumerable.Range(0, sheet.ColumnCount)
|
||||
.Where(col => Math.Abs(sheet.Columns[col] - sheet.Columns.Size) > 1e-6)
|
||||
.Where(col => Math.Abs(sheet.Columns[col] - sheet.Columns.Size) > 1e-6 || sheet.Columns.IsAutoFit(col))
|
||||
.Select(col => new XElement(XName.Get("col", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"),
|
||||
new XAttribute("min", col + 1),
|
||||
new XAttribute("max", col + 1),
|
||||
new XAttribute("width", Math.Round(sheet.Columns[col] / 7.0, 8)),
|
||||
new XAttribute("width", ColumnWidthConversion.PixelsToChars(sheet.Columns[col])),
|
||||
sheet.Columns.IsAutoFit(col) ? new XAttribute("bestFit", "1") : null,
|
||||
new XAttribute("customWidth", "1")))
|
||||
.ToList();
|
||||
|
||||
|
||||
@@ -2709,12 +2709,15 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
internal const double MinColumnWidth = 24;
|
||||
internal const double MinRowHeight = 16;
|
||||
|
||||
private Task OnColumnResizePointerMoveAsync(ColumnResizeCapture capture, PointerEventArgs pointer)
|
||||
{
|
||||
if (Worksheet != null && capture.Column >= 0 && capture.Column < Worksheet.Columns.Count)
|
||||
{
|
||||
var delta = pointer.ClientX - capture.StartX;
|
||||
var newWidth = Math.Max(24, capture.StartWidth + delta);
|
||||
var newWidth = Math.Max(MinColumnWidth, capture.StartWidth + delta);
|
||||
Worksheet.Columns[capture.Column] = newWidth;
|
||||
}
|
||||
|
||||
@@ -2726,7 +2729,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
if (Worksheet != null && capture.Row >= 0 && capture.Row < Worksheet.Rows.Count)
|
||||
{
|
||||
var delta = pointer.ClientY - capture.StartY;
|
||||
var newHeight = Math.Max(16, capture.StartHeight + delta);
|
||||
var newHeight = Math.Max(MinRowHeight, capture.StartHeight + delta);
|
||||
Worksheet.Rows[capture.Row] = newHeight;
|
||||
}
|
||||
|
||||
@@ -3156,8 +3159,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
activeCapture = null;
|
||||
}
|
||||
/// <summary>
|
||||
/// Automatically adjusts the column width to fit the content of the longest cell,
|
||||
/// constrained by a maximum width of 800 pixels or defaults to 100 pixels if empty.
|
||||
/// Invoked by JS interop when a column resize handle is double-clicked. Auto fits the column width to its displayed content.
|
||||
/// </summary>
|
||||
/// <param name="args">The column event arguments containing the target column index.</param>
|
||||
[JSInvokable]
|
||||
@@ -3165,47 +3167,189 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(args);
|
||||
|
||||
if (!IsFeatureAllowed(SpreadsheetFeature.Resizing) || Worksheet == null)
|
||||
if (!IsFeatureAllowed(SpreadsheetFeature.Resizing) || Worksheet == null || jsRef == null ||
|
||||
args.Column < 0 || args.Column >= Worksheet.Columns.Count || Worksheet.Columns.IsHidden(args.Column))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await AcceptAsync();
|
||||
|
||||
if (result)
|
||||
if (!await AcceptAsync())
|
||||
{
|
||||
int targetColumn = args.Column;
|
||||
double maxWidth = 0;
|
||||
bool hasContent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (int r = 0; r < Worksheet.RowCount; r++)
|
||||
{
|
||||
if (Worksheet.Cells.TryGet(r, targetColumn, out var cell))
|
||||
{
|
||||
var text = cell.GetValue()?.ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
hasContent = true;
|
||||
var estimatedWidth = (text.Length * 8) + 16;
|
||||
|
||||
if (estimatedWidth > maxWidth)
|
||||
{
|
||||
maxWidth = estimatedWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var items = GetAutoFitMeasureItems(Worksheet, args.Column);
|
||||
|
||||
var startWidth = Worksheet.Columns[targetColumn];
|
||||
var finalWidth = hasContent ? Math.Min(Math.Max(maxWidth, 24), 800) : 100; // fall back to the default 100
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (startWidth != finalWidth)
|
||||
{
|
||||
await ExecuteAsync(new ResizeColumnCommand(Worksheet, targetColumn, startWidth, finalWidth));
|
||||
}
|
||||
double[]? widths;
|
||||
|
||||
try
|
||||
{
|
||||
widths = await jsRef.InvokeAsync<double[]>("measureTexts", items);
|
||||
}
|
||||
catch (Exception ex) when (ex is JSException or JSDisconnectedException or ObjectDisposedException or TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (widths is not { Length: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var maxWidth = widths.Max();
|
||||
|
||||
if (maxWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var targetWidth = Math.Clamp(maxWidth, MinColumnWidth, ColumnWidthConversion.MaxWidthInPixels);
|
||||
var startWidth = Worksheet.Columns[args.Column];
|
||||
|
||||
if (startWidth != targetWidth || !Worksheet.Columns.IsAutoFit(args.Column))
|
||||
{
|
||||
await ExecuteAsync(new ResizeColumnCommand(Worksheet, args.Column, startWidth, targetWidth, isAutoFit: true));
|
||||
}
|
||||
}
|
||||
|
||||
private const int AutoFitMaxTextLength = 1000;
|
||||
private const int AutoFitCandidatesPerFontGroup = 20;
|
||||
private const int AutoFitMaxMeasureItems = 200;
|
||||
|
||||
private static List<TextMeasureItem> GetAutoFitMeasureItems(Worksheet sheet, int column)
|
||||
{
|
||||
// Snapshot: computing effective formats can populate cells in the store.
|
||||
var cells = sheet.Cells.GetPopulatedCells().Where(cell => cell.Address.Column == column).ToList();
|
||||
|
||||
var conditionalRanges = sheet.ConditionalFormats.Ranges
|
||||
.Where(range => range.Start.Column <= column && column <= range.End.Column)
|
||||
.ToList();
|
||||
|
||||
var seen = new HashSet<(string, bool, bool, double?, string?)>();
|
||||
var candidates = new List<(Cell Cell, string Text, (bool Bold, bool Italic, double? FontSize, string? FontFamily) Font)>();
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (sheet.MergedCells.Contains(cell.Address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? text;
|
||||
|
||||
try
|
||||
{
|
||||
text = cell.GetDisplayText();
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var format = cell.FormatOrNull;
|
||||
|
||||
if (text.Contains('\n', StringComparison.Ordinal) || text.Contains('\r', StringComparison.Ordinal))
|
||||
{
|
||||
// Wrap renders hard line breaks so the longest line governs; nowrap collapses them.
|
||||
text = text.Replace("\r", "", StringComparison.Ordinal);
|
||||
text = format?.WrapText == true ? LongestLine(text) : text.Replace('\n', ' ');
|
||||
}
|
||||
|
||||
if (text.Length > AutoFitMaxTextLength)
|
||||
{
|
||||
text = text[..AutoFitMaxTextLength];
|
||||
}
|
||||
|
||||
var font = (format?.Bold == true, format?.Italic == true, format?.FontSize, format?.FontFamily);
|
||||
|
||||
// Conditionally formatted cells are exempt from dedupe - an unstyled twin must not replace them.
|
||||
var conditional = conditionalRanges.Any(range => range.Contains(cell.Address));
|
||||
|
||||
if (!conditional && !seen.Add((text, font.Item1, font.Item2, font.Item3, font.Item4)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.Add((cell, text, font));
|
||||
}
|
||||
|
||||
var survivors = candidates
|
||||
.GroupBy(candidate => candidate.Font)
|
||||
.SelectMany(group => group.OrderByDescending(candidate => candidate.Text.Length).Take(AutoFitCandidatesPerFontGroup));
|
||||
|
||||
var items = new List<TextMeasureItem>();
|
||||
|
||||
foreach (var (cell, text, font) in survivors)
|
||||
{
|
||||
var (bold, italic, fontSize, fontFamily) = font;
|
||||
|
||||
if (conditionalRanges.Count > 0)
|
||||
{
|
||||
Format? effective;
|
||||
|
||||
try
|
||||
{
|
||||
effective = cell.GetEffectiveFormat();
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bold = effective?.Bold == true;
|
||||
italic = effective?.Italic == true;
|
||||
fontSize = effective?.FontSize;
|
||||
fontFamily = effective?.FontFamily;
|
||||
}
|
||||
|
||||
if (fontSize is { } size && !double.IsFinite(size))
|
||||
{
|
||||
fontSize = null;
|
||||
}
|
||||
|
||||
items.Add(new TextMeasureItem
|
||||
{
|
||||
Text = text,
|
||||
Bold = bold,
|
||||
Italic = italic,
|
||||
FontSize = fontSize,
|
||||
FontFamily = fontFamily
|
||||
});
|
||||
}
|
||||
|
||||
if (items.Count > AutoFitMaxMeasureItems)
|
||||
{
|
||||
items = items.OrderByDescending(item => item.Text.Length).Take(AutoFitMaxMeasureItems).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static string LongestLine(string text)
|
||||
{
|
||||
var longest = "";
|
||||
|
||||
foreach (var line in text.Split('\n'))
|
||||
{
|
||||
if (line.Length > longest.Length)
|
||||
{
|
||||
longest = line;
|
||||
}
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
activeCapture = null;
|
||||
|
||||
@@ -123,7 +123,7 @@ public partial class CellView : CellBase, IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
var (formatted, color) = NumberFormat.ApplyWithColor(cell.Format?.NumberFormat, cell.Value, cell.ValueType);
|
||||
var (formatted, color) = NumberFormat.ApplyWithColor(cell.FormatOrNull?.NumberFormat, cell.Value, cell.ValueType);
|
||||
numberFormatColor = color;
|
||||
return formatted ?? cell.Value?.ToString();
|
||||
}
|
||||
|
||||
@@ -5,15 +5,29 @@ namespace Radzen.Blazor.Spreadsheet;
|
||||
/// <summary>
|
||||
/// Undoable command that resizes a column to a new width.
|
||||
/// </summary>
|
||||
public sealed class ResizeColumnCommand(Worksheet sheet, int column, double oldWidth, double newWidth) : ICommand
|
||||
public sealed class ResizeColumnCommand(Worksheet sheet, int column, double oldWidth, double newWidth, bool isAutoFit = false) : ICommand
|
||||
{
|
||||
private bool wasAutoFit;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SpreadsheetFeature? Feature => null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Execute()
|
||||
{
|
||||
wasAutoFit = sheet.Columns.IsAutoFit(column);
|
||||
|
||||
sheet.Columns[column] = newWidth;
|
||||
|
||||
if (isAutoFit)
|
||||
{
|
||||
sheet.Columns.SetAutoFit(column);
|
||||
}
|
||||
else
|
||||
{
|
||||
sheet.Columns.ClearAutoFit(column);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,6 +35,15 @@ public sealed class ResizeColumnCommand(Worksheet sheet, int column, double oldW
|
||||
public void Unexecute()
|
||||
{
|
||||
sheet.Columns[column] = oldWidth;
|
||||
|
||||
if (wasAutoFit)
|
||||
{
|
||||
sheet.Columns.SetAutoFit(column);
|
||||
}
|
||||
else
|
||||
{
|
||||
sheet.Columns.ClearAutoFit(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,9 +183,7 @@ public partial class SpreadsheetAccessibility : ComponentBase, IDisposable
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NumberFormat.Apply(c.Format.NumberFormat, c.Value, c.ValueType)
|
||||
?? c.Value?.ToString()
|
||||
?? string.Empty;
|
||||
return c.GetDisplayText() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string GetHeader(Worksheet worksheet, CellRef cell, string content)
|
||||
|
||||
12
Radzen.Blazor/Spreadsheet/TextMeasureItem.cs
Normal file
12
Radzen.Blazor/Spreadsheet/TextMeasureItem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Radzen.Blazor.Spreadsheet;
|
||||
|
||||
#nullable enable
|
||||
|
||||
internal sealed class TextMeasureItem
|
||||
{
|
||||
public string Text { get; set; } = "";
|
||||
public bool Bold { get; set; }
|
||||
public bool Italic { get; set; }
|
||||
public double? FontSize { get; set; }
|
||||
public string? FontFamily { get; set; }
|
||||
}
|
||||
@@ -6210,25 +6210,49 @@ class Spreadsheet {
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleClick = (e) => {
|
||||
if (e.target.matches('.rz-spreadsheet-cell')) {
|
||||
const cell = e.target;
|
||||
const row = +cell.dataset.row;
|
||||
const column = +cell.dataset.column;
|
||||
this.dotNetRef.invokeMethodAsync('OnCellDoubleClickAsync', { row, column, pointer: this.toEventArgs(e) });
|
||||
} else if (e.target.matches('.rz-spreadsheet-column-resize-handle')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const column = +e.target.dataset.column;
|
||||
|
||||
this.dotNetRef.invokeMethodAsync('OnColumnResizeDoubleClickAsync', {
|
||||
row: 0,
|
||||
column: column,
|
||||
pointer: this.toEventArgs(e)
|
||||
});
|
||||
}
|
||||
onDoubleClick = (e) => {
|
||||
if (e.target.matches('.rz-spreadsheet-cell')) {
|
||||
const cell = e.target;
|
||||
const row = +cell.dataset.row;
|
||||
const column = +cell.dataset.column;
|
||||
this.dotNetRef.invokeMethodAsync('OnCellDoubleClickAsync', { row, column, pointer: this.toEventArgs(e) });
|
||||
} else if (e.target.matches('.rz-spreadsheet-column-resize-handle')) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const column = +e.target.dataset.column;
|
||||
this.dotNetRef.invokeMethodAsync('OnColumnResizeDoubleClickAsync', { row: 0, column, pointer: this.toEventArgs(e) });
|
||||
}
|
||||
}
|
||||
|
||||
measureTexts = (items) => {
|
||||
const probe = document.createElement('div');
|
||||
probe.className = 'rz-spreadsheet-cell';
|
||||
probe.style.visibility = 'hidden';
|
||||
this.element.appendChild(probe);
|
||||
const style = getComputedStyle(probe);
|
||||
const baseFont = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
|
||||
const padding = (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0)
|
||||
+ (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.borderRightWidth) || 0);
|
||||
const baseFontSize = style.fontSize;
|
||||
const baseFontFamily = style.fontFamily;
|
||||
this.element.removeChild(probe);
|
||||
|
||||
this.measureCanvas ??= document.createElement('canvas');
|
||||
const context = this.measureCanvas.getContext('2d');
|
||||
context.direction = this.rtl ? 'rtl' : 'ltr';
|
||||
|
||||
return items.map(item => {
|
||||
// Invalid font shorthand assignments are silently ignored; reset so they fall back to the base font.
|
||||
context.font = baseFont;
|
||||
if (item.bold || item.italic || item.fontSize != null || item.fontFamily) {
|
||||
const size = item.fontSize != null ? item.fontSize + 'pt' : baseFontSize;
|
||||
const family = item.fontFamily ? `"${item.fontFamily.replaceAll('"', '\\"')}", ${baseFontFamily}` : baseFontFamily;
|
||||
context.font = `${item.italic ? 'italic ' : ''}${item.bold ? 'bold ' : ''}${size} ${family}`;
|
||||
}
|
||||
const width = context.measureText(item.text).width;
|
||||
return Number.isFinite(width) ? Math.ceil(width + padding) : 0;
|
||||
});
|
||||
}
|
||||
|
||||
onColumnPointerUp = (e) => {
|
||||
removeEventListener('pointermove', this.onColumnPointerMove);
|
||||
@@ -6612,6 +6636,9 @@ class SheetEditor {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Radzen.createSheetEditor = (element, value, autoFocus, dotNetRef) => new SheetEditor(element, value, autoFocus, dotNetRef);
|
||||
Radzen.createSpreadsheet = (element, dotNetRef, shortcuts) => new Spreadsheet(element, dotNetRef, shortcuts);
|
||||
Radzen.createVirtualItemContainer = (scrollable, content, ref) => {
|
||||
|
||||
Reference in New Issue
Block a user