Skip to content
2 changes: 1 addition & 1 deletion @codexteam/ui/dev/pages/components/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
placeholder="Write something or press / to select a tool"
first-block-placeholder="Untitled"
autofocus
:inlineToolbar="true"
:inline-toolbar="true"
/>
</template>

Expand Down
2 changes: 1 addition & 1 deletion @codexteam/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@codexteam/ui",
"version": "0.2.3",
"version": "0.2.5",
"type": "module",
"sideEffects": [
"*.css",
Expand Down
3 changes: 1 addition & 2 deletions @codexteam/ui/src/vue/components/editor/useEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ export function useEditor(editorConfig: MaybeRefOrGetter<EditorConfig>, options:
* Destroy editor instance after unmount
*/
onBeforeUnmount(() => {
editor?.destroy();
editor = undefined;
destroyEditor();
});

return {
Expand Down
58 changes: 44 additions & 14 deletions src/application/services/useNoteEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useAppState } from './useAppState';
import type EditorTool from '@/domain/entities/EditorTool';
import { type NoteContent } from '@/domain/entities/Note';
import { editorToolsService } from '@/domain';
import type { EditorjsConfigTool } from '@/domain/entities/EditorTool';
import type { EditorjsToolsConfig } from '@/domain/entities/EditorTool';
import { useI18n } from 'vue-i18n';

interface UseNoteEditorOptions {
Expand Down Expand Up @@ -68,21 +68,27 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption
* Loaded tools classes by grouped by tool.name
* Undefined when tools are not loaded yet
*/
let toolsUserConfig: Record<string, { class: EditorjsConfigTool; inlineToolbar: boolean }> | undefined = undefined;
let toolsUserConfig: EditorjsToolsConfig | undefined = undefined;

/**
* We can't make toolsUserConfig reactive since it contains excecutable js-classes, Vue can't handle that.
* So we store reactive flag to indicate that tools are loaded
*/
const toolsUserConfigLoaded = ref<boolean>(false);

/**
* Incremented on each new load request to discard stale async results.
* Prevents race conditions when rapid note switching causes multiple
* concurrent loadToolsScripts invocations.
*/
let currentLoadId = 0;

/**
* Combine note and user tools
* Undefined when user or note is not loaded
*/
const noteAndUserTools = computed<EditorTool[] | undefined>(() => {
const isDraft = options.isDraftResolver();
const noteTools = isDraft ? [] : toValue(options.noteTools);
const noteTools = toValue(options.noteTools) || [];
const userTools = toValue(userEditorTools) ?? [];

/**
Expand All @@ -102,10 +108,12 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption
});

/**
* Downloads passed tools scripts and toggles-on the isEditorReady flag
* Downloads passed tools scripts and returns the loaded config object.
* Does not mutate shared state — the caller is responsible for applying the result
* @param toolsConfigs - tools to download
* @returns loaded tools config
*/
async function loadToolsScripts(toolsConfigs: EditorTool[]): Promise<void> {
async function loadToolsScripts(toolsConfigs: EditorTool[]): Promise<EditorjsToolsConfig> {
const loadedTools = await editorToolsService.getToolsLoaded(toolsConfigs);

/**
Expand All @@ -114,7 +122,7 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption
*/
const loadedToolsWithoutParagraph = loadedTools.filter(tool => tool.tool.name !== 'paragraph');

toolsUserConfig = Object.fromEntries(
return Object.fromEntries(
loadedToolsWithoutParagraph
.map(toolClassAndInfo => [
toolClassAndInfo.tool.name,
Expand All @@ -124,12 +132,6 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption
},
])
);
toolsUserConfigLoaded.value = true;

/**
* Now all tools are loaded, we're ready to use the editor
*/
isEditorReady.value = true;
}

/**
Expand All @@ -144,7 +146,35 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption
return;
}

await loadToolsScripts(tools);
const loadId = ++currentLoadId;

isEditorReady.value = false;
Comment thread
Reversean marked this conversation as resolved.
toolsUserConfigLoaded.value = false;

try {
const loadedConfig = await loadToolsScripts(tools);

/**
* If a newer load request has superseded this one — discard stale results
* to prevent overwriting state with tools from a previous note.
*/
if (loadId !== currentLoadId) {
return;
}

toolsUserConfig = loadedConfig;
toolsUserConfigLoaded.value = true;
} catch (error) {
throw new Error(`Failed to load tools scripts: ${error instanceof Error ? error.message : String(error)}`);
} finally {
/**
* Display the editor regardless of tool loading failures, as it can be displayed with default tools.
* Only the latest load request may mark the editor as ready
*/
if (loadId === currentLoadId) {
isEditorReady.value = true;
}
}
}, {
immediate: true, // load tools if they are passed to the composable immediately
});
Expand Down
5 changes: 5 additions & 0 deletions src/domain/entities/EditorTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export type NewToolData = Omit<EditorTool, 'userId' | 'id' | 'cover'> & {
*/
export type EditorjsConfigTool = ToolSettings | ToolConstructable;

/**
* Editor.js tools config — map of tool name to its class and inline toolbar flag
*/
export type EditorjsToolsConfig = Record<string, { class: EditorjsConfigTool; inlineToolbar: boolean }>;

/**
* Editor tool info alogn with its plugin's class ready to use
*/
Expand Down