Greasy Fork 支持简体中文。

popup-inject

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

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

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

  1. // @name popup-inject
  2. // @description 向网页中插入一个侧边按钮和一个弹窗
  3. // @version 1.0.6
  4. // @author paso
  5.  
  6. ;(function () {
  7. 'use strict'
  8.  
  9. const _injectHtml = (config, resolve) => {
  10. document.head.insertAdjacentHTML(
  11. 'beforeend',
  12. `
  13. <style data-namespace="${config.namespace}">
  14. .${config.namespace} {
  15. color: black;
  16. }
  17. .${config.namespace} * {
  18. box-sizing: border-box;
  19. }
  20. .${config.namespace} *::-webkit-scrollbar {
  21. width: 8px;
  22. height: 8px;
  23. }
  24. .${config.namespace} *::-webkit-scrollbar-thumb {
  25. border-radius: 4px;
  26. background-color: rgba(0, 0, 0, 0.5);
  27. }
  28. .${config.namespace} *::-webkit-scrollbar-track {
  29. border-radius: 4px;
  30. background-color: transparent;
  31. }
  32. .${config.namespace} .flex {
  33. display: flex;
  34. flex-direction: row;
  35. align-items: stretch;
  36. justify-content: flex-start;
  37. }
  38. .${config.namespace} .flex.col {
  39. flex-direction: column;
  40. }
  41. .${config.namespace} .sticky-bar {
  42. position: fixed;
  43. top: ${config.location};
  44. left: 0;
  45. transform: translateX(calc(12px - ${config.collapse}));
  46. z-index: 99999999;
  47. background: #3D7FFF;
  48. color: white;
  49. padding: 2px 10px 2px 4px;
  50. cursor: pointer;
  51. user-select: none;
  52. border-radius: 0 12px 12px 0;
  53. box-shadow: 0 2px 4px 1px #0006;
  54. transition: transform 0.5s ease;
  55. }
  56. .${config.namespace} .sticky-bar:hover {
  57. transform: none;
  58. }
  59. .${config.namespace} .mask {
  60. position: fixed;
  61. inset: 0;
  62. padding: 24px;
  63. overflow: auto;
  64. z-index: 99999999;
  65. background-color: rgba(0, 0, 0, 0.4);
  66. display: flex;
  67. align-items: center;
  68. justify-content: center;
  69. opacity: 0;
  70. pointer-events: none;
  71. transition: opacity .6s;
  72. }
  73. .${config.namespace}.open .mask {
  74. opacity: 1;
  75. pointer-events: all;
  76. }
  77. .${config.namespace} .popup {
  78. position: relative;
  79. margin: auto;
  80. padding: 16px;
  81. background: #f0f2f5;
  82. border-radius: 2px;
  83. box-shadow: 0 1px 12px 2px rgba(0, 0, 0, 0.4);
  84. transform: scale(0);
  85. transition: transform .3s;
  86. }
  87. .${config.namespace}.open .popup {
  88. transform: scale(1);
  89. }
  90. .${config.namespace} label {
  91. margin-top: .5em;
  92. margin-bottom: 0;
  93. user-select: none;
  94. }
  95. .${config.namespace} .monospace {
  96. 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";
  97. }
  98. .${config.namespace} .button {
  99. user-select: none;
  100. min-width: unset;
  101. min-height: unset;
  102. margin: unset;
  103. padding: 4px 16px;
  104. color: #fff;
  105. border: 1px solid #3D7FFF;
  106. border-radius: 2px;
  107. background: #3D7FFF;
  108. text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
  109. box-shadow: 0 2px 0 rgba(0, 0, 0, 0.05);
  110. }
  111. .${config.namespace} textarea {
  112. background: white;
  113. resize: vertical;
  114. }
  115. .${config.namespace} .input, .${config.namespace} .button {
  116. height: 32px;
  117. transition: all 0.3s, height 0s;
  118. }
  119. .${config.namespace} .button:before, .${config.namespace} .button:after {
  120. content: none;
  121. }
  122. .${config.namespace} .button:hover, .${config.namespace} .button:focus {
  123. border-color: #669eff;
  124. background: #669eff;
  125. }
  126. .${config.namespace} .button:active {
  127. border-color: #295ed9;
  128. background: #295ed9;
  129. }
  130. .${config.namespace} .input {
  131. padding: 4px 8px;
  132. background: white;
  133. border: 1px solid #d9d9d9;
  134. border-radius: 2px;
  135. }
  136. .${config.namespace} .input:hover {
  137. border-color: #669eff;
  138. outline: 0;
  139. }
  140. .${config.namespace} .input:focus {
  141. border-color: #669eff;
  142. box-shadow: 0 0 0 2px rgba(61, 127, 255, 0.2);
  143. border-right-width: 1px;
  144. outline: 0;
  145. }
  146. ${config.style.replaceAll(/<\/?style>/g, '')}
  147. </style>`
  148. )
  149. const container = createElement('div', { class: config.namespace, 'data-version': 'v1.0.5' })
  150. const stickyBar = createElement('div', { class: 'sticky-bar' }, config.actionName)
  151. const mask = createElement('div', { class: 'mask' })
  152. const popup = createElement('div', { class: 'popup flex col' }, config.content)
  153. container.append(stickyBar, mask)
  154. mask.append(popup)
  155.  
  156. excludeClick(mask, popup, () => {
  157. container.classList.remove('open')
  158. config.onPopHide && config.onPopHide()
  159. })
  160.  
  161. withDrag(
  162. stickyBar,
  163. (e, d) => {
  164. requestAnimationFrame(() => {
  165. let height = document.documentElement.clientHeight - d.outerHeight
  166. let newTop = e.pageY - d.innerOffsetY
  167. if (newTop >= 0 && newTop <= height) {
  168. stickyBar.style.top = `${newTop}px`
  169. }
  170. })
  171. },
  172. () => {
  173. container.classList.add('open')
  174. config.onPopShow && config.onPopShow()
  175. }
  176. )
  177.  
  178. document.body.append(container)
  179. // ---- other code
  180. resolve && resolve({ container, stickyBar, mask, popup })
  181. }
  182.  
  183. function createElement(tag, attrs, children) {
  184. const el = document.createElement(tag)
  185. if (attrs) {
  186. Object.entries(attrs).forEach(([k, v]) => {
  187. el.setAttribute(k, v)
  188. })
  189. }
  190. if (children instanceof HTMLElement) {
  191. el.append(children)
  192. } else if (typeof children === 'string') {
  193. el.innerHTML = children
  194. }
  195. return el
  196. }
  197.  
  198. function excludeClick(included, excluded, onClick) {
  199. const _data = {
  200. excludeDown: false,
  201. inIncluded: false,
  202. inExcluded: false
  203. }
  204. excluded.addEventListener('mousedown', () => (_data.excludeDown = true))
  205. excluded.addEventListener('mouseup', () => (_data.excludeDown = false))
  206. excluded.addEventListener('mouseenter', () => (_data.inExcluded = true))
  207. excluded.addEventListener('mouseleave', () => (_data.inExcluded = false))
  208. included.addEventListener('mouseenter', () => (_data.inIncluded = true))
  209. included.addEventListener('mouseleave', () => (_data.inIncluded = false))
  210. included.addEventListener('click', (e) => {
  211. if (_data.inIncluded && !_data.inExcluded) {
  212. if (_data.excludeDown) {
  213. _data.excludeDown = false
  214. } else {
  215. onClick?.(e)
  216. }
  217. }
  218. })
  219. }
  220.  
  221. function withDrag(el, onMove, onClick) {
  222. const _data = {
  223. outerHeight: 0,
  224. innerOffsetY: 0,
  225. justClick: false
  226. }
  227.  
  228. const onElMouseMove = (e) => {
  229. _data.justClick = false
  230. onMove?.(e, _data)
  231. }
  232.  
  233. const onElMouseUp = leftKey(() => {
  234. document.removeEventListener('mousemove', onElMouseMove)
  235. document.removeEventListener('mouseup', onElMouseUp)
  236. })
  237.  
  238. el.addEventListener(
  239. 'mousedown',
  240. leftKey((e) => {
  241. _data.justClick = true
  242. const elComputedStyle = window.getComputedStyle(el)
  243. _data.innerOffsetY = e.pageY - getNumber(elComputedStyle.top)
  244. _data.outerHeight =
  245. el.clientHeight + getNumber(elComputedStyle.borderTopWidth) + getNumber(elComputedStyle.borderBottomWidth)
  246. document.addEventListener('mousemove', onElMouseMove)
  247. document.addEventListener('mouseup', onElMouseUp)
  248. })
  249. )
  250.  
  251. el.addEventListener(
  252. 'mouseup',
  253. leftKey((e) => {
  254. if (_data.justClick) {
  255. onClick?.(e, _data)
  256. _data.justClick = false
  257. }
  258. onElMouseUp()
  259. e.stopPropagation()
  260. })
  261. )
  262. }
  263.  
  264. function leftKey(fn) {
  265. return (...args) => {
  266. let key = args && args[0] && args[0].button
  267. if (key === 0 || key === void 0) {
  268. fn.apply(this, args)
  269. }
  270. }
  271. }
  272.  
  273. function getNumber(str) {
  274. if (str) {
  275. let mArr = str.match(/\d+(\.\d*)?|\.\d+/)
  276. if (mArr && mArr.length) {
  277. return parseFloat(mArr[0])
  278. }
  279. }
  280. return void 0
  281. }
  282.  
  283. function _checkConfig(config) {
  284. if (!config) throw new Error('config is required. you should call window.paso.injectPopup(config)')
  285. if (!config.namespace) throw new Error('config.namespace is required and it cannot be empty.')
  286. if (!/^[-\w]+$/.test(config.namespace)) throw new Error('config.namespace must match the regex /^[-\\w]+$/.')
  287. }
  288.  
  289. if (!window.paso || !(window.paso instanceof Object)) window.paso = {}
  290. window.paso.injectPopup = (config) => {
  291. _checkConfig(config)
  292. const _config = Object.assign(
  293. {},
  294. {
  295. namespace: '',
  296. actionName: 'Action',
  297. collapse: '100%',
  298. location: '25%',
  299. content: '<label>Hello World</label>',
  300. style: '',
  301. onPopShow() {},
  302. onPopHide() {}
  303. },
  304. config
  305. )
  306. return new Promise((resolve) => {
  307. _injectHtml(_config, resolve)
  308. })
  309. }
  310. })()