UNPKG

styled-components

Version:

Visual primitives for the component age. Use the best bits of ES6 and CSS to style your apps without stress 💅

478 lines (404 loc) • 12.5 kB
// @flow /* eslint-disable flowtype/object-type-delimiter */ /* eslint-disable react/prop-types */ import React, { type Element } from 'react' import { IS_BROWSER, DISABLE_SPEEDY, SC_ATTR } from '../constants' import { type ExtractedComp } from '../utils/extractCompsFromCSS' import { splitByRules } from '../utils/stringifyRules' import getNonce from '../utils/nonce' import once from '../utils/once' import { type Names, addNameForId, resetIdNames, hasNameForId, stringifyNames, cloneNames, } from '../utils/styleNames' import { sheetForTag, safeInsertRule, deleteRules, } from '../utils/insertRuleHelpers' export interface Tag<T> { // $FlowFixMe: Doesn't seem to accept any combination w/ HTMLStyleElement for some reason styleTag: HTMLStyleElement | null; /* lists all ids of the tag */ getIds(): string[]; /* checks whether `name` is already injected for `id` */ hasNameForId(id: string, name: string): boolean; /* inserts a marker to ensure the id's correct position in the sheet */ insertMarker(id: string): T; /* inserts rules according to the ids markers */ insertRules(id: string, cssRules: string[], name: ?string): void; /* removes all rules belonging to the id, keeping the marker around */ removeRules(id: string): void; css(): string; toHTML(additionalAttrs: ?string): string; toElement(): Element<*>; clone(): Tag<T>; } /* this error is used for makeStyleTag */ const parentNodeUnmountedErr = process.env.NODE_ENV !== 'production' ? ` Trying to insert a new style tag, but the given Node is unmounted! - Are you using a custom target that isn't mounted? - Does your document not have a valid head element? - Have you accidentally removed a style tag manually? `.trim() : '' /* this error is used for tags */ const throwCloneTagErr = () => { throw new Error( process.env.NODE_ENV !== 'production' ? ` The clone method cannot be used on the client! - Are you running in a client-like environment on the server? - Are you trying to run SSR on the client? `.trim() : '' ) } /* this marker separates component styles and is important for rehydration */ const makeTextMarker = id => `\n/* sc-component-id: ${id} */\n` /* add up all numbers in array up until and including the index */ const addUpUntilIndex = (sizes: number[], index: number): number => { let totalUpToIndex = 0 for (let i = 0; i <= index; i += 1) { totalUpToIndex += sizes[i] } return totalUpToIndex } /* create a new style tag after lastEl */ const makeStyleTag = ( target: ?HTMLElement, tagEl: ?Node, insertBefore: ?boolean ) => { const el = document.createElement('style') el.setAttribute(SC_ATTR, '') const nonce = getNonce() if (nonce) { el.setAttribute('nonce', nonce) } /* Work around insertRule quirk in EdgeHTML */ el.appendChild(document.createTextNode('')) if (target && !tagEl) { /* Append to target when no previous element was passed */ target.appendChild(el) } else { if (!tagEl || !target || !tagEl.parentNode) { throw new Error(parentNodeUnmountedErr) } /* Insert new style tag after the previous one */ tagEl.parentNode.insertBefore(el, insertBefore ? tagEl : tagEl.nextSibling) } return el } /* takes a css factory function and outputs an html styled tag factory */ const wrapAsHtmlTag = (css: () => string, names: Names) => ( additionalAttrs: ?string ): string => { const nonce = getNonce() const attrs = [ nonce && `nonce="${nonce}"`, `${SC_ATTR}="${stringifyNames(names)}"`, additionalAttrs, ] const htmlAttr = attrs.filter(Boolean).join(' ') return `<style ${htmlAttr}>${css()}</style>` } /* takes a css factory function and outputs an element factory */ const wrapAsElement = (css: () => string, names: Names) => () => { const props = { [SC_ATTR]: stringifyNames(names), } const nonce = getNonce() if (nonce) { // $FlowFixMe props.nonce = nonce } // eslint-disable-next-line react/no-danger return <style {...props} dangerouslySetInnerHTML={{ __html: css() }} /> } const getIdsFromMarkersFactory = (markers: Object) => (): string[] => Object.keys(markers) /* speedy tags utilise insertRule */ const makeSpeedyTag = ( el: HTMLStyleElement, getImportRuleTag: ?() => Tag<any> ): Tag<number> => { const names: Names = (Object.create(null): Object) const markers = Object.create(null) const sizes: number[] = [] const extractImport = getImportRuleTag !== undefined /* indicates whther getImportRuleTag was called */ let usedImportRuleTag = false const insertMarker = id => { const prev = markers[id] if (prev !== undefined) { return prev } markers[id] = sizes.length sizes.push(0) resetIdNames(names, id) return markers[id] } const insertRules = (id, cssRules, name) => { const marker = insertMarker(id) const sheet = sheetForTag(el) const insertIndex = addUpUntilIndex(sizes, marker) let injectedRules = 0 const importRules = [] const cssRulesSize = cssRules.length for (let i = 0; i < cssRulesSize; i += 1) { const cssRule = cssRules[i] let mayHaveImport = extractImport /* @import rules are reordered to appear first */ if (mayHaveImport && cssRule.indexOf('@import') !== -1) { importRules.push(cssRule) } else if (safeInsertRule(sheet, cssRule, insertIndex + injectedRules)) { mayHaveImport = false injectedRules += 1 } } if (extractImport && importRules.length > 0) { usedImportRuleTag = true // $FlowFixMe getImportRuleTag().insertRules(`${id}-import`, importRules) } sizes[marker] += injectedRules /* add up no of injected rules */ addNameForId(names, id, name) } const removeRules = id => { const marker = markers[id] if (marker === undefined) return const size = sizes[marker] const sheet = sheetForTag(el) const removalIndex = addUpUntilIndex(sizes, marker) deleteRules(sheet, removalIndex, size) sizes[marker] = 0 resetIdNames(names, id) if (extractImport && usedImportRuleTag) { // $FlowFixMe getImportRuleTag().removeRules(`${id}-import`) } } const css = () => { const { cssRules } = sheetForTag(el) let str = '' // eslint-disable-next-line guard-for-in for (const id in markers) { str += makeTextMarker(id) const marker = markers[id] const end = addUpUntilIndex(sizes, marker) const size = sizes[marker] for (let i = end - size; i < end; i += 1) { const rule = cssRules[i] if (rule !== undefined) { str += rule.cssText } } } return str } return { styleTag: el, getIds: getIdsFromMarkersFactory(markers), hasNameForId: hasNameForId(names), insertMarker, insertRules, removeRules, css, toHTML: wrapAsHtmlTag(css, names), toElement: wrapAsElement(css, names), clone: throwCloneTagErr, } } const makeTextNode = id => document.createTextNode(makeTextMarker(id)) const makeBrowserTag = ( el: HTMLStyleElement, getImportRuleTag: ?() => Tag<any> ): Tag<Text> => { const names = (Object.create(null): Object) const markers = Object.create(null) const extractImport = getImportRuleTag !== undefined /* indicates whther getImportRuleTag was called */ let usedImportRuleTag = false const insertMarker = id => { const prev = markers[id] if (prev !== undefined) { return prev } markers[id] = makeTextNode(id) el.appendChild(markers[id]) names[id] = Object.create(null) return markers[id] } const insertRules = (id, cssRules, name) => { const marker = insertMarker(id) const importRules = [] const cssRulesSize = cssRules.length for (let i = 0; i < cssRulesSize; i += 1) { const rule = cssRules[i] let mayHaveImport = extractImport if (mayHaveImport && rule.indexOf('@import') !== -1) { importRules.push(rule) } else { mayHaveImport = false const separator = i === cssRulesSize - 1 ? '' : ' ' marker.appendData(`${rule}${separator}`) } } addNameForId(names, id, name) if (extractImport && importRules.length > 0) { usedImportRuleTag = true // $FlowFixMe getImportRuleTag().insertRules(`${id}-import`, importRules) } } const removeRules = id => { const marker = markers[id] if (marker === undefined) return /* create new empty text node and replace the current one */ const newMarker = makeTextNode(id) el.replaceChild(newMarker, marker) markers[id] = newMarker resetIdNames(names, id) if (extractImport && usedImportRuleTag) { // $FlowFixMe getImportRuleTag().removeRules(`${id}-import`) } } const css = () => { let str = '' // eslint-disable-next-line guard-for-in for (const id in markers) { str += markers[id].data } return str } return { clone: throwCloneTagErr, css, getIds: getIdsFromMarkersFactory(markers), hasNameForId: hasNameForId(names), insertMarker, insertRules, removeRules, styleTag: el, toElement: wrapAsElement(css, names), toHTML: wrapAsHtmlTag(css, names), } } const makeServerTagInternal = (namesArg, markersArg): Tag<[string]> => { const names = namesArg === undefined ? (Object.create(null): Object) : namesArg const markers = markersArg === undefined ? Object.create(null) : markersArg const insertMarker = id => { const prev = markers[id] if (prev !== undefined) { return prev } return (markers[id] = ['']) } const insertRules = (id, cssRules, name) => { const marker = insertMarker(id) marker[0] += cssRules.join(' ') addNameForId(names, id, name) } const removeRules = id => { const marker = markers[id] if (marker === undefined) return marker[0] = '' resetIdNames(names, id) } const css = () => { let str = '' // eslint-disable-next-line guard-for-in for (const id in markers) { const cssForId = markers[id][0] if (cssForId) { str += makeTextMarker(id) + cssForId } } return str } const clone = () => { const namesClone = cloneNames(names) const markersClone = Object.create(null) // eslint-disable-next-line guard-for-in for (const id in markers) { markersClone[id] = [markers[id][0]] } return makeServerTagInternal(namesClone, markersClone) } const tag = { clone, css, getIds: getIdsFromMarkersFactory(markers), hasNameForId: hasNameForId(names), insertMarker, insertRules, removeRules, styleTag: null, toElement: wrapAsElement(css, names), toHTML: wrapAsHtmlTag(css, names), } return tag } const makeServerTag = (): Tag<[string]> => makeServerTagInternal() export const makeTag = ( target: ?HTMLElement, tagEl: ?HTMLStyleElement, forceServer?: boolean, insertBefore?: boolean, getImportRuleTag?: () => Tag<any> ): Tag<any> => { if (IS_BROWSER && !forceServer) { const el = makeStyleTag(target, tagEl, insertBefore) if (DISABLE_SPEEDY) { return makeBrowserTag(el, getImportRuleTag) } else { return makeSpeedyTag(el, getImportRuleTag) } } return makeServerTag() } /* wraps a given tag so that rehydration is performed once when necessary */ export const makeRehydrationTag = ( tag: Tag<any>, els: HTMLStyleElement[], extracted: ExtractedComp[], immediateRehydration: boolean ): Tag<any> => { /* rehydration function that adds all rules to the new tag */ const rehydrate = once(() => { /* add all extracted components to the new tag */ for (let i = 0, len = extracted.length; i < len; i += 1) { const { componentId, cssFromDOM } = extracted[i] const cssRules = splitByRules(cssFromDOM) tag.insertRules(componentId, cssRules) } /* remove old HTMLStyleElements, since they have been rehydrated */ for (let i = 0, len = els.length; i < len; i += 1) { const el = els[i] if (el.parentNode) { el.parentNode.removeChild(el) } } }) if (immediateRehydration) rehydrate() return { ...tag, /* add rehydration hook to insertion methods */ insertMarker: id => { rehydrate() return tag.insertMarker(id) }, insertRules: (id, cssRules, name) => { rehydrate() return tag.insertRules(id, cssRules, name) }, } }