1 | ;
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 |
|
7 | var _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 |
|
9 | var _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 |
|
11 | 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; }; }();
|
12 |
|
13 | var _react = require('react');
|
14 |
|
15 | var _react2 = _interopRequireDefault(_react);
|
16 |
|
17 | var _reactLibAdler = require('react-lib-adler32');
|
18 |
|
19 | var _reactLibAdler2 = _interopRequireDefault(_reactLibAdler);
|
20 |
|
21 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
22 |
|
23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
24 |
|
25 | function _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 |
|
27 | function _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 |
|
35 | var __DEV__ = process.env.NODE_ENV !== 'production';
|
36 |
|
37 | var 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 | '>': '>',
|
171 | '<': '<'
|
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 |
|
505 | Style.it = function (cssText, rootElement) {
|
506 | return _react2.default.createElement(
|
507 | Style,
|
508 | null,
|
509 | cssText,
|
510 | rootElement
|
511 | );
|
512 | };
|
513 |
|
514 | exports.default = Style; |
\ | No newline at end of file |