文章说明
拖了很久,总算是自己写了一个简单的代码编辑组件,虽然还有不少的bug,真的很难写,在写的过程中感觉自己的前端技术根本不够用,好像总是方案不够好;目前写出了这个效果,等待后续学习别的现有产品,再慢慢补充
采用div的设置可编辑属性 contenteditable=“true”,然后结合 highlight的代码高亮,效果还不错;最开始相加的代码输入提示,以为不难实现,但是真的写起来,总是没有办法很好的实现那个输入面板的位置控制;索性就暂时不加输入提示,然后搜索功能,也是有不小的难点;目前仍然存在着一些bug待修复,但是可以先作为小demo试用一下
目前算是完成第一阶段,虽然组件的功能不是很完善,但是基本的代码高亮和搜索功能也有了,算是差强人意啦
核心代码
输入组件
<script setup>
import {onMounted, reactive} from "vue";
import Search from "@/components/Search.vue";
import hljs from 'highlight.js';
import "highlight.js/styles/idea.css";
import {useSearchStore} from "@/stores/search";
import {appendDom} from "@/utils";
const data = reactive({
lineNumber: [1],
language: "html"
});
const search = useSearchStore();
function refresh() {
const height = inputElem.scrollHeight;
const number = Math.ceil(height / 21);
if (number !== data.lineNumber.length) {
data.lineNumber = [];
for (let i = 0; i < number; i++) {
data.lineNumber.push(i + 1);
}
}
if (search.showSearch) {
search.search(false);
}
}
let leftLineNumberContainer;
let rightInputArea;
let inputElem;
function syncScroll(event) {
if (event.target === leftLineNumberContainer) {
rightInputArea.scrollTo({
top: event.target.scrollTop,
});
}
if (event.target === rightInputArea) {
leftLineNumberContainer.scrollTo({
top: event.target.scrollTop,
});
}
}
function inputFocus() {
inputElem.focus();
const range = document.createRange();
range.selectNodeContents(inputElem);
range.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
if (!leftLineNumberContainer) {
leftLineNumberContainer = document.getElementsByClassName("left-line-number-container")[0];
}
if (!rightInputArea) {
rightInputArea = document.getElementsByClassName("right-input-area")[0];
}
leftLineNumberContainer.scrollTo({
top: rightInputArea.scrollHeight,
});
rightInputArea.scrollTo({
top: rightInputArea.scrollHeight,
});
}
onMounted(() => {
inputElem = document.getElementsByClassName("input-elem")[0];
rightInputArea = document.getElementsByClassName("right-input-area")[0];
search.rightInputArea = rightInputArea;
search.inputElem = inputElem;
search.language = data.language;
search.refresh = refresh;
inputFocus();
});
function getPasteData(event) {
const clipData = event.clipboardData || window.clipboardData
const value = clipData.getData('text/plain');
let highlightedCode = hljs.highlight(value, {
language: data.language
}).value;
const container = document.createElement("span");
container.innerHTML = highlightedCode;
function wrapTextNodesInSpan(element) {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes[i];
if (child.nodeType === Node.TEXT_NODE) {
const span = document.createElement('span');
span.textContent = child.nodeValue;
element.insertBefore(span, child);
element.removeChild(child);
} else if (child.nodeType === Node.ELEMENT_NODE) {
wrapTextNodesInSpan(child);
}
}
}
wrapTextNodesInSpan(container);
appendDom(rightInputArea, container);
refresh();
}
function keydown(event) {
search.keydown(event);
if (event.key === "Tab") {
event.preventDefault();
appendContent(" ");
}
if (event.ctrlKey) {
return;
}
if (event.key === "Enter" || event.key === "Backspace" || event.key === "Delete") {
setTimeout(() => {
refresh();
}, 10);
return;
}
if (isAlphaNumeric(event.key)) {
event.preventDefault();
appendContent(event.key);
refresh();
}
}
function isAlphaNumeric(key) {
return /^[a-zA-Z0-9]$/.test(key);
}
function appendContent(content) {
const span = document.createElement("span");
span.textContent = content;
appendDom(rightInputArea, span, false);
}
</script>
<template>
<div class="editor-container">
<Search/>
<div style="display: flex" :style="{ height: search.showSearch ? 'calc(100% - 30px)' : '100%' }">
<div class="left-line-number-container" @scroll="syncScroll($event)">
<template v-for="(item, index) in data.lineNumber" :key="index">
<p>{{ item }}</p>
</template>
</div>
<div class="right-input-area" @scroll="syncScroll($event)" @click.self="inputFocus">
<div class="input-elem" :contenteditable="!search.showSearch" @keydown="keydown($event)"
@paste.prevent="getPasteData($event)"></div>
</div>
</div>
</div>
</template>
<style lang="scss">
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
.hljs-tag {
background-color: transparent !important;
}
.hljs-attribute, .hljs-number, .hljs-regexp, .hljs-link {
font-weight: normal;
color: #3931c5;
}
.hljs-section, .hljs-name, .hljs-literal, .hljs-keyword, .hljs-selector-tag, .hljs-type, .hljs-selector-id, .hljs-selector-class {
font-weight: normal;
color: #3931c5;
}
.editor-container {
width: 100vw;
height: 100vh;
background-color: #ffffff;
padding-left: 200px;
.left-line-number-container {
min-width: 60px;
background-color: #f2f2f2;
border: 1px solid #d4d4d4;
padding: 4px 4px 230px;
overflow: auto;
&::-webkit-scrollbar {
height: 0;
width: 0;
}
p {
width: fit-content;
color: #adadad;
font-size: 14px;
line-height: 1.5;
font-family: "JetBrains Mono", sans-serif;
word-spacing: 0.2rem;
text-align: right;
}
}
.right-input-area {
flex: 1;
border: 1px solid #d4d4d4;
border-left: none;
padding: 4px 4px 230px;
overflow: auto;
position: relative;
cursor: default;
#default-cursor {
width: 0;
height: 0;
display: inline-block;
}
&::-webkit-scrollbar {
height: 10px;
width: 10px;
}
&::-webkit-scrollbar-thumb {
background-color: #e2e2e2;
border-radius: 0;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
.input-elem {
border: none;
outline: none;
height: fit-content;
min-width: 100%;
font-size: 14px;
color: #080808;
min-height: 21px;
line-height: 21px;
font-family: "JetBrains Mono", sans-serif;
word-spacing: 0.2rem;
white-space: pre;
word-break: break-all;
&::selection {
background-color: #a6d2ff;
}
.highlight-item {
background-color: #ffe959;
}
.current-highlight-item {
background-color: #a6d2ff;
}
pre {
font-family: "JetBrains Mono", sans-serif;
&::selection {
background-color: #a6d2ff;
}
}
}
}
}
</style>
搜索组件
<script setup>
import {useSearchStore} from "@/stores/search";
const search = useSearchStore();
function changeCase() {
search.caseSelected = !search.caseSelected;
search.search();
}
function changeWord() {
search.wordSelected = !search.wordSelected;
search.search();
}
function close() {
search.currentIndex = 1;
search.showSearch = false;
search.recover();
}
function last() {
search.beginSearch = true;
if (search.currentIndex === 1) {
search.currentIndex = search.searchResult.length;
} else {
search.currentIndex--;
}
search.search();
}
function next() {
search.beginSearch = true;
if (search.currentIndex === search.searchResult.length) {
search.currentIndex = 1;
} else {
search.currentIndex++;
}
search.search();
}
</script>
<template>
<div v-show="search.showSearch" class="search-container" @click.stop>
🔍
<input v-model="search.searchText" @input="search.search" id="input"/>
<div :class="search.caseSelected ? ' active-case ' : ''" class="case" @click="changeCase">Cc
</div>
<div :class="search.wordSelected ? ' active-word ' : ''" class="word" @click="changeWord">W
</div>
<div class="result">
<template v-if="!search.beginSearch || search.searchResult.length === 0">{{ search.searchResult.length }}
results
</template>
<template v-if="search.beginSearch && search.searchResult.length > 0">{{
search.currentIndex
}}/{{ search.searchResult.length }}
</template>
</div>
<div class="last" @click="last">↑</div>
<div class="next" @click="next">↓</div>
<div class="close" @click="close">×</div>
</div>
</template>
<style lang="scss" scoped>
.search-container {
width: 100%;
height: 30px;
border: 1px solid #d1d1d1;
border-bottom: none;
display: flex;
align-items: center;
user-select: none;
input {
border: none;
outline: none;
width: 350px;
height: 28px;
margin: 10px;
}
.case, .word {
background-color: #ffffff;
color: #bfc5c8;
width: 25px;
height: 25px;
padding: 3px;
margin-right: 4px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
font-weight: 600;
font-family: "JetBrains Mono", sans-serif;
border-radius: 5px;
cursor: default;
&:hover {
background-color: #dfdfdf;
color: #899399;
}
}
.active-case, .active-word {
background-color: #dae4ed;
color: #40b6e0;
&:hover {
background-color: #dfdfdf;
color: #44b7e0;
}
}
.result {
font-size: 12px;
font-family: "JetBrains Mono", sans-serif;
margin: 0 20px;
width: 70px;
height: 28px;
display: flex;
justify-content: center;
align-items: center;
}
.last, .next {
width: 22px;
height: 22px;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: #6e6e6e;
&:hover {
background-color: #dfdfdf;
border-radius: 5px;
cursor: default;
}
}
.close {
margin-left: auto;
margin-right: 5px;
width: 22px;
height: 22px;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
color: #bec4c6;
&:hover {
background-color: #dfdfdf;
border-radius: 5px;
cursor: default;
}
}
}
</style>
运行演示
Java代码编辑
HTML代码编辑