This commit addresses several issues related to the migration from TipTap to CodeMirror:
- **CodeMirror Initialization Fixes:**
- Resolved 'Unrecognized extension value' errors by refactoring to explicitly import individual CodeMirror extensions instead of relying on . This ensures proper singleton usage and prevents module duplication issues.
- Updated and to utilize these individual extensions.
- **Text Wrapping Enabled:**
- Added to the extensions in and to enable text wrapping in the CodeMirror editors.
- **Content Saving Fixes:**
- Corrected content binding for CodeMirror editor instances in various IDAA components:
- (description, location_text, attend_text)
- (content, notes)
- (content)
- (description, notes)
- (description, notes)
- Ensured that the prop of is correctly bound to the respective state variables in the parent components, and these state variables are initialized with existing content.
- **Save Button Enablement:**
- Fixed an issue in where the Save button was not enabling on content changes. The logic now directly compares the and with the original object's content, ensuring reactivity.
130 lines
4.8 KiB
Svelte
130 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import { browser } from '$app/environment';
|
|
import { ensureCodeMirrorModules } from './codemirror_modules';
|
|
|
|
export let content: string = '';
|
|
export let placeholder: string = 'Start typing...';
|
|
|
|
let editor_container: HTMLDivElement;
|
|
let editor_view: any;
|
|
|
|
async function createEditor() {
|
|
if (!browser) return;
|
|
const cm = await ensureCodeMirrorModules();
|
|
if (!cm) return;
|
|
|
|
// If an existing instance exists (HMR / remount), destroy it first
|
|
if (editor_view && typeof editor_view.destroy === 'function') {
|
|
try { editor_view.destroy(); } catch (e) { /* ignore */ }
|
|
editor_view = null;
|
|
}
|
|
|
|
// Build extensions defensively
|
|
const extensions = [
|
|
cm.lineNumbers(),
|
|
cm.highlightSpecialChars(),
|
|
cm.history(),
|
|
cm.foldGutter(),
|
|
cm.drawSelection(),
|
|
cm.dropCursor(),
|
|
cm.EditorState_allowMultipleSelections.of(true),
|
|
cm.indentOnInput(),
|
|
cm.bracketMatching(),
|
|
cm.closeBrackets(),
|
|
cm.autocompletion(),
|
|
cm.rectangularSelection(),
|
|
cm.crosshairCursor(),
|
|
cm.highlightActiveLine(),
|
|
cm.highlightActiveLineGutter(),
|
|
cm.EditorView_lineWrapping,
|
|
cm.keymap.of([
|
|
...cm.defaultKeymap,
|
|
...cm.searchKeymap,
|
|
...cm.historyKeymap,
|
|
...cm.foldKeymap,
|
|
...cm.completionKeymap,
|
|
...cm.lintKeymap
|
|
]),
|
|
cm.markdown ? cm.markdown({ base: cm.markdownLanguage }) : null,
|
|
cm.EditorView && cm.EditorView.updateListener ? cm.EditorView.updateListener.of((update: any) => {
|
|
if (update.docChanged) content = update.state.doc.toString();
|
|
}) : null
|
|
].filter(Boolean);
|
|
|
|
// Create editor
|
|
editor_view = new cm.EditorView({
|
|
state: cm.EditorState.create({
|
|
doc: content ?? '',
|
|
extensions
|
|
}),
|
|
parent: editor_container
|
|
});
|
|
}
|
|
|
|
onMount(async () => {
|
|
// ensure it's created only in browser and after modules resolved
|
|
await createEditor();
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (editor_view && typeof editor_view.destroy === 'function') {
|
|
try { editor_view.destroy(); } catch (e) { /* ignore */ }
|
|
editor_view = null;
|
|
}
|
|
});
|
|
|
|
// Helper functions using the live editor_view (unchanged)
|
|
const wrapSelection = (before: string, after: string = before) => {
|
|
if (!editor_view) return;
|
|
const state = editor_view.state;
|
|
const changes = state.changeByRange((range: any) => {
|
|
const isAlreadyWrapped =
|
|
state.sliceDoc(range.from - before.length, range.from) === before &&
|
|
state.sliceDoc(range.to, range.to + after.length) === after;
|
|
|
|
if (isAlreadyWrapped) {
|
|
return {
|
|
changes: [
|
|
{ from: range.from - before.length, to: range.from, insert: '' },
|
|
{ from: range.to, to: range.to + after.length, insert: '' }
|
|
],
|
|
range: (state as any).constructor.range(range.from - before.length, range.to - before.length)
|
|
};
|
|
}
|
|
|
|
return {
|
|
changes: [
|
|
{ from: range.from, insert: before },
|
|
{ from: range.to, insert: after }
|
|
],
|
|
range: (state as any).constructor.range(range.from + before.length, range.to + before.length)
|
|
};
|
|
});
|
|
editor_view.dispatch(changes);
|
|
editor_view.focus();
|
|
};
|
|
|
|
const insertList = () => {
|
|
if (!editor_view) return;
|
|
const state = editor_view.state;
|
|
const changes = state.changeByRange((range: any) => {
|
|
const line = state.doc.lineAt(range.from);
|
|
return {
|
|
changes: [{ from: line.from, insert: '- ' }],
|
|
range: (state as any).constructor.range(range.from + 2, range.to + 2)
|
|
};
|
|
});
|
|
editor_view.dispatch(changes);
|
|
editor_view.focus();
|
|
};
|
|
</script>
|
|
|
|
<div class="codemirror-wrapper border rounded">
|
|
<div class="toolbar p-1 bg-gray-100 border-b">
|
|
<button on:click={() => wrapSelection('**')} class="px-2 py-1 rounded hover:bg-gray-200"><b>B</b></button>
|
|
<button on:click={() => wrapSelection('*')} class="px-2 py-1 rounded hover:bg-gray-200"><i>I</i></button>
|
|
<button on:click={insertList} class="px-2 py-1 rounded hover:bg-gray-200">List</button>
|
|
</div>
|
|
<div bind:this={editor_container} class="h-full min-h-[150px] overflow-auto"></div>
|
|
</div> |