Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
47cbc26
feat: 增加 Key 参数
ArgoZhang Aug 1, 2025
da33062
refactor: 使用回调方法 GetKeyByITem 获得行 Key 值
ArgoZhang Aug 1, 2025
e7dfe41
chore: bump version 9.6.5
ArgoZhang Aug 2, 2025
1a71e4c
feat: 增加 PdfOptions 配置项
ArgoZhang Oct 27, 2025
63ad92d
chore: bump version 9.6.6
ArgoZhang Oct 27, 2025
cce713e
refactor: 调整 IconTemplate 优先级别
ArgoZhang Oct 29, 2025
c8a6c7c
feat: 增加 ActionButtonTemplate 模板
ArgoZhang Oct 30, 2025
afa14bd
refactor: 增加条件
ArgoZhang Oct 30, 2025
be72b25
chore: bump version 9.6.7
ArgoZhang Oct 30, 2025
eed08c3
feat(IpAddress): support paste function (#7276)
ArgoZhang Dec 9, 2025
bfc8abb
chore: bump version 9.6.8
ArgoZhang Dec 9, 2025
943e0bb
feat(IpAddress): trigger ValueChanged on paste event (#7280)
ArgoZhang Dec 9, 2025
5260e88
chore: bump version 9.6.9
ArgoZhang Dec 9, 2025
044a84a
chore: 支持 Interop 参数
ArgoZhang Dec 9, 2025
77da6d8
chore: bump version 9.6.10
ArgoZhang Dec 9, 2025
924fcb8
feat: 增加 OnBeforeTreeItemClick 方法
ArgoZhang Dec 16, 2025
ffde92e
chore: bump version 9.6.11
ArgoZhang Dec 16, 2025
9fb4eda
refactor: 兼容嵌套问题
ArgoZhang Dec 18, 2025
ae0b16f
chore: bump version 9.6.12
ArgoZhang Dec 18, 2025
16782ef
feat: 支持端口
ArgoZhang Dec 18, 2025
740ebf1
chore: bump version 9.6-13-beta01
ArgoZhang Dec 18, 2025
1efed9a
chore: bump version 9.6.13
ArgoZhang Dec 18, 2025
8b0b637
feat(ZipArchiveService): add ArchiveDirectoryAsync method
ArgoZhang Dec 19, 2025
fd7a470
test: 增加单元测试
ArgoZhang Dec 19, 2025
69c80c2
chore: bump version 9.6.14
ArgoZhang Dec 19, 2025
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/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.6.4</Version>
<Version>9.6.14</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ protected virtual void OnLoadJSModule()
/// call JavaScript method
/// </summary>
/// <returns></returns>
protected virtual Task InvokeInitAsync() => InvokeVoidAsync("init", Id);
protected virtual Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop);
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InvokeInitAsync method now unconditionally passes the Interop parameter to all JavaScript init functions. This is a breaking change that could cause issues for existing JavaScript modules that don't expect this parameter. Consider making this conditional based on whether Interop is not null, or ensure backward compatibility by checking if the JS module accepts this parameter.

Suggested change
protected virtual Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop);
protected virtual Task InvokeInitAsync()
{
// For backward compatibility, only pass Interop when it is available.
return Interop is not null
? InvokeVoidAsync("init", Id, Interop)
: InvokeVoidAsync("init", Id);
}

Copilot uses AI. Check for mistakes.

/// <summary>
/// call JavaScript method
Expand Down
11 changes: 11 additions & 0 deletions src/BootstrapBlazor/Components/BaseComponents/DynamicElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ public class DynamicElement : BootstrapComponentBase
[Parameter]
public bool GenerateElement { get; set; } = true;

/// <summary>
/// 获得/设置 组件 Key 值
/// </summary>
[Parameter]
public object? Key { get; set; }

/// <summary>
/// BuildRenderTree 方法
/// </summary>
Expand Down Expand Up @@ -119,6 +125,11 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)

builder.AddContent(8, ChildContent);

if (Key != null)
{
builder.SetKey(Key);
Comment on lines +128 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Key is applied too late in the render sequence and won’t key the intended element.

In Blazor, builder.SetKey must be called immediately after OpenElement/OpenComponent and before any attributes or children so the key applies to that element. Placing it after AddContent(8, ChildContent) will key the next sequence instead of the wrapper DynamicElement. When GenerateElement is true, move SetKey(Key) to directly after builder.OpenElement(0, TagName) and before adding attributes/children, to preserve the behavior callers expect when replacing @key with Key="...".

}
Comment on lines +128 to +131
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SetKey method is called at the wrong position in the render tree. According to Blazor's rendering semantics, SetKey must be called immediately after OpenElement/OpenComponent and before any attributes or content are added. Currently, it's being called after content has been added (line 126), which means the key will not work as intended.

Copilot uses AI. Check for mistakes.

if (GenerateElement || IsTriggerClick() || IsTriggerDoubleClick())
{
builder.CloseElement();
Expand Down
7 changes: 4 additions & 3 deletions src/BootstrapBlazor/Components/Drawer/Drawer.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const initDrag = el => {
let height = 0;
let isVertical = false;
const drawerBody = el.querySelector('.drawer-body');
const bar = el.querySelector('.drawer-bar');
const bar = [...drawerBody.children].find(i => i.classList.contains('drawer-bar'));
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no null check after finding the drawer-bar element. If the drawer-bar element doesn't exist in the children, 'bar' will be undefined, and passing it to Drag.drag() could cause a runtime error. Add a null/undefined check before calling Drag.drag().

Copilot uses AI. Check for mistakes.
Drag.drag(bar,
e => {
isVertical = drawerBody.classList.contains("top") || drawerBody.classList.contains("bottom")
Expand Down Expand Up @@ -105,7 +105,7 @@ export function execute(id, open) {
showDrawer();
}
}

const showDrawer = () => {
drawerBody.classList.add('show');
if (drawerBackdrop) {
Expand Down Expand Up @@ -175,7 +175,8 @@ export function dispose(id) {
body.classList.remove('overflow-hidden')
}

const bar = el.querySelector('.drawer-bar');
const drawerBody = el.querySelector('.drawer-body');
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no null check after finding the drawer-bar element in the dispose function. If the drawer-bar element doesn't exist in the children, 'bar' will be undefined, and the subsequent null check on line 180 will pass (since undefined is falsy), but it's semantically unclear. Consider adding an explicit check or comment to clarify the intent.

Suggested change
const drawerBody = el.querySelector('.drawer-body');
const drawerBody = el.querySelector('.drawer-body');
if (!drawerBody) {
return;
}

Copilot uses AI. Check for mistakes.
const bar = [...drawerBody.children].find(i => i.classList.contains('drawer-bar'));
if (bar) {
Drag.dispose(bar)
}
Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Components/IpAddress/IpAddress.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@namespace BootstrapBlazor.Components
@namespace BootstrapBlazor.Components
@inherits ValidateBase<string>
@attribute [BootstrapModuleAutoLoader]
@attribute [BootstrapModuleAutoLoader(JSObjectReference = true)]

@if (IsShowLabel)
{
Expand Down
23 changes: 20 additions & 3 deletions src/BootstrapBlazor/Components/IpAddress/IpAddress.razor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand All @@ -21,7 +21,7 @@ public partial class IpAddress
/// <summary>
/// 获得 class 样式集合
/// </summary>
protected string? ClassName => CssBuilder.Default("ipaddress form-control")
protected string? ClassName => CssBuilder.Default("bb-ip form-control")
.AddClass("disabled", IsDisabled)
.AddClass(CssClass).AddClass(ValidCss)
.Build();
Expand Down Expand Up @@ -62,7 +62,6 @@ private void ValueChanged1(ChangeEventArgs args)
{
Value1 = Value1[0..3];
}

UpdateValue();
}

Expand Down Expand Up @@ -112,4 +111,22 @@ private void UpdateValue()
{
CurrentValueAsString = $"{Value1}.{Value2}.{Value3}.{Value4}";
}

/// <summary>
/// 更新 值方法供 JS 调用
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <param name="v3"></param>
/// <param name="v4"></param>
[JSInvokable]
public void TriggerUpdate(int v1, int v2, int v3, int v4)
{
Value1 = v1.ToString();
Value2 = v2.ToString();
Value3 = v3.ToString();
Value4 = v4.ToString();

UpdateValue();
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TriggerUpdate method is missing accessibility for assistive technologies. When the IP address is updated via JavaScript (e.g., from paste), there's no indication to screen readers that the value has changed. Consider triggering a StateHasChanged call or firing an appropriate event to notify assistive technologies of the value change.

Suggested change
UpdateValue();
UpdateValue();
InvokeAsync(StateHasChanged);

Copilot uses AI. Check for mistakes.
}
}
46 changes: 42 additions & 4 deletions src/BootstrapBlazor/Components/IpAddress/IpAddress.razor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Data from "../../modules/data.js"
import Data from "../../modules/data.js"
import EventHandler from "../../modules/event-handler.js"

const selectCell = (el, index) => {
Expand All @@ -13,7 +13,7 @@ const selectCell = (el, index) => {
return c
}

Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function signature has been updated to include an 'invoke' parameter, but there is no JSDoc comment explaining what this parameter is for or how it should be used. Adding documentation would improve code maintainability.

Suggested change
/**
* Initializes the IPv4 address input component.
*
* @param {string} id The id of the root element that contains the IPv4 input cells.
* @param {*} invoke An interop object that exposes `invokeMethodAsync`, used to notify the host
* (for example, a .NET/Blazor component) when the IP address changes, such as
* after a successful paste operation.
*/

Copilot uses AI. Check for mistakes.
export function init(id) {
export function init(id, invoke) {
const el = document.getElementById(id)
if (el === null) {
return
Expand All @@ -25,7 +25,6 @@ export function init(id) {
el.querySelectorAll(".ipv4-cell").forEach((c, index) => {
EventHandler.on(c, 'keydown', e => {
if ((e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 96 && e.keyCode <= 105)) {
// numbers, backup last status
ip.prevValues[index] = c.value
if (c.value === "0") {
c.value = ""
Expand Down Expand Up @@ -56,7 +55,15 @@ export function init(id) {
selectCell(el, index - 1)
}
}
else if (e.key === 'Delete' || e.key === 'Tab' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
else if (current.selectionStart === current.value.length && (e.key === 'Space' || e.key === 'ArrowRight')) {
e.preventDefault()
selectCell(el, index + 1)
}
else if (current.selectionStart === 0 && e.key === 'ArrowLeft') {
e.preventDefault()
selectCell(el, index - 1)
}
else if (e.composed || e.key === 'Delete' || e.key === 'Tab' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {

}
else {
Expand All @@ -72,6 +79,37 @@ export function init(id) {
}
}
})

EventHandler.on(c, 'paste', e => {
e.preventDefault();
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The preventDefault() call should be placed after validation checks. If the clipboard data is empty or doesn't contain a valid IP address, the default paste behavior is still prevented, which could confuse users. Consider moving preventDefault() to after the validation on line 94 where it's confirmed the data is valid.

Copilot uses AI. Check for mistakes.
const raw = (e.clipboardData || window.clipboardData)?.getData('text') ?? '';
if (!raw) {
return;
}

const ipRegex = /\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/;
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern for IP address validation is duplicated as a magic string. Consider extracting this to a named constant at the module level for better maintainability and potential reuse.

Copilot uses AI. Check for mistakes.
const match = raw.match(ipRegex);
const parts = match ? match[0] : null;
if (parts === null) {
return;
}

const cells = el.querySelectorAll(".ipv4-cell");
let pos = 0;
const args = [];
parts.split('.').forEach(p => {
if (pos > 3) {
return;
}
const num = parseInt(p, 10);
args.push(num);
cells[pos].value = num.toString();
ip.prevValues[pos] = cells[pos].value;
pos++;
});

invoke.invokeMethodAsync("TriggerUpdate", ...args);
});
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.ipaddress {
.bb-ip {
--bb-ip-cell-max-width: #{$bb-ip-cell-max-width};
display: flex;
flex-wrap: nowrap;
Expand Down
4 changes: 2 additions & 2 deletions src/BootstrapBlazor/Components/Table/Table.razor
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
}
else
{
<DynamicElement class="@GetRowClassString(item, "table-row")" @key="item"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Row key expression is treated as a literal string instead of being evaluated.

Because Key="GetKeyByITem(item)" lacks @, Razor treats it as the literal string "GetKeyByITem(item)", so all rows share the same key, breaking diffing and risking rendering issues. The same applies in the later RenderRow fragment. Please change these to Key="@GetKeyByITem(item)" (or an @ block) so the method is invoked, and consider renaming GetKeyByITemGetKeyByItem for clarity.

<DynamicElement class="@GetRowClassString(item, "table-row")" Key="GetKeyByITem(item)"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)"
@ontouchstart="e => OnTouchStart(e, item)"
@ontouchend="OnTouchEnd"
Expand Down Expand Up @@ -675,7 +675,7 @@
</thead>;

RenderFragment<TItem> RenderRow => item =>
@<DynamicElement TagName="tr" class="@GetRowClassString(item)" @key="item"
@<DynamicElement TagName="tr" class="@GetRowClassString(item)" Key="GetKeyByITem(item)"
TriggerContextMenu="ContextMenuZone != null" OnContextMenu="e => OnContextMenu(e, item)"
@ontouchstart="e => OnTouchStart(e, item)" @ontouchend="OnTouchEnd"
TriggerClick="@(ClickToSelect || OnClickRowCallback != null)" OnClick="() => ClickRow(item)"
Expand Down
8 changes: 8 additions & 0 deletions src/BootstrapBlazor/Components/Table/Table.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ private string GetSortTooltip(ITableColumn col) => SortName != col.GetFieldName(
[Parameter]
public bool ShowColumnWidthTooltip { get; set; }

/// <summary>
/// 获得/设置 行 Key 回调方法
/// </summary>
[Parameter]
public Func<TItem, object?>? OnGetRowKey { get; set; }

private string ScrollWidthString => $"width: {ActualScrollWidth}px;";

private string? GetScrollStyleString(bool condition) => condition
Expand Down Expand Up @@ -1667,6 +1673,8 @@ private void OnTouchEnd()
TouchStart = false;
}

private object? GetKeyByITem(TItem item) => OnGetRowKey?.Invoke(item);

/// <summary>
/// Dispose 方法
/// </summary>
Expand Down
17 changes: 16 additions & 1 deletion src/BootstrapBlazor/Components/TreeView/TreeView.razor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone
Expand Down Expand Up @@ -152,6 +152,12 @@ public partial class TreeView<TItem> : IModelEqualityComparer<TItem>
[Parameter]
public Func<TreeViewItem<TItem>, Task>? OnTreeItemClick { get; set; }

/// <summary>
/// 获得/设置 点击节点前回调方法
/// </summary>
[Parameter]
public Func<TreeViewItem<TItem>, Task<bool>>? OnBeforeTreeItemClick { get; set; }

/// <summary>
/// Gets or sets the callback method when a tree item is checked.
/// </summary>
Expand Down Expand Up @@ -545,6 +551,15 @@ private async Task<IEnumerable<IExpandableNode<TItem>>> GetChildrenRowAsync(Tree

private async Task OnClick(TreeViewItem<TItem> item)
{
if (OnBeforeTreeItemClick != null)
{
var ret = await OnBeforeTreeItemClick(item);
if (ret == false)
Copy link

Copilot AI Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == false' can be simplified to '!A'.

Suggested change
if (ret == false)
if (!ret)

Copilot uses AI. Check for mistakes.
{
return;
}
}

_activeItem = item;
if (ClickToggleNode && item.CanTriggerClickNode(IsDisabled, CanExpandWhenDisabled))
{
Expand Down
12 changes: 8 additions & 4 deletions src/BootstrapBlazor/Components/Upload/CardUpload.razor
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
{
<div @key="@item" class="@GetItemClassString(item)">
<div class="upload-item-body">
@if (IsImage(item))
@if (IconTemplate != null)
{
<img class="upload-item-body-image" alt="prevUrl" src="@item.PrevUrl" @onclick="() => OnClickZoom(item)" />
@IconTemplate(item)
}
else if (IconTemplate != null)
else if (IsImage(item))
{
@IconTemplate(item)
<img class="upload-item-body-image" alt="prevUrl" src="@item.PrevUrl" @onclick="() => OnClickZoom(item)" />
}
else
{
Expand All @@ -32,6 +32,10 @@
<div class="upload-item-size"><span>@item.GetFileName()</span> (@item.Size.ToFileSizeString())</div>
<div class="upload-item-actions">
<div class="btn-group">
@if (ActionButtonTemplate != null)
{
@ActionButtonTemplate(item)
}
@if (ShowZoomButton)
{
<button type="button" class="btn btn-sm btn-secondary btn-zoom" disabled="@GetDisabledString(item)" @onclick="() => OnClickZoom(item)" aria-label="zoom">
Expand Down
6 changes: 6 additions & 0 deletions src/BootstrapBlazor/Components/Upload/CardUpload.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ public partial class CardUpload<TValue>
[Parameter]
public RenderFragment<UploadFile>? IconTemplate { get; set; }

/// <summary>
/// 获得/设置 操作按钮模板
/// </summary>
[Parameter]
public RenderFragment<UploadFile>? ActionButtonTemplate { get; set; }

/// <summary>
/// 获得/设置 新建图标
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions src/BootstrapBlazor/Options/PdfOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the Apache 2.0 License
// See the LICENSE file in the project root for more information.
// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone

namespace BootstrapBlazor.Components;

/// <summary>
/// PdfOptions 实例用于设置导出 Pdf 相关选项
/// </summary>
public class PdfOptions
{
/// <summary>
/// 获得/设置 是否横向打印 默认 false
/// </summary>
public bool Landscape { get; set; }
}
26 changes: 4 additions & 22 deletions src/BootstrapBlazor/Services/DefaultHtml2PdfService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,11 @@ class DefaultHtml2PdfService : IHtml2Pdf
{
private const string ErrorMessage = "请增加依赖包 BootstrapBlazor.Html2Pdf 通过 AddBootstrapBlazorHtml2PdfService 进行服务注入; Please add BootstrapBlazor.Html2Pdf package and use AddBootstrapBlazorHtml2PdfService inject service";

/// <summary>
/// <inheritdoc/>
/// </summary>
public Task<byte[]> PdfDataAsync(string url) => throw new NotImplementedException(ErrorMessage);
public Task<byte[]> PdfDataAsync(string url, PdfOptions? options = null) => throw new NotImplementedException(ErrorMessage);

/// <summary>
/// <inheritdoc/>
/// </summary>
public Task<Stream> PdfStreamAsync(string url) => throw new NotImplementedException(ErrorMessage);
public Task<Stream> PdfStreamAsync(string url, PdfOptions? options = null) => throw new NotImplementedException(ErrorMessage);

/// <summary>
/// Export method
/// </summary>
/// <param name="html">html raw string</param>
/// <param name="links"></param>
/// <param name="scripts"></param>
public Task<byte[]> PdfDataFromHtmlAsync(string html, IEnumerable<string>? links = null, IEnumerable<string>? scripts = null) => throw new NotImplementedException(ErrorMessage);
public Task<byte[]> PdfDataFromHtmlAsync(string html, IEnumerable<string>? links = null, IEnumerable<string>? scripts = null, PdfOptions? options = null) => throw new NotImplementedException(ErrorMessage);

/// <summary>
/// Export method
/// </summary>
/// <param name="html">html raw string</param>
/// <param name="links"></param>
/// <param name="scripts"></param>
public Task<Stream> PdfStreamFromHtmlAsync(string html, IEnumerable<string>? links = null, IEnumerable<string>? scripts = null) => throw new NotImplementedException(ErrorMessage);
public Task<Stream> PdfStreamFromHtmlAsync(string html, IEnumerable<string>? links = null, IEnumerable<string>? scripts = null, PdfOptions? options = null) => throw new NotImplementedException(ErrorMessage);
}
Loading
Loading