UNPKG

14.2 kBJavaScriptView Raw
1import PropTypes from 'prop-types';
2import React, { PureComponent } from 'react';
3import { ScrollView, View, Text, TouchableOpacity, PanResponder, Animated, Dimensions, StyleSheet, TextInput, Keyboard, NativeModules, Platform, KeyboardAvoidingView } from 'react-native';
4import event from './src/event';
5import Network, { traceNetwork } from './src/network';
6import Log, { traceLog } from './src/log';
7import Info from './src/info';
8import HocComp from './src/hoc';
9import Storage from './src/storage';
10import { replaceReg } from './src/tool';
11
12const { width, height } = Dimensions.get('window');
13
14let commandContext = global;
15
16export const setExternalContext = externalContext => {
17 if (externalContext) commandContext = externalContext;
18};
19
20// Log/network trace when Element is not initialized.
21export const initTrace = () => {
22 traceLog();
23 traceNetwork();
24};
25
26class VDebug extends PureComponent {
27 static propTypes = {
28 // Info panel (Optional)
29 info: PropTypes.object,
30 // Expansion panel (Optional)
31 panels: PropTypes.array
32 };
33
34 static defaultProps = {
35 info: {},
36 panels: null
37 };
38
39 constructor(props) {
40 super(props);
41 initTrace();
42 this.containerHeight = (height / 3) * 2;
43 this.refsObj = {};
44 this.state = {
45 commandValue: '',
46 showPanel: false,
47 currentPageIndex: 0,
48 pan: new Animated.ValueXY(),
49 scale: new Animated.Value(1),
50 panelHeight: new Animated.Value(0),
51 panels: this.addPanels(),
52 history: [],
53 historyFilter: [],
54 showHistory: false
55 };
56 this.panResponder = PanResponder.create({
57 onStartShouldSetPanResponder: () => true,
58 onPanResponderGrant: () => {
59 this.state.pan.setOffset({
60 x: this.state.pan.x._value,
61 y: this.state.pan.y._value
62 });
63 this.state.pan.setValue({ x: 0, y: 0 });
64 Animated.spring(this.state.scale, {
65 useNativeDriver: true,
66 toValue: 1.3,
67 friction: 7
68 }).start();
69 },
70 onPanResponderMove: Animated.event([null, { dx: this.state.pan.x, dy: this.state.pan.y }]),
71 onPanResponderRelease: ({ nativeEvent }, gestureState) => {
72 if (Math.abs(gestureState.dx) < 5 && Math.abs(gestureState.dy) < 5) this.togglePanel();
73 setTimeout(() => {
74 Animated.spring(this.state.scale, {
75 useNativeDriver: true,
76 toValue: 1,
77 friction: 7
78 }).start(() => {
79 this.setState({
80 top: nativeEvent.pageY
81 });
82 });
83 this.state.pan.flattenOffset();
84 }, 0);
85 }
86 });
87 }
88
89 componentDidMount() {
90 this.state.pan.setValue({ x: 0, y: 0 });
91 Storage.support() &&
92 Storage.get('react-native-vdebug@history').then(res => {
93 if (res) {
94 this.setState({
95 history: res
96 });
97 }
98 });
99 }
100
101 getRef(index) {
102 return ref => {
103 if (!this.refsObj[index]) this.refsObj[index] = ref;
104 };
105 }
106
107 addPanels() {
108 let defaultPanels = [
109 {
110 title: 'Log',
111 component: HocComp(Log, this.getRef(0))
112 },
113 {
114 title: 'Network',
115 component: HocComp(Network, this.getRef(1))
116 },
117 {
118 title: 'Info',
119 component: HocComp(Info, this.getRef(2)),
120 props: { info: this.props.info }
121 }
122 ];
123 if (this.props.panels && this.props.panels.length) {
124 this.props.panels.forEach((item, index) => {
125 // support up to five extended panels
126 if (index >= 3) return;
127 if (item.title && item.component) {
128 item.component = HocComp(item.component, this.getRef(defaultPanels.length));
129 defaultPanels.push(item);
130 }
131 });
132 }
133 return defaultPanels;
134 }
135
136 togglePanel() {
137 this.state.panelHeight.setValue(this.state.panelHeight._value ? 0 : this.containerHeight);
138 }
139
140 clearLogs() {
141 const tabName = this.state.panels[this.state.currentPageIndex].title;
142 event.trigger('clear', tabName);
143 }
144
145 showDev() {
146 NativeModules?.DevMenu?.show();
147 }
148
149 reloadDev() {
150 NativeModules?.DevMenu?.reload();
151 }
152
153 evalInContext(js, context) {
154 return function (str) {
155 let result = '';
156 try {
157 // eslint-disable-next-line no-eval
158 result = eval(str);
159 } catch (err) {
160 result = 'Invalid input';
161 }
162 return event.trigger('addLog', result);
163 }.call(context, `with(this) { ${js} } `);
164 }
165
166 execCommand() {
167 if (!this.state.commandValue) return;
168 this.evalInContext(this.state.commandValue, commandContext);
169 this.syncHistory();
170 Keyboard.dismiss();
171 }
172
173 clearCommand() {
174 this.textInput.clear();
175 this.setState({
176 historyFilter: []
177 });
178 }
179
180 scrollToPage(index, animated = true) {
181 this.scrollToCard(index, animated);
182 }
183
184 scrollToCard(cardIndex, animated = true) {
185 if (cardIndex < 0) cardIndex = 0;
186 else if (cardIndex >= this.cardCount) cardIndex = this.cardCount - 1;
187 if (this.scrollView) {
188 this.scrollView.scrollTo({ x: width * cardIndex, y: 0, animated: animated });
189 }
190 }
191
192 scrollToTop() {
193 const item = this.refsObj[this.state.currentPageIndex];
194 const instance = item?.getScrollRef && item?.getScrollRef();
195 if (instance) {
196 // FlatList
197 instance.scrollToOffset && instance.scrollToOffset({ animated: true, viewPosition: 0, index: 0 });
198 // ScrollView
199 instance.scrollTo && instance.scrollTo({ x: 0, y: 0, animated: true });
200 }
201 }
202
203 renderPanelHeader() {
204 return (
205 <View style={styles.panelHeader}>
206 {this.state.panels.map((item, index) => (
207 <TouchableOpacity
208 key={index.toString()}
209 onPress={() => {
210 if (index != this.state.currentPageIndex) {
211 this.scrollToPage(index);
212 this.setState({ currentPageIndex: index });
213 } else {
214 this.scrollToTop();
215 }
216 }}
217 style={[styles.panelHeaderItem, index === this.state.currentPageIndex && styles.activeTab]}
218 >
219 <Text style={styles.panelHeaderItemText}>{item.title}</Text>
220 </TouchableOpacity>
221 ))}
222 </View>
223 );
224 }
225
226 syncHistory() {
227 if (!Storage.support()) return;
228 const res = this.state.history.filter(f => {
229 return f == this.state.commandValue;
230 });
231 if (res && res.length) return;
232 this.state.history.splice(0, 0, this.state.commandValue);
233 this.state.historyFilter.splice(0, 0, this.state.commandValue);
234 this.setState(
235 {
236 history: this.state.history,
237 historyFilter: this.state.historyFilter
238 },
239 () => {
240 Storage.save('react-native-vdebug@history', this.state.history);
241 this.forceUpdate();
242 }
243 );
244 }
245
246 onChange(text) {
247 const state = { commandValue: text };
248 if (text) {
249 const res = this.state.history.filter(f => f.toLowerCase().match(replaceReg(text)));
250 if (res && res.length) state.historyFilter = res;
251 } else {
252 state.historyFilter = [];
253 }
254 this.setState(state);
255 }
256
257 renderCommandBar() {
258 return (
259 <KeyboardAvoidingView
260 keyboardVerticalOffset={Platform.OS == 'android' ? 0 : 300}
261 contentContainerStyle={{ flex: 1 }}
262 behavior={'position'}
263 style={{
264 height: this.state.historyFilter.length ? 120 : 40,
265 borderWidth: StyleSheet.hairlineWidth,
266 borderColor: '#d9d9d9'
267 }}
268 >
269 <View style={[styles.historyContainer, { height: this.state.historyFilter.length ? 80 : 0 }]}>
270 <ScrollView>
271 {this.state.historyFilter.map(text => {
272 return (
273 <TouchableOpacity
274 style={{ borderBottomWidth: 1, borderBottomColor: '#eeeeeea1' }}
275 onPress={() => {
276 if (text && text.toString) {
277 this.setState({
278 commandValue: text.toString()
279 });
280 }
281 }}
282 >
283 <Text style={{ lineHeight: 25 }}>{text}</Text>
284 </TouchableOpacity>
285 );
286 })}
287 </ScrollView>
288 </View>
289 <View style={styles.commandBar}>
290 <TextInput
291 ref={ref => {
292 this.textInput = ref;
293 }}
294 style={styles.commandBarInput}
295 placeholderTextColor={'#000000a1'}
296 placeholder="Command..."
297 onChangeText={this.onChange.bind(this)}
298 value={this.state.commandValue}
299 onFocus={() => {
300 this.setState({ showHistory: true });
301 }}
302 onSubmitEditing={this.execCommand.bind(this)}
303 />
304 <TouchableOpacity style={styles.commandBarBtn} onPress={this.clearCommand.bind(this)}>
305 <Text>X</Text>
306 </TouchableOpacity>
307 <TouchableOpacity style={styles.commandBarBtn} onPress={this.execCommand.bind(this)}>
308 <Text>OK</Text>
309 </TouchableOpacity>
310 </View>
311 </KeyboardAvoidingView>
312 );
313 }
314
315 renderPanelFooter() {
316 return (
317 <View style={styles.panelBottom}>
318 <TouchableOpacity onPress={this.clearLogs.bind(this)} style={styles.panelBottomBtn}>
319 <Text style={styles.panelBottomBtnText}>Clear</Text>
320 </TouchableOpacity>
321 {__DEV__ && Platform.OS == 'ios' && (
322 <TouchableOpacity onPress={this.showDev.bind(this)} onLongPress={this.reloadDev.bind(this)} style={styles.panelBottomBtn}>
323 <Text style={styles.panelBottomBtnText}>Dev</Text>
324 </TouchableOpacity>
325 )}
326 <TouchableOpacity onPress={this.togglePanel.bind(this)} style={styles.panelBottomBtn}>
327 <Text style={styles.panelBottomBtnText}>Hide</Text>
328 </TouchableOpacity>
329 </View>
330 );
331 }
332
333 onScrollAnimationEnd({ nativeEvent }) {
334 const currentPageIndex = Math.floor(nativeEvent.contentOffset.x / Math.floor(width));
335 currentPageIndex != this.state.currentPageIndex &&
336 this.setState({
337 currentPageIndex: currentPageIndex
338 });
339 }
340
341 renderPanel() {
342 return (
343 <Animated.View style={[styles.panel, { height: this.state.panelHeight }]}>
344 {this.renderPanelHeader()}
345 <ScrollView
346 onMomentumScrollEnd={this.onScrollAnimationEnd.bind(this)}
347 ref={ref => {
348 this.scrollView = ref;
349 }}
350 pagingEnabled={true}
351 showsHorizontalScrollIndicator={false}
352 horizontal={true}
353 style={styles.panelContent}
354 >
355 {this.state.panels.map((item, index) => {
356 return (
357 <View key={index} style={{ width: width }}>
358 <item.component {...(item.props ?? {})} />
359 </View>
360 );
361 })}
362 </ScrollView>
363 {this.renderCommandBar()}
364 {this.renderPanelFooter()}
365 </Animated.View>
366 );
367 }
368
369 renderDebugBtn() {
370 const { pan, scale } = this.state;
371 const [translateX, translateY] = [pan.x, pan.y];
372 const btnStyle = { transform: [{ translateX }, { translateY }, { scale }] };
373
374 return (
375 <Animated.View {...this.panResponder.panHandlers} style={[styles.homeBtn, btnStyle]}>
376 <Text style={styles.homeBtnText}>Debug</Text>
377 </Animated.View>
378 );
379 }
380
381 render() {
382 return (
383 <View style={{ flex: 1 }}>
384 {this.renderPanel()}
385 {this.renderDebugBtn()}
386 </View>
387 );
388 }
389}
390
391const styles = StyleSheet.create({
392 activeTab: {
393 backgroundColor: '#fff'
394 },
395 panel: {
396 position: 'absolute',
397 zIndex: 99998,
398 elevation: 99998,
399 backgroundColor: '#fff',
400 width,
401 bottom: 0,
402 right: 0
403 },
404 panelHeader: {
405 width,
406 backgroundColor: '#eee',
407 flexDirection: 'row',
408 borderWidth: StyleSheet.hairlineWidth,
409 borderColor: '#d9d9d9'
410 },
411 panelHeaderItem: {
412 flex: 1,
413 height: 40,
414 color: '#000',
415 borderRightWidth: StyleSheet.hairlineWidth,
416 borderColor: '#d9d9d9',
417 justifyContent: 'center'
418 },
419 panelHeaderItemText: {
420 textAlign: 'center'
421 },
422 panelContent: {
423 width,
424 flex: 0.9
425 },
426 panelBottom: {
427 width,
428 borderWidth: StyleSheet.hairlineWidth,
429 borderColor: '#d9d9d9',
430 flexDirection: 'row',
431 alignItems: 'center',
432 backgroundColor: '#eee',
433 height: 40
434 },
435 panelBottomBtn: {
436 flex: 1,
437 height: 40,
438 borderRightWidth: StyleSheet.hairlineWidth,
439 borderColor: '#d9d9d9',
440 justifyContent: 'center'
441 },
442 panelBottomBtnText: {
443 color: '#000',
444 fontSize: 14,
445 textAlign: 'center'
446 },
447 panelEmpty: {
448 flex: 1,
449 alignItems: 'center',
450 justifyContent: 'center'
451 },
452 homeBtn: {
453 width: 60,
454 paddingVertical: 5,
455 backgroundColor: '#04be02',
456 borderRadius: 4,
457 alignItems: 'center',
458 justifyContent: 'center',
459 position: 'absolute',
460 zIndex: 99999,
461 bottom: height / 2,
462 right: 0,
463 shadowColor: 'rgb(18,34,74)',
464 shadowOffset: { width: 0, height: 1 },
465 shadowOpacity: 0.08,
466 elevation: 99999
467 },
468 homeBtnText: {
469 color: '#fff'
470 },
471 commandBar: {
472 flexDirection: 'row',
473 borderWidth: StyleSheet.hairlineWidth,
474 borderColor: '#d9d9d9',
475 flexDirection: 'row',
476 height: 40
477 },
478 commandBarInput: {
479 flex: 1,
480 paddingLeft: 10,
481 backgroundColor: '#ffffff',
482 color: '#000000'
483 },
484 commandBarBtn: {
485 width: 40,
486 alignItems: 'center',
487 justifyContent: 'center',
488 backgroundColor: '#eee'
489 },
490 historyContainer: {
491 borderTopWidth: 1,
492 borderTopColor: '#d9d9d9',
493 backgroundColor: '#ffffff',
494 paddingHorizontal: 10
495 }
496});
497
498export default VDebug;