Selection Context

Get the selected text along with text before and after the selection

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/528822/1737952/Selection%20Context.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Selection Context
// @namespace    http://tampermonkey.net/
// @version      0.3.2
// @description  Get the selected text along with text before and after the selection
// @author       RoCry
// @license MIT
// ==/UserScript==
const DEFAULT_CONTEXT_LENGTH = 500;
const MAX_CONTEXT_LENGTH = 8192;
const BLOCK_SELECTORS =
  "article, section, main, p, div, li, td, th, blockquote, pre";
function getSelectionRoot(range) {
  const container =
    range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
      ? range.commonAncestorContainer
      : range.commonAncestorContainer.parentElement;
  if (!container) return document.body;
  return container.closest(BLOCK_SELECTORS) || document.body;
}
function extractSelectedHTML(range) {
  try {
    const fragment = range.cloneContents();
    const container = document.createElement("div");
    container.appendChild(fragment);
    const parentElement =
      range.commonAncestorContainer.nodeType === Node.TEXT_NODE
        ? range.commonAncestorContainer.parentElement
        : range.commonAncestorContainer;
    if (parentElement && parentElement.nodeName !== "BODY") {
      const tagName = parentElement.nodeName.toLowerCase();
      return `<${tagName}>${container.innerHTML}</${tagName}>`;
    }
    return container.innerHTML;
  } catch (error) {
    console.error("Error extracting HTML from selection:", error);
    return null;
  }
}
function getTextNodesIn(node) {
  const textNodes = [];
  const walker = document.createTreeWalker(
    node,
    NodeFilter.SHOW_TEXT,
    null,
    false,
  );
  let currentNode = walker.nextNode();
  while (currentNode) {
    textNodes.push(currentNode);
    currentNode = walker.nextNode();
  }
  return textNodes;
}
/**
 * Gets the selected text along with text before and after the selection
 * @param {number} tryContextLength - Desired length of context to try to collect (before + after selection)
 * @returns {Object|null} Object containing selectedHTML, selectedText, textBefore, textAfter, paragraphText
 */
function GetSelectionContext(tryContextLength = DEFAULT_CONTEXT_LENGTH) {
  const selection = window.getSelection();
  if (!selection || selection.rangeCount === 0) return null;
  const selectedText = selection.toString().trim();
  if (!selectedText) return null;
  const range = selection.getRangeAt(0);
  const actualContextLength = Math.min(tryContextLength, MAX_CONTEXT_LENGTH);
  const halfContextLength = Math.floor(actualContextLength / 2);
  const root = getSelectionRoot(range) || document.body;
  const allTextNodes = getTextNodesIn(root);
  const startNode = range.startContainer;
  const endNode = range.endContainer;
  const startIndex = allTextNodes.indexOf(startNode);
  const endIndex = allTextNodes.indexOf(endNode);
  if (startIndex === -1 || endIndex === -1) {
    console.warn(
      "Selection nodes not found in text node list. Returning minimal context.",
    );
    return {
      selectedHTML: extractSelectedHTML(range) || selectedText,
      selectedText,
      textBefore: "",
      textAfter: "",
      paragraphText: selectedText,
    };
  }
  let textBefore = "";
  let textAfter = "";
  let currentLength = 0;
  if (startNode.nodeType === Node.TEXT_NODE) {
    textBefore = startNode.textContent.substring(0, range.startOffset);
    currentLength = textBefore.length;
  }
  let beforeIndex = startIndex - 1;
  while (beforeIndex >= 0 && currentLength < halfContextLength) {
    const nodeText = allTextNodes[beforeIndex].textContent || "";
    textBefore = `${nodeText}\n${textBefore}`;
    currentLength += nodeText.length;
    beforeIndex -= 1;
  }
  if (beforeIndex >= 0) {
    textBefore = `...\n${textBefore}`;
  }
  currentLength = 0;
  if (endNode.nodeType === Node.TEXT_NODE) {
    textAfter = endNode.textContent.substring(range.endOffset);
    currentLength = textAfter.length;
  }
  let afterIndex = endIndex + 1;
  while (
    afterIndex < allTextNodes.length &&
    currentLength < halfContextLength
  ) {
    const nodeText = allTextNodes[afterIndex].textContent || "";
    textAfter += `${nodeText}\n`;
    currentLength += nodeText.length;
    afterIndex += 1;
  }
  if (afterIndex < allTextNodes.length) {
    textAfter += "\n...";
  }
  textBefore = textBefore.trim();
  textAfter = textAfter.trim();
  const paragraphText = `${textBefore} ${selectedText} ${textAfter}`.trim();
  return {
    selectedHTML: extractSelectedHTML(range) || selectedText,
    selectedText,
    textBefore,
    textAfter,
    paragraphText,
  };
}
const TextExplainerUI = (() => {
  const IDS = {
    popup: "explainer-popup",
    overlay: "explainer-overlay",
    content: "explainer-content",
    loading: "explainer-loading",
    error: "explainer-error",
    floatingButton: "explainer-floating-button",
  };
  const POPUP_WIDTH = 450;
  const POPUP_MAX_HEIGHT_RATIO = 0.8;
  const STYLE_TEXT = `#${IDS.popup}{position:absolute;width:${POPUP_WIDTH}px;max-width:90vw;max-height:80vh;padding:16px 16px 14px;z-index:2147483647;overflow:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;background:rgba(255,255,255,0.96);border:1px solid rgba(15,23,42,0.12);border-radius:10px;box-shadow:0 12px 28px rgba(15,23,42,0.12);color:#0f172a;font-family:inherit;font-size:0.98rem;line-height:1.65;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:opacity 0.2s ease,transform 0.2s ease;}#${IDS.popup}.dark-theme{background:rgba(24,24,28,0.96);border:1px solid rgba(255,255,255,0.12);box-shadow:0 16px 34px rgba(0,0,0,0.5);color:#e5e7eb;}#${IDS.overlay}{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483646;background:transparent;}@supports (-webkit-touch-callout: none){#${IDS.popup}{backdrop-filter:none;-webkit-backdrop-filter:none;}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes fadeOut{from{opacity:1}to{opacity:0}}#${IDS.content}{font-family:inherit;font-size:0.98rem;line-height:1.65;}#${IDS.content} p{margin:0 0 10px;}#${IDS.content} ul,#${IDS.content} ol{margin:6px 0 10px 20px;padding:0;}#${IDS.content} li{margin:4px 0;}#${IDS.content} a{color:inherit;text-decoration:underline;text-decoration-color:rgba(15,23,42,0.35);text-decoration-thickness:2px;text-underline-offset:3px;}#${IDS.popup}.dark-theme #${IDS.content} a{text-decoration-color:rgba(229,231,235,0.5);}#${IDS.content} code{font-family:ui-monospace,"SFMono-Regular","Menlo",monospace;font-size:0.92em;background:rgba(15,23,42,0.08);padding:2px 4px;border-radius:4px;}#${IDS.popup}.dark-theme #${IDS.content} code{background:rgba(255,255,255,0.12);}#${IDS.loading}{text-align:center;padding:14px 0;display:flex;align-items:center;justify-content:center;}#${IDS.loading}:after{content:"";width:20px;height:20px;border:3px solid rgba(15,23,42,0.12);border-top:3px solid rgba(15,23,42,0.45);border-radius:50%;animation:spin 1s linear infinite;display:inline-block;}#${IDS.popup}.dark-theme #${IDS.loading}:after{border:3px solid rgba(255,255,255,0.18);border-top:3px solid rgba(255,255,255,0.55);}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}#${IDS.error}{color:#b42318;padding:8px 10px;border-radius:6px;margin-bottom:10px;font-size:0.9rem;display:none;background:rgba(180,35,24,0.08);}#${IDS.popup}.dark-theme #${IDS.error}{background:rgba(220,80,80,0.18);color:#ffb4b4;}@media (prefers-color-scheme: dark){#${IDS.popup}{background:rgba(24,24,28,0.96);color:#e5e7eb;}#${IDS.floatingButton}{background-color:rgba(33,150,243,0.9);}}@media (hover:none) and (pointer:coarse){#${IDS.popup}{width:95vw;max-height:90vh;padding:16px;font-size:1rem;}#${IDS.popup} p,#${IDS.popup} li{line-height:1.7;margin-bottom:12px;}#${IDS.popup} a{padding:8px 0;}}`;
  let stylesInjected = false;
  let currentPopup = null;
  function ensureStyles() {
    if (stylesInjected) return;
    const addStyle =
      typeof GM_addStyle === "function"
        ? GM_addStyle
        : (cssText) => {
            const style = document.createElement("style");
            style.textContent = cssText;
            document.head.appendChild(style);
            return style;
          };
    if (!document.head) {
      throw new Error("document.head is not available");
    }
    addStyle(STYLE_TEXT);
    stylesInjected = true;
  }
  function isTouchDevice() {
    return (
      "ontouchstart" in window ||
      navigator.maxTouchPoints > 0 ||
      navigator.msMaxTouchPoints > 0
    );
  }
  function parseRgb(color) {
    const match = color.match(
      /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,
    );
    if (!match) return null;
    return {
      r: Number(match[1]),
      g: Number(match[2]),
      b: Number(match[3]),
    };
  }
  function luminance(color) {
    const rgb = parseRgb(color);
    if (!rgb) return 128;
    return 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b;
  }
  function isPageDarkMode() {
    const bodyStyle = window.getComputedStyle(document.body);
    const htmlStyle = window.getComputedStyle(document.documentElement);
    const bodyBg = bodyStyle.backgroundColor;
    const htmlBg = htmlStyle.backgroundColor;
    const threshold = 128;
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    if (luminance(bodyBg) < threshold) return true;
    if (bodyBg === "rgba(0, 0, 0, 0)" && luminance(htmlBg) < threshold)
      return true;
    if (bodyBg === "rgba(0, 0, 0, 0)" && htmlBg === "rgba(0, 0, 0, 0)")
      return prefersDark;
    return false;
  }
  function calculatePopupPosition() {
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) return null;
    const range = selection.getRangeAt(0);
    const selectionRect = range.getBoundingClientRect();
    const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
    const scrollTop = window.scrollY || document.documentElement.scrollTop;
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;
    const popupHeight = Math.min(500, viewportHeight * POPUP_MAX_HEIGHT_RATIO);
    const margin = 20;
    const position = {};
    if (selectionRect.bottom + margin + popupHeight <= viewportHeight) {
      position.top = selectionRect.bottom + scrollTop + margin;
      position.left = Math.min(
        Math.max(
          10 + scrollLeft,
          selectionRect.left +
            scrollLeft +
            selectionRect.width / 2 -
            POPUP_WIDTH / 2,
        ),
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      );
      position.placement = "below";
      return position;
    }
    if (selectionRect.top - margin - popupHeight >= 0) {
      position.top = selectionRect.top + scrollTop - margin - popupHeight;
      position.left = Math.min(
        Math.max(
          10 + scrollLeft,
          selectionRect.left +
            scrollLeft +
            selectionRect.width / 2 -
            POPUP_WIDTH / 2,
        ),
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      );
      position.placement = "above";
      return position;
    }
    if (selectionRect.right + margin + POPUP_WIDTH <= viewportWidth) {
      position.top = Math.max(
        10 + scrollTop,
        Math.min(
          selectionRect.top + scrollTop,
          viewportHeight + scrollTop - popupHeight - 10,
        ),
      );
      position.left = selectionRect.right + scrollLeft + margin;
      position.placement = "right";
      return position;
    }
    if (selectionRect.left - margin - POPUP_WIDTH >= 0) {
      position.top = Math.max(
        10 + scrollTop,
        Math.min(
          selectionRect.top + scrollTop,
          viewportHeight + scrollTop - popupHeight - 10,
        ),
      );
      position.left = selectionRect.left + scrollLeft - margin - POPUP_WIDTH;
      position.placement = "left";
      return position;
    }
    position.top = Math.max(
      10 + scrollTop,
      Math.min(
        selectionRect.top + selectionRect.height + scrollTop + margin,
        viewportHeight / 2 + scrollTop - popupHeight / 2,
      ),
    );
    position.left = Math.max(
      10 + scrollLeft,
      Math.min(
        selectionRect.left +
          selectionRect.width / 2 +
          scrollLeft -
          POPUP_WIDTH / 2,
        viewportWidth + scrollLeft - POPUP_WIDTH - 10,
      ),
    );
    position.placement = "center";
    return position;
  }
  function openPopup({ isTouch, isDark }) {
    ensureStyles();
    closePopup();
    const popup = document.createElement("div");
    popup.id = IDS.popup;
    if (isDark) popup.classList.add("dark-theme");
    popup.innerHTML = `
      <div id="${IDS.error}"></div>
      <div id="${IDS.loading}"></div>
      <div id="${IDS.content}"></div>
    `;
    if (!document.body) {
      throw new Error("document.body is not available");
    }
    document.body.appendChild(popup);
    if (isTouch) {
      popup.style.position = "fixed";
      popup.style.top = "50%";
      popup.style.left = "50%";
      popup.style.transform = "translate(-50%, -50%)";
      popup.style.width = "90vw";
      popup.style.maxHeight = "85vh";
    } else {
      const position = calculatePopupPosition();
      if (position) {
        popup.style.transform = "none";
        if (position.top !== undefined) popup.style.top = `${position.top}px`;
        if (position.left !== undefined)
          popup.style.left = `${position.left}px`;
      } else {
        popup.style.top = "50%";
        popup.style.left = "50%";
        popup.style.transform = "translate(-50%, -50%)";
      }
    }
    popup.style.animation = "fadeIn 0.3s ease";
    const popupState = {
      popup,
      contentEl: popup.querySelector(`#${IDS.content}`),
      loadingEl: popup.querySelector(`#${IDS.loading}`),
      errorEl: popup.querySelector(`#${IDS.error}`),
      overlay: null,
      cleanup: [],
    };
    function closeOnEsc(event) {
      if (event.key === "Escape") {
        closePopup();
      }
    }
    document.addEventListener("keydown", closeOnEsc);
    popupState.cleanup.push(() =>
      document.removeEventListener("keydown", closeOnEsc),
    );
    if (isTouch) {
      const overlay = document.createElement("div");
      overlay.id = IDS.overlay;
      popupState.overlay = overlay;
      document.body.appendChild(overlay);
      let touchStarted = false;
      let startX = 0;
      let startY = 0;
      const moveThreshold = 30;
      function onOverlayTouchStart(event) {
        touchStarted = true;
        startX = event.touches[0].clientX;
        startY = event.touches[0].clientY;
      }
      function onOverlayTouchEnd(event) {
        if (!touchStarted) return;
        const touch = event.changedTouches[0];
        const moveX = Math.abs(touch.clientX - startX);
        const moveY = Math.abs(touch.clientY - startY);
        if (moveX < moveThreshold && moveY < moveThreshold) {
          closePopup();
        }
        touchStarted = false;
      }
      function stopPropagation(event) {
        event.stopPropagation();
      }
      overlay.addEventListener("touchstart", onOverlayTouchStart, {
        passive: true,
      });
      overlay.addEventListener("touchmove", () => {}, { passive: true });
      overlay.addEventListener("touchend", onOverlayTouchEnd, {
        passive: true,
      });
      popup.addEventListener("touchstart", stopPropagation, { passive: false });
      popupState.cleanup.push(() =>
        overlay.removeEventListener("touchstart", onOverlayTouchStart),
      );
      popupState.cleanup.push(() =>
        overlay.removeEventListener("touchend", onOverlayTouchEnd),
      );
      popupState.cleanup.push(() =>
        popup.removeEventListener("touchstart", stopPropagation),
      );
    } else {
      function onOutsideClick(event) {
        if (popup.contains(event.target)) return;
        closePopup();
      }
      document.addEventListener("click", onOutsideClick);
      popupState.cleanup.push(() =>
        document.removeEventListener("click", onOutsideClick),
      );
    }
    currentPopup = popupState;
    return popupState;
  }
  function closePopup() {
    if (!currentPopup) return;
    const popup = currentPopup.popup;
    popup.style.animation = "fadeOut 0.2s ease";
    const { overlay, cleanup } = currentPopup;
    const remove = () => {
      cleanup.forEach((fn) => fn());
      if (overlay) overlay.remove();
      popup.remove();
      currentPopup = null;
    };
    setTimeout(remove, 200);
  }
  function setLoading(popupState, isVisible) {
    if (!popupState || !popupState.loadingEl) return;
    popupState.loadingEl.style.display = isVisible ? "flex" : "none";
  }
  function showError(popupState, message) {
    if (!popupState || !popupState.errorEl) return;
    popupState.errorEl.textContent = message;
    popupState.errorEl.style.display = "block";
    setLoading(popupState, false);
  }
  function updateContent(popupState, text) {
    if (!popupState || !popupState.contentEl) return;
    if (!text) return;
    let content = text.trim();
    if (!content) return;
    try {
      if (content.startsWith("```")) {
        if (content.endsWith("```")) {
          content = content.split("\n").slice(1, -1).join("\n");
        } else {
          content = content.split("\n").slice(1).join("\n");
        }
      }
      if (!content.startsWith("<")) {
        content = `<p>${content.replace(/\n/g, "<br>")}</p>`;
      }
      popupState.contentEl.innerHTML = content;
    } catch (error) {
      popupState.contentEl.innerHTML = `<p>${content.replace(/\n/g, "<br>")}</p>`;
    }
  }
  function createFloatingButton({ size, onTrigger, label }) {
    const button = document.createElement("div");
    button.id = IDS.floatingButton;
    let buttonSize = "50px";
    if (size === "small") buttonSize = "40px";
    if (size === "large") buttonSize = "60px";
    button.style.cssText = `
      width: ${buttonSize};
      height: ${buttonSize};
      border-radius: 50%;
      background-color: rgba(33, 150, 243, 0.8);
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      position: fixed;
      z-index: 9999;
      box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
      cursor: pointer;
      font-weight: bold;
      font-size: ${parseInt(buttonSize, 10) * 0.4}px;
      opacity: 0;
      transition: opacity 0.3s ease, transform 0.2s ease;
      pointer-events: none;
      touch-action: manipulation;
      -webkit-tap-highlight-color: transparent;
    `;
    button.setAttribute("aria-label", "Explain selection");
    button.innerHTML = label || "TE";
    if (!document.body) {
      throw new Error("document.body is not available");
    }
    document.body.appendChild(button);
    function handleButtonAction(event) {
      event.preventDefault();
      event.stopPropagation();
      if (typeof onTrigger === "function") {
        onTrigger(event);
      }
    }
    button.addEventListener("click", handleButtonAction);
    button.addEventListener(
      "touchstart",
      (event) => {
        event.preventDefault();
        event.stopPropagation();
        button.style.transform = "scale(0.95)";
      },
      { passive: false },
    );
    button.addEventListener(
      "touchend",
      (event) => {
        event.preventDefault();
        event.stopPropagation();
        button.style.transform = "scale(1)";
        handleButtonAction(event);
      },
      { passive: false },
    );
    button.addEventListener("mousedown", (event) => {
      event.preventDefault();
      event.stopPropagation();
    });
    return button;
  }
  function showFloatingButton(button) {
    if (!button) return false;
    const selection = window.getSelection();
    if (!selection || selection.rangeCount === 0) {
      hideFloatingButton(button);
      return false;
    }
    const range = selection.getRangeAt(0);
    const rect = range.getBoundingClientRect();
    const buttonSize = parseInt(button.style.width, 10);
    const margin = 10;
    let top = rect.bottom + margin;
    let left = rect.left + rect.width / 2 - buttonSize / 2;
    if (top + buttonSize > window.innerHeight) {
      top = rect.top - buttonSize - margin;
    }
    left = Math.max(10, Math.min(left, window.innerWidth - buttonSize - 10));
    button.style.top = `${top}px`;
    button.style.left = `${left}px`;
    button.style.opacity = "1";
    button.style.pointerEvents = "auto";
    return true;
  }
  function hideFloatingButton(button) {
    if (!button) return;
    button.style.opacity = "0";
    button.style.pointerEvents = "none";
  }
  return {
    ensureStyles,
    isTouchDevice,
    isPageDarkMode,
    openPopup,
    closePopup,
    setLoading,
    showError,
    updateContent,
    createFloatingButton,
    showFloatingButton,
    hideFloatingButton,
  };
})();
window.GetSelectionContext = GetSelectionContext;
window.TextExplainerUI = TextExplainerUI;
if (typeof module !== "undefined" && module.exports) {
  module.exports = { GetSelectionContext, TextExplainerUI };
} else {
  window.SelectionUtils = window.SelectionUtils || {};
  window.SelectionUtils.GetSelectionContext = GetSelectionContext;
}