文本大爆炸

仿照锤子的大爆炸,对选中文本进行分词

// ==UserScript==
// @name         文本大爆炸
// @namespace    http://tampermonkey.net/
// @author       突徒土兔
// @version      3.1
// @description  仿照锤子的大爆炸,对选中文本进行分词
// @match        *://*/*
// @license      CC-BY-NC-4.0
// @icon         https://s2.loli.net/2024/09/25/6PxlMHA7EZVqwsJ.png
// @require      https://cdn.jsdelivr.net/npm/segmentit@2.0.3/dist/umd/segmentit.js
// ==/UserScript==

(function () {
  "use strict";

  // 触发分词按钮
  let button = null;
  // 弹出窗口
  let popupContainer = null;
  // 分词器
  const segmentit = Segmentit.useDefault(new Segmentit.Segment());
  // 是否处于拖动
  let isDragging = false;
  // 开始拖动的元素
  let startElement = null;

  /**
   * 创建样式
   */
  function createStyles() {
    const style = document.createElement("style");
    style.textContent = `
              .word-explosion-button {
                  position: absolute;
                  background-color: rgba(255,255,255, 0.4);
                  color: #000;
                  border: none;
                  border-radius: 50%;
                  cursor: pointer;
                  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                  font-size: 16px;
                  box-shadow: 0 2px 10px rgba(0,0,0,0.15);
                  transition: all 0.3s ease;
                  z-index: 9999;
                  width: 30px;
                  height: 30px;
                  display: flex;
                  justify-content: center;
                  align-items: center;
              }
              .word-explosion-button:hover {
                  background-color: rgba(255,255,255, 0.75);
                  box-shadow: 0 4px 20px rgba(0,0,0,0.25);
                  transform: scale(1.1);
                  transition: transform 0.3s ease;
              }
              .word-explosion-popup {
                  position: fixed;
                  top: 50%;
                  left: 50%;
                  transform: translate(-50%, -50%);
                  background-color: rgba(240, 240, 240, 0.8);
                  backdrop-filter: blur(10px);
                  padding: 20px;
                  border-radius: 10px;
                  box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
                  z-index: 10000;
                  max-width: 80%;
                  max-height: 80%;
                  overflow: auto;
                  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                  display: flex;
                  flex-wrap: wrap;
                  justify-content: center;
                  align-items: center;
                  opacity: 0;
                  animation: fadeIn 0.5s ease forwards;
              }

              @keyframes fadeIn {
                  from {
                      opacity: 0;
                      transform: translate(-50%, -50%) scale(0.9);
                  }
                  to {
                      opacity: 1;
                      transform: translate(-50%, -50%) scale(1);
                  }
              }
              .word-explosion-word {
                  margin: 2px;
                  height: 30px; /* 让所有该类所有对象都有一样的高度 */
                  padding: 4px 8px;
                  background-color: rgba(255, 255, 255, 0.5);
                  border: none;
                  border-radius: 10px;
                  cursor: pointer;
                  transition: all 0.3s ease;
                  font-size: 14px;
                  display: inline-flex;
                  align-items: center;
              }
              .word-explosion-word.selected {
                  background-color: #0078D4;
                  color: white;
              }
              .word-explosion-copy {
                  display: block;
                  margin-top: 15px;
                  padding: 10px 20px;
                  background-color: #0078D4;
                  color: white;
                  border: none;
                  border-radius: 20px;
                  cursor: pointer;
                  font-size: 14px;
                  transition: all 0.3s ease;
              }
              .word-explosion-copy:hover {
                  background-color: #106EBE;
              }
          `;
    document.head.appendChild(style);
  }

  /**
   * 创建按钮
   * 该函数用于在页面中创建一个按钮,并将其添加到文档的 body 中。
   * 按钮的初始状态是隐藏的。
   */
  function createButton() {
    // 创建一个新的按钮元素
    button = document.createElement("button");
    // 设置按钮的文本内容
    button.textContent = "🔨";
    // 设置按钮的类名
    button.className = "word-explosion-button";
    // 设置按钮的初始显示状态为隐藏
    button.style.display = "none";
    // 将按钮添加到文档的 body 中
    document.body.appendChild(button);
  }

  /**
   * 显示按钮并将其定位到选中文本旁边
   * 该函数用于在用户选择文本后,显示按钮并将按钮定位到选中文本的旁边。
   */
  function showButtonAtSelection() {
    // 获取当前的文本选择对象
    const selection = window.getSelection();
    // 检查是否有选中的文本
    if (selection.rangeCount > 0) {
      // 获取选中的第一个范围
      const range = selection.getRangeAt(0);
      // 获取选中范围的边界矩形
      const rect = range.getBoundingClientRect();
      // 设置按钮的顶部位置为选中范围的底部加上滚动条的偏移量
      button.style.top = `${rect.bottom + window.scrollY + 5}px`;
      // 设置按钮的左侧位置为选中范围的左侧加上滚动条的偏移量
      button.style.left = `${rect.left + window.scrollX}px`;
      // 显示按钮
      button.style.display = "block";
    }
  }

  /**
   * 隐藏按钮
   * 该函数用于隐藏按钮。
   */
  function hideButton() {
    // 将按钮的显示状态设置为隐藏
    button.style.display = "none";
  }

  /**
   * 创建弹出窗口
   * 该函数用于创建一个弹出窗口,并将其添加到文档的 body 中。
   * 弹出窗口的初始状态是隐藏的。
   */
  function createPopup() {
    // 创建一个新的 div 元素作为弹出窗口的容器
    popupContainer = document.createElement("div");
    // 设置弹出窗口的类名为 "word-explosion-popup"
    popupContainer.className = "word-explosion-popup";
    // 设置弹出窗口的初始显示状态为隐藏
    popupContainer.style.display = "none";
    // 将弹出窗口添加到文档的 body 中
    document.body.appendChild(popupContainer);

    // 添加事件监听器,用于实现拖动选择功能
    popupContainer.addEventListener("mousedown", onMouseDown);
    document.addEventListener("mousemove", onMouseMove);
    document.addEventListener("mouseup", onMouseUp);

    // 添加事件监听器,用于隐藏弹出窗口
    document.addEventListener("click", (event) => {
      // 如果点击事件的目标不在弹出窗口内且不在按钮内,则隐藏弹出窗口
      if (
        !popupContainer.contains(event.target) &&
        !button.contains(event.target)
      ) {
        hidePopup();
      }
    });
  }

  /**
   * 显示弹出窗口
   * 该函数用于显示弹出窗口,并将分词结果显示在弹出窗口中。
   * @param {Array} words - 分词结果数组
   */
  function showPopup(words) {
    // 清空弹出窗口的内容
    popupContainer.innerHTML = "";
    // 遍历分词结果数组
    words.forEach((word) => {
      // 创建一个新的按钮元素
      const wordButton = document.createElement("button");
      // 设置按钮的文本内容为分词结果
      wordButton.textContent = word;
      // 设置按钮的类名为 "word-explosion-word"
      wordButton.className = "word-explosion-word";
      // 为按钮添加点击事件监听器,用于切换 "selected" 类
      wordButton.addEventListener("click", () =>
        wordButton.classList.toggle("selected")
      );
      // 将按钮添加到弹出窗口中
      popupContainer.appendChild(wordButton);
    });

    // 创建一个新的按钮元素,用于复制选中的文本
    const copyButton = document.createElement("button");
    // 设置按钮的文本内容为 "复制选中文本"
    copyButton.textContent = "复制选中文本";
    // 设置按钮的类名为 "word-explosion-copy"
    copyButton.className = "word-explosion-copy";
    // 设置按钮的宽度为 100%
    copyButton.style.width = "100%";
    // 为按钮添加点击事件监听器,用于复制选中的文本
    copyButton.addEventListener("click", copySelectedWords);
    // 将按钮添加到弹出窗口中
    popupContainer.appendChild(copyButton);

    // 显示弹出窗口
    popupContainer.style.display = "flex";
  }

  /**
   * 隐藏弹出窗口
   * 该函数用于隐藏弹出窗口。
   */
  function hidePopup() {
    // 将弹出窗口的显示状态设置为隐藏
    popupContainer.style.display = "none";
  }

  /**
   * 分词函数
   * 该函数用于对输入的文本进行分词,并返回分词结果数组。
   * @param {string} text - 需要分词的文本
   * @returns {Array} - 分词结果数组
   */
  function wordExplosion(text) {
    // 使用 segmentit 库对文本进行分词,并提取分词结果
    let result = segmentit.doSegment(text).map((item) => item.w);
    // 初始化一个空数组来存储带有空格的分词结果
    let newResult = [];
    // 分词过程中丢失了空格
    // 遍历原始文本,插入空格
    let textIndex = 0;
    for (let i = 0; i < result.length; i++) {
      newResult.push(result[i]);
      textIndex += result[i].length;
      while (textIndex < text.length && text[textIndex] === " ") {
        newResult.push(" ");
        textIndex++;
      }
    }
    // 在控制台输出分词结果
    console.log(`分词结果:\n${newResult}`);
    // 返回分词结果数组,如果结果为空则返回空数组
    return newResult || [];
  }

  /**
   * 复制选中的单词
   * 该函数用于将选中的单词复制到剪贴板,并弹出提示。
   */
  function copySelectedWords() {
    // 获取所有选中的单词按钮
    const selectedWords = Array.from(
      popupContainer.querySelectorAll(".word-explosion-word.selected")
    )
      // 提取每个按钮的文本内容
      .map((button) => button.textContent)
      // 将所有选中的单词连接成一个字符串
      .join("");
    // 将选中的单词复制到剪贴板
    navigator.clipboard
      .writeText(selectedWords)
      .then(() => {
        // 复制成功后弹出提示
        alert(`已复制:\n${selectedWords}`);
      })
      .catch((err) => {
        // 复制失败时在控制台输出错误信息
        console.error("复制失败: ", err);
      });
  }

  /**
   * 监听选择事件
   * 该函数用于监听用户的选择事件,并在用户选择文本后显示按钮。
   */
  document.addEventListener("selectionchange", function () {
    // 获取当前的文本选择对象
    const selection = window.getSelection();
    // 检查选中的文本是否不为空
    if (selection.toString().trim() !== "") {
      // 显示按钮并将其定位到选中文本旁边
      showButtonAtSelection();
    } else {
      // 隐藏按钮
      hideButton();
    }
  });

  /**
   * 监听按钮点击事件
   * 该函数用于在用户点击按钮后,对选中的文本进行分词,并显示弹出窗口。
   */
  function onButtonClick() {
    // 获取当前的文本选择对象
    const selection = window.getSelection();
    // 获取选中的文本
    const text = selection.toString();
    // 对选中的文本进行分词
    const words = wordExplosion(text);
    // 显示弹出窗口并将分词结果显示在弹出窗口中
    showPopup(words);
    // 隐藏按钮
    hideButton();
  }

  let longPressTimer = null;
  const longPressThreshold = 200; // 长按阈值,单位为毫秒

  /**
   * 处理鼠标按下事件
   * 该函数用于处理鼠标按下事件,实现长按选择功能。
   * @param {MouseEvent} event - 鼠标按下事件对象
   */
  function onMouseDown(event) {
    // 检查鼠标按下的目标是否为单词按钮
    if (event.target.classList.contains("word-explosion-word")) {
      // 设置一个定时器,用于检测长按操作
      longPressTimer = setTimeout(() => {
        // 如果长按时间超过阈值,则开始拖动选择
        isDragging = true;
        // 记录开始拖动的元素
        startElement = event.target;
        // 为开始拖动的元素添加 "selected" 类
        startElement.classList.add("selected");
      }, longPressThreshold);
    }
  }

  /**
   * 处理鼠标移动事件
   * 该函数用于处理鼠标移动事件,实现拖动选择功能。
   * @param {MouseEvent} event - 鼠标移动事件对象
   */
  function onMouseMove(event) {
    // 检查是否正在进行拖动选择
    if (isDragging && startElement) {
      // 获取当前鼠标位置下的元素
      const currentElement = document.elementFromPoint(
        event.clientX,
        event.clientY
      );
      // 检查当前元素是否为单词按钮且不是开始拖动的元素
      if (
        currentElement &&
        currentElement.classList.contains("word-explosion-word") &&
        currentElement !== startElement
      ) {
        // 为当前元素添加 "selected" 类
        currentElement.classList.add("selected");
      }
    }
  }

  /**
   * 处理鼠标松开事件
   * 该函数用于处理鼠标松开事件,结束拖动选择功能。
   * @param {MouseEvent} event - 鼠标松开事件对象
   */
  function onMouseUp(event) {
    // 清除长按定时器
    clearTimeout(longPressTimer);
    // 结束拖动选择
    isDragging = false;
    // 清空开始拖动的元素
    startElement = null;
  }

  /**
   * 初始化脚本
   * 该函数用于初始化脚本,创建样式、按钮和弹出窗口,并添加事件监听器。
   */
  function init() {
    // 创建样式
    createStyles();
    // 创建按钮
    createButton();
    // 创建弹出窗口
    createPopup();
    // 为按钮添加点击事件监听器
    button.addEventListener("click", onButtonClick);
  }

  // 初始化脚本
  init();
})();