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