Browse Source

feat: 封装ACE组件

master
wangxiang 1 year ago
parent
commit
c5182e42d8
  1. 1
      package.json
  2. 6
      pnpm-lock.yaml
  3. 6
      src/components/AceEditor/index.ts
  4. 192
      src/components/AceEditor/src/AceEditor.tsx
  5. 55
      src/components/AceEditor/src/ace-config.ts
  6. 12
      src/components/AceEditor/src/emits.ts
  7. 21
      src/components/AceEditor/src/props.ts
  8. 38
      src/components/AceEditor/src/types.ts
  9. 77
      src/components/AceEditor/src/useAceEditor.ts
  10. 2
      src/locales/lang/en/routes/demo.ts
  11. 2
      src/locales/lang/zh-CN/routes/demo.ts
  12. 20
      src/router/routes/demo/comp.ts
  13. 69
      src/views/demo/editor/ace/index.vue

1
package.json

@ -33,6 +33,7 @@
"@vueuse/core": "^9.1.1", "@vueuse/core": "^9.1.1",
"@vueuse/shared": "^9.1.1", "@vueuse/shared": "^9.1.1",
"@zxcvbn-ts/core": "^2.0.1", "@zxcvbn-ts/core": "^2.0.1",
"ace-builds": "^1.32.6",
"ant-design-vue": "^3.2.12", "ant-design-vue": "^3.2.12",
"axios": "^0.26.1", "axios": "^0.26.1",
"cropperjs": "^1.5.12", "cropperjs": "^1.5.12",

6
pnpm-lock.yaml

@ -31,6 +31,7 @@ specifiers:
'@vueuse/core': ^9.1.1 '@vueuse/core': ^9.1.1
'@vueuse/shared': ^9.1.1 '@vueuse/shared': ^9.1.1
'@zxcvbn-ts/core': ^2.0.1 '@zxcvbn-ts/core': ^2.0.1
ace-builds: ^1.32.6
ant-design-vue: ^3.2.12 ant-design-vue: ^3.2.12
autoprefixer: ^10.4.4 autoprefixer: ^10.4.4
axios: ^0.26.1 axios: ^0.26.1
@ -103,6 +104,7 @@ dependencies:
'@vueuse/core': 9.13.0_vue@3.3.4 '@vueuse/core': 9.13.0_vue@3.3.4
'@vueuse/shared': 9.13.0_vue@3.3.4 '@vueuse/shared': 9.13.0_vue@3.3.4
'@zxcvbn-ts/core': 2.2.1 '@zxcvbn-ts/core': 2.2.1
ace-builds: 1.32.6
ant-design-vue: 3.2.20_vue@3.3.4 ant-design-vue: 3.2.20_vue@3.3.4
axios: 0.26.1 axios: 0.26.1
cropperjs: 1.6.1 cropperjs: 1.6.1
@ -2480,6 +2482,10 @@ packages:
through: 2.3.8 through: 2.3.8
dev: true dev: true
/ace-builds/1.32.6:
resolution: {integrity: sha512-dO5BnyDOhCnznhOpILzXq4jqkbhRXxNkf3BuVTmyxGyRLrhddfdyk6xXgy+7A8LENrcYoFi/sIxMuH3qjNUN4w==}
dev: false
/acorn-jsx/5.3.2_acorn@8.10.0: /acorn-jsx/5.3.2_acorn@8.10.0:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:

6
src/components/AceEditor/index.ts

@ -0,0 +1,6 @@
import { withInstall } from '/@/utils';
import aceEditor from './src/AceEditor';
export * from './src/types';
export const AceEditor = withInstall(aceEditor);
export { useAceEditor } from './src/useAceEditor';

192
src/components/AceEditor/src/AceEditor.tsx

@ -0,0 +1,192 @@
import ace, { type Ace } from 'ace-builds';
import {
defineComponent,
markRaw,
watch,
reactive,
onBeforeUnmount,
onMounted,
ref,
unref,
computed,
nextTick
} from 'vue';
import ResizeObserver from 'resize-observer-polyfill';
import { basicEmits } from './emits';
import { basicProps } from './props';
import type { AceEditorState, AceEditorMethods, AceEditorProps } from './types';
import { deepMerge } from '/@/utils';
import { useAppStore } from '/@/store/modules/app';
import './ace-config';
export default defineComponent({
name: 'AceEditor',
props: basicProps,
emits: basicEmits,
setup(props, { emit, attrs }) {
const aceEditorElRef = ref();
const propsRef = ref<Partial<AceEditorProps>>();
const appStore = useAppStore();
const state = reactive<AceEditorState>({
_editor: undefined!,
_ro: undefined!,
_contentBackup: '',
_isSettingContent: false,
});
const getProps = computed((): Recordable => {
return {
...props,
...(unref(propsRef) as any),
};
});
onMounted(async () => {
await nextTick();
const editor = state._editor = markRaw(ace.edit(aceEditorElRef.value, {
placeholder: props.placeholder,
readOnly: props.readonly,
value: props.value,
mode: 'ace/mode/' + props.lang,
theme: 'ace/theme/' + props.theme,
wrap: props.wrap,
printMargin: props.printMargin,
useWorker: false,
minLines: props.minLines,
maxLines: props.maxLines,
...props.options,
}));
setTheme();
state._contentBackup = props.value;
state._isSettingContent = false;
editor.on('change', () => {
// ref: https://github.com/CarterLi/vue3-ace-editor/issues/11
if (state._isSettingContent) return;
const content = editor.getValue();
state._contentBackup = content;
emit('update:value', content);
});
// Event Binding
editor.on('blur', (e: Event) => emit('blur', e));
editor.on('input', () => emit('input'));
editor.on('changeSelectionStyle', (obj: { data: string }) => emit('changeSelectionStyle', obj));
editor.on('changeSession', (obj: { session: Ace.EditSession, oldSession: Ace.EditSession }) => emit('changeSelectionStyle', obj));
editor.on('copy', (obj: { text: string }) => emit('copy', obj));
editor.on('focus', (e: Event) => emit('focus', e));
editor.on('paste', (obj: { text: string }) => emit('paste', obj));
state._ro = new ResizeObserver(() => editor.resize());
state._ro.observe(aceEditorElRef.value);
emit('register', aceEditorMethods);
});
onBeforeUnmount(() => {
state._ro?.disconnect();
state._editor?.destroy();
});
watch(() => appStore.getDarkMode, async () => {
setTheme();
},
{
immediate: true,
},
);
watch(() => unref(getProps).value, (val) => {
if (state._contentBackup !== val) {
try {
state._isSettingContent = true;
state._editor.setValue(val, 1);
} finally {
state._isSettingContent = false;
}
state._contentBackup = val;
}
});
watch(() => unref(getProps).theme, (val) => {
state._editor.setTheme('ace/theme/' + val);
});
watch(() => unref(getProps).options, (val: Partial<Ace.EditorOptions>) => {
state._editor.setOptions(val);
});
watch(() => unref(getProps).readonly, (val) => {
state._editor.setReadOnly(val);
});
watch(() => unref(getProps).placeholder, (val) => {
state._editor.setOption('placeholder', val);
});
watch(() => unref(getProps).wrap, (val) => {
state._editor.setWrapBehavioursEnabled(val);
});
watch(() => unref(getProps).printMargin, (val) => {
state._editor.setOption('printMargin', val);
});
watch(() => unref(getProps).lang, (val) => {
state._editor.setOption('mode', 'ace/mode/' + val);
});
watch(() => unref(getProps).minLines, (val) => {
state._editor.setOption('minLines', val);
});
watch(() => unref(getProps).maxLines, (val) => {
state._editor.setOption('maxLines', val);
});
function setTheme() {
const theme = appStore.getDarkMode === 'light' ? 'chrome' : 'monokai';
state._editor?.setTheme('ace/theme/' + theme);
}
function setProps(props: Partial<AceEditorProps>) {
propsRef.value = deepMerge(unref(propsRef) || {}, props);
}
function focus() {
state._editor.focus();
}
function blur() {
state._editor.blur();
}
function selectAll() {
state._editor.selectAll();
}
function getAceInstance(): Ace.Editor {
return state._editor;
}
const aceEditorMethods: AceEditorMethods = {
setProps,
focus,
blur,
selectAll,
getAceInstance
};
return {
aceEditorElRef,
setProps,
focus,
blur,
selectAll,
getAceInstance,
};
},
render() {
return (
<div class="!h-full w-full overflow-hidden" ref="aceEditorElRef" { ...this.$attrs }></div>
);
}
});

55
src/components/AceEditor/src/ace-config.ts

@ -0,0 +1,55 @@
import ace from 'ace-builds';
import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
ace.config.setModuleUrl('ace/mode/json', modeJsonUrl);
import modeJavascriptUrl from 'ace-builds/src-noconflict/mode-javascript?url';
ace.config.setModuleUrl('ace/mode/javascript', modeJavascriptUrl);
import modeHtmlUrl from 'ace-builds/src-noconflict/mode-html?url';
ace.config.setModuleUrl('ace/mode/html', modeHtmlUrl);
import modeYamlUrl from 'ace-builds/src-noconflict/mode-yaml?url';
ace.config.setModuleUrl('ace/mode/yaml', modeYamlUrl);
import themeGithubUrl from 'ace-builds/src-noconflict/theme-github?url';
ace.config.setModuleUrl('ace/theme/github', themeGithubUrl);
import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url';
ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl);
import themeMonokaiUrl from 'ace-builds/src-noconflict/theme-monokai?url';
ace.config.setModuleUrl('ace/theme/monokai', themeMonokaiUrl);
import themeDraculaUrl from 'ace-builds/src-noconflict/theme-dracula?url';
ace.config.setModuleUrl('ace/theme/dracula', themeDraculaUrl);
import workerBaseUrl from 'ace-builds/src-noconflict/worker-base?url';
ace.config.setModuleUrl('ace/mode/base', workerBaseUrl);
import workerJsonUrl from 'ace-builds/src-noconflict/worker-json?url';
ace.config.setModuleUrl('ace/mode/json_worker', workerJsonUrl);
import workerJavascriptUrl from 'ace-builds/src-noconflict/worker-javascript?url';
ace.config.setModuleUrl('ace/mode/javascript_worker', workerJavascriptUrl);
import workerHtmlUrl from 'ace-builds/src-noconflict/worker-html?url';
ace.config.setModuleUrl('ace/mode/html_worker', workerHtmlUrl);
import workerYamlUrl from 'ace-builds/src-noconflict/worker-yaml?url';
ace.config.setModuleUrl('ace/mode/yaml_worker', workerYamlUrl);
import snippetsHtmlUrl from 'ace-builds/src-noconflict/snippets/html?url';
ace.config.setModuleUrl('ace/snippets/html', snippetsHtmlUrl);
import snippetsJsUrl from 'ace-builds/src-noconflict/snippets/javascript?url';
ace.config.setModuleUrl('ace/snippets/javascript', snippetsJsUrl);
import snippetsYamlUrl from 'ace-builds/src-noconflict/snippets/yaml?url';
ace.config.setModuleUrl('ace/snippets/javascript', snippetsYamlUrl);
import snippetsJsonUrl from 'ace-builds/src-noconflict/snippets/json?url';
ace.config.setModuleUrl('ace/snippets/json', snippetsJsonUrl);
import 'ace-builds/src-noconflict/ext-language_tools';
ace.require('ace/ext/language_tools');

12
src/components/AceEditor/src/emits.ts

@ -0,0 +1,12 @@
export const basicEmits = [
'register',
'update:value',
'blur',
'input',
'change',
'changeSelectionStyle',
'changeSession',
'copy',
'focus',
'paste',
];

21
src/components/AceEditor/src/props.ts

@ -0,0 +1,21 @@
import { propTypes } from '/@/utils/propTypes';
import type { PropType } from 'vue';
import { Ace } from 'ace-builds';
export const basicProps = {
value: propTypes.string.isRequired,
lang: propTypes.string.def('text'),
theme: propTypes.string.def('chrome'),
options: {
type: Object as PropType<Ace.EditorOptions>,
},
placeholder: propTypes.string.def(''),
readonly: propTypes.bool,
wrap: propTypes.bool,
printMargin: {
type: [Boolean, Number] as PropType<boolean | number>,
default: true,
},
minLines: propTypes.number,
maxLines: propTypes.number,
};

38
src/components/AceEditor/src/types.ts

@ -0,0 +1,38 @@
import type { Ace } from 'ace-builds';
export interface AceEditorPrivate {
_editor: Ace.Editor;
_ro: ResizeObserver;
_contentBackup: string;
_isSettingContent: boolean;
}
export interface AceEditorState extends AceEditorPrivate {
[key: string]: any;
}
export interface AceEditorProps {
value: string;
lang: string;
theme: string;
options: Partial<Ace.EditorOptions>;
placeholder: string;
readonly: boolean;
wrap: boolean;
printMargin: boolean | number;
minLines: number;
maxLines: number;
}
export interface AceEditorMethods {
focus: () => void;
blur: () => void;
selectAll: () => void;
setProps: (props: Partial<AceEditorProps>) => void;
getAceInstance: () => Ace.Editor;
}
export interface AceEditorInstance extends
AceEditorPrivate,
AceEditorProps,
AceEditorMethods {}

77
src/components/AceEditor/src/useAceEditor.ts

@ -0,0 +1,77 @@
import { AceEditorProps, AceEditorMethods, AceEditorInstance } from '/@/components/AceEditor/src/types';
import { onUnmounted, ref, unref, watch, type WatchStopHandle } from 'vue';
import { isProdMode } from '/@/utils/env';
import { getDynamicProps } from '/@/utils';
import { error } from '/@/utils/log';
import type { DynamicProps } from '/#/utils';
type Props = Partial<DynamicProps<AceEditorProps>>;
export function useAceEditor(aceEditorProps?: Props): [
(instance: AceEditorInstance) => void,
AceEditorMethods
] {
const aceEditorRef = ref<Nullable<AceEditorInstance>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
let stopWatch: WatchStopHandle;
async function register(instance: AceEditorInstance) {
isProdMode() &&
onUnmounted(() => {
aceEditorRef.value = null;
loadedRef.value = null;
});
// 防止同一个组件重复注册
if (unref(loadedRef) && isProdMode() && instance === unref(aceEditorRef)) return;
aceEditorRef.value = instance;
aceEditorProps && instance.setProps(getDynamicProps(aceEditorProps));
loadedRef.value = true;
stopWatch?.();
stopWatch = watch(
() => aceEditorProps,
() => {
aceEditorProps && instance.setProps(getDynamicProps(aceEditorProps));
},
{
immediate: true,
deep: true,
},
);
}
function getAceEditorInstance(): AceEditorInstance {
const aceEditor = unref(aceEditorRef);
if (!aceEditor) {
error(
'The AceEditor instance has not been obtained yet, please make sure the AceEditor is presented when performing the AceEditor operation!',
);
}
return aceEditor as AceEditorInstance;
}
const methods: AceEditorMethods = {
focus: () => {
getAceEditorInstance().focus();
},
blur: () => {
getAceEditorInstance().blur();
},
selectAll: () => {
getAceEditorInstance().selectAll();
},
setProps: (props: Partial<AceEditorProps>) => {
getAceEditorInstance().setProps(props);
},
getAceInstance: () => {
return getAceEditorInstance().getAceInstance();
}
};
return [register, methods];
}

2
src/locales/lang/en/routes/demo.ts

@ -51,6 +51,8 @@ export default {
editor: 'Editor', editor: 'Editor',
jsonEditor: 'Json editor', jsonEditor: 'Json editor',
markdown: 'Markdown editor', markdown: 'Markdown editor',
aceEditor: 'Ace editor',
aceEditorBasic: 'Ace Basic',
tinymce: 'Rich text', tinymce: 'Rich text',
tinymceBasic: 'Basic', tinymceBasic: 'Basic',

2
src/locales/lang/zh-CN/routes/demo.ts

@ -50,6 +50,8 @@ export default {
editor: '编辑器', editor: '编辑器',
jsonEditor: 'Json编辑器', jsonEditor: 'Json编辑器',
markdown: 'markdown编辑器', markdown: 'markdown编辑器',
aceEditor: 'Ace编辑器',
aceEditorBasic: 'ACE基础使用',
tinymce: '富文本', tinymce: '富文本',
tinymceBasic: '基础使用', tinymceBasic: '基础使用',

20
src/router/routes/demo/comp.ts

@ -335,6 +335,26 @@ const comp: AppRouteModule = {
title: t('routes.demo.editor.editor'), title: t('routes.demo.editor.editor'),
}, },
children: [ children: [
{
path: 'aceEditor',
component: getParentLayout('AceEditorDemo'),
name: 'AceEditorDemo',
meta: {
title: t('routes.demo.editor.aceEditor'),
},
redirect: '/comp/editor/aceEditor/index',
children: [
{
path: 'index',
name: 'AceEditorBasicDemo',
component: () => import('/@/views/demo/editor/ace/index.vue'),
meta: {
title: t('routes.demo.editor.aceEditorBasic'),
},
},
],
},
{ {
path: 'markdown', path: 'markdown',
component: getParentLayout('MarkdownDemo'), component: getParentLayout('MarkdownDemo'),

69
src/views/demo/editor/ace/index.vue

@ -0,0 +1,69 @@
<template>
<PageWrapper title="AceEditor组件示例">
<a-select v-model:value="states.lang" class="mb-2">
<a-select-option v-for="(lang, index) of langs" :key="index" :value="lang"> {{ lang }} </a-select-option>
</a-select>
<a-select v-model:value="states.theme" class="mb-2">
<a-select-option v-for="(theme, index) of themes" :key="index" :value="theme"> {{ theme }} </a-select-option>
</a-select>
<AceEditor v-model:value="states.content"
class="vue-ace-editor"
@register="registerAceEditor"
/>
</PageWrapper>
</template>
<script setup lang="ts">
import { reactive, watch, ref, onMounted } from 'vue';
import { AceEditor, useAceEditor } from '/@/components/AceEditor';
const langs = ['json', 'javascript', 'html', 'yaml'];
const themes = ['github', 'chrome', 'monokai', 'dracula'];
const states = reactive({
lang: 'yaml',
theme: 'github',
content: '',
});
const [
registerAceEditor,
{
setProps,
blur,
focus,
getAceInstance,
selectAll
},
] = useAceEditor({
lang: states.lang,
theme: states.theme,
options: {
useWorker: true,
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: true,
}
});
watch(
() => states.lang,
async lang => {
states.content = (
await {
json: import('../../../../../package.json?raw'),
javascript: import('/@/components/AceEditor/src/ace-config.js?raw'),
html: import('../../../../../index.html?raw'),
yaml: import('../../../../../pnpm-lock.yaml?raw'),
}[lang]
|| {}).default!;
},
{ immediate: true }
);
</script>
<style lang="less" scoped>
/*.vue-ace-editor {
font-size: 16px;
}*/
</style>
Loading…
Cancel
Save