Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 3 additions & 1 deletion src/application/services/useNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt
/**
* List of tools used in the note
* Undefined when note is not loaded yet
* Empty array for drafts since they have no note tools
*/
const noteTools = ref<EditorTool[] | undefined>(undefined);
const noteTools = ref<EditorTool[] | undefined>(currentId.value === null ? [] : undefined);

/**
* Router instance used to replace the current route with note id
Expand Down Expand Up @@ -334,6 +335,7 @@ export default function (options: UseNoteComposableOptions): UseNoteComposableSt
*/
function resetNote(): void {
note.value = createDraft();
noteTools.value = [];
canEdit.value = true;
lastUpdateContent.value = null;
noteHierarchy.value = null;
Expand Down
87 changes: 62 additions & 25 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 All @@ -17,12 +17,6 @@ interface UseNoteEditorOptions {
*/
noteContentResolver: () => NoteContent | undefined;

/**
* Function to check if the note is a draft
* In draft we wont wait for note tools loading
*/
isDraftResolver: () => boolean;

/**
* Flag indicating that user can edit the note
*/
Expand Down Expand Up @@ -61,51 +55,72 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption

/**
* Reactive object with editor tools installed by user
* User is undefined while authorization is in progress,
* null when user is not authenticated, User instance otherwise
*/
const { userEditorTools } = useAppState();
const { userEditorTools, user } = useAppState();

/**
* 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
* Returns undefined when tools are not loaded yet to prevent
* premature editor rendering with an incomplete tools set
*/
const noteAndUserTools = computed<EditorTool[] | undefined>(() => {
const isDraft = options.isDraftResolver();
const noteTools = isDraft ? [] : toValue(options.noteTools);
const userTools = toValue(userEditorTools) ?? [];
const noteTools = toValue(options.noteTools);
const userTools = toValue(userEditorTools);
const currentUser = toValue(user);

/**
* If tools are not loaded yet, return undefined
* If note tools are not loaded yet, return undefined to prevent
* premature editor rendering
*/
if (noteTools === undefined) {
return undefined;
}

/**
* If user is authenticated but their tools are not loaded yet, wait for them to load
* When user is not authenticated userTools stays undefined
*/
if (currentUser !== null && userTools === undefined) {
return undefined;
}

/**
* Return unique array of tools grouped by tool.name
*/
const combinedTools = [...noteTools, ...userTools];
const combinedTools = [...noteTools, ...(userTools ?? [])];
const uniqueTools = new Map(combinedTools.map(tool => [tool.name, tool]));

return Array.from(uniqueTools.values());
});

/**
* 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 +129,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 +139,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 +153,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
1 change: 0 additions & 1 deletion src/presentation/pages/HistoryVersion.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ const canEdit = ref(false);

const { isEditorReady, editorConfig } = useNoteEditor({
noteTools: historyTools,
isDraftResolver: () => false,
noteContentResolver: () => historyContent.value,
canEdit,
});
Expand Down
1 change: 0 additions & 1 deletion src/presentation/pages/Note.vue
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ const { updateCover } = useNoteSettings();

const { isEditorReady, editorConfig } = useNoteEditor({
noteTools,
isDraftResolver: () => noteId.value === null,
noteContentResolver: () => note.value?.content,
canEdit,
});
Expand Down