Skip to content

Instantly share code, notes, and snippets.

@necolas
Last active January 26, 2019 13:42
Show Gist options
  • Save necolas/6154c89121d3c29e5f7cf057d2bc9b70 to your computer and use it in GitHub Desktop.
Save necolas/6154c89121d3c29e5f7cf057d2bc9b70 to your computer and use it in GitHub Desktop.
OrderedCSSStyleSheet: control the insertion order of CSS
/**
* 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