feat: add ToolbarScope parameter to editors for scoped toolbar contributions
- Introduce a new ToolbarScope parameter in SbMarkdownEditor, SbMarkEditor, and SbRichTextEditor to filter toolbar contributors based on context. - Update related services and interfaces to support scoped contributions, enhancing the flexibility of toolbar item management. - Add disposal handling in SbSelectOption to unregister options properly. - Implement CSS coverage fixes for various components to ensure consistent styling across the application.
This commit is contained in:
@@ -13,5 +13,6 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<NoWarn>$(NoWarn);CS8601;CS8602;CS8603;CS8604;CS8613;CS8618;CS8619;CS8620;CS8625;CS8629;CS8632;CS8669;CS8609;CS8767</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
SourceLanguage="@ResolveSourceLanguage()"
|
||||
UseToolbarContributors="@UseToolbarContributors"
|
||||
IncludeDefaultToolbarItems="@IncludeDefaultToolbarItems"
|
||||
ToolbarScope="@ToolbarScope"
|
||||
HideToolbar="@HideToolbar"
|
||||
ToolbarItems="@ToolbarItems"
|
||||
OnShortcut="@OnShortcut"
|
||||
|
||||
@@ -23,6 +23,11 @@ public partial class SbMarkEditor
|
||||
[Parameter] public string? SourceLanguage { get; set; }
|
||||
[Parameter] public bool UseToolbarContributors { get; set; }
|
||||
[Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Optional toolbar scope forwarded to <see cref="SbMarkdownEditor"/> to
|
||||
/// filter which registered <see cref="IMdToolbarContributor"/> instances run.
|
||||
/// </summary>
|
||||
[Parameter] public string? ToolbarScope { get; set; }
|
||||
[Parameter] public bool HideToolbar { get; set; }
|
||||
[Parameter] public IReadOnlyList<SbMarkdownToolbarItem>? ToolbarItems { get; set; }
|
||||
[Parameter] public EventCallback<string> OnShortcut { get; set; }
|
||||
|
||||
@@ -28,6 +28,14 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
[Parameter] public string? SourceLanguage { get; set; }
|
||||
[Parameter] public bool UseToolbarContributors { get; set; }
|
||||
[Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Optional toolbar scope that filters which <see cref="IMdToolbarContributor"/>
|
||||
/// instances run for this editor. Contributors whose <c>Scope</c> is non-null
|
||||
/// only execute when it matches this value. Contributors with a null scope
|
||||
/// always run. This prevents page-specific toolbar items from leaking across
|
||||
/// navigations within the same Blazor circuit.
|
||||
/// </summary>
|
||||
[Parameter] public string? ToolbarScope { get; set; }
|
||||
[Parameter] public bool HideToolbar { get; set; }
|
||||
[Parameter] public IReadOnlyList<SbMarkdownToolbarItem>? ToolbarItems { get; set; }
|
||||
[Parameter] public EventCallback<string> OnShortcut { get; set; }
|
||||
@@ -56,6 +64,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
private SbMarkdownEditorMode _lastEditorMode;
|
||||
private string? _lastSourceLanguage;
|
||||
private bool _lastIncludeDefaultToolbarItems;
|
||||
private string? _lastToolbarScope;
|
||||
private List<SbMarkdownToolbarItem> _toolbarItems = new();
|
||||
private bool _useFallback;
|
||||
private bool _disposed;
|
||||
@@ -88,6 +97,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
_lastEditorMode = EditorMode;
|
||||
_lastSourceLanguage = SourceLanguage;
|
||||
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
|
||||
_lastToolbarScope = ToolbarScope;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
@@ -115,11 +125,13 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
else if (Value != _lastRenderedValue ||
|
||||
EditorMode != _lastEditorMode ||
|
||||
SourceLanguage != _lastSourceLanguage ||
|
||||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
|
||||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems ||
|
||||
ToolbarScope != _lastToolbarScope)
|
||||
{
|
||||
if (EditorMode != _lastEditorMode ||
|
||||
SourceLanguage != _lastSourceLanguage ||
|
||||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
|
||||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems ||
|
||||
ToolbarScope != _lastToolbarScope)
|
||||
{
|
||||
await ReinitializeEditorAsync();
|
||||
}
|
||||
@@ -145,7 +157,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
_toolbarItems = await ToolbarService.GetToolbarItemsAsync(
|
||||
editorId,
|
||||
includeDefaults: IncludeDefaultToolbarItems,
|
||||
includeContributors: true);
|
||||
includeContributors: true,
|
||||
scope: ToolbarScope);
|
||||
}
|
||||
else if (ToolbarItems != null)
|
||||
{
|
||||
@@ -261,6 +274,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
|
||||
_lastEditorMode = EditorMode;
|
||||
_lastSourceLanguage = SourceLanguage;
|
||||
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
|
||||
_lastToolbarScope = ToolbarScope;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
|
||||
@@ -382,6 +382,16 @@
|
||||
[Parameter]
|
||||
public bool IncludeDefaultToolbarItems { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional toolbar scope that filters which <see cref="IRteToolbarContributor"/>
|
||||
/// instances run for this editor. Contributors whose <c>Scope</c> is non-null
|
||||
/// only execute when it matches this value. Contributors with a null scope
|
||||
/// always run. This prevents page-specific toolbar items from leaking across
|
||||
/// navigations within the same Blazor circuit.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? ToolbarScope { get; set; }
|
||||
|
||||
#region Localization Parameters
|
||||
|
||||
/// <summary>
|
||||
@@ -568,10 +578,11 @@
|
||||
// Get contributed items
|
||||
_contributedToolbarItems = await _toolbarService.GetToolbarItemsAsync(
|
||||
_editorId,
|
||||
IncludeDefaultToolbarItems);
|
||||
IncludeDefaultToolbarItems,
|
||||
ToolbarScope);
|
||||
|
||||
// Build a map of contributed items for click handling
|
||||
var contributedItems = await _toolbarService.GetContributedItemsAsync(_editorId);
|
||||
var contributedItems = await _toolbarService.GetContributedItemsAsync(_editorId, ToolbarScope);
|
||||
_contributedItemsMap = contributedItems
|
||||
.Where(item => item.OnClickAsync != null)
|
||||
.ToDictionary(item => item.Id, item => item);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@namespace SufiChain.SufiBlazor.Components.Forms
|
||||
@typeparam TValue
|
||||
@implements IDisposable
|
||||
|
||||
@* This component is used as a child of SbSelect to define options declaratively. *@
|
||||
@* It registers itself with the parent SbSelect via cascading parameter. *@
|
||||
@@ -54,4 +55,9 @@
|
||||
Disabled = Disabled
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Context?.UnregisterOption(Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,15 +220,36 @@
|
||||
|
||||
void ISbSelectOptionContext<TValue>.RegisterOption(SbSelectOptionInfo<TValue> option)
|
||||
{
|
||||
if (!_options.Any(o => EqualityComparer<TValue>.Default.Equals(o.Value, option.Value)))
|
||||
// Update existing entry in place so Text/ChildContent changes propagate;
|
||||
// otherwise add. We must NOT clear _options here or in OnParametersSet,
|
||||
// because Blazor skips calling OnParametersSet on SbSelectOption children
|
||||
// whose parameters haven't changed, which would leave _options empty and
|
||||
// cause GetDisplayText to fall back to Value.ToString() (e.g. a Guid).
|
||||
var existingIndex = _options.FindIndex(o =>
|
||||
EqualityComparer<TValue>.Default.Equals(o.Value, option.Value));
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
_options[existingIndex] = option;
|
||||
}
|
||||
else
|
||||
{
|
||||
_options.Add(option);
|
||||
}
|
||||
}
|
||||
|
||||
void ISbSelectOptionContext<TValue>.UnregisterOption(TValue? value)
|
||||
{
|
||||
var index = _options.FindIndex(o =>
|
||||
EqualityComparer<TValue>.Default.Equals(o.Value, value));
|
||||
if (index >= 0)
|
||||
{
|
||||
_options.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
_options.Clear();
|
||||
// Intentionally do NOT clear _options here. See RegisterOption note above.
|
||||
}
|
||||
|
||||
private IEnumerable<SbSelectOptionInfo<TValue>> FilteredOptions
|
||||
|
||||
@@ -15,6 +15,15 @@ public interface IMdToolbarContributor
|
||||
/// </summary>
|
||||
int Order => 100;
|
||||
|
||||
/// <summary>
|
||||
/// The toolbar scope this contributor belongs to. When non-null, the
|
||||
/// contributor only runs on editors whose <c>ToolbarScope</c> parameter
|
||||
/// matches this value. When null (default), the contributor runs on every
|
||||
/// editor instance — use this for self-filtering contributors (e.g. ones
|
||||
/// that check a host registration) or for globally-applicable items.
|
||||
/// </summary>
|
||||
string? Scope => null;
|
||||
|
||||
/// <summary>
|
||||
/// Configure the toolbar by adding custom items to the context.
|
||||
/// </summary>
|
||||
@@ -30,6 +39,13 @@ public class MdToolbarContext
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
public string? EditorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scope declared by the editor instance via its <c>ToolbarScope</c>
|
||||
/// parameter. Contributors whose <see cref="IMdToolbarContributor.Scope"/>
|
||||
/// is non-null only run when this value matches.
|
||||
/// </summary>
|
||||
public string? Scope { get; set; }
|
||||
|
||||
public MdToolbarContext(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
|
||||
@@ -16,6 +16,15 @@ public interface IRteToolbarContributor
|
||||
/// </summary>
|
||||
int Order => 100;
|
||||
|
||||
/// <summary>
|
||||
/// The toolbar scope this contributor belongs to. When non-null, the
|
||||
/// contributor only runs on editors whose <c>ToolbarScope</c> parameter
|
||||
/// matches this value. When null (default), the contributor runs on every
|
||||
/// editor instance — use this for self-filtering contributors or for
|
||||
/// globally-applicable items (e.g. culture-aware font selectors).
|
||||
/// </summary>
|
||||
string? Scope => null;
|
||||
|
||||
/// <summary>
|
||||
/// Configure the toolbar by adding custom items to the context.
|
||||
/// </summary>
|
||||
@@ -43,6 +52,13 @@ public class RteToolbarContext
|
||||
/// </summary>
|
||||
public string? EditorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The scope declared by the editor instance via its <c>ToolbarScope</c>
|
||||
/// parameter. Contributors whose <see cref="IRteToolbarContributor.Scope"/>
|
||||
/// is non-null only run when this value matches.
|
||||
/// </summary>
|
||||
public string? Scope { get; set; }
|
||||
|
||||
public RteToolbarContext(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
|
||||
@@ -24,7 +24,8 @@ public class MdToolbarService : IMdToolbarService
|
||||
public async Task<List<SbMarkdownToolbarItem>> GetToolbarItemsAsync(
|
||||
string? editorId = null,
|
||||
bool? includeDefaults = null,
|
||||
bool includeContributors = true)
|
||||
bool includeContributors = true,
|
||||
string? scope = null)
|
||||
{
|
||||
var shouldIncludeDefaults = includeDefaults ?? _options.IncludeDefaultItems;
|
||||
var allItems = new List<MdToolbarContributedItem>();
|
||||
@@ -36,8 +37,12 @@ public class MdToolbarService : IMdToolbarService
|
||||
|
||||
if (includeContributors)
|
||||
{
|
||||
var context = new MdToolbarContext(_serviceProvider) { EditorId = editorId };
|
||||
foreach (var contributor in GetContributors().OrderBy(c => c.Order))
|
||||
var context = new MdToolbarContext(_serviceProvider)
|
||||
{
|
||||
EditorId = editorId,
|
||||
Scope = scope
|
||||
};
|
||||
foreach (var contributor in GetContributors(scope).OrderBy(c => c.Order))
|
||||
{
|
||||
await contributor.ConfigureToolbarAsync(context);
|
||||
}
|
||||
@@ -80,11 +85,12 @@ public class MdToolbarService : IMdToolbarService
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IMdToolbarContributor> GetContributors()
|
||||
private IEnumerable<IMdToolbarContributor> GetContributors(string? scope)
|
||||
{
|
||||
foreach (var contributorType in _options.Contributors)
|
||||
{
|
||||
if (_serviceProvider.GetService(contributorType) is IMdToolbarContributor contributor)
|
||||
if (_serviceProvider.GetService(contributorType) is IMdToolbarContributor contributor
|
||||
&& (contributor.Scope == null || string.Equals(contributor.Scope, scope, StringComparison.Ordinal)))
|
||||
{
|
||||
yield return contributor;
|
||||
}
|
||||
@@ -122,6 +128,7 @@ public interface IMdToolbarService
|
||||
Task<List<SbMarkdownToolbarItem>> GetToolbarItemsAsync(
|
||||
string? editorId = null,
|
||||
bool? includeDefaults = null,
|
||||
bool includeContributors = true);
|
||||
bool includeContributors = true,
|
||||
string? scope = null);
|
||||
Task ExecuteItemActionAsync(MdToolbarContributedItem item, MdToolbarActionContext actionContext);
|
||||
}
|
||||
|
||||
@@ -28,9 +28,11 @@ public class RteToolbarService : IRteToolbarService
|
||||
/// </summary>
|
||||
/// <param name="editorId">The editor instance ID.</param>
|
||||
/// <param name="includeDefaults">Whether to include default toolbar items.</param>
|
||||
/// <param name="scope">Optional toolbar scope that filters which contributors run.</param>
|
||||
public async Task<List<SbEditorToolbarItem>> GetToolbarItemsAsync(
|
||||
string? editorId = null,
|
||||
bool? includeDefaults = null)
|
||||
bool? includeDefaults = null,
|
||||
string? scope = null)
|
||||
{
|
||||
var shouldIncludeDefaults = includeDefaults ?? _options.IncludeDefaultItems;
|
||||
var allItems = new List<RteToolbarContributedItem>();
|
||||
@@ -42,9 +44,13 @@ public class RteToolbarService : IRteToolbarService
|
||||
}
|
||||
|
||||
// Get contributed items from all registered contributors
|
||||
var context = new RteToolbarContext(_serviceProvider) { EditorId = editorId };
|
||||
var context = new RteToolbarContext(_serviceProvider)
|
||||
{
|
||||
EditorId = editorId,
|
||||
Scope = scope
|
||||
};
|
||||
|
||||
var contributors = GetContributors();
|
||||
var contributors = GetContributors(scope);
|
||||
foreach (var contributor in contributors.OrderBy(c => c.Order))
|
||||
{
|
||||
await contributor.ConfigureToolbarAsync(context);
|
||||
@@ -84,11 +90,17 @@ public class RteToolbarService : IRteToolbarService
|
||||
/// <summary>
|
||||
/// Get the contributed items only (without defaults).
|
||||
/// </summary>
|
||||
public async Task<List<RteToolbarContributedItem>> GetContributedItemsAsync(string? editorId = null)
|
||||
public async Task<List<RteToolbarContributedItem>> GetContributedItemsAsync(
|
||||
string? editorId = null,
|
||||
string? scope = null)
|
||||
{
|
||||
var context = new RteToolbarContext(_serviceProvider) { EditorId = editorId };
|
||||
var context = new RteToolbarContext(_serviceProvider)
|
||||
{
|
||||
EditorId = editorId,
|
||||
Scope = scope
|
||||
};
|
||||
|
||||
var contributors = GetContributors();
|
||||
var contributors = GetContributors(scope);
|
||||
foreach (var contributor in contributors.OrderBy(c => c.Order))
|
||||
{
|
||||
await contributor.ConfigureToolbarAsync(context);
|
||||
@@ -110,12 +122,13 @@ public class RteToolbarService : IRteToolbarService
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IRteToolbarContributor> GetContributors()
|
||||
private IEnumerable<IRteToolbarContributor> GetContributors(string? scope)
|
||||
{
|
||||
foreach (var contributorType in _options.Contributors)
|
||||
{
|
||||
var contributor = _serviceProvider.GetService(contributorType) as IRteToolbarContributor;
|
||||
if (contributor != null)
|
||||
if (contributor != null
|
||||
&& (contributor.Scope == null || string.Equals(contributor.Scope, scope, StringComparison.Ordinal)))
|
||||
{
|
||||
yield return contributor;
|
||||
}
|
||||
@@ -201,12 +214,17 @@ public interface IRteToolbarService
|
||||
/// <summary>
|
||||
/// Get all toolbar items including default and contributed items.
|
||||
/// </summary>
|
||||
Task<List<SbEditorToolbarItem>> GetToolbarItemsAsync(string? editorId = null, bool? includeDefaults = null);
|
||||
Task<List<SbEditorToolbarItem>> GetToolbarItemsAsync(
|
||||
string? editorId = null,
|
||||
bool? includeDefaults = null,
|
||||
string? scope = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get the contributed items only (without defaults).
|
||||
/// </summary>
|
||||
Task<List<RteToolbarContributedItem>> GetContributedItemsAsync(string? editorId = null);
|
||||
Task<List<RteToolbarContributedItem>> GetContributedItemsAsync(
|
||||
string? editorId = null,
|
||||
string? scope = null);
|
||||
|
||||
/// <summary>
|
||||
/// Execute a contributed toolbar item's click handler.
|
||||
|
||||
@@ -13,6 +13,12 @@ internal interface ISbSelectOptionContext<TValue>
|
||||
/// </summary>
|
||||
/// <param name="option">The option information to register.</param>
|
||||
void RegisterOption(SbSelectOptionInfo<TValue> option);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered option (e.g. when its SbSelectOption is disposed).
|
||||
/// </summary>
|
||||
/// <param name="value">The value of the option to unregister.</param>
|
||||
void UnregisterOption(TValue? value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<!-- Generate XML documentation -->
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user