popup-inject

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

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/473443/1374764/popup-inject.js

  1. // @name popup-inject
  2. // @name:zh 弹窗注入
  3. // @description Insert a sidebar button and a popup window into the webpage.
  4. // @description:zh 向网页中插入一个侧边按钮和一个弹窗。
  5. // @namespace https://github.com/pansong291/
  6. // @version 1.0.9
  7. // @author paso
  8. // @license Apache-2.0
  9.  
  10. /**
  11. * @typedef {object} PopupInjectConfig
  12. * @property {string} namespace
  13. * @property {string} [actionName] 侧边按钮文案
  14. * @property {string} [collapse] 折叠 <length-percentage>
  15. * @property {string} [location] 顶部位置 <length-percentage>
  16. * @property {string} [content] DOMString
  17. * @property {string} [style] StyleString
  18. * @property {VoidFunction} [onPopShow]
  19. * @property {VoidFunction} [onPopHide]
  20. */
  21. /**
  22. * @typedef {object} PopupInjectResult
  23. * @property {{
  24. * container: HTMLElement,
  25. * stickyBar: HTMLElement,
  26. * mask: HTMLElement,
  27. * popup: HTMLElement
  28. * }} elem
  29. * @property {{
  30. * createElement: CreateElementFunction,
  31. * excludeClick: ExcludeClickFuction,
  32. * leftKey: LeftKeyFunction<Function>,
  33. * getNumber: GetNumberFunction
  34. * }} func
  35. */
  36. /**
  37. * @typedef {(tag: string, attrs?: Record<string, string>, children?: string | (Node | string)[]) => HTMLElement} CreateElementFunction
  38. */
  39. /**
  40. * @typedef {(included: HTMLElement, excluded: HTMLElement, onClick?: EventListener) => void} ExcludeClickFuction
  41. */
  42. /**
  43. * @template {Function} T
  44. * @typedef {(fn: T) => T} LeftKeyFunction
  45. */
  46. /**
  47. * @typedef {(str?: string) => number | undefined} GetNumberFunction
  48. */
  49. ;(function () {
  50. 'use strict'
  51. const version = 'v1.0.9'
  52.  
  53. /**
  54. * @type CreateElementFunction
  55. */
  56. const createElement = (tag, attrs, children) => {
  57. const el = document.createElement(tag)
  58. if (attrs) Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v))
  59. if (Array.isArray(children)) {
  60. el.append.apply(el, children)
  61. } else if (typeof children === 'string') {
  62. el.innerHTML = children
  63. }
  64. return el
  65. }
  66.  
  67. /**
  68. * @type ExcludeClickFuction
  69. */
  70. const excludeClick = (included, excluded, onClick) => {
  71. const _data = {
  72. excludeDown: false,
  73. inIncluded: false,
  74. inExcluded: false
  75. }
  76. excluded.addEventListener('mousedown', () => (_data.excludeDown = true))
  77. excluded.addEventListener('mouseup', () => (_data.excludeDown = false))
  78. excluded.addEventListener('mouseenter', () => (_data.inExcluded = true))
  79. excluded.addEventListener('mouseleave', () => (_data.inExcluded = false))
  80. included.addEventListener('mouseenter', () => (_data.inIncluded = true))
  81. included.addEventListener('mouseleave', () => (_data.inIncluded = false))
  82. included.addEventListener('click', (e) => {
  83. if (_data.inIncluded && !_data.inExcluded) {
  84. if (_data.excludeDown) {
  85. _data.excludeDown = false
  86. } else {
  87. onClick?.(e)
  88. }
  89. }
  90. })
  91. }
  92.  
  93. /**
  94. * @type LeftKeyFunction<Function>
  95. */
  96. const leftKey = (fn) => {
  97. return (...args) => {
  98. const key = args?.[0]?.button
  99. if (key === 0 || key === void 0) {
  100. fn.apply(this, args)
  101. }
  102. }
  103. }
  104.  
  105. /**
  106. * @type GetNumberFunction
  107. */
  108. const getNumber = (str) => {
  109. const mArr = str?.match(/\d+(\.\d*)?|\.\d+/)
  110. return mArr?.length ? parseFloat(mArr[0]) : void 0
  111. }
  112.  
  113. /**
  114. * @param {string} originStyleContent
  115. * @param {string} ancestor
  116. * @returns {string}
  117. */
  118. const addCSSAncestor = (originStyleContent, ancestor) => {
  119. originStyleContent = '}' + originStyleContent
  120. return originStyleContent.replaceAll(/}([^{}]+?){/g, (_, p1) => {
  121. return `}\n${p1.trim().split(',').map(it => `${ancestor} ${it}`).join(', ')} {`
  122. }).substring(1)
  123. }
  124.  
  125. /**
  126. * @param {HTMLElement} el
  127. * @param {(e: MouseEvent, d: WithDragData) => void} [onMove]
  128. * @param {(e: MouseEvent, d: WithDragData) => void} [onClick]
  129. */
  130. const withDrag = (el, onMove, onClick) => {
  131. /**
  132. * @typedef {{innerOffsetY: number, outerHeight: number, justClick: boolean}} WithDragData
  133. */
  134. const _data = {
  135. outerHeight: 0,
  136. innerOffsetY: 0,
  137. justClick: false
  138. }
  139.  
  140. const onElMouseMove = (e) => {
  141. _data.justClick = false
  142. onMove?.(e, _data)
  143. }
  144.  
  145. const onElMouseUp = leftKey(() => {
  146. document.removeEventListener('mousemove', onElMouseMove)
  147. document.removeEventListener('mouseup', onElMouseUp)
  148. })
  149.  
  150. el.addEventListener(
  151. 'mousedown',
  152. leftKey((e) => {
  153. _data.justClick = true
  154. const elComputedStyle = window.getComputedStyle(el)
  155. _data.innerOffsetY = e.pageY - getNumber(elComputedStyle.top)
  156. _data.outerHeight =
  157. el.clientHeight + getNumber(elComputedStyle.borderTopWidth) + getNumber(elComputedStyle.borderBottomWidth)
  158. document.addEventListener('mousemove', onElMouseMove)
  159. document.addEventListener('mouseup', onElMouseUp)
  160. })
  161. )
  162.  
  163. el.addEventListener(
  164. 'mouseup',
  165. leftKey((e) => {
  166. if (_data.justClick) {
  167. onClick?.(e, _data)
  168. _data.justClick = false
  169. }
  170. onElMouseUp()
  171. e.stopPropagation()
  172. })
  173. )
  174. }
  175.  
  176. /**
  177. * @param {PopupInjectConfig} config
  178. * @returns {string}
  179. */
  180. const getBaseStyle = (config) => `
  181. <style>
  182. :not(svg *) {
  183. align-content: revert;
  184. align-items: revert;
  185. align-self: revert;
  186. animation: revert;
  187. background: revert;
  188. border: revert;
  189. border-radius: revert;
  190. box-shadow: revert;
  191. box-sizing: border-box;
  192. color: inherit;
  193. cursor: inherit;
  194. display: revert;
  195. flex: revert;
  196. float: revert;
  197. font: inherit;
  198. height: revert;
  199. inset: revert;
  200. justify-content: revert;
  201. justify-items: revert;
  202. justify-self: revert;
  203. letter-spacing: inherit;
  204. list-style: inherit;
  205. margin: revert;
  206. mask: revert;
  207. max-height: revert;
  208. max-width: revert;
  209. min-height: revert;
  210. min-width: revert;
  211. offset: revert;
  212. opacity: revert;
  213. outline: revert;
  214. overflow: revert;
  215. overscroll-behavior: revert;
  216. padding: revert;
  217. pointer-events: inherit;
  218. position: revert;
  219. text-align: inherit;
  220. text-shadow: inherit;
  221. text-transform: inherit;
  222. transform: revert;
  223. transition: revert;
  224. user-select: revert;
  225. visibility: inherit;
  226. width: revert;
  227. z-index: revert;
  228. }
  229. *::before, *::after {
  230. content: none;
  231. }
  232. *::-webkit-scrollbar {
  233. width: 8px;
  234. height: 8px;
  235. }
  236. *::-webkit-scrollbar-thumb {
  237. border-radius: 4px;
  238. background-color: rgba(0, 0, 0, 0.5);
  239. }
  240. *::-webkit-scrollbar-track {
  241. border-radius: 4px;
  242. background-color: transparent;
  243. }
  244. .flex {
  245. display: flex;
  246. flex-direction: row;
  247. align-items: stretch;
  248. justify-content: flex-start;
  249. }
  250. .flex.col {
  251. flex-direction: column;
  252. }
  253. .container {
  254. all: revert;
  255. color: black;
  256. font-size: 14px;
  257. line-height: 1.5;
  258. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
  259. font-style: normal;
  260. font-weight: normal;
  261. }
  262. .monospace {
  263. font-family: v-mono, "JetBrains 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";
  264. }
  265. .sticky-bar {
  266. position: fixed;
  267. top: ${config.location};
  268. left: 0;
  269. transform: translateX(calc(12px - ${config.collapse}));
  270. z-index: 99999999;
  271. background: #3d7fff;
  272. color: white;
  273. padding: 4px 12px 4px 6px;
  274. cursor: pointer;
  275. user-select: none;
  276. border-radius: 0 12px 12px 0;
  277. box-shadow: 0 2px 4px 1px #0006;
  278. transition: transform 0.5s ease;
  279. }
  280. .sticky-bar:hover {
  281. transform: none;
  282. }
  283. .mask {
  284. position: fixed;
  285. inset: 0;
  286. padding: 24px;
  287. overflow: auto;
  288. z-index: 99999999;
  289. background-color: rgba(0, 0, 0, 0.4);
  290. display: flex;
  291. align-items: center;
  292. justify-content: center;
  293. opacity: 0;
  294. pointer-events: none;
  295. transition: opacity .6s;
  296. }
  297. .container.open .mask {
  298. opacity: 1;
  299. pointer-events: all;
  300. }
  301. .popup {
  302. position: relative;
  303. margin: auto;
  304. padding: 16px;
  305. background: #f0f2f5;
  306. border-radius: 2px;
  307. box-shadow: 0 1px 12px 2px rgba(0, 0, 0, 0.4);
  308. transform: scale(0);
  309. transition: transform .3s;
  310. }
  311. .container.open .popup {
  312. transform: scale(1);
  313. }
  314. label {
  315. user-select: none;
  316. }
  317. textarea {
  318. resize: vertical;
  319. }
  320. .input, .button {
  321. height: 32px;
  322. transition: all 0.3s, height 0s;
  323. }
  324. .button {
  325. user-select: none;
  326. display: flex;
  327. align-items: center;
  328. justify-content: center;
  329. padding: 4px 16px;
  330. color: #fff;
  331. border: none;
  332. border-radius: 2px;
  333. background: #3d7fff;
  334. text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
  335. box-shadow: 0 2px 0 rgba(0, 0, 0, 0.05);
  336. }
  337. .button:hover, .button:focus {
  338. border-color: #669eff;
  339. background: #669eff;
  340. }
  341. .button:active {
  342. border-color: #295ed9;
  343. background: #295ed9;
  344. }
  345. .input {
  346. padding: 4px 8px;
  347. background: white;
  348. border: 1px solid #d9d9d9;
  349. border-radius: 2px;
  350. }
  351. .input:hover, .input:focus {
  352. border-color: #669eff;
  353. }
  354. .input:focus-visible {
  355. outline: none;
  356. }
  357. .input:focus {
  358. box-shadow: 0 0 0 2px rgba(61, 127, 255, 0.2);
  359. }
  360. ${config.style}
  361. </style>`
  362.  
  363. /**
  364. * @param {PopupInjectConfig} config
  365. * @param {(value: PopupInjectResult) => void} resolve
  366. */
  367. const _injectHtml = (config, resolve) => {
  368. const anchorId = 'x' + Math.floor(Math.random() * 100_000_000).toString(16)
  369. const styleContent = addCSSAncestor(getBaseStyle(config).replaceAll(/<\/?style>/g, ''), `#${anchorId}`)
  370. document.head.insertAdjacentHTML('beforeend', `<style data-namespace='${config.namespace}'>${styleContent}</style>`)
  371. const stickyBar = createElement('div', { class: 'sticky-bar' }, config.actionName)
  372. const popup = createElement('div', { class: 'popup flex col' }, config.content)
  373. const mask = createElement('div', { class: 'mask' }, [popup])
  374. const container = createElement('div', { class: 'container' }, [stickyBar, mask])
  375. const anchor = createElement('div', { id: anchorId, 'data-namespace': config.namespace, 'data-version': version }, [container])
  376.  
  377. excludeClick(mask, popup, () => {
  378. container.classList.remove('open')
  379. config.onPopHide?.()
  380. })
  381.  
  382. withDrag(
  383. stickyBar,
  384. (e, d) => {
  385. requestAnimationFrame(() => {
  386. const height = document.documentElement.clientHeight - d.outerHeight
  387. const newTop = e.pageY - d.innerOffsetY
  388. if (newTop <= 0) stickyBar.style.top = '0'
  389. else if (newTop > height) stickyBar.style.top = `${height}px`
  390. else stickyBar.style.top = `${newTop}px`
  391. })
  392. },
  393. () => {
  394. container.classList.add('open')
  395. config.onPopShow?.()
  396. }
  397. )
  398.  
  399. document.body.append(anchor)
  400. // ---- other code
  401. resolve?.({
  402. elem: {
  403. container, stickyBar, mask, popup
  404. },
  405. func: {
  406. createElement, excludeClick, leftKey, getNumber
  407. }
  408. })
  409. }
  410.  
  411. /**
  412. * @param {PopupInjectConfig} config
  413. * @returns {PopupInjectConfig}
  414. */
  415. const _checkConfig = (config) => {
  416. if (!config) throw new Error('config is required. you should call window.paso.injectPopup(config)')
  417. if (!config.namespace) throw new Error('config.namespace is required and it cannot be empty.')
  418. if (!/^[-\w]+$/.test(config.namespace)) throw new Error('config.namespace must match the regex /^[-\\w]+$/.')
  419. return config
  420. }
  421.  
  422. if (!window.paso || !(window.paso instanceof Object)) window.paso = {}
  423. /**
  424. * @param {PopupInjectConfig} config
  425. * @returns {Promise<PopupInjectResult>}
  426. */
  427. window.paso.injectPopup = (config) => {
  428. const _config = Object.assign(
  429. {
  430. namespace: '',
  431. actionName: 'Action',
  432. collapse: '100%',
  433. location: '25%',
  434. content: '<label>Hello World</label>',
  435. style: '',
  436. onPopShow() {
  437. },
  438. onPopHide() {
  439. }
  440. },
  441. _checkConfig(config)
  442. )
  443. return new Promise((resolve) => _injectHtml(_config, resolve))
  444. }
  445. })()