diff --git a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs index 8785262..92d403a 100644 --- a/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs +++ b/src/SufiChain.SufiBlazor/Components/Forms/SbMarkdownEditor.razor.cs @@ -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? ToolbarItems { get; set; } [Parameter] public EventCallback 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 _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) { - await _interop.SetValueAsync(_editorId, Value); - _lastRenderedValue = Value; - _lastEditorMode = EditorMode; + if (EditorMode != _lastEditorMode || + SourceLanguage != _lastSourceLanguage || + 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 = _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) { diff --git a/src/SufiChain.SufiBlazor/Interop/SbMarkdownEditorInterop.cs b/src/SufiChain.SufiBlazor/Interop/SbMarkdownEditorInterop.cs index 3258c32..b8b54fc 100644 --- a/src/SufiChain.SufiBlazor/Interop/SbMarkdownEditorInterop.cs +++ b/src/SufiChain.SufiBlazor/Interop/SbMarkdownEditorInterop.cs @@ -88,7 +88,19 @@ public class SbMarkdownEditorInterop : IAsyncDisposable { 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 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; diff --git a/src/SufiChain.SufiBlazor/wwwroot/sufiblazor-markdown-editor.js b/src/SufiChain.SufiBlazor/wwwroot/sufiblazor-markdown-editor.js index a2e8ab3..b4a7465 100644 --- a/src/SufiChain.SufiBlazor/wwwroot/sufiblazor-markdown-editor.js +++ b/src/SufiChain.SufiBlazor/wwwroot/sufiblazor-markdown-editor.js @@ -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); } }