zhangjie 3 жил өмнө
parent
commit
fa4d808fb3

+ 227 - 0
src/features/OnlineExam/TextEdit.vue

@@ -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>