Skip to content

Instantly share code, notes, and snippets.

@icai
Created January 26, 2025 03:06
Show Gist options
  • Select an option

  • Save icai/7cdb9a61fe10dfaa174e1ee824c56e45 to your computer and use it in GitHub Desktop.

Select an option

Save icai/7cdb9a61fe10dfaa174e1ee824c56e45 to your computer and use it in GitHub Desktop.
simple code editor, only for view
<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, '&amp;$1;');
code = code
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
code = code.replace(/&lt;(\/?)(\w+)(\s*[^&]*?)&gt;/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 ? `&lt;/<span class="tag">${tag}</span>&gt;` : `&lt;<span class="tag">${tag}</span>${attributes}&gt;`;
});
code = code.replace(/&lt;!--(.*?)--&gt;/g, '<span class="comment">&lt;!--$1--&gt;</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