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