13 changed files with 501 additions and 0 deletions
@ -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'; |
@ -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> |
||||||
|
); |
||||||
|
} |
||||||
|
}); |
@ -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'); |
@ -0,0 +1,12 @@ |
|||||||
|
export const basicEmits = [ |
||||||
|
'register', |
||||||
|
'update:value', |
||||||
|
'blur', |
||||||
|
'input', |
||||||
|
'change', |
||||||
|
'changeSelectionStyle', |
||||||
|
'changeSession', |
||||||
|
'copy', |
||||||
|
'focus', |
||||||
|
'paste', |
||||||
|
]; |
@ -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, |
||||||
|
}; |
@ -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 {} |
@ -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]; |
||||||
|
} |
@ -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…
Reference in new issue