UNPKG

21.3 kBJavaScriptView Raw
1import React from 'react';
2import { css, styleSheet } from 'glamor';
3
4var CHANNEL = '__glamorous__'; /* istanbul ignore next */
5
6var isPreact = false;
7
8var _PropTypes = void 0;
9
10/* istanbul ignore next */
11if (isPreact) {
12 if (!React.PropTypes) {
13 _PropTypes = function PropTypes() {
14 return _PropTypes;
15 };
16
17 ['array', 'bool', 'func', 'number', 'object', 'string', 'symbol', 'any', 'arrayOf', 'element', 'instanceOf', 'node', 'objectOf', 'oneOf', 'oneOfType', 'shape', 'exact'].forEach(function (type) {
18 _PropTypes[type] = _PropTypes;
19 });
20 }
21 // copied from preact-compat
22 /* eslint-disable no-eq-null, eqeqeq, consistent-return */
23 if (!React.Children) {
24 var Children = {
25 map: function map(children, fn, ctx) {
26 if (children == null) {
27 return null;
28 }
29 children = Children.toArray(children);
30 if (ctx && ctx !== children) {
31 fn = fn.bind(ctx);
32 }
33 return children.map(fn);
34 },
35 forEach: function forEach(children, fn, ctx) {
36 if (children == null) {
37 return null;
38 }
39 children = Children.toArray(children);
40 if (ctx && ctx !== children) {
41 fn = fn.bind(ctx);
42 }
43 children.forEach(fn);
44 },
45 count: function count(children) {
46 return children && children.length || 0;
47 },
48 only: function only(children) {
49 children = Children.toArray(children);
50 if (children.length !== 1) {
51 throw new Error('Children.only() expects only one child.');
52 }
53 return children[0];
54 },
55 toArray: function toArray(children) {
56 if (children == null) {
57 return [];
58 }
59 return [].concat(children);
60 }
61 };
62 React.Children = Children;
63 }
64 /* eslint-enable no-eq-null, eqeqeq, consistent-return */
65} else if (parseFloat(React.version.slice(0, 4)) >= 15.5) {
66 /* istanbul ignore next */
67 try {
68 _PropTypes = require('prop-types');
69 /* istanbul ignore next */
70 } catch (error) {
71 // ignore
72 }
73}
74/* istanbul ignore next */
75_PropTypes = _PropTypes || React.PropTypes;
76
77/*
78eslint
79 import/no-mutable-exports:0,
80 import/prefer-default-export:0,
81 react/no-deprecated:0
82 */
83
84var classCallCheck = function (instance, Constructor) {
85 if (!(instance instanceof Constructor)) {
86 throw new TypeError("Cannot call a class as a function");
87 }
88};
89
90var _extends = Object.assign || function (target) {
91 for (var i = 1; i < arguments.length; i++) {
92 var source = arguments[i];
93
94 for (var key in source) {
95 if (Object.prototype.hasOwnProperty.call(source, key)) {
96 target[key] = source[key];
97 }
98 }
99 }
100
101 return target;
102};
103
104var inherits = function (subClass, superClass) {
105 if (typeof superClass !== "function" && superClass !== null) {
106 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
107 }
108
109 subClass.prototype = Object.create(superClass && superClass.prototype, {
110 constructor: {
111 value: subClass,
112 enumerable: false,
113 writable: true,
114 configurable: true
115 }
116 });
117 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
118};
119
120var objectWithoutProperties = function (obj, keys) {
121 var target = {};
122
123 for (var i in obj) {
124 if (keys.indexOf(i) >= 0) continue;
125 if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
126 target[i] = obj[i];
127 }
128
129 return target;
130};
131
132var possibleConstructorReturn = function (self, call) {
133 if (!self) {
134 throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
135 }
136
137 return call && (typeof call === "object" || typeof call === "function") ? call : self;
138};
139
140function generateWarningMessage(Comp) {
141 var componentName = Comp.displayName || Comp.name || 'FunctionComponent';
142 // eslint-disable-next-line max-len
143 return 'glamorous warning: Expected component called "' + componentName + '" which uses withTheme to be within a ThemeProvider but none was found.';
144}
145
146function withTheme(ComponentToTheme) {
147 var _defaultContextTypes;
148
149 var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
150 _ref$noWarn = _ref.noWarn,
151 noWarn = _ref$noWarn === undefined ? false : _ref$noWarn,
152 _ref$createElement = _ref.createElement,
153 createElement = _ref$createElement === undefined ? true : _ref$createElement;
154
155 var ThemedComponent = function (_React$Component) {
156 inherits(ThemedComponent, _React$Component);
157
158 function ThemedComponent() {
159 var _temp, _this, _ret;
160
161 classCallCheck(this, ThemedComponent);
162
163 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
164 args[_key] = arguments[_key];
165 }
166
167 return _ret = (_temp = (_this = possibleConstructorReturn(this, _React$Component.call.apply(_React$Component, [this].concat(args))), _this), _this.warned = noWarn, _this.state = { theme: {} }, _this.setTheme = function (theme) {
168 return _this.setState({ theme: theme });
169 }, _temp), possibleConstructorReturn(_this, _ret);
170 }
171
172 // eslint-disable-next-line complexity
173 ThemedComponent.prototype.componentWillMount = function componentWillMount() {
174 if (!this.context[CHANNEL]) {
175 if (process.env.NODE_ENV !== 'production' && !this.warned) {
176 this.warned = true;
177 // eslint-disable-next-line no-console
178 console.warn(generateWarningMessage(ComponentToTheme));
179 }
180 }
181 var theme = this.props.theme;
182
183 if (this.context[CHANNEL]) {
184 // if a theme is provided via props,
185 // it takes precedence over context
186 this.setTheme(theme ? theme : this.context[CHANNEL].getState());
187 } else {
188 this.setTheme(theme || {});
189 }
190 };
191
192 ThemedComponent.prototype.componentWillReceiveProps = function componentWillReceiveProps(nextProps) {
193 if (this.props.theme !== nextProps.theme) {
194 this.setTheme(nextProps.theme);
195 }
196 };
197
198 ThemedComponent.prototype.componentDidMount = function componentDidMount() {
199 if (this.context[CHANNEL] && !this.props.theme) {
200 // subscribe to future theme changes
201 this.subscriptionId = this.context[CHANNEL].subscribe(this.setTheme);
202 }
203 };
204
205 ThemedComponent.prototype.componentWillUnmount = function componentWillUnmount() {
206 // cleanup subscription
207 this.subscriptionId && this.context[CHANNEL].unsubscribe(this.subscriptionId);
208 };
209
210 ThemedComponent.prototype.render = function render() {
211 if (createElement) {
212 return React.createElement(ComponentToTheme, _extends({}, this.props, this.state));
213 } else {
214 // this allows us to effectively use the GlamorousComponent
215 // as our `render` method without going through lifecycle hooks.
216 // Also allows us to forward the context in the scenario where
217 // a user wants to add more context.
218 // eslint-disable-next-line babel/new-cap
219 return ComponentToTheme.call(this, _extends({}, this.props, this.state), this.context);
220 }
221 };
222
223 return ThemedComponent;
224 }(React.Component);
225
226 process.env.NODE_ENV !== "production" ? ThemedComponent.propTypes = {
227 theme: _PropTypes.object
228 } : void 0;
229
230
231 var defaultContextTypes = (_defaultContextTypes = {}, _defaultContextTypes[CHANNEL] = _PropTypes.object, _defaultContextTypes);
232
233 var userDefinedContextTypes = null;
234
235 // configure the contextTypes to be settable by the user,
236 // however also retaining the glamorous channel.
237 Object.defineProperty(ThemedComponent, 'contextTypes', {
238 enumerable: true,
239 configurable: true,
240 set: function set$$1(value) {
241 userDefinedContextTypes = value;
242 },
243 get: function get$$1() {
244 // if the user has provided a contextTypes definition,
245 // merge the default context types with the provided ones.
246 if (userDefinedContextTypes) {
247 return _extends({}, defaultContextTypes, userDefinedContextTypes);
248 }
249 return defaultContextTypes;
250 }
251 });
252
253 return ThemedComponent;
254}
255
256/**
257 * This function takes a className string and gets all the
258 * associated glamor styles. It's used to merge glamor styles
259 * from a className to make sure that specificity is not
260 * a problem when passing a className to a component.
261 * @param {String} [className=''] the className string
262 * @return {Object} { glamorStyles, glamorlessClassName }
263 * - glamorStyles is an array of all the glamor styles objects
264 * - glamorlessClassName is the rest of the className string
265 * without the glamor classNames
266 */
267function extractGlamorStyles(className) {
268 var glamorlessClassName = [];
269 var glamorStyles = [];
270 className.toString().split(' ').forEach(function (name) {
271 if (styleSheet.registered[name.substring(4)] === undefined) {
272 glamorlessClassName.push(name);
273 } else {
274 var style = buildGlamorSrcFromClassName(name);
275 glamorStyles.push(style);
276 }
277 });
278
279 return { glamorlessClassName: glamorlessClassName, glamorStyles: glamorStyles };
280}
281
282/** Glamor's css function returns an object with the shape
283 *
284 * {
285 * [`data-css-${hash}`]: '',
286 * toString() { return `css-${hash}` }
287 * }
288 *
289 * Whenever glamor's build function encounters an object with
290 * this shape it just pulls the resulting styles from the cache.
291 *
292 * note: the toString method is not needed to qualify the shape
293 **/
294function buildGlamorSrcFromClassName(className) {
295 var _ref;
296
297 return _ref = {}, _ref['data-' + className] = '', _ref;
298}
299
300function getGlamorClassName(_ref2) {
301 var styles = _ref2.styles,
302 props = _ref2.props,
303 cssOverrides = _ref2.cssOverrides,
304 cssProp = _ref2.cssProp,
305 context = _ref2.context,
306 displayName = _ref2.displayName;
307
308 var _handleStyles = handleStyles([].concat(styles, [props.className, cssOverrides, cssProp]), props, context),
309 mappedArgs = _handleStyles.mappedArgs,
310 nonGlamorClassNames = _handleStyles.nonGlamorClassNames;
311 // eslint-disable-next-line max-len
312
313
314 var isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
315 var devRules = isDev ? { label: displayName } : null;
316 var glamorClassName = css.apply(undefined, [devRules].concat(mappedArgs)).toString();
317 var extras = nonGlamorClassNames.join(' ').trim();
318 return (glamorClassName + ' ' + extras).trim();
319}
320
321// this next function is on a "hot" code-path
322// so it's pretty complex to make sure it's fast.
323// eslint-disable-next-line complexity
324function handleStyles(styles, props, context) {
325 var current = void 0;
326 var mappedArgs = [];
327 var nonGlamorClassNames = [];
328 for (var i = 0; i < styles.length; i++) {
329 current = styles[i];
330 while (typeof current === 'function') {
331 current = current(props, context);
332 }
333 if (typeof current === 'string') {
334 var _extractGlamorStyles = extractGlamorStyles(current),
335 glamorStyles = _extractGlamorStyles.glamorStyles,
336 glamorlessClassName = _extractGlamorStyles.glamorlessClassName;
337
338 mappedArgs.push.apply(mappedArgs, glamorStyles);
339 nonGlamorClassNames.push.apply(nonGlamorClassNames, glamorlessClassName);
340 } else if (Array.isArray(current)) {
341 var recursed = handleStyles(current, props, context);
342 mappedArgs.push.apply(mappedArgs, recursed.mappedArgs);
343 nonGlamorClassNames.push.apply(nonGlamorClassNames, recursed.nonGlamorClassNames);
344 } else {
345 mappedArgs.push(current);
346 }
347 }
348 return { mappedArgs: mappedArgs, nonGlamorClassNames: nonGlamorClassNames };
349}
350
351/*
352 * This is a relatively small abstraction that's ripe for open sourcing.
353 * Documentation is in the README.md
354 */
355
356function createGlamorous(splitProps) {
357 return glamorous;
358
359 /**
360 * This is the main export and the function that people
361 * interact with most directly.
362 *
363 * It accepts a component which can be a string or
364 * a React Component and returns
365 * a "glamorousComponentFactory"
366 * @param {String|ReactComponent} comp the component to render
367 * @param {Object} options helpful info for the GlamorousComponents
368 * @return {Function} the glamorousComponentFactory
369 */
370 function glamorous(comp) {
371 var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
372 var rootEl = config.rootEl,
373 displayName = config.displayName,
374 shouldClassNameUpdate = config.shouldClassNameUpdate,
375 _config$filterProps = config.filterProps,
376 filterProps = _config$filterProps === undefined ? [] : _config$filterProps,
377 _config$forwardProps = config.forwardProps,
378 forwardProps = _config$forwardProps === undefined ? [] : _config$forwardProps,
379 _config$propsAreCssOv = config.propsAreCssOverrides,
380 propsAreCssOverrides = _config$propsAreCssOv === undefined ? comp.propsAreCssOverrides : _config$propsAreCssOv,
381 basePropsToApply = config.withProps;
382
383 Object.assign(glamorousComponentFactory, { withConfig: withConfig });
384 return glamorousComponentFactory;
385
386 function withConfig(newConfig) {
387 return glamorous(comp, _extends({}, config, newConfig));
388 }
389
390 /**
391 * This returns a React Component that renders the comp (closure)
392 * with a className based on the given glamor styles object(s)
393 * @param {...Object|Function} styles the styles to create with glamor.
394 * If any of these are functions, they are invoked with the component
395 * props and the return value is used.
396 * @return {ReactComponent} the ReactComponent function
397 */
398 function glamorousComponentFactory() {
399 for (var _len = arguments.length, styles = Array(_len), _key = 0; _key < _len; _key++) {
400 styles[_key] = arguments[_key];
401 }
402
403 /**
404 * This is a component which will render the comp (closure)
405 * with the glamorous styles (closure). Forwards any valid
406 * props to the underlying component.
407 */
408 var GlamorousComponent = withTheme(function (props, context) {
409 props = getPropsToApply(GlamorousComponent.propsToApply, {}, props, context);
410 var updateClassName = shouldUpdate(props, context, this.previous);
411
412 if (shouldClassNameUpdate) {
413 this.previous = { props: props, context: context };
414 }
415
416 var _splitProps = splitProps(props, GlamorousComponent),
417 toForward = _splitProps.toForward,
418 cssOverrides = _splitProps.cssOverrides,
419 cssProp = _splitProps.cssProp;
420
421 // create className to apply
422
423
424 this.className = updateClassName ? getGlamorClassName({
425 styles: GlamorousComponent.styles,
426 props: props,
427 cssOverrides: cssOverrides,
428 cssProp: cssProp,
429 context: context,
430 displayName: GlamorousComponent.displayName
431 }) : this.className;
432
433 return React.createElement(GlamorousComponent.comp, _extends({
434 // if innerRef is forwarded we don't want to apply it here
435 ref: 'innerRef' in toForward ? undefined : props.innerRef
436 }, toForward, {
437 className: this.className
438 }));
439 }, { noWarn: true, createElement: false });
440
441 process.env.NODE_ENV !== "production" ? GlamorousComponent.propTypes = {
442 // className accepts an object due to glamor's css function
443 // returning an object with a toString method that gives the className
444 className: _PropTypes.oneOfType([_PropTypes.string, _PropTypes.object]),
445 cssOverrides: _PropTypes.object,
446 innerRef: _PropTypes.oneOfType([_PropTypes.func, _PropTypes.object]),
447 glam: _PropTypes.object
448 } : void 0;
449
450 function shouldUpdate(props, context, previous) {
451 // exiting early so components which do not use this
452 // optimization are not penalized by hanging onto
453 // references to previous props and context
454 if (!shouldClassNameUpdate) {
455 return true;
456 }
457 var update = true;
458 if (previous) {
459 if (!shouldClassNameUpdate(previous.props, props, previous.context, context)) {
460 update = false;
461 }
462 }
463
464 return update;
465 }
466
467 Object.assign(GlamorousComponent, getGlamorousComponentMetadata({
468 comp: comp,
469 styles: styles,
470 rootEl: rootEl,
471 filterProps: filterProps,
472 forwardProps: forwardProps,
473 displayName: displayName,
474 propsToApply: basePropsToApply
475 }), {
476 isGlamorousComponent: true,
477 propsAreCssOverrides: propsAreCssOverrides,
478 withComponent: function (newComp) {
479 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
480 var fwp = GlamorousComponent.forwardProps,
481 flp = GlamorousComponent.filterProps,
482 componentProperties = objectWithoutProperties(GlamorousComponent, ['forwardProps', 'filterProps']);
483
484 return glamorous(_extends({}, componentProperties, {
485 comp: newComp,
486 rootEl: getRootEl(newComp)
487 }), _extends({
488 // allows the forwardProps and filterProps to be overridden
489 forwardProps: fwp,
490 filterProps: flp
491 }, options))();
492 },
493 withProps: function () {
494 for (var _len2 = arguments.length, propsToApply = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
495 propsToApply[_key2] = arguments[_key2];
496 }
497
498 return glamorous(GlamorousComponent, { withProps: propsToApply })();
499 },
500 withConfig: withConfig
501 });
502 return GlamorousComponent;
503 }
504 }
505
506 function getGlamorousComponentMetadata(_ref) {
507 var comp = _ref.comp,
508 styles = _ref.styles,
509 rootEl = _ref.rootEl,
510 filterProps = _ref.filterProps,
511 forwardProps = _ref.forwardProps,
512 displayName = _ref.displayName,
513 basePropsToApply = _ref.propsToApply;
514
515 var componentsComp = comp.comp ? comp.comp : comp;
516 var propsToApply = comp.propsToApply ? [].concat(comp.propsToApply, arrayify(basePropsToApply)) : arrayify(basePropsToApply);
517 return {
518 // join styles together (for anyone doing: glamorous(glamorous.a({}), {}))
519 styles: when(comp.styles, styles),
520 // keep track of the ultimate rootEl to render (we never
521 // actually render anything but
522 // the base component, even when people wrap a glamorous
523 // component in glamorous
524 comp: componentsComp,
525 rootEl: rootEl || getRootEl(comp),
526 // join forwardProps and filterProps
527 // (for anyone doing: glamorous(glamorous.a({}), {}))
528 forwardProps: when(comp.forwardProps, forwardProps),
529 filterProps: when(comp.filterProps, filterProps),
530 // set the displayName to something that's slightly more
531 // helpful than `GlamorousComponent` :)
532 displayName: displayName || 'glamorous(' + getDisplayName(comp) + ')',
533 // these are props that should be applied to the component at render time
534 propsToApply: propsToApply
535 };
536 }
537}
538
539/**
540 * reduces the propsToApply given to a single props object
541 * @param {Array} propsToApply an array of propsToApply objects:
542 * - object
543 * - array of propsToApply items
544 * - function that accepts the accumulated props and the context
545 * @param {Object} accumulator an object to apply props onto
546 * @param {Object} props the props that should ultimately take precedence
547 * @param {*} context the context object
548 * @return {Object} the reduced props
549 */
550function getPropsToApply(propsToApply, accumulator, props, context) {
551 // using forEach rather than reduce here because the reduce solution
552 // effectively did the same thing because we manipulate the `accumulator`
553 propsToApply.forEach(function (propsToApplyItem) {
554 if (typeof propsToApplyItem === 'function') {
555 return Object.assign(accumulator, propsToApplyItem(Object.assign({}, accumulator, props), context));
556 } else if (Array.isArray(propsToApplyItem)) {
557 return Object.assign(accumulator, getPropsToApply(propsToApplyItem, accumulator, props, context));
558 }
559 return Object.assign(accumulator, propsToApplyItem);
560 });
561 // props wins
562 return Object.assign(accumulator, props);
563}
564
565function arrayify() {
566 var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
567
568 return Array.isArray(x) ? x : [x];
569}
570
571function when(comp, prop) {
572 return comp ? comp.concat(prop) : prop;
573}
574
575function getRootEl(comp) {
576 return comp.rootEl ? comp.rootEl : comp.comp || comp;
577}
578
579function getDisplayName(comp) {
580 return typeof comp === 'string' ? comp : comp.displayName || comp.name || 'unknown';
581}
582
583/* eslint no-unused-vars:0 */
584
585function splitProps(_ref, _ref2) {
586 var forwardProps = _ref2.forwardProps;
587 var cssProp = _ref.css,
588 innerRef = _ref.innerRef,
589 theme = _ref.theme,
590 className = _ref.className,
591 glam = _ref.glam,
592 rest = objectWithoutProperties(_ref, ['css', 'innerRef', 'theme', 'className', 'glam']);
593
594 // forward innerRef if user wishes to do so
595 if (innerRef !== undefined && forwardProps.indexOf('innerRef') !== -1) {
596 rest.innerRef = innerRef;
597 }
598 return { toForward: rest, cssProp: cssProp };
599}
600
601var glamorous = createGlamorous(splitProps);
602
603export default glamorous;