170 lines
5.0 KiB
JavaScript
170 lines
5.0 KiB
JavaScript
/**
|
|
* 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.
|
|
*
|
|
*
|
|
*/
|
|
var slice = Array.prototype.slice;
|
|
/**
|
|
* Order-based insertion of CSS.
|
|
*
|
|
* Each rule is associated with a numerically defined group.
|
|
* Groups are ordered within the style sheet according to their number, with the
|
|
* lowest first.
|
|
*
|
|
* Groups are implemented using marker rules. The selector of the first rule of
|
|
* each group is used only to encode the group number for hydration. An
|
|
* alternative implementation could rely on CSSMediaRule, allowing groups to be
|
|
* treated as a sub-sheet, but the Edge implementation of CSSMediaRule is
|
|
* broken.
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
|
|
* https://gist.github.com/necolas/aa0c37846ad6bd3b05b727b959e82674
|
|
*/
|
|
|
|
export default function createOrderedCSSStyleSheet(sheet) {
|
|
var groups = {};
|
|
var selectors = {};
|
|
/**
|
|
* Hydrate approximate record from any existing rules in the sheet.
|
|
*/
|
|
|
|
if (sheet != null) {
|
|
var group;
|
|
slice.call(sheet.cssRules).forEach(function (cssRule, i) {
|
|
var cssText = cssRule.cssText; // Create record of existing selectors and rules
|
|
|
|
if (cssText.indexOf('stylesheet-group') > -1) {
|
|
group = decodeGroupRule(cssRule);
|
|
groups[group] = {
|
|
start: i,
|
|
rules: [cssText]
|
|
};
|
|
} else {
|
|
var selectorText = getSelectorText(cssText);
|
|
|
|
if (selectorText != null) {
|
|
selectors[selectorText] = true;
|
|
groups[group].rules.push(cssText);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function sheetInsert(sheet, group, text) {
|
|
var orderedGroups = getOrderedGroups(groups);
|
|
var groupIndex = orderedGroups.indexOf(group);
|
|
var nextGroupIndex = groupIndex + 1;
|
|
var nextGroup = orderedGroups[nextGroupIndex]; // Insert rule before the next group, or at the end of the stylesheet
|
|
|
|
var position = nextGroup != null && groups[nextGroup].start != null ? groups[nextGroup].start : sheet.cssRules.length;
|
|
var isInserted = insertRuleAt(sheet, text, position);
|
|
|
|
if (isInserted) {
|
|
// Set the starting index of the new group
|
|
if (groups[group].start == null) {
|
|
groups[group].start = position;
|
|
} // Increment the starting index of all subsequent groups
|
|
|
|
|
|
for (var i = nextGroupIndex; i < orderedGroups.length; i += 1) {
|
|
var groupNumber = orderedGroups[i];
|
|
var previousStart = groups[groupNumber].start;
|
|
groups[groupNumber].start = previousStart + 1;
|
|
}
|
|
}
|
|
|
|
return isInserted;
|
|
}
|
|
|
|
var OrderedCSSStyleSheet = {
|
|
/**
|
|
* The textContent of the style sheet.
|
|
*/
|
|
getTextContent: function getTextContent() {
|
|
return getOrderedGroups(groups).map(function (group) {
|
|
var rules = groups[group].rules;
|
|
return rules.join('\n');
|
|
}).join('\n');
|
|
},
|
|
|
|
/**
|
|
* Insert a rule into the style sheet
|
|
*/
|
|
insert: function insert(cssText, groupValue) {
|
|
var group = Number(groupValue); // Create a new group.
|
|
|
|
if (groups[group] == null) {
|
|
var markerRule = encodeGroupRule(group); // Create the internal record.
|
|
|
|
groups[group] = {
|
|
start: null,
|
|
rules: [markerRule]
|
|
}; // Update CSSOM.
|
|
|
|
if (sheet != null) {
|
|
sheetInsert(sheet, group, markerRule);
|
|
}
|
|
} // selectorText is more reliable than cssText for insertion checks. The
|
|
// browser excludes vendor-prefixed properties and rewrites certain values
|
|
// making cssText more likely to be different from what was inserted.
|
|
|
|
|
|
var selectorText = getSelectorText(cssText);
|
|
|
|
if (selectorText != null && selectors[selectorText] == null) {
|
|
// Update the internal records.
|
|
selectors[selectorText] = true;
|
|
groups[group].rules.push(cssText); // Update CSSOM.
|
|
|
|
if (sheet != null) {
|
|
var isInserted = sheetInsert(sheet, group, cssText);
|
|
|
|
if (!isInserted) {
|
|
// Revert internal record change if a rule was rejected (e.g.,
|
|
// unrecognized pseudo-selector)
|
|
groups[group].rules.pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return OrderedCSSStyleSheet;
|
|
}
|
|
/**
|
|
* Helper functions
|
|
*/
|
|
|
|
function encodeGroupRule(group) {
|
|
return "[stylesheet-group=\"" + group + "\"]{}";
|
|
}
|
|
|
|
function decodeGroupRule(cssRule) {
|
|
return Number(cssRule.selectorText.split(/["']/)[1]);
|
|
}
|
|
|
|
function getOrderedGroups(obj) {
|
|
return Object.keys(obj).map(Number).sort(function (a, b) {
|
|
return a > b ? 1 : -1;
|
|
});
|
|
}
|
|
|
|
var pattern = /\s*([,])\s*/g;
|
|
|
|
function getSelectorText(cssText) {
|
|
var selector = cssText.split('{')[0].trim();
|
|
return selector !== '' ? selector.replace(pattern, '$1') : null;
|
|
}
|
|
|
|
function insertRuleAt(root, cssText, position) {
|
|
try {
|
|
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
|
|
root.insertRule(cssText, position);
|
|
return true;
|
|
} catch (e) {
|
|
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
|
|
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
|
|
return false;
|
|
}
|
|
} |