|
@@ -0,0 +1,492 @@
|
|
|
+<template>
|
|
|
+ <div :class="['tagInputarea', className]">
|
|
|
+ <div
|
|
|
+ ref="cmEle"
|
|
|
+ :class="[
|
|
|
+ 'tagInputareaIuput',
|
|
|
+ 'ThemeBorderColor3',
|
|
|
+ !!readonly ? 'readonlyBg' : '',
|
|
|
+ ]"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+<script lang="ts" setup>
|
|
|
+import CodeMirror from "codemirror";
|
|
|
+import "codemirror/lib/codemirror.css";
|
|
|
+import { getRePosFromStr, MODE } from "./util";
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ mode: {
|
|
|
+ type: Number,
|
|
|
+ default: MODE.TEXT,
|
|
|
+ },
|
|
|
+ className: {
|
|
|
+ type: String,
|
|
|
+ default: "",
|
|
|
+ },
|
|
|
+ defaultValue: {
|
|
|
+ type: String,
|
|
|
+ default: "",
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ renderTag: {
|
|
|
+ type: Function,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ minHeight: {
|
|
|
+ type: Number,
|
|
|
+ default: 20,
|
|
|
+ },
|
|
|
+ readonly: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ noCursor: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ operatorsSetMargin: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ autoComma: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ // 是否禁用复制
|
|
|
+ disabledCopy: {
|
|
|
+ type: Boolean,
|
|
|
+ required: false,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+});
|
|
|
+const emit = defineEmits(["onChange", "onBlur", "onFocus"]);
|
|
|
+//编辑器挂载dom节点
|
|
|
+const cmEle = ref();
|
|
|
+
|
|
|
+//编辑器实例
|
|
|
+let cmInstance: any = null;
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ if (cmEle.value) {
|
|
|
+ //编辑器初始化
|
|
|
+ cmInstance = CodeMirror(cmEle.value, {
|
|
|
+ value: props.defaultValue,
|
|
|
+ mode: "",
|
|
|
+ lineWrapping: true,
|
|
|
+ cursorHeight: props.noCursor || props.readonly ? 0 : 1,
|
|
|
+ });
|
|
|
+ //最小高度初始化
|
|
|
+ if (props.minHeight) {
|
|
|
+ let height =
|
|
|
+ typeof props.minHeight === "number"
|
|
|
+ ? `${props.minHeight}px`
|
|
|
+ : props.minHeight;
|
|
|
+ //编辑器高度
|
|
|
+ cmInstance.setSize("100%", height);
|
|
|
+ //编辑内容高度
|
|
|
+ let codeEle = cmEle.value.getElementsByClassName("CodeMirror-code")[0];
|
|
|
+ codeEle && codeEle.setAttribute("style", `min-height:${height}`);
|
|
|
+ }
|
|
|
+ //默认值初始化
|
|
|
+ if (props.defaultValue) {
|
|
|
+ updateTextareaView();
|
|
|
+ cmInstance.execCommand("goDocEnd");
|
|
|
+ }
|
|
|
+ //监听 value change
|
|
|
+ cmInstance.on("change", cmChange);
|
|
|
+ //监听 value beforeChange
|
|
|
+ cmInstance.on("beforeChange", cmBeforeChange);
|
|
|
+ //监听表单聚焦
|
|
|
+ cmInstance.on("focus", () => {
|
|
|
+ cmEle.value.classList.add("active");
|
|
|
+ cmFocus();
|
|
|
+ });
|
|
|
+
|
|
|
+ //监听表单失焦
|
|
|
+ cmInstance.on("blur", () => {
|
|
|
+ if (cmEle.value) {
|
|
|
+ cmEle.value.classList.remove("active");
|
|
|
+ }
|
|
|
+ cmBlur();
|
|
|
+ });
|
|
|
+ if (props.disabledCopy) {
|
|
|
+ // 禁止复制,防止tag复制的是id
|
|
|
+ cmInstance.on("copy", (cm: any, e: Event) => {
|
|
|
+ e.preventDefault();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+//编辑器change
|
|
|
+const cmChange = (cm: any, obj: any): void => {
|
|
|
+ let value = cm.getValue();
|
|
|
+ if (obj.origin !== "setValue") {
|
|
|
+ emit("onChange", null, value, obj);
|
|
|
+ }
|
|
|
+
|
|
|
+ updateTextareaView();
|
|
|
+};
|
|
|
+
|
|
|
+//编辑器beforeChange
|
|
|
+const cmBeforeChange = (cm: any, obj: any): any => {
|
|
|
+ let { text } = obj;
|
|
|
+ // 如果是自定义公式,只能允许数字+、-、*、/、( \ ) , .,大写字符
|
|
|
+ if (
|
|
|
+ props.mode === MODE.FORMULA &&
|
|
|
+ (obj.origin === "paste" ||
|
|
|
+ obj.origin === "+input" ||
|
|
|
+ obj.origin === "*compose")
|
|
|
+ ) {
|
|
|
+ text = text.map((t: string) =>
|
|
|
+ t.replace(/[^+\-*\/0-9/a-z/A-Z\(\)\,\.]/gm, "").toUpperCase()
|
|
|
+ );
|
|
|
+ // 最大输入10000字符
|
|
|
+ if (text.map((t: string) => t).join("").length > 10000) {
|
|
|
+ text = text
|
|
|
+ .map((t: string) => t)
|
|
|
+ .join("")
|
|
|
+ .slice(0, 10000)
|
|
|
+ .split(",");
|
|
|
+ obj.update(obj.from, obj.to, text);
|
|
|
+ obj.cancel();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (obj.origin === "undo" || obj.origin === "redo") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 事件内,mode只能从每次props取,不然取不到最新
|
|
|
+ if (
|
|
|
+ props.readonly ||
|
|
|
+ (props.mode === MODE.ONLYTAG &&
|
|
|
+ obj.origin !== "+delete" &&
|
|
|
+ obj.origin !== "inserttag" &&
|
|
|
+ obj.origin !== "setValue")
|
|
|
+ ) {
|
|
|
+ obj.cancel();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 事件内,mode只能从每次props取,不然取不到最新
|
|
|
+ if (
|
|
|
+ props.mode === MODE.FORMULA &&
|
|
|
+ obj.origin !== "+delete" &&
|
|
|
+ obj.origin !== "inserttag" &&
|
|
|
+ obj.origin !== "setValue"
|
|
|
+ ) {
|
|
|
+ text = text.map((t: any) =>
|
|
|
+ t
|
|
|
+ .toUpperCase()
|
|
|
+ .split("")
|
|
|
+ .filter((t: any) =>
|
|
|
+ (obj.origin === "paste"
|
|
|
+ ? /[0-9A-Z\+\-\*\/\(\)\,\.\$]/
|
|
|
+ : /[0-9A-Z\+\-\*\/\(\)\,\.]/
|
|
|
+ ).test(t)
|
|
|
+ )
|
|
|
+ .join("")
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ props.mode === MODE.DATE &&
|
|
|
+ obj.origin !== "+delete" &&
|
|
|
+ obj.origin !== "inserttag" &&
|
|
|
+ obj.origin !== "setValue"
|
|
|
+ ) {
|
|
|
+ text = text.map((t: any) =>
|
|
|
+ t
|
|
|
+ .split("")
|
|
|
+ .filter((t: any) =>
|
|
|
+ (obj.origin === "paste" ? /[0-9YMdhm\+\-\$]/ : /[0-9YMdhm\+\-]/).test(
|
|
|
+ t
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .join("")
|
|
|
+ );
|
|
|
+ }
|
|
|
+ obj.update(obj.from, obj.to, text);
|
|
|
+};
|
|
|
+
|
|
|
+//编辑器 foucus
|
|
|
+const cmFocus = (): void => {
|
|
|
+ emit("onFocus");
|
|
|
+};
|
|
|
+
|
|
|
+//编辑器 blur
|
|
|
+const cmBlur = (): void => {
|
|
|
+ emit("onBlur");
|
|
|
+};
|
|
|
+
|
|
|
+//设置 value
|
|
|
+const setValue = (value: string): void => {
|
|
|
+ // const position = cmInstance.getCursor()
|
|
|
+ cmInstance.setValue(value || "");
|
|
|
+ // const newPosition = {
|
|
|
+ // line: position.line,
|
|
|
+ // ch: position.ch + value.length - 2,
|
|
|
+ // }
|
|
|
+
|
|
|
+ // cmInstance.focus()
|
|
|
+ // cmInstance.setCursor(newPosition)
|
|
|
+ // cmInstance.execCommand('goDocEnd')
|
|
|
+};
|
|
|
+const getValue = () => {
|
|
|
+ return cmInstance.getValue();
|
|
|
+};
|
|
|
+//回显,更新渲染
|
|
|
+let markers: any = null;
|
|
|
+const updateTextareaView = () => {
|
|
|
+ const { mode, operatorsSetMargin } = props;
|
|
|
+ const value = cmInstance.getValue();
|
|
|
+ if (markers) {
|
|
|
+ markers.forEach((marker: any) => marker.clear());
|
|
|
+ }
|
|
|
+ markers = [];
|
|
|
+ markColumns(markers, value);
|
|
|
+ if (mode === MODE.FORMULA || operatorsSetMargin) {
|
|
|
+ markOperators(markers, value);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+//光标行
|
|
|
+const markColumns = (markers: any, value: any) => {
|
|
|
+ const poss = getRePosFromStr(value);
|
|
|
+ poss.forEach((pos: any, i: number) => {
|
|
|
+ renderColumnTag(pos.tag, { isLast: i === poss.length - 1 }, (node: any) => {
|
|
|
+ markers.push(
|
|
|
+ cmInstance.markText(
|
|
|
+ { line: pos.line, ch: pos.start },
|
|
|
+ { line: pos.line, ch: pos.stop },
|
|
|
+ { replacedWith: node, handleMouseEvents: true }
|
|
|
+ )
|
|
|
+ );
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+//光标操作
|
|
|
+const markOperators = (markers: any, value: any) => {
|
|
|
+ const poss = getRePosFromStr(value, /\+|\-|\*|\/|\(|\)|,/g);
|
|
|
+ poss.forEach((pos: any, i: number) => {
|
|
|
+ const operatorEle = document.createElement("span");
|
|
|
+ operatorEle.classList.add("operator");
|
|
|
+ operatorEle.innerHTML = pos.tag;
|
|
|
+ markers.push(
|
|
|
+ cmInstance.markText(
|
|
|
+ { line: pos.line, ch: pos.start },
|
|
|
+ { line: pos.line, ch: pos.stop },
|
|
|
+ { replacedWith: operatorEle, handleMouseEvents: true }
|
|
|
+ )
|
|
|
+ );
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+//渲染 tag
|
|
|
+const renderColumnTag = (id: any, options: any, cb: any) => {
|
|
|
+ const node = document.createElement("div");
|
|
|
+ node.classList.add("columnTagCon");
|
|
|
+ //自定义渲染tag
|
|
|
+ if (props.renderTag) {
|
|
|
+ const tag = props.renderTag(id, options);
|
|
|
+ if (tag instanceof HTMLElement) {
|
|
|
+ node.appendChild(tag);
|
|
|
+ cb(node);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ node.append(id);
|
|
|
+ cb(node);
|
|
|
+ return;
|
|
|
+};
|
|
|
+
|
|
|
+//插入 tag
|
|
|
+const insertColumnTag = (id: string) => {
|
|
|
+ const { mode, autoComma } = props;
|
|
|
+ const position = cmInstance.getCursor();
|
|
|
+ const editorValue = cmInstance.getValue();
|
|
|
+
|
|
|
+ cmInstance.replaceRange(
|
|
|
+ `${
|
|
|
+ mode === MODE.FORMULA && autoComma && editorValue[position.ch - 1] === "$"
|
|
|
+ ? ","
|
|
|
+ : ""
|
|
|
+ }$${id}$`,
|
|
|
+ position,
|
|
|
+ undefined,
|
|
|
+ "inserttag"
|
|
|
+ );
|
|
|
+ cmInstance.focus();
|
|
|
+ cmInstance.execCommand("goDocEnd");
|
|
|
+ if (cmEle.value) {
|
|
|
+ cmEle.value.scrollTop = cmEle.value.scrollHeight - cmEle.value.clientHeight;
|
|
|
+ }
|
|
|
+};
|
|
|
+// 获取光标位置
|
|
|
+const getCursor = () => {
|
|
|
+ return cmInstance.getCursor();
|
|
|
+};
|
|
|
+// 设置光标位置
|
|
|
+const setCustomCursor = (position: { line: number; ch: number }) => {
|
|
|
+ cmInstance.setCursor(position);
|
|
|
+};
|
|
|
+// 插入公式
|
|
|
+const insertFormula = (value: string) => {
|
|
|
+ const { mode, autoComma } = props;
|
|
|
+ const position = cmInstance.getCursor();
|
|
|
+
|
|
|
+ cmInstance.replaceRange(value, position, undefined, "insertFormula");
|
|
|
+ setTimeout(() => {
|
|
|
+ const newPosition = {
|
|
|
+ line: position.line,
|
|
|
+ ch: position.ch + value.length - 2,
|
|
|
+ };
|
|
|
+
|
|
|
+ cmInstance.focus();
|
|
|
+ cmInstance.setCursor(newPosition);
|
|
|
+ // cmInstance.execCommand('goDocEnd')
|
|
|
+ }, 0);
|
|
|
+ if (cmEle.value) {
|
|
|
+ cmEle.value.scrollTop = cmEle.value.scrollHeight - cmEle.value.clientHeight;
|
|
|
+ }
|
|
|
+};
|
|
|
+defineExpose({
|
|
|
+ insertColumnTag,
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.tagInputarea {
|
|
|
+ min-width: 0;
|
|
|
+ :deep {
|
|
|
+ .tagInputareaIuput {
|
|
|
+ border-radius: 3px;
|
|
|
+ border: 1px solid #409eff;
|
|
|
+ overflow: auto;
|
|
|
+
|
|
|
+ &:not(.active) {
|
|
|
+ border-color: #ccc !important;
|
|
|
+ }
|
|
|
+ // &.hasRightIcon {
|
|
|
+ // border-radius: 3px 0 0 3px;
|
|
|
+ // }
|
|
|
+ &.readonlyBg {
|
|
|
+ .CodeMirror {
|
|
|
+ background-color: #eee !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // .rightIcon {
|
|
|
+ // background-color: #f5f5f5;
|
|
|
+ // font-size: 20px;
|
|
|
+ // color: #757575;
|
|
|
+ // height: 34px;
|
|
|
+ // line-height: 33px;
|
|
|
+ // padding: 0 7px;
|
|
|
+ // border: 1px solid #ccc;
|
|
|
+ // border-left: none;
|
|
|
+ // border-radius: 0 3px 3px 0;
|
|
|
+ // }
|
|
|
+ .CodeMirror {
|
|
|
+ font-family: inherit !important;
|
|
|
+ box-sizing: border-box;
|
|
|
+ height: auto !important;
|
|
|
+
|
|
|
+ .CodeMirror-vscrollbar {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ // .CodeMirror-scroll {
|
|
|
+ // height: auto;
|
|
|
+ // overflow-y: hidden;
|
|
|
+ // overflow-x: auto;
|
|
|
+ // }
|
|
|
+
|
|
|
+ .CodeMirror-lines {
|
|
|
+ padding: 6px 0;
|
|
|
+ min-height: 35px;
|
|
|
+ }
|
|
|
+ .CodeMirror-line {
|
|
|
+ padding: 0 10px;
|
|
|
+ }
|
|
|
+ .columnTagCon {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ // box-sizing: border-box;
|
|
|
+ // padding: 2px 4px;
|
|
|
+ // max-width: 100%;
|
|
|
+ // background: #d8eeff;
|
|
|
+ // color: #174c76;
|
|
|
+ // border: 1px solid #bbd6ea;
|
|
|
+ // border-radius: 5px;
|
|
|
+ }
|
|
|
+ .columnTag {
|
|
|
+ position: relative;
|
|
|
+ display: inline-flex;
|
|
|
+ box-sizing: border-box;
|
|
|
+ cursor: pointer;
|
|
|
+ height: 24px;
|
|
|
+ font-size: 12px;
|
|
|
+ border: 1px solid #90caf9;
|
|
|
+ border-radius: 24px;
|
|
|
+ .columnName,
|
|
|
+ .columnValue {
|
|
|
+ display: inline-block;
|
|
|
+ height: 22px;
|
|
|
+ line-height: 22px;
|
|
|
+ padding: 0 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ .columnName {
|
|
|
+ color: #2196f3;
|
|
|
+ background-color: rgba(33, 150, 243, 0.06);
|
|
|
+ }
|
|
|
+ .columnValue {
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 0 22px 22px 0;
|
|
|
+ background-color: #249eff;
|
|
|
+ .ellipsis {
|
|
|
+ max-width: 9em;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.onlytag {
|
|
|
+ margin-right: 6px;
|
|
|
+ &:after {
|
|
|
+ content: ",";
|
|
|
+ position: absolute;
|
|
|
+ right: -6px;
|
|
|
+ top: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.deleted {
|
|
|
+ border-color: #f44336;
|
|
|
+ .columnName {
|
|
|
+ color: #f44336;
|
|
|
+ background-color: rgba(244, 67, 54, 0.06);
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ border-color: #f44336;
|
|
|
+ .columnName {
|
|
|
+ color: #f44336;
|
|
|
+ background-color: rgba(244, 67, 54, 0.12);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &:hover {
|
|
|
+ border-color: #ddd;
|
|
|
+ .columnName {
|
|
|
+ color: #9e9e9e;
|
|
|
+ background-color: rgba(158, 158, 158, 0.06);
|
|
|
+ }
|
|
|
+ .columnValue {
|
|
|
+ background-color: #bdbdbd;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .operator {
|
|
|
+ margin: 0 4px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|