/** * 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; } }