popup-inject

向网页中插入一个侧边按钮和一个弹窗

目前為 2023-08-19 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/473443/1237616/popup-inject.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// @name         popup-inject
// @description  向网页中插入一个侧边按钮和一个弹窗
// @version      1.0.5
// @author       paso

;(function () {
  'use strict'

  const _injectHtml = (config, resolve) => {
    document.head.insertAdjacentHTML(
      'beforeend',
      `
      <style>
        .${config.namespace} {
            color: black;
        }
        .${config.namespace} * {
            box-sizing: border-box;
        }
        .${config.namespace} *::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }
        .${config.namespace} *::-webkit-scrollbar-thumb {
            border-radius: 4px;
            background-color: rgba(0, 0, 0, 0.5);
        }
        .${config.namespace} *::-webkit-scrollbar-track {
            border-radius: 4px;
            background-color: transparent;
        }
        .${config.namespace} .flex {
            display: flex;
            flex-direction: row;
            align-items: stretch;
            justify-content: flex-start;
        }
        .${config.namespace} .flex.col {
            flex-direction: column;
        }
        .${config.namespace} .sticky-bar {
            position: fixed;
            top: ${config.location};
            left: 0;
            transform: translateX(calc(12px - ${config.collapse}));
            z-index: 99999999;
            background: #3D7FFF;
            color: white;
            padding: 2px 10px 2px 4px;
            cursor: pointer;
            user-select: none;
            border-radius: 0 12px 12px 0;
            box-shadow: 0 2px 4px 1px #0006;
            transition: transform 0.5s ease;
        }
        .${config.namespace} .sticky-bar:hover {
            transform: none;
        }
        .${config.namespace} .mask {
            position: fixed;
            inset: 0;
            padding: 24px;
            overflow: auto;
            z-index: 99999999;
            background-color: rgba(0, 0, 0, 0.4);
            display: flex;
            align-items: center;
            justify-content: center;
            opacity: 0;
            pointer-events: none;
            transition: opacity .6s;
        }
        .${config.namespace}.open .mask {
            opacity: 1;
            pointer-events: all;
        }
        .${config.namespace} .popup {
            position: relative;
            margin: auto;
            padding: 16px;
            background: #f0f2f5;
            border-radius: 2px;
            box-shadow: 0 1px 12px 2px rgba(0, 0, 0, 0.4);
            transform: scale(0);
            transition: transform .3s;
        }
        .${config.namespace}.open .popup {
            transform: scale(1);
        }
        .${config.namespace} label {
            margin-top: .5em;
            margin-bottom: 0;
            user-select: none;
        }
        .${config.namespace} .monospace {
            font-family: v-mono, Consolas, SFMono-Regular, Menlo, Courier, v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
        }
        .${config.namespace} .button {
            user-select: none;
            min-width: unset;
            min-height: unset;
            margin: unset;
            padding: 4px 16px;
            color: #fff;
            border: 1px solid #3D7FFF;
            border-radius: 2px;
            background: #3D7FFF;
            text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
            box-shadow: 0 2px 0 rgba(0, 0, 0, 0.05);
        }
        .${config.namespace} textarea {
            background: white;
            resize: vertical;
        }
        .${config.namespace} .input, .${config.namespace} .button {
            height: 32px;
            transition: all 0.3s, height 0s;
        }
        .${config.namespace} .button:before, .${config.namespace} .button:after {
            content: none;
        }
        .${config.namespace} .button:hover, .${config.namespace} .button:focus {
            border-color: #669eff;
            background: #669eff;
        }
        .${config.namespace} .button:active {
            border-color: #295ed9;
            background: #295ed9;
        }
        .${config.namespace} .input {
            padding: 4px 8px;
            background: white;
            border: 1px solid #d9d9d9;
            border-radius: 2px;
        }
        .${config.namespace} .input:hover {
            border-color: #669eff;
            outline: 0;
        }
        .${config.namespace} .input:focus {
            border-color: #669eff;
            box-shadow: 0 0 0 2px rgba(61, 127, 255, 0.2);
            border-right-width: 1px;
            outline: 0;
        }
        ${config.style.replaceAll(/<\/?style>/g, '')}
      </style>`
    )
    const container = createElement('div', { class: config.namespace })
    const stickyBar = createElement('div', { class: 'sticky-bar' }, config.actionName)
    const mask = createElement('div', { class: 'mask' })
    const popup = createElement('div', { class: 'popup flex col' }, config.content)
    container.append(stickyBar, mask)
    mask.append(popup)

    mask.onclick = () => {
      container.classList.remove('open')
      config.onPopHide && config.onPopHide()
    }
    popup.onclick = (e) => e.stopPropagation()

    const _data = {
      stickyBarHeight: 0,
      innerOffset: 0,
      stickyBarClick: false
    }

    const stickyMouseMove = (e) => {
      _data.stickyBarClick = false
      requestAnimationFrame(() => {
        let height = document.documentElement.clientHeight - _data.stickyBarHeight
        let newTop = e.pageY - _data.innerOffset
        if (newTop >= 0 && newTop <= height) {
          stickyBar.style.top = `${newTop}px`
        }
      })
    }

    const stickyMouseUp = leftKey(() => {
      document.removeEventListener('mousemove', stickyMouseMove)
      document.removeEventListener('mouseup', stickyMouseUp)
    })

    stickyBar.onmousedown = leftKey((e) => {
      _data.stickyBarClick = true
      const stickyBarStyle = window.getComputedStyle(stickyBar)
      _data.innerOffset = e.pageY - getNumber(stickyBarStyle.top)
      _data.stickyBarHeight =
        stickyBar.clientHeight + getNumber(stickyBarStyle.borderTopWidth) + getNumber(stickyBarStyle.borderBottomWidth)
      document.addEventListener('mousemove', stickyMouseMove)
      document.addEventListener('mouseup', stickyMouseUp)
    })

    stickyBar.onmouseup = leftKey((e) => {
      if (_data.stickyBarClick) {
        _data.stickyBarClick = false
        container.classList.add('open')
        config.onPopShow && config.onPopShow()
      }
      stickyMouseUp()
      e.stopPropagation()
    })

    document.body.append(container)
    // ---- other code
    resolve && resolve({ container, stickyBar, mask, popup })
  }

  function createElement(tag, attrs, children) {
    const el = document.createElement(tag)
    if (attrs) {
      Object.entries(attrs).forEach(([k, v]) => {
        el.setAttribute(k, v)
      })
    }
    if (children instanceof HTMLElement) {
      el.append(children)
    } else if (typeof children === 'string') {
      el.innerHTML = children
    }
    return el
  }

  function leftKey(fn) {
    return (...args) => {
      let key = args && args[0] && args[0].button
      if (key === 0 || key === void 0) {
        fn.apply(this, args)
      }
    }
  }

  function getNumber(str) {
    if (str) {
      let mArr = str.match(/\d+(\.\d*)?|\.\d+/)
      if (mArr && mArr.length) {
        return parseFloat(mArr[0])
      }
    }
    return void 0
  }

  function _checkConfig(config) {
    if (!config) throw new Error('config is required. you should call window.paso.injectPopup(config)')
    if (!config.namespace) throw new Error('config.namespace is required and it cannot be empty.')
    if (!/^[-\w]+$/.test(config.namespace)) throw new Error('config.namespace must match the regex /^[-\\w]+$/.')
  }

  if (!window.paso || !(window.paso instanceof Object)) window.paso = {}
  window.paso.injectPopup = (config) => {
    _checkConfig(config)
    const _config = Object.assign(
      {},
      {
        namespace: '',
        actionName: 'Action',
        collapse: '100%',
        location: '25%',
        content: '<label>Hello World</label>',
        style: '',
        onPopShow() {},
        onPopHide() {}
      },
      config
    )
    return new Promise((resolve) => {
      _injectHtml(_config, resolve)
    })
  }
})()