Home Reference Source

src/dom.js

import {
  typeOf
} from './base'
import {
  trim,
  dashToCamel,
} from './string'
import { delay } from './func'
import Queue from './queue'
import { nextFrame } from './tick'

let styleKeys = null

/**
 * @ignore
 */
const setSingleStyle = (ele, name, value) => {
  if (!ele || !ele.style) {
    throw new Error('missing HTML node element for setStyle!')
  }
  name = dashToCamel(name)
  name = checkVendorPrefix(name)
  if (typeOf(name) !== 'Undefined' && typeOf(value) !== 'Undefined') {
    ele.style[name] = tryAddPX(name, value)
  }
}

/**
 * @param {HTMLElement} ele - dom element 
 * @param {String} name - the style property name, it also can be an object to set multiple style properties
 * @param {String|Number} value - the value of the style property
 * 
 * @example
 * 
 * setStyle(document.body, 'fontSize', '16px')
 * setStyle(doucment.body, {
 *  backgroundColor: '#ffffff',
 *  minHeight: '100vh'
 * })
 */
export const setStyle = (ele, name, value) => {
  if (typeOf(name) === 'Object') {
    Object.keys(name)
      .map(n => {
        setSingleStyle(ele, n, name[n])
      })
  } else {
    setSingleStyle(ele, name, value)
  }
}

/**
 * @ignore
 */
function checkVendorPrefix(name) {
  let styleKeys = getStyleKeys()

  return styleKeys[name] || name
}

/**
 * @ignore
 */
function getStyleKeys() {
  if (styleKeys !== null) return styleKeys

  if (typeof document === 'undefined') {
    throw new Error('missing document!')
  }

  let computedStyles = Array.from(getComputedStyle(document.body))
  let keys = Object.keys(computedStyles).filter(
    key => /^webkit(transform|transition)$/i.test(key)
  )
  styleKeys = keys.reduce((res, key) => {
    let name = key.replace(/^webkit(\w)/, (a, s) => s.toLowerCase())
    res[name] = key
    return res
  }, {})

  return styleKeys
}

const properties = [
  'width',
  'height',
  'minWidth',
  'maxWidth',
  'minHeight',
  'maxHeight',
  'left',
  'top',
  'right',
  'bottom',
  'borderWidth',
  'borderTopWidth',
  'borderBottomWidth',
  'borderLeftWidth',
  'borderRightWidth',
  'borderRadius',
  'borderTopLeftRadius',
  'borderTopRightRadius',
  'borderBottomLeftRadius',
  'borderBottomRightRadius',
  'borderSpacing',
  'fontSize',
  'letterSpacing',
  'margin',
  'marginLeft',
  'marginRight',
  'marginTop',
  'marginBottom',
  'padding',
  'paddingLeft',
  'paddingRight',
  'paddingTop',
  'paddingBottom',
]
/**
 * @ignore 
 */
function tryAddPX(name, value) {
  // check if name in the property list which can add px, if not return value 
  if (properties.indexOf(name) == -1) {
    return value
  }

  let cValue = trim(value) - 0

  // only add px to numeric value
  if (typeOf(cValue) !== 'Number') {
    return value
  }

  return `${cValue}px`
}

/**
 * @desc set styles in batch style. It merge separate style setting into a task and execute it as soon as possible.
 * 
 * @todo 
 *  1. async set style call by delay
 *  2. merge separate call into a task
 *  3. lock the style setting when doing a task, put incoming task into a queue
 *  4. use tick as setting frame, check remaining task at every frame
 */

class TaskQueue extends Queue {
  constructor(options) {
    super(options)
    this.waittingTasks = []
    this.doingTask = false
  }
  enqueue(item) {
    if (this.doingTask) {
      this.waittingTasks.push(item)
    } else {
      super.enqueue(item)
    }
    return this
  }
  doTask() {
    if (this.doingTask) return

    this.doingTask = true
    let task
    while (task = tasks.dequeue()) {
      try {
        setStyle(...task)
      } catch (error) {
        console.log(error)
      }
    }
    this.doingTask = false
    if (this.waittingTasks.length > 0) {
      this.mergeTask()
      nextFrame(() => {
        this.doTask()
      })
    }
    return this
  }
  mergeTask() {
    this.data = [...this.data, ...this.waittingTasks]
    return this
  }
}

const tasks = new TaskQueue()
let delayHandle = null

export function batchSetStyle(...args) {
  if (delayHandle) {
    clearTimeout(delayHandle)
    delayHandle = null
  }
  tasks.enqueue(args)
  delayHandle = delay(() => {
    tasks.doTask()
    delayHandle = null
  }, 0)
}

let remSetuped = false

export function rem({
  designWidth = 750,
  designDPR = 1,
  rem2px = 100,
  bodyFontSize = 14,
  win,
  doc
}) {
  let documentElement = doc.documentElement
  const dpr = () => win.devicePixelRatio || 1
  let expectedWidth = designWidth * designDPR
  let expectedDocumentElementFontSize = rem2px

  !remSetuped && setBodyFontSize()

  function setDocumentElementFontSize() {
    let adaptedWidth = documentElement.clientWidth * dpr()
    let adaptedDocumentElementFontSize = expectedDocumentElementFontSize * adaptedWidth / expectedWidth
    setStyle(documentElement, 'font-size', adaptedDocumentElementFontSize)
    console.log(`dpr: ${dpr()}, expectedWidth: ${expectedWidth}, deFontSize: ${expectedDocumentElementFontSize}, adaptedWidth: ${adaptedWidth}, adaptedFontSize: ${adaptedDocumentElementFontSize}`)
  }

  function setBodyFontSize() {
    if (doc.body) {
      setStyle(doc.body, 'font-size', bodyFontSize * dpr())
    }
    else {
      doc.addEventListener('DOMContentLoaded', setBodyFontSize)
    }
  }
  if (!remSetuped) {
    setDocumentElementFontSize()

    // reset rem unit on page resize
    window.addEventListener('resize', setDocumentElementFontSize)
    window.addEventListener('pageshow', function (e) {
      if (e.persisted) {
        setDocumentElementFontSize()
      }
    })
  }

  remSetuped = true
}

export const performanceNow = (function () {
  let now
  if (typeof performance !== 'undefined') {
    now = () => performance.now()
  } else {
    now = () => +new Date
  }
  return now
})()