Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: editor to services #85

Merged
merged 6 commits into from
Feb 23, 2024
Merged
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
114 changes: 114 additions & 0 deletions src/application/services/useEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { BlockTool } from '@editorjs/editorjs';
import Editor, { type OutputData, type API } from '@editorjs/editorjs';
// @ts-expect-error editor plugins have no types
import Header from '@editorjs/header';
import type { Ref } from 'vue';
import { onBeforeUnmount, onMounted } from 'vue';
import { useAppState } from './useAppState';
import type EditorTool from '@/domain/entities/EditorTool';
import { loadScript } from '@/infrastructure/utils/load-script';

/**
* Downloaded tools data structure
*/
type DownloadedTools = Record<string, BlockTool>;

/**
* UseEditor composable params
*/
interface UseEditorParams {
/**
* Host element id
*/
id: string;

/**
* Editor initial content
*/
content?: OutputData;

/**
* True if editor should not allow editing
*/
isReadOnly?: boolean;

/**
* Handles content change in Editor
*/
onChange?: (api: API) => void;
}


/**
* Handles Editor.js instance creation
*
* @param params - Editor.js params
*/
export function useEditor({ id, content, isReadOnly, onChange }: UseEditorParams): void {
/**
* Editor instance
*/
let editor: Editor | undefined;

/**
* User notes tools
*/
const { userEditorTools } = useAppState();

/**
* Download all the user tools and return a map to use in Editor.js constructor
*
* @param tools - tools data
*/
async function downloadTools(tools: Ref<EditorTool[]>): Promise<DownloadedTools> {
const downloadedTools: DownloadedTools = {};

for (const tool of tools.value) {
if (tool.source.cdn === undefined) {
continue;
}

await loadScript(tool.source.cdn);

downloadedTools[tool.name] = window[tool.exportName as keyof typeof window];
}

return downloadedTools;
}

/**
* Initializes editorjs instance
*/
async function mountEditor(): Promise<void> {
try {
const tools = await downloadTools(userEditorTools);

editor = new Editor({
holder: id,
data: content,
tools: {
header: Header,
...tools,
},
onChange,
readOnly: isReadOnly,
});

await editor.isReady;
} catch (e) {
console.error(e);
}
}

onMounted(() => {
void mountEditor();
});

/**
* Destroy editor instance after unmount
*/
onBeforeUnmount(() => {
editor?.destroy();
editor = undefined;
});
}
15 changes: 15 additions & 0 deletions src/infrastructure/utils/load-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Loads script by specified url
*
* @param src - script source url
*/
export function loadScript(src: string): Promise<Event> {
return new Promise(function (resolve, reject) {
const script = document.createElement('script');

script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
162 changes: 11 additions & 151 deletions src/presentation/components/editor/Editor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,22 @@
</template>

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import Editor, { type OutputData, type API } from '@editorjs/editorjs';

// @ts-expect-error: we need to rewrite plugins to TS to get their types
import Header from '@editorjs/header';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Image from '@editorjs/image';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import CodeTool from '@editorjs/code';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import List from '@editorjs/list';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Delimiter from '@editorjs/delimiter';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Table from '@editorjs/table';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Warning from '@editorjs/warning';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Checklist from '@editorjs/checklist';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import LinkTool from '@editorjs/link';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import RawTool from '@editorjs/raw';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Embed from '@editorjs/embed';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import InlineCode from '@editorjs/inline-code';
// // @ts-expect-error: we need to rewrite plugins to TS to get their types
// import Marker from '@editorjs/marker';

import EditorTool from '@/domain/entities/EditorTool';
import { useAppState } from '@/application/services/useAppState';


const { userEditorTools } = useAppState();


/**
* Load one tool at a time
*
* @param src - source path to tool
*/
function loadScript(src: string) {
return new Promise(function (resolve, reject) {
const editorToolScript = document.createElement('script');

editorToolScript.src = src;
editorToolScript.onload = resolve;
editorToolScript.onerror = reject;
document.head.appendChild(editorToolScript);
});
}
import { ref } from 'vue';
import { type OutputData, type API } from '@editorjs/editorjs';
import { useEditor } from '@/application/services/useEditor';

/**
* Define the props for the component
*/
const props = defineProps<{
content?: OutputData,
readOnly?: boolean,
content?: OutputData;
readOnly?: boolean;
}>();

const emit = defineEmits<{
'change': [data: OutputData],
change: [data: OutputData];
}>();

/**
* Editor.js instance
*/
const editor = ref<Editor | undefined>(undefined);

/**
* Attribute containing is-empty state.
* It is updated on every change of the editor
Expand Down Expand Up @@ -127,97 +73,11 @@ async function onChange(api: API): Promise<void> {
emit('change', data);
}

const isEditorMounted = ref(false);

const mountEditorOnce = async () => {
console.log('mount');
isEditorMounted.value = true;

Promise.allSettled(userEditorTools.value.map((spec: EditorTool) => {
if (!spec.source.cdn) {
return;
}

return loadScript(spec.source.cdn);
})).then(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const loadedTools: {[key: string]: any } = userEditorTools.value.reduce(
(acc, spec: EditorTool) => {
// @ts-expect-error: we need to rewrite plugins to TS to get their types
const windowPlugin = window[spec.exportName];

return {
...acc,
[spec.title]: windowPlugin,
};
},
{}
);


const editorInstance = new Editor({
/**
* Block Tools
*/
tools: {
header: {
class: Header,
config: {
placeholder: 'Title...',
},
},
// image: Image,
// code: CodeTool,
// list: List,
// delimiter: Delimiter,
// table: Table,
// warning: Warning,
// checklist: Checklist,
// linkTool: LinkTool,
// raw: RawTool,
// embed: Embed,

// /**
// * Inline Tools
// */
// inlineCode: InlineCode,
// marker: Marker,
...loadedTools,
},
data: props.content,
onChange,
readOnly: props.readOnly,
});

await editorInstance.isReady;

editor.value = editorInstance;
});
};

watch(userEditorTools, mountEditorOnce);
onMounted(() => {
console.log('mount', userEditorTools.value);
if (userEditorTools.value.length > 0) {
mountEditorOnce();
}
});

watch(() => props.content, (content) => {
if (content === undefined) {
editor.value?.clear();

return;
}

if (editor.value) {
editor.value.render(content);
}
});

onBeforeUnmount(() => {
editor.value?.destroy();
editor.value = undefined;
useEditor({
id: 'editorjs',
content: props.content,
isReadOnly: props.readOnly,
onChange,
});

defineExpose({
Expand Down
Loading