feat: tagInput组件

Former-commit-id: 8329eb1c7566bba3081234ffae05d1e645751662
This commit is contained in:
april
2023-06-01 23:22:58 +08:00
parent 44a1e3ae1c
commit 7ad9a9a476
7 changed files with 906 additions and 10 deletions

View File

@@ -0,0 +1,493 @@
<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";
// import { isFunction } from "lodash-es";
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 && isFunction(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>

View File

@@ -0,0 +1,34 @@
/**
* getRePosFromStr 正则匹配字段返回位置信息
* */
export function getRePosFromStr(text: any = '', re: any = /\$.+?\$/g) {
const lines = text.split('\n')
const positions: any = []
let m
for (let i = 0; i < lines.length; i++) {
const l = lines[i]
while ((m = re.exec(l)) !== null) {
var tag = m[0].substring(1, m[0].length - 1)
positions.push({
line: i,
start: m.index,
stop: m.index + m[0].length,
tag,
})
}
}
return positions
}
/**
* 输入框模式
*/
export enum MODE {
// 文本
TEXT = 1,
// 公式
FORMULA,
// 只允许选择tag
ONLYTAG,
// 日期
DATE
}