代码编辑组件
- 文章说明
- 核心代码
- 运行演示
- 源码下载
文章说明
拖了很久,总算是自己写了一个简单的代码编辑组件,虽然还有不少的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.clipboardDataconst 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代码编辑
源码下载
代码编辑组件