fix(creator): resolve editor duplication and theme synchronization issues
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private IJSObjectReference? _module;
|
||||
private DotNetObjectReference<MarkdownEditor>? _dotNetHelper;
|
||||
private string? _lastInitializedEditorId;
|
||||
|
||||
private enum SaveStatus
|
||||
{
|
||||
@@ -136,9 +137,11 @@
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender || _reinitializeEditor)
|
||||
var shouldInit = (firstRender || _reinitializeEditor) && (EditorId != _lastInitializedEditorId);
|
||||
if (shouldInit)
|
||||
{
|
||||
_reinitializeEditor = false;
|
||||
_lastInitializedEditorId = EditorId; // Set immediately before any async yield to prevent concurrent triggers
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
@@ -153,7 +156,7 @@
|
||||
{
|
||||
_module = await JS.InvokeAsync<IJSObjectReference>(
|
||||
"import",
|
||||
"./_content/NexusReader.UI.Shared/js/milkdownWrapper.js"
|
||||
$"./_content/NexusReader.UI.Shared/js/milkdownWrapper.js?v={Guid.NewGuid():N}"
|
||||
);
|
||||
}
|
||||
await _module.InvokeVoidAsync("initEditor", EditorId, _dotNetHelper, InitialMarkdown);
|
||||
@@ -178,7 +181,7 @@
|
||||
{
|
||||
_module = await JS.InvokeAsync<IJSObjectReference>(
|
||||
"import",
|
||||
"./_content/NexusReader.UI.Shared/js/milkdownWrapper.js"
|
||||
$"./_content/NexusReader.UI.Shared/js/milkdownWrapper.js?v={Guid.NewGuid():N}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -507,11 +510,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Always try to destroy via global window registration first to handle null _module
|
||||
await JS.InvokeVoidAsync("milkdownWrapper.destroyEditor", EditorId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to module if global is not set
|
||||
if (_module is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _module.InvokeVoidAsync("destroyEditor", EditorId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fail silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_module is not null)
|
||||
{
|
||||
await _module.InvokeVoidAsync("destroyEditor", EditorId);
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +183,11 @@
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await ThemeService.InitializeAsync();
|
||||
_isFullyLoaded = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
@@ -354,6 +354,8 @@
|
||||
/* --- Desktop Sidebar: warm paper shadow --- */
|
||||
.theme-light ::deep .hub-sidebar {
|
||||
box-shadow: 4px 0 20px rgba(139, 130, 115, 0.08);
|
||||
background: var(--bg-surface) !important;
|
||||
border-right: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
/* --- Logo icon: remove neon glow --- */
|
||||
|
||||
@@ -2,66 +2,185 @@
|
||||
@page "/creator/edit/{BookId}/{ChapterId}"
|
||||
@layout MainHubLayout
|
||||
@attribute [Authorize]
|
||||
@using NexusReader.UI.Shared.Components
|
||||
|
||||
<div class="creator-edit-fullscreen-wrapper">
|
||||
|
||||
<div class="chapters-sidebar">
|
||||
<div class="sidebar-meta-header">
|
||||
<h2>Rozdziały</h2>
|
||||
</div>
|
||||
<div class="chapters-list-wrapper">
|
||||
<div class="chapter-item active">
|
||||
<div class="active-indicator"></div>
|
||||
<i class="fa-solid fa-book-open chapter-icon"></i>
|
||||
<span class="chapter-title-text">1. Rozdział 1: Wprowadzenie do Zen Mode</span>
|
||||
</div>
|
||||
<div class="chapter-item">
|
||||
<i class="fa-solid fa-file-lines chapter-icon"></i>
|
||||
<span class="chapter-title-text">2. Rozdział 2: Zabezpieczenia i Architektura</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (_loadingChapters)
|
||||
{
|
||||
<div class="hub-loading" style="height: calc(100vh - 4rem); display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: var(--bg-base);">
|
||||
<div class="nexus-loader"></div>
|
||||
<p style="margin-top: 1rem; color: var(--text-muted); font-family: var(--nexus-font-sans);">Ładowanie struktury książki...</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="creator-edit-fullscreen-wrapper">
|
||||
|
||||
<div class="editor-workspace-area">
|
||||
|
||||
<div class="editor-header-row">
|
||||
<div class="title-zone">
|
||||
<h1 class="chapter-title">Rozdział 1: Wprowadzenie do Zen Mode</h1>
|
||||
<div class="chapters-sidebar">
|
||||
<div class="sidebar-meta-header">
|
||||
<h2>Rozdziały</h2>
|
||||
</div>
|
||||
<div class="telemetry-zone">
|
||||
<span class="chapter-id-badge">ID: @ChapterId</span>
|
||||
<div class="chapters-list-wrapper">
|
||||
@foreach (var ch in _chapters)
|
||||
{
|
||||
var isActive = ch.Id == _activeChapterId;
|
||||
<a class="chapter-item @(isActive ? "active" : "")" href="/creator/edit/@BookId/@ch.Id">
|
||||
@if (isActive)
|
||||
{
|
||||
<div class="active-indicator"></div>
|
||||
<i class="fa-solid fa-book-open chapter-icon"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa-solid fa-file-lines chapter-icon"></i>
|
||||
}
|
||||
<span class="chapter-title-text">@ch.Title</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-canvas-card">
|
||||
<div class="editor-workspace-area">
|
||||
|
||||
<div class="milkdown-premium-container" spellcheck="false">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
|
||||
<div class="editor-footer-bar">
|
||||
<div class="cloud-status-container">
|
||||
<span class="cloud-status-pulse"></span>
|
||||
<span class="cloud-status-text">Saved to Cloud</span>
|
||||
<div class="editor-header-row">
|
||||
<div class="title-zone">
|
||||
<h1 class="chapter-title">@_activeChapterTitle</h1>
|
||||
</div>
|
||||
<div class="telemetry-zone">
|
||||
<span class="chapter-id-badge">ID: @_activeChapterId</span>
|
||||
</div>
|
||||
|
||||
<button class="btn-nexus-premium" @onclick="FetchContent">
|
||||
<span>Fetch Markdown Content</span>
|
||||
<i class="fa-solid fa-arrow-right-long"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-canvas-card">
|
||||
@if (_loadingChapter)
|
||||
{
|
||||
<div class="hub-loading" style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||||
<div class="nexus-loader"></div>
|
||||
<p style="margin-top: 1rem; color: var(--text-muted); font-family: var(--nexus-font-sans);">Wczytywanie treści rozdziału...</p>
|
||||
</div>
|
||||
}
|
||||
else if (_isChapterLoaded)
|
||||
{
|
||||
<div class="milkdown-premium-container" spellcheck="false">
|
||||
<MarkdownEditor @key="_activeChapterId"
|
||||
ChapterId="_activeChapterId"
|
||||
InitialMarkdown="@_initialMarkdown"
|
||||
ShowFetchButton="false" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; color: var(--text-muted); font-family: var(--nexus-font-sans);">
|
||||
<p>Wybierz lub utwórz rozdział, aby rozpocząć edycję.</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject] private HttpClient Http { get; set; } = default!;
|
||||
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Parameter] public string BookId { get; set; } = string.Empty;
|
||||
[Parameter] public string ChapterId { get; set; } = string.Empty;
|
||||
private string _retrievedMarkdown = string.Empty;
|
||||
|
||||
private async Task FetchContent()
|
||||
private List<ChapterListItem> _chapters = new();
|
||||
private Guid _parsedBookId = Guid.Empty;
|
||||
private Guid _activeChapterId = Guid.Empty;
|
||||
private string _activeChapterTitle = string.Empty;
|
||||
private string _initialMarkdown = string.Empty;
|
||||
private bool _loadingChapters = true;
|
||||
private bool _loadingChapter = false;
|
||||
private bool _isChapterLoaded = false;
|
||||
|
||||
private class ChapterListItem
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
private class ChapterDetail
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string MarkdownContent { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnParametersSetAsync();
|
||||
|
||||
if (!Guid.TryParse(BookId, out var parsedBookId))
|
||||
{
|
||||
NavigationManager.NavigateTo("/creator");
|
||||
return;
|
||||
}
|
||||
|
||||
_parsedBookId = parsedBookId;
|
||||
|
||||
// Fetch chapters list if empty or if book ID has changed
|
||||
if (_chapters.Count == 0)
|
||||
{
|
||||
_loadingChapters = true;
|
||||
try
|
||||
{
|
||||
var chapters = await Http.GetFromJsonAsync<List<ChapterListItem>>($"/api/creator/books/{_parsedBookId}/chapters");
|
||||
_chapters = chapters ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[CreatorEdit] Error fetching chapters list: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingChapters = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If ChapterId is empty/null, select the first chapter from list and navigate
|
||||
if (string.IsNullOrEmpty(ChapterId))
|
||||
{
|
||||
if (_chapters.Any())
|
||||
{
|
||||
NavigationManager.NavigateTo($"/creator/edit/{BookId}/{_chapters.First().Id}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(ChapterId, out var parsedChapterId))
|
||||
{
|
||||
// If active chapter changed, fetch its details
|
||||
if (parsedChapterId != _activeChapterId)
|
||||
{
|
||||
_activeChapterId = parsedChapterId;
|
||||
var ch = _chapters.FirstOrDefault(c => c.Id == _activeChapterId);
|
||||
_activeChapterTitle = ch?.Title ?? "Rozdział";
|
||||
|
||||
_loadingChapter = true;
|
||||
_isChapterLoaded = false;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var detail = await Http.GetFromJsonAsync<ChapterDetail>($"/api/chapters/{_activeChapterId}");
|
||||
if (detail != null)
|
||||
{
|
||||
_initialMarkdown = detail.MarkdownContent;
|
||||
_isChapterLoaded = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[CreatorEdit] Error fetching chapter content: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadingChapter = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
}
|
||||
|
||||
/* DEEP MOUNTING COMPONENT INTEROP */
|
||||
::deep .milkdown {
|
||||
.milkdown-premium-container ::deep .milkdown {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
@@ -196,7 +196,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
::deep .ProseMirror {
|
||||
.milkdown-premium-container ::deep .ProseMirror {
|
||||
color: #e4e1d9 !important;
|
||||
background-color: transparent !important;
|
||||
font-size: 1.15rem !important;
|
||||
@@ -209,87 +209,132 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-light ::deep .ProseMirror {
|
||||
.theme-light .milkdown-premium-container ::deep .ProseMirror {
|
||||
color: #2d2a26 !important;
|
||||
}
|
||||
|
||||
/* Precise matching text selection token */
|
||||
::deep .ProseMirror ::selection {
|
||||
.milkdown-premium-container ::deep .ProseMirror ::selection {
|
||||
background-color: rgba(0, 255, 153, 0.2) !important;
|
||||
}
|
||||
|
||||
.theme-light ::deep .ProseMirror ::selection {
|
||||
.theme-light .milkdown-premium-container ::deep .ProseMirror ::selection {
|
||||
background-color: rgba(16, 185, 129, 0.18) !important;
|
||||
}
|
||||
|
||||
/* Core webkit custom scrollbar mapping */
|
||||
::deep .ProseMirror::-webkit-scrollbar {
|
||||
.milkdown-premium-container ::deep .ProseMirror::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
::deep .ProseMirror::-webkit-scrollbar-track {
|
||||
.milkdown-premium-container ::deep .ProseMirror::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::deep .ProseMirror::-webkit-scrollbar-thumb {
|
||||
.milkdown-premium-container ::deep .ProseMirror::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.theme-light ::deep .ProseMirror::-webkit-scrollbar-thumb {
|
||||
.theme-light .milkdown-premium-container ::deep .ProseMirror::-webkit-scrollbar-thumb {
|
||||
background: #dcd7cc;
|
||||
}
|
||||
|
||||
/* 5. SEAMLESS INTEGRATED ACTIONS FOOTER BAR */
|
||||
.editor-footer-bar {
|
||||
/* 5. SEAMLESS INTEGRATED ACTIONS FOOTER BAR (OVERWRITING FOR MARKDOWNEDITOR COMPONENT INTEGRATION) */
|
||||
.milkdown-premium-container ::deep .markdown-editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .milkdown-editor-wrapper {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
flex-grow: 1;
|
||||
overflow: hidden !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .milkdown {
|
||||
flex-grow: 1;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .editor-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||||
margin-top: 2rem !important;
|
||||
padding: 1.5rem 0 0 0 !important;
|
||||
border: none !important;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.04) !important;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-light .editor-footer-bar {
|
||||
border-top: 1px solid #dcd7cc;
|
||||
.theme-light .milkdown-premium-container ::deep .editor-footer {
|
||||
border-top: 1px solid #dcd7cc !important;
|
||||
}
|
||||
|
||||
/* Telemetry cloud synchronization line */
|
||||
.cloud-status-container {
|
||||
/* Telemetry cloud synchronization line mapping */
|
||||
.milkdown-premium-container ::deep .status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cloud-status-pulse {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background-color: #00ff99;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 153, 0.8);
|
||||
}
|
||||
|
||||
.theme-light .cloud-status-pulse {
|
||||
background-color: #10b981;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6);
|
||||
}
|
||||
|
||||
.cloud-status-text {
|
||||
font-family: 'Azeret Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #71717a;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-premium-container ::deep .status-indicator {
|
||||
color: #78716c;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .status-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .status-dot.saved {
|
||||
background-color: #00ff99 !important;
|
||||
box-shadow: 0 0 10px rgba(0, 255, 153, 0.8) !important;
|
||||
color: #00ff99 !important;
|
||||
}
|
||||
|
||||
.theme-light .milkdown-premium-container ::deep .status-dot.saved {
|
||||
background-color: #10b981 !important;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.6) !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .status-dot.saving {
|
||||
background-color: #F59E0B !important;
|
||||
box-shadow: 0 0 10px rgba(245, 158, 11, 0.8) !important;
|
||||
color: #F59E0B !important;
|
||||
}
|
||||
|
||||
.milkdown-premium-container ::deep .status-dot.offline {
|
||||
background-color: #EF4444 !important;
|
||||
box-shadow: 0 0 10px rgba(239, 68, 68, 0.8) !important;
|
||||
color: #EF4444 !important;
|
||||
}
|
||||
|
||||
/* Premium Tactile Operational Button Trigger */
|
||||
.btn-nexus-premium {
|
||||
.milkdown-premium-container ::deep .nexus-btn {
|
||||
background-color: #00ff99 !important;
|
||||
color: #121214 !important;
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: -0.1px;
|
||||
padding: 11px 24px;
|
||||
padding: 11px 24px !important;
|
||||
border: none !important;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
@@ -298,28 +343,23 @@
|
||||
gap: 10px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 20px rgba(0, 255, 153, 0.15);
|
||||
height: auto !important;
|
||||
min-height: unset !important;
|
||||
}
|
||||
|
||||
.theme-light .btn-nexus-premium {
|
||||
.theme-light .milkdown-premium-container ::deep .nexus-btn {
|
||||
background-color: #10b981 !important;
|
||||
color: #ffffff !important;
|
||||
box-shadow: 0 4px 20px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.btn-nexus-premium i {
|
||||
font-size: 0.85rem;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-nexus-premium:hover {
|
||||
.milkdown-premium-container ::deep .nexus-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 24px rgba(0, 255, 153, 0.3);
|
||||
}
|
||||
|
||||
.theme-light .btn-nexus-premium:hover {
|
||||
.theme-light .milkdown-premium-container ::deep .nexus-btn:hover {
|
||||
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.btn-nexus-premium:hover i {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// Map to keep track of active Crepe editor instances by elementId (container ID)
|
||||
const editorCache = new Map();
|
||||
// Initialize global stores on window to share state across dynamically imported module instances (preventing cache-buster isolation)
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!window.editorCache) window.editorCache = new Map();
|
||||
if (!window.editorStates) window.editorStates = new Map();
|
||||
}
|
||||
|
||||
const editorCache = typeof window !== 'undefined' ? window.editorCache : new Map();
|
||||
const editorStates = typeof window !== 'undefined' ? window.editorStates : new Map();
|
||||
|
||||
/**
|
||||
* Asynchronously injects a stylesheet link tag into the document head
|
||||
@@ -23,19 +29,64 @@ async function ensureStylesheet(href) {
|
||||
* Initializes a Milkdown Crepe editor on the specified element.
|
||||
*/
|
||||
export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
|
||||
// Check if already destroyed or initializing
|
||||
if (editorStates.get(elementId) === 'destroyed') {
|
||||
console.warn(`[Milkdown] initEditor called on already destroyed element: ${elementId}. Aborting.`);
|
||||
return;
|
||||
}
|
||||
if (editorStates.get(elementId) === 'initializing' || editorStates.get(elementId) === 'ready') {
|
||||
console.warn(`[Milkdown] Editor is already initializing or ready for element: ${elementId}. Ignoring.`);
|
||||
return;
|
||||
}
|
||||
|
||||
editorStates.set(elementId, 'initializing');
|
||||
|
||||
// Guard 1: Destroy previous cached editor instance with the same ID if it exists
|
||||
if (editorCache.has(elementId)) {
|
||||
console.warn(`[Milkdown] Editor instance already exists in cache for: ${elementId}. Destroying first.`);
|
||||
await destroyEditor(elementId);
|
||||
}
|
||||
|
||||
const container = document.getElementById(elementId);
|
||||
if (!container) {
|
||||
console.error(`[Milkdown] Container with ID "${elementId}" not found.`);
|
||||
editorStates.delete(elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Guard 2: Clear container children to prevent double-initialization of crepe editor DOM
|
||||
if (container.children.length > 0) {
|
||||
console.warn(`[Milkdown] Container "${elementId}" is not empty. Clearing children before initialization.`);
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
||||
// Guard 3: Search the parent workspace card to purge any other leftover editor components
|
||||
const parentCard = container.closest('.milkdown-premium-container') || container.parentElement;
|
||||
if (parentCard) {
|
||||
const existingEditors = parentCard.querySelectorAll('.milkdown, .crepe');
|
||||
if (existingEditors.length > 0) {
|
||||
console.warn(`[Milkdown] Found ${existingEditors.length} leftover editor DOM elements in the workspace card. Purging them.`);
|
||||
existingEditors.forEach(el => el.remove());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Condition 2: Prevent FOUC by loading stylesheets before instantiating the editor
|
||||
await ensureStylesheet('/_content/NexusReader.UI.Shared/css/vendor/milkdown-crepe.css');
|
||||
|
||||
if (editorStates.get(elementId) === 'destroyed') {
|
||||
console.warn(`[Milkdown] Element ${elementId} destroyed during stylesheet loading. Aborting.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dynamically import the local JS bundle
|
||||
await import('/_content/NexusReader.UI.Shared/js/vendor/milkdown-crepe.js');
|
||||
|
||||
if (editorStates.get(elementId) === 'destroyed') {
|
||||
console.warn(`[Milkdown] Element ${elementId} destroyed during crepe bundle loading. Aborting.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Crepe constructor from the global window.milkdownCrepe namespace
|
||||
const Crepe = window.milkdownCrepe?.Crepe;
|
||||
if (!Crepe) {
|
||||
@@ -100,6 +151,7 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
|
||||
clearTimeout(debounceTimeout);
|
||||
}
|
||||
debounceTimeout = setTimeout(() => {
|
||||
if (editorStates.get(elementId) === 'destroyed') return;
|
||||
dotNetHelper.invokeMethodAsync('OnEditorContentChanged', markdown)
|
||||
.catch(err => console.error("[Milkdown] Failed to notify editor content changed:", err));
|
||||
}, 300);
|
||||
@@ -112,8 +164,17 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
|
||||
// Create the editor view asynchronously
|
||||
await crepe.create();
|
||||
|
||||
if (editorStates.get(elementId) === 'destroyed') {
|
||||
console.warn(`[Milkdown] Element ${elementId} destroyed during crepe.create(). Cleaning up.`);
|
||||
await crepe.destroy();
|
||||
editorCache.delete(elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
editorStates.set(elementId, 'ready');
|
||||
console.log(`[Milkdown] Editor successfully initialized on element: ${elementId}`);
|
||||
} catch (error) {
|
||||
editorStates.delete(elementId);
|
||||
console.error(`[Milkdown] Failed to initialize editor on "${elementId}":`, error);
|
||||
}
|
||||
}
|
||||
@@ -134,6 +195,8 @@ export function getMarkdownContent(elementId) {
|
||||
* Safely disposes of the editor instance to prevent memory leaks in WASM.
|
||||
*/
|
||||
export async function destroyEditor(elementId) {
|
||||
editorStates.set(elementId, 'destroyed');
|
||||
|
||||
const crepe = editorCache.get(elementId);
|
||||
if (crepe) {
|
||||
try {
|
||||
@@ -144,6 +207,12 @@ export async function destroyEditor(elementId) {
|
||||
}
|
||||
editorCache.delete(elementId);
|
||||
}
|
||||
|
||||
// Explicitly clean up container DOM children
|
||||
const container = document.getElementById(elementId);
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,3 +232,13 @@ export function getBackupKeys() {
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// Attach to window for global access (especially from DisposeAsync when module reference is null)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.milkdownWrapper = {
|
||||
initEditor,
|
||||
getMarkdownContent,
|
||||
destroyEditor,
|
||||
getBackupKeys
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user