main
js 315 lines 7.99 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import Agent from 'react-devtools-shared/src/backend/agent';
11 import resolveBoxStyle from './resolveBoxStyle';
12 import isArray from 'react-devtools-shared/src/isArray';
13
14 import type {BackendBridge} from 'react-devtools-shared/src/bridge';
15 import type {RendererID} from '../types';
16 import type {StyleAndLayout} from './types';
17
18 export type ResolveNativeStyle = (stylesheetID: any) => ?Object;
19 export type SetupNativeStyleEditor = typeof setupNativeStyleEditor;
20
21 export default function setupNativeStyleEditor(
22 bridge: BackendBridge,
23 agent: Agent,
24 resolveNativeStyle: ResolveNativeStyle,
25 validAttributes?: $ReadOnlyArray<string> | null,
26 ) {
27 bridge.addListener(
28 'NativeStyleEditor_measure',
29 ({id, rendererID}: {id: number, rendererID: RendererID}) => {
30 measureStyle(agent, bridge, resolveNativeStyle, id, rendererID);
31 },
32 );
33
34 bridge.addListener(
35 'NativeStyleEditor_renameAttribute',
36 ({
37 id,
38 rendererID,
39 oldName,
40 newName,
41 value,
42 }: {
43 id: number,
44 rendererID: RendererID,
45 oldName: string,
46 newName: string,
47 value: string,
48 }) => {
49 renameStyle(agent, id, rendererID, oldName, newName, value);
50 setTimeout(() =>
51 measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
52 );
53 },
54 );
55
56 bridge.addListener(
57 'NativeStyleEditor_setValue',
58 ({
59 id,
60 rendererID,
61 name,
62 value,
63 }: {
64 id: number,
65 rendererID: number,
66 name: string,
67 value: string,
68 }) => {
69 setStyle(agent, id, rendererID, name, value);
70 setTimeout(() =>
71 measureStyle(agent, bridge, resolveNativeStyle, id, rendererID),
72 );
73 },
74 );
75
76 bridge.send('isNativeStyleEditorSupported', {
77 isSupported: true,
78 validAttributes,
79 });
80 }
81
82 const EMPTY_BOX_STYLE = {
83 top: 0,
84 left: 0,
85 right: 0,
86 bottom: 0,
87 };
88
89 const componentIDToStyleOverrides: Map<number, Object> = new Map();
90
91 function measureStyle(
92 agent: Agent,
93 bridge: BackendBridge,
94 resolveNativeStyle: ResolveNativeStyle,
95 id: number,
96 rendererID: RendererID,
97 ) {
98 const data = agent.getInstanceAndStyle({id, rendererID});
99 if (!data || !data.style) {
100 bridge.send('NativeStyleEditor_styleAndLayout', {
101 id,
102 layout: null,
103 style: null,
104 } as StyleAndLayout);
105 return;
106 }
107
108 const {instance, style} = data;
109
110 let resolvedStyle = resolveNativeStyle(style);
111
112 // If it's a host component we edited before, amend styles.
113 const styleOverrides = componentIDToStyleOverrides.get(id);
114 if (styleOverrides != null) {
115 resolvedStyle = Object.assign({}, resolvedStyle, styleOverrides);
116 }
117
118 if (!instance || typeof instance.measure !== 'function') {
119 bridge.send('NativeStyleEditor_styleAndLayout', {
120 id,
121 layout: null,
122 style: resolvedStyle || null,
123 } as StyleAndLayout);
124 return;
125 }
126
127 instance.measure((x, y, width, height, left, top) => {
128 // RN Android sometimes returns undefined here. Don't send measurements in this case.
129 // https://github.com/jhen0409/react-native-debugger/issues/84#issuecomment-304611817
130 if (typeof x !== 'number') {
131 bridge.send('NativeStyleEditor_styleAndLayout', {
132 id,
133 layout: null,
134 style: resolvedStyle || null,
135 } as StyleAndLayout);
136 return;
137 }
138 const margin =
139 (resolvedStyle != null && resolveBoxStyle('margin', resolvedStyle)) ||
140 EMPTY_BOX_STYLE;
141 const padding =
142 (resolvedStyle != null && resolveBoxStyle('padding', resolvedStyle)) ||
143 EMPTY_BOX_STYLE;
144 bridge.send('NativeStyleEditor_styleAndLayout', {
145 id,
146 layout: {
147 x,
148 y,
149 width,
150 height,
151 left,
152 top,
153 margin,
154 padding,
155 },
156 style: resolvedStyle || null,
157 } as StyleAndLayout);
158 });
159 }
160
161 function shallowClone(object: Object): Object {
162 const cloned: {[string]: $FlowFixMe} = {};
163 for (const n in object) {
164 cloned[n] = object[n];
165 }
166 return cloned;
167 }
168
169 function renameStyle(
170 agent: Agent,
171 id: number,
172 rendererID: RendererID,
173 oldName: string,
174 newName: string,
175 value: string,
176 ): void {
177 const data = agent.getInstanceAndStyle({id, rendererID});
178 if (!data || !data.style) {
179 return;
180 }
181
182 const {instance, style} = data;
183
184 const newStyle = newName
185 ? {[oldName]: undefined as string | void, [newName]: value}
186 : {[oldName]: undefined};
187
188 let customStyle;
189
190 // TODO It would be nice if the renderer interface abstracted this away somehow.
191 if (instance !== null && typeof instance.setNativeProps === 'function') {
192 // In the case of a host component, we need to use setNativeProps().
193 // Remember to "correct" resolved styles when we read them next time.
194 const styleOverrides = componentIDToStyleOverrides.get(id);
195 if (!styleOverrides) {
196 componentIDToStyleOverrides.set(id, newStyle);
197 } else {
198 Object.assign(styleOverrides, newStyle);
199 }
200 // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
201 instance.setNativeProps({style: newStyle});
202 } else if (isArray(style)) {
203 const lastIndex = style.length - 1;
204 if (typeof style[lastIndex] === 'object' && !isArray(style[lastIndex])) {
205 customStyle = shallowClone(style[lastIndex]);
206 delete customStyle[oldName];
207 if (newName) {
208 customStyle[newName] = value;
209 } else {
210 customStyle[oldName] = undefined;
211 }
212
213 agent.overrideValueAtPath({
214 type: 'props',
215 id,
216 rendererID,
217 path: ['style', lastIndex],
218 value: customStyle,
219 });
220 } else {
221 agent.overrideValueAtPath({
222 type: 'props',
223 id,
224 rendererID,
225 path: ['style'],
226 value: style.concat([newStyle]),
227 });
228 }
229 } else if (typeof style === 'object') {
230 customStyle = shallowClone(style);
231 delete customStyle[oldName];
232 if (newName) {
233 customStyle[newName] = value;
234 } else {
235 customStyle[oldName] = undefined;
236 }
237
238 agent.overrideValueAtPath({
239 type: 'props',
240 id,
241 rendererID,
242 path: ['style'],
243 value: customStyle,
244 });
245 } else {
246 agent.overrideValueAtPath({
247 type: 'props',
248 id,
249 rendererID,
250 path: ['style'],
251 value: [style, newStyle],
252 });
253 }
254
255 agent.emit('hideNativeHighlight');
256 }
257
258 function setStyle(
259 agent: Agent,
260 id: number,
261 rendererID: RendererID,
262 name: string,
263 value: string,
264 ) {
265 const data = agent.getInstanceAndStyle({id, rendererID});
266 if (!data || !data.style) {
267 return;
268 }
269
270 const {instance, style} = data;
271 const newStyle = {[name]: value};
272
273 // TODO It would be nice if the renderer interface abstracted this away somehow.
274 if (instance !== null && typeof instance.setNativeProps === 'function') {
275 // In the case of a host component, we need to use setNativeProps().
276 // Remember to "correct" resolved styles when we read them next time.
277 const styleOverrides = componentIDToStyleOverrides.get(id);
278 if (!styleOverrides) {
279 componentIDToStyleOverrides.set(id, newStyle);
280 } else {
281 Object.assign(styleOverrides, newStyle);
282 }
283 // TODO Fabric does not support setNativeProps; chat with Sebastian or Eli
284 instance.setNativeProps({style: newStyle});
285 } else if (isArray(style)) {
286 const lastLength = style.length - 1;
287 if (typeof style[lastLength] === 'object' && !isArray(style[lastLength])) {
288 agent.overrideValueAtPath({
289 type: 'props',
290 id,
291 rendererID,
292 path: ['style', lastLength, name],
293 value,
294 });
295 } else {
296 agent.overrideValueAtPath({
297 type: 'props',
298 id,
299 rendererID,
300 path: ['style'],
301 value: style.concat([newStyle]),
302 });
303 }
304 } else {
305 agent.overrideValueAtPath({
306 type: 'props',
307 id,
308 rendererID,
309 path: ['style'],
310 value: [style, newStyle],
311 });
312 }
313
314 agent.emit('hideNativeHighlight');
315 }