UserscriptAPIDom

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

Verze ze dne 22. 09. 2021. Zobrazit nejnovější verzi.

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greasyfork.org/scripts/431998/972856/UserscriptAPIDom.js

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

/**
 * UserscriptAPIDom
 *
 * 依赖于 `UserscriptAPI`。
 * @version 1.2.1.20210922
 * @author Laster2800
 * @see {@link https://gitee.com/liangjiancang/userscript/tree/master/lib/UserscriptAPI UserscriptAPI}
 */
class UserscriptAPIDom {
  /**
   * @param {UserscriptAPI} api `UserscriptAPI`
   */
  constructor(api) {
    this.api = api
  }

  /**
   * 查找符合条件的祖先元素
   * @param {HTMLElement} target 目标元素
   * @param {(el: HTMLElement) => boolean} condition 条件
   * @param {boolean} [positioned] 以 `offsetParent` 而非 `parentElement` 上溯
   * @returns {HTMLElement} 符合条件的祖先元素
   */
  findAncestor(target, condition, positioned) {
    const p = positioned ? 'offsetParent' : 'parentElement'
    let element = target[p]
    while (element && !condition(element)) {
      element = element[p]
    }
    return element
  }

  /**
   * 设定元素位置,默认设定为绝对居中
   *
   * 要求该元素此时可见且尺寸为确定值(一般要求为块状元素)。
   * @param {HTMLElement} target 目标元素
   * @param {Object} [options] 选项
   * @param {string} [options.position='fixed'] 定位方式
   * @param {string} [options.top='50%'] `style.top`
   * @param {string} [options.left='50%'] `style.left`
   */
  setPosition(target, options) {
    options = {
      position: 'fixed',
      top: '50%',
      left: '50%',
      ...options,
    }
    target.style.position = options.position
    const style = window.getComputedStyle(target)
    const top = (Number.parseFloat(style.height) + Number.parseFloat(style.paddingTop) + Number.parseFloat(style.paddingBottom)) / 2
    const left = (Number.parseFloat(style.width) + Number.parseFloat(style.paddingLeft) + Number.parseFloat(style.paddingRight)) / 2
    target.style.top = `calc(${options.top} - ${top}px)`
    target.style.left = `calc(${options.left} - ${left}px)`
  }

  /**
   * @typedef FadeTargetElement
   * @property {string} [fadeInDisplay] 渐显开始后的 `display` 样式。若没有设定:
   *  * 若当前 `display` 与 `fadeOutDisplay` 不同,默认值为当前 `display`。
   *  * 若当前 `display` 与 `fadeOutDisplay` 相同,默认值为 `block`。
   * @property {string} [fadeOutDisplay='none'] 渐隐开始后的 `display` 样式
   * @property {number} [fadeInTime] 渐显时间;缺省时,元素的 `transition-duration` 必须与 `api.options.fadeTime` 一致
   * @property {number} [fadeOutTime] 渐隐时间;缺省时,元素的 `transition-duration` 必须与 `api.options.fadeTime` 一致
   * @property {string} [fadeInFunction='ease-in-out'] 渐显效果,应为符合 `transition-timing-function` 的有效值
   * @property {string} [fadeOutFunction='ease-in-out'] 渐隐效果,应为符合 `transition-timing-function` 的有效值
   * @property {boolean} [fadeInNoInteractive] 渐显期间是否禁止交互
   * @property {boolean} [fadeOutNoInteractive] 渐隐期间是否禁止交互
   */
  /**
   * 处理 HTML 元素的渐显和渐隐
   * @param {boolean} inOut 渐显/渐隐
   * @param {HTMLElement & FadeTargetElement} target HTML 元素
   * @param {() => void} [callback] 渐显/渐隐完成的回调函数
   */
  fade(inOut, target, callback) {
    const api = this.api
    let transitionChanged = false
    const fadeId = Date.now() // 等同于当前时间戳,其意义在于保证对于同一元素,后执行的操作必将覆盖前的操作
    const fadeOutDisplay = target.fadeOutDisplay ?? 'none'
    target._fadeId = fadeId
    if (inOut) { // 渐显
      let displayChanged = false
      if (typeof target.fadeInTime == 'number' || target.fadeInFunction) {
        target.style.transition = `opacity ${target.fadeInTime ?? api.options.fadeTime}ms ${target.fadeInFunction ?? 'ease-in-out'}`
        transitionChanged = true
      }
      if (target.fadeInNoInteractive) {
        target.style.pointerEvents = 'none'
      }
      const originalDisplay = window.getComputedStyle(target).display
      let fadeInDisplay = target.fadeInDisplay
      if (!fadeInDisplay) {
        if (originalDisplay != fadeOutDisplay) {
          fadeInDisplay = originalDisplay
        } else {
          fadeInDisplay = 'block'
        }
      }
      if (originalDisplay != fadeInDisplay) {
        target.style.display = fadeInDisplay
        displayChanged = true
      }
      setTimeout(() => {
        let success = false
        if (target._fadeId <= fadeId) {
          target.style.opacity = '1'
          success = true
        }
        setTimeout(() => {
          callback?.(success)
          if (target._fadeId <= fadeId) {
            if (transitionChanged) {
              target.style.transition = ''
            }
            if (target.fadeInNoInteractive) {
              target.style.pointerEvents = ''
            }
          }
        }, target.fadeInTime ?? api.options.fadeTime)
      }, displayChanged ? 10 : 0) // 此处的 10ms 是为了保证修改 display 后在浏览器上真正生效;按 HTML5 定义,浏览器需保证 display 在修改后 4ms 内生效,但实际上大部分浏览器貌似做不到,等个 10ms 再修改 opacity
    } else { // 渐隐
      if (typeof target.fadeOutTime == 'number' || target.fadeOutFunction) {
        target.style.transition = `opacity ${target.fadeOutTime ?? api.options.fadeTime}ms ${target.fadeOutFunction ?? 'ease-in-out'}`
        transitionChanged = true
      }
      if (target.fadeOutNoInteractive) {
        target.style.pointerEvents = 'none'
      }
      target.style.opacity = '0'
      setTimeout(() => {
        let success = false
        if (target._fadeId <= fadeId) {
          target.style.display = fadeOutDisplay
          success = true
        }
        callback?.(success)
        if (success) {
          if (transitionChanged) {
            target.style.transition = ''
          }
          if (target.fadeOutNoInteractive) {
            target.style.pointerEvents = ''
          }
        }
      }, target.fadeOutTime ?? api.options.fadeTime)
    }
  }

  /**
   * 判断 HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
   * @param {HTMLElement} el 目标元素
   * @param {HTMLElement} [endEl] 终止元素,当搜索到该元素时终止判断(不会判断该元素)
   * @returns {boolean} HTML 元素是否为 `fixed` 定位,或其是否在 `fixed` 定位的元素下
   */
  isFixed(el, endEl) {
    while (el && el != endEl) {
      if (window.getComputedStyle(el).position == 'fixed') return true
      el = el.offsetParent
    }
    return false
  }
}

/* global UserscriptAPI */
{ UserscriptAPI.registerModule('dom', UserscriptAPIDom) }