Created
January 26, 2025 03:06
-
-
Save icai/7cdb9a61fe10dfaa174e1ee824c56e45 to your computer and use it in GitHub Desktop.
simple code editor, only for view
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <div class="code-editor" :style="{ width, height }"> | |
| <div class="line-numbers" ref="lineNumbersRef"> | |
| <span v-for="line in lineCount" :key="line">{{ line }}</span> | |
| </div> | |
| <pre | |
| ref="editableCodeRef" | |
| contenteditable="true" | |
| @input="handleInput" | |
| @scroll="syncScroll" | |
| @paste="handlePaste" | |
| placeholder="Enter your code here..." | |
| ><code v-html="highlightedCode"></code></pre> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref, watch, nextTick } from 'vue'; | |
| const props = defineProps({ | |
| modelValue: { | |
| type: String, | |
| default: '' | |
| }, | |
| width: { | |
| type: String, | |
| default: '100%' | |
| }, | |
| height: { | |
| type: String, | |
| default: '300px' | |
| } | |
| }); | |
| const emit = defineEmits(['update:modelValue']); | |
| const localCode = ref(props.modelValue); | |
| const highlightedCode = ref(''); | |
| const lineCount = ref(1); | |
| const editableCodeRef = ref(null); | |
| const lineNumbersRef = ref(null); | |
| const highlight = (code) => { | |
| code = code.replace(/&(lt|gt|amp|quot|apos);/g, '&$1;'); | |
| code = code | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| code = code.replace(/<(\/?)(\w+)(\s*[^&]*?)>/g, (match, p1, p2, p3) => { | |
| const tag = p2.toLowerCase(); | |
| const isClosing = p1 === '/'; | |
| const attributes = p3.replace(/(\w+)=(".*?"|'.*?'|\S+)/g, '<span class="attr">$1</span>=<span class="string">$2</span>'); | |
| return isClosing ? `</<span class="tag">${tag}</span>>` : `<<span class="tag">${tag}</span>${attributes}>`; | |
| }); | |
| code = code.replace(/<!--(.*?)-->/g, '<span class="comment"><!--$1--></span>'); | |
| code = code | |
| .replace(/\/\/(.*)/gm, '<span class="comment">//$1</span>') | |
| .replace(/('.*?')/gm, '<span class="string">$1</span>') | |
| .replace(/(\d+\.\d+)/gm, '<span class="number">$1</span>') | |
| .replace(/(\d+)/gm, '<span class="number">$1</span>') | |
| .replace(/\bnew *(\w+)/gm, '<span class="keyword">new</span> <span class="init">$1</span>') | |
| .replace(/\b(import|from|function|new|throw|return|var|if|else)\b/gm, '<span class="keyword">$1</span>'); | |
| return code; | |
| }; | |
| const saveCursorPosition = () => { | |
| const selection = window.getSelection(); | |
| return selection.rangeCount > 0 ? selection.getRangeAt(0) : null; | |
| }; | |
| const restoreCursorPosition = (range) => { | |
| if (range) { | |
| const selection = window.getSelection(); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| } | |
| }; | |
| const updateHighlight = (code) => { | |
| const range = saveCursorPosition(); | |
| highlightedCode.value = highlight(code); | |
| nextTick(() => { | |
| restoreCursorPosition(range); | |
| editableCodeRef.value.focus(); // 确保编辑器保持焦点 | |
| }); | |
| updateLineNumbers(code); | |
| }; | |
| const updateLineNumbers = (code) => { | |
| const lines = code.split('\n').length; | |
| lineCount.value = lines < 1 ? 1 : lines; | |
| }; | |
| const syncScroll = (event) => { | |
| const pre = editableCodeRef.value; | |
| const lineNumbers = lineNumbersRef.value; | |
| if (pre && lineNumbers) { | |
| lineNumbers.scrollTop = pre.scrollTop; | |
| } | |
| }; | |
| const handleInput = (event) => { | |
| const newCode = event.target.textContent; | |
| if (newCode !== localCode.value) { | |
| localCode.value = newCode; | |
| emit('update:modelValue', newCode); | |
| requestAnimationFrame(() => updateHighlight(newCode)); // 使用 requestAnimationFrame 优化性能 | |
| } | |
| }; | |
| const handlePaste = (event) => { | |
| event.preventDefault(); | |
| const pasteText = (event.clipboardData || window.clipboardData).getData('text/plain'); | |
| const filteredText = filterPastedContent(pasteText); | |
| const newCode = filteredText; | |
| localCode.value = newCode; | |
| emit('update:modelValue', newCode); | |
| requestAnimationFrame(() => updateHighlight(newCode)); // 使用 requestAnimationFrame 优化性能 | |
| }; | |
| const filterPastedContent = (text) => { | |
| return text; | |
| }; | |
| watch(() => props.modelValue, (newValue) => { | |
| if (newValue !== localCode.value) { | |
| localCode.value = newValue; | |
| if (editableCodeRef.value) { | |
| editableCodeRef.value.textContent = newValue; | |
| } | |
| requestAnimationFrame(() => updateHighlight(newValue)); // 使用 requestAnimationFrame 优化性能 | |
| } | |
| }, { immediate: true }); | |
| </script> | |
| <style scoped> | |
| .code-editor { | |
| position: relative; | |
| display: flex; | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| .line-numbers { | |
| width: 50px; | |
| padding: 10px 5px 10px 10px; | |
| text-align: right; | |
| background-color: #f5f5f5; | |
| border-right: 1px solid #ccc; | |
| overflow: hidden; | |
| user-select: none; | |
| } | |
| .line-numbers span { | |
| display: block; | |
| } | |
| pre { | |
| flex: 1; | |
| padding: 10px; | |
| margin: 0; | |
| color: #000; | |
| background: #f5f5f5; | |
| border: 1px solid #ccc; | |
| overflow: auto; | |
| outline: none; | |
| white-space: pre-wrap; | |
| } | |
| code { | |
| display: block; | |
| white-space: pre-wrap; | |
| } | |
| /* 自定义高亮样式 */ | |
| code:deep() .comment { color: #888; } | |
| code:deep() .init { color: #2F6FAD; } | |
| code:deep() .string { color: #5890AD; } | |
| code:deep() .keyword { color: #8A6343; } | |
| code:deep() .number { color: #2F6FAD; } | |
| code:deep() .tag { color: #2F6FAD; } | |
| code:deep() .attr { color: #8A6343; } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment