T4rk1n/tarkjs

View on GitHub
src/dom/dom-manipulations.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * Created by T4rk on 6/19/2017.
 */
import { objMapReducer, objItems, objExtend } from '../extensions/obj-extensions'

/**
 * Set the attributes of a DOM Element
 * @param {!Element} elem
 * @param {!Object} attributes
 */
export const setElementAttributes = (elem, attributes) => objItems(attributes)
    .forEach(([k, v]) => elem.setAttribute(k, v))

let headCache

/**
 * Cached head after first get for faster access.
 */
export const getHead = () => {
    if (headCache) return headCache
    else {
        headCache = document.querySelector('head')
        return headCache
    }
}

/**
 * {@link createElement} options.
 * @typedef {Object} CreateElementOptions
 * @property {string} [elementType='div'] The string representation of a html tag without the `<>`
 * @property {Object} [attributes={}] Attributes to set on the element before inserting in the dom.
 * @property {string} [innerHtml=''] Set the inner html before inserting in the dom.
 * @property {boolean} [front=false] Insert as first child of the container.
 * @property {boolean} [insert=true] Insert the element upon creation or later.
 * @property {function} [onload] Callback when the element has been loaded.
 */

/**
 * @type {CreateElementOptions}
 */
const defaultCreateElementOptions = {
    elementType: 'div',
    attributes: {},
    innerHtml: '',
    front: false,
    insert: true,
    onload: null
}

/**
 * Create an element only if it doesn't already exist.
 * @param {!Element} container the parent to append the new element to.
 * @param {!string} elementId unique id of the element.
 * @param {CreateElementOptions} [options]
 * @return {Element} The created or found element.
 */
export const createElement = (container, elementId, options=defaultCreateElementOptions) => {
    let element = document.getElementById(elementId)
    if (!element) {
        const {
            elementType, attributes, innerHtml, onload, front, insert
        } = objExtend({}, defaultCreateElementOptions, options)
        element = document.createElement(elementType)
        element.id = elementId
        if (innerHtml) element.innerHTML = innerHtml
        if (onload) element.onload = onload
        setElementAttributes(element, attributes)
        if (insert) {
            if (front)
                container.insertBefore(element, container.firstChild)
            else
                container.appendChild(element)
        }
    }
    return element
}

/**
 * Remove an element from the DOM.
 * @param {Element} elem
 */
export const removeElement = (elem) => elem.parentNode.removeChild(elem)

/**
 * Apply disabled attribute to a link tag.
 * @param {string} styleId
 */
export const disableStyle = (styleId) => setElementAttributes(document.getElementById(styleId), {disabled: 'disabled'})

/**
 * Get the position of an element in a page.
 * @param {Element} elem
 * @return {{left: number, top: number}}
 */
export const getOffset = (elem) => {
    const off = elem.getBoundingClientRect()
    return {
        left: off.left + window.scrollX,
        top: off.top + window.scrollY
    }
}

/**
 * Get the actual numeric value of an element style property.
 * @param {Element} elem
 * @param {string} propName
 * @return {number}
 */
export const getComputedNumStyleAttr = (elem, propName) => parseFloat(
    window.getComputedStyle(elem, null).getPropertyValue(propName))

/**
 * Get the actual font-size in pixel of an element.
 * @param {Element} elem
 * @return {number}
 */
export const getFontSize = (elem) => getComputedNumStyleAttr(elem, 'font-size')

const concatStr = (prev, next) => `${prev}${next}`

/**
 * Change the key style from camelCase to dash separated
 * @param {string} key
 * @return {string}
 */
export const formatStyleKey = (key) => key.split('')
    .map(item => item.charCodeAt() > 96 ? item: `-${item.toLowerCase()}`).reduce(concatStr, '')

/**
 * Serialize a style object to apply to an element on createElement attributes style
 * @param {Object} styleObj
 * @return {string}
 */
export const serializeStyleObj = (styleObj) => objItems(styleObj)
    .map(([k,v]) => `${formatStyleKey(k)}: ${v};`).reduce(concatStr, '')

/**
 * Get all the url params of the current page.
 * @return {Object}
 */
export const getUrlParams = () => window.location.search.substring(1).split('&')
    .map(p => p.split('=')).reduce(objMapReducer)

/**
 * {@link createDownload} options.
 * @typedef {Object} CreateDownloadOptions
 * @property {string} [type="text/plain;charset=utf-8;"] a valid mimetype.
 * @property {boolean} [revoke=true] revoke the url object created to release memory.
 * @property {click} [click=false]
 */

/**
 * @type {CreateDownloadOptions}
 */
const createDownloadOptions = {
    type: 'text/plain;charset=utf-8;', revoke: true, click: false
}

/**
 * Cross compatible download from text content.
 * @param {string} filename
 * @param {string} content
 * @param {CreateDownloadOptions} [options]
 * @return {function|undefined} activate the download
 */
export const createDownload = (filename, content, options) => {
    let activate
    const { type, revoke, click } = objExtend({}, createDownloadOptions, options)
    const blob = new Blob([content], {type})
    if (window.navigator.msSaveOrOpenBlob) {
        activate = () => window.navigator.msSaveOrOpenBlob(blob, filename)
    } else {
        activate = () => {
            const href = window.URL.createObjectURL(blob)
            const elem = createElement(document.querySelector('body'), filename, {elementType: 'a', attributes: {
                href, download: filename
            }})
            elem.click()
            removeElement(elem)
            if (revoke) window.URL.revokeObjectURL(href)
        }
    }
    if (click) activate()
    else return activate
}