CSS 样式注入

插入自定义 CSS 样式到任意网址

// ==UserScript==
// @name         CSS 样式注入
// @namespace    https://diaoqi.gitee.io/blog/
// @version      0.5
// @description  插入自定义 CSS 样式到任意网址
// @author       掉漆
// @license      MIT
// @match        *
// @include      *
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==
(function () {
  'use strict';
  // 代码来源于Youth霖,原地址:https://gist.github.com/youthlin/c4c08ffe4273ca7ebbf759289cef9964
  // 本代码简化源代码部分功能、优化部分展示逻辑、以及添加部分功能。
  const SETTING_KEY = 'cssMap'
  const RAND = (Math.random() * 100000).toFixed(0)

  // 实现类似jQuery中的on方法
  const on = function (eventName, selector, handler) {
    // http://youmightnotneedjquery.com/
    document.addEventListener(eventName, function (e) {
      for (let target = e.target; target && target !== this; target = target.parentNode) {
        if (target.matches !== undefined && target.matches(selector)) {
          handler.call(target, e);
          break;
        }
        if (target.tagName === 'HTML') {
          break;
        }
      }
    }, false);
  }

  function start() {
    const cssMap = cssValue()
    const html = document.getElementsByTagName('html')[0]
    const inject = `<div id="inject-watermark-${RAND}" class="inject-watermark"></div><style>
    #inject-watermark-${RAND} {position: fixed;left: 0;top: 0;right: 0;bottom: 0;z-index: 1;pointer-events: none;}
    #inject-css-${RAND} {display: none;position: fixed;left: 0;top: 0;right: 0;bottom: 0;z-index: 999;background-color: rgba(0,0,0,.8);justify-content: center;align-items: center;font-size: 16px;}
    #inject-css-setting {max-height: 90%;overflow: auto;color: #000;}
    #inject-css-setting button {border: 1px solid #ccc;background-color: #fff;cursor: pointer;padding: 4px;}
    #inject-css-setting table {border-collapse: collapse;background-color: #fff;}
    #inject-css-setting th, #inject-css-setting td {border: 1px solid #ccc;padding: .5em;max-width: 50vw;overflow: auto;}
    #inject-css-setting tr.active {font-weight: bold;}
    #inject-css-setting td.active:after {content: ' ⭐️';}
    #inject-css-setting tr:nth-child(even) {background-color: #f2f2f2;}
    #inject-css-setting tr:hover {background-color: #ddd;}
    </style><div id="inject-css-${RAND}"></div>`

    html.insertAdjacentHTML('beforeend', inject)
    const wrapper = document.getElementById(`inject-css-${RAND}`)

    // 点击设置按钮事件
    GM_registerMenuCommand('设置CSS规则', function (e) {
      wrapper.style.display = 'flex'
    }, 's')

    // 渲染当前规则
    render(cssMap, wrapper)

    // 编辑规则详情
    on('click', '.inject-css-edit', function (e) {
      // GM.setClipboard(e.target.dataset.key, "text");
      document.querySelector('#inject-css-url').value = e.target.dataset.key
      document.querySelector('#inject-css-value').value = e.target.dataset.value
    })
    // 删除规则
    on('click', '.inject-css-delete', function (e) {
      const res = confirm(`确认删除${e.target.dataset.key}吗?`);
      if (res === true) {
        const deleteKey = e.target.dataset.key
        cssMap.delete(deleteKey)
        save(cssMap)
        render(cssMap, wrapper)
      }
    })
    // 添加规则
    on('click', '.inject-css-add', function (e) {
      e.preventDefault()
      const k = document.querySelector('#inject-css-url').value.trim()
      const v = document.querySelector('#inject-css-value').value.trim()
      if (k !== '' && v !== '') {
        try {
          new RegExp(k)
        } catch (e) {
          alert('`' + k + '`: 不是有效的正则表达式. error=' + e)
          return
        }
        cssMap.set(k, v)
        save(cssMap)
        render(cssMap, wrapper)
      }
    })
    // 关闭按钮
    on('click', '.inject-css-hide', function () {
      wrapper.style.display = 'none'
    })
    // 键盘退出按钮
    on('keyup', 'html', function (e) {
      if (e.code === 'Escape') {
        wrapper.style.display = 'none'
      }
    })
    // 默认隐藏界面
    document.querySelector('.inject-css-hide').click()
  }

  // 获取保存的规则信息
  function cssValue() {
    const settingValue = GM_getValue(SETTING_KEY, '{}');
    let cssMap = JSON.parse(settingValue) // url regex -> css value
    cssMap = new Map(Object.entries(cssMap)) // to Map
    return cssMap
  }

  // 保存规则
  function save(cssMap) {
    const s = JSON.stringify(Object.fromEntries(cssMap));// Map 需要先转为 Object 才能序列化为 JSON
    GM_setValue(SETTING_KEY, s)
  }

  // 渲染当前CSS规则 渲染配置的表格
  function render(cssMap, wrapper) {
    const url = window.location.href
    let injectCss = ``
    let tableBody = ''
    // 遍历所有规则,渲染整个表格。若找到当前页规则,保存在injectCss中,最后统一渲染。
    for (const entry of cssMap) {
      try {
        let active = ''
        if (new RegExp(entry[0]).test(url)) {
          injectCss += entry[1] + "\n"
          active = 'active'
        }
        tableBody += `<tr class="${active}">
          <td class="${active}"><code>${entry[0]}</code></td>
          <td><pre style="margin-bottom:0;overflow: auto;height:70px;border:1px solid #aaa;font-size:12px">${entry[1]}</pre></td>
          <td>
            <button data-key="${entry[0]}" data-value="${entry[1]}" class="inject-css-edit">编辑</button>
            <button data-key="${entry[0]}" class="inject-css-delete">删除</button>
          </td></tr>`
      } catch (e) {
        cssMap.delete(entry[0])
      }
    }
    wrapper.innerHTML = `<style>${injectCss}</style><div id="inject-css-setting"><button class="inject-css-hide">关闭(Esc)</button>
      <table><tbody><tr><th>URL 正则</th><th>注入 CSS</th><th>操作</th></tr>
      ${tableBody}
      <tr><td><label><input type="text" id="inject-css-url" placeholder="点号斜线记得转义"></label></td>
      <td><textarea id="inject-css-value" cols="100" rows="3" placeholder="html { background-color: #ccc; }"></textarea></td>
      <td><button class="inject-css-add">添加</button></td></tr>
      </tbody></table></div>`
  }

  // 延时执行当前脚本
  setTimeout(start, 2000)

})();