feat: enhance SbMarkdownEditor with source language support and toolbar item loading

- Add SourceLanguage and IncludeDefaultToolbarItems parameters to SbMarkdownEditor
- Refactor toolbar item loading logic into a separate method for better readability
- Update JavaScript interop to handle source language in editor initialization
- Improve error handling in destroyEditor method to prevent crashes on disconnection
This commit is contained in:
2026-06-22 18:58:16 +03:30
parent f9ba4f2980
commit c4403527ad
3 changed files with 110 additions and 22 deletions
@@ -25,7 +25,9 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
[Parameter] public bool EnableHighlight { get; set; } = true;
[Parameter] public string HighlightTheme { get; set; } = "github";
[Parameter] public SbMarkdownEditorMode EditorMode { get; set; } = SbMarkdownEditorMode.Markdown;
[Parameter] public string? SourceLanguage { get; set; }
[Parameter] public bool UseToolbarContributors { get; set; }
[Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true;
[Parameter] public bool HideToolbar { get; set; }
[Parameter] public IReadOnlyList<SbMarkdownToolbarItem>? ToolbarItems { get; set; }
[Parameter] public EventCallback<string> OnShortcut { get; set; }
@@ -52,6 +54,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
private string? _lastRenderedSuggested;
private bool _lastRenderedDiffMode;
private SbMarkdownEditorMode _lastEditorMode;
private string? _lastSourceLanguage;
private bool _lastIncludeDefaultToolbarItems;
private List<SbMarkdownToolbarItem> _toolbarItems = new();
private bool _useFallback;
@@ -64,18 +68,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_interop = new SbMarkdownEditorInterop(JSRuntime);
_dotNetRef = DotNetObjectReference.Create(this);
if (!IsDiffReview && UseToolbarContributors)
{
_toolbarItems = await ToolbarService.GetToolbarItemsAsync(_editorId, includeContributors: true);
}
else if (!IsDiffReview && ToolbarItems != null)
{
_toolbarItems = ToolbarItems.ToList();
}
else if (!IsDiffReview)
{
_toolbarItems = await ToolbarService.GetToolbarItemsAsync(_editorId, includeDefaults: true, includeContributors: false);
}
await LoadToolbarItemsAsync();
await InitializeEditorAsync();
_lastRenderedValue = Value;
@@ -83,6 +76,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_lastRenderedSuggested = SuggestedValue;
_lastRenderedDiffMode = IsDiffReview;
_lastEditorMode = EditorMode;
_lastSourceLanguage = SourceLanguage;
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
await InvokeAsync(StateHasChanged);
return;
}
@@ -107,11 +102,48 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_lastRenderedSuggested = SuggestedValue;
}
}
else if (Value != _lastRenderedValue || EditorMode != _lastEditorMode)
else if (Value != _lastRenderedValue ||
EditorMode != _lastEditorMode ||
SourceLanguage != _lastSourceLanguage ||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
{
if (EditorMode != _lastEditorMode ||
SourceLanguage != _lastSourceLanguage ||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
{
await ReinitializeEditorAsync();
}
else
{
await _interop.SetValueAsync(_editorId, Value);
_lastRenderedValue = Value;
_lastEditorMode = EditorMode;
}
}
}
private async Task LoadToolbarItemsAsync()
{
if (IsDiffReview)
{
return;
}
var editorId = GetEditorId();
if (UseToolbarContributors)
{
_toolbarItems = await ToolbarService.GetToolbarItemsAsync(
editorId,
includeDefaults: IncludeDefaultToolbarItems,
includeContributors: true);
}
else if (ToolbarItems != null)
{
_toolbarItems = ToolbarItems.ToList();
}
else
{
_toolbarItems = await ToolbarService.GetToolbarItemsAsync(editorId, includeDefaults: true, includeContributors: false);
}
}
@@ -141,12 +173,13 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_editorId = await _interop.InitEditorAsync(_textarea, _dotNetRef!, new SbMarkdownEditorInitOptions
{
EditorId = _elementId,
EditorId = GetEditorId(),
Value = Value,
Placeholder = Placeholder,
ReadOnly = ReadOnly,
Direction = RightToLeft ? "rtl" : "ltr",
EditorMode = EditorMode == SbMarkdownEditorMode.Source ? "source" : "markdown",
SourceLanguage = SourceLanguage,
EnablePreview = EnablePreview && EditorMode != SbMarkdownEditorMode.Source,
EnableMermaid = EnableMermaid,
EnableHighlight = EnableHighlight,
@@ -177,14 +210,23 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
IsReady = false;
_useFallback = false;
await InvokeAsync(StateHasChanged);
await LoadToolbarItemsAsync();
await InitializeEditorAsync();
_lastRenderedDiffMode = IsDiffReview;
_lastRenderedValue = Value;
_lastRenderedOriginal = OriginalValue;
_lastRenderedSuggested = SuggestedValue;
_lastEditorMode = EditorMode;
_lastSourceLanguage = SourceLanguage;
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
await InvokeAsync(StateHasChanged);
}
private string GetEditorId() =>
string.Equals(SourceLanguage, "html", StringComparison.OrdinalIgnoreCase)
? $"{_elementId}-html"
: _elementId;
[JSInvokable]
public async Task OnEditorChangeAsync(string value, string html)
{
@@ -87,9 +87,21 @@ public class SbMarkdownEditorInterop : IAsyncDisposable
public async Task DestroyEditorAsync(string editorId)
{
if (_module != null)
{
try
{
await _module.InvokeVoidAsync("destroyEditor", editorId);
}
catch (JSDisconnectedException)
{
}
catch (JSException)
{
}
catch (ObjectDisposedException)
{
}
}
}
public async Task<string> RenderMarkdownAsync(string content, SbMarkdownAssetOptions? options = null)
@@ -135,6 +147,7 @@ public class SbMarkdownEditorInitOptions
public bool ReadOnly { get; set; }
public string? Direction { get; set; }
public string EditorMode { get; set; } = "markdown";
public string? SourceLanguage { get; set; }
public bool EnablePreview { get; set; } = true;
public bool EnableMermaid { get; set; } = true;
public bool EnableHighlight { get; set; } = true;
@@ -11,6 +11,7 @@ let easyMdeLoadPromise = null;
let markedLoadPromise = null;
let assetsLoadPromise = null;
let diffAssetsLoadPromise = null;
let codeAssetsLoadPromise = null;
function getContentBasePath() {
const scripts = document.querySelectorAll('script[src*="sufiblazor-markdown-editor.js"]');
@@ -131,6 +132,27 @@ async function ensureDiffAssets() {
return diffAssetsLoadPromise;
}
async function ensureCodeAssets() {
if (typeof CodeMirror !== 'undefined' && CodeMirror.modes?.htmlmixed) {
return true;
}
if (codeAssetsLoadPromise) {
return codeAssetsLoadPromise;
}
codeAssetsLoadPromise = (async () => {
const base = getContentBasePath();
loadCss(`${base}/vendor/codemirror/codemirror.css`);
await loadScript(`${base}/vendor/codemirror/codemirror.js`);
await loadScript(`${base}/vendor/codemirror/xml.js`);
await loadScript(`${base}/vendor/codemirror/javascript.js`);
await loadScript(`${base}/vendor/codemirror/css.js`);
await loadScript(`${base}/vendor/codemirror/htmlmixed.js`);
await loadScript(`${base}/vendor/codemirror/markdown.js`);
return typeof CodeMirror !== 'undefined';
})();
return codeAssetsLoadPromise;
}
function configureMarked() {
if (!marked || window.__sbMarkedConfigured) {
return;
@@ -268,7 +290,9 @@ export async function initEditor(textarea, dotNetRef, options) {
return null;
}
if (!sourceMode) {
if (sourceMode) {
await ensureCodeAssets();
} else {
await ensureAssets({
enableMermaid: options.enableMermaid,
enableHighlight: options.enableHighlight,
@@ -307,6 +331,12 @@ export async function initEditor(textarea, dotNetRef, options) {
const easyMDE = new EasyMDE(easyOptions);
if (sourceMode) {
const language = (options.sourceLanguage || '').toLowerCase();
easyMDE.codemirror.setOption('mode', language === 'html' ? 'htmlmixed' : 'markdown');
easyMDE.codemirror.setOption('htmlMode', language === 'html');
}
easyMDE.codemirror.on('change', () => {
const value = easyMDE.value();
let html = '';
@@ -461,14 +491,17 @@ export function setPreview(editorId, show) {
export function destroyEditor(editorId) {
const diff = diffEditors.get(editorId);
if (diff) {
diff.mergeView.wrapper.parentNode?.removeChild(diff.mergeView.wrapper);
const wrapper = diff.mergeView?.wrapper;
if (wrapper?.parentNode) {
wrapper.parentNode.removeChild(wrapper);
}
diffEditors.delete(editorId);
return;
}
const stored = editors.get(editorId);
if (stored) {
stored.easyMDE.toTextArea();
stored.easyMDE.clearAutosavedValue?.();
stored.easyMDE?.toTextArea?.();
stored.easyMDE?.clearAutosavedValue?.();
editors.delete(editorId);
}
}