'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _warning = require('warning'); var _warning2 = _interopRequireDefault(_warning); var _sheets = require('../sheets'); var _sheets2 = _interopRequireDefault(_sheets); var _StyleRule = require('../rules/StyleRule'); var _StyleRule2 = _interopRequireDefault(_StyleRule); var _toCssValue = require('../utils/toCssValue'); var _toCssValue2 = _interopRequireDefault(_toCssValue); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Cache the value from the first time a function is called. */ var memoize = function memoize(fn) { var value = void 0; return function () { if (!value) value = fn(); return value; }; }; /** * Get a style property value. */ function getPropertyValue(cssRule, prop) { try { return cssRule.style.getPropertyValue(prop); } catch (err) { // IE may throw if property is unknown. return ''; } } /** * Set a style property. */ function setProperty(cssRule, prop, value) { try { var cssValue = value; if (Array.isArray(value)) { cssValue = (0, _toCssValue2['default'])(value, true); if (value[value.length - 1] === '!important') { cssRule.style.setProperty(prop, cssValue, 'important'); return true; } } cssRule.style.setProperty(prop, cssValue); } catch (err) { // IE may throw if property is unknown. return false; } return true; } /** * Remove a style property. */ function removeProperty(cssRule, prop) { try { cssRule.style.removeProperty(prop); } catch (err) { (0, _warning2['default'])(false, '[JSS] DOMException "%s" was thrown. Tried to remove property "%s".', err.message, prop); } } var CSSRuleTypes = { STYLE_RULE: 1, KEYFRAMES_RULE: 7 /** * Get the CSS Rule key. */ };var getKey = function () { var extractKey = function extractKey(cssText) { var from = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; return cssText.substr(from, cssText.indexOf('{') - 1); }; return function (cssRule) { if (cssRule.type === CSSRuleTypes.STYLE_RULE) return cssRule.selectorText; if (cssRule.type === CSSRuleTypes.KEYFRAMES_RULE) { var name = cssRule.name; if (name) return '@keyframes ' + name; // There is no rule.name in the following browsers: // - IE 9 // - Safari 7.1.8 // - Mobile Safari 9.0.0 var cssText = cssRule.cssText; return '@' + extractKey(cssText, cssText.indexOf('keyframes')); } // Conditionals. return extractKey(cssRule.cssText); }; }(); /** * Set the selector. */ function setSelector(cssRule, selectorText) { cssRule.selectorText = selectorText; // Return false if setter was not successful. // Currently works in chrome only. return cssRule.selectorText === selectorText; } /** * Gets the `head` element upon the first call and caches it. */ var getHead = memoize(function () { return document.head || document.getElementsByTagName('head')[0]; }); /** * Gets a map of rule keys, where the property is an unescaped key and value * is a potentially escaped one. * It is used to identify CSS rules and the corresponding JSS rules. As an identifier * for CSSStyleRule we normally use `selectorText`. Though if original selector text * contains escaped code points e.g. `:not(#\\20)`, CSSOM will compile it to `:not(# )` * and so CSS rule's `selectorText` won't match JSS rule selector. * * https://www.w3.org/International/questions/qa-escapes#cssescapes */ var getUnescapedKeysMap = function () { var style = void 0; var isAttached = false; return function (rules) { var map = {}; // https://github.com/facebook/flow/issues/2696 if (!style) style = document.createElement('style'); for (var i = 0; i < rules.length; i++) { var rule = rules[i]; if (!(rule instanceof _StyleRule2['default'])) continue; var selector = rule.selector; // Only unescape selector over CSSOM if it contains a back slash. if (selector && selector.indexOf('\\') !== -1) { // Lazilly attach when needed. if (!isAttached) { getHead().appendChild(style); isAttached = true; } style.textContent = selector + ' {}'; var _style = style, sheet = _style.sheet; if (sheet) { var cssRules = sheet.cssRules; if (cssRules) map[cssRules[0].selectorText] = rule.key; } } } if (isAttached) { getHead().removeChild(style); isAttached = false; } return map; }; }(); /** * Find attached sheet with an index higher than the passed one. */ function findHigherSheet(registry, options) { for (var i = 0; i < registry.length; i++) { var sheet = registry[i]; if (sheet.attached && sheet.options.index > options.index && sheet.options.insertionPoint === options.insertionPoint) { return sheet; } } return null; } /** * Find attached sheet with the highest index. */ function findHighestSheet(registry, options) { for (var i = registry.length - 1; i >= 0; i--) { var sheet = registry[i]; if (sheet.attached && sheet.options.insertionPoint === options.insertionPoint) { return sheet; } } return null; } /** * Find a comment with "jss" inside. */ function findCommentNode(text) { var head = getHead(); for (var i = 0; i < head.childNodes.length; i++) { var node = head.childNodes[i]; if (node.nodeType === 8 && node.nodeValue.trim() === text) { return node; } } return null; } /** * Find a node before which we can insert the sheet. */ function findPrevNode(options) { var registry = _sheets2['default'].registry; if (registry.length > 0) { // Try to insert before the next higher sheet. var sheet = findHigherSheet(registry, options); if (sheet) return sheet.renderer.element; // Otherwise insert after the last attached. sheet = findHighestSheet(registry, options); if (sheet) return sheet.renderer.element.nextElementSibling; } // Try to find a comment placeholder if registry is empty. var insertionPoint = options.insertionPoint; if (insertionPoint && typeof insertionPoint === 'string') { var comment = findCommentNode(insertionPoint); if (comment) return comment.nextSibling; // If user specifies an insertion point and it can't be found in the document - // bad specificity issues may appear. (0, _warning2['default'])(insertionPoint === 'jss', '[JSS] Insertion point "%s" not found.', insertionPoint); } return null; } /** * Insert style element into the DOM. */ function insertStyle(style, options) { var insertionPoint = options.insertionPoint; var prevNode = findPrevNode(options); if (prevNode) { var parentNode = prevNode.parentNode; if (parentNode) parentNode.insertBefore(style, prevNode); return; } // Works with iframes and any node types. if (insertionPoint && typeof insertionPoint.nodeType === 'number') { // https://stackoverflow.com/questions/41328728/force-casting-in-flow var insertionPointElement = insertionPoint; var _parentNode = insertionPointElement.parentNode; if (_parentNode) _parentNode.insertBefore(style, insertionPointElement.nextSibling);else (0, _warning2['default'])(false, '[JSS] Insertion point is not in the DOM.'); return; } getHead().insertBefore(style, prevNode); } /** * Read jss nonce setting from the page if the user has set it. */ var getNonce = memoize(function () { var node = document.querySelector('meta[property="csp-nonce"]'); return node ? node.getAttribute('content') : null; }); var DomRenderer = function () { function DomRenderer(sheet) { _classCallCheck(this, DomRenderer); this.getPropertyValue = getPropertyValue; this.setProperty = setProperty; this.removeProperty = removeProperty; this.setSelector = setSelector; this.getKey = getKey; this.getUnescapedKeysMap = getUnescapedKeysMap; this.hasInsertedRules = false; // There is no sheet when the renderer is used from a standalone StyleRule. if (sheet) _sheets2['default'].add(sheet); this.sheet = sheet; var _ref = this.sheet ? this.sheet.options : {}, media = _ref.media, meta = _ref.meta, element = _ref.element; this.element = element || document.createElement('style'); this.element.setAttribute('data-jss', ''); if (media) this.element.setAttribute('media', media); if (meta) this.element.setAttribute('data-meta', meta); var nonce = getNonce(); if (nonce) this.element.setAttribute('nonce', nonce); } /** * Insert style element into render tree. */ // HTMLStyleElement needs fixing https://github.com/facebook/flow/issues/2696 _createClass(DomRenderer, [{ key: 'attach', value: function attach() { // In the case the element node is external and it is already in the DOM. if (this.element.parentNode || !this.sheet) return; // When rules are inserted using `insertRule` API, after `sheet.detach().attach()` // browsers remove those rules. // TODO figure out if its a bug and if it is known. // Workaround is to redeploy the sheet before attaching as a string. if (this.hasInsertedRules) { this.deploy(); this.hasInsertedRules = false; } insertStyle(this.element, this.sheet.options); } /** * Remove style element from render tree. */ }, { key: 'detach', value: function detach() { this.element.parentNode.removeChild(this.element); } /** * Inject CSS string into element. */ }, { key: 'deploy', value: function deploy() { if (!this.sheet) return; this.element.textContent = '\n' + this.sheet.toString() + '\n'; } /** * Insert a rule into element. */ }, { key: 'insertRule', value: function insertRule(rule, index) { var sheet = this.element.sheet; var cssRules = sheet.cssRules; var str = rule.toString(); if (!index) index = cssRules.length; if (!str) return false; try { sheet.insertRule(str, index); } catch (err) { (0, _warning2['default'])(false, '[JSS] Can not insert an unsupported rule \n\r%s', rule); return false; } this.hasInsertedRules = true; return cssRules[index]; } /** * Delete a rule. */ }, { key: 'deleteRule', value: function deleteRule(cssRule) { var sheet = this.element.sheet; var index = this.indexOf(cssRule); if (index === -1) return false; sheet.deleteRule(index); return true; } /** * Get index of a CSS Rule. */ }, { key: 'indexOf', value: function indexOf(cssRule) { var cssRules = this.element.sheet.cssRules; for (var _index = 0; _index < cssRules.length; _index++) { if (cssRule === cssRules[_index]) return _index; } return -1; } /** * Generate a new CSS rule and replace the existing one. */ }, { key: 'replaceRule', value: function replaceRule(cssRule, rule) { var index = this.indexOf(cssRule); var newCssRule = this.insertRule(rule, index); this.element.sheet.deleteRule(index); return newCssRule; } /** * Get all rules elements. */ }, { key: 'getRules', value: function getRules() { return this.element.sheet.cssRules; } }]); return DomRenderer; }(); exports['default'] = DomRenderer;