UserscriptAPIDom

https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI

目前為 2021-09-07 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/431998/968204/UserscriptAPIDom.js

  1. /**
  2. * UserscriptAPIDom
  3. *
  4. * 依赖于 `UserscriptAPI`。
  5. * @version 1.0.1.20210907
  6. * @author Laster2800
  7. * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
  8. */
  9. class UserscriptAPIDom {
  10. /**
  11. * @param {UserscriptAPI} api `UserscriptAPI`
  12. */
  13. constructor(api) {
  14. this.api = api
  15. }
  16.  
  17. /**
  18. * 初始化 urlchange 事件
  19. * @see {@link https://stackoverflow.com/a/52809105 How to detect if URL has changed after hash in JavaScript}
  20. */
  21. initUrlchangeEvent() {
  22. if (!history._urlchangeEventInitialized) {
  23. const urlEvent = () => {
  24. const event = new Event('urlchange')
  25. // 添加属性,使其与 Tampermonkey urlchange 保持一致
  26. event.url = location.href
  27. return event
  28. }
  29. history.pushState = (f => function pushState() {
  30. const ret = f.apply(this, arguments)
  31. window.dispatchEvent(new Event('pushstate'))
  32. window.dispatchEvent(urlEvent())
  33. return ret
  34. })(history.pushState)
  35. history.replaceState = (f => function replaceState() {
  36. const ret = f.apply(this, arguments)
  37. window.dispatchEvent(new Event('replacestate'))
  38. window.dispatchEvent(urlEvent())
  39. return ret
  40. })(history.replaceState)
  41. window.addEventListener('popstate', () => {
  42. window.dispatchEvent(urlEvent())
  43. })
  44. history._urlchangeEventInitialized = true
  45. }
  46. }
  47.  
  48. /**
  49. * 添加样式
  50. * @param {string} css 样式
  51. * @param {HTMLDocument} [doc=document] 文档
  52. * @returns {HTMLStyleElement} `<style>`
  53. */
  54. addStyle(css, doc = document) {
  55. const api = this.api
  56. const style = doc.createElement('style')
  57. style.setAttribute('type', 'text/css')
  58. style.className = `${api.options.id}-style`
  59. style.appendChild(doc.createTextNode(css))
  60. const parent = doc.head || doc.documentElement
  61. if (parent) {
  62. parent.appendChild(style)
  63. } else { // 极端情况下会出现,DevTools 网络+CPU 双限制可模拟
  64. api.wait?.waitForConditionPassed({
  65. condition: () => doc.head || doc.documentElement,
  66. timeout: 0,
  67. }).then(parent => parent.appendChild(style))
  68. }
  69. return style
  70. }
  71.  
  72. /**
  73. * 设定元素位置,默认设定为绝对居中
  74. *
  75. * 要求该元素此时可见且尺寸为确定值(一般要求为块状元素)。
  76. * @param {HTMLElement} target 目标元素
  77. * @param {Object} [options] 选项
  78. * @param {string} [options.position='fixed'] 定位方式
  79. * @param {string} [options.top='50%'] `style.top`
  80. * @param {string} [options.left='50%'] `style.left`
  81. */
  82. setPosition(target, options) {
  83. options = {
  84. position: 'fixed',
  85. top: '50%',
  86. left: '50%',
  87. ...options,
  88. }
  89. target.style.position = options.position
  90. const style = window.getComputedStyle(target)
  91. const top = (parseFloat(style.height) + parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)) / 2
  92. const left = (parseFloat(style.width) + parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)) / 2
  93. target.style.top = `calc(${options.top} - ${top}px)`
  94. target.style.left = `calc(${options.left} - ${left}px)`
  95. }
  96.  
  97. /**
  98. * @typedef FadeTargetElement
  99. * @property {string} [fadeInDisplay] 渐显开始后的 `display` 样式。若没有设定:
  100. * * 若当前 `display` 与 `fadeOutDisplay` 不同,默认值为当前 `display`。
  101. * * 若当前 `display` 与 `fadeOutDisplay` 相同,默认值为 `block`。
  102. * @property {string} [fadeOutDisplay='none'] 渐隐开始后的 `display` 样式
  103. * @property {number} [fadeInTime] 渐显时间;缺省时,元素的 `transition-duration` 必须与 `api.options.fadeTime` 一致
  104. * @property {number} [fadeOutTime] 渐隐时间;缺省时,元素的 `transition-duration` 必须与 `api.options.fadeTime` 一致
  105. * @property {string} [fadeInFunction='ease-in-out'] 渐显效果,应为符合 `transition-timing-function` 的有效值
  106. * @property {string} [fadeOutFunction='ease-in-out'] 渐隐效果,应为符合 `transition-timing-function` 的有效值
  107. * @property {boolean} [fadeInNoInteractive] 渐显期间是否禁止交互
  108. * @property {boolean} [fadeOutNoInteractive] 渐隐期间是否禁止交互
  109. */
  110. /**
  111. * 处理 HTML 元素的渐显和渐隐
  112. * @param {boolean} inOut 渐显/渐隐
  113. * @param {HTMLElement & FadeTargetElement} target HTML 元素
  114. * @param {() => void} [callback] 渐显/渐隐完成的回调函数
  115. */
  116. fade(inOut, target, callback) {
  117. const api = this.api
  118. let transitionChanged = false
  119. const fadeId = new Date().getTime() // 等同于当前时间戳,其意义在于保证对于同一元素,后执行的操作必将覆盖前的操作
  120. const fadeOutDisplay = target.fadeOutDisplay ?? 'none'
  121. target._fadeId = fadeId
  122. if (inOut) { // 渐显
  123. let displayChanged = false
  124. if (typeof target.fadeInTime == 'number' || target.fadeInFunction) {
  125. target.style.transition = `opacity ${target.fadeInTime ?? api.options.fadeTime}ms ${target.fadeInFunction ?? 'ease-in-out'}`
  126. transitionChanged = true
  127. }
  128. if (target.fadeInNoInteractive) {
  129. target.style.pointerEvents = 'none'
  130. }
  131. const originalDisplay = window.getComputedStyle(target).display
  132. let fadeInDisplay = target.fadeInDisplay
  133. if (!fadeInDisplay) {
  134. if (originalDisplay != fadeOutDisplay) {
  135. fadeInDisplay = originalDisplay
  136. } else {
  137. fadeInDisplay = 'block'
  138. }
  139. }
  140. if (originalDisplay != fadeInDisplay) {
  141. target.style.display = fadeInDisplay
  142. displayChanged = true
  143. }
  144. setTimeout(() => {
  145. let success = false
  146. if (target._fadeId <= fadeId) {
  147. target.style.opacity = '1'
  148. success = true
  149. }
  150. setTimeout(() => {
  151. callback?.(success)
  152. if (target._fadeId <= fadeId) {
  153. if (transitionChanged) {
  154. target.style.transition = ''
  155. }
  156. if (target.fadeInNoInteractive) {
  157. target.style.pointerEvents = ''
  158. }
  159. }
  160. }, target.fadeInTime ?? api.options.fadeTime)
  161. }, displayChanged ? 10 : 0) // 此处的 10ms 是为了保证修改 display 后在浏览器上真正生效;按 HTML5 定义,浏览器需保证 display 在修改后 4ms 内生效,但实际上大部分浏览器貌似做不到,等个 10ms 再修改 opacity
  162. } else { // 渐隐
  163. if (typeof target.fadeOutTime == 'number' || target.fadeOutFunction) {
  164. target.style.transition = `opacity ${target.fadeOutTime ?? api.options.fadeTime}ms ${target.fadeOutFunction ?? 'ease-in-out'}`
  165. transitionChanged = true
  166. }
  167. if (target.fadeOutNoInteractive) {
  168. target.style.pointerEvents = 'none'
  169. }
  170. target.style.opacity = '0'
  171. setTimeout(() => {
  172. let success = false
  173. if (target._fadeId <= fadeId) {
  174. target.style.display = fadeOutDisplay
  175. success = true
  176. }
  177. callback?.(success)
  178. if (success) {
  179. if (transitionChanged) {
  180. target.style.transition = ''
  181. }
  182. if (target.fadeOutNoInteractive) {
  183. target.style.pointerEvents = ''
  184. }
  185. }
  186. }, target.fadeOutTime ?? api.options.fadeTime)
  187. }
  188. }
  189.  
  190. /**
  191. * 为 HTML 元素添加 `class`
  192. * @param {HTMLElement} el 目标元素
  193. * @param {...string} className `class`
  194. */
  195. addClass(el, ...className) {
  196. el.classList?.add(...className)
  197. }
  198.  
  199. /**
  200. * 为 HTML 元素移除 `class`
  201. * @param {HTMLElement} el 目标元素
  202. * @param {...string} [className] `class`,未指定时移除所有 `class`
  203. */
  204. removeClass(el, ...className) {
  205. if (className.length > 0) {
  206. el.classList?.remove(...className)
  207. } else if (el.className) {
  208. el.className = ''
  209. }
  210. }
  211.  
  212. /**
  213. * 判断 HTML 元素类名中是否含有 `class`
  214. * @param {HTMLElement} el 目标元素
  215. * @param {string | string[]} className `class`,支持同时判断多个
  216. * @param {boolean} [and] 同时判断多个 `class` 时,默认采取 `OR` 逻辑,是否采用 `AND` 逻辑
  217. * @returns {boolean} 是否含有 `class`
  218. */
  219. containsClass(el, className, and = false) {
  220. if (el.classList) {
  221. const trim = clz => clz.startsWith('.') ? clz.slice(1) : clz
  222. if (className instanceof Array) {
  223. if (and) {
  224. for (const c of className) {
  225. if (!el.classList.contains(trim(c))) {
  226. return false
  227. }
  228. }
  229. return true
  230. } else {
  231. for (const c of className) {
  232. if (el.classList.contains(trim(c))) {
  233. return true
  234. }
  235. }
  236. return false
  237. }
  238. } else {
  239. return el.classList.contains(trim(className))
  240. }
  241. }
  242. return false
  243. }
  244.  
  245. /**
  246. * 判断 HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
  247. * @param {HTMLElement} el 目标元素
  248. * @param {HTMLElement} [endEl] 终止元素,当搜索到该元素时终止判断(不会判断该元素)
  249. * @returns {boolean} HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
  250. */
  251. isFixed(el, endEl) {
  252. while (el && el != endEl) {
  253. if (window.getComputedStyle(el).position == 'fixed') {
  254. return true
  255. }
  256. el = el.offsetParent
  257. }
  258. return false
  259. }
  260. }
  261.  
  262. /* global UserscriptAPI */
  263. { UserscriptAPI.registerModule('dom', UserscriptAPIDom) }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址