JS 在页面光标处插入文本的操作

87 min read
// Listen for messages from the background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "ASK_CHATGPT") {
    let originalActiveElement;
    let text;

    // If there's an active text input
    if (
      document.activeElement &&
      (document.activeElement.isContentEditable ||
        document.activeElement.nodeName.toUpperCase() === "TEXTAREA" ||
        document.activeElement.nodeName.toUpperCase() === "INPUT")
    ) {
      // Set as original for later
      originalActiveElement = document.activeElement;
      // Use selected text or all text in the input
      text =
        document.getSelection().toString().trim() ||
        document.activeElement.textContent.trim();
    } else {
      // If no active text input use any selected text on page
      text = document.getSelection().toString().trim();
    }

    if (!text) {
      alert(
        "No text found. Select this option after right clicking on a textarea that contains text or on a selected portion of text."
      );
      return;
    }

    showLoadingCursor();

    // Send the text to the API endpoint
    fetch("http://localhost:3000", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ message: text }),
    })
      .then((response) => response.json())
      .then(async (data) => {
        // Use original text element and fallback to current active text element
        const activeElement =
          originalActiveElement ||
          (document.activeElement.isContentEditable && document.activeElement);

        if (activeElement) {
          if (
            activeElement.nodeName.toUpperCase() === "TEXTAREA" ||
            activeElement.nodeName.toUpperCase() === "INPUT"
          ) {
            // Insert after selection
            activeElement.value =
              activeElement.value.slice(0, activeElement.selectionEnd) +
              `\n\n${data.reply}` +
              activeElement.value.slice(
                activeElement.selectionEnd,
                activeElement.length
              );
          } else {
            // Special handling for contenteditable
            // 创建一个文本节点 replyNode,节点内容为 \n\n 加上 data.reply 的值
            const replyNode = document.createTextNode(`\n\n${data.reply}`);
            // 获取当前文档中的选区对象 selection。
            const selection = window.getSelection();
						// 如果选区中没有任何范围(即当前没有选中任何文本),那么使用 selection.addRange() 方法添加一个范围,并将其折叠到当前获得焦点的可编辑元素的末尾。
            if (selection.rangeCount === 0) {
              selection.addRange(document.createRange());
              selection.getRangeAt(0).collapse(activeElement, 1);
            }
						
            // 根据选区获取Rane对象
            const range = selection.getRangeAt(0);
            range.collapse(false);

            // Insert reply
            range.insertNode(replyNode);

            // Move the cursor to the end 光标移动位置
            selection.collapse(replyNode, replyNode.length);
          }
        } else {
          // Alert reply since no active text area
          alert(`ChatGPT says: ${data.reply}`);
        }

        restoreCursor();
      })
      .catch((error) => {
        restoreCursor();
        alert(
          "Error. Make sure you're running the server by following the instructions on https://github.com/gragland/chatgpt-chrome-extension. Also make sure you don't have an adblocker preventing requests to localhost:3000."
        );
        throw new Error(error);
      });
  }
});

const showLoadingCursor = () => {
  const style = document.createElement("style");
  style.id = "cursor_wait";
  style.innerHTML = `* {cursor: wait;}`;
  document.head.insertBefore(style, null);
};

const restoreCursor = () => {
  document.getElementById("cursor_wait").remove();
};

标签名是大小写不敏感的

标签名是大小写不敏感的避免因为大小写问题导致判断错误使用了 toUpperCase() 方法将标签名转换为大写字母形式

document.activeElement 获取焦点的元素

document.activeElement 属性返回当前文档中获得焦点的元素。在一个页面中,只有一个元素可以获得焦点,即当前被选中的元素,可以通过鼠标点击或者键盘操作来选中元素并使其获得焦点。

document.activeElement 属性返回的是一个元素对象,可以通过该对象来访问和操作当前活动元素的属性和方法。例如,可以使用 value 属性获取当前活动元素的值,或者使用 blur() 方法将焦点从当前元素上移除,等等。

  • document.activeElement.selectionEnd 属性是一个只读属性,用于获取当前获得焦点的可编辑元素中用户选择文本的结束位置(即光标所在的位置)。该属性只能用于可编辑元素,例如 <input><textarea> 等。

    该属性返回的是一个整数值,表示用户选择文本的结束位置(光标所在的位置),从 0 开始计数。如果用户没有选择任何文本,该属性值等于当前光标所在位置。

    以下是一个示例代码,用于获取当前可编辑元素中用户选择文本的结束位置:

    const inputElem = document.activeElement;
    if (inputElem instanceof HTMLInputElement) {
        const selectionEnd = inputElem.selectionEnd;
        console.log(`用户选择文本的结束位置为:${selectionEnd}`);
    }
    

isContentEditable

isContentEditable 属性是一个布尔值属性,用于表示当前元素是否可以被编辑。如果该属性值为 true,则表示当前元素可以被编辑;如果该属性值为 false,则表示当前元素不可以被编辑。

该属性通常用于判断当前元素是否可编辑,以便进行相应的操作。例如,可以使用该属性来实现富文本编辑器、可编辑表格、可编辑列表等功能。

需要注意的是,isContentEditable 属性只能用于 HTML 元素(即可以通过 DOM API 创建的元素),不能用于 XML 元素。此外,该属性值可以被设置为 truefalse,但是一旦被设置为 true,就无法再被设置为 false

以下是一个示例代码,用于判断当前元素是否可以被编辑:

if (document.activeElement.isContentEditable) {
  console.log("该元素可以被编辑");
} else {
  console.log("该元素不可以被编辑");
}

document.createRange()

document.createRange() 方法用于创建一个新的范围对象(Range 对象),该对象用于表示文档中的一个范围。

范围对象通常用于表示文档中的一个文本范围,例如一个选区、一个光标位置等。范围对象可以用于获取和修改文档中的文本内容、样式、布局等信息,是 Web 应用程序中处理文本的重要工具之一。

document.createRange() 方法返回的是一个新的范围对象,该对象可以通过一系列的方法和属性来设置和获取范围的起始位置和结束位置、文本内容、样式等信息。例如,可以使用 setStart()setEnd() 方法来设置范围的起始位置和结束位置,使用 cloneContents() 方法来复制范围中的内容等。

以下是一个示例代码,用于创建一个新的范围对象,并设置其起始位置和结束位置:

const range = document.createRange();
range.setStart(startNode, startOffset);
range.setEnd(endNode, endOffset);

需要注意的是,document.createRange() 方法只能在文档对象(document)上调用,不能在其他节点对象上调用。此外,范围对象的使用需要结合具体的需求和场景,需要了解范围对象的相关知识,以确保代码的正确性和可靠性。