|
@@ -0,0 +1,227 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, watch } from "vue";
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ value: string;
|
|
|
+}>();
|
|
|
+const emit = defineEmits<{
|
|
|
+ (e: "update:value", value: string): void;
|
|
|
+ (e: "change", value: string): void;
|
|
|
+}>();
|
|
|
+
|
|
|
+let textValue = $ref("");
|
|
|
+const editContentRef: Element = $ref();
|
|
|
+let copyFragment: DocumentFragment = $ref();
|
|
|
+
|
|
|
+function clearDomScriptTag(s: string) {
|
|
|
+ const reg = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
|
|
|
+ s = s.replace(reg, "");
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+function toCopy() {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection) return;
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ if (range.collapsed) return;
|
|
|
+ copyFragment = range.cloneContents();
|
|
|
+}
|
|
|
+function toCut() {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection) return;
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ if (range.collapsed) return;
|
|
|
+ copyFragment = range.extractContents();
|
|
|
+ textInput();
|
|
|
+}
|
|
|
+function toPaste() {
|
|
|
+ if (!copyFragment) return;
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection) return;
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ range.deleteContents();
|
|
|
+ range.insertNode(copyFragment.cloneNode(true));
|
|
|
+ textInput();
|
|
|
+}
|
|
|
+/**
|
|
|
+ * 设置上标下标
|
|
|
+ * @param type 类型,上标(sup)或下标(sub)
|
|
|
+ * @param cancel 是否取消上标下标
|
|
|
+ */
|
|
|
+function toSupSub(type: "sub" | "sup", cancel: boolean) {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ if (!selection) return;
|
|
|
+ const range = selection.getRangeAt(0);
|
|
|
+ if (range.collapsed) return;
|
|
|
+
|
|
|
+ const selectFragment = range.cloneContents();
|
|
|
+ const editContent = selectFragment.textContent as string;
|
|
|
+ const fullFragment = range.commonAncestorContainer;
|
|
|
+
|
|
|
+ console.dir(fullFragment.parentNode);
|
|
|
+ if (
|
|
|
+ fullFragment.parentNode &&
|
|
|
+ ["SUP", "SUB"].includes(fullFragment.parentNode.nodeName)
|
|
|
+ ) {
|
|
|
+ const perNodeName = fullFragment.parentNode.nodeName;
|
|
|
+ const [startOffset, endOffset] = [range.startOffset, range.endOffset];
|
|
|
+ console.log(startOffset, endOffset);
|
|
|
+ const fullContent = fullFragment.parentNode.textContent as string;
|
|
|
+ range.selectNode(fullFragment.parentNode);
|
|
|
+ range.deleteContents();
|
|
|
+ // 后段
|
|
|
+ const nextContent = fullContent.substring(endOffset);
|
|
|
+ if (nextContent) {
|
|
|
+ const nextDom = document.createElement(perNodeName);
|
|
|
+ nextDom.appendChild(document.createTextNode(nextContent));
|
|
|
+ range.insertNode(nextDom);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 中段
|
|
|
+ let midDom = null;
|
|
|
+ if (cancel) {
|
|
|
+ midDom = document.createTextNode(editContent);
|
|
|
+ range.insertNode(midDom);
|
|
|
+ } else {
|
|
|
+ midDom = document.createElement(type);
|
|
|
+ midDom.appendChild(document.createTextNode(editContent));
|
|
|
+ range.insertNode(midDom);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 前段
|
|
|
+ const prevContent = fullContent.substring(0, startOffset);
|
|
|
+ if (prevContent) {
|
|
|
+ const prevDom = document.createElement(perNodeName);
|
|
|
+ prevDom.appendChild(document.createTextNode(prevContent));
|
|
|
+ range.insertNode(prevDom);
|
|
|
+ }
|
|
|
+ range.selectNode(midDom);
|
|
|
+ } else {
|
|
|
+ range.deleteContents();
|
|
|
+ if (cancel) {
|
|
|
+ const modDom = document.createTextNode(editContent);
|
|
|
+ range.insertNode(modDom);
|
|
|
+ } else {
|
|
|
+ const modDom = document.createElement(type);
|
|
|
+ modDom.appendChild(document.createTextNode(editContent));
|
|
|
+ range.insertNode(modDom);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ textInput();
|
|
|
+}
|
|
|
+
|
|
|
+/** 操作上标下标中间内容时,将编辑整个上标下标块 */
|
|
|
+// function toSupSub1(type: "sub" | "sup", cancel: boolean) {
|
|
|
+// const selection = window.getSelection();
|
|
|
+// if (!selection) return;
|
|
|
+// const range = selection.getRangeAt(0);
|
|
|
+// let selectFragment = range.cloneContents();
|
|
|
+// if (!selectFragment.textContent) return;
|
|
|
+
|
|
|
+// const fullFragment = range.commonAncestorContainer;
|
|
|
+// if (
|
|
|
+// fullFragment.parentNode &&
|
|
|
+// ["SUP", "SUB"].includes(fullFragment.parentNode.nodeName)
|
|
|
+// ) {
|
|
|
+// range.selectNode(fullFragment.parentNode);
|
|
|
+// }
|
|
|
+
|
|
|
+// selectFragment = range.cloneContents();
|
|
|
+// if (!selectFragment.textContent) return;
|
|
|
+
|
|
|
+// if (cancel) {
|
|
|
+// const modDom = document.createTextNode(selectFragment.textContent);
|
|
|
+// range.deleteContents();
|
|
|
+// range.insertNode(modDom);
|
|
|
+// } else {
|
|
|
+// const modDom = document.createElement(type);
|
|
|
+// modDom.appendChild(document.createTextNode(selectFragment.textContent));
|
|
|
+// range.deleteContents();
|
|
|
+// range.insertNode(modDom);
|
|
|
+// }
|
|
|
+// textInput();
|
|
|
+// }
|
|
|
+
|
|
|
+function textInput() {
|
|
|
+ let content = editContentRef.innerHTML;
|
|
|
+ content = clearDomScriptTag(content);
|
|
|
+ content = content.replace(/<sub><\/sub>/g, "");
|
|
|
+ content = content.replace(/<sup><\/sup>/g, "");
|
|
|
+ textValue = content;
|
|
|
+ emitInput(textValue);
|
|
|
+}
|
|
|
+function emitInput(val: string) {
|
|
|
+ emit("update:value", val);
|
|
|
+ emit("change", val);
|
|
|
+}
|
|
|
+
|
|
|
+function keydownEvent(e: KeyboardEvent) {
|
|
|
+ if (e.ctrlKey || e.metaKey || e.altKey) {
|
|
|
+ e.preventDefault();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => props.value,
|
|
|
+ (val) => {
|
|
|
+ if (val !== textValue) {
|
|
|
+ textValue = val;
|
|
|
+ editContentRef.innerHTML = props.value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+);
|
|
|
+onMounted(() => {
|
|
|
+ editContentRef.innerHTML = props.value;
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="text-edit">
|
|
|
+ <div class="edit-menu">
|
|
|
+ <n-button type="primary" size="small" @click="toCopy">复制</n-button>
|
|
|
+ <n-button type="primary" size="small" @click="toCut">剪切</n-button>
|
|
|
+ <n-button type="primary" size="small" @click="toPaste">粘贴</n-button>
|
|
|
+ <n-button type="primary" size="small" @click="toSupSub('sup', false)"
|
|
|
+ >上标</n-button
|
|
|
+ >
|
|
|
+ <n-button type="primary" size="small" @click="toSupSub('sup', true)"
|
|
|
+ >取消上标</n-button
|
|
|
+ >
|
|
|
+ <n-button type="primary" size="small" @click="toSupSub('sub', false)"
|
|
|
+ >下标</n-button
|
|
|
+ >
|
|
|
+ <n-button type="primary" size="small" @click="toSupSub('sub', true)"
|
|
|
+ >取消下标</n-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ ref="editContentRef"
|
|
|
+ class="edit-content"
|
|
|
+ :contenteditable="true"
|
|
|
+ ondragstart="return false"
|
|
|
+ ondrop="return false"
|
|
|
+ @keydown="keydownEvent($event)"
|
|
|
+ @input="textInput"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.edit-content {
|
|
|
+ outline: none;
|
|
|
+ border: 1px solid var(--app-color-border-dark);
|
|
|
+ border-radius: var(--app-border-radius);
|
|
|
+ padding: 10px;
|
|
|
+ line-height: 20px;
|
|
|
+ min-height: 100px;
|
|
|
+ max-height: 300px;
|
|
|
+ margin-top: 10px;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+.edit-content:focus {
|
|
|
+ border-color: var(--app-color-primary);
|
|
|
+}
|
|
|
+</style>
|