rofrischmann/react-look

View on GitHub
packages/react-look/modules/core/renderer.js

Summary

Maintainability
A
1 hr
Test Coverage
import _ from 'lodash'

import StyleContainer from '../api/StyleContainer'
import sortPseudoClasses from '../utils/sortPseudoClasses'
import isMediaQuery from '../utils/isMediaQuery'
import isPseudo from '../utils/isPseudo'
import isDynamicArray from '../utils/isDynamicArray'

/**
 * Extracts all possible dynamic styles out of a style object
 * To be able to render all other (static) styles directly to CSS
 * @param {Object} styles - pure style object which gets parsed
 */
export function extractDynamicStyles(styles) {
  // early return stateful selectors
  if (_.isFunction(styles)) {
    return { _statefulSelector: styles }
  }

  return Object.keys(styles).reduce((dynamic, property) => {
    const value = styles[property]; // eslint-disable-line
    if (_.isPlainObject(value)) {
      // only consider pseudo classes and media queries
      // that contain inner dynamic styles
      if (isPseudo(property) || isMediaQuery(property)) {
        const valueCount = Object.keys(value).length

        const innerDynamic = extractDynamicStyles(value)

        // if the inner styles contain dynamic styles
        // extract them into the output object
        if (!_.isEmpty(innerDynamic)) {
          dynamic[property] = innerDynamic
        }

        // Remove the property if all inner styles
        // are actually dynamic styles
        if (Object.keys(innerDynamic).length === valueCount) {
          delete styles[property]
        }
      } else {
        dynamic[property] = value
        delete styles[property]
      }
    }

    // function are considered stateful styles and therefore
    // treated as dynamic styles
    if (_.isFunction(value) || _.isBoolean(value) || (_.isArray(value) && isDynamicArray(value))) {
      dynamic[property] = value
      delete styles[property]
    }

    return dynamic; // eslint-disable-line
  }, { })
}

/**
 * Renders special styles as pseudo classes and media queries
 * and adds them to the CSS StyleContainer
 * @param {string} selector - base selector used as className
 * @param {Object} styles - static styles containing special extensions
 * @param {string} pseudo - prior outer pseudo class
 * @param {string} media - prior outer media query
 */
export function renderSpecialStyles(selector, styles, pseudo = '', media = '') {
  return Object.keys(styles).sort(sortPseudoClasses).reduce((extension, property) => {
    const value = styles[property]; // eslint-disable-line

    if (_.isPlainObject(value)) {
      if (isPseudo(property)) {
        const innerStyles = renderSpecialStyles(selector, value, pseudo + property, media)
        // Adds a pseudo class to an existing selector
        StyleContainer.add('.' + selector + pseudo + property, innerStyles, media)
      }
      if (isMediaQuery(property)) {
        // Concatenate multiple media queries if a media query already exists
        const newMedia = ((media !== '' ? media + ' and ' : '') + property.replace('@media', '').trim())
        const innerStyles = renderSpecialStyles(selector, value, pseudo, newMedia)
        // Adds the selector to the media group
        StyleContainer.add('.' + selector + pseudo, innerStyles, newMedia)
      }
    } else {
      extension[property] = value
    }
    return extension; // eslint-disable-line
  }, { })
}


/**
 * Renders static styles to the CSS StyleContainer
 * and directly scopes them to the Component
 * @param {Object} styles - static styles to be rendered
 * @param {string} scope - scope selector
 * @param {string} selector - base selector used as className
 */
export default function renderStaticStyles(styles, scope) {
  // Extracts dynamic parts remaining only static styles
  const dynamicStyles = extractDynamicStyles(styles)

  // Determines the base styles used to generate the className
  const baseStyles = Object.keys(styles).reduce((base, property) => {
    const value = styles[property]
    if (!_.isPlainObject(value)) {
      base[property] = value
      delete styles[property]
    }
    return base; // eslint-disable-line
  }, { })

  // Generate a unique className
  const className = scope ? scope + StyleContainer.generateClassName(baseStyles) : StyleContainer.requestClassName()

  // Add the className to the global style container if it has styles
  if (!_.isEmpty(baseStyles)) {
    StyleContainer.add('.' + className, baseStyles)
  }

  // Also add the dynamic styles if they exist
  if (!_.isEmpty(dynamicStyles) || _.isFunction(dynamicStyles)) {
    StyleContainer._addDynamic(className, dynamicStyles)
  }

  // Renders pseudo classes and media queries to the style container
  renderSpecialStyles(className, styles)

  return className
}