输入内容的时候,输入斜杠展示一个弹窗并且选中弹窗的内容可以追加到当前输入区的功能实现

先上效果图:

它也支持根据点击的位置以及键盘方向键的移动判断当光标处于斜杠后的时候弹出弹窗

代码如下:

复制代码
import { defineComponent, ref, nextTick, onMounted, onUnmounted } from "vue";
import { List, ListItem } from "@arco-design/web-vue";
import "./index.less";

export default defineComponent({
  name: "MyComponent",
  setup() {
    const popupVisible = ref(false);
    const textareaRef = ref<HTMLElement | null>(null);
    let lastCaretPosition: any = null; // 记录光标位置

    /**
     * 更新光标位置
     */
    const updateCaretPosition = () => {
      const selection = window.getSelection();
      if (selection && selection.rangeCount > 0) {
        lastCaretPosition = selection.getRangeAt(0); // 记录光标位置
      }
    };

    /**
     * 设置弹窗位置
     * @param {HTMLElement} popover - 弹窗元素
     * @param {HTMLElement} textarea - 输入框元素
     */
    const setPopupPosition = (popover: HTMLElement, textarea: HTMLElement) => {
      if (popover && textarea) {
        const cursorPos = getCaretPosition();
        const rect = textarea.getBoundingClientRect();
        const offset = 10; // 光标和弹窗之间的额外空间

        popover.style.position = "absolute";

        // 调整垂直位置
        if (
          cursorPos.top + cursorPos.height + popover.offsetHeight >
          window.innerHeight
        ) {
          popover.style.top = `${
            rect.top +
            cursorPos.top -
            cursorPos.height -
            popover.offsetHeight +
            window.scrollY -
            offset
          }px`;
        } else {
          popover.style.top = `${
            rect.top +
            cursorPos.top +
            cursorPos.height +
            window.scrollY +
            offset
          }px`;
        }

        // 调整水平位置
        if (cursorPos.left + popover.offsetWidth > window.innerWidth) {
          popover.style.left = `${
            rect.left + cursorPos.left - popover.offsetWidth
          }px`;
        } else {
          popover.style.left = `${rect.left + cursorPos.left}px`;
        }
      }
    };

    /**
     * 处理输入事件
     * @param {InputEvent} inputValue - 输入事件对象
     */
    const handleInput = async (inputValue: InputEvent) => {
      const lastChar = (inputValue.data || "").slice(-1);
      if (lastChar === "/") {
        popupVisible.value = true;
        await nextTick();
        const textarea = textareaRef.value;
        if (textarea) {
          updateCaretPosition();
          const popover =
            document.querySelector<HTMLElement>(".instruction-list");
          if (popover) {
            setPopupPosition(popover, textarea);
          }
        }
      } else {
        popupVisible.value = false;
      }
    };

    /**
     * 处理键盘事件
     * @param {KeyboardEvent} event - 键盘事件对象
     */
    const handleKeyDown = (event: KeyboardEvent) => {
      if (
        ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)
      ) {
        setTimeout(() => {
          checkCaretPosition();
          updateCaretPosition();
        }, 0);
      }
    };

    /**
     * 处理点击事件
     */
    const handleClick = async () => {
      await nextTick();
      checkCaretPosition();
      updateCaretPosition();
    };

    const createNode = (text: string) => {
      // 创建新的文本内容
      const newText = `<span class="highlight">${text}</span>`;
      const tmpSpan = document.createElement("span");
      tmpSpan.innerHTML = newText;
      tmpSpan.setAttribute("contentEditable", "false");
      return tmpSpan;
    };

    /**
     * 处理选择项点击事件
     * @param {string} item - 选择的项
     */
    const handleItemClick = (item: string) => {
      const selection = window.getSelection();

      // 如果之前保存了光标位置,则恢复它
      if (lastCaretPosition) {
        selection?.removeAllRanges();
        selection?.addRange(lastCaretPosition);
      }

      const range: any = selection?.getRangeAt(0);
      // 获取当前选区的第一个 Range 对象
      const startOffset = Math.max(range.startOffset - 1, 0);
      // 计算起始偏移量,确保它不会小于 0
      const endOffset = range.endOffset;
      // 获取结束偏移量
      if (range.startContainer.nodeType === Node.TEXT_NODE) {
        // 确保起始容器是文本节点
        range.setStart(range.startContainer, startOffset);
        // 设置 Range 的起始位置为调整后的起始偏移量
        range.setEnd(range.endContainer, endOffset);
        // 设置 Range 的结束位置
        range.deleteContents();
        // 删除 Range 内的内容
        const spanNode = createNode(item);
        // 创建包含选项文本的新节点
        range.insertNode(spanNode);
        // 将新节点插入到 Range 内
        range.setStartAfter(spanNode);
        // 将 Range 的起始位置设置到新节点之后
        range.setEndAfter(spanNode);
        // 将 Range 的结束位置设置到新节点之后
        selection?.removeAllRanges();
        // 清空当前的所有 Range
        selection?.addRange(range);
        // 将新的 Range 添加到选区
        popupVisible.value = false;
        // 隐藏弹窗
        textareaRef.value?.focus();
        // 重新聚焦到输入框
      }
    };

    /**
     * 检查光标位置
     */
    const checkCaretPosition = () => {
      const textarea = textareaRef.value;
      if (textarea) {
        const selection = window.getSelection();
        if (selection && selection.rangeCount > 0) {
          const range = selection.getRangeAt(0);
          const text = range.startContainer.textContent;
          const caretPos = range.startOffset;

          if (text && text[caretPos - 1] === "/" && caretPos > 0) {
            popupVisible.value = true;
            const popover =
              document.querySelector<HTMLElement>(".instruction-list");
            if (popover) {
              setPopupPosition(popover, textarea);
            }
          } else {
            popupVisible.value = false;
          }
        }
      }
    };

    /**
     * 获取光标位置
     * @returns {Object} 光标位置对象,包含 left、top 和 height 属性
     */
    const getCaretPosition = () => {
      const selection = window.getSelection();
      if (selection && selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        const rect = range.getBoundingClientRect();
        return {
          left: rect.left,
          top: rect.top,
          height: rect.height,
        };
      }
      return { left: 0, top: 0, height: 0 };
    };

    onMounted(() => {
      const textarea = textareaRef.value;
      if (textarea) {
        textarea.addEventListener("keydown", handleKeyDown);
        textarea.addEventListener("click", handleClick);
      }
    });

    onUnmounted(() => {
      const textarea = textareaRef.value;
      if (textarea) {
        textarea.removeEventListener("keydown", handleKeyDown);
        textarea.removeEventListener("click", handleClick);
      }
    });

    return () => (
      <div style={{ position: "relative" }}>
        <div
          placeholder="请输入内容"
          contenteditable
          class="editor"
          allow-clear
          ref={textareaRef}
          onInput={handleInput}
        />
        <div id="popover" v-show={popupVisible.value}>
          <List class="instruction-list">
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京1科技有限公司")}
            >
              北京1科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京2科技有限公司")}
            >
              北京2科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京3科技有限公司")}
            >
              北京3科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京4科技有限公司")}
            >
              北京4科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京5科技有限公司")}
            >
              北京5科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京6科技有限公司")}
            >
              北京6科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京7科技有限公司")}
            >
              北京7科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京8科技有限公司")}
            >
              北京8科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京9科技有限公司")}
            >
              北京9科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京0科技有限公司")}
            >
              北京0科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京11科技有限公司")}
            >
              北京11科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京12科技有限公司")}
            >
              北京12科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京13科技有限公司")}
            >
              北京13科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京14科技有限公司")}
            >
              北京14科技有限公司
            </ListItem>
            <ListItem
              tabindex="0"
              onClick={() => handleItemClick("北京15科技有限公司")}
            >
              北京15科技有限公司
            </ListItem>
          </List>
        </div>
      </div>
    );
  },
});
复制代码

 

 CSS如下:

复制代码
.instruction-list {
  height: 200px;
  z-index: 1000;
  background-color: #fff;
  box-shadow: 0 0 0 2px rgba(72, 5, 255, 0.06);
  .arco-list-content {
    height: 200px;
    overflow: auto;
    &::-webkit-scrollbar {
      background: transparent;
      width: 0.25rem;
      height: 0.2rem;
    }
    &::-webkit-scrollbar-thumb {
      /*滚动条里面小方块*/
      background: #856dfc;
      opacity: 0.12;
      border-radius: 0.125rem;
    }
    &::-webkit-scrollbar-track {
      /*滚动条里面轨道*/
      background: transparent;
    }
  }
}

.editor {
  margin: 0 auto;
  width: 100%;
  height: 100px;
  background: #fff;
  border: 1px solid #d9d9d9;
  border-radius: 5px;
  text-align: left;
  padding: 10px;
  overflow: auto;
  line-height: 30px;
  color: #333;
  &:hover {
    border-color: #856dfc;
  }
  &:focus {
    outline: none;
    border-color: #856dfc;
    box-shadow: 0 0 0 2px rgba(72, 5, 255, 0.06);
    border-inline-end-width: 1px;
  }
}

.highlight {
  display: inline-block;
  border: 1px solid purple;
  border-radius: 5px;
  padding: 2px 4px;
}
复制代码

 

posted @   洛晨随风  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
历史上的今天:
2020-01-06 id4的数据库持久化写法
2016-01-06 远程文件拷贝(fastcopy为例)
点击右上角即可分享
微信分享提示