vue textarea @ 艾特弹窗 和光标位置

在这里插入图片描述
能@xx删除,

代码

<template>
  <div>
    <div class="btncontainer">
      <textarea
        class="editor"
        ref="textarea"
        @keydown="handleKeyDown"
        v-model="text"
        @input.prevent="handleInput"
        @focus="handleFocus"
        @mouseup="handleMouseUp"
      ></textarea>
      <template v-if="showboard">
        <div
          class="board"
          :style="{
            position: 'absolute',
            left: position.x + 'px',
            top: position.y + 'px'
          }"
        >
          <div
            v-for="(item, index) in list"
            @click="handleItemClick(index, item)"
            :key="index"
          >
            {{ item.approvalManName }}
          </div>
        </div>
      </template>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import getCaretCoordinates from "@/utils/getCaretCoordinates"; //获取光标的位置,具体代码在下面

const text = ref();
const list = ref([
 {
      approvalManName: "张三",
      id: "202"
    },
    {
      approvalManName: "李四",
      id: "203"
    }]);
const showboard = ref(false);
const textlist = ref<any[]>([]);
const markdisable = ref(false);
const relationApprovalRemarksList = ref([]);
const markselectpeople = ref(false);
const operationindex = ref(-1);
let unwatch = () => {};
const position = ref({ x: 0, y: 0 });

function watchText() {
  unwatch = watch(
    () => text.value,
    (cv, ov) => {
      console.log(cv, ov);
      if (ov) {
        if (ov.length > cv.length) {
          let ovlist = [...ov];
          let cvlist = [...cv];
          let startremove = findDiffStart(cv, ov);
          let removestr = "";
          let difflength = ovlist.length - cvlist.length;
          for (let j = startremove; j <= startremove + difflength - 1; j++) {
            removestr += ovlist[j];
          }
          //console.log("对比结果", startremove, ov, cv, removestr);

          //console.log(removestr, "匹配器结果");
          let atnamelist = findAtNameList();
          //console.log("atnamelist", atnamelist, removestr, startremove);
          for (let j = 0; j < atnamelist.length; j++) {
            for (
              let k = atnamelist[j].startindex;
              k <= atnamelist[j].endindex;
              k++
            ) {
              if (k >= startremove && k <= startremove + removestr.length - 1) {
                atnamelist[j].remove = true;
              }
            }
          }
          let temp = [...ov];
          let tempstr = [...ov];
          let finalstr = "";
          let temptextlist = [...textlist.value];
          //console.log("temp", temp);
          for (let j = 0; j < temp.length; j++) {
            // 拿出@xxx并标记
            for (let k = 0; k < atnamelist.length; k++) {
              if (
                atnamelist[k].remove &&
                j >= atnamelist[k].startindex &&
                j <= atnamelist[k].endindex
              ) {
                // 使用ᑒ特殊符号进行标记
                tempstr[j] = "ᑒ";
                temptextlist[j] = "ᑒ";
              }
            }
            // 拿出正常删除的并标记
            if (j >= startremove && j <= startremove + removestr.length - 1) {
              tempstr[j] = "ᑒ";
              temptextlist[j] = "ᑒ";
            }
          }
          for (let j = 0; j < tempstr.length; j++) {
            if (tempstr[j] != "ᑒ") {
              finalstr += tempstr[j];
            }
          }
          textlist.value = [];
          for (let j = 0; j < temptextlist.length; j++) {
            if (temptextlist[j] != "ᑒ") {
              textlist.value.push(temptextlist[j]);
            }
          }
          if (finalstr !== ov) {
            //console.log("finalstr", finalstr);
            text.value = finalstr;
            //console.log("之后的textlist", textlist.value);
            // 重新赋值 textlist
            unwatch();
            setTimeout(() => {
              watchText();
            });
          } else {
            // 此时校验长度
          }

          //console.log(finalstr, "最终");
          markdisable.value = false;
        } else {
          if (markdisable.value) {
            text.value = ov;
            unwatch();
            watchText();
            return;
          }
          let startremove = findDiffForcvmoreOv(cv, ov);

          let removestr = "";
          let difflength = cv.length - ov.length;

          for (let j = startremove; j <= startremove + difflength - 1; j++) {
            removestr += cv[j];
          }
          //console.log("对比结果" + removestr);
          let beforelinelist = textlist.value.slice(0, startremove);
          let endlinelist = textlist.value.slice(startremove);
          let namelist = [...removestr];
          textlist.value = [...beforelinelist, ...namelist, ...endlinelist];
        }
      } else {
        if (markdisable.value) {
          text.value = ov;
          unwatch();
          watchText();
          return;
        }
        let startremove = findDiffForcvmoreOv(cv, ov);

        let removestr = "";

        let difflength = 0;

        if (ov) {
          difflength = cv.length - ov.length;
        } else {
          difflength = cv.length - 0;
        }

        for (let j = startremove; j <= startremove + difflength - 1; j++) {
          removestr += cv[j];
        }
        //console.log("对比结果" + removestr);
        let beforelinelist = textlist.value.slice(0, startremove);
        let endlinelist = textlist.value.slice(startremove);
        let namelist = [...removestr];
        textlist.value = [...beforelinelist, ...namelist, ...endlinelist];
      }
    }
  );
}
// 查找开始不同的index
function findDiffStart(cv, ov) {
  let str1 = ov;
  let str2 = cv;
  let str1list = [...str1];
  let str2list = [...str2];
  let thestartindex = null;
  for (let j = 0; j < str1list.length; j++) {
    let sliced = str1list.slice(j, j + str1.length - str2.length);
    let find = false;
    for (let k = 0; k < str2list.length; k++) {
      let beforestr = str2list.slice(0, j);
      let centerstr = sliced;
      let endstr = str2list.slice(j);
      //console.log([...beforestr, ...centerstr, ...endstr].join(""), "最终结果");
      if ([...beforestr, ...centerstr, ...endstr].join("") == str1) {
        find = true;

        break;
      }
    }
    if (find) {
      thestartindex = j;
      //console.log(j, "哈哈哈");
      break;
    }
  }
  return thestartindex;
}
// 当cv大于ov时不一样
function findDiffForcvmoreOv(cv, ov) {
  if (ov) {
    let shorter = ov;
    let longer = cv;
    let longerlist = [...longer];
    let shorterlist = [...shorter];
    let thestartindex = null;
    for (let j = 0; j < shorterlist.length + 1; j++) {
      let insertindex = j;
      for (let k = 0; k < longerlist.length; k++) {
        let sliced = longerlist.slice(k, k + longer.length - shorter.length);
        let begin = shorterlist.slice(0, j);
        let center = sliced;
        let end = shorterlist.slice(j);
        let finalstr = [...begin, ...center, ...end].join("");
        if (finalstr == longer) {
          return j;
        }
      }
    }
  }
}
// 监听方向键
function handleKeyDown(e) {
  if (e.keyCode >= 37 && e.keyCode <= 40) {
    setTimeout(() => {
      //console.log("位置", e.keyCode, e.target.selectionStart);
      let index = e.target.selectionStart - 1;
      //console.log(index);
      let atgroup = findAtNameList();
      let disabled = false;
      for (let j = 0; j < atgroup.length; j++) {
        if (
          index >= atgroup[j].startindex &&
          index < atgroup[j].endindex &&
          index != text.value.length - 1
        ) {
          // e.target.selectionStart = atgroup[j].endindex
          // e.target.disabled = true
          disabled = true;
          break;
        }
      }
      markdisable.value = disabled;
    }, 5);
  }
}

// 处理鼠标左键按下
function handleMouseUp(e) {
  let index = e.target.selectionStart - 1;
  //console.log(index);
  let atgroup = findAtNameList();
  let disabled = false;
  for (let j = 0; j < atgroup.length; j++) {
    if (
      index >= atgroup[j].startindex &&
      index < atgroup[j].endindex &&
      index != text.value.length - 1
    ) {
      // e.target.selectionStart = atgroup[j].endindex
      // e.target.disabled = true
      disabled = true;
      break;
    }
  }
  markdisable.value = disabled;
  e.stopPropagation();
  e.preventDefault();
}
function handleFocus(e) {
  if (markselectpeople.value) {
    // 表明用户未选择内容
    textlist.value.splice(operationindex.value - 1, 0, "@");
    //console.log("聚焦后的textlist", textlist.value);
    showboard.value = false;
    watchText();
    markselectpeople.value = false;
  } else {
    // 聚焦到非@xxxx的地方
    //console.log(e.target.selectionStart, e.target.selectionEnd);
  }
  e.stopPropagation();
  e.preventDefault();
}
function handleInput(e) {
  if (e.data == "@") {
    if (markdisable.value) {
      return;
    }
    showboard.value = true;

    markselectpeople.value = true;

    unwatch();
    setTimeout(() => {
      e.target.blur();
    }, 10);
    operationindex.value = e.target.selectionStart;
  } else {
  }
  operationindex.value = e.target.selectionStart;
  // 获取父级元素, 光标位置
  let coordinates = getCaretCoordinates(e.target, e.target.selectionEnd);
  //console.log(coordinates);
  position.value = {
    x: coordinates.left + 10,
    y: coordinates.top
  };
  console.log(position.value, 666);
  e.stopPropagation();
  e.preventDefault();
}
function handleItemClick(index, val) {
  let textlists = [...text.value];
  let beforeline = textlists.slice(0, operationindex.value);
  let endline = textlists.slice(operationindex.value);
  //console.log(beforeline, endline);
  text.value =
    beforeline.join("") + list.value[index].approvalManName + endline.join("");
  textlist.value.splice(operationindex.value - 1, 0, "`@");
  let beforelinelist = textlist.value.slice(0, operationindex.value);
  let endlinelist = textlist.value.slice(operationindex.value);
  let namelist = [...list.value[index].approvalManName];
  namelist[namelist.length - 1] = namelist[namelist.length - 1] + "`";
  textlist.value = [...beforelinelist, ...namelist, ...endlinelist];

  let remarksListData = [];
  if (relationApprovalRemarksList.value.length > 0) {
    relationApprovalRemarksList.value.forEach(data => {
      remarksListData.push(data.id);
    });
  }
  //存储选中的@选中的数组
  let isAt = remarksListData.indexOf(val.id);
  if (isAt == -1) {
    relationApprovalRemarksList.value.push({
      approvalManName: val.approvalManName,
      id: val.id
    });
  }
  console.log(relationApprovalRemarksList.value, 888);
  // TODO 添加响应式
  setTimeout(() => {
    watchText();
    showboard.value = false;
    markselectpeople.value = false;
  }, 10);
}
// 找寻@名称列表
function findAtNameList() {
  let atgroup = [];
  let textlists = textlist.value;
  let startindex = null;
  let endindex = null;
  console.log("findAtNameList", [...textlists]);
  for (let j = 0; j < textlists.length; j++) {
    if (textlists[j] == "`@") {
      startindex = j;
      // 开始标记
      // str += textlist[j]
      endindex = null;
    }
    if (textlists[j][textlists[j].length - 1] == "`") {
      // 结束符号
      if (startindex !== null) {
        endindex = j;
      }
    }
    if (startindex !== null && endindex !== null) {
      let item = {
        startindex: startindex,
        endindex: endindex
      };
      startindex = null;
      endindex = null;
      atgroup.push(item);
    }
  }
  return atgroup;
}
onMounted(() => {
  watchText();
});
</script>

<style scoped="scoped" lang="scss">
.remarksList {
  display: flex;
  flex-direction: column;
  margin-top: 30px;
  height: 400px;
  overflow-y: auto;
  .remarksBox {
    display: flex;
    flex-direction: column;
    background: #f7f9fc;
    border-radius: 3px;
    padding: 15px;
    margin-bottom: 20px;
    .remarkContent {
      color: #313840;
      font-size: 14px;
      line-height: 22px;
      span {
        color: #1f5fb1;
      }
    }
    .line {
      width: 506px;
      height: 1px;
      border: 1px solid #e5eaef;
      margin: 15px 0;
    }
    .remarkTips {
      display: flex;
      align-items: center;
      span {
        color: #838a92;
        font-size: 14px;
        line-height: 20px;
        margin-right: 10px;
      }
      div {
        color: #aeb6c0;
        font-size: 14px;
        line-height: 20px;
      }
    }
  }
}
.btncontainer {
  display: flex;
  position: relative;
  flex-direction: column;
  textarea:disabled {
    background-color: white;
  }
  .totest,
  .tohome {
    font-size: 12px;
    height: 30px;
    width: 80px;
    border: 1px solid gray;
    border-radius: 10px;
    margin-top: 10px;
    text-align: center;
    line-height: 30px;
    margin-left: 10px;
  }
  .text {
    width: 200px;
    height: 200px;
  }
  .board {
    width: 200px;
    max-height: 181px;
    overflow: scroll;
    cursor: pointer;
    margin-top: 5px;
    background: #ffffff;
    box-shadow: 0px 3px 8px 0px rgba(166, 166, 166, 0.15);
    div {
      height: 32px;
      line-height: 32px;
      cursor: pointer;
      font-size: 16px;
      padding: 0 12px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    div:hover {
      background: #1f5fb1;
      color: #fff;
    }
  }
}
.addBtn {
  width: 60px;
  height: 36px;
  background: #1f5fb1;
  border-radius: 4px;
  font-size: 14px;
  color: #fff;
  line-height: 36px;
  text-align: center;
  margin-top: 12px;
  cursor: pointer;
}
.editor {
  margin: 0 auto;
  width: 100%;
  height: 102px;
  background: #fff;
  border: 1px solid #e5eaef;
  border-radius: 4px;
  text-align: left;
  padding: 8px 12px;
  overflow: auto;
  line-height: 20px;
  &:focus {
    outline: none;
  }
}
</style>


2.getCaretCoordinates.js

// The properties that we copy into a mirrored div.
// Note that some browsers, such as Firefox,
// do not concatenate properties, i.e. padding-top, bottom etc. -> padding,
// so we have to do every single property specifically.
var properties = [
  "direction", // RTL support
  "boxSizing",
  "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
  "height",
  "overflowX",
  "overflowY", // copy the scrollbar for IE

  "borderTopWidth",
  "borderRightWidth",
  "borderBottomWidth",
  "borderLeftWidth",
  "borderStyle",

  "paddingTop",
  "paddingRight",
  "paddingBottom",
  "paddingLeft",

  // https://developer.mozilla.org/en-US/docs/Web/CSS/font
  "fontStyle",
  "fontVariant",
  "fontWeight",
  "fontStretch",
  "fontSize",
  "fontSizeAdjust",
  "lineHeight",
  "fontFamily",

  "textAlign",
  "textTransform",
  "textIndent",
  "textDecoration", // might not make a difference, but better be safe

  "letterSpacing",
  "wordSpacing",

  "tabSize",
  "MozTabSize"
];

var isBrowser = typeof window !== "undefined";
var isFirefox = isBrowser && window.mozInnerScreenX != null;

export default function getCaretCoordinates(element, position, options) {
  if (!isBrowser) {
    throw new Error(
      "textarea-caret-position#getCaretCoordinates should only be called in a browser"
    );
  }

  var debug = (options && options.debug) || false;
  if (debug) {
    var el = document.querySelector(
      "#input-textarea-caret-position-mirror-div"
    );
    if (el) {
      el.parentNode.removeChild(el);
    }
  }

  // mirrored div
  var div = document.createElement("div");
  div.id = "input-textarea-caret-position-mirror-div";
  document.body.appendChild(div);

  var style = div.style;
  var computed = window.getComputedStyle
    ? getComputedStyle(element)
    : element.currentStyle; // currentStyle for IE < 9

  // default textarea styles
  style.whiteSpace = "pre-wrap";
  if (element.nodeName !== "INPUT") style.wordWrap = "break-word"; // only for textarea-s

  // position off-screen
  style.position = "absolute"; // required to return coordinates properly
  if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering

  // transfer the element's properties to the div
  properties.forEach(function (prop) {
    style[prop] = computed[prop];
  });

  if (isFirefox) {
    // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
    if (element.scrollHeight > parseInt(computed.height))
      style.overflowY = "scroll";
  } else {
    style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll'
  }

  div.textContent = element.value.substring(0, position);
  // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
  if (element.nodeName === "INPUT")
    div.textContent = div.textContent.replace(/\s/g, "\u00a0");

  var span = document.createElement("span");
  // Wrapping must be replicated *exactly*, including when a long word gets
  // onto the next line, with whitespace at the end of the line before (#7).
  // The  *only* reliable way to do that is to copy the *entire* rest of the
  // textarea's content into the <span> created at the caret position.
  // for inputs, just '.' would be enough, but why bother?
  span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all
  div.appendChild(span);

  var coordinates = {
    top: span.offsetTop + parseInt(computed["borderTopWidth"]),
    left: span.offsetLeft + parseInt(computed["borderLeftWidth"])
  };

  if (debug) {
    span.style.backgroundColor = "#aaa";
  } else {
    document.body.removeChild(div);
  }

  return coordinates;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值