Source: utils/css.js

/**
 * @namespace qui.utils.css
 */

import $ from '$qui/lib/jquery.module.js'

import * as Window from '$qui/window.js'


const CSS_VALUE_RE = new RegExp('([\\d.]+)(.*)')

let customStyleElement = null
let cachedBodyEmPx = null


function getCustomStyleElement() {
    if (!customStyleElement) {
        customStyleElement = createStyleElement()
    }

    return customStyleElement
}


/**
 * Create an empty style element and add it to document head.
 * @alias qui.utils.css.createStyleElement
 * @returns {HTMLStyleElement}
 */
export function createStyleElement() {
    let style = document.createElement('style')
    style.type = 'text/css'
    style.appendChild(document.createTextNode('')) /* Webkit hack */

    let head = document.getElementsByTagName('head')[0]
    head.appendChild(style)

    return style
}

/**
 * Return the link element from the DOM corresponding to a given CSS file.
 * @alias qui.utils.css.getStyleLinkElement
 * @param {String} filename
 * @returns {?HTMLLinkElement}
 */
export function getStyleLinkElement(filename) {
    let links = [...document.getElementsByTagName('link')]
    return links.find(l => l.href.endsWith(filename)) || null
}

/**
 * Create and add a style rule.
 * @alias qui.utils.css.addRule
 * @param {String} selector a CSS selector (e.g. `"div.my-class > div.my-sub-class"`)
 * @param {CSSStyleDeclaration|String} style style declaration (e.g. `"text-align: left; padding: 5px;"`)
 * @param {HTMLStyleElement} [styleElement] an optional style element to add the rule to; by default, rules are added to
 * a common custom style element
 * @returns {Number} the index of the rule within the style element rules list
 */
export function addRule(selector, style, styleElement = null) {
    if (style instanceof window.CSSStyleDeclaration) {
        style = style.cssText
    }

    styleElement = styleElement || getCustomStyleElement()
    return styleElement.sheet.insertRule(`${selector} { ${style} }`, styleElement.sheet.cssRules.length)
}

/**
 * Delete a style rule that matches a given selector.
 * @alias qui.utils.css.delRule
 * @param {String} selector a CSS selector (e.g. `"div.my-class > div.my-sub-class"`)
 * @param {HTMLStyleElement} [styleElement] an optional style element to restrict the search to
 */
export function delRule(selector, styleElement = null) {
    let styleElements

    if (styleElement) {
        styleElements = [styleElement]
    }
    else {
        /* Look through all existing style elements */
        styleElements = [...document.getElementsByTagName('style')]
    }

    styleElements.forEach(function (styleElement) {
        for (let i = 0; i < styleElement.sheet.cssRules.length; i++) {
            let rule = styleElement.sheet.cssRules[i]
            if (rule.selectorText === selector) {
                styleElement.sheet.deleteRule(i--)
            }
        }
    })
}

/**
 * Find all style rules that match a selector regular expression.
 * @alias qui.utils.css.findRules
 * @param {String|RegExp} selectorRe
 * @returns {Object[]} a list of objects with `selector` and `declaration`
 */
export function findRules(selectorRe) {
    if (!(selectorRe instanceof RegExp)) {
        selectorRe = new RegExp(selectorRe)
    }

    let matchedRules = []
    let styleSheets = [...document.styleSheets]
    styleSheets.forEach(function (sheet) {
        if (sheet.disabled) {
            return
        }

        let rules = [...sheet.cssRules]
        let mRules = rules.filter(r => r.selectorText && r.selectorText.match(selectorRe)).map(function (rule) {
            return {
                selector: rule.selectorText,
                declaration: rule.cssText.substring(rule.selectorText.length).trim()
            }
        })

        matchedRules = matchedRules.concat(mRules)
    })

    return matchedRules
}

/**
 * Perform an add operation on a CSS value with unit, preserving the unit.
 * @alias qui.utils.css.addValue
 * @param {String} value the CSS value (e.g. `"15px"`)
 * @param {Number|String} operand the number to add
 * @returns {String}
 */
export function addValue(value, operand) {
    let parts = value.toString().match(CSS_VALUE_RE)
    if (!parts) {
        return value
    }

    return parseFloat(parts[1]) + parseFloat(operand) + parts[2]
}

/**
 * Perform a subtract operation on a CSS value with unit, preserving the unit.
 * @alias qui.utils.css.subValue
 * @param {String} value the CSS value (e.g. `"15px"`)
 * @param {Number|String} operand the number to subtract
 * @returns {String}
 */
export function subValue(value, operand) {
    let parts = value.toString().match(CSS_VALUE_RE)
    if (!parts) {
        return value
    }

    return parseFloat(parts[1]) - parseFloat(operand) + parts[2]
}

/**
 * Perform a multiply operation on a CSS value with unit, preserving the unit.
 * @alias qui.utils.css.mulValue
 * @param {String} value the CSS value (e.g. `"15px"`)
 * @param {Number|String} operand the number to multiply by
 * @returns {String}
 */
export function mulValue(value, operand) {
    let parts = value.toString().match(CSS_VALUE_RE)
    if (!parts) {
        return value
    }

    return parseFloat(parts[1]) * parseFloat(operand) + parts[2]
}

/**
 * Perform a divide operation on a CSS value with unit, preserving the unit.
 * @alias qui.utils.css.divValue
 * @param {String} value the CSS value (e.g. `"15px"`)
 * @param {Number|String} operand the number to divide by
 * @returns {String}
 */
export function divValue(value, operand) {
    let parts = value.toString().match(CSS_VALUE_RE)
    if (!parts) {
        return value
    }

    return parseFloat(parts[1]) / parseFloat(operand) + parts[2]
}

/**
 * Convert *em* units to pixel units.
 * @alias qui.utils.css.em2px
 * @param {Number} em
 * @param {jQuery} [elem] optional HTML element; document's body is used by default
 * @returns {Number}
 */
export function em2px(em, elem = Window.$body) {
    if (elem === Window.$body && cachedBodyEmPx != null) {
        return cachedBodyEmPx * em
    }

    let dummyDiv = $('<div></div>', {style: 'width: 1em'})
    elem.append(dummyDiv)

    let clientWidth = dummyDiv[0].clientWidth
    let width = clientWidth * em
    dummyDiv.remove()

    if (elem === Window.$body) {
        cachedBodyEmPx = clientWidth
    }

    return width
}

/**
 * Convert pixel units to *em* units.
 * @alias qui.utils.css.px2em
 * @param {Number} px
 * @param {jQuery} [elem] optional HTML element; document's body is used by default
 * @returns {Number}
 */
export function px2em(px, elem = Window.$body) {
    let dummyDiv = $('<div style="width: 1em"></div>')
    elem.append(dummyDiv)

    let width = px / dummyDiv.width()
    dummyDiv.remove()

    return width
}