-
-
Save m-e-h/9ec044433cd35af21d1a36149972a63d to your computer and use it in GitHub Desktop.
OrderedCSSStyleSheet: control the insertion order of CSS
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) Nicolas Gallagher. | |
* | |
* This source code is licensed under the MIT license found in the | |
* LICENSE file in the root directory of this source tree. | |
* | |
* @flow strict-local | |
*/ | |
type Groups = { [key: number]: Array<string> }; | |
/** | |
* Order-based insertion of CSS. | |
* | |
* Each rule can be inserted (appended) into a numerically defined group. | |
* Groups are ordered within the style sheet according to their number, with the | |
* lowest first. | |
* | |
* Groups are implemented using Media Query blocks. CSSMediaRule implements the | |
* CSSGroupingRule, which includes 'insertRule', allowing groups to be treated as | |
* a sub-sheet. | |
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule | |
* The selector of the first rule of each group is used only to encode the group | |
* number for hydration. | |
*/ | |
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) { | |
const groups: Groups = {}; | |
if (sheet != null) { | |
hydrate(groups, sheet); | |
} | |
const OrderedCSSStyleSheet = { | |
/** | |
* The textContent of the style sheet. | |
* Each group's rules are wrapped in a media query. | |
*/ | |
getTextContent(): string { | |
return getOrderedGroups(groups) | |
.map(group => { | |
const rules = groups[group]; | |
const str = rules.join('\n'); | |
return createMediaRule(str); | |
}) | |
.join('\n'); | |
}, | |
/** | |
* Insert a rule into a media query in the style sheet | |
*/ | |
insert(rule: string, group: number) { | |
// Create a new group. | |
if (groups[group] == null) { | |
const markerRule = encodeGroupRule(group); | |
// Create the internal record. | |
groups[group] = []; | |
groups[group].push(markerRule); | |
// Create CSSOM CSSMediaRule. | |
if (sheet != null) { | |
const groupIndex = getOrderedGroups(groups).indexOf(group); | |
insertRuleAt(sheet, createMediaRule(markerRule), groupIndex); | |
} | |
} | |
// Add rule to group. | |
if (groups[group].indexOf(rule) === -1) { | |
// Update the internal record. | |
groups[group].push(rule); | |
// Update CSSOM CSSMediaRule. | |
if (sheet != null) { | |
const groupIndex = getOrderedGroups(groups).indexOf(group); | |
const root = sheet.cssRules[groupIndex]; | |
if (root != null) { | |
// $FlowFixMe: Flow is missing CSSOM types | |
insertRuleAt(root, rule, root.cssRules.length); | |
} | |
} | |
} | |
} | |
}; | |
return OrderedCSSStyleSheet; | |
} | |
/** | |
* Helper functions | |
*/ | |
function createMediaRule(content) { | |
return '@media all {\n' + content + '\n}'; | |
} | |
function encodeGroupRule(group) { | |
return `[stylesheet-group="${group}"] {}`; | |
} | |
function decodeGroupRule(mediaRule) { | |
return mediaRule.cssRules[0].selectorText.split('"')[1]; | |
} | |
function getOrderedGroups(obj: { [key: number]: any }) { | |
return Object.keys(obj) | |
.sort() | |
.map(k => Number(k)); | |
} | |
function hydrate(groups: Groups, sheet: CSSStyleSheet) { | |
if (sheet == null) { | |
throw new Error('OrderedCSSStyleSheet: no style sheet provided'); | |
} | |
const slice = Array.prototype.slice; | |
const mediaRules = slice.call(sheet.cssRules); | |
mediaRules.forEach(mediaRule => { | |
if (mediaRule.media == null) { | |
throw new Error( | |
'OrderedCSSStyleSheet: hydrating invalid stylesheet. Expected only @media rules.' | |
); | |
} | |
const group = decodeGroupRule(mediaRule); | |
const rules = slice.call(mediaRule.cssRules); | |
// TODO: normalize cssText across hydration and insertion | |
groups[group] = rules.map(rule => rule.cssText); | |
}); | |
} | |
function insertRuleAt(root, rule: string, position: number) { | |
try { | |
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'. | |
root.insertRule(rule, position); | |
} catch (e) { | |
// JSDOM doesn't support `CSSSMediaRule#insertRule`. | |
if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { | |
console.warn(`OrderedCSSStyleSheet: failed to inject CSS "${rule}".`); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment