feat(editor): align selection popup and all editor control elements styling with Reader #81

Merged
mjasin merged 14 commits from feature/milkdown-integration into develop 2026-06-11 18:07:53 +00:00
5 changed files with 1177 additions and 15 deletions
Showing only changes of commit 56677556cd - Show all commits
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,20 +1,20 @@
// Map to keep track of active Crepe editor instances by elementId (container ID) // Map to keep track of active Crepe editor instances by elementId (container ID)
const editors = new Map(); const editorCache = new Map();
/** /**
* Asynchronously injects a stylesheet link tag into the document head * Asynchronously injects a stylesheet link tag into the document head
* and returns a Promise that resolves when the stylesheet is fully loaded. * and returns a Promise that resolves when the stylesheet is fully loaded.
*/ */
async function injectStylesheet(url) { async function ensureStylesheet(href) {
if (document.querySelector(`link[href="${url}"]`)) { if (document.querySelector(`link[href="${href}"]`)) {
return Promise.resolve(); return Promise.resolve();
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.href = url; link.href = href;
link.onload = () => resolve(); link.onload = () => resolve();
link.onerror = (err) => reject(new Error(`Failed to load stylesheet: ${url}. ${err}`)); link.onerror = (err) => reject(new Error(`Failed to load stylesheet: ${href}. ${err}`));
document.head.appendChild(link); document.head.appendChild(link);
}); });
} }
@@ -31,13 +31,16 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
try { try {
// Condition 2: Prevent FOUC by loading stylesheets before instantiating the editor // Condition 2: Prevent FOUC by loading stylesheets before instantiating the editor
await Promise.all([ await ensureStylesheet('/_content/NexusReader.UI.Shared/css/vendor/milkdown-crepe.css');
injectStylesheet('https://esm.sh/@milkdown/crepe/lib/theme/common/style.css'),
injectStylesheet('https://esm.sh/@milkdown/crepe/lib/theme/frame/style.css')
]);
// Dynamically import the Crepe ESM module // Dynamically import the local JS bundle
const { Crepe } = await import('https://esm.sh/@milkdown/crepe'); await import('/_content/NexusReader.UI.Shared/js/vendor/milkdown-crepe.js');
// Get Crepe constructor from the global window.milkdownCrepe namespace
const Crepe = window.milkdownCrepe?.Crepe;
if (!Crepe) {
throw new Error("Crepe constructor not found on window.milkdownCrepe");
}
// Initialize the Crepe editor instance with custom ImageBlock upload handler // Initialize the Crepe editor instance with custom ImageBlock upload handler
const crepe = new Crepe({ const crepe = new Crepe({
@@ -61,7 +64,7 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
}); });
// Store the editor instance in the map // Store the editor instance in the map
editors.set(elementId, crepe); editorCache.set(elementId, crepe);
// Create the editor view asynchronously // Create the editor view asynchronously
await crepe.create(); await crepe.create();
@@ -76,7 +79,7 @@ export async function initEditor(elementId, dotNetHelper, initialMarkdown) {
* Retrieves the current Markdown content from a specific editor instance. * Retrieves the current Markdown content from a specific editor instance.
*/ */
export function getMarkdownContent(elementId) { export function getMarkdownContent(elementId) {
const crepe = editors.get(elementId); const crepe = editorCache.get(elementId);
if (!crepe) { if (!crepe) {
console.warn(`[Milkdown] No editor instance found for element: ${elementId}`); console.warn(`[Milkdown] No editor instance found for element: ${elementId}`);
return ""; return "";
@@ -88,7 +91,7 @@ export function getMarkdownContent(elementId) {
* Safely disposes of the editor instance to prevent memory leaks in WASM. * Safely disposes of the editor instance to prevent memory leaks in WASM.
*/ */
export async function destroyEditor(elementId) { export async function destroyEditor(elementId) {
const crepe = editors.get(elementId); const crepe = editorCache.get(elementId);
if (crepe) { if (crepe) {
try { try {
await crepe.destroy(); await crepe.destroy();
@@ -96,6 +99,6 @@ export async function destroyEditor(elementId) {
} catch (error) { } catch (error) {
console.error(`[Milkdown] Error destroying editor for element "${elementId}":`, error); console.error(`[Milkdown] Error destroying editor for element "${elementId}":`, error);
} }
editors.delete(elementId); editorCache.delete(elementId);
} }
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long