all files / src/ inject.js

100% Statements 86/86
100% Branches 37/37
100% Functions 16/16
100% Lines 86/86
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257                                              27×   26×     26×     24× 24×   24× 24× 24×         27×     26×                     17× 13×                                                   10×                                                             59× 58×     17×           16× 16×     57× 57×                   68× 53×       53×       42× 42× 42× 42×     25×     24×     36× 36× 36× 36×     28× 28× 27×                                             57×   57× 57× 57×     68× 62× 62×       57×   55×   55×               55×    
/* @flow */
import asap from 'asap';
 
import OrderedElements from './ordered-elements';
import {generateCSS} from './generate';
import {flattenDeep, hashObject} from './util';
 
/* ::
import type { SheetDefinition, SheetDefinitions } from './index.js';
import type { MaybeSheetDefinition } from './exports.js';
import type { SelectorHandler } from './generate.js';
*/
 
// The current <style> tag we are inserting into, or null if we haven't
// inserted anything yet. We could find this each time using
// `document.querySelector("style[data-aphrodite"])`, but holding onto it is
// faster.
let styleTag = null;
 
// Inject a string of styles into a <style> tag in the head of the document. This
// will automatically create a style tag and then continue to use it for
// multiple injections. It will also use a style tag with the `data-aphrodite`
// tag on it if that exists in the DOM. This could be used for e.g. reusing the
// same style tag that server-side rendering inserts.
const injectStyleTag = (cssContents /* : string */) => {
    if (styleTag == null) {
        // Try to find a style tag with the `data-aphrodite` attribute first.
        styleTag = document.querySelector("style[data-aphrodite]");
 
        // If that doesn't work, generate a new style tag.
        if (styleTag == null) {
            // Taken from
            // http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
            const head = document.head || document.getElementsByTagName('head')[0];
            styleTag = document.createElement('style');
 
            styleTag.type = 'text/css';
            styleTag.setAttribute("data-aphrodite", "");
            head.appendChild(styleTag);
        }
    }
 
 
    if (styleTag.styleSheet) {
        // $FlowFixMe: legacy Internet Explorer compatibility
        styleTag.styleSheet.cssText += cssContents;
    } else {
        styleTag.appendChild(document.createTextNode(cssContents));
    }
};
 
// Custom handlers for stringifying CSS values that have side effects
// (such as fontFamily, which can cause @font-face rules to be injected)
const stringHandlers = {
    // With fontFamily we look for objects that are passed in and interpret
    // them as @font-face rules that we need to inject. The value of fontFamily
    // can either be a string (as normal), an object (a single font face), or
    // an array of objects and strings.
    fontFamily: function fontFamily(val) {
        if (Array.isArray(val)) {
            return val.map(fontFamily).join(",");
        } else if (typeof val === "object") {
            injectStyleOnce(val.src, "@font-face", [val], false);
            return `"${val.fontFamily}"`;
        } else {
            return val;
        }
    },
 
    // With animationName we look for an object that contains keyframes and
    // inject them as an `@keyframes` block, returning a uniquely generated
    // name. The keyframes object should look like
    //  animationName: {
    //    from: {
    //      left: 0,
    //      top: 0,
    //    },
    //    '50%': {
    //      left: 15,
    //      top: 5,
    //    },
    //    to: {
    //      left: 20,
    //      top: 20,
    //    }
    //  }
    // TODO(emily): `stringHandlers` doesn't let us rename the key, so I have
    // to use `animationName` here. Improve that so we can call this
    // `animation` instead of `animationName`.
    animationName: function animationName(val, selectorHandlers) {
        if (Array.isArray(val)) {
            return val.map(v => animationName(v, selectorHandlers)).join(",");
        } else if (typeof val === "object") {
            // Generate a unique name based on the hash of the object. We can't
            // just use the hash because the name can't start with a number.
            // TODO(emily): this probably makes debugging hard, allow a custom
            // name?
            const name = `keyframe_${hashObject(val)}`;
 
            // Since keyframes need 3 layers of nesting, we use `generateCSS` to
            // build the inner layers and wrap it in `@keyframes` ourselves.
            let finalVal = `@keyframes ${name}{`;
 
            if (val instanceof OrderedElements) {
                val.forEach((valVal, valKey) => {
                    finalVal += generateCSS(
                        valKey, [valVal], selectorHandlers, stringHandlers, false);
                });
            } else {
                Object.keys(val).forEach(key => {
                    finalVal += generateCSS(
                        key, [val[key]], selectorHandlers, stringHandlers, false);
                });
            }
            finalVal += '}';
 
            injectGeneratedCSSOnce(name, finalVal);
 
            return name;
        } else {
            return val;
        }
    },
};
 
// This is a map from Aphrodite's generated class names to `true` (acting as a
// set of class names)
let alreadyInjected = {};
 
// This is the buffer of styles which have not yet been flushed.
let injectionBuffer = "";
 
// A flag to tell if we are already buffering styles. This could happen either
// because we scheduled a flush call already, so newly added styles will
// already be flushed, or because we are statically buffering on the server.
let isBuffering = false;
 
const injectGeneratedCSSOnce = (key, generatedCSS) => {
    if (!alreadyInjected[key]) {
        if (!isBuffering) {
            // We should never be automatically buffering on the server (or any
            // place without a document), so guard against that.
            if (typeof document === "undefined") {
                throw new Error(
                    "Cannot automatically buffer without a document");
            }
 
            // If we're not already buffering, schedule a call to flush the
            // current styles.
            isBuffering = true;
            asap(flushToStyleTag);
        }
 
        injectionBuffer += generatedCSS;
        alreadyInjected[key] = true;
    }
}
 
export const injectStyleOnce = (
    key /* : string */,
    selector /* : string */,
    definitions /* : SheetDefinition[] */,
    useImportant /* : boolean */,
    selectorHandlers /* : SelectorHandler[] */ = []
) => {
    if (!alreadyInjected[key]) {
        const generated = generateCSS(
            selector, definitions, selectorHandlers,
            stringHandlers, useImportant);
 
        injectGeneratedCSSOnce(key, generated);
    }
};
 
export const reset = () => {
    injectionBuffer = "";
    alreadyInjected = {};
    isBuffering = false;
    styleTag = null;
};
 
export const startBuffering = () => {
    if (isBuffering) {
        throw new Error(
            "Cannot buffer while already buffering");
    }
    isBuffering = true;
};
 
export const flushToString = () => {
    isBuffering = false;
    const ret = injectionBuffer;
    injectionBuffer = "";
    return ret;
};
 
export const flushToStyleTag = () => {
    const cssContent = flushToString();
    if (cssContent.length > 0) {
        injectStyleTag(cssContent);
    }
};
 
export const getRenderedClassNames = () => {
    return Object.keys(alreadyInjected);
};
 
export const addRenderedClassNames = (classNames /* : string[] */) => {
    classNames.forEach(className => {
        alreadyInjected[className] = true;
    });
};
 
/**
 * Inject styles associated with the passed style definition objects, and return
 * an associated CSS class name.
 *
 * @param {boolean} useImportant If true, will append !important to generated
 *     CSS output. e.g. {color: red} -> "color: red !important".
 * @param {(Object|Object[])[]} styleDefinitions style definition objects, or
 *     arbitrarily nested arrays of them, as returned as properties of the
 *     return value of StyleSheet.create().
 */
export const injectAndGetClassName = (
    useImportant /* : boolean */,
    styleDefinitions /* : MaybeSheetDefinition[] */,
    selectorHandlers /* : SelectorHandler[] */
) /* : string */ => {
    styleDefinitions = flattenDeep(styleDefinitions);
 
    const classNameBits = [];
    const definitionBits = [];
    for (let i = 0; i < styleDefinitions.length; i += 1) {
        // Filter out falsy values from the input, to allow for
        // `css(a, test && c)`
        if (styleDefinitions[i]) {
            classNameBits.push(styleDefinitions[i]._name);
            definitionBits.push(styleDefinitions[i]._definition);
        }
    }
    // Break if there aren't any valid styles.
    if (classNameBits.length === 0) {
        return "";
    }
    const className = classNameBits.join("-o_O-");
 
    injectStyleOnce(
        className,
        `.${className}`,
        definitionBits,
        useImportant,
        selectorHandlers
    );
 
    return className;
}