1 |
|
2 |
|
3 | import type {Config} from './config';
|
4 |
|
5 | import appendImportantToEachValue from './append-important-to-each-value';
|
6 | import cssRuleSetToString from './css-rule-set-to-string';
|
7 | import getState from './get-state';
|
8 | import getStateKey from './get-state-key';
|
9 | import cleanStateKey from './clean-state-key';
|
10 | import getRadiumStyleState from './get-radium-style-state';
|
11 | import hash from './hash';
|
12 | import {isNestedStyle, mergeStyles} from './merge-styles';
|
13 | import Plugins from './plugins/';
|
14 |
|
15 | import ExecutionEnvironment from 'exenv';
|
16 | import React from 'react';
|
17 |
|
18 | const DEFAULT_CONFIG = {
|
19 | plugins: [
|
20 | Plugins.mergeStyleArray,
|
21 | Plugins.checkProps,
|
22 | Plugins.resolveMediaQueries,
|
23 | Plugins.resolveInteractionStyles,
|
24 | Plugins.keyframes,
|
25 | Plugins.visited,
|
26 | Plugins.removeNestedStyles,
|
27 | Plugins.prefix,
|
28 | Plugins.checkProps
|
29 | ]
|
30 | };
|
31 |
|
32 |
|
33 | let globalState = {};
|
34 |
|
35 | type ResolvedStyles = {
|
36 | extraStateKeyMap: {[key: string]: boolean},
|
37 | element: any
|
38 | };
|
39 |
|
40 |
|
41 | let resolveStyles = ((null: any): (
|
42 | component: any,
|
43 | renderedElement: any,
|
44 | config: Config,
|
45 | existingKeyMap: {[key: string]: boolean},
|
46 | shouldCheckBeforeResolve: boolean,
|
47 | extraStateKeyMap?: {[key: string]: boolean}
|
48 | ) => ResolvedStyles);
|
49 |
|
50 | const _shouldResolveStyles = function(component) {
|
51 | return component.type && !component.type._isRadiumEnhanced;
|
52 | };
|
53 |
|
54 | const _resolveChildren = function(
|
55 | {
|
56 | children,
|
57 | component,
|
58 | config,
|
59 | existingKeyMap,
|
60 | extraStateKeyMap
|
61 | }
|
62 | ) {
|
63 | if (!children) {
|
64 | return children;
|
65 | }
|
66 |
|
67 | const childrenType = typeof children;
|
68 |
|
69 | if (childrenType === 'string' || childrenType === 'number') {
|
70 |
|
71 | return children;
|
72 | }
|
73 |
|
74 | if (childrenType === 'function') {
|
75 |
|
76 | return function() {
|
77 | const result = children.apply(this, arguments);
|
78 |
|
79 | if (React.isValidElement(result)) {
|
80 | const key = getStateKey(result);
|
81 | delete extraStateKeyMap[key];
|
82 | const {element} = resolveStyles(
|
83 | component,
|
84 | result,
|
85 | config,
|
86 | existingKeyMap,
|
87 | true,
|
88 | extraStateKeyMap
|
89 | );
|
90 | return element;
|
91 | }
|
92 |
|
93 | return result;
|
94 | };
|
95 | }
|
96 |
|
97 | if (React.Children.count(children) === 1 && children.type) {
|
98 |
|
99 |
|
100 | const onlyChild = React.Children.only(children);
|
101 | const key = getStateKey(onlyChild);
|
102 | delete extraStateKeyMap[key];
|
103 | const {element} = resolveStyles(
|
104 | component,
|
105 | onlyChild,
|
106 | config,
|
107 | existingKeyMap,
|
108 | true,
|
109 | extraStateKeyMap
|
110 | );
|
111 | return element;
|
112 | }
|
113 |
|
114 | return React.Children.map(children, function(child) {
|
115 | if (React.isValidElement(child)) {
|
116 | const key = getStateKey(child);
|
117 | delete extraStateKeyMap[key];
|
118 | const {element} = resolveStyles(
|
119 | component,
|
120 | child,
|
121 | config,
|
122 | existingKeyMap,
|
123 | true,
|
124 | extraStateKeyMap
|
125 | );
|
126 | return element;
|
127 | }
|
128 |
|
129 | return child;
|
130 | });
|
131 | };
|
132 |
|
133 |
|
134 | const _resolveProps = function(
|
135 | {
|
136 | component,
|
137 | config,
|
138 | existingKeyMap,
|
139 | props,
|
140 | extraStateKeyMap
|
141 | }
|
142 | ) {
|
143 | let newProps = props;
|
144 |
|
145 | Object.keys(props).forEach(prop => {
|
146 |
|
147 | if (prop === 'children') {
|
148 | return;
|
149 | }
|
150 |
|
151 | const propValue = props[prop];
|
152 | if (React.isValidElement(propValue)) {
|
153 | const key = getStateKey(propValue);
|
154 | delete extraStateKeyMap[key];
|
155 | newProps = {...newProps};
|
156 | const {element} = resolveStyles(
|
157 | component,
|
158 | propValue,
|
159 | config,
|
160 | existingKeyMap,
|
161 | true,
|
162 | extraStateKeyMap
|
163 | );
|
164 | newProps[prop] = element;
|
165 | }
|
166 | });
|
167 |
|
168 | return newProps;
|
169 | };
|
170 |
|
171 | const _buildGetKey = function(
|
172 | {
|
173 | componentName,
|
174 | existingKeyMap,
|
175 | renderedElement
|
176 | }
|
177 | ) {
|
178 |
|
179 |
|
180 |
|
181 | const originalKey = getStateKey(renderedElement);
|
182 | const key = cleanStateKey(originalKey);
|
183 |
|
184 | let alreadyGotKey = false;
|
185 | const getKey = function() {
|
186 | if (alreadyGotKey) {
|
187 | return key;
|
188 | }
|
189 |
|
190 | alreadyGotKey = true;
|
191 |
|
192 | if (existingKeyMap[key]) {
|
193 | let elementName;
|
194 | if (typeof renderedElement.type === 'string') {
|
195 | elementName = renderedElement.type;
|
196 | } else if (renderedElement.type.constructor) {
|
197 | elementName = renderedElement.type.constructor.displayName ||
|
198 | renderedElement.type.constructor.name;
|
199 | }
|
200 |
|
201 | throw new Error(
|
202 | 'Radium requires each element with interactive styles to have a unique ' +
|
203 | 'key, set using either the ref or key prop. ' +
|
204 | (originalKey
|
205 | ? 'Key "' + originalKey + '" is a duplicate.'
|
206 | : 'Multiple elements have no key specified.') +
|
207 | ' ' +
|
208 | 'Component: "' +
|
209 | componentName +
|
210 | '". ' +
|
211 | (elementName ? 'Element: "' + elementName + '".' : '')
|
212 | );
|
213 | }
|
214 |
|
215 | existingKeyMap[key] = true;
|
216 |
|
217 | return key;
|
218 | };
|
219 |
|
220 | return getKey;
|
221 | };
|
222 |
|
223 | const _setStyleState = function(component, key, stateKey, value) {
|
224 | if (!component._radiumIsMounted) {
|
225 | return;
|
226 | }
|
227 |
|
228 | const existing = getRadiumStyleState(component);
|
229 | const state = {_radiumStyleState: {...existing}};
|
230 |
|
231 | state._radiumStyleState[key] = {...state._radiumStyleState[key]};
|
232 | state._radiumStyleState[key][stateKey] = value;
|
233 |
|
234 | component._lastRadiumState = state._radiumStyleState;
|
235 | component.setState(state);
|
236 | };
|
237 |
|
238 | const _runPlugins = function(
|
239 | {
|
240 | component,
|
241 | config,
|
242 | existingKeyMap,
|
243 | props,
|
244 | renderedElement
|
245 | }
|
246 | ) {
|
247 |
|
248 |
|
249 | if (
|
250 | !React.isValidElement(renderedElement) ||
|
251 | typeof renderedElement.type !== 'string' ||
|
252 | !props.style
|
253 | ) {
|
254 | return props;
|
255 | }
|
256 |
|
257 | let newProps = props;
|
258 |
|
259 | const plugins = config.plugins || DEFAULT_CONFIG.plugins;
|
260 |
|
261 | const componentName = component.constructor.displayName ||
|
262 | component.constructor.name;
|
263 | const getKey = _buildGetKey({
|
264 | renderedElement,
|
265 | existingKeyMap,
|
266 | componentName
|
267 | });
|
268 | const getComponentField = key => component[key];
|
269 | const getGlobalState = key => globalState[key];
|
270 | const componentGetState = (stateKey, elementKey) =>
|
271 | getState(component.state, elementKey || getKey(), stateKey);
|
272 | const setState = (stateKey, value, elementKey) =>
|
273 | _setStyleState(component, elementKey || getKey(), stateKey, value);
|
274 |
|
275 | const addCSS = css => {
|
276 | const styleKeeper = component._radiumStyleKeeper ||
|
277 | component.context._radiumStyleKeeper;
|
278 | if (!styleKeeper) {
|
279 | if (__isTestModeEnabled) {
|
280 | return {remove() {}};
|
281 | }
|
282 |
|
283 | throw new Error(
|
284 | 'To use plugins requiring `addCSS` (e.g. keyframes, media queries), ' +
|
285 | 'please wrap your application in the StyleRoot component. Component ' +
|
286 | 'name: `' +
|
287 | componentName +
|
288 | '`.'
|
289 | );
|
290 | }
|
291 |
|
292 | return styleKeeper.addCSS(css);
|
293 | };
|
294 |
|
295 | let newStyle = props.style;
|
296 |
|
297 | plugins.forEach(plugin => {
|
298 | const result = plugin({
|
299 | ExecutionEnvironment,
|
300 | addCSS,
|
301 | appendImportantToEachValue,
|
302 | componentName,
|
303 | config,
|
304 | cssRuleSetToString,
|
305 | getComponentField,
|
306 | getGlobalState,
|
307 | getState: componentGetState,
|
308 | hash,
|
309 | mergeStyles,
|
310 | props: newProps,
|
311 | setState,
|
312 | isNestedStyle,
|
313 | style: newStyle
|
314 | }) || {};
|
315 |
|
316 | newStyle = result.style || newStyle;
|
317 |
|
318 | newProps = result.props && Object.keys(result.props).length
|
319 | ? {...newProps, ...result.props}
|
320 | : newProps;
|
321 |
|
322 | const newComponentFields = result.componentFields || {};
|
323 | Object.keys(newComponentFields).forEach(fieldName => {
|
324 | component[fieldName] = newComponentFields[fieldName];
|
325 | });
|
326 |
|
327 | const newGlobalState = result.globalState || {};
|
328 | Object.keys(newGlobalState).forEach(key => {
|
329 | globalState[key] = newGlobalState[key];
|
330 | });
|
331 | });
|
332 |
|
333 | if (newStyle !== props.style) {
|
334 | newProps = {...newProps, style: newStyle};
|
335 | }
|
336 |
|
337 | return newProps;
|
338 | };
|
339 |
|
340 |
|
341 |
|
342 |
|
343 | const _cloneElement = function(renderedElement, newProps, newChildren) {
|
344 |
|
345 | if (typeof renderedElement.type === 'string') {
|
346 | newProps = {...newProps, 'data-radium': true};
|
347 | }
|
348 |
|
349 | return React.cloneElement(renderedElement, newProps, newChildren);
|
350 | };
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | resolveStyles = function(
|
361 | component: any, // ReactComponent, flow+eslint complaining
|
362 | renderedElement: any, // ReactElement
|
363 | config: Config = DEFAULT_CONFIG,
|
364 | existingKeyMap: {[key: string]: boolean} = {},
|
365 | shouldCheckBeforeResolve: boolean = false,
|
366 | extraStateKeyMap?: {[key: string]: boolean}
|
367 | ): ResolvedStyles {
|
368 |
|
369 |
|
370 |
|
371 | if (!extraStateKeyMap) {
|
372 | const state = getRadiumStyleState(component);
|
373 | extraStateKeyMap = Object.keys(state).reduce(
|
374 | (acc, key) => {
|
375 |
|
376 |
|
377 |
|
378 |
|
379 | if (key !== 'main') {
|
380 | acc[key] = true;
|
381 | }
|
382 | return acc;
|
383 | },
|
384 | {}
|
385 | );
|
386 | }
|
387 |
|
388 | if (Array.isArray(renderedElement) && !renderedElement.props) {
|
389 | const elements = renderedElement.map(element => {
|
390 |
|
391 | if (extraStateKeyMap) {
|
392 | const key = getStateKey(element);
|
393 | delete extraStateKeyMap[key];
|
394 | }
|
395 |
|
396 |
|
397 |
|
398 | return resolveStyles(
|
399 | component,
|
400 | element,
|
401 | config,
|
402 | existingKeyMap,
|
403 | shouldCheckBeforeResolve,
|
404 | extraStateKeyMap
|
405 | ).element;
|
406 | });
|
407 | return {
|
408 | extraStateKeyMap,
|
409 | element: elements
|
410 | };
|
411 | }
|
412 |
|
413 |
|
414 | if (
|
415 | !renderedElement ||
|
416 |
|
417 |
|
418 |
|
419 |
|
420 | (renderedElement.props && renderedElement.props['data-radium']) ||
|
421 |
|
422 |
|
423 | (shouldCheckBeforeResolve && !_shouldResolveStyles(renderedElement))
|
424 | ) {
|
425 | return {extraStateKeyMap, element: renderedElement};
|
426 | }
|
427 |
|
428 | const children = renderedElement.props.children;
|
429 |
|
430 | const newChildren = _resolveChildren({
|
431 | children,
|
432 | component,
|
433 | config,
|
434 | existingKeyMap,
|
435 | extraStateKeyMap
|
436 | });
|
437 |
|
438 | let newProps = _resolveProps({
|
439 | component,
|
440 | config,
|
441 | existingKeyMap,
|
442 | extraStateKeyMap,
|
443 | props: renderedElement.props
|
444 | });
|
445 |
|
446 | newProps = _runPlugins({
|
447 | component,
|
448 | config,
|
449 | existingKeyMap,
|
450 | props: newProps,
|
451 | renderedElement
|
452 | });
|
453 |
|
454 |
|
455 |
|
456 |
|
457 | if (newChildren === children && newProps === renderedElement.props) {
|
458 | return {extraStateKeyMap, element: renderedElement};
|
459 | }
|
460 |
|
461 | const element = _cloneElement(
|
462 | renderedElement,
|
463 | newProps !== renderedElement.props ? newProps : {},
|
464 | newChildren
|
465 | );
|
466 |
|
467 | return {extraStateKeyMap, element};
|
468 | };
|
469 |
|
470 |
|
471 |
|
472 | let __isTestModeEnabled = false;
|
473 | if (process.env.NODE_ENV !== 'production') {
|
474 | resolveStyles.__clearStateForTests = function() {
|
475 | globalState = {};
|
476 | };
|
477 | resolveStyles.__setTestMode = function(isEnabled) {
|
478 | __isTestModeEnabled = isEnabled;
|
479 | };
|
480 | }
|
481 |
|
482 | export default resolveStyles;
|