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 bool EnableHighlight { get; set; } = true;
[Parameter] public string HighlightTheme { get; set; } = "github"; [Parameter] public string HighlightTheme { get; set; } = "github";
[Parameter] public SbMarkdownEditorMode EditorMode { get; set; } = SbMarkdownEditorMode.Markdown; [Parameter] public SbMarkdownEditorMode EditorMode { get; set; } = SbMarkdownEditorMode.Markdown;
[Parameter] public string? SourceLanguage { get; set; }
[Parameter] public bool UseToolbarContributors { get; set; } [Parameter] public bool UseToolbarContributors { get; set; }
[Parameter] public bool IncludeDefaultToolbarItems { get; set; } = true;
[Parameter] public bool HideToolbar { get; set; } [Parameter] public bool HideToolbar { get; set; }
[Parameter] public IReadOnlyList<SbMarkdownToolbarItem>? ToolbarItems { get; set; } [Parameter] public IReadOnlyList<SbMarkdownToolbarItem>? ToolbarItems { get; set; }
[Parameter] public EventCallback<string> OnShortcut { get; set; } [Parameter] public EventCallback<string> OnShortcut { get; set; }
@@ -52,6 +54,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
private string? _lastRenderedSuggested; private string? _lastRenderedSuggested;
private bool _lastRenderedDiffMode; private bool _lastRenderedDiffMode;
private SbMarkdownEditorMode _lastEditorMode; private SbMarkdownEditorMode _lastEditorMode;
private string? _lastSourceLanguage;
private bool _lastIncludeDefaultToolbarItems;
private List<SbMarkdownToolbarItem> _toolbarItems = new(); private List<SbMarkdownToolbarItem> _toolbarItems = new();
private bool _useFallback; private bool _useFallback;
@@ -64,18 +68,7 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_interop = new SbMarkdownEditorInterop(JSRuntime); _interop = new SbMarkdownEditorInterop(JSRuntime);
_dotNetRef = DotNetObjectReference.Create(this); _dotNetRef = DotNetObjectReference.Create(this);
if (!IsDiffReview && UseToolbarContributors) await LoadToolbarItemsAsync();
{
_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 InitializeEditorAsync(); await InitializeEditorAsync();
_lastRenderedValue = Value; _lastRenderedValue = Value;
@@ -83,6 +76,8 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_lastRenderedSuggested = SuggestedValue; _lastRenderedSuggested = SuggestedValue;
_lastRenderedDiffMode = IsDiffReview; _lastRenderedDiffMode = IsDiffReview;
_lastEditorMode = EditorMode; _lastEditorMode = EditorMode;
_lastSourceLanguage = SourceLanguage;
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
return; return;
} }
@@ -107,11 +102,48 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
_lastRenderedSuggested = SuggestedValue; _lastRenderedSuggested = SuggestedValue;
} }
} }
else if (Value != _lastRenderedValue || EditorMode != _lastEditorMode) else if (Value != _lastRenderedValue ||
EditorMode != _lastEditorMode ||
SourceLanguage != _lastSourceLanguage ||
IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
{ {
await _interop.SetValueAsync(_editorId, Value); if (EditorMode != _lastEditorMode ||
_lastRenderedValue = Value; SourceLanguage != _lastSourceLanguage ||
_lastEditorMode = EditorMode; IncludeDefaultToolbarItems != _lastIncludeDefaultToolbarItems)
{
await ReinitializeEditorAsync();
}
else
{
await _interop.SetValueAsync(_editorId, Value);
_lastRenderedValue = Value;
}
}
}
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 = await _interop.InitEditorAsync(_textarea, _dotNetRef!, new SbMarkdownEditorInitOptions
{ {
EditorId = _elementId, EditorId = GetEditorId(),
Value = Value, Value = Value,
Placeholder = Placeholder, Placeholder = Placeholder,
ReadOnly = ReadOnly, ReadOnly = ReadOnly,
Direction = RightToLeft ? "rtl" : "ltr", Direction = RightToLeft ? "rtl" : "ltr",
EditorMode = EditorMode == SbMarkdownEditorMode.Source ? "source" : "markdown", EditorMode = EditorMode == SbMarkdownEditorMode.Source ? "source" : "markdown",
SourceLanguage = SourceLanguage,
EnablePreview = EnablePreview && EditorMode != SbMarkdownEditorMode.Source, EnablePreview = EnablePreview && EditorMode != SbMarkdownEditorMode.Source,
EnableMermaid = EnableMermaid, EnableMermaid = EnableMermaid,
EnableHighlight = EnableHighlight, EnableHighlight = EnableHighlight,
@@ -177,14 +210,23 @@ public partial class SbMarkdownEditor : ComponentBase, IAsyncDisposable
IsReady = false; IsReady = false;
_useFallback = false; _useFallback = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await LoadToolbarItemsAsync();
await InitializeEditorAsync(); await InitializeEditorAsync();
_lastRenderedDiffMode = IsDiffReview; _lastRenderedDiffMode = IsDiffReview;
_lastRenderedValue = Value; _lastRenderedValue = Value;
_lastRenderedOriginal = OriginalValue; _lastRenderedOriginal = OriginalValue;
_lastRenderedSuggested = SuggestedValue; _lastRenderedSuggested = SuggestedValue;
_lastEditorMode = EditorMode;
_lastSourceLanguage = SourceLanguage;
_lastIncludeDefaultToolbarItems = IncludeDefaultToolbarItems;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private string GetEditorId() =>
string.Equals(SourceLanguage, "html", StringComparison.OrdinalIgnoreCase)
? $"{_elementId}-html"
: _elementId;
[JSInvokable] [JSInvokable]
public async Task OnEditorChangeAsync(string value, string html) public async Task OnEditorChangeAsync(string value, string html)
{ {
@@ -88,7 +88,19 @@ public class SbMarkdownEditorInterop : IAsyncDisposable
{ {
if (_module != null) if (_module != null)
{ {
await _module.InvokeVoidAsync("destroyEditor", editorId); try
{
await _module.InvokeVoidAsync("destroyEditor", editorId);
}
catch (JSDisconnectedException)
{
}
catch (JSException)
{
}
catch (ObjectDisposedException)
{
}
} }
} }
@@ -135,6 +147,7 @@ public class SbMarkdownEditorInitOptions
public bool ReadOnly { get; set; } public bool ReadOnly { get; set; }
public string? Direction { get; set; } public string? Direction { get; set; }
public string EditorMode { get; set; } = "markdown"; public string EditorMode { get; set; } = "markdown";
public string? SourceLanguage { get; set; }
public bool EnablePreview { get; set; } = true; public bool EnablePreview { get; set; } = true;
public bool EnableMermaid { get; set; } = true; public bool EnableMermaid { get; set; } = true;
public bool EnableHighlight { get; set; } = true; public bool EnableHighlight { get; set; } = true;
@@ -11,6 +11,7 @@ let easyMdeLoadPromise = null;
let markedLoadPromise = null; let markedLoadPromise = null;
let assetsLoadPromise = null; let assetsLoadPromise = null;
let diffAssetsLoadPromise = null; let diffAssetsLoadPromise = null;
let codeAssetsLoadPromise = null;
function getContentBasePath() { function getContentBasePath() {
const scripts = document.querySelectorAll('script[src*="sufiblazor-markdown-editor.js"]'); const scripts = document.querySelectorAll('script[src*="sufiblazor-markdown-editor.js"]');
@@ -131,6 +132,27 @@ async function ensureDiffAssets() {
return diffAssetsLoadPromise; 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() { function configureMarked() {
if (!marked || window.__sbMarkedConfigured) { if (!marked || window.__sbMarkedConfigured) {
return; return;
@@ -268,7 +290,9 @@ export async function initEditor(textarea, dotNetRef, options) {
return null; return null;
} }
if (!sourceMode) { if (sourceMode) {
await ensureCodeAssets();
} else {
await ensureAssets({ await ensureAssets({
enableMermaid: options.enableMermaid, enableMermaid: options.enableMermaid,
enableHighlight: options.enableHighlight, enableHighlight: options.enableHighlight,
@@ -307,6 +331,12 @@ export async function initEditor(textarea, dotNetRef, options) {
const easyMDE = new EasyMDE(easyOptions); 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', () => { easyMDE.codemirror.on('change', () => {
const value = easyMDE.value(); const value = easyMDE.value();
let html = ''; let html = '';
@@ -461,14 +491,17 @@ export function setPreview(editorId, show) {
export function destroyEditor(editorId) { export function destroyEditor(editorId) {
const diff = diffEditors.get(editorId); const diff = diffEditors.get(editorId);
if (diff) { 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); diffEditors.delete(editorId);
return; return;
} }
const stored = editors.get(editorId); const stored = editors.get(editorId);
if (stored) { if (stored) {
stored.easyMDE.toTextArea(); stored.easyMDE?.toTextArea?.();
stored.easyMDE.clearAutosavedValue?.(); stored.easyMDE?.clearAutosavedValue?.();
editors.delete(editorId); editors.delete(editorId);
} }
} }