Forked from necolas/createOrderedCSSStyleSheet.js
Last active
January 25, 2019 09:02
-
-
Save giuseppeg/ff4e4870548e22e9ce587999c0371cd4 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. | |
*/ | |
type OrderedCSSStyleSheet = { | |
getTextContent: () => string, | |
insert: (rule: string, group: number, position: number) => number | |
} | |
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet): OrderedCSSStyleSheet { | |
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() { | |
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, group, position = 0) { | |
// 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); | |
} | |
} | |
if (groups[group].indexOf(rule) !== -1) { | |
throw new Error('OrderedCSSStyleSheet: trying to insert duplicate rule: ' + rule) | |
} | |
// Add rule to group. | |
// 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 && root.cssRules) { | |
return 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: CSSMediaRule): string { | |
let groupRule = mediaRule.cssRules[0] | |
if (groupRule) { | |
groupRule = groupRule.selectorText.split('"')[1]; | |
if (groupRule) { | |
return groupRule | |
} | |
} | |
throw new Error('OrderedCSSStyleSheet: group rule not found.') | |
} | |
function getOrderedGroups(obj: { [key: number]: any }) { | |
return Object.keys(obj) | |
.sort() | |
.map(k => Number(k)); | |
} | |
function hydrate(groups: Groups, sheet: CSSStyleSheet) { | |
Array.prototype.forEach.call(sheet.cssRules, mediaRule => { | |
if (mediaRule.media == null) { | |
throw new Error( | |
'OrderedCSSStyleSheet: hydrating invalid stylesheet. Expected only @media rules.' | |
); | |
} | |
const group = decodeGroupRule(mediaRule); | |
// TODO: normalize cssText across hydration and insertion | |
groups[group] = Array.prototype.map.call(rules, rule => rule.cssText); | |
}); | |
} | |
// The default for position is 0 | |
function insertRuleAt(root: CSSStyleSheet|CSSSMediaRule, rule: string, position: number = 0): ?number { | |
// JSDOM doesn't support `CSSSMediaRule#insertRule`. | |
if (typeof root.insertRule !== 'function' && process.env.NODE_ENV !== 'production') { | |
console.warn(`OrderedCSSStyleSheet: failed to inject CSS "${rule}".`); | |
return null | |
} | |
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'. | |
return root.insertRule(rule, position); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment