Skip to content

Instantly share code, notes, and snippets.

@giuseppeg
Forked from necolas/createOrderedCSSStyleSheet.js
Last active January 25, 2019 09:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save giuseppeg/ff4e4870548e22e9ce587999c0371cd4 to your computer and use it in GitHub Desktop.
Save giuseppeg/ff4e4870548e22e9ce587999c0371cd4 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.
*/
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