Quick Edit

"Quick Edit Popup for Web Content"

// ==UserScript==
// @name         Quick Edit
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  "Quick Edit Popup for Web Content"
// @author       DuyNguyen2k6 
// @match        *://*/*
// @grant        none
// @icon       https://github.com/DuyNguyen2k6/quick-edit_Extension/blob/main/icon.png?raw=true
// ==/UserScript==

(function() {
  // Ẩn hoàn toàn scrollbar popup và textarea, nhưng vẫn scroll được
  (function insertHideScrollbarCSS() {
    const style = document.createElement("style");
    style.textContent = `
      /* Ẩn scrollbar nhưng vẫn scroll được */
      #html-quick-edit-popup,
      #html-quick-edit-popup textarea {
        scrollbar-width: none; /* Firefox */
        -ms-overflow-style: none;  /* IE 10+ */
      }
      #html-quick-edit-popup::-webkit-scrollbar,
      #html-quick-edit-popup textarea::-webkit-scrollbar {
        display: none; /* Chrome, Safari, Edge */
      }
    `;
    document.head.appendChild(style);
  })();

  // Snackbar style & function
  (function insertSnackbarCSS() {
    const style = document.createElement("style");
    style.id = "snackbar-style";
    style.textContent = `
      #snackbar {
        visibility: hidden;
        min-width: 250px;
        background-color: rgba(255,255,255,0.95);
        color: #222;
        text-align: center;
        border-radius: 12px;
        padding: 14px 24px;
        position: fixed;
        left: 50%;
        bottom: 30px;
        font-size: 16px;
        transform: translateX(-50%);
        z-index: 1000001;
        box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        opacity: 0;
        transition: opacity 0.4s ease, visibility 0.4s;
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        backdrop-filter: none;
        -webkit-backdrop-filter: none;
        border: 1px solid #ddd;
      }
      @media (prefers-color-scheme: dark) {
        #snackbar {
          background-color: rgba(20,20,20,0.75);
          color: #eee;
          box-shadow: 0 4px 20px rgba(0,0,0,0.6);
          backdrop-filter: blur(12px) saturate(180%);
          -webkit-backdrop-filter: blur(12px) saturate(180%);
          border: 1px solid rgba(255,255,255,0.15);
        }
      }
      #snackbar.show {
        visibility: visible;
        opacity: 1;
      }
    `;
    document.head.appendChild(style);
  })();

  function showSnackbar(message, duration = 3000) {
    let snackbar = document.getElementById("snackbar");
    if (snackbar) {
      snackbar.remove();
    }

    snackbar = document.createElement("div");
    snackbar.id = "snackbar";
    snackbar.textContent = message;

    document.body.appendChild(snackbar);

    setTimeout(() => {
      snackbar.classList.add("show");
    }, 100);

    setTimeout(() => {
      snackbar.classList.remove("show");
      setTimeout(() => {
        if (snackbar.parentNode) snackbar.parentNode.removeChild(snackbar);
      }, 400);
    }, duration + 100);
  }

  function sanitizeText(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.textContent;
  }

  function replaceTextInRange(range, newText) {
    const startNode = range.startContainer;
    const endNode = range.endContainer;
    const startOffset = range.startOffset;
    const endOffset = range.endOffset;

    if (startNode === endNode && startNode.nodeType === Node.TEXT_NODE) {
      const originalText = startNode.textContent;
      startNode.textContent =
        originalText.substring(0, startOffset) +
        newText +
        originalText.substring(endOffset);
    } else {
      const commonAncestor = range.commonAncestorContainer;
      const walker = document.createTreeWalker(
        commonAncestor,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode: (node) =>
            range.intersectsNode(node)
              ? NodeFilter.FILTER_ACCEPT
              : NodeFilter.FILTER_SKIP,
        }
      );

      let node;
      const nodesToUpdate = [];
      while ((node = walker.nextNode())) {
        nodesToUpdate.push(node);
      }

      nodesToUpdate.forEach((textNode, index) => {
        if (index === 0) {
          textNode.textContent =
            textNode.textContent.substring(0, startOffset) + newText;
        } else if (index === nodesToUpdate.length - 1) {
          textNode.textContent = textNode.textContent.substring(endOffset);
        } else {
          textNode.textContent = "";
        }
      });

      nodesToUpdate.forEach((node) => {
        if (node.textContent === "" && node.parentNode) {
          node.parentNode.removeChild(node);
        }
      });
    }
  }

  function createEditorPopup(selectedText, range) {
    const existing = document.getElementById("html-quick-edit-popup");
    if (existing) existing.remove();

    const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;

    const popup = document.createElement("div");
    popup.id = "html-quick-edit-popup";

    Object.assign(popup.style, {
      position: "fixed",
      zIndex: "1000000",
      background: isDarkMode
        ? "rgba(18, 18, 18, 0.75)"
        : "rgba(249, 249, 249, 0.9)",
      color: isDarkMode ? "#e0e0e0" : "#1c1c1c",
      borderRadius: "16px",
      boxShadow: isDarkMode
        ? "0 6px 20px rgba(0,0,0,0.8)"
        : "0 6px 20px rgba(0,0,0,0.15)",
      padding: "20px",
      minWidth: "380px",
      maxWidth: "90vw",
      maxHeight: "70vh",
      opacity: "0",
      visibility: "hidden",
      transform: "scale(0.8)",
      transition: "opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease",
      display: "flex",
      flexDirection: "column",
      fontFamily:
        "SF Pro Text, -apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
      userSelect: "text",

      backdropFilter: isDarkMode ? "blur(20px) saturate(180%)" : "blur(10px)",
      WebkitBackdropFilter: isDarkMode ? "blur(20px) saturate(180%)" : "blur(10px)",
      border: isDarkMode
        ? "1px solid rgba(255, 255, 255, 0.15)"
        : "1px solid rgba(0, 0, 0, 0.1)",

      overflowY: "auto",
    });

    const header = document.createElement("div");
    header.textContent = "Quick Edit by DuyNguyen2k6";
    Object.assign(header.style, {
      fontWeight: "700",
      fontSize: "18px",
      marginBottom: "15px",
      textAlign: "center",
      cursor: "move",
      userSelect: "none",
      color: isDarkMode ? "#ddd" : "#1c1c1e",
      letterSpacing: "0.05em",
    });
    popup.appendChild(header);

    const textarea = document.createElement("textarea");
    textarea.value = selectedText;
    Object.assign(textarea.style, {
      flexGrow: "1",
      width: "100%",
      borderRadius: "16px",
      border: isDarkMode
        ? "1.5px solid rgba(255, 255, 255, 0.3)"
        : "1.5px solid rgba(0, 0, 0, 0.1)",
      backgroundColor: isDarkMode
        ? "rgba(255, 255, 255, 0.1)"
        : "rgba(255, 255, 255, 0.7)",
      color: isDarkMode ? "#eee" : "#1c1c1e",
      fontSize: "16px",
      fontFamily: "inherit",
      padding: "15px",
      resize: "vertical",
      outline: "none",
      transition: "border-color 0.3s ease",
      boxSizing: "border-box",
      minHeight: "130px",
      backdropFilter: isDarkMode ? "blur(12px) saturate(180%)" : "blur(8px)",
      WebkitBackdropFilter: isDarkMode ? "blur(12px) saturate(180%)" : "blur(8px)",

      overflowY: "auto",
    });
    textarea.oninput = () => {
      textarea.style.borderColor = isDarkMode ? "#90caf9" : "#007aff";
      setTimeout(() => {
        textarea.style.borderColor = isDarkMode
          ? "rgba(255, 255, 255, 0.3)"
          : "rgba(0, 0, 0, 0.1)";
      }, 500);
    };
    textarea.onkeydown = (e) => {
      if (e.key === "Tab") {
        e.preventDefault();
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        textarea.value =
          textarea.value.substring(0, start) + "\n" + textarea.value.substring(end);
        textarea.selectionStart = textarea.selectionEnd = start + 1;
      }
    };
    popup.appendChild(textarea);

    const infoText = document.createElement("div");
    infoText.textContent = "Press Enter to save, Escape to cancel, Tab for new line.";
    Object.assign(infoText.style, {
      marginTop: "12px",
      fontSize: "12px",
      color: isDarkMode ? "#999" : "#8e8e93",
      textAlign: "center",
      userSelect: "none",
    });
    popup.appendChild(infoText);

    // Thêm popup vào DOM trước để đo kích thước
    document.body.appendChild(popup);

    // Đo popup và tính vị trí pixel tuyệt đối giữa màn hình
    const popupRect = popup.getBoundingClientRect();
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;

    let currentX = (viewportWidth - popupRect.width) / 2;
    let currentY = (viewportHeight - popupRect.height) / 2;

    // Gán vị trí pixel tuyệt đối, bỏ transform (sẽ có transform scale, nhưng vị trí cố định)
    popup.style.left = `${currentX}px`;
    popup.style.top = `${currentY}px`;
    popup.style.transformOrigin = "center center";

    // Hiệu ứng Zoom & Fade mở popup
    setTimeout(() => {
      popup.style.visibility = "visible";
      popup.style.opacity = "1";
      popup.style.transform = "scale(1)";
    }, 10);

    // Hàm đóng popup với hiệu ứng thu nhỏ và mờ dần
    function closePopup() {
      popup.style.opacity = "0";
      popup.style.transform = "scale(0.8)";
      popup.style.visibility = "hidden";
      setTimeout(() => {
        if (popup.parentNode) {
          popup.parentNode.removeChild(popup);
        }
      }, 300);
    }

    popup.addEventListener("keydown", (e) => {
      if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        try {
          const cleanedText = sanitizeText(textarea.value);
          replaceTextInRange(range, cleanedText);
          showSnackbar("Changes saved!");
          closePopup();
        } catch (error) {
          showSnackbar("Error saving changes.");
          console.error(error);
        }
      } else if (e.key === "Escape") {
        closePopup();
      }
    });

    // Drag functionality
    let isDragging = false,
      initialX = 0,
      initialY = 0,
      currentXPos = currentX,
      currentYPos = currentY;

    header.addEventListener("mousedown", (e) => {
      isDragging = true;

      initialX = e.clientX - currentXPos;
      initialY = e.clientY - currentYPos;

      document.addEventListener("mousemove", onMouseMove);
      document.addEventListener("mouseup", onMouseUp);
    });

    function onMouseMove(e) {
      if (isDragging) {
        e.preventDefault();

        currentXPos = e.clientX - initialX;
        currentYPos = e.clientY - initialY;

        const maxX = window.innerWidth - popup.offsetWidth;
        const maxY = window.innerHeight - popup.offsetHeight;
        currentXPos = Math.max(0, Math.min(currentXPos, maxX));
        currentYPos = Math.max(0, Math.min(currentYPos, maxY));

        popup.style.left = currentXPos + "px";
        popup.style.top = currentYPos + "px";
      }
    }

    function onMouseUp() {
      isDragging = false;
      document.removeEventListener("mousemove", onMouseMove);
      document.removeEventListener("mouseup", onMouseUp);
    }

    textarea.focus();
  }

  // Lắng nghe middle-click để mở popup
  document.addEventListener("mousedown", (e) => {
    if (e.button === 1) { // Middle click
      const selection = window.getSelection();
      if (selection && !selection.isCollapsed) {
        e.preventDefault();
        const range = selection.getRangeAt(0);
        const selectedText = selection.toString();
        createEditorPopup(selectedText, range);
      }
    }
  });
})();