|
|
@@ -0,0 +1,134 @@
|
|
|
+import { type MaybeComputedElementRef, tryOnMounted, tryOnUnmounted, unrefElement, useElementSize } from '@vueuse/core';
|
|
|
+import { createEditor, createToolbar } from '@wangeditor/editor';
|
|
|
+import { getDefaultToolbarConfig } from './config';
|
|
|
+import { getElement, getResources, isEditorInstance, register, upload } from './tools';
|
|
|
+import type { EditorEmits, EditorImageElement, EditorInstance, EditorProps, EditorVideoElement, ToolbarInstance } from './types';
|
|
|
+
|
|
|
+export type EmitsToProps<T extends Record<string, any>> = {
|
|
|
+ [K in keyof T as `on${Capitalize<string & K>}`]?: T[K] extends [infer U] ? (event: U) => void : T[K] extends [...infer Args] ? (...args: Args) => void : never;
|
|
|
+};
|
|
|
+
|
|
|
+export function useEditor(target: MaybeComputedElementRef, props?: EditorProps, emits?: EmitsToProps<EditorEmits>) {
|
|
|
+ register();
|
|
|
+
|
|
|
+ const editor = shallowRef<EditorInstance>();
|
|
|
+ const toolbar = shallowRef<ToolbarInstance>();
|
|
|
+
|
|
|
+ const handle1 = useElementSize(target, { height: 0, width: 0 });
|
|
|
+ const handle2 = watch(
|
|
|
+ [editor, () => props?.defaultValue],
|
|
|
+ ([editor, html]) => {
|
|
|
+ if (!isEditorInstance(editor)) return;
|
|
|
+ const value = toValue(html);
|
|
|
+ if (value) editor.dangerouslyInsertHtml(value);
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+ );
|
|
|
+ const handle3 = watch(
|
|
|
+ [editor, () => props?.disable],
|
|
|
+ ([editor, disable = false]) => {
|
|
|
+ if (!isEditorInstance(editor)) return;
|
|
|
+ if (disable) editor.disable();
|
|
|
+ else editor.enable();
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+ );
|
|
|
+
|
|
|
+ const stop = () => {
|
|
|
+ handle1.stop();
|
|
|
+ handle2.stop();
|
|
|
+ handle3.stop();
|
|
|
+ };
|
|
|
+
|
|
|
+ const resources = new Set<string>();
|
|
|
+
|
|
|
+ tryOnMounted(() => {
|
|
|
+ const container = unrefElement(target);
|
|
|
+ if (container) {
|
|
|
+ const height = handle1.height.value;
|
|
|
+ if (container && height > 0 && /* 全屏 */ height < window.innerHeight) container.style.setProperty('--height', `${height}px`);
|
|
|
+
|
|
|
+ const querySelector = getElement.bind(null, container as HTMLElement);
|
|
|
+ editor.value = createEditor({
|
|
|
+ selector: querySelector('.content-container')!,
|
|
|
+ html: toValue(props?.defaultValue ?? ''),
|
|
|
+ config: {
|
|
|
+ placeholder: toValue(props?.placeholder ?? '请输入内容...'),
|
|
|
+ scroll: false,
|
|
|
+ MENU_CONF: {
|
|
|
+ insertImage: {
|
|
|
+ onInsertedImage(node: EditorImageElement | null) {
|
|
|
+ if (node?.src) resources.add(node.src);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ insertVideo: {
|
|
|
+ onInsertedVideo(node: EditorVideoElement | null) {
|
|
|
+ if (node?.src) resources.add(node.src);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ deleteVideo: {},
|
|
|
+ },
|
|
|
+ hoverbarKeys: {
|
|
|
+ text: {
|
|
|
+ menuKeys: ['headerSelect', '|', 'bold', 'italic', 'underline', 'through', 'sup', 'sub', 'color', 'bgColor', 'clearStyle'],
|
|
|
+ },
|
|
|
+ video: {
|
|
|
+ menuKeys: ['enter', 'editVideoSize', 'deleteVideo'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ onCreated(editor) {
|
|
|
+ emits?.onCreated?.call(editor, editor);
|
|
|
+ },
|
|
|
+ onChange(editor) {
|
|
|
+ const text = editor.getText();
|
|
|
+ const html = text ? editor.getHtml() : '';
|
|
|
+ emits?.onChange?.call(editor, editor, { html, text });
|
|
|
+ },
|
|
|
+ customAlert(message, type) {
|
|
|
+ editor.value?.emit('message', { type, content: message });
|
|
|
+ },
|
|
|
+ },
|
|
|
+ });
|
|
|
+ toolbar.value = createToolbar({
|
|
|
+ selector: querySelector('.toolbar-container')!,
|
|
|
+ editor: editor.value,
|
|
|
+ config: {
|
|
|
+ toolbarKeys: toValue(props?.toolbar) ?? getDefaultToolbarConfig(),
|
|
|
+ excludeKeys: [],
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ if (props?.uploadPicture) Object.assign(editor.value.getMenuConfig('uploadImage'), upload.call(editor.value, props.uploadPicture));
|
|
|
+ else toolbar.value?.getConfig().excludeKeys?.push('uploadImage');
|
|
|
+
|
|
|
+ if (props?.uploadVideo) Object.assign(editor.value.getMenuConfig('uploadVideo'), upload.call(editor.value, props.uploadVideo));
|
|
|
+ else toolbar.value?.getConfig().excludeKeys?.push('uploadVideo');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ tryOnUnmounted(() => {
|
|
|
+ stop();
|
|
|
+ editor.value?.destroy();
|
|
|
+ toolbar.value = void 0;
|
|
|
+ editor.value = void 0;
|
|
|
+ resources.clear();
|
|
|
+ });
|
|
|
+
|
|
|
+ function save() {
|
|
|
+ if (!editor.value) throw { message: `编辑器未初始化` };
|
|
|
+
|
|
|
+ const text = editor.value.getText();
|
|
|
+ if (!text?.trim().length) throw { message: `编辑器内容为空` };
|
|
|
+
|
|
|
+ const lapsedResources: string[] = [];
|
|
|
+ if (resources.size) {
|
|
|
+ const value = getResources.call(editor.value);
|
|
|
+ resources.forEach((r) => {
|
|
|
+ if (!value.includes(r)) lapsedResources.push(r);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const html = editor.value?.getHtml();
|
|
|
+ return { text, html, lapsedResources };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { editor, toolbar, save };
|
|
|
+}
|