Optimize editor data flow
This commit is contained in:
Vendored
+2
-1
@@ -3,5 +3,6 @@
|
|||||||
"cSpell.words": ["pako", "Serde", "serdes"],
|
"cSpell.words": ["pako", "Serde", "serdes"],
|
||||||
"vitest.commandLine": "yarn test:unit",
|
"vitest.commandLine": "yarn test:unit",
|
||||||
"vitest.enable": true,
|
"vitest.enable": true,
|
||||||
"testing.autoRun.mode": "rerun"
|
"testing.autoRun.mode": "rerun",
|
||||||
|
"svelte.enable-ts-plugin": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
export let isCloseable = true;
|
export let isCloseable = true;
|
||||||
export let isOpen = true;
|
export let isOpen = true;
|
||||||
export let tabs: Tab[] = [];
|
export let tabs: Tab[] = [];
|
||||||
|
export let activeTabID: string = '';
|
||||||
export let title: string;
|
export let title: string;
|
||||||
$: isOpen = isCloseable ? isOpen : true;
|
$: isOpen = isCloseable ? isOpen : true;
|
||||||
$: isTabsShown = isOpen && tabs.length > 0;
|
$: isTabsShown = isOpen && tabs.length > 0;
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
class="bg-primary p-2 {isTabsShown ? 'pb-0' : ''} flex-none cursor-pointer"
|
class="bg-primary p-2 {isTabsShown ? 'pb-0' : ''} flex-none cursor-pointer"
|
||||||
on:click={() => (isOpen = !isOpen)}>
|
on:click={() => (isOpen = !isOpen)}>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Tabs on:select {tabs} bind:isOpen {title} {isCloseable} />
|
<Tabs on:select {tabs} bind:isOpen {title} {isCloseable} {activeTabID} />
|
||||||
<div class="flex gap-x-4 items-center {isTabsShown ? '-mt-2' : ''}">
|
<div class="flex gap-x-4 items-center {isTabsShown ? '-mt-2' : ''}">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
export let isCloseable = true;
|
export let isCloseable = true;
|
||||||
export let tabs: Tab[] = [];
|
export let tabs: Tab[];
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let isOpen = false;
|
export let isOpen = false;
|
||||||
|
export let activeTabID: string;
|
||||||
|
|
||||||
$: activeTabID = tabs[0]?.id;
|
if (!activeTabID && tabs.length > 0) {
|
||||||
|
activeTabID = tabs[0].id;
|
||||||
|
}
|
||||||
const dispatch = createEventDispatcher<TabEvents>();
|
const dispatch = createEventDispatcher<TabEvents>();
|
||||||
const toggleTabs = (tab: Tab) => {
|
const toggleTabs = (tab: Tab) => {
|
||||||
activeTabID = tab.id;
|
activeTabID = tab.id;
|
||||||
|
|||||||
@@ -1,77 +1,97 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EditorEvents } from '$lib/types';
|
import type { EditorMode } from '$lib/types';
|
||||||
import { stateStore } from '$lib/util/state';
|
import { stateStore, updateCode, updateConfig } from '$lib/util/state';
|
||||||
import { themeStore } from '$lib/util/theme';
|
import { themeStore } from '$lib/util/theme';
|
||||||
import { syncDiagram } from '$lib/util/util';
|
import { debounceEnabled, syncDiagram } from '$lib/util/util';
|
||||||
import type monaco from 'monaco-editor';
|
import type monaco from 'monaco-editor';
|
||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import initEditor from 'monaco-mermaid';
|
import initEditor from 'monaco-mermaid';
|
||||||
import { logEvent } from '$lib/util/stats';
|
import { logEvent } from '$lib/util/stats';
|
||||||
|
|
||||||
let divEl: HTMLDivElement = null;
|
let divEl: HTMLDivElement = null;
|
||||||
let editor: monaco.editor.IStandaloneCodeEditor;
|
let editor: monaco.editor.IStandaloneCodeEditor;
|
||||||
let Monaco;
|
let Monaco: typeof monaco;
|
||||||
|
let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
|
||||||
export let text: string;
|
|
||||||
export let language: string;
|
|
||||||
export let editorOptions: monaco.editor.IStandaloneEditorConstructionOptions = {
|
|
||||||
value: text,
|
|
||||||
language: language,
|
|
||||||
minimap: {
|
minimap: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
theme: 'mermaid',
|
theme: 'mermaid',
|
||||||
overviewRulerLanes: 0
|
overviewRulerLanes: 0
|
||||||
};
|
};
|
||||||
let oldText = text;
|
let text = '';
|
||||||
$: editor && Monaco?.editor.setModelLanguage(editor.getModel(), language);
|
let mode: EditorMode | undefined = undefined;
|
||||||
|
|
||||||
const handleTextUpdate = (newText: string) => {
|
stateStore.subscribe(({ errorMarkers, editorMode, code, mermaid }) => {
|
||||||
if (newText !== oldText) {
|
if (!editor) return;
|
||||||
if ($stateStore.updateEditor) {
|
|
||||||
editor?.setValue(newText);
|
|
||||||
}
|
|
||||||
oldText = newText;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$: handleTextUpdate(text);
|
// Update editor text if it's different
|
||||||
stateStore.subscribe(({ errorMarkers }) => {
|
const newText = editorMode === 'code' ? code : mermaid;
|
||||||
editor && Monaco?.editor.setModelMarkers(editor.getModel(), 'test', errorMarkers);
|
if (newText !== text) {
|
||||||
|
editor.setValue(newText);
|
||||||
|
text = newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update editor mode if it's different
|
||||||
|
if (mode !== editorMode) {
|
||||||
|
const language = editorMode === 'code' ? 'mermaid' : 'json';
|
||||||
|
Monaco?.editor.setModelLanguage(editor.getModel(), language);
|
||||||
|
mode = editorMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display errors if present
|
||||||
|
if (errorMarkers.length > 0) {
|
||||||
|
Monaco?.editor.setModelMarkers(editor.getModel(), 'test', errorMarkers);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
themeStore.subscribe(({ isDark }) => {
|
themeStore.subscribe(({ isDark }) => {
|
||||||
editor && Monaco?.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
|
editor && Monaco?.editor.setTheme(isDark ? 'mermaid-dark' : 'mermaid');
|
||||||
});
|
});
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<EditorEvents>();
|
const handleUpdate = (text: string, mode: EditorMode) => {
|
||||||
|
if (mode === 'code') {
|
||||||
|
updateCode(text, {
|
||||||
|
updateEditor: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateConfig(text, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debounce state updates to avoid performance issues
|
||||||
|
let debounce: { [key: string]: number } = {};
|
||||||
|
const updateHandler = (newText: string) => {
|
||||||
|
text = newText;
|
||||||
|
if (debounceEnabled) {
|
||||||
|
clearTimeout(debounce[mode]);
|
||||||
|
debounce[mode] = window.setTimeout(() => {
|
||||||
|
handleUpdate(text, mode);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
handleUpdate(text, mode);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadMonaco = async () => {
|
const loadMonaco = async () => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i++ < 10) {
|
while (i++ < 500) {
|
||||||
try {
|
try {
|
||||||
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
|
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
|
||||||
Monaco = monaco;
|
Monaco = window.monaco;
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
await new Promise((r) => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
alert('Loading Monaco Editor failed. Please try refreshing the page.');
|
alert('Loading Monaco Editor failed. Please try refreshing the page.');
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
|
||||||
// @ts-ignore : This is a hack to handle a svelte-kit error when importing monaco.
|
|
||||||
Monaco = monaco;
|
|
||||||
} catch {
|
|
||||||
await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175
|
await loadMonaco(); // Fix https://github.com/mermaid-js/mermaid-live-editor/issues/175
|
||||||
}
|
|
||||||
initEditor(Monaco);
|
initEditor(Monaco);
|
||||||
editor = Monaco.editor.create(divEl, editorOptions);
|
editor = Monaco.editor.create(divEl, editorOptions);
|
||||||
editor.onDidChangeModelContent(() => {
|
editor.onDidChangeModelContent((e) => {
|
||||||
oldText = editor.getValue();
|
updateHandler(editor.getValue());
|
||||||
dispatch('update', {
|
|
||||||
text: oldText
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
editor.addAction({
|
editor.addAction({
|
||||||
id: 'mermaid-render-diagram',
|
id: 'mermaid-render-diagram',
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
outOfSync = true;
|
outOfSync = true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('view fail', e);
|
console.error('view fail', e);
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+3
-7
@@ -16,13 +16,6 @@ export interface MarkerData {
|
|||||||
endColumn: number;
|
endColumn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorUpdateEvent {
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
export interface EditorEvents {
|
|
||||||
update: EditorUpdateEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TabEvents {
|
export interface TabEvents {
|
||||||
select: Tab;
|
select: Tab;
|
||||||
}
|
}
|
||||||
@@ -39,6 +32,7 @@ export interface State {
|
|||||||
updateEditor: boolean;
|
updateEditor: boolean;
|
||||||
updateDiagram: boolean;
|
updateDiagram: boolean;
|
||||||
autoSync: boolean;
|
autoSync: boolean;
|
||||||
|
editorMode?: EditorMode;
|
||||||
panZoom?: boolean;
|
panZoom?: boolean;
|
||||||
pan?: { x: number; y: number };
|
pan?: { x: number; y: number };
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
@@ -86,5 +80,7 @@ export interface DocConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EditorMode = 'code' | 'config';
|
||||||
|
|
||||||
export type Loader = (url: string) => Promise<State>;
|
export type Loader = (url: string) => Promise<State>;
|
||||||
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { initURLSubscription, loadState, updateCodeStore } from './state';
|
|||||||
import { analytics, initAnalytics } from './stats';
|
import { analytics, initAnalytics } from './stats';
|
||||||
import { loadDataFromUrl } from './fileLoaders/loader';
|
import { loadDataFromUrl } from './fileLoaders/loader';
|
||||||
import { initLoading } from './loading';
|
import { initLoading } from './loading';
|
||||||
|
import { applyMigrations } from './migrations';
|
||||||
|
|
||||||
export const loadStateFromURL = (): void => {
|
export const loadStateFromURL = (): void => {
|
||||||
loadState(window.location.hash.slice(1));
|
loadState(window.location.hash.slice(1));
|
||||||
@@ -14,6 +15,7 @@ export const syncDiagram = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const initHandler = async (): Promise<void> => {
|
export const initHandler = async (): Promise<void> => {
|
||||||
|
applyMigrations();
|
||||||
loadStateFromURL();
|
loadStateFromURL();
|
||||||
await initLoading('Loading Gist...', loadDataFromUrl().catch(console.error));
|
await initLoading('Loading Gist...', loadDataFromUrl().catch(console.error));
|
||||||
syncDiagram();
|
syncDiagram();
|
||||||
|
|||||||
@@ -6,12 +6,10 @@
|
|||||||
import { setTheme, themeStore } from '$lib/util/theme';
|
import { setTheme, themeStore } from '$lib/util/theme';
|
||||||
import { toggleDarkTheme } from '$lib/util/state';
|
import { toggleDarkTheme } from '$lib/util/state';
|
||||||
import { initHandler } from '$lib/util/util';
|
import { initHandler } from '$lib/util/util';
|
||||||
import { applyMigrations } from '$lib/util/migrations';
|
|
||||||
|
|
||||||
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
|
// This can be removed once https://github.com/sveltejs/kit/issues/1612 is fixed.
|
||||||
// Then move it into src and vite will bundle it automatically.
|
// Then move it into src and vite will bundle it automatically.
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
applyMigrations();
|
|
||||||
window.addEventListener('hashchange', async (ev) => {
|
window.addEventListener('hashchange', async (ev) => {
|
||||||
await initHandler();
|
await initHandler();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,20 +6,12 @@
|
|||||||
import View from '$lib/components/view.svelte';
|
import View from '$lib/components/view.svelte';
|
||||||
import Card from '$lib/components/card/card.svelte';
|
import Card from '$lib/components/card/card.svelte';
|
||||||
import History from '$lib/components/history/history.svelte';
|
import History from '$lib/components/history/history.svelte';
|
||||||
import { updateCode, updateConfig, inputStateStore, stateStore } from '$lib/util/state';
|
import { inputStateStore, stateStore, updateCodeStore } from '$lib/util/state';
|
||||||
import { cmdKey, debounceEnabled, initHandler, syncDiagram } from '$lib/util/util';
|
import { cmdKey, initHandler, syncDiagram } from '$lib/util/util';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { EditorUpdateEvent, State, Tab, DocConfig } from '$lib/types';
|
import type { Tab, DocConfig, EditorMode, ValidatedState } from '$lib/types';
|
||||||
import { base } from '$app/paths';
|
import { base } from '$app/paths';
|
||||||
|
|
||||||
type Modes = 'code' | 'config';
|
|
||||||
type Languages = 'mermaid' | 'json';
|
|
||||||
|
|
||||||
let selectedMode: Modes = 'code';
|
|
||||||
const languageMap: { [key in Modes]: Languages } = {
|
|
||||||
code: 'mermaid',
|
|
||||||
config: 'json'
|
|
||||||
};
|
|
||||||
const docURLBase = 'https://mermaid-js.github.io/mermaid';
|
const docURLBase = 'https://mermaid-js.github.io/mermaid';
|
||||||
const docMap: DocConfig = {
|
const docMap: DocConfig = {
|
||||||
graph: {
|
graph: {
|
||||||
@@ -60,34 +52,23 @@
|
|||||||
config: '/#/gitgraph?id=gitgraph-specific-configuration-options'
|
config: '/#/gitgraph?id=gitgraph-specific-configuration-options'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let text = '';
|
|
||||||
let docURL = docURLBase;
|
let docURL = docURLBase;
|
||||||
let language: Languages = 'mermaid';
|
let activeTabID = 'code';
|
||||||
const handleModeUpdate = (mode: Modes) => {
|
stateStore.subscribe(({ code, editorMode }: ValidatedState) => {
|
||||||
if (mode === 'code') {
|
activeTabID = editorMode ?? 'code';
|
||||||
text = $stateStore.code;
|
const codeTypeMatch = /([\S]+)[\s\n]/.exec(code);
|
||||||
} else {
|
|
||||||
text = $stateStore.mermaid;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
$: language = languageMap[selectedMode];
|
|
||||||
$: handleModeUpdate(selectedMode);
|
|
||||||
|
|
||||||
stateStore.subscribe((state: State) => {
|
|
||||||
if (state.updateEditor) {
|
|
||||||
text = selectedMode === 'code' ? state.code : state.mermaid;
|
|
||||||
}
|
|
||||||
const codeTypeMatch = /([\S]+)[\s\n]/.exec(state.code);
|
|
||||||
if (codeTypeMatch && codeTypeMatch.length > 1) {
|
if (codeTypeMatch && codeTypeMatch.length > 1) {
|
||||||
const docKey = codeTypeMatch[1];
|
const docKey = codeTypeMatch[1];
|
||||||
const docConfig = docMap[docKey] ?? { code: '' };
|
const docConfig = docMap[docKey] ?? { code: '' };
|
||||||
docURL = docURLBase + (docConfig[selectedMode] ?? docConfig.code ?? '');
|
docURL = docURLBase + (docConfig[editorMode] ?? docConfig.code ?? '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const tabSelectHandler = (message: CustomEvent<Tab>) => {
|
const tabSelectHandler = (message: CustomEvent<Tab>) => {
|
||||||
selectedMode = message.detail.id === 'code' ? 'code' : 'config';
|
const editorMode: EditorMode = message.detail.id === 'code' ? 'code' : 'config';
|
||||||
$inputStateStore.updateEditor = true;
|
updateCodeStore({ updateEditor: true, editorMode });
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: Tab[] = [
|
const tabs: Tab[] = [
|
||||||
{
|
{
|
||||||
id: 'code',
|
id: 'code',
|
||||||
@@ -101,28 +82,6 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleUpdate = (text: string) => {
|
|
||||||
if (selectedMode === 'code') {
|
|
||||||
updateCode(text, {
|
|
||||||
updateEditor: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
updateConfig(text, false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let debounce: { [key: string]: number } = {};
|
|
||||||
const updateHandler = ({ detail: { text } }: CustomEvent<EditorUpdateEvent>) => {
|
|
||||||
if (debounceEnabled) {
|
|
||||||
clearTimeout(debounce[selectedMode]);
|
|
||||||
debounce[selectedMode] = window.setTimeout(() => {
|
|
||||||
handleUpdate(text);
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
handleUpdate(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await initHandler();
|
await initHandler();
|
||||||
const resizer = document.getElementById('resizeHandler');
|
const resizer = document.getElementById('resizeHandler');
|
||||||
@@ -149,7 +108,7 @@
|
|||||||
<Navbar />
|
<Navbar />
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<div class="hidden md:flex flex-col" id="editorPane" style="width: 40%">
|
<div class="hidden md:flex flex-col" id="editorPane" style="width: 40%">
|
||||||
<Card on:select={tabSelectHandler} {tabs} isCloseable={false} title="Mermaid">
|
<Card on:select={tabSelectHandler} {tabs} isCloseable={false} {activeTabID} title="Mermaid">
|
||||||
<div slot="actions" class="flex flex-row items-center">
|
<div slot="actions" class="flex flex-row items-center">
|
||||||
<div class="form-control flex-row items-center">
|
<div class="form-control flex-row items-center">
|
||||||
<label class="cursor-pointer label" for="autoSync">
|
<label class="cursor-pointer label" for="autoSync">
|
||||||
@@ -175,7 +134,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Editor on:update={updateHandler} {language} {text} />
|
<Editor />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div class="-mt-2">
|
<div class="-mt-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user