copy selector

Gets the shortest selector for a page element. The effect is the same as Chrome DevTools Copy Selector.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         copy selector
// @namespace    https://github.com/Allen-1998
// @version      0.4
// @description  Gets the shortest selector for a page element. The effect is the same as Chrome DevTools Copy Selector.
// @author       Allen-1998
// @match        *://*/*
// @license      BSD-3-Clause license
// ==/UserScript==

(function () {
  "use strict";
  // style
  const head = document.querySelector("head");
  const style = document.createElement("style");
  style.setAttribute("type", "text/css");
  style.innerText = `
    .copy-selector-hover {
      background: #00f3;
    }
    .copy-selector-focus {
      background: #0f03;
    }

    #copy-success-message {
      position: fixed;
      top: 0;
      left: 50%;
      transform: translate(-50%, -150%);
      padding: 0 40px;
      border-radius: 4px;
      height: 30px;
      line-height: 30px;
      color: #67c23a;
      background-color: #e1f3d8;
      font-size: 14px !important;
      outline: 1px solid #67c23a66 !important;
      pointer-events: none !important;
      z-index: 2147483647;
    }
    .show-copy-success-message {
      animation: copySuccessMessage 2s;
    }

    #copy-selector-panel {
      position: fixed;
      top: 0;
      right: 0;
      background: #fff;
      box-shadow: 0 0 5px 5px #0002;
      padding: 10px;
      display: flex;
      align-items: center;
      font-size: 14px !important;
      outline-style: none !important;
      z-index: 2147483647;
    }
    #copy-selector-switch {
      position: relative;
      cursor: pointer;
      appearance: none;
      width: 24px;
      height: 14px;
      border: 1px solid #999;
      background: #999;
      border-radius: 10px;
      transition: background-color 0.1s, border 0.1s;
      outline-style: none !important;
    }
    #copy-selector-switch:after {
      content: " ";
      position: absolute;
      left: 0;
      top: 0;
      height: 12px;
      width: 12px;
      border-radius: 50%;
      background: #fff;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
      transition: transform 0.35s cubic-bezier(0.4, 0.4, 0.25, 1.35);
    }
    #copy-selector-switch:checked {
      background: #67c23a;
      border: 1px solid #67c23a;
    }
    #copy-selector-switch:checked:after {
      transform: translateX(10px);
    }

    @keyframes copySuccessMessage {
      0% {
        transform: translate(-50%, -150%);
      }
      10% {
        transform: translate(-50%, 100%);
      }
      70% {
        transform: translate(-50%, 100%);
        opacity: 1;
      }
      100% {
        transform: translate(-50%, -150%);
        opacity: 0;
      }
    }
  `;
  head.append(style);

  // copy success message
  const message = document.createElement("div");
  message.setAttribute("id", "copy-success-message");
  message.innerText = "Copy success!";
  const body = document.querySelector("body");
  body.append(message);

  // switch
  const panel = document.createElement("div");
  panel.setAttribute("id", "copy-selector-panel");
  panel.innerHTML =
    'copy selector:<input type="checkbox" id="copy-selector-switch" />';
  body.append(panel);

  const copySelectorSwitch = document.querySelector("#copy-selector-switch");
  copySelectorSwitch.onclick = function (e) {
    if (e.target.checked) {
      const copySelectorModeStyle = document.createElement("style");
      copySelectorModeStyle.setAttribute("type", "text/css");
      copySelectorModeStyle.setAttribute("id", "copy-selector-mode-style");
      copySelectorModeStyle.innerText = `
        * {
          cursor: pointer;
          outline: 1px solid #000 !important;
        }
      `;
      head.append(copySelectorModeStyle);
      document.addEventListener("click", clickListenerFn, true);
      document.addEventListener("mousemove", hoverListenerFn, true);
    } else {
      head.removeChild(document.querySelector("#copy-selector-mode-style"));
      document
        .querySelector(lastClickelector)
        ?.classList.remove("copy-selector-focus");
      document
        .querySelector(lastHoverSelector)
        ?.classList.remove("copy-selector-hover");
      document.removeEventListener("click", clickListenerFn, true);
      document.removeEventListener("mousemove", hoverListenerFn, true);
    }
  };

  // copy
  function copy(value) {
    const message = document.querySelector("#copy-success-message");
    message.classList.remove("show-copy-success-message");
    navigator.clipboard.writeText(value).then(() => {
      message.classList.add("show-copy-success-message");
    });
  }

  // cssPath
  function cssPath(node, optimized = true) {
    if (node.nodeType !== Node.ELEMENT_NODE) {
      return "";
    }

    const steps = [];
    let contextNode = node;
    while (contextNode) {
      const step = cssPathStep(
        contextNode,
        Boolean(optimized),
        contextNode === node
      );
      if (!step) {
        break;
      }
      steps.unshift(step);
      if (step.optimized) {
        break;
      }
      contextNode = contextNode.parentNode;
    }

    return steps.join(" > ");
  }

  function cssPathStep(node, optimized, isTargetNode) {
    if (node.nodeType !== Node.ELEMENT_NODE) {
      return null;
    }

    const id = node.getAttribute("id");
    if (optimized) {
      if (id) {
        return new Step(idSelector(id), true);
      }
      const nodeNameLower = node.nodeName.toLowerCase();
      if (
        nodeNameLower === "body" ||
        nodeNameLower === "head" ||
        nodeNameLower === "html"
      ) {
        return new Step(node.nodeName.toLowerCase(), true);
      }
    }
    const nodeName = node.nodeName.toLowerCase();

    if (id) {
      return new Step(nodeName + idSelector(id), true);
    }
    const parent = node.parentNode;
    if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {
      return new Step(nodeName, true);
    }

    function prefixedElementClassNames(node) {
      const classAttribute = node.getAttribute("class");
      if (!classAttribute) {
        return [];
      }

      return classAttribute
        .split(/\s+/g)
        .filter(Boolean)
        .map(function (name) {
          return "$" + name;
        });
    }

    function idSelector(id) {
      return "#" + CSS.escape(id);
    }

    const prefixedOwnClassNamesArray = prefixedElementClassNames(node);
    let needsClassNames = false;
    let needsNthChild = false;
    let ownIndex = -1;
    let elementIndex = -1;
    const siblings = parent.children;
    for (
      let i = 0;
      siblings && (ownIndex === -1 || !needsNthChild) && i < siblings.length;
      ++i
    ) {
      const sibling = siblings[i];
      if (sibling.nodeType !== Node.ELEMENT_NODE) {
        continue;
      }
      elementIndex += 1;
      if (sibling === node) {
        ownIndex = elementIndex;
        continue;
      }
      if (needsNthChild) {
        continue;
      }
      if (sibling.nodeName.toLowerCase() !== nodeName) {
        continue;
      }

      needsClassNames = true;
      const ownClassNames = new Set(prefixedOwnClassNamesArray);
      if (!ownClassNames.size) {
        needsNthChild = true;
        continue;
      }
      const siblingClassNamesArray = prefixedElementClassNames(sibling);
      for (let j = 0; j < siblingClassNamesArray.length; ++j) {
        const siblingClass = siblingClassNamesArray[j];
        if (!ownClassNames.has(siblingClass)) {
          continue;
        }
        ownClassNames.delete(siblingClass);
        if (!ownClassNames.size) {
          needsNthChild = true;
          break;
        }
      }
    }

    let result = nodeName;
    if (
      isTargetNode &&
      nodeName.toLowerCase() === "input" &&
      node.getAttribute("type") &&
      !node.getAttribute("id") &&
      !node.getAttribute("class")
    ) {
      result += "[type=" + CSS.escape(node.getAttribute("type") || "") + "]";
    }
    if (needsNthChild) {
      result += ":nth-child(" + (ownIndex + 1) + ")";
    } else if (needsClassNames) {
      for (const prefixedName of prefixedOwnClassNamesArray) {
        result += "." + CSS.escape(prefixedName.slice(1));
      }
    }

    return new Step(result, false);
  }

  class Step {
    value;
    optimized;
    constructor(value, optimized) {
      this.value = value;
      this.optimized = optimized || false;
    }

    toString() {
      return this.value;
    }
  }

  // eventListener
  let lastClickelector;
  function clickListenerFn(e) {
    if (e.target.id.startsWith("copy-selector")) return;
    e.preventDefault();
    e.stopImmediatePropagation();
    e.stopPropagation();
    const el = e.target;
    el.classList.remove("copy-selector-hover");
    document
      .querySelector(lastClickelector)
      ?.classList.remove("copy-selector-focus");
    const clickSelector = cssPath(el);
    copy(clickSelector);
    lastClickelector = clickSelector;
    el.classList.add("copy-selector-focus");
  }

  let lastHoverSelector;
  function hoverListenerFn(e) {
    const el = e.target;
    document
      .querySelector(lastHoverSelector)
      ?.classList.remove("copy-selector-hover");
    const hoverSelector = cssPath(el);
    lastHoverSelector = hoverSelector;
    el.classList.add("copy-selector-hover");
  }
})();