Home Reference Source

src/utilities.js

/**
 * @module utilities
 * Helper and misc functions
 */

import $ from 'jquery'

import 'polyfill!Object.defineProperty'
import 'polyfill!Object.getOwnPropertyNames'
import 'polyfill!Object.getOwnPropertyDescriptor'
import 'polyfill!Object.getPrototypeOf'

import { Debug } from './Debug'

/**
 * Checks whether an argument can be interpreted as an even integer
 * @param   {Object}  value A value of any type
 * @returns {Boolean} True when value is numeric, parses as an integer (no matter if decimal, octal or sexadecimal)
 */
export function even (value) {
  if ($.isNumeric(value)) {
    const valueAsInteger = parseInt(value)
    if ((value === valueAsInteger) && (valueAsInteger % 2 === 0)) {
      return true
    }
  }
  return false
}

/**
 * Checks whether an argument can be interpreted as an even integer
 * @param   {Object}  value A value of any type
 * @returns {Boolean} True when value is numeric, parses as an integer (no matter if decimal, octal or sexadecimal)
 */
export function odd (value) {
  if ($.isNumeric(value)) {
    const valueAsInteger = parseInt(value)
    if ((value === valueAsInteger) && (valueAsInteger % 2 === 1)) {
      return true
    }
  }
  return false
}

/**
 * Check for a label in a configuration object
 * @param   {Object}  configurationObject a configuration object
 * @param   {String}  label               a label to check for
 * @returns {boolean} true if label present and true, false otherwise
 */
export function checkFor (configurationObject, label) {
  return ((label in configurationObject) && (configurationObject[label]))
}

/**
 * return argument as object (if it is no object the value is {})
 * @returns {Object}
 */
export function asObject (argument) {
  return (typeof argument === 'object') ? argument : {}
}

/**
 * Gets the subconfig or an empty object
 * @param {object} config
 * @param {string} name
 * @returns {object}
 */
export function getConfig (config, name) {
  if (config.hasOwnProperty(name) && config[name]) {
    return asObject(config[name])
  }
}

// //////////////////////////////////////////////////////////////////////////////////////// //
//                      async image load                                                    //
// //////////////////////////////////////////////////////////////////////////////////////// //

/**
 * @param {HTMLImageElement} image
 * @param {URL} origUrl
 * @param {string} [finalUrl]
 * @returns {Promise}
 */
export function asyncImageLoad (image, origUrl, finalUrl) {
  if (!finalUrl) {
    finalUrl = origUrl.finalize()
  }
  return new Promise((resolve, reject) => {
    function onError () {
      reject(new Error(`Error loading url ${finalUrl}`))
    }
    image.addEventListener('load', resolve)
    image.addEventListener('error', onError)
    if (!origUrl.username || !origUrl.password) {
      image.src = finalUrl
    } else {
      const xhr = new XMLHttpRequest() // eslint-disable-line no-undef
      xhr.open('GET', finalUrl, true)
      xhr.responseType = 'blob'

      origUrl.setAuth(xhr)

      xhr.addEventListener('load', function () {
        if (this.status === 200) {
          const urlCreator = window.URL || window.webkitURL
          image.src = urlCreator.createObjectURL(this.response)
        } else {
          onError()
        }
      })

      xhr.send()
    }
  })
}

// //////////////////////////////////////////////////////////////////////////////////////// //
//                      finish all ajax requests then continue                              //
// //////////////////////////////////////////////////////////////////////////////////////// //

/**
 * finishs loading all images contained in the given jQuery object.
 * @param {jQuery} $object
 * @returns {Promise}
 */
export function finishAllImages ($object) {
  const imagePromises = []

  const $images = recursiveSelect($object, 'img')

  $images.each(function () {
    const image = this

    if (!image.complete) {
      imagePromises.push(new Promise(resolve => {
        $(image).on('load', resolve)
        $(image).on('error', resolve)
      }))
    }
  })

  return Promise.all(imagePromises)
}

/**
 * calculates the distance between one and another jQuery element
 * @param {jQuery} $one
 * @param {jQuery} $other
 * @returns {{top: number, left: number}}
 */
export function offset ($one, $other) {
  const oneOff = $one.offset()
  const otherOff = $other.offset()
  return { top: oneOff.top - otherOff.top, left: oneOff.left - otherOff.left }
}

// //////////////////////////////////////////////////////////////////////////////////////////
//                                   jQuery Extensions                                    //
// //////////////////////////////////////////////////////////////////////////////////////////

/**
 * selects all matching elements and child elements
 * @param {jQuery} $elem
 * @param {string} query
 * @returns {jQuery}
 */
export function recursiveSelect ($elem, query) {
  return $elem.filter(query).add($elem.find(query))
}

// //////////////////////////////////////////////////////////////////////////////////////// //
//                                Structural Functions                                      //
// //////////////////////////////////////////////////////////////////////////////////////// //

export function showInteractionActivity (map) {
  Debug.info('superseding interactions:')
  let k, i
  let total
  let amountActive

  for (k in map.supersedingInteractions_) {
    total = map.supersedingInteractions_[k].length
    amountActive = 0
    for (i = 0; i < total; i++) {
      if (map.supersedingInteractions_[k][i].getActive()) {
        amountActive += 1
      }
    }

    Debug.info('  ' + k + ': total: ' + total + ' active: ' + amountActive)
  }

  Debug.info('default interactions:')

  for (k in map.defaultInteractions_) {
    total = map.defaultInteractions_[k].length
    amountActive = 0
    for (i = 0; i < total; i++) {
      if (map.defaultInteractions_[k][i].getActive()) {
        amountActive += 1
      }
    }

    Debug.info('  ' + k + ': total: ' + total + ' active: ' + amountActive)
  }
}

// //////////////////////////////////////////////////////////////////////////////////////// //
//                                     URL Functions                                        //
// //////////////////////////////////////////////////////////////////////////////////////// //

// the functions are designed to mimic the behaviour of the node path module.
// differences: dirURLs will end in /'s
// the are in no way complete and don't claim to be complete
// NOTE: another way to solve this would be using String.split instead of regular expressions

/**
 * A function that tries to get the dir url of an url
 * @param {string} url
 * @returns {string}
 */
export function urlDirname (url) {
  return url.replace(/\/([^/]*)(\?.*)?$/, '/')
}

/**
 * A function that normalizes a url
 * @param {string} url
 * @returns {string}
 */
export function urlNormalize (url) {
  if (url.match(/^\.\//)) {
    return urlNormalize(url.replace(/^\.\//, ''))
  } else {
    return url
  }
}

/**
 * A function that adds urls
 * @param {string} urlRoot
 * @param {string} urlExt
 * @returns {string}
 */
export function urlJoin (urlRoot, urlExt) {
  let normPathRoot = urlDirname(urlNormalize(urlRoot))
  let normPathExt = urlNormalize(urlExt)

  const lastPart = /[^/]+\/$/
  const leadingDoubleDots = /^\.\.\//

  while (normPathRoot.match(lastPart) && normPathExt.match(leadingDoubleDots)) {
    normPathRoot = normPathRoot.replace(lastPart, '')
    normPathExt = normPathExt.replace(leadingDoubleDots, '')
  }

  return normPathRoot + normPathExt
}

/**
 * A function that tries to get the relative url between to urles
 * @param {string} source
 * @param {string} target
 * @returns {string}
 */
export function urlRelative (source, target) {
  let sourceNorm = urlDirname(urlNormalize(source))
  let targetNorm = urlNormalize(target)

  let urlRelative = ''

  const firstPart = /^\/?((\/\/)|[^/])+\//

  let firstSourcePart
  let firstTargetPart

  while (sourceNorm.match(firstPart) && targetNorm.match(firstPart)) {
    firstSourcePart = sourceNorm.match(firstPart)[0]
    firstTargetPart = targetNorm.match(firstPart)[0]

    if (firstSourcePart.toUpperCase() !== firstTargetPart.toUpperCase()) {
      break
    }

    sourceNorm = sourceNorm.replace(firstPart, '')
    targetNorm = targetNorm.replace(firstPart, '')
  }

  while (sourceNorm.match(firstPart)) {
    firstSourcePart = sourceNorm.match(firstPart)[0]
    sourceNorm = sourceNorm.replace(firstPart, '')
    urlRelative += '../'
  }

  return urlRelative + targetNorm
}

/**
 * @param {string} url
 * @returns {boolean}
 */
export function urlIsAbsolute (url) {
  return !!url.match(/^\/|.*:\/\//)
}

// //////////////////////////////////////////////////////////////////////////////////////// //
//                                         Other                                            //
// //////////////////////////////////////////////////////////////////////////////////////// //

function getPropertyNamesAndDescriptions (obj) {
  const props = {}

  do {
    Object.getOwnPropertyNames(obj).forEach(function (prop) {
      if (!props.hasOwnProperty(prop)) {
        props[prop] = Object.getOwnPropertyDescriptor(obj, prop)
      }
    })
    obj = Object.getPrototypeOf(obj)
  } while (obj !== Object.prototype)

  return props
}

/**
 * This creates a new class which inherits from the base class and mixes in every method (except any method named
 * 'initialize') from the mixin class. The mixin class may not overwrite any existing method. If it has a method called
 * 'initialize' this will be remembered and called after the constructor of the base class has finished
 * @param baseClass
 * @param mixinClasses
 * @returns {class}
 */
export function mixin (baseClass, mixinClasses) {
  if (!Array.isArray(mixinClasses)) {
    mixinClasses = [mixinClasses]
  }
  const initializes = mixinClasses.map(mC => mC.prototype.initialize)

  const mixed = class extends baseClass {
    constructor (options) {
      super(options)
      for (const initialize of initializes) {
        if (initialize) {
          initialize.call(this, options)
        }
      }
    }
  }

  for (const mixinClass of mixinClasses) {
    const propsAndDescriptions = getPropertyNamesAndDescriptions(mixinClass.prototype)

    for (const name in propsAndDescriptions) {
      if (name !== 'constructor' && name !== 'initialize') {
        if (name in mixed.prototype) {
          throw new Error('mixins should not overwrite methods')
        }
        Object.defineProperty(mixed.prototype, name, propsAndDescriptions[name])
      }
    }
  }

  return mixed
}

/**
 * This returns a mixin as a normal class.
 * @param mixinClass
 * @returns {class}
 */
export function mixinAsClass (mixinClass) {
  const initialize = mixinClass.prototype.initialize

  const m = class extends mixinClass {
    constructor (options) {
      super(options)
      if (initialize) {
        initialize.call(this, options)
      }
    }
  }

  return m
}

const $p = $('<p>')

/**
 * Takes a string with HTML and returns the containing resulting text.
 * @param stringWithHTML string with encoded HTML entities
 * @returns {string}
 */
export function html2Text (stringWithHTML) {
  return $p.html(stringWithHTML).text()
}