feat(sufi-blazor): introduce sb mark editor unified editing surface

This commit is contained in:
2026-06-27 16:36:22 +03:30
parent 6406d77732
commit 239b1780fc
5 changed files with 172 additions and 1 deletions
@@ -0,0 +1,34 @@
@namespace SufiChain.SufiBlazor.Components.Forms
@using SufiChain.SufiBlazor.Contracts.Editors
<SbMarkdownEditor Value="@Value"
ValueChanged="@ValueChanged"
ValueHtml="@ValueHtml"
ValueHtmlChanged="@ValueHtmlChanged"
ReadOnly="@ReadOnly"
Disabled="@Disabled"
Placeholder="@Placeholder"
Class="@Class"
Style="@Style"
RightToLeft="@RightToLeft"
EnablePreview="@EnablePreview"
EnableMermaid="@EnableMermaid"
EnableHighlight="@EnableHighlight"
HighlightTheme="@HighlightTheme"
EditorMode="@ResolveEditorMode()"
SourceLanguage="@ResolveSourceLanguage()"
UseToolbarContributors="@UseToolbarContributors"
IncludeDefaultToolbarItems="@IncludeDefaultToolbarItems"
HideToolbar="@HideToolbar"
ToolbarItems="@ToolbarItems"
OnShortcut="@OnShortcut"
MinHeight="@MinHeight"
MaxHeight="@MaxHeight"
FallbackRows="@FallbackRows"
IsDiffReview="@IsDiffReview"
OriginalValue="@OriginalValue"
SuggestedValue="@SuggestedValue"
SuggestedValueChanged="@SuggestedValueChanged"
OnApplyChanges="@OnApplyChanges"
OnDiscardChanges="@OnDiscardChanges"
AdditionalAttributes="@AdditionalAttributes" />
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Components;
using SufiChain.SufiBlazor.Contracts.Editors;
namespace SufiChain.SufiBlazor.Components.Forms;
public partial class SbMarkEditor
{
[Parameter] public string Value { get; set; } = string.Empty;
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public string? ValueHtml { get; set; }
[Parameter] public EventCallback<string?> ValueHtmlChanged { get; set; }
[Parameter] public bool ReadOnly { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter] public string? Placeholder { get; set; }
[Parameter] public string? Class { get; set; }
[Parameter] public string? Style { get; set; }
[Parameter] public bool RightToLeft { get; set; }
[Parameter] public bool EnablePreview { get; set; } = true;
[Parameter] public bool EnableMermaid { get; set; } = true;
[Parameter] public bool EnableHighlight { get; set; } = true;
[Parameter] public string HighlightTheme { get; set; } = "github";
[Parameter] public SbMarkEditorMode Mode { get; set; } = SbMarkEditorMode.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; }
[Parameter] public string? MinHeight { get; set; } = "200px";
[Parameter] public string? MaxHeight { get; set; }
[Parameter] public int FallbackRows { get; set; } = 12;
[Parameter] public bool IsDiffReview { get; set; }
[Parameter] public string OriginalValue { get; set; } = string.Empty;
[Parameter] public string SuggestedValue { get; set; } = string.Empty;
[Parameter] public EventCallback<string> SuggestedValueChanged { get; set; }
[Parameter] public EventCallback OnApplyChanges { get; set; }
[Parameter] public EventCallback OnDiscardChanges { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
private SbMarkdownEditorMode ResolveEditorMode()
{
return Mode switch
{
SbMarkEditorMode.Source => SbMarkdownEditorMode.Source,
SbMarkEditorMode.Markup => SbMarkdownEditorMode.Source,
_ => SbMarkdownEditorMode.Markdown
};
}
private string? ResolveSourceLanguage()
{
if (!string.IsNullOrWhiteSpace(SourceLanguage))
{
return SourceLanguage;
}
return Mode switch
{
SbMarkEditorMode.Markup => "html",
SbMarkEditorMode.Source => "html",
_ => null
};
}
}
@@ -0,0 +1,8 @@
namespace SufiChain.SufiBlazor.Contracts.Editors;
public enum SbMarkEditorMode
{
Markdown,
Markup,
Source
}
@@ -323,6 +323,7 @@ export async function initEditor(textarea, dotNetRef, options) {
easyOptions.preview = false; easyOptions.preview = false;
easyOptions.sideBySideFullscreen = false; easyOptions.sideBySideFullscreen = false;
} else { } else {
easyOptions.sideBySideFullscreen = false;
easyOptions.previewRender = buildPreviewRender(options); easyOptions.previewRender = buildPreviewRender(options);
easyOptions.renderingConfig.markedOptions = { easyOptions.renderingConfig.markedOptions = {
highlight: buildHighlightFn(options.enableHighlight, options.enableMermaid) highlight: buildHighlightFn(options.enableHighlight, options.enableMermaid)
@@ -435,6 +436,20 @@ export function insertTextAtCursor(editorId, text) {
cm.focus(); cm.focus();
} }
function toggleContainerFullscreen(stored) {
const wrapper = stored.easyMDE.codemirror.getWrapperElement();
const container = wrapper?.closest('.sb-markdown-editor');
if (!container) {
return;
}
container.classList.toggle('sb-markdown-editor--fullscreen');
document.documentElement.classList.toggle(
'sb-markdown-editor-fullscreen-active',
container.classList.contains('sb-markdown-editor--fullscreen'));
stored.easyMDE.codemirror.refresh();
}
export function execAction(editorId, action, value) { export function execAction(editorId, action, value) {
const stored = editors.get(editorId); const stored = editors.get(editorId);
if (!stored) { if (!stored) {
@@ -457,7 +472,7 @@ export function execAction(editorId, action, value) {
case 'image': easyMDE.drawImage(); break; case 'image': easyMDE.drawImage(); break;
case 'preview': easyMDE.togglePreview(); break; case 'preview': easyMDE.togglePreview(); break;
case 'side-by-side': easyMDE.toggleSideBySide(); break; case 'side-by-side': easyMDE.toggleSideBySide(); break;
case 'fullscreen': easyMDE.toggleFullScreen(); break; case 'fullscreen': toggleContainerFullscreen(stored); break;
case 'undo': easyMDE.codemirror.undo(); break; case 'undo': easyMDE.codemirror.undo(); break;
case 'redo': easyMDE.codemirror.redo(); break; case 'redo': easyMDE.codemirror.redo(); break;
default: default:
@@ -8445,6 +8445,55 @@ body.sb-resizing {
color: var(--sb-color-text); color: var(--sb-color-text);
} }
.sb-markdown-editor .EasyMDEContainer.sided--no-fullscreen {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.sb-markdown-editor .EasyMDEContainer.sided--no-fullscreen .CodeMirror-sided,
.sb-markdown-editor .EasyMDEContainer.sided--no-fullscreen .editor-preview-side {
flex: 1 1 50%;
width: 50% !important;
min-width: 20rem;
position: static !important;
inset: auto !important;
height: auto;
max-height: none;
}
.sb-markdown-editor .EasyMDEContainer.sided--no-fullscreen .editor-preview-active-side {
display: block;
}
.sb-markdown-editor-fullscreen-active,
.sb-markdown-editor-fullscreen-active body {
overflow: hidden;
}
.sb-markdown-editor.sb-markdown-editor--fullscreen {
position: fixed;
inset: var(--sb-space-4);
z-index: 2000;
min-height: auto;
background: var(--sb-color-surface);
box-shadow: var(--sb-shadow-xl, 0 24px 64px rgba(15, 23, 42, 0.22));
}
.sb-markdown-editor.sb-markdown-editor--fullscreen .EasyMDEContainer {
flex: 1 1 auto;
min-height: 0;
}
.sb-markdown-editor.sb-markdown-editor--fullscreen .EasyMDEContainer .CodeMirror {
height: calc(100vh - 9rem) !important;
}
.sb-markdown-editor.sb-markdown-editor--fullscreen .EasyMDEContainer.sided--no-fullscreen .CodeMirror-sided,
.sb-markdown-editor.sb-markdown-editor--fullscreen .EasyMDEContainer.sided--no-fullscreen .editor-preview-side {
height: calc(100vh - 9rem) !important;
}
.sb-markdown-viewer pre, .sb-markdown-viewer pre,
.sb-markdown-viewer code { .sb-markdown-viewer code {
font-family: var(--sb-font-family-mono, monospace); font-family: var(--sb-font-family-mono, monospace);