UNPKG

22.9 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
8
9var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
10
11var _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; }; }();
12
13var _react = require('react');
14
15var _react2 = _interopRequireDefault(_react);
16
17var _reactLibAdler = require('react-lib-adler32');
18
19var _reactLibAdler2 = _interopRequireDefault(_reactLibAdler);
20
21function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22
23function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
24
25function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
26
27function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /**
28 * Copyright 2016-present, Joshua Robinson
29 * All rights reserved.
30 *
31 * This source code is licensed under the MIT license.
32 *
33 */
34
35var __DEV__ = process.env.NODE_ENV !== 'production';
36
37var Style = function (_Component) {
38 _inherits(Style, _Component);
39
40 function Style(props) {
41 _classCallCheck(this, Style);
42
43 var _this = _possibleConstructorReturn(this, (Style.__proto__ || Object.getPrototypeOf(Style)).call(this, props));
44
45 _this.getStyleString = function () {
46 if (_this.props.children instanceof Array) {
47 var styleString = _this.props.children.filter(function (child) {
48 return !(0, _react.isValidElement)(child) && typeof child === 'string';
49 });
50
51 if (styleString.length > 1) {
52 throw new Error('Multiple style objects as direct descedents of a ' + 'Style component are not supported (' + styleString.length + ' style objects detected): \n\n' + styleString[0]);
53 }
54
55 return styleString[0];
56 } else if (typeof _this.props.children === 'string' && !(0, _react.isValidElement)(_this.props.children)) {
57 return _this.props.children;
58 } else {
59 return null;
60 }
61 };
62
63 _this.getRootElement = function () {
64 if (_this.props.children instanceof Array) {
65 var rootElement = _this.props.children.filter(function (child) {
66 return (0, _react.isValidElement)(child);
67 });
68
69 if (__DEV__) {
70 if (rootElement.length > 1) {
71 console.log(rootElement);
72 throw new Error('Adjacent JSX elements must be wrapped in an enclosing tag (' + rootElement.length + ' root elements detected).');
73 }
74
75 if (typeof rootElement[0] !== 'undefined' && _this.isVoidElement(rootElement[0].type)) {
76 throw new Error('Self-closing void elements like ' + rootElement.type + ' must be wrapped ' + 'in an enclosing tag. Reactive Style must be able to nest a style element ' + 'inside of the root element and void element content models never allow' + 'it to have contents under any circumstances.');
77 }
78 }
79
80 return rootElement[0];
81 } else if ((0, _react.isValidElement)(_this.props.children)) {
82 return _this.props.children;
83 } else {
84 return null;
85 }
86 };
87
88 _this.getRootSelectors = function (rootElement) {
89 var rootSelectors = [];
90
91 // Handle id
92 if (rootElement.props.id) {
93 rootSelectors.push('#' + rootElement.props.id);
94 }
95
96 // Handle classes
97 if (rootElement.props.className) {
98 rootElement.props.className.trim().split(/\s+/g).forEach(function (className) {
99 return rootSelectors.push(className);
100 });
101 }
102
103 // Handle no root selector by using type
104 if (!rootSelectors.length && typeof rootElement.type !== 'function') {
105 rootSelectors.push(rootElement.type);
106 }
107
108 return rootSelectors;
109 };
110
111 _this.processCSSText = function (styleString, scopeClassName, rootSelectors) {
112 // TODO: Look into using memoizeStringOnly from fbjs/lib for escaped strings;
113 // can avoid much of the computation as long as scoped doesn't come into play
114 // which would be unique
115
116 // TODO: If dev lint and provide feedback
117 // if linting fails we need to error out because
118 // the style string will not be parsed correctly
119
120 return styleString.replace(/\s*\/\/(?![^(]*\)).*|\s*\/\*.*\*\//g, '') // Strip javascript style comments
121 .replace(/\s\s+/g, ' ') // Convert multiple to single whitespace
122 .split('}') // Start breaking down statements
123 .map(function (fragment) {
124 var isDeclarationBodyPattern = /.*:.*;/g;
125 var isLastItemDeclarationBodyPattern = /.*:.*(;|$|\s+)/g;
126 var isAtRulePattern = /\s*@/g;
127 var isKeyframeOffsetPattern = /\s*(([0-9][0-9]?|100)\s*%)|\s*(to|from)\s*$/g;
128
129 // Split fragment into selector and declarationBody; escape declaration body
130 return fragment.split('{').map(function (statement, i, arr) {
131 // Avoid processing whitespace
132 if (!statement.trim().length) {
133 return '';
134 }
135
136 var isDeclarationBodyItemWithOptionalSemicolon =
137 // Only for the last property-value in a
138 // CSS declaration body is a semicolon optional
139 arr.length - 1 === i && statement.match(isLastItemDeclarationBodyPattern);
140 // Skip escaping selectors statements since that would break them;
141 // note in docs that selector statements are not escaped and should
142 // not be generated from user provided strings
143 if (statement.match(isDeclarationBodyPattern) || isDeclarationBodyItemWithOptionalSemicolon) {
144 return _this.escapeTextContentForBrowser(statement);
145 } else {
146 // Statement is a selector
147 var selector = statement;
148
149 if (scopeClassName && !/:target/gi.test(selector)) {
150 // Prefix the scope to the selector if it is not an at-rule
151 if (!selector.match(isAtRulePattern) && !selector.match(isKeyframeOffsetPattern)) {
152 return _this.scopeSelector(scopeClassName, selector, rootSelectors);
153 } else {
154 // Is at-rule or keyframe offset and should not be scoped
155 return selector;
156 }
157 } else {
158 // No scope; do nothing to the selector
159 return selector;
160 }
161 }
162
163 // Pretty print in dev
164 }).join('{\n');
165 }).join('}\n');
166 };
167
168 _this.escaper = function (match) {
169 var ESCAPE_LOOKUP = {
170 '>': '&gt;',
171 '<': '&lt;'
172 };
173
174 return ESCAPE_LOOKUP[match];
175 };
176
177 _this.escapeTextContentForBrowser = function (text) {
178 var ESCAPE_REGEX = /[><]/g;
179 return ('' + text).replace(ESCAPE_REGEX, _this.escaper);
180 };
181
182 _this.scopeSelector = function (scopeClassName, selector, rootSelectors) {
183 var scopedSelector = [];
184
185 // Matches comma-delimiters in multi-selectors (".fooClass, .barClass {...}" => "," );
186 // ignores commas-delimiters inside of brackets and parenthesis ([attr=value], :not()..)
187 var groupOfSelectorsPattern = /,(?![^(|[]*\)|\])/g;
188
189 var selectors = selector.split(groupOfSelectorsPattern);
190
191 for (var i = 0; i < selectors.length; i++) {
192 var containsSelector = void 0; // [data-scoped="54321"] .someClass
193 var unionSelector = void 0; // [data-scoped="54321"].someClass (account for root)
194
195 if (rootSelectors.length && rootSelectors.some(function (rootSelector) {
196 return selector.match(rootSelector);
197 })) {
198 unionSelector = selectors[i];
199
200 // Can't just add them together because of selector combinator complexity
201 // like '.rootClassName.someClass.otherClass > *' or :not('.rootClassName'),
202 // replace must be used
203
204 // Escape valid CSS special characters that are also RegExp special characters
205 var escapedRootSelectors = rootSelectors.map(function (rootSelector) {
206 return rootSelector.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
207 });
208
209 unionSelector = unionSelector.replace(new RegExp('(' + // Start capture group
210 escapedRootSelectors.join('|') + // Match any one root selector
211 ')' // End capture group
212 ), '$1' + scopeClassName // Replace any one root selector match with a union
213 ); // of the root selector and scoping class (e.g., .rootSelector._scoped-1). Order matters here because of type-class union support like div._scoped-1
214
215 // Do both union and contains selectors because of case <div><div></div></div>
216 // or <div className="foo"><div className="foo"></div></div>
217 containsSelector = scopeClassName + ' ' + selectors[i];
218 scopedSelector.push(unionSelector, containsSelector);
219 } else {
220 containsSelector = scopeClassName + ' ' + selectors[i];
221 scopedSelector.push(containsSelector);
222 }
223 }
224
225 return scopedSelector.join(', ');
226 };
227
228 _this.getScopeClassName = function (styleString, rootElement) {
229 var hash = styleString;
230
231 if (rootElement) {
232 _this.pepper = '';
233 _this.traverseObjectToGeneratePepper(rootElement);
234 hash += _this.pepper;
235 }
236
237 return (__DEV__ ? 'scope-' : 's') + (0, _reactLibAdler2.default)(hash);
238 };
239
240 _this.traverseObjectToGeneratePepper = function (obj) {
241 var depth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
242
243 // Max depth is equal to max depth of JSON.stringify
244 // Max length of 10,000 is arbitrary
245 if (depth > 32 || _this.pepper.length > 10000) return;
246
247 for (var prop in obj) {
248 // Avoid internal props that are unreliable
249 var isPropReactInternal = /^[_$]|type|ref|^value$/.test(prop);
250 if (!!obj[prop] && _typeof(obj[prop]) === 'object' && !isPropReactInternal) {
251 _this.traverseObjectToGeneratePepper(obj[prop], depth + 1);
252 } else if (!!obj[prop] && !isPropReactInternal && typeof obj[prop] !== 'function') {
253 _this.pepper += obj[prop];
254 }
255 }
256 };
257
258 _this.isVoidElement = function (type) {
259 return ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'].some(function (voidType) {
260 return type === voidType;
261 });
262 };
263
264 _this.addCSSTextToHead = function (cssText) {
265 if (!cssText.length) {
266 return;
267 } else {
268 var cssTextHash = (0, _reactLibAdler2.default)(cssText);
269
270 if (!window._reactiveStyle.cssTextHashesAddedToHead.some(function (hash) {
271 return hash === cssTextHash;
272 })) {
273 window._reactiveStyle.el.innerHTML += cssText;
274 window._reactiveStyle.cssTextHashesAddedToHead.push(cssTextHash);
275 }
276 }
277 };
278
279 _this.createStyleElement = function (cssText, scopeClassName) {
280 return _react2.default.createElement('style', { type: 'text/css', key: scopeClassName, ref: function ref(c) {
281 return _this._style = c;
282 },
283 dangerouslySetInnerHTML: {
284 __html: cssText || ''
285 } });
286 };
287
288 _this.getNewChildrenForCloneElement = function (cssText, rootElement, scopeClassName) {
289 return [_this.createStyleElement(cssText, scopeClassName)].concat(rootElement.props.children);
290 };
291
292 _this.scopeClassNameCache = {};
293 _this.scopedCSSTextCache = {};
294 return _this;
295 }
296
297 _createClass(Style, [{
298 key: 'render',
299 value: function render() {
300 if (!this.props.children) {
301 return this.createStyleElement();
302 }
303
304 var styleString = this.getStyleString();
305 var rootElement = this.getRootElement();
306
307 if (!styleString && rootElement) {
308 // Passthrough; no style actions
309 return rootElement.props.children;
310 } else if (styleString && !rootElement) {
311 // Global styling with no scoping
312 return this.createStyleElement(this.processCSSText(styleString), this.getScopeClassName(styleString, rootElement));
313 } else {
314 // Style tree of elements
315 var rootElementClassNames = rootElement.props.className ? rootElement.props.className + ' ' : '';
316 var rootElementId = rootElement.props.id ? rootElement.props.id : '';
317
318 // If styleString has already been calculated before and CSS text is unchanged;
319 // use the cached version. No need to recalculate.
320 var scopeClassName = void 0;
321 var scopedCSSText = void 0;
322 // Include rootElementClassName and rootElementId as part of cache address
323 // to ensure upon state/prop change resulting in new id/class on root element
324 // will properly generate a union selector.
325 // WARNING: May be a preoptimization; cost of adding union selector to all selectors
326 // could be so low that its worth doing so to avoid surface space for bugs
327 var scopeClassNameAddress = rootElementClassNames + rootElementId + styleString;
328 if (this.scopeClassNameCache[scopeClassNameAddress]) {
329 // Use cached scope and scoped CSS Text
330 scopeClassName = this.scopeClassNameCache[scopeClassNameAddress];
331 scopedCSSText = this.scopedCSSTextCache[scopeClassName];
332 } else {
333 // Calculate scope and scoped CSS Text
334 scopeClassName = this.getScopeClassName(styleString, rootElement);
335 scopedCSSText = this.processCSSText(styleString, '.' + scopeClassName, this.getRootSelectors(rootElement));
336
337 // Cache for future use
338 this.scopeClassNameCache[scopeClassNameAddress] = scopeClassName;
339 this.scopedCSSTextCache[scopeClassName] = scopedCSSText;
340 }
341
342 return (0, _react.cloneElement)(rootElement, _extends({}, rootElement.props, {
343 className: '' + rootElementClassNames + scopeClassName
344 }), this.getNewChildrenForCloneElement(scopedCSSText, rootElement, scopeClassName));
345 }
346 }
347
348 /**
349 * Filters out the style string from this.props.children
350 *
351 * > getStyleString()
352 * ".foo { color: red; }"
353 *
354 * @return {?string} string Style string
355 */
356
357
358 /**
359 * Filters out the root element from this.props.children
360 *
361 * > getRootElement()
362 * "<MyRootElement />"
363 *
364 * @return {?ReactDOMComponent} component Root element component
365 */
366
367
368 /**
369 * Creates an array of selectors which target the root element
370 *
371 * > getRootSelectors( <div id="foo" className="bar" /> )
372 * "['#foo', '.bar']"
373 *
374 * @param {ReactDOMComponent} component
375 * @return {!array} array Array of selectors that target the root element
376 */
377
378
379 /**
380 * Scopes CSS statement with a given scoping class name as a union or contains selector;
381 * also escapes CSS declaration bodies
382 *
383 * > proccessStyleString( '.foo { color: red; } .bar { color: green; }', '_scoped-1234, ['.root', '.foo'] )
384 * ".scoped-1234.foo { color: red; } .scoped-1234 .bar { color: green; }"
385 *
386 * @param {string} styleString String of style rules
387 * @param {string} scopeClassName Class name used to create a unique scope
388 * @param {array} rootSelectors Array of selectors on the root element; ids and classNames
389 * @return {!string} Scoped style rule string
390 */
391
392
393 /**
394 * Escaper used in escapeTextContentForBrowser
395 *
396 */
397
398
399 /**
400 * Escapes text to prevent scripting attacks.
401 *
402 * @param {*} text Text value to escape.
403 * @return {string} An escaped string.
404 */
405
406
407 /**
408 * Scopes a selector with a given scoping className as a union or contains selector
409 *
410 * > scopeSelector( '_scoped-1827481', '.root', ['.root', '.foo'] )
411 * ".scoped-1827481.root"
412 *
413 * @param {string} scopeClassName Class name used to scope selectors
414 * @param {string} selector Selector to scope
415 * @param {array} rootSelectors Array of selectors on the root element; ids and classNames
416 * @return {!string} Union or contains selector scoped with the scoping className
417 */
418
419
420 /**
421 * Creates a className used as a CSS scope by generating a checksum from a styleString
422 *
423 * > scoped( 'footer { color: red; }' )
424 * "_scoped-182938591"
425 *
426 * @param {string} String of style rules
427 * @return {!string} A scoping class name
428 */
429
430
431 /**
432 * Traverses an object tree looking for anything that is not internal or a circular
433 * reference. Accumulates values on this.pepper
434 *
435 * > traverseObjectToGeneratePepper(obj)
436 * void
437 * @param {object} object Object to traverse
438 */
439
440
441 /**
442 * Checks if a tag type is a self-closing void element
443 *
444 * > isVoidElement( "img" )
445 * "true"
446 *
447 * @param {*} string Element type to check
448 * @return {!bool} bool True or false
449 */
450
451
452 /**
453 * Add CSS text to the style element in the head of document unless it has
454 * already been added.
455 *
456 * > addCSSTextToHead( ".foo { color: red; }" )
457 *
458 * @param {string} string CSS text to add to head
459 */
460
461
462 /**
463 * Creates the style element used for server side rendering
464 * > createStyleElement( ".foo._scoped-1 { color: red; }" )
465 *
466 *
467 * @param {string} string CSS string
468 * @return {ReactDOMComponent} component
469 */
470
471
472 /**
473 * Returns new children for a root element being cloned. If mounted the CSS text
474 * is added to the style element in head, otherwise we are doing server side rendering
475 * and to avoid flash of unstyled content (FOUC) a style element is added to children
476 * to avoid FOUC on first render.
477 *
478 * > getNewChildrenForCloneElement( ".foo._scoped-1 { color: red; }" )
479 * "<NewChildren />"
480 *
481 * @param {string} string CSS string
482 * @return {ReactDOMComponent} component
483 */
484
485
486 /**
487 * Syntactic sugar for functional usage of Reactive Style
488 *
489 * > Style.it( ".foo { color: red; }", <div /> )
490 * "<div class="_scoped-1">
491 * <style type="text/css">
492 * .foo._scoped-1 { color: red; }
493 * </style>
494 * </div>"
495 *
496 * @param {string} string CSS string
497 * @param {ReactDOMComponent} component
498 */
499
500 }]);
501
502 return Style;
503}(_react.Component);
504
505Style.it = function (cssText, rootElement) {
506 return _react2.default.createElement(
507 Style,
508 null,
509 cssText,
510 rootElement
511 );
512};
513
514exports.default = Style;
\No newline at end of file