util/util.js

/**
 * @module Util
 * @description
 *
 * ## 工具库 / Util
 *
 * 这里提供了Vimo使用的工具库, 当然业务代码中也可以按需使用.
 *
 * @usage
 * import {isBoolean, isString} from 'vimo/lib/util/util'
 * */

/**
 * @typedef {Object} PointerCoordinates   - 坐标对象
 * @property {number} x - x坐标
 * @property {number} y - y坐标
 * */

/**
 * @function isBoolean
 * @description 判断传入值是否为 Boolean
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isBoolean = val => typeof val === 'boolean'

/**
 * @function isString
 * @description 判断传入值是否为 String
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isString = val => typeof val === 'string'

/**
 * @function isNumber
 * @description 判断传入值是否为 Number
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isNumber = val => typeof val === 'number'

/**
 * @function isFunction
 * @description 判断传入值是否为 Function
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isFunction = val => typeof val === 'function'

/**
 * @function isDefined
 * @description 判断传入值已定义
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isDefined = val => typeof val !== 'undefined'

/**
 * @function isUndefined
 * @description 判断传入值未定义
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isUndefined = val => typeof val === 'undefined'

/**
 * @function isPresent
 * @description 判断传入值不为空
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isPresent = val => val !== undefined && val !== null && val !== ''

/**
 * @function isBlank
 * @description 判断传入值为空
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isBlank = val => val === undefined || val === null

/**
 * @function isObject
 * @description 判断传入值为 Object
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isObject = val => typeof val === 'object'

/**
 * @function isDate
 * @description 判断传入值为 Date类型
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isDate = val =>
  Object.prototype.toString
    .call(val)
    .match(/^(\[object )(\w+)\]$/i)[2]
    .toLowerCase() === 'date'

/**
 * @function isRegexp
 * @description 判断传入值为 Regexp
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isRegexp = val =>
  Object.prototype.toString
    .call(val)
    .match(/^(\[object )(\w+)\]$/i)[2]
    .toLowerCase() === 'regexp'

/**
 * @function isArray
 * @description 判断传入值为 Array
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isArray = Array.isArray

/**
 * @function isPlainObject
 * @description 判断传入值为 纯对象
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export const isPlainObject = val =>
  isObject(val) && Object.getPrototypeOf(val) === Object.prototype

/**
 * @function isPrimitive
 * @description 判断传入值为 基础变量
 * @param {*} val - 判断值
 * @return {Boolean}
 * @static
 * */
export function isPrimitive (val) {
  return isString(val) || isBoolean(val) || (isNumber(val) && !isNaN(val))
}

/**
 * 判断元素属性是否存在设置
 * 一般vue的props不需要这个, 因为其内部会进行这个判断, 没有传入值则为false
 * @param {string} val - 判断值
 * @example
 * 'true' => true
 * 'on' => true
 * '' => true
 * */
export function isTrueProperty (val) {
  if (typeof val === 'string') {
    val = val.toLowerCase().trim()
    return val === 'true' || val === 'on' || val === ''
  }
  return !!val
}

/**
 * 判断Checked属性的值
 * 判断是否相等,比如:'true'和true,'0'和0
 * @param {*} a - 判断的第一个值
 * @param {*} b - 判断的第二个值
 * return {Boolean}
 *
 * @example
 * undefined == null
 * undefined == ''
 * null == ''
 * true == 'true'
 * false == 'false'
 * 0 == '0'
 * */
export function isCheckedProperty (a, b) {
  if (a === undefined || a === null || a === '') {
    return b === undefined || b === null || b === ''
  } else if (a === true || a === 'true') {
    return b === true || b === 'true'
  } else if (a === false || a === 'false') {
    return b === false || b === 'false'
  } else if (a === 0 || a === '0') {
    return b === 0 || b === '0'
  }

  // not using strict comparison on purpose
  // eslint-disable-next-line eqeqeq
  return a == b
}

/**
 * transitionEnd事件注册,绑定的函数触发后会自动解绑
 * @param {HTMLElement} el      - 绑定的元素
 * @param {Function} callbackFn   - 绑定的函数
 * @return {Function}           - 取消绑定的函数
 * */
export function transitionEnd (el, callbackFn) {
  const unRegs = []

  function unregister () {
    unRegs.forEach(function (unReg) {
      unReg && unReg()
    })
  }

  function onTransitionEnd (ev) {
    if (el === ev.target) {
      callbackFn && callbackFn(ev)
      unregister()
    }
  }

  if (el) {
    registerListener(el, 'webkitTransitionEnd', onTransitionEnd, {}, unRegs)
    registerListener(el, 'transitionend', onTransitionEnd, {}, unRegs)
  }

  return unregister
}

// /**
//  * hashChange,hash变化后执行回调, 并自动解绑
//  * @param {function} callback - 回调函数
//  * @return {function} - 解绑函数
//  * */
// export function hashChange (callback) {
//   let unReg = null
//
//   const onHashChange = (ev) => {
//     unReg && unReg()
//     callback(ev)
//   }
//
//   unReg = registerListener(window, 'hashchange', onHashChange, {})
//   return unReg
// }

/**
 * urlChange(popstate)注册,绑定的函数触发后会自动解绑
 * @param {function} callback - 回调函数
 * @return {function} - 解绑函数
 * */
export function urlChange (callback) {
  let unReg = null
  const onStateChange = ev => {
    unReg && unReg()
    callback(ev)
  }
  unReg = registerListener(window, 'popstate', onStateChange, {})
  return unReg
}

/**
 *
 * 给addEventListener增加passive属性, 如果不支持将降级使用!!opts.capture, 事件的关闭需要自己手动解除, 切记!!
 * @param {any} ele                               - 监听的元素
 * @param {string} eventName                      - 监听的名称
 * @param {function} callback                     - 回调
 * @param {object} [opts]                         - addEventListener的第三个参数 EventListenerOptions
 * @param {object} [opts.capture]                 - capture
 * @param {object} [opts.passive]                 - passive
 * @param {array} [unregisterListenersCollection] - 如果提供Function[], 则unReg将压如这个列表中
 * @return {Function}                             - 返回removeEventListener的函数
 */
export function registerListener (
  ele,
  eventName,
  callback,
  opts = {},
  unregisterListenersCollection
) {
  // use event listener options when supported
  // otherwise it's just a boolean for the "capture" arg
  const listenerOpts = isPassive()
    ? {
      capture: !!opts.capture,
      passive: !!opts.passive
    }
    : !!opts.capture

  // use the native addEventListener
  ele['addEventListener'](eventName, callback, listenerOpts)

  let unReg = function unregisterListener () {
    ele['removeEventListener'](eventName, callback, listenerOpts)
  }

  if (
    unregisterListenersCollection &&
    Array.isArray(unregisterListenersCollection)
  ) {
    unregisterListenersCollection.push(unReg)
  }

  return unReg
}

/**
 * 判断的当前浏览器是否支持isPassive属性
 * @return {Boolean}
 * */
export function isPassive () {
  var supportsPassiveOption = false
  try {
    window.addEventListener(
      'test',
      null,
      Object.defineProperty({}, 'passive', {
        get: function () {
          supportsPassiveOption = true
        }
      })
    )
  } catch (e) {}
  return supportsPassiveOption
}

/**
 * document的ready事件监听
 * @param {Function} [callback] - 回调函数
 * @return {Promise} - 返回promise,completed后自动解绑
 * */
export function docReady (callback) {
  let promise = null // Promise;

  if (!callback) {
    // a callback wasn't provided, so let's return a promise instead
    promise = new Promise(function (resolve) {
      callback = resolve
    })
  }

  if (
    document.readyState === 'complete' ||
    document.readyState === 'interactive'
  ) {
    callback()
  } else {
    document.addEventListener('DOMContentLoaded', completed, false)
    window.addEventListener('load', completed, false)
  }

  return promise

  function completed () {
    document.removeEventListener('DOMContentLoaded', completed, false)
    window.removeEventListener('load', completed, false)
    callback()
  }
}

/**
 * 根据click或者touch的事件对象, 获取event事件对象中的点击位置(坐标xy值)
 * @param {any} ev - 事件对象
 * @return  {PointerCoordinates}
 * */
export function pointerCoord (ev) {
  if (ev) {
    var changedTouches = ev.changedTouches
    if (changedTouches && changedTouches.length > 0) {
      var touch = changedTouches[0]
      return { x: touch.clientX, y: touch.clientY }
    }
    var pageX = ev.pageX
    if (pageX !== undefined) {
      return { x: pageX, y: ev.pageY }
    }
  }
  return { x: 0, y: 0 }
}

// /**
//  * 判断是否移动
//  * @param {number} threshold - 阈值
//  * @param {PointerCoordinates} startCoord - 开始坐标
//  * @param {PointerCoordinates} endCoord - 结束坐标
//  * */
// export function hasPointerMoved (threshold, startCoord, endCoord) {
//   if (startCoord && endCoord) {
//     const deltaX = (startCoord.x - endCoord.x)
//     const deltaY = (startCoord.y - endCoord.y)
//     const distance = deltaX * deltaX + deltaY * deltaY
//     return distance > (threshold * threshold)
//   }
//   return false
// }

/**
 * 判断元素是否在激活状态, 比如input
 * @param {HTMLElement} ele - 元素
 * @return {boolean}
 * */
export function isActive (ele) {
  return !!(ele && document.activeElement === ele)
}

/**
 * 判断元素是否在focus状态, 比如input
 * @param {HTMLElement} ele - 元素
 * @return {boolean}
 * */
export function hasFocus (ele) {
  return isActive(ele) && ele.parentElement.querySelector(':focus') === ele
}

// /**
//  * 判断TEXTAREA或者INPUT是否可输入
//  * @param {HTMLElement} ele - 元素
//  * @return {boolean}
//  * */
// export function isTextInput (ele) {
//   return !!ele &&
//     (ele.tagName === 'TEXTAREA' ||
//       ele.contentEditable === 'true' ||
//       (ele.tagName === 'INPUT' && !(NON_TEXT_INPUT_REGEX.test(ele.type))))
// }
//
// export const NON_TEXT_INPUT_REGEX = /^(radio|checkbox|range|file|submit|reset|color|image|button)$/i

// /**
//  * 判断TEXTAREA或者INPUT是否在focus状态
//  * @return {boolean}
//  * */
// export function hasFocusedTextInput () {
//   const ele = document.activeElement // <HTMLElement>
//   if (isTextInput(ele)) {
//     return (ele.parentElement.querySelector(':focus') === ele)
//   }
//   return false
// }

/**
 * blur out TEXTAREA或者INPUT的状态
 * @return {boolean}
 * */
export function focusOutActiveElement () {
  const activeElement = document.activeElement //  <HTMLElement>
  activeElement && activeElement.blur && activeElement.blur()
}

/**
 * 元素的class操作
 * @param {HTMLElement} ele - 添加、删除class的元素
 * @param {string} className
 * @param {boolean} add - 是添加还是删除
 * */
export function setElementClass (ele, className, add) {
  if (add) {
    addClass(ele, className)
  } else {
    removeClass(ele, className)
  }
}

/**
 * 元素的class操作
 * */
export function hasClass (obj, cls) {
  return obj.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
}

export function addClass (obj, cls) {
  if (!hasClass(obj, cls)) {
    obj.className += ' ' + cls
  }
}

export function removeClass (obj, cls) {
  if (hasClass(obj, cls)) {
    var reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
    obj.className = obj.className.replace(reg, ' ').trim()
  }
}

/**
 * 如果n的大小在max和min之间,则返回n, 否则返回最大最小值
 *
 * @example
 * clamp(1,5,10)  -> 5
 * clamp(6,5,10)  -> 6
 * clamp(1,5,4)   -> 4
 *
 * @param {number} min - 最小值
 * @param  {number} n - 测试值
 * @param {number} max - 最大值
 */
export function clamp (min, n, max) {
  return Math.max(min, Math.min(n, max))
}

// /**
//  * 参数后面的对象合并到第一个对象中,以最右面的对象中属性值为准, 如果提供了`Object.assign()`则使用这个
//  *
//  * @param {object} target  - 合并目标
//  * @param {object} [source(s)] - 合并元
//  *
//  * @example assign({a:1},{b:10},{b:1,a:2}) => 返回第一个对象{a: 2, b: 1}
//  */
// export function assign (...args) {
//   if (typeof Object.assign !== 'function') {
//     // use the old-school shallow extend method
//     return _baseExtend(args[0], [].slice.call(args, 1), false)
//   }
//
//   // use the built in ES6 Object.assign method
//   return Object.assign.apply(null, args)
// }

// /**
//  * 深度合并, 最后面的对象将有最高优先级, dst对象将存放最终结果, 使用的是迭代替换方法
//  * @param {object} dst - 最终汇总的结果
//  * @param {object} [source(s)] - 数据源
//  * @return {object} - 最终结果
//  */
// export function merge (dst, ...args) {
//   return _baseExtend(dst, [].slice.call(arguments, 1), true)
// }

// /**
//  * 对象合并
//  * @param {any} dst
//  * @param {Array} objs
//  * @param {boolean} deep
//  * @private
//  * */
// function _baseExtend (dst, objs, deep) {
//   const isObject = (val) => typeof val === 'object'
//   const isFunction = (val) => typeof val === 'function'
//   const isArray = Array.isArray
//   for (var i = 0, ii = objs.length; i < ii; ++i) {
//     var obj = objs[i]
//     if ((!obj || !isObject(obj)) && !isFunction(obj)) continue
//     var keys = Object.keys(obj)
//     for (var j = 0, jj = keys.length; j < jj; j++) {
//       var key = keys[j]
//       var src = obj[key]
//
//       if (deep && isObject(src)) {
//         if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}
//         _baseExtend(dst[key], [src], true)
//       } else {
//         dst[key] = src
//       }
//     }
//   }
//
//   return dst
// }

// /**
//  * 对象深度拷贝, 只处理对象, 使用: `JSON.parse(JSON.stringify(obj))`方法
//  * @param {object} obj - 拷贝的对象
//  * @return {object} - 复制品
//  * */
// export function deepClone (obj) {
//   return JSON.parse(JSON.stringify(obj))
// }

/**
 * 优先使用最左边的对象中的数据,即保持默认值,当在第一个对象中没找到key时才添加新key
 * @param {any} dest the destination to apply defaults to.
 * @example
 * defaults({a:1},{b:1,a:2},{b:10}) => 返回第一个对象 {a: 1, b: 10}
 */
export function defaults (dest, ...args) {
  for (var i = arguments.length - 1; i >= 1; i--) {
    var source = arguments[i]
    if (source) {
      for (var key in source) {
        if (source.hasOwnProperty(key) && !dest.hasOwnProperty(key)) {
          dest[key] = source[key]
        }
      }
    }
  }
  return dest
}

/**
 * 首字母大写
 * @param {string} str - 传入string
 * @return {string}
 * */
export function firstUpperCase (str) {
  return str.toString()[0].toUpperCase() + str.toString().slice(1)
}

/**
 * 将带px单位的string转化为数字
 * @param {string} val - 传入的string
 * @return {number}
 * @example
 * 10px -> 10
 * */
export function parsePxUnit (val) {
  return !!val && val.indexOf('px') > 0 ? parseInt(val, 10) : 0
}

// /**
//  * 从数组中移除某个item
//  * @param {Array} array - 处理的数组
//  * @param {*} item - 移除的元素
//  * @return {Boolean} - 是否成功
//  * */
// export function removeArrayItem (array, item) {
//   const index = array.indexOf(item)
//   // ~index => index*(-1)-1
//   // ~-1 => 0
//   return !!~index && !!array.splice(index, 1)
// }