soiz1 commited on
Commit
271613e
·
verified ·
1 Parent(s): 6fb6a50

Migrated from GitHub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .eustia.js +9 -0
  2. .prettierignore +1 -0
  3. .prettierrc.json +5 -0
  4. CHANGELOG.md +489 -0
  5. LICENSE +21 -0
  6. ORIGINAL_README.md +85 -0
  7. build/build.js +13 -0
  8. build/loaders/handlebars-minifier-loader.js +3 -0
  9. build/webpack.analyser.js +8 -0
  10. build/webpack.base.js +112 -0
  11. build/webpack.dev.js +14 -0
  12. build/webpack.polyfill.js +10 -0
  13. build/webpack.prod.js +23 -0
  14. eruda.d.ts +536 -0
  15. eslint.config.mjs +34 -0
  16. karma.conf.js +61 -0
  17. package.json +88 -0
  18. src/Console/Console.js +399 -0
  19. src/Console/Console.scss +145 -0
  20. src/DevTools/DevTools.js +408 -0
  21. src/DevTools/DevTools.scss +37 -0
  22. src/DevTools/Tool.js +20 -0
  23. src/Elements/CssStore.js +112 -0
  24. src/Elements/Detail.js +518 -0
  25. src/Elements/Elements.js +325 -0
  26. src/Elements/Elements.scss +253 -0
  27. src/Elements/util.js +39 -0
  28. src/EntryBtn/EntryBtn.js +176 -0
  29. src/EntryBtn/EntryBtn.scss +22 -0
  30. src/Info/Info.js +123 -0
  31. src/Info/Info.scss +60 -0
  32. src/Info/defInfo.js +57 -0
  33. src/Network/Detail.js +166 -0
  34. src/Network/Network.js +385 -0
  35. src/Network/Network.scss +175 -0
  36. src/Network/util.js +108 -0
  37. src/Resources/Cookie.js +190 -0
  38. src/Resources/Resources.js +444 -0
  39. src/Resources/Resources.scss +87 -0
  40. src/Resources/Storage.js +229 -0
  41. src/Resources/util.js +40 -0
  42. src/Settings/Settings.js +150 -0
  43. src/Settings/Settings.scss +10 -0
  44. src/Snippets/Snippets.js +99 -0
  45. src/Snippets/Snippets.scss +44 -0
  46. src/Snippets/defSnippets.js +236 -0
  47. src/Snippets/searchText.scss +9 -0
  48. src/Sources/Sources.js +270 -0
  49. src/Sources/Sources.scss +65 -0
  50. src/eruda.js +322 -0
.eustia.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ test: {
3
+ library: ['node_modules/eustia-module'],
4
+ files: ['test/*.js', 'test/*.html'],
5
+ exclude: ['js'],
6
+ namespace: 'util',
7
+ output: 'test/util.js',
8
+ },
9
+ }
.prettierignore ADDED
@@ -0,0 +1 @@
 
 
1
+ test/util.js
.prettierrc.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "singleQuote": true,
3
+ "tabWidth": 2,
4
+ "semi": false
5
+ }
CHANGELOG.md ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 3.4.1 (10 Nov 2024)
2
+
3
+ * fix: no copy and delete for shadow root
4
+ * fix: fetch remains pending when error occurs
5
+ * fix: theme not updated if system theme changed
6
+
7
+ ## 3.4.0 (27 Sep 2024)
8
+
9
+ * feat: support shadow dom [#158](https://github.com/liriliri/eruda/issues/158)
10
+ * fix: quirks mode table rendering [#459](https://github.com/liriliri/eruda/issues/459)
11
+
12
+ ## 3.3.0 (9 Sep 2024)
13
+
14
+ * feat: add vue devtools plugin
15
+
16
+ ## 3.2.3 (10 AUG 2024)
17
+
18
+ * fix: WebSocket message base64 encoded [#447](https://github.com/liriliri/eruda/issues/447)
19
+
20
+ ## 3.2.2 (8 AUG 2024)
21
+
22
+ * chore: update plugin versions
23
+
24
+ ## 3.2.1 (20 JUL 2024)
25
+
26
+ * fix: touches plugin [#344](https://github.com/liriliri/eruda/issues/344)
27
+
28
+ ## 3.2.0 (16 JUL 2024)
29
+
30
+ * feat: support inline mode
31
+ * feat: allow spaces in plugin name
32
+ * fix: some typescript d.ts mistakes
33
+ * chore: remove elements set api
34
+ * chore: update monitor plugin version
35
+
36
+ ## 3.1.0 (9 JUL 2024)
37
+
38
+ * feat: add AMOLED theme [#414](https://github.com/liriliri/eruda/pull/414)
39
+ * feat: support system preference theme config
40
+ * feat: add isDarkTheme, getTheme util
41
+ * fix: backers.svg lazy loading [#407](https://github.com/liriliri/eruda/issues/407)
42
+
43
+ ## 3.0.1 (18 JUL 2023)
44
+
45
+ * fix: can not print string with %o [#336](https://github.com/liriliri/eruda/issues/336)
46
+ * fix: mouse event on touch device [#302](https://github.com/liriliri/eruda/issues/302)
47
+ * fix: unable to remove snippets [#349](https://github.com/liriliri/eruda/issues/349)
48
+
49
+ ## 3.0.0 (2 Apr 2023)
50
+
51
+ * feat: replace fps and memory with monitor plugin
52
+ * fix: resource stylesheet show failed
53
+ * chore: remove licia utils
54
+ * chore: separate polyfill
55
+
56
+ ## 2.11.3 (3 Mar 2023)
57
+
58
+ * fix: scale [#307](https://github.com/liriliri/eruda/issues/307)
59
+
60
+ ## 2.11.2 (28 Jan 2023)
61
+
62
+ * fix: check safe area error
63
+
64
+ ## 2.11.1 (28 Jan 2023)
65
+
66
+ * fix: bottom safe area
67
+ * fix(console): filter function support
68
+ * fix: click event stop propagation [#155](https://github.com/liriliri/eruda/issues/155)
69
+ * fix: worker null error [#152](https://github.com/liriliri/eruda/issues/152)
70
+
71
+ ## 2.11.0 (19 Jan 2023)
72
+
73
+ * feat(network): filter
74
+ * feat(info): add backers
75
+ * feat(settings): use luna setting
76
+ * feat(resources): use luna data grid
77
+ * feat(resources): copy storage, cookie
78
+ * fix(sources): code not selectable
79
+ * fix(console): filter api
80
+
81
+ ## 2.10.0 (24 Dec 2022)
82
+
83
+ * feat(sources): use luna text viewer
84
+ * feat(elements): split mode
85
+ * feat(network): split mode
86
+ * fix(resources): delete cookie
87
+
88
+ ## 2.9.1 (20 Dec 2022)
89
+
90
+ * fix(elements): select element using touch events
91
+
92
+ ## 2.9.0 (20 Dec 2022)
93
+
94
+ * feat(elements): integrate dom viewer
95
+ * feat(elements): element crumbs
96
+ * feat(elements): copy node and delete node
97
+ * feat(network): copy response
98
+ * feat(network): toggle recording
99
+ * chore: remove dom plugin snippet
100
+
101
+ ## 2.8.3 (13 Dec 2022)
102
+
103
+ * fix(network): remove data grid ios outline
104
+ * chore: update luna console and luna object viewer
105
+
106
+ ## 2.8.2 (12 Dec 2022)
107
+
108
+ * fix: some variables not reset when destroy
109
+
110
+ ## 2.8.1 (12 Dec 2022)
111
+
112
+ * fix: remove luna syntax highlighter
113
+
114
+ ## 2.8.0 (11 Dec 2022)
115
+
116
+ * feat(info): copy
117
+ * feat(sources): use luna syntax highlighter
118
+ * feat(network): use luna data grid
119
+ * feat(network): copy as curl [#220](https://github.com/liriliri/eruda/issues/220)
120
+ * fix(network): recognize JSON [#201](https://github.com/liriliri/eruda/issues/201)
121
+ * fix: init with shadow dom style error [#195](https://github.com/liriliri/eruda/issues/195)
122
+
123
+ ## 2.7.4 (10 Dec 2022)
124
+
125
+ * fix: firefox document.body is null error [#293](https://github.com/liriliri/eruda/issues/293)
126
+
127
+ ## 2.7.3 (8 Dec 2022)
128
+
129
+ * fix: remove tabs horizontal scrollbar [#236](https://github.com/liriliri/eruda/issues/236)
130
+
131
+ ## 2.7.2 (7 Dec 2022)
132
+
133
+ * fix: luna modal style
134
+
135
+ ## 2.7.1 (7 Dec 2022)
136
+
137
+ * fix: remove debug log
138
+
139
+ ## 2.7.0 (7 Dec 2022)
140
+
141
+ * feat: drag to resize
142
+ * feat: update icons
143
+ * feat: use luna modal to replace browser prompt
144
+
145
+ ## 2.6.2 (3 Dec 2022)
146
+
147
+ * feat: support android 5.0
148
+ * feat(sources): remove code beautify
149
+ * fix: code plugin theme
150
+
151
+ ## 2.6.1 (26 Nov 2022)
152
+
153
+ * fix: dark mode scrollbar style
154
+ * fix: unable to load timing plugin
155
+
156
+ ## 2.6.0 (25 Nov 2022)
157
+
158
+ * feat(console): select and copy
159
+ * chore: update luna console
160
+ * chore: update chobitsu
161
+
162
+ ## 2.5.0 (9 Jul 2022)
163
+
164
+ * feat: add ts declaration [#187](https://github.com/liriliri/eruda/pull/187)
165
+ * refactor: use luna console
166
+ * refactor: use chobitsu for highlighting element
167
+
168
+ ## 2.4.1 (28 Sep 2020)
169
+
170
+ * fix: remove arrow function [#160](https://github.com/liriliri/eruda/issues/160)
171
+
172
+ ## 2.4.0 (14 Sep 2020)
173
+
174
+ * feat: default settings [#141](https://github.com/liriliri/eruda/issues/141)
175
+ * fix(elements): highlight
176
+ * fix(console): blinks frequently as it scroll to the border
177
+ * refactor: use chobitsu
178
+
179
+ ## 2.3.3 (3 May 2020)
180
+
181
+ * fix: unsafe-eval CSP violation [#140](https://github.com/liriliri/eruda/issues/140)
182
+
183
+ ## v2.3.2 (29 Apr 2020)
184
+
185
+ * fix(console): scroll performance
186
+
187
+ ## v2.3.1 (28 Apr 2020)
188
+
189
+ * fix(elements): content highlight
190
+
191
+ ## v2.3.0 (28 Apr 2020)
192
+
193
+ * feat: refresh notification
194
+ * fix(console): safari bounce effect
195
+ * fix(elements): highlight
196
+
197
+ ## v2.2.2 (17 Apr 2020)
198
+
199
+ * fix(console): extra info from
200
+ * chore: update icons
201
+
202
+ ## v2.2.1 (20 Mar 2020)
203
+
204
+ * fix: redundant evaluated style
205
+ * chore: use [luna-object-viewer](https://github.com/liriliri/luna) for viewing object
206
+
207
+ ## v2.2.0 (9 Feb 2020)
208
+
209
+ * feat: use dark theme for dark mode
210
+ * feat(elements): computed style filter
211
+ * feat(resources): storage and cookie filter
212
+ * fix(snippet): error loading plugin for local page
213
+ * fix(console): unable to clear filter
214
+
215
+ ## v2.1.0 (2 Feb 2020)
216
+
217
+ * feat: change navigation bar height
218
+ * feat: change default transparency to 1
219
+ * feat: change loaded plugin position
220
+ * feat(console): remove debug filter
221
+ * feat(console): improve input style
222
+ * feat(console): show filter text
223
+ * feat(network): add requests api [#132](https://github.com/liriliri/eruda/issues/132)
224
+
225
+ ## v2.0.2 (9 Jan 2020)
226
+
227
+ * chore: reduce file size (452kb -> 418kb)
228
+
229
+ ## v2.0.1 (6 Jan 2020)
230
+
231
+ * chore: update plugins
232
+
233
+ ## v2.0.0 (3 Jan 2020)
234
+
235
+ * feat: theme support
236
+ * feat(console): $x utility
237
+ * feat(console): remove useWorker
238
+ * feat(sources): indent size configuration
239
+ * fix(console): url recognition
240
+ * fix(console): log style
241
+ * fix(sources): scrolling
242
+ * perf(console): large object expansion
243
+ * chore: reduce file size (472kb -> 452kb)
244
+
245
+ ## v1.10.3 (8 Nov 2019)
246
+
247
+ * fix(info): escape location [#127](https://github.com/liriliri/eruda/issues/127)
248
+ * chore: update refresh icon
249
+ * chore: update timing plugin version
250
+
251
+ ## v1.10.2 (5 Nov 2019)
252
+
253
+ * fix: must add .default if using require
254
+
255
+ ## v1.10.1 (4 Nov 2019)
256
+
257
+ * fix(console): error display when js execution disabled
258
+
259
+ ## v1.10.0 (4 Nov 2019)
260
+
261
+ * chore: updated to babel7, must add .default if using require
262
+ * feat(console): multiple console instance
263
+ * perf(console): rendering for a large number of logs
264
+
265
+ ## v1.9.2 (1 Nov 2019)
266
+
267
+ * perf(console): rendering
268
+
269
+ ## v1.9.1 (27 Oct 2019)
270
+
271
+ * perf(console): asynchronous log render
272
+ * perf(console): reduce memory usage, 50% drop
273
+
274
+ ## v1.9.0 (20 Oct 2019)
275
+
276
+ * feat: add snippet for loading touches plugin
277
+ * feat: add fit screen snippet
278
+ * fix(console): filter shouldn't affect group
279
+
280
+ ## v1.8.1 (14 Oct 2019)
281
+
282
+ * fix(network): style [#121](https://github.com/liriliri/eruda/issues/121)
283
+
284
+ ## v1.8.0 (13 Oct 2019)
285
+
286
+ * feat(network): display optimization
287
+ * feat: move http view from sources to network
288
+ * fix(console): group object expansion
289
+
290
+ ## v1.7.2 (11 Oct 2019)
291
+
292
+ * fix(console): blank bottom if js input is disabled
293
+ * chore: update eruda-dom version
294
+
295
+ ## v1.7.1 (10 Oct 2019)
296
+
297
+ * fix: resize
298
+
299
+ ## v1.7.0 (8 Oct 2019)
300
+
301
+ * feat: resize [#89](https://github.com/liriliri/eruda/issues/89)
302
+ * feat(console): replace help button with filter
303
+ * feat(console): disable js execution
304
+ * feat(console): [utilities api](https://developers.google.cn/web/tools/chrome-devtools/console/utilities)
305
+ * fix(console): disable log collapsing for group
306
+ * fix(elements): select not working for desktop
307
+
308
+ ## v1.6.3 (1 Oct 2019)
309
+
310
+ * fix(console): log border style
311
+
312
+ ## v1.6.2 (29 Sep 2019)
313
+
314
+ * fix: container style affected [#119](https://github.com/liriliri/eruda/issues/119)
315
+ * fix(console): log style, line-height should be normal
316
+
317
+ ## v1.6.1 (27 Sep 2019)
318
+
319
+ * feat(network): catch fetch request headers
320
+ * feat(console): timeLog, countReset
321
+ * fix(console): clear not working
322
+ * fix(console): table
323
+
324
+ ## v1.6.0 (26 Sep 2019)
325
+
326
+ * feat: console group
327
+ * fix: console style, width and height is forbidden
328
+ * fix: regexp json view
329
+ * chore: update fps and memory plugin version
330
+
331
+ ## v1.5.8 (2 Aug 2019)
332
+
333
+ * fix: safeStorage undefined [#108](https://github.com/liriliri/eruda/issues/108)
334
+
335
+ ## v1.5.7 (15 Jul 2019)
336
+
337
+ * Fix iOS max log number
338
+ * Disable calling init if already initialized
339
+ * Disable worker by default
340
+ * Support xhr blob response type [#104](https://github.com/liriliri/eruda/issues/100)
341
+
342
+ ## v1.5.6 (17 Jun 2019)
343
+
344
+ * Disable log collapse for objects
345
+
346
+ ## v1.5.5 (25 May 2019)
347
+
348
+ * Fix resources error when cookie has % [#100](https://github.com/liriliri/eruda/issues/100)
349
+ * Update dom plugin version
350
+
351
+ ## v1.5.4 (23 Sep 2018)
352
+
353
+ * Fix network url start with //
354
+ * Smaller padding for logs
355
+
356
+ ## v1.5.3 (2 Sep 2018)
357
+
358
+ * Add load dom plugin snippet
359
+ * Disable highlight for invisible elements
360
+ * Fix unexpected token \t in JSON
361
+ * Add load orientation plugin snippet
362
+
363
+ ## v1.5.2 (23 Aug 2018)
364
+
365
+ * Fix console show in sources panel
366
+ * Fix log merge
367
+ * Support getting entryBtn instance
368
+ * Update timing plugin version
369
+ * Add remove setting api
370
+ * Fix safari merge log exception
371
+
372
+ ## v1.5.1 (18 Aug 2018)
373
+
374
+ * Fix uglifyjs unicode escape [#69](https://github.com/liriliri/eruda/issues/69)
375
+ * Update icons, use [iconfont](http://www.iconfont.cn) instead of [icomoon](https://icomoon.io/)
376
+ * Show custom request headers [#78](https://github.com/liriliri/eruda/pull/78)
377
+ * Add get api to info panel [#83](https://github.com/liriliri/eruda/issues/83)
378
+ * Fix responseType json error [#82](https://github.com/liriliri/eruda/issues/82)
379
+ * Support console lazy evaluation
380
+
381
+ ## v1.5.0 (19 Jun 2018)
382
+
383
+ * Use shadow dom to encapsulate css
384
+ * Enable sources copy [#71](https://github.com/liriliri/eruda/issues/71)
385
+ * Improve **borderAll** style
386
+ * Add **position** api [#74](https://github.com/liriliri/eruda/issues/74)
387
+ * Fix nav bottom bar wrong position when removed
388
+
389
+ ## v1.4.4 (27 May 2018)
390
+
391
+ * Improve console line break display
392
+ * Add **rmCookie** util
393
+ * Add **Load Geolocation Plugin** snippet
394
+ * Fix Elements cssRules [#63](https://github.com/liriliri/eruda/issues/63)
395
+ * Support console events [#66](https://github.com/liriliri/eruda/issues/66)
396
+ * Fix Uc browser console worker [#62](https://github.com/liriliri/eruda/issues/62)
397
+
398
+ ## v1.4.3 (7 Feb 2018)
399
+
400
+ * Dynamic info content support [#51](https://github.com/liriliri/eruda/issues/51)
401
+ * Fix console input covered by error log
402
+ * Add elements box model chart
403
+ * Fix source code white-space style [#53](https://github.com/liriliri/eruda/issues/53)
404
+ * Resources support iframe
405
+ * Add **Load Benchmark Plugin** snippet
406
+
407
+ ## v1.4.2 (28 Jan 2018)
408
+
409
+ * Extract viewportScale util into [eris](https://github.com/liriliri/eris)
410
+ * Improve image list view using flex
411
+ * Add DevTools display event hooks [#50](https://github.com/liriliri/eruda/issues/50)
412
+
413
+ ## v1.4.1 (13 Jan 2018)
414
+
415
+ * Update timing plugin version
416
+ * Fix viewportScale
417
+ * Optimize console performance for big data
418
+ * Expose snippets run api
419
+ * Delete desktop scrollbar style
420
+ * Add code plugin to snippets
421
+
422
+ ## v1.4.0 (7 Jan 2018)
423
+
424
+ * Remove network timing into external plugin
425
+ * Add system info
426
+ * Add memory plugin snippet
427
+ * Monitor fetch requests [#24](https://github.com/liriliri/eruda/issues/24)
428
+ * Reduce json viewer click area
429
+ * Use resource timing for image capture
430
+
431
+ ## v1.3.2 (14 Dec 2017)
432
+
433
+ * Fix restore settings snippet
434
+ * Extract *features* into an external plugin
435
+
436
+ ## v1.3.1 (19 Nov 2017)
437
+
438
+ * Observe elements in resources panel
439
+ * Fix performance timing not supported [#40](https://github.com/liriliri/eruda/issues/40)
440
+
441
+ ## v1.3.0 (5 Nov 2017)
442
+
443
+ * Remove log margin
444
+ * Fix css custom properties [#33](https://github.com/liriliri/eruda/issues/33)
445
+ * Add version info
446
+ * Change icomoon generated font name
447
+ * Improve snippets style
448
+ * Add *Load Fps Plugin* and *Restore Settings* snippets
449
+ * Support navbar color customization
450
+ * Support range in settings panel
451
+ * Support auto scale [#32](https://github.com/liriliri/eruda/issues/32)
452
+ * Improve *Border All* snippet
453
+ * Use high resolution time for console time
454
+
455
+ ## v1.2.6 (31 Aug 2017)
456
+
457
+ * Fix catch global errors
458
+
459
+ ## v1.2.5 (20 Aug 2017)
460
+
461
+ * Fix cookie URI malformed
462
+ * Fix single string argument unescaped
463
+ * Update util library and dependencies
464
+ * Fix catch event listeners [#31](https://github.com/liriliri/eruda/issues/31)
465
+ * Console log scroll automatically only at bottom
466
+ * Fix unformatted html tag
467
+
468
+ ## v1.2.4 (1 Jul 2017)
469
+
470
+ * Fix uncaught promise error [#29](https://github.com/liriliri/eruda/issues/23)
471
+ * Fix bad classes [#28](https://github.com/liriliri/eruda/issues/23)
472
+
473
+ ## v1.2.3 (15 May 2017)
474
+
475
+ * Disable modernizr classes
476
+ * Update eustia util
477
+ * Fix console resize [#23](https://github.com/liriliri/eruda/issues/23)
478
+ * Improve object log
479
+ * Use outline for borderAll snippet
480
+
481
+ ## v1.2.2 (11 Mar 2017)
482
+
483
+ * Fix log url recognition
484
+ * Fix error log stack url and style
485
+ * Fix table log ouput
486
+ * Fix storage initialization [#20](https://github.com/liriliri/eruda/issues/20)
487
+ * Update eustia lib
488
+ * Elements auto refresh
489
+ * Add pc scrollbar style
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016-present liriliri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ORIGINAL_README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <a href="https://eruda.liriliri.io/" target="_blank">
3
+ <img src="https://eruda.liriliri.io/icon.png" width="400">
4
+ </a>
5
+ </div>
6
+
7
+ <h1 align="center">Eruda</h1>
8
+
9
+ <div align="center">
10
+
11
+ Console for Mobile Browsers.
12
+
13
+ [![NPM version][npm-image]][npm-url]
14
+ [![Build status][ci-image]][ci-url]
15
+ [![Test coverage][codecov-image]][codecov-url]
16
+ [![Downloads][jsdelivr-image]][jsdelivr-url]
17
+ [![License][license-image]][npm-url]
18
+
19
+ </div>
20
+
21
+ [npm-image]: https://img.shields.io/npm/v/eruda?style=flat-square
22
+ [npm-url]: https://npmjs.org/package/eruda
23
+ [jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hm/eruda?style=flat-square
24
+ [jsdelivr-url]: https://www.jsdelivr.com/package/npm/eruda
25
+ [ci-image]: https://img.shields.io/github/actions/workflow/status/liriliri/eruda/main.yml?branch=master&style=flat-square
26
+ [ci-url]: https://github.com/liriliri/eruda/actions/workflows/main.yml
27
+ [codecov-image]: https://img.shields.io/codecov/c/github/liriliri/eruda?style=flat-square
28
+ [codecov-url]: https://codecov.io/github/liriliri/eruda?branch=master
29
+ [license-image]: https://img.shields.io/npm/l/eruda?style=flat-square
30
+ [donate-image]: https://img.shields.io/badge/$-donate-0070ba.svg?style=flat-square
31
+
32
+ <img src="https://eruda.liriliri.io/screenshot.jpg" style="width:100%">
33
+
34
+ ## Demo
35
+
36
+ ![Demo](https://eruda.liriliri.io/qrcode.png)
37
+
38
+ Browse it on your phone: [eruda.liriliri.io](https://eruda.liriliri.io/)
39
+
40
+ ## Install
41
+
42
+ You can get it on npm.
43
+
44
+ ```bash
45
+ npm install eruda --save-dev
46
+ ```
47
+
48
+ Add this script to your page.
49
+
50
+ ```html
51
+ <script src="node_modules/eruda/eruda.js"></script>
52
+ <script>eruda.init();</script>
53
+ ```
54
+
55
+ It's also available on [jsDelivr](http://www.jsdelivr.com/projects/eruda) and [cdnjs](https://cdnjs.com/libraries/eruda).
56
+
57
+ ```html
58
+ <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
59
+ <script>eruda.init();</script>
60
+ ```
61
+
62
+ For more detailed usage instructions, please read the documentation at [eruda.liriliri.io](https://eruda.liriliri.io/docs/)!
63
+
64
+ ## Related Projects
65
+
66
+ * [eruda-android](https://github.com/liriliri/eruda-android): Simple webview with eruda loaded automatically.
67
+ * [chii](https://github.com/liriliri/chii): Remote debugging tool.
68
+ * [chobitsu](https://github.com/liriliri/chobitsu): Chrome devtools protocol JavaScript implementation.
69
+ * [licia](https://github.com/liriliri/licia): Utility library used by eruda.
70
+ * [luna](https://github.com/liriliri/luna): UI components used by eruda.
71
+ * [vivy](https://github.com/liriliri/vivy-docs): Icon image generation.
72
+
73
+ ## Third Party
74
+
75
+ * [eruda-pixel](https://github.com/Faithree/eruda-pixel): UI pixel restoration tool.
76
+ * [eruda-webpack-plugin](https://github.com/huruji/eruda-webpack-plugin): Eruda webpack plugin.
77
+ * [eruda-vue-devtools](https://github.com/Zippowxk/vue-devtools-plugin): Eruda Vue-devtools plugin.
78
+
79
+ ## Backers
80
+
81
+ <a rel="noreferrer noopener" href="https://opencollective.com/eruda" target="_blank"><img src="https://opencollective.com/eruda/backers.svg?width=890"></a>
82
+
83
+ ## Contribution
84
+
85
+ Read [Contributing Guide](https://eruda.liriliri.io/docs/contributing.html) for development setup instructions.
build/build.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const path = require('path')
2
+ const fs = require('licia/fs')
3
+
4
+ const pkg = require('../package.json')
5
+
6
+ delete pkg.scripts
7
+ delete pkg.devDependencies
8
+
9
+ fs.writeFile(
10
+ path.resolve(__dirname, '../dist/package.json'),
11
+ JSON.stringify(pkg, null, 2),
12
+ 'utf8'
13
+ )
build/loaders/handlebars-minifier-loader.js ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ module.exports = function (src) {
2
+ return src.replace(/"loc":\{"start":\{"line":\d+,"column":\d+},"end":\{"line":\d+,"column":\d+\}\}/g, '')
3
+ }
build/webpack.analyser.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ const BundleAnalyzerPlugin =
2
+ require('webpack-bundle-analyzer').BundleAnalyzerPlugin
3
+
4
+ exports = require('./webpack.prod')
5
+
6
+ exports.plugins.push(new BundleAnalyzerPlugin())
7
+
8
+ module.exports = exports
build/webpack.base.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const autoprefixer = require('autoprefixer')
2
+ const prefixer = require('postcss-prefixer')
3
+ const clean = require('postcss-clean')
4
+ const webpack = require('webpack')
5
+ const pkg = require('../package.json')
6
+ const path = require('path')
7
+ const ESLintPlugin = require('eslint-webpack-plugin')
8
+
9
+ process.traceDeprecation = true
10
+
11
+ const banner = pkg.name + ' v' + pkg.version + ' ' + pkg.homepage
12
+
13
+ const postcssLoader = {
14
+ loader: 'postcss-loader',
15
+ options: {
16
+ plugins: [
17
+ prefixer({
18
+ prefix: '_',
19
+ ignore: [/luna-*/],
20
+ }),
21
+ autoprefixer,
22
+ clean(),
23
+ ],
24
+ },
25
+ }
26
+
27
+ const rawLoader = {
28
+ loader: 'raw-loader',
29
+ options: {
30
+ esModule: false,
31
+ },
32
+ }
33
+
34
+ module.exports = {
35
+ entry: './src/index',
36
+ resolve: {
37
+ symlinks: false,
38
+ alias: {
39
+ axios: path.resolve(__dirname, '../src/lib/empty.js'),
40
+ micromark: path.resolve(__dirname, '../src/lib/micromark.js'),
41
+ },
42
+ },
43
+ devServer: {
44
+ static: {
45
+ directory: path.join(__dirname, '../test'),
46
+ },
47
+ port: 8080,
48
+ },
49
+ output: {
50
+ path: path.resolve(__dirname, '../dist'),
51
+ publicPath: '/assets/',
52
+ library: 'eruda',
53
+ libraryTarget: 'umd',
54
+ },
55
+ module: {
56
+ rules: [
57
+ {
58
+ test: /\.js$/,
59
+ include: [
60
+ path.resolve(__dirname, '../src'),
61
+ path.resolve(__dirname, '../node_modules/luna-console'),
62
+ path.resolve(__dirname, '../node_modules/luna-modal'),
63
+ path.resolve(__dirname, '../node_modules/luna-tab'),
64
+ path.resolve(__dirname, '../node_modules/luna-data-grid'),
65
+ path.resolve(__dirname, '../node_modules/luna-object-viewer'),
66
+ path.resolve(__dirname, '../node_modules/luna-dom-viewer'),
67
+ path.resolve(__dirname, '../node_modules/luna-text-viewer'),
68
+ path.resolve(__dirname, '../node_modules/luna-setting'),
69
+ path.resolve(__dirname, '../node_modules/luna-box-model'),
70
+ path.resolve(__dirname, '../node_modules/luna-notification'),
71
+ ],
72
+ use: [
73
+ {
74
+ loader: 'babel-loader',
75
+ options: {
76
+ sourceType: 'unambiguous',
77
+ presets: ['@babel/preset-env'],
78
+ plugins: [
79
+ '@babel/plugin-transform-runtime',
80
+ '@babel/plugin-proposal-class-properties',
81
+ ],
82
+ },
83
+ },
84
+ ],
85
+ },
86
+ {
87
+ test: /\.scss$/,
88
+ use: [
89
+ 'css-loader',
90
+ postcssLoader,
91
+ { loader: 'sass-loader', options: { api: 'modern' } },
92
+ ],
93
+ },
94
+ {
95
+ test: /\.css$/,
96
+ exclude: /luna-dom-highlighter/,
97
+ use: ['css-loader', postcssLoader],
98
+ },
99
+ {
100
+ test: /luna-dom-highlighter\.css$/,
101
+ use: [rawLoader],
102
+ },
103
+ ],
104
+ },
105
+ plugins: [
106
+ new webpack.BannerPlugin(banner),
107
+ new webpack.DefinePlugin({
108
+ VERSION: '"' + pkg.version + '"',
109
+ }),
110
+ new ESLintPlugin(),
111
+ ],
112
+ }
build/webpack.dev.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const webpack = require('webpack')
2
+
3
+ exports = require('./webpack.base')
4
+
5
+ exports.mode = 'development'
6
+ exports.output.filename = 'eruda.js'
7
+ exports.devtool = 'source-map'
8
+ exports.plugins = exports.plugins.concat([
9
+ new webpack.DefinePlugin({
10
+ ENV: '"development"',
11
+ }),
12
+ ])
13
+
14
+ module.exports = exports
build/webpack.polyfill.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ const path = require('path')
2
+
3
+ module.exports = {
4
+ mode: 'production',
5
+ entry: './src/polyfill',
6
+ output: {
7
+ path: path.resolve(__dirname, '../dist'),
8
+ filename: 'eruda-polyfill.js',
9
+ },
10
+ }
build/webpack.prod.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const webpack = require('webpack')
2
+ const TerserPlugin = require('terser-webpack-plugin')
3
+
4
+ exports = require('./webpack.base')
5
+
6
+ exports.mode = 'production'
7
+ exports.output.filename = 'eruda.js'
8
+ exports.devtool = 'source-map'
9
+ exports.plugins = exports.plugins.concat([
10
+ new webpack.DefinePlugin({
11
+ ENV: '"production"',
12
+ }),
13
+ ])
14
+ exports.optimization = {
15
+ minimize: true,
16
+ minimizer: [
17
+ new TerserPlugin({
18
+ extractComments: false,
19
+ }),
20
+ ],
21
+ }
22
+
23
+ module.exports = exports
eruda.d.ts ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Type definitions for Eruda
3
+ * @see https://github.com/liriliri/eruda
4
+ */
5
+ declare module 'eruda' {
6
+ export interface InitDefaults {
7
+ /**
8
+ * Transparency, 0 to 1
9
+ */
10
+ transparency?: number
11
+ /**
12
+ * Display size, 0 to 100
13
+ */
14
+ displaySize?: number
15
+ /**
16
+ * Theme, defaults to Light or Dark in dark mode
17
+ */
18
+ theme?: string
19
+ }
20
+
21
+ export interface InitOptions {
22
+ /**
23
+ * Container element. If not set, it will append an element directly under html root element
24
+ */
25
+ container?: HTMLElement
26
+ /**
27
+ * Choose which default tools you want, by default all will be added
28
+ */
29
+ tool?: string[]
30
+ /**
31
+ * Auto scale eruda for different viewport settings
32
+ */
33
+ autoScale?: boolean
34
+ /**
35
+ * Use shadow dom for css encapsulation
36
+ */
37
+ useShadowDom?: boolean
38
+ /**
39
+ * Enable inline mode
40
+ */
41
+ inline?: boolean
42
+ /**
43
+ * Default settings
44
+ */
45
+ defaults?: InitDefaults
46
+ }
47
+
48
+ export interface Position {
49
+ x: number
50
+ y: number
51
+ }
52
+
53
+ type AnyFn = (...args: any[]) => any
54
+
55
+ export interface Emitter {
56
+ on(event: string, listener: AnyFn): Emitter
57
+ off(event: string, listener: AnyFn): Emitter
58
+ once(event: string, listener: AnyFn): Emitter
59
+ emit(event: string, ...args: any[]): Emitter
60
+ removeAllListeners(event?: string): Emitter
61
+ }
62
+
63
+ /**
64
+ * Eruda Plugin
65
+ * @see https://eruda.liriliri.io/docs/plugin.html
66
+ */
67
+ export interface Tool {
68
+ /**
69
+ * Every plugin must have a unique name, which will be shown as the tab name on the top.
70
+ */
71
+ name: string
72
+ /**
73
+ * Called when plugin is added, and a document element used to display content is passed in.
74
+ * The element is wrapped as a jQuery like object, provided by the licia utility library.
75
+ */
76
+ init(el: unknown): void
77
+ /**
78
+ * Called when switch to the panel. Usually all you need to do is to show the container element.
79
+ */
80
+ show(): Tool | undefined
81
+ /**
82
+ * Called when switch to other panel. You should at least hide the container element here.
83
+ */
84
+ hide(): Tool | undefined
85
+ /**
86
+ * Called when plugin is removed using `eruda.remove('plugin name')`.
87
+ */
88
+ destroy(): void
89
+ }
90
+
91
+ export interface ToolConstructor {
92
+ new (): Tool
93
+ readonly prototype: Tool
94
+
95
+ extend(tool: Tool): ToolConstructor
96
+ }
97
+
98
+ export interface ConsoleConfig {
99
+ /**
100
+ * Asynchronous rendering
101
+ */
102
+ asyncRender?: boolean
103
+ /**
104
+ * Enable JavaScript execution
105
+ */
106
+ jsExecution?: boolean
107
+ /**
108
+ * Catch global errors
109
+ */
110
+ catchGlobalErr?: boolean
111
+ /**
112
+ * Override console
113
+ */
114
+ overrideConsole?: boolean
115
+ /**
116
+ * Display extra information
117
+ */
118
+ displayExtraInfo?: boolean
119
+ /**
120
+ * Display unenumerable properties
121
+ */
122
+ displayUnenumerable?: boolean
123
+ /**
124
+ * Access getter value
125
+ */
126
+ displayGetterVal?: boolean
127
+ /**
128
+ * Stringify object when clicked
129
+ */
130
+ lazyEvaluation?: boolean
131
+ /**
132
+ * Auto display if error occurs
133
+ */
134
+ displayIfErr?: boolean
135
+ /**
136
+ * Max log number
137
+ */
138
+ maxLogNum?: string
139
+ }
140
+
141
+ export interface Log {
142
+ type: string
143
+ }
144
+
145
+ export interface ErudaConsole extends Tool, Console {
146
+ config: {
147
+ set<K extends keyof ConsoleConfig>(name: K, value: ConsoleConfig[K]): void
148
+ }
149
+ /**
150
+ * Custom filter
151
+ */
152
+ filter(pattern: string | RegExp | ((log: Log) => boolean)): void
153
+ /**
154
+ * Html string
155
+ */
156
+ html(htmlStr: string): void
157
+ }
158
+
159
+ export interface ErudaConsoleConstructor {
160
+ new (): ErudaConsole
161
+ readonly prototype: ErudaConsole
162
+ }
163
+
164
+ export interface ElementsConfig {
165
+ /**
166
+ * Catch Event Listeners
167
+ */
168
+ overrideEventTarget?: boolean
169
+ /**
170
+ * Auto Refresh
171
+ */
172
+ observeElement?: boolean
173
+ }
174
+
175
+ export interface Elements extends Tool {
176
+ config: {
177
+ set<K extends keyof ElementsConfig>(
178
+ name: K,
179
+ value: ElementsConfig[K]
180
+ ): void
181
+ }
182
+ /**
183
+ * Element to display
184
+ */
185
+ select(el: HTMLElement): void
186
+ }
187
+
188
+ export interface ElementsConstructor {
189
+ new (): Elements
190
+ readonly prototype: Elements
191
+ }
192
+
193
+ export interface Network extends Tool {
194
+ /**
195
+ * Clear requests
196
+ */
197
+ clear(): void
198
+ /**
199
+ * Get request data
200
+ */
201
+ requests(): object[]
202
+ }
203
+
204
+ export interface NetworkConstructor {
205
+ new (): Network
206
+ readonly prototype: Network
207
+ }
208
+
209
+ export interface ResourcesConfig {
210
+ /**
211
+ * Hide Eruda Setting
212
+ */
213
+ hideErudaSetting?: boolean
214
+ /**
215
+ * Auto Refresh Elements
216
+ */
217
+ observeElement?: boolean
218
+ }
219
+
220
+ export interface Resources extends Tool {
221
+ config: {
222
+ set<K extends keyof ResourcesConfig>(
223
+ name: K,
224
+ value: ResourcesConfig[K]
225
+ ): void
226
+ }
227
+ }
228
+
229
+ export interface ResourcesConstructor {
230
+ new (): Resources
231
+ readonly prototype: Resources
232
+ }
233
+
234
+ export interface SourcesConfig {
235
+ /**
236
+ * Show Line Numbers
237
+ */
238
+ showLineNum?: boolean
239
+ /**
240
+ * Beautify Code
241
+ */
242
+ formatCode?: boolean
243
+ /**
244
+ * Indent Size
245
+ */
246
+ indentSize?: string
247
+ }
248
+
249
+ export interface Sources extends Tool {
250
+ config: {
251
+ set<K extends keyof SourcesConfig>(name: K, value: SourcesConfig[K]): void
252
+ }
253
+ }
254
+
255
+ export interface SourcesConstructor {
256
+ new (): Sources
257
+ readonly prototype: Sources
258
+ }
259
+
260
+ export interface InfoItem {
261
+ name: string
262
+ val: string
263
+ }
264
+
265
+ export interface Info extends Tool {
266
+ /**
267
+ * Clear infos
268
+ */
269
+ clear(): void
270
+ /**
271
+ * Add info
272
+ */
273
+ add(name: string, content: string | (() => void)): void
274
+ /**
275
+ * Get info or infos
276
+ */
277
+ get(): InfoItem[]
278
+ get(name: string): string
279
+ /**
280
+ * Remove specified info
281
+ */
282
+ remove(name: string): void
283
+ }
284
+
285
+ export interface InfoConstructor {
286
+ new (): Info
287
+ readonly prototype: Info
288
+ }
289
+
290
+ export interface Snippets extends Tool {
291
+ /**
292
+ * Clear snippets
293
+ */
294
+ clear(): void
295
+ /**
296
+ * Add snippet
297
+ * @param name Snippet name
298
+ * @param fn Function to be triggered
299
+ * @param desc Snippet description
300
+ */
301
+ add(name: string, fn: Function, desc: string): void
302
+ /**
303
+ * Remove specified snippet
304
+ * @param name Snippet name
305
+ */
306
+ remove(name: string): void
307
+ /**
308
+ * Run specified snippet
309
+ * @param name Snippet name
310
+ */
311
+ run(name: string): void
312
+ }
313
+
314
+ export interface SnippetsConstructor {
315
+ new (): Snippets
316
+ readonly prototype: Snippets
317
+ }
318
+
319
+ export interface SettingsRangeOptions {
320
+ min?: number
321
+ max?: number
322
+ step?: number
323
+ }
324
+
325
+ export interface Settings extends Tool {
326
+ /**
327
+ * Clear settings
328
+ */
329
+ clear(): void
330
+ /**
331
+ * Remove setting
332
+ * @param cfg Config object
333
+ * @param name Option name
334
+ */
335
+ remove(cfj: object, name: string): void
336
+ /**
337
+ * Add text
338
+ */
339
+ text(str: string): void
340
+ /**
341
+ * Add switch to toggle a boolean value
342
+ * @param cfg Config object created by util.createCfg
343
+ * @param name Option name
344
+ * @param desc Option description
345
+ */
346
+ switch(cfg: object, name: string, desc: string): void
347
+ /**
348
+ * Add select to select a number of string values
349
+ * @param cfg Config object
350
+ * @param name Option name
351
+ * @param desc Option description
352
+ * @param values Array of strings to select
353
+ */
354
+ select(cfg: object, name: string, desc: string, values: string[]): void
355
+ /**
356
+ * Add range to input a number
357
+ * @param cfg Config object
358
+ * @param name Option name
359
+ * @param desc Option description
360
+ * @param options Min, max, step
361
+ */
362
+ range(
363
+ cfg: object,
364
+ name: string,
365
+ desc: string,
366
+ options?: SettingsRangeOptions
367
+ ): void
368
+ /**
369
+ * Add a separator
370
+ */
371
+ separator(): void
372
+ }
373
+
374
+ export interface SettingsConstructor {
375
+ new (): Settings
376
+ readonly prototype: Settings
377
+ }
378
+
379
+ export interface EntryBtn extends Emitter {
380
+ show(): void
381
+ hide(): void
382
+ getPos(): Position
383
+ setPos(pos: Position): void
384
+ destroy(): void
385
+ }
386
+
387
+ export interface EntryBtnConstructor {
388
+ new (): EntryBtn
389
+ readonly prototype: EntryBtn
390
+ }
391
+
392
+ export interface DevTools extends Emitter {
393
+ show(): DevTools
394
+ hide(): DevTools
395
+ toggle(): void
396
+ add(tool: Tool | object): DevTools
397
+ remove(name: string): DevTools
398
+ removeAll(): DevTools
399
+ get<T extends ToolConstructor>(name: string): InstanceType<T> | undefined
400
+ showTool(name: string): DevTools
401
+ initCfg(settings: Settings): void
402
+ notify(content: string, options: object): void
403
+ destroy(): void
404
+ }
405
+
406
+ export interface DevToolsConstructor {
407
+ new (): DevTools
408
+ readonly prototype: DevTools
409
+ }
410
+
411
+ /**
412
+ * Eruda Util
413
+ * @see https://eruda.liriliri.io/docs/plugin.html#utility
414
+ */
415
+ export interface Util {
416
+ evalCss(css: string): HTMLStyleElement
417
+ isErudaEl(val: any): boolean
418
+ isDarkTheme(theme?: string): boolean
419
+ getTheme(): string
420
+ }
421
+
422
+ interface IToolNameMap {
423
+ console: InstanceType<ErudaConsoleConstructor>
424
+ elements: InstanceType<ElementsConstructor>
425
+ info: InstanceType<InfoConstructor>
426
+ network: InstanceType<NetworkConstructor>
427
+ resources: InstanceType<ResourcesConstructor>
428
+ settings: InstanceType<SettingsConstructor>
429
+ snippets: InstanceType<SnippetsConstructor>
430
+ sources: InstanceType<SourcesConstructor>
431
+ entryBtn: InstanceType<EntryBtnConstructor>
432
+ }
433
+
434
+ /**
435
+ * Eruda APIs
436
+ * @see https://eruda.liriliri.io/docs/api.html
437
+ */
438
+ export interface ErudaApis {
439
+ /**
440
+ * Initialize eruda.
441
+ */
442
+ init(options?: InitOptions): void
443
+ /**
444
+ * Destory eruda.
445
+ * Note: You can call `init` method again after destruction.
446
+ */
447
+ destroy(): void
448
+ /**
449
+ * Set or get scale.
450
+ */
451
+ scale(): number
452
+ scale(s: number): Eruda
453
+ /**
454
+ * Set or get entry button position.
455
+ * It will not take effect if given pos is out of range.
456
+ */
457
+ position(): Position
458
+ position(p: Position): Eruda
459
+ /**
460
+ * Get tool, eg. console, elements panels.
461
+ */
462
+ get<K extends keyof IToolNameMap>(name: K): IToolNameMap[K]
463
+ get<T extends ToolConstructor>(name: string): InstanceType<T> | undefined
464
+ get(): InstanceType<DevToolsConstructor>
465
+ /**
466
+ * Add tool.
467
+ */
468
+ add<T extends ToolConstructor>(
469
+ tool: InstanceType<T> | ((eruda: Eruda) => InstanceType<T>)
470
+ ): Eruda | undefined
471
+ /**
472
+ * Remove tool.
473
+ */
474
+ remove(name: string): Eruda | undefined
475
+ /**
476
+ * Show eruda panel.
477
+ */
478
+ show(name?: string): Eruda | undefined
479
+ /**
480
+ * Hide eruda panel.
481
+ */
482
+ hide(): Eruda | undefined
483
+ }
484
+
485
+ export interface Eruda extends ErudaApis {
486
+ /**
487
+ * Display console logs. Implementation detail follows the console api spec.
488
+ */
489
+ Console: ErudaConsoleConstructor
490
+ /**
491
+ * Check dom element status.
492
+ */
493
+ Elements: ElementsConstructor
494
+ /**
495
+ * Display special information, could be used for displaying user info to track user logs.
496
+ * By default, page url and browser user agent is shown.
497
+ */
498
+ Info: InfoConstructor
499
+ /**
500
+ * Display requests.
501
+ */
502
+ Network: NetworkConstructor
503
+ /**
504
+ * LocalStorage, sessionStorage, cookies, scripts, styleSheets and images.
505
+ */
506
+ Resources: ResourcesConstructor
507
+ /**
508
+ * Customization for all tools.
509
+ */
510
+ Settings: SettingsConstructor
511
+ /**
512
+ * Allow you to register small functions that can be triggered multiple times.
513
+ */
514
+ Snippets: SnippetsConstructor
515
+ /**
516
+ * View object, html, js, and css.
517
+ */
518
+ Sources: SourcesConstructor
519
+ /**
520
+ * Eruda Tool
521
+ */
522
+ Tool: ToolConstructor
523
+ /**
524
+ * Eruda Util
525
+ */
526
+ util: Util
527
+ /**
528
+ * Eruda version
529
+ */
530
+ readonly version: string
531
+ }
532
+
533
+ const eruda: Eruda
534
+
535
+ export default eruda
536
+ }
eslint.config.mjs ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import babelEslintParser from '@babel/eslint-parser'
2
+ import eslintJs from '@eslint/js'
3
+ import globals from 'globals'
4
+
5
+ export default [
6
+ eslintJs.configs.recommended,
7
+ {
8
+ languageOptions: {
9
+ parser: babelEslintParser,
10
+ parserOptions: {
11
+ requireConfigFile: false,
12
+ babelOptions: {
13
+ babelrc: false,
14
+ configFile: false,
15
+ },
16
+ },
17
+ globals: {
18
+ ...globals.builtin,
19
+ ...globals.browser,
20
+ ...globals.commonjs,
21
+ VERSION: true,
22
+ ENV: true,
23
+ },
24
+ },
25
+ rules: {
26
+ quotes: ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
27
+ 'prefer-const': 2,
28
+ },
29
+ },
30
+ {files: ['build/**/*.js'], languageOptions:{globals: {...globals.node}}},
31
+ {
32
+ ignores: ['test','dist','coverage'],
33
+ }
34
+ ]
karma.conf.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const webpackCfg = require('./build/webpack.dev')
2
+ webpackCfg.devtool = 'inline-source-map'
3
+ webpackCfg.module.rules.push({
4
+ test: /\.js$/,
5
+ exclude: /node_modules|lib\/util\.js/,
6
+ loader: '@jsdevtools/coverage-istanbul-loader',
7
+ enforce: 'post',
8
+ options: {
9
+ esModules: true,
10
+ },
11
+ })
12
+
13
+ module.exports = function (config) {
14
+ config.set({
15
+ basePath: '',
16
+ frameworks: ['jquery-1.8.3'],
17
+ files: [
18
+ 'src/index.js',
19
+ 'test/init.js',
20
+ 'node_modules/jasmine-core/lib/jasmine-core/jasmine.js',
21
+ 'node_modules/karma-jasmine/lib/boot.js',
22
+ 'node_modules/karma-jasmine/lib/adapter.js',
23
+ 'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
24
+ 'test/util.js',
25
+ 'test/console.js',
26
+ 'test/elements.js',
27
+ 'test/info.js',
28
+ 'test/network.js',
29
+ 'test/resources.js',
30
+ 'test/snippets.js',
31
+ 'test/sources.js',
32
+ 'test/settings.js',
33
+ 'test/eruda.js',
34
+ ],
35
+ plugins: [
36
+ 'karma-jasmine',
37
+ 'karma-jquery',
38
+ 'karma-chrome-launcher',
39
+ 'karma-webpack',
40
+ 'karma-sourcemap-loader',
41
+ 'karma-coverage-istanbul-reporter',
42
+ ],
43
+ webpackServer: {
44
+ noInfo: true,
45
+ },
46
+ preprocessors: {
47
+ 'src/index.js': ['webpack', 'sourcemap'],
48
+ },
49
+ webpack: webpackCfg,
50
+ coverageIstanbulReporter: {
51
+ reports: ['html', 'lcovonly', 'text', 'text-summary'],
52
+ },
53
+ reporters: ['progress', 'coverage-istanbul'],
54
+ port: 9876,
55
+ colors: true,
56
+ logLevel: config.LOG_INFO,
57
+ browsers: ['ChromeHeadless'],
58
+ singleRun: true,
59
+ concurrency: Infinity,
60
+ })
61
+ }
package.json ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "eruda",
3
+ "version": "3.4.1",
4
+ "description": "Console for Mobile Browsers",
5
+ "main": "eruda.js",
6
+ "browserslist": [
7
+ "since 2015",
8
+ "not dead"
9
+ ],
10
+ "scripts": {
11
+ "ci": "npm run lint && npm run test && npm run build && npm run es5",
12
+ "build": "lsla shx rm -rf dist && webpack --config build/webpack.prod.js && webpack --config build/webpack.polyfill.js && node build/build && lsla shx cp README.md eruda.d.ts dist",
13
+ "build:analyser": "webpack --config build/webpack.analyser.js",
14
+ "dev": "webpack-dev-server --config build/webpack.dev.js --host 0.0.0.0",
15
+ "test": "karma start",
16
+ "format": "lsla prettier \"*.{js,ts}\" \"src/**/*.{js,scss,css}\" \"build/*.js\" \"test/*.{js,html}\" --write",
17
+ "lint": "eslint .",
18
+ "lint:fix": "npm run lint -- --fix",
19
+ "es5": "es-check es5 dist/eruda.js dist/eruda-polyfill.js",
20
+ "setup": "lsla shx mkdir -p test/lib && lsla shx cp node_modules/jasmine-core/lib/jasmine-core/{jasmine.css,jasmine.js,jasmine-html.js,boot.js} test/lib && lsla shx cp node_modules/jasmine-jquery/lib/jasmine-jquery.js test/lib && lsla shx cp node_modules/jquery/dist/jquery.js test/lib",
21
+ "genIcon": "lsla genIcon --input src/style/icon --output src/style/icon.css --name eruda-icon && lsla prettier src/**/*.css --write"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/liriliri/eruda.git"
26
+ },
27
+ "keywords": [
28
+ "console",
29
+ "mobile",
30
+ "debug"
31
+ ],
32
+ "author": "redhoodsu",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/liriliri/eruda/issues"
36
+ },
37
+ "homepage": "https://eruda.liriliri.io/",
38
+ "devDependencies": {
39
+ "@babel/core": "^7.18.6",
40
+ "@babel/eslint-parser": "^7.26.10",
41
+ "@babel/plugin-proposal-class-properties": "^7.18.6",
42
+ "@babel/plugin-transform-runtime": "^7.18.6",
43
+ "@babel/preset-env": "^7.18.6",
44
+ "@babel/runtime": "^7.18.6",
45
+ "@eslint/js": "^9.22.0",
46
+ "@jsdevtools/coverage-istanbul-loader": "^3.0.5",
47
+ "autoprefixer": "^9.7.4",
48
+ "babel-loader": "^8.2.5",
49
+ "chobitsu": "^1.8.4",
50
+ "core-js": "^3.37.1",
51
+ "css-loader": "^3.4.2",
52
+ "es-check": "^6.2.1",
53
+ "eslint": "^9.22.0",
54
+ "eslint-webpack-plugin": "^5.0.0",
55
+ "globals": "^16.0.0",
56
+ "jasmine-core": "^2.99.1",
57
+ "jasmine-jquery": "^2.1.1",
58
+ "jquery": "^3.4.1",
59
+ "karma": "^6.4.0",
60
+ "karma-chrome-launcher": "^3.1.0",
61
+ "karma-coverage-istanbul-reporter": "^2.1.1",
62
+ "karma-jasmine": "^1.1.2",
63
+ "karma-jquery": "^0.2.4",
64
+ "karma-sourcemap-loader": "^0.3.7",
65
+ "karma-webpack": "^5.0.0",
66
+ "licia": "^1.44.0",
67
+ "luna-box-model": "^1.0.0",
68
+ "luna-console": "^1.3.6",
69
+ "luna-data-grid": "^1.4.2",
70
+ "luna-dom-viewer": "^1.8.3",
71
+ "luna-modal": "^1.3.1",
72
+ "luna-notification": "^0.3.3",
73
+ "luna-object-viewer": "^0.3.2",
74
+ "luna-setting": "^2.0.2",
75
+ "luna-tab": "^0.3.4",
76
+ "luna-text-viewer": "^0.2.1",
77
+ "postcss-clean": "^1.2.2",
78
+ "postcss-loader": "^3.0.0",
79
+ "postcss-prefixer": "^2.1.3",
80
+ "raw-loader": "^4.0.2",
81
+ "sass": "^1.77.6",
82
+ "sass-loader": "^14.2.1",
83
+ "webpack": "^5.92.1",
84
+ "webpack-bundle-analyzer": "^4.7.0",
85
+ "webpack-cli": "^5.1.4",
86
+ "webpack-dev-server": "^5.0.4"
87
+ }
88
+ }
src/Console/Console.js ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import noop from 'licia/noop'
3
+ import $ from 'licia/$'
4
+ import toStr from 'licia/toStr'
5
+ import isFn from 'licia/isFn'
6
+ import Emitter from 'licia/Emitter'
7
+ import isStr from 'licia/isStr'
8
+ import isRegExp from 'licia/isRegExp'
9
+ import uncaught from 'licia/uncaught'
10
+ import trim from 'licia/trim'
11
+ import upperFirst from 'licia/upperFirst'
12
+ import isHidden from 'licia/isHidden'
13
+ import isNull from 'licia/isNull'
14
+ import isArr from 'licia/isArr'
15
+ import extend from 'licia/extend'
16
+ import evalCss from '../lib/evalCss'
17
+ import Settings from '../Settings/Settings'
18
+ import LunaConsole from 'luna-console'
19
+ import LunaModal from 'luna-modal'
20
+ import { classPrefix as c } from '../lib/util'
21
+
22
+ uncaught.start()
23
+
24
+ export default class Console extends Tool {
25
+ constructor({ name = 'console' } = {}) {
26
+ super()
27
+
28
+ Emitter.mixin(this)
29
+
30
+ this.name = name
31
+ this._selectedLog = null
32
+ }
33
+ init($el, container) {
34
+ super.init($el)
35
+ this._container = container
36
+
37
+ this._appendTpl()
38
+
39
+ this._initCfg()
40
+
41
+ this._initLogger()
42
+ this._exposeLogger()
43
+ this._bindEvent()
44
+ }
45
+ show() {
46
+ super.show()
47
+ this._handleShow()
48
+ }
49
+ overrideConsole() {
50
+ const origConsole = (this._origConsole = {})
51
+ const winConsole = window.console
52
+
53
+ CONSOLE_METHOD.forEach((name) => {
54
+ let origin = (origConsole[name] = noop)
55
+ if (winConsole[name]) {
56
+ origin = origConsole[name] = winConsole[name].bind(winConsole)
57
+ }
58
+
59
+ winConsole[name] = (...args) => {
60
+ this[name](...args)
61
+ origin(...args)
62
+ }
63
+ })
64
+
65
+ return this
66
+ }
67
+ setGlobal(name, val) {
68
+ this._logger.setGlobal(name, val)
69
+ }
70
+ restoreConsole() {
71
+ if (!this._origConsole) return this
72
+
73
+ CONSOLE_METHOD.forEach(
74
+ (name) => (window.console[name] = this._origConsole[name])
75
+ )
76
+ delete this._origConsole
77
+
78
+ return this
79
+ }
80
+ catchGlobalErr() {
81
+ uncaught.addListener(this._handleErr)
82
+
83
+ return this
84
+ }
85
+ ignoreGlobalErr() {
86
+ uncaught.rmListener(this._handleErr)
87
+
88
+ return this
89
+ }
90
+ filter(filter) {
91
+ const $filterText = this._$filterText
92
+ const logger = this._logger
93
+
94
+ if (isStr(filter)) {
95
+ $filterText.text(filter)
96
+ logger.setOption('filter', trim(filter))
97
+ } else if (isRegExp(filter)) {
98
+ $filterText.text(toStr(filter))
99
+ logger.setOption('filter', filter)
100
+ } else if (isFn(filter)) {
101
+ $filterText.text('ƒ')
102
+ logger.setOption('filter', filter)
103
+ }
104
+ }
105
+ destroy() {
106
+ this._logger.destroy()
107
+ super.destroy()
108
+
109
+ this._container.off('show', this._handleShow)
110
+
111
+ if (this._style) {
112
+ evalCss.remove(this._style)
113
+ }
114
+ this.ignoreGlobalErr()
115
+ this.restoreConsole()
116
+ this._rmCfg()
117
+ }
118
+ _handleShow = () => {
119
+ if (isHidden(this._$el.get(0))) return
120
+ this._logger.renderViewport()
121
+ }
122
+ _handleErr = (err) => {
123
+ this._logger.error(err)
124
+ }
125
+ _enableJsExecution(enabled) {
126
+ const $el = this._$el
127
+ const $jsInput = $el.find(c('.js-input'))
128
+
129
+ if (enabled) {
130
+ $jsInput.show()
131
+ $el.rmClass(c('js-input-hidden'))
132
+ } else {
133
+ $jsInput.hide()
134
+ $el.addClass(c('js-input-hidden'))
135
+ }
136
+ }
137
+ _appendTpl() {
138
+ const $el = this._$el
139
+
140
+ this._style = evalCss(require('./Console.scss'))
141
+ $el.append(
142
+ c(`
143
+ <div class="control">
144
+ <span class="icon-clear clear-console"></span>
145
+ <span class="level active" data-level="all">All</span>
146
+ <span class="level" data-level="info">Info</span>
147
+ <span class="level" data-level="warning">Warning</span>
148
+ <span class="level" data-level="error">Error</span>
149
+ <span class="filter-text"></span>
150
+ <span class="icon-filter filter"></span>
151
+ <span class="icon-copy icon-disabled copy"></span>
152
+ </div>
153
+ <div class="logs-container"></div>
154
+ <div class="js-input">
155
+ <div class="buttons">
156
+ <div class="button cancel">Cancel</div>
157
+ <div class="button execute">Execute</div>
158
+ </div>
159
+ <span class="icon-arrow-right"></span>
160
+ <textarea></textarea>
161
+ </div>
162
+ `)
163
+ )
164
+
165
+ const _$inputContainer = $el.find(c('.js-input'))
166
+ const _$input = _$inputContainer.find('textarea')
167
+ const _$inputBtns = _$inputContainer.find(c('.buttons'))
168
+
169
+ extend(this, {
170
+ _$control: $el.find(c('.control')),
171
+ _$logs: $el.find(c('.logs-container')),
172
+ _$inputContainer,
173
+ _$input,
174
+ _$inputBtns,
175
+ _$filterText: $el.find(c('.filter-text')),
176
+ })
177
+ }
178
+ _initLogger() {
179
+ const cfg = this.config
180
+ let maxLogNum = cfg.get('maxLogNum')
181
+ maxLogNum = maxLogNum === 'infinite' ? 0 : +maxLogNum
182
+
183
+ const $level = this._$control.find(c('.level'))
184
+ const logger = new LunaConsole(this._$logs.get(0), {
185
+ asyncRender: cfg.get('asyncRender'),
186
+ maxNum: maxLogNum,
187
+ showHeader: cfg.get('displayExtraInfo'),
188
+ unenumerable: cfg.get('displayUnenumerable'),
189
+ accessGetter: cfg.get('displayGetterVal'),
190
+ lazyEvaluation: cfg.get('lazyEvaluation'),
191
+ })
192
+
193
+ logger.on('optionChange', (name, val) => {
194
+ switch (name) {
195
+ case 'level':
196
+ $level.each(function () {
197
+ const $this = $(this)
198
+ const level = $this.data('level')
199
+ const isMatch = level === val || (level === 'all' && isArr(val))
200
+
201
+ $this[isMatch ? 'addClass' : 'rmClass'](c('active'))
202
+ })
203
+ break
204
+ }
205
+ })
206
+
207
+ if (cfg.get('overrideConsole')) this.overrideConsole()
208
+
209
+ this._logger = logger
210
+ }
211
+ _exposeLogger() {
212
+ const logger = this._logger
213
+ const methods = ['html'].concat(CONSOLE_METHOD)
214
+
215
+ methods.forEach(
216
+ (name) =>
217
+ (this[name] = (...args) => {
218
+ logger[name](...args)
219
+ this.emit(name, ...args)
220
+
221
+ return this
222
+ })
223
+ )
224
+ }
225
+ _bindEvent() {
226
+ const container = this._container
227
+ const $input = this._$input
228
+ const $inputBtns = this._$inputBtns
229
+ const $control = this._$control
230
+
231
+ const logger = this._logger
232
+ const config = this.config
233
+
234
+ $control
235
+ .on('click', c('.clear-console'), () => logger.clear(true))
236
+ .on('click', c('.level'), function () {
237
+ let level = $(this).data('level')
238
+ if (level === 'all') {
239
+ level = ['verbose', 'info', 'warning', 'error']
240
+ }
241
+ logger.setOption('level', level)
242
+ })
243
+ .on('click', c('.filter'), () => {
244
+ LunaModal.prompt('Filter').then((filter) => {
245
+ if (isNull(filter)) return
246
+ this.filter(filter)
247
+ })
248
+ })
249
+ .on('click', c('.copy'), () => {
250
+ this._selectedLog.copy()
251
+ container.notify('Copied', { icon: 'success' })
252
+ })
253
+
254
+ $inputBtns
255
+ .on('click', c('.cancel'), () => this._hideInput())
256
+ .on('click', c('.execute'), () => {
257
+ const jsInput = $input.val().trim()
258
+ if (jsInput === '') return
259
+
260
+ logger.evaluate(jsInput)
261
+ $input.val('').get(0).blur()
262
+ this._hideInput()
263
+ })
264
+
265
+ $input.on('focusin', () => this._showInput())
266
+
267
+ logger.on('insert', (log) => {
268
+ const autoShow = log.type === 'error' && config.get('displayIfErr')
269
+
270
+ if (autoShow) container.showTool('console').show()
271
+ })
272
+
273
+ logger.on('select', (log) => {
274
+ this._selectedLog = log
275
+ $control.find(c('.icon-copy')).rmClass(c('icon-disabled'))
276
+ })
277
+
278
+ logger.on('deselect', () => {
279
+ this._selectedLog = null
280
+ $control.find(c('.icon-copy')).addClass(c('icon-disabled'))
281
+ })
282
+
283
+ container.on('show', this._handleShow)
284
+ }
285
+ _hideInput() {
286
+ this._$inputContainer.rmClass(c('active'))
287
+ this._$inputBtns.css('display', 'none')
288
+ }
289
+ _showInput() {
290
+ this._$inputContainer.addClass(c('active'))
291
+ this._$inputBtns.css('display', 'flex')
292
+ }
293
+ _rmCfg() {
294
+ const cfg = this.config
295
+
296
+ const settings = this._container.get('settings')
297
+ if (!settings) return
298
+
299
+ settings
300
+ .remove(cfg, 'asyncRender')
301
+ .remove(cfg, 'jsExecution')
302
+ .remove(cfg, 'catchGlobalErr')
303
+ .remove(cfg, 'overrideConsole')
304
+ .remove(cfg, 'displayExtraInfo')
305
+ .remove(cfg, 'displayUnenumerable')
306
+ .remove(cfg, 'displayGetterVal')
307
+ .remove(cfg, 'lazyEvaluation')
308
+ .remove(cfg, 'displayIfErr')
309
+ .remove(cfg, 'maxLogNum')
310
+ .remove(upperFirst(this.name))
311
+ }
312
+ _initCfg() {
313
+ const container = this._container
314
+
315
+ const cfg = (this.config = Settings.createCfg(this.name, {
316
+ asyncRender: true,
317
+ catchGlobalErr: true,
318
+ jsExecution: true,
319
+ overrideConsole: true,
320
+ displayExtraInfo: false,
321
+ displayUnenumerable: true,
322
+ displayGetterVal: true,
323
+ lazyEvaluation: true,
324
+ displayIfErr: false,
325
+ maxLogNum: 'infinite',
326
+ }))
327
+
328
+ this._enableJsExecution(cfg.get('jsExecution'))
329
+ if (cfg.get('catchGlobalErr')) this.catchGlobalErr()
330
+
331
+ cfg.on('change', (key, val) => {
332
+ const logger = this._logger
333
+ switch (key) {
334
+ case 'asyncRender':
335
+ return logger.setOption('asyncRender', val)
336
+ case 'jsExecution':
337
+ return this._enableJsExecution(val)
338
+ case 'catchGlobalErr':
339
+ return val ? this.catchGlobalErr() : this.ignoreGlobalErr()
340
+ case 'overrideConsole':
341
+ return val ? this.overrideConsole() : this.restoreConsole()
342
+ case 'maxLogNum':
343
+ return logger.setOption('maxNum', val === 'infinite' ? 0 : +val)
344
+ case 'displayExtraInfo':
345
+ return logger.setOption('showHeader', val)
346
+ case 'displayUnenumerable':
347
+ return logger.setOption('unenumerable', val)
348
+ case 'displayGetterVal':
349
+ return logger.setOption('accessGetter', val)
350
+ case 'lazyEvaluation':
351
+ return logger.setOption('lazyEvaluation', val)
352
+ }
353
+ })
354
+
355
+ const settings = container.get('settings')
356
+ if (!settings) return
357
+
358
+ settings
359
+ .text(upperFirst(this.name))
360
+ .switch(cfg, 'asyncRender', 'Asynchronous Rendering')
361
+ .switch(cfg, 'jsExecution', 'Enable JavaScript Execution')
362
+ .switch(cfg, 'catchGlobalErr', 'Catch Global Errors')
363
+ .switch(cfg, 'overrideConsole', 'Override Console')
364
+ .switch(cfg, 'displayIfErr', 'Auto Display If Error Occurs')
365
+ .switch(cfg, 'displayExtraInfo', 'Display Extra Information')
366
+ .switch(cfg, 'displayUnenumerable', 'Display Unenumerable Properties')
367
+ .switch(cfg, 'displayGetterVal', 'Access Getter Value')
368
+ .switch(cfg, 'lazyEvaluation', 'Lazy Evaluation')
369
+ .select(cfg, 'maxLogNum', 'Max Log Number', [
370
+ 'infinite',
371
+ '250',
372
+ '125',
373
+ '100',
374
+ '50',
375
+ '10',
376
+ ])
377
+ .separator()
378
+ }
379
+ }
380
+
381
+ const CONSOLE_METHOD = [
382
+ 'log',
383
+ 'error',
384
+ 'info',
385
+ 'warn',
386
+ 'dir',
387
+ 'time',
388
+ 'timeLog',
389
+ 'timeEnd',
390
+ 'clear',
391
+ 'table',
392
+ 'assert',
393
+ 'count',
394
+ 'countReset',
395
+ 'debug',
396
+ 'group',
397
+ 'groupCollapsed',
398
+ 'groupEnd',
399
+ ]
src/Console/Console.scss ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #console {
5
+ padding-top: 40px;
6
+ padding-bottom: 24px;
7
+ width: 100%;
8
+ height: 100%;
9
+ &.js-input-hidden {
10
+ padding-bottom: 0;
11
+ }
12
+ .control {
13
+ padding: 10px 10px 10px 35px;
14
+ @include control();
15
+ .icon-clear {
16
+ padding-right: 0px;
17
+ left: 0;
18
+ }
19
+ .icon-copy {
20
+ right: 0;
21
+ }
22
+ .icon-filter {
23
+ right: 23px;
24
+ }
25
+ .level {
26
+ cursor: pointer;
27
+ font-size: $font-size-s;
28
+ height: 20px;
29
+ display: inline-block;
30
+ margin: 0 2px;
31
+ padding: 0 4px;
32
+ line-height: 20px;
33
+ transition: background-color $anim-duration, color $anim-duration;
34
+ &.active {
35
+ background: var(--highlight);
36
+ color: var(--select-foreground);
37
+ }
38
+ }
39
+ .filter-text {
40
+ white-space: nowrap;
41
+ position: absolute;
42
+ line-height: 20px;
43
+ max-width: 80px;
44
+ overflow: hidden;
45
+ right: 55px;
46
+ font-size: $font-size;
47
+ text-overflow: ellipsis;
48
+ }
49
+ }
50
+ .js-input {
51
+ pointer-events: none;
52
+ position: absolute;
53
+ z-index: 100;
54
+ left: 0;
55
+ bottom: 0;
56
+ width: 100%;
57
+ border-top: 1px solid var(--border);
58
+ height: 24px;
59
+ .icon-arrow-right {
60
+ line-height: 23px;
61
+ color: var(--accent);
62
+ position: absolute;
63
+ left: 10px;
64
+ top: 0;
65
+ z-index: 10;
66
+ }
67
+ &.active {
68
+ height: 100%;
69
+ padding-top: 40px;
70
+ padding-bottom: 40px;
71
+ border-top: none;
72
+ .icon-arrow-right {
73
+ display: none;
74
+ }
75
+ textarea {
76
+ overflow: auto;
77
+ padding-left: 10px;
78
+ }
79
+ }
80
+ .buttons {
81
+ display: none;
82
+ position: absolute;
83
+ left: 0;
84
+ bottom: 0;
85
+ width: 100%;
86
+ height: 40px;
87
+ color: var(--primary);
88
+ background: var(--darker-background);
89
+ font-size: $font-size-s;
90
+ border-top: 1px solid var(--border);
91
+ .button {
92
+ pointer-events: all;
93
+ cursor: pointer;
94
+ flex: 1;
95
+ text-align: center;
96
+ border-right: 1px solid var(--border);
97
+ height: 40px;
98
+ line-height: 40px;
99
+ transition: background-color $anim-duration, color $anim-duration;
100
+ &:last-child {
101
+ border-right: none;
102
+ }
103
+ &:active {
104
+ color: var(--select-foreground);
105
+ background: var(--highlight);
106
+ }
107
+ }
108
+ }
109
+ textarea {
110
+ overflow: hidden;
111
+ pointer-events: all;
112
+ padding: 3px 10px;
113
+ padding-left: 25px;
114
+ outline: none;
115
+ border: none;
116
+ font-size: $font-size;
117
+ width: 100%;
118
+ height: 100%;
119
+ user-select: text;
120
+ resize: none;
121
+ color: var(--primary);
122
+ background: var(--background);
123
+ }
124
+ }
125
+ }
126
+
127
+ .safe-area #console {
128
+ @include safe-area(padding-bottom, 24px);
129
+ &.js-input-hidden {
130
+ padding-bottom: 0;
131
+ }
132
+ .js-input {
133
+ @include safe-area(height, 24px);
134
+ &.active {
135
+ height: 100%;
136
+ @include safe-area(padding-bottom, 40px);
137
+ }
138
+ .buttons {
139
+ @include safe-area(height, 40px);
140
+ .button {
141
+ @include safe-area(height, 40px);
142
+ }
143
+ }
144
+ }
145
+ }
src/DevTools/DevTools.js ADDED
@@ -0,0 +1,408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logger from '../lib/logger'
2
+ import Tool from './Tool'
3
+ import Settings from '../Settings/Settings'
4
+ import Emitter from 'licia/Emitter'
5
+ import defaults from 'licia/defaults'
6
+ import keys from 'licia/keys'
7
+ import last from 'licia/last'
8
+ import each from 'licia/each'
9
+ import isNum from 'licia/isNum'
10
+ import nextTick from 'licia/nextTick'
11
+ import $ from 'licia/$'
12
+ import toNum from 'licia/toNum'
13
+ import extend from 'licia/extend'
14
+ import isStr from 'licia/isStr'
15
+ import theme from 'licia/theme'
16
+ import upperFirst from 'licia/upperFirst'
17
+ import startWith from 'licia/startWith'
18
+ import ready from 'licia/ready'
19
+ import pointerEvent from 'licia/pointerEvent'
20
+ import evalCss from '../lib/evalCss'
21
+ import emitter from '../lib/emitter'
22
+ import { isDarkTheme } from '../lib/themes'
23
+ import LunaNotification from 'luna-notification'
24
+ import LunaModal from 'luna-modal'
25
+ import LunaTab from 'luna-tab'
26
+ import {
27
+ classPrefix as c,
28
+ eventClient,
29
+ hasSafeArea,
30
+ safeStorage,
31
+ } from '../lib/util'
32
+
33
+ export default class DevTools extends Emitter {
34
+ constructor($container, { defaults = {}, inline = false } = {}) {
35
+ super()
36
+
37
+ this._defCfg = extend(
38
+ {
39
+ transparency: 1,
40
+ displaySize: 80,
41
+ theme: 'System preference',
42
+ },
43
+ defaults
44
+ )
45
+
46
+ this._style = evalCss(require('./DevTools.scss'))
47
+
48
+ this.$container = $container
49
+ this._isShow = false
50
+ this._opacity = 1
51
+ this._tools = {}
52
+ this._isResizing = false
53
+ this._resizeTimer = null
54
+ this._resizeStartY = 0
55
+ this._resizeStartSize = 0
56
+ this._inline = inline
57
+
58
+ this._initTpl()
59
+ this._initTab()
60
+ this._initNotification()
61
+ this._initModal()
62
+
63
+ ready(() => this._checkSafeArea())
64
+ this._bindEvent()
65
+ }
66
+ show() {
67
+ this._isShow = true
68
+
69
+ this._$el.show()
70
+ this._tab.updateSlider()
71
+
72
+ // Need a delay after show to enable transition effect.
73
+ setTimeout(() => {
74
+ this._$el.css('opacity', this._opacity)
75
+ }, 50)
76
+
77
+ this.emit('show')
78
+
79
+ return this
80
+ }
81
+ hide() {
82
+ if (this._inline) {
83
+ return
84
+ }
85
+
86
+ this._isShow = false
87
+ this.emit('hide')
88
+
89
+ this._$el.css({ opacity: 0 })
90
+ setTimeout(() => this._$el.hide(), 300)
91
+
92
+ return this
93
+ }
94
+ toggle() {
95
+ return this._isShow ? this.hide() : this.show()
96
+ }
97
+ add(tool) {
98
+ const tab = this._tab
99
+
100
+ if (!(tool instanceof Tool)) {
101
+ const { init, show, hide, destroy } = new Tool()
102
+ defaults(tool, { init, show, hide, destroy })
103
+ }
104
+
105
+ const name = tool.name
106
+ if (!name) {
107
+ return logger.error('You must specify a name for a tool')
108
+ }
109
+
110
+ if (this._tools[name]) {
111
+ return logger.warn(`Tool ${name} already exists`)
112
+ }
113
+
114
+ const id = name.replace(/\s+/g, '-')
115
+ this._$tools.prepend(`<div id="${c(id)}" class="${c(id + ' tool')}"></div>`)
116
+ tool.init(this._$tools.find(`.${c(id)}.${c('tool')}`), this)
117
+ tool.active = false
118
+ this._tools[name] = tool
119
+
120
+ if (name === 'settings') {
121
+ tab.append({
122
+ id: name,
123
+ title: name,
124
+ })
125
+ } else {
126
+ tab.insert(tab.length - 1, {
127
+ id: name,
128
+ title: name,
129
+ })
130
+ }
131
+
132
+ return this
133
+ }
134
+ remove(name) {
135
+ const tools = this._tools
136
+
137
+ if (!tools[name]) return logger.warn(`Tool ${name} doesn't exist`)
138
+
139
+ this._tab.remove(name)
140
+
141
+ const tool = tools[name]
142
+ delete tools[name]
143
+ if (tool.active) {
144
+ const toolKeys = keys(tools)
145
+ if (toolKeys.length > 0) this.showTool(tools[last(toolKeys)].name)
146
+ }
147
+ tool.destroy()
148
+
149
+ return this
150
+ }
151
+ removeAll() {
152
+ each(this._tools, (tool) => this.remove(tool.name))
153
+
154
+ return this
155
+ }
156
+ get(name) {
157
+ const tool = this._tools[name]
158
+
159
+ if (tool) return tool
160
+ }
161
+ showTool(name) {
162
+ if (this._curTool === name) {
163
+ return this
164
+ }
165
+ this._curTool = name
166
+
167
+ const tools = this._tools
168
+
169
+ const tool = tools[name]
170
+ if (!tool) return
171
+
172
+ let lastTool = {}
173
+
174
+ each(tools, (tool) => {
175
+ if (tool.active) {
176
+ lastTool = tool
177
+ tool.active = false
178
+ tool.hide()
179
+ }
180
+ })
181
+
182
+ tool.active = true
183
+ tool.show()
184
+
185
+ this._tab.select(name)
186
+
187
+ this.emit('showTool', name, lastTool)
188
+
189
+ return this
190
+ }
191
+ initCfg(settings) {
192
+ const cfg = (this.config = Settings.createCfg('dev-tools', this._defCfg))
193
+
194
+ this._setTransparency(cfg.get('transparency'))
195
+ this._setDisplaySize(cfg.get('displaySize'))
196
+ this._setTheme(cfg.get('theme'))
197
+
198
+ cfg.on('change', (key, val) => {
199
+ switch (key) {
200
+ case 'transparency':
201
+ return this._setTransparency(val)
202
+ case 'displaySize':
203
+ return this._setDisplaySize(val)
204
+ case 'theme':
205
+ return this._setTheme(val)
206
+ }
207
+ })
208
+
209
+ settings
210
+ .separator()
211
+ .select(cfg, 'theme', 'Theme', [
212
+ 'System preference',
213
+ ...keys(evalCss.getThemes()),
214
+ ])
215
+
216
+ if (!this._inline) {
217
+ settings
218
+ .range(cfg, 'transparency', 'Transparency', {
219
+ min: 0.2,
220
+ max: 1,
221
+ step: 0.01,
222
+ })
223
+ .range(cfg, 'displaySize', 'Display Size', {
224
+ min: 40,
225
+ max: 100,
226
+ step: 1,
227
+ })
228
+ }
229
+
230
+ settings
231
+ .button('Restore defaults and reload', function () {
232
+ const store = safeStorage('local')
233
+
234
+ const data = JSON.parse(JSON.stringify(store))
235
+ each(data, (val, key) => {
236
+ if (!isStr(val)) {
237
+ return
238
+ }
239
+
240
+ if (startWith(key, 'eruda')) {
241
+ store.removeItem(key)
242
+ }
243
+ })
244
+
245
+ window.location.reload()
246
+ })
247
+ .separator()
248
+ }
249
+ notify(content, options) {
250
+ this._notification.notify(content, options)
251
+ }
252
+ destroy() {
253
+ evalCss.remove(this._style)
254
+ this.removeAll()
255
+ this._tab.destroy()
256
+ this._$el.remove()
257
+ window.removeEventListener('resize', this._checkSafeArea)
258
+ emitter.off(emitter.SCALE, this._updateTabHeight)
259
+ }
260
+ _checkSafeArea = () => {
261
+ const { $container } = this
262
+
263
+ if (hasSafeArea()) {
264
+ $container.addClass(c('safe-area'))
265
+ } else {
266
+ $container.rmClass(c('safe-area'))
267
+ }
268
+ }
269
+ _setTheme(t) {
270
+ const { $container } = this
271
+
272
+ if (t === 'System preference') {
273
+ t = upperFirst(theme.get())
274
+ }
275
+
276
+ if (isDarkTheme(t)) {
277
+ $container.addClass(c('dark'))
278
+ } else {
279
+ $container.rmClass(c('dark'))
280
+ }
281
+ evalCss.setTheme(t)
282
+ }
283
+ _setTransparency(opacity) {
284
+ if (!isNum(opacity)) return
285
+
286
+ this._opacity = opacity
287
+ if (this._isShow) this._$el.css({ opacity })
288
+ }
289
+ _setDisplaySize(height) {
290
+ if (this._inline) {
291
+ height = 100
292
+ }
293
+
294
+ if (!isNum(height)) return
295
+
296
+ this._$el.css({ height: height + '%' })
297
+ }
298
+ _initTpl() {
299
+ const $container = this.$container
300
+
301
+ $container.append(
302
+ c(`
303
+ <div class="dev-tools">
304
+ <div class="resizer"></div>
305
+ <div class="tab"></div>
306
+ <div class="tools"></div>
307
+ <div class="notification"></div>
308
+ <div class="modal"></div>
309
+ </div>
310
+ `)
311
+ )
312
+
313
+ this._$el = $container.find(c('.dev-tools'))
314
+ this._$tools = this._$el.find(c('.tools'))
315
+ }
316
+ _initTab() {
317
+ this._tab = new LunaTab(this._$el.find(c('.tab')).get(0), {
318
+ height: 40,
319
+ })
320
+ this._tab.on('select', (id) => this.showTool(id))
321
+ }
322
+ _updateTabHeight = (scale) => {
323
+ this._tab.setOption('height', 40 * scale)
324
+ nextTick(() => {
325
+ this._tab.updateSlider()
326
+ })
327
+ }
328
+ _initNotification() {
329
+ this._notification = new LunaNotification(
330
+ this._$el.find(c('.notification')).get(0),
331
+ {
332
+ position: {
333
+ x: 'center',
334
+ y: 'top',
335
+ },
336
+ }
337
+ )
338
+ }
339
+ _initModal() {
340
+ LunaModal.setContainer(this._$el.find(c('.modal')).get(0))
341
+ }
342
+ _bindEvent() {
343
+ const $resizer = this._$el.find(c('.resizer'))
344
+ const $navBar = this._$el.find(c('.nav-bar'))
345
+ const $document = $(document)
346
+
347
+ if (this._inline) {
348
+ $resizer.hide()
349
+ }
350
+
351
+ const startListener = (e) => {
352
+ e.preventDefault()
353
+ e.stopPropagation()
354
+
355
+ e = e.origEvent
356
+ this._isResizing = true
357
+ this._resizeStartSize = this.config.get('displaySize')
358
+ this._resizeStartY = eventClient('y', e)
359
+
360
+ $resizer.css('height', '100%')
361
+
362
+ $document.on(pointerEvent('move'), moveListener)
363
+ $document.on(pointerEvent('up'), endListener)
364
+ }
365
+ const moveListener = (e) => {
366
+ if (!this._isResizing) {
367
+ return
368
+ }
369
+ e.preventDefault()
370
+ e.stopPropagation()
371
+
372
+ e = e.origEvent
373
+ const deltaY =
374
+ ((this._resizeStartY - eventClient('y', e)) / window.innerHeight) * 100
375
+ let displaySize = this._resizeStartSize + deltaY
376
+ if (displaySize < 40) {
377
+ displaySize = 40
378
+ } else if (displaySize > 100) {
379
+ displaySize = 100
380
+ }
381
+ this.config.set('displaySize', toNum(displaySize.toFixed(2)))
382
+ }
383
+ const endListener = () => {
384
+ clearTimeout(this._resizeTimer)
385
+ this._isResizing = false
386
+
387
+ $resizer.css('height', 10)
388
+
389
+ $document.off(pointerEvent('move'), moveListener)
390
+ $document.off(pointerEvent('up'), endListener)
391
+ }
392
+ $resizer.css('height', 10)
393
+ $resizer.on(pointerEvent('down'), startListener)
394
+
395
+ $navBar.on('contextmenu', (e) => e.preventDefault())
396
+ this.$container.on('click', (e) => e.stopPropagation())
397
+ window.addEventListener('resize', this._checkSafeArea)
398
+
399
+ emitter.on(emitter.SCALE, this._updateTabHeight)
400
+
401
+ theme.on('change', () => {
402
+ const t = this.config.get('theme')
403
+ if (t === 'System preference') {
404
+ this._setTheme(t)
405
+ }
406
+ })
407
+ }
408
+ }
src/DevTools/DevTools.scss ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ .dev-tools {
5
+ position: absolute;
6
+ width: 100%;
7
+ height: 100%;
8
+ left: 0;
9
+ bottom: 0;
10
+ background: var(--background);
11
+ z-index: 500;
12
+ display: none;
13
+ padding-top: 40px !important;
14
+ opacity: 0;
15
+ transition: opacity $anim-duration;
16
+ border-top: 1px solid var(--border);
17
+ .resizer {
18
+ position: absolute;
19
+ width: 100%;
20
+ touch-action: none;
21
+ left: 0;
22
+ top: -8px;
23
+ cursor: row-resize;
24
+ z-index: 120;
25
+ }
26
+ .tools {
27
+ @include overflow-auto();
28
+ height: 100%;
29
+ width: 100%;
30
+ position: relative;
31
+ .tool {
32
+ @include absolute();
33
+ overflow: hidden;
34
+ display: none;
35
+ }
36
+ }
37
+ }
src/DevTools/Tool.js ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Class from 'licia/Class'
2
+
3
+ export default Class({
4
+ init($el) {
5
+ this._$el = $el
6
+ },
7
+ show() {
8
+ this._$el.show()
9
+
10
+ return this
11
+ },
12
+ hide() {
13
+ this._$el.hide()
14
+
15
+ return this
16
+ },
17
+ destroy() {
18
+ this._$el.remove()
19
+ },
20
+ })
src/Elements/CssStore.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import each from 'licia/each'
2
+ import sortKeys from 'licia/sortKeys'
3
+
4
+ function formatStyle(style) {
5
+ const ret = {}
6
+
7
+ for (let i = 0, len = style.length; i < len; i++) {
8
+ const name = style[i]
9
+
10
+ if (style[name] === 'initial') continue
11
+
12
+ ret[name] = style[name]
13
+ }
14
+
15
+ return sortStyleKeys(ret)
16
+ }
17
+
18
+ const elProto = Element.prototype
19
+
20
+ let matchesSel = function () {
21
+ return false
22
+ }
23
+
24
+ if (elProto.webkitMatchesSelector) {
25
+ matchesSel = (el, selText) => el.webkitMatchesSelector(selText)
26
+ } else if (elProto.mozMatchesSelector) {
27
+ matchesSel = (el, selText) => el.mozMatchesSelector(selText)
28
+ }
29
+
30
+ export default class CssStore {
31
+ constructor(el) {
32
+ this._el = el
33
+ }
34
+ getComputedStyle() {
35
+ const computedStyle = window.getComputedStyle(this._el)
36
+
37
+ return formatStyle(computedStyle)
38
+ }
39
+ getMatchedCSSRules() {
40
+ const ret = []
41
+
42
+ each(document.styleSheets, (styleSheet) => {
43
+ try {
44
+ // Started with version 64, Chrome does not allow cross origin script to access this property.
45
+ if (!styleSheet.cssRules) return
46
+ } catch {
47
+ return
48
+ }
49
+
50
+ each(styleSheet.cssRules, (cssRule) => {
51
+ let matchesEl = false
52
+
53
+ // Mobile safari will throw DOM Exception 12 error, need to try catch it.
54
+ try {
55
+ matchesEl = this._elMatchesSel(cssRule.selectorText)
56
+ } catch {
57
+ // No op
58
+ }
59
+
60
+ if (!matchesEl) return
61
+
62
+ ret.push({
63
+ selectorText: cssRule.selectorText,
64
+ style: formatStyle(cssRule.style),
65
+ })
66
+ })
67
+ })
68
+
69
+ return ret
70
+ }
71
+ _elMatchesSel(selText) {
72
+ return matchesSel(this._el, selText)
73
+ }
74
+ }
75
+
76
+ function sortStyleKeys(style) {
77
+ return sortKeys(style, {
78
+ comparator: (a, b) => {
79
+ const lenA = a.length
80
+ const lenB = b.length
81
+ const len = lenA > lenB ? lenB : lenA
82
+
83
+ for (let i = 0; i < len; i++) {
84
+ const codeA = a.charCodeAt(i)
85
+ const codeB = b.charCodeAt(i)
86
+ const cmpResult = cmpCode(codeA, codeB)
87
+
88
+ if (cmpResult !== 0) return cmpResult
89
+ }
90
+
91
+ if (lenA > lenB) return 1
92
+ if (lenA < lenB) return -1
93
+
94
+ return 0
95
+ },
96
+ })
97
+ }
98
+
99
+ function cmpCode(a, b) {
100
+ a = transCode(a)
101
+ b = transCode(b)
102
+
103
+ if (a > b) return 1
104
+ if (a < b) return -1
105
+ return 0
106
+ }
107
+
108
+ function transCode(code) {
109
+ // - should be placed after lowercase chars.
110
+ if (code === 45) return 123
111
+ return code
112
+ }
src/Elements/Detail.js ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import isEmpty from 'licia/isEmpty'
2
+ import lowerCase from 'licia/lowerCase'
3
+ import pick from 'licia/pick'
4
+ import toStr from 'licia/toStr'
5
+ import map from 'licia/map'
6
+ import isEl from 'licia/isEl'
7
+ import escape from 'licia/escape'
8
+ import startWith from 'licia/startWith'
9
+ import contain from 'licia/contain'
10
+ import unique from 'licia/unique'
11
+ import each from 'licia/each'
12
+ import keys from 'licia/keys'
13
+ import isNull from 'licia/isNull'
14
+ import trim from 'licia/trim'
15
+ import isFn from 'licia/isFn'
16
+ import isBool from 'licia/isBool'
17
+ import safeGet from 'licia/safeGet'
18
+ import $ from 'licia/$'
19
+ import h from 'licia/h'
20
+ import extend from 'licia/extend'
21
+ import MutationObserver from 'licia/MutationObserver'
22
+ import CssStore from './CssStore'
23
+ import Settings from '../Settings/Settings'
24
+ import LunaModal from 'luna-modal'
25
+ import LunaBoxModel from 'luna-box-model'
26
+ import chobitsu from '../lib/chobitsu'
27
+ import { formatNodeName } from './util'
28
+ import { isErudaEl, classPrefix as c } from '../lib/util'
29
+
30
+ export default class Detail {
31
+ constructor($container, devtools) {
32
+ this._$container = $container
33
+ this._devtools = devtools
34
+ this._curEl = document.documentElement
35
+ this._initObserver()
36
+ this._initCfg()
37
+ this._initTpl()
38
+ this._bindEvent()
39
+ }
40
+ show(el) {
41
+ this._curEl = el
42
+ this._rmDefComputedStyle = true
43
+ this._computedStyleSearchKeyword = ''
44
+ this._enableObserver()
45
+ this._render()
46
+ this._highlight()
47
+ }
48
+ hide = () => {
49
+ this._$container.hide()
50
+ this._disableObserver()
51
+ chobitsu.domain('Overlay').hideHighlight()
52
+ }
53
+ destroy() {
54
+ this._disableObserver()
55
+ this.restoreEventTarget()
56
+ this._rmCfg()
57
+ }
58
+ overrideEventTarget() {
59
+ const winEventProto = getWinEventProto()
60
+
61
+ const origAddEvent = (this._origAddEvent = winEventProto.addEventListener)
62
+ const origRmEvent = (this._origRmEvent = winEventProto.removeEventListener)
63
+
64
+ winEventProto.addEventListener = function (type, listener, useCapture) {
65
+ addEvent(this, type, listener, useCapture)
66
+ origAddEvent.apply(this, arguments)
67
+ }
68
+
69
+ winEventProto.removeEventListener = function (type, listener, useCapture) {
70
+ rmEvent(this, type, listener, useCapture)
71
+ origRmEvent.apply(this, arguments)
72
+ }
73
+ }
74
+ restoreEventTarget() {
75
+ const winEventProto = getWinEventProto()
76
+
77
+ if (this._origAddEvent) winEventProto.addEventListener = this._origAddEvent
78
+ if (this._origRmEvent) winEventProto.removeEventListener = this._origRmEvent
79
+ }
80
+ _highlight = (type) => {
81
+ const el = this._curEl
82
+
83
+ const highlightConfig = {
84
+ showInfo: false,
85
+ }
86
+ if (!type || type === 'all') {
87
+ extend(highlightConfig, {
88
+ showInfo: true,
89
+ contentColor: 'rgba(111, 168, 220, .66)',
90
+ paddingColor: 'rgba(147, 196, 125, .55)',
91
+ borderColor: 'rgba(255, 229, 153, .66)',
92
+ marginColor: 'rgba(246, 178, 107, .66)',
93
+ })
94
+ } else if (type === 'margin') {
95
+ highlightConfig.marginColor = 'rgba(246, 178, 107, .66)'
96
+ } else if (type === 'border') {
97
+ highlightConfig.borderColor = 'rgba(255, 229, 153, .66)'
98
+ } else if (type === 'padding') {
99
+ highlightConfig.paddingColor = 'rgba(147, 196, 125, .55)'
100
+ } else if (type === 'content') {
101
+ highlightConfig.contentColor = 'rgba(111, 168, 220, .66)'
102
+ }
103
+
104
+ const { nodeId } = chobitsu.domain('DOM').getNodeId({ node: el })
105
+ chobitsu.domain('Overlay').highlightNode({
106
+ nodeId,
107
+ highlightConfig,
108
+ })
109
+ }
110
+ _initTpl() {
111
+ const $container = this._$container
112
+
113
+ const html = `<div class="${c('control')}">
114
+ <span class="${c('icon-arrow-left back')}"></span>
115
+ <span class="${c('element-name')}"></span>
116
+ <span class="${c('icon-refresh refresh')}"></span>
117
+ </div>
118
+ <div class="${c('element')}">
119
+ <div class="${c('attributes section')}"></div>
120
+ <div class="${c('styles section')}"></div>
121
+ <div class="${c('computed-style section')}"></div>
122
+ <div class="${c('listeners section')}"></div>
123
+ </div>`
124
+
125
+ $container.html(html)
126
+
127
+ this._$elementName = $container.find(c('.element-name'))
128
+ this._$attributes = $container.find(c('.attributes'))
129
+ this._$styles = $container.find(c('.styles'))
130
+ this._$listeners = $container.find(c('.listeners'))
131
+ this._$computedStyle = $container.find(c('.computed-style'))
132
+
133
+ const boxModelContainer = h('div')
134
+ this._$boxModel = $(boxModelContainer)
135
+ this._boxModel = new LunaBoxModel(boxModelContainer)
136
+ }
137
+ _toggleAllComputedStyle() {
138
+ this._rmDefComputedStyle = !this._rmDefComputedStyle
139
+
140
+ this._render()
141
+ }
142
+ _render() {
143
+ const data = this._getData(this._curEl)
144
+ const $attributes = this._$attributes
145
+ const $elementName = this._$elementName
146
+ const $styles = this._$styles
147
+ const $computedStyle = this._$computedStyle
148
+ const $listeners = this._$listeners
149
+
150
+ $elementName.html(data.name)
151
+
152
+ let attributes = '<tr><td>Empty</td></tr>'
153
+ if (!isEmpty(data.attributes)) {
154
+ attributes = map(data.attributes, ({ name, value }) => {
155
+ return `<tr>
156
+ <td class="${c('attribute-name-color')}">${escape(name)}</td>
157
+ <td class="${c('string-color')}">${value}</td>
158
+ </tr>`
159
+ }).join('')
160
+ }
161
+ attributes = `<h2>Attributes</h2>
162
+ <div class="${c('table-wrapper')}">
163
+ <table>
164
+ <tbody>
165
+ ${attributes}
166
+ </tbody>
167
+ </table>
168
+ </div>`
169
+ $attributes.html(attributes)
170
+
171
+ let styles = ''
172
+ if (!isEmpty(data.styles)) {
173
+ const style = map(data.styles, ({ selectorText, style }) => {
174
+ style = map(style, (val, key) => {
175
+ return `<div class="${c('rule')}"><span>${escape(
176
+ key
177
+ )}</span>: ${val};</div>`
178
+ }).join('')
179
+ return `<div class="${c('style-rules')}">
180
+ <div>${escape(selectorText)} {</div>
181
+ ${style}
182
+ <div>}</div>
183
+ </div>`
184
+ }).join('')
185
+ styles = `<h2>Styles</h2>
186
+ <div class="${c('style-wrapper')}">
187
+ ${style}
188
+ </div>`
189
+ $styles.html(styles).show()
190
+ } else {
191
+ $styles.hide()
192
+ }
193
+
194
+ let computedStyle = ''
195
+ if (data.computedStyle) {
196
+ let toggleButton = c(`<div class="btn toggle-all-computed-style">
197
+ <span class="icon-expand"></span>
198
+ </div>`)
199
+ if (data.rmDefComputedStyle) {
200
+ toggleButton = c(`<div class="btn toggle-all-computed-style">
201
+ <span class="icon-compress"></span>
202
+ </div>`)
203
+ }
204
+
205
+ computedStyle = `<h2>
206
+ Computed Style
207
+ ${toggleButton}
208
+ <div class="${c('btn computed-style-search')}">
209
+ <span class="${c('icon-filter')}"></span>
210
+ </div>
211
+ ${
212
+ data.computedStyleSearchKeyword
213
+ ? `<div class="${c('btn filter-text')}">${escape(
214
+ data.computedStyleSearchKeyword
215
+ )}</div>`
216
+ : ''
217
+ }
218
+ </h2>
219
+ <div class="${c('box-model')}"></div>
220
+ <div class="${c('table-wrapper')}">
221
+ <table>
222
+ <tbody>
223
+ ${map(data.computedStyle, (val, key) => {
224
+ return `<tr>
225
+ <td class="${c('key')}">${escape(key)}</td>
226
+ <td>${val}</td>
227
+ </tr>`
228
+ }).join('')}
229
+ </tbody>
230
+ </table>
231
+ </div>`
232
+
233
+ $computedStyle.html(computedStyle).show()
234
+ this._boxModel.setOption('element', this._curEl)
235
+ $computedStyle.find(c('.box-model')).append(this._$boxModel.get(0))
236
+ } else {
237
+ $computedStyle.text('').hide()
238
+ }
239
+
240
+ let listeners = ''
241
+ if (data.listeners) {
242
+ listeners = map(data.listeners, (listeners, key) => {
243
+ listeners = map(listeners, ({ useCapture, listenerStr }) => {
244
+ return `<li ${useCapture ? `class="${c('capture')}"` : ''}>${escape(
245
+ listenerStr
246
+ )}</li>`
247
+ }).join('')
248
+ return `<div class="${c('listener')}">
249
+ <div class="${c('listener-type')}">${escape(key)}</div>
250
+ <ul class="${c('listener-content')}">
251
+ ${listeners}
252
+ </ul>
253
+ </div>`
254
+ }).join('')
255
+ listeners = `<h2>Event Listeners</h2>
256
+ <div class="${c('listener-wrapper')}">
257
+ ${listeners}
258
+ </div>`
259
+ $listeners.html(listeners).show()
260
+ } else {
261
+ $listeners.hide()
262
+ }
263
+
264
+ this._$container.show()
265
+ }
266
+ _getData(el) {
267
+ const ret = {}
268
+
269
+ const cssStore = new CssStore(el)
270
+
271
+ const { className, id, attributes, tagName } = el
272
+
273
+ ret.computedStyleSearchKeyword = this._computedStyleSearchKeyword
274
+ ret.attributes = formatAttr(attributes)
275
+ ret.name = formatNodeName({ tagName, id, className, attributes })
276
+
277
+ const events = el.erudaEvents
278
+ if (events && keys(events).length !== 0) ret.listeners = events
279
+
280
+ if (needNoStyle(tagName)) {
281
+ return ret
282
+ }
283
+
284
+ let computedStyle = cssStore.getComputedStyle()
285
+
286
+ const styles = cssStore.getMatchedCSSRules()
287
+ styles.unshift(getInlineStyle(el.style))
288
+ styles.forEach((style) => processStyleRules(style.style))
289
+ ret.styles = styles
290
+
291
+ if (this._rmDefComputedStyle) {
292
+ computedStyle = rmDefComputedStyle(computedStyle, styles)
293
+ }
294
+ ret.rmDefComputedStyle = this._rmDefComputedStyle
295
+ const computedStyleSearchKeyword = lowerCase(ret.computedStyleSearchKeyword)
296
+ if (computedStyleSearchKeyword) {
297
+ computedStyle = pick(computedStyle, (val, property) => {
298
+ return (
299
+ contain(property, computedStyleSearchKeyword) ||
300
+ contain(val, computedStyleSearchKeyword)
301
+ )
302
+ })
303
+ }
304
+ processStyleRules(computedStyle)
305
+ ret.computedStyle = computedStyle
306
+
307
+ return ret
308
+ }
309
+ _bindEvent() {
310
+ const devtools = this._devtools
311
+
312
+ this._$container
313
+ .on('click', c('.toggle-all-computed-style'), () =>
314
+ this._toggleAllComputedStyle()
315
+ )
316
+ .on('click', c('.computed-style-search'), () => {
317
+ LunaModal.prompt('Filter').then((filter) => {
318
+ if (isNull(filter)) return
319
+ filter = trim(filter)
320
+ this._computedStyleSearchKeyword = filter
321
+ this._render()
322
+ })
323
+ })
324
+ .on('click', '.eruda-listener-content', function () {
325
+ const text = $(this).text()
326
+ const sources = devtools.get('sources')
327
+
328
+ if (sources) {
329
+ sources.set('js', text)
330
+ devtools.showTool('sources')
331
+ }
332
+ })
333
+ .on('click', c('.element-name'), () => {
334
+ const sources = devtools.get('sources')
335
+
336
+ if (sources) {
337
+ sources.set('object', this._curEl)
338
+ devtools.showTool('sources')
339
+ }
340
+ })
341
+ .on('click', c('.back'), this.hide)
342
+ .on('click', c('.refresh'), () => {
343
+ this._render()
344
+ devtools.notify('Refreshed', { icon: 'success' })
345
+ })
346
+
347
+ this._boxModel.on('highlight', this._highlight)
348
+ }
349
+ _initObserver() {
350
+ this._observer = new MutationObserver((mutations) => {
351
+ each(mutations, (mutation) => this._handleMutation(mutation))
352
+ })
353
+ }
354
+ _enableObserver() {
355
+ this._observer.observe(document.documentElement, {
356
+ attributes: true,
357
+ childList: true,
358
+ subtree: true,
359
+ })
360
+ }
361
+ _disableObserver() {
362
+ this._observer.disconnect()
363
+ }
364
+ _handleMutation(mutation) {
365
+ if (isErudaEl(mutation.target)) return
366
+
367
+ if (mutation.type === 'attributes') {
368
+ if (mutation.target !== this._curEl) return
369
+ this._render()
370
+ }
371
+ }
372
+ _rmCfg() {
373
+ const cfg = this.config
374
+
375
+ const settings = this._devtools.get('settings')
376
+
377
+ if (!settings) return
378
+
379
+ settings
380
+ .remove(cfg, 'overrideEventTarget')
381
+ .remove(cfg, 'observeElement')
382
+ .remove('Elements')
383
+ }
384
+ _initCfg() {
385
+ const cfg = (this.config = Settings.createCfg('elements', {
386
+ overrideEventTarget: true,
387
+ }))
388
+
389
+ if (cfg.get('overrideEventTarget')) this.overrideEventTarget()
390
+
391
+ cfg.on('change', (key, val) => {
392
+ switch (key) {
393
+ case 'overrideEventTarget':
394
+ return val ? this.overrideEventTarget() : this.restoreEventTarget()
395
+ }
396
+ })
397
+
398
+ const settings = this._devtools.get('settings')
399
+ if (!settings) return
400
+
401
+ settings
402
+ .text('Elements')
403
+ .switch(cfg, 'overrideEventTarget', 'Catch Event Listeners')
404
+
405
+ settings.separator()
406
+ }
407
+ }
408
+
409
+ function processStyleRules(style) {
410
+ each(style, (val, key) => (style[key] = processStyleRule(val)))
411
+ }
412
+
413
+ const formatAttr = (attributes) =>
414
+ map(attributes, (attr) => {
415
+ let { value } = attr
416
+ const { name } = attr
417
+ value = escape(value)
418
+
419
+ const isLink =
420
+ (name === 'src' || name === 'href') && !startWith(value, 'data')
421
+ if (isLink) value = wrapLink(value)
422
+ if (name === 'style') value = processStyleRule(value)
423
+
424
+ return { name, value }
425
+ })
426
+
427
+ const regColor = /rgba?\((.*?)\)/g
428
+ const regCssUrl = /url\("?(.*?)"?\)/g
429
+
430
+ function processStyleRule(val) {
431
+ // For css custom properties, val is unable to retrieved.
432
+ val = toStr(val)
433
+
434
+ return val
435
+ .replace(
436
+ regColor,
437
+ '<span class="eruda-style-color" style="background-color: $&"></span>$&'
438
+ )
439
+ .replace(regCssUrl, (match, url) => `url("${wrapLink(url)}")`)
440
+ }
441
+
442
+ function getInlineStyle(style) {
443
+ const ret = {
444
+ selectorText: 'element.style',
445
+ style: {},
446
+ }
447
+
448
+ for (let i = 0, len = style.length; i < len; i++) {
449
+ const s = style[i]
450
+
451
+ ret.style[s] = style[s]
452
+ }
453
+
454
+ return ret
455
+ }
456
+
457
+ function rmDefComputedStyle(computedStyle, styles) {
458
+ const ret = {}
459
+
460
+ let keepStyles = ['display', 'width', 'height']
461
+ each(styles, (style) => {
462
+ keepStyles = keepStyles.concat(keys(style.style))
463
+ })
464
+ keepStyles = unique(keepStyles)
465
+
466
+ each(computedStyle, (val, key) => {
467
+ if (!contain(keepStyles, key)) return
468
+
469
+ ret[key] = val
470
+ })
471
+
472
+ return ret
473
+ }
474
+
475
+ const NO_STYLE_TAG = ['script', 'style', 'meta', 'title', 'link', 'head']
476
+
477
+ const needNoStyle = (tagName) => {
478
+ NO_STYLE_TAG.indexOf(tagName.toLowerCase()) > -1
479
+ }
480
+
481
+ const wrapLink = (link) => `<a href="${link}" target="_blank">${link}</a>`
482
+
483
+ function addEvent(el, type, listener, useCapture = false) {
484
+ if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
485
+
486
+ const events = (el.erudaEvents = el.erudaEvents || {})
487
+
488
+ events[type] = events[type] || []
489
+ events[type].push({
490
+ listener: listener,
491
+ listenerStr: listener.toString(),
492
+ useCapture: useCapture,
493
+ })
494
+ }
495
+
496
+ function rmEvent(el, type, listener, useCapture = false) {
497
+ if (!isEl(el) || !isFn(listener) || !isBool(useCapture)) return
498
+
499
+ const events = el.erudaEvents
500
+
501
+ if (!(events && events[type])) return
502
+
503
+ const listeners = events[type]
504
+
505
+ for (let i = 0, len = listeners.length; i < len; i++) {
506
+ if (listeners[i].listener === listener) {
507
+ listeners.splice(i, 1)
508
+ break
509
+ }
510
+ }
511
+
512
+ if (listeners.length === 0) delete events[type]
513
+ if (keys(events).length === 0) delete el.erudaEvents
514
+ }
515
+
516
+ const getWinEventProto = () => {
517
+ return safeGet(window, 'EventTarget.prototype') || window.Node.prototype
518
+ }
src/Elements/Elements.js ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import $ from 'licia/$'
3
+ import isEl from 'licia/isEl'
4
+ import nextTick from 'licia/nextTick'
5
+ import Emitter from 'licia/Emitter'
6
+ import map from 'licia/map'
7
+ import MediaQuery from 'licia/MediaQuery'
8
+ import isEmpty from 'licia/isEmpty'
9
+ import toNum from 'licia/toNum'
10
+ import copy from 'licia/copy'
11
+ import isMobile from 'licia/isMobile'
12
+ import isShadowRoot from 'licia/isShadowRoot'
13
+ import LunaDomViewer from 'luna-dom-viewer'
14
+ import { isErudaEl, classPrefix as c, isChobitsuEl } from '../lib/util'
15
+ import evalCss from '../lib/evalCss'
16
+ import Detail from './Detail'
17
+ import chobitsu from '../lib/chobitsu'
18
+ import emitter from '../lib/emitter'
19
+ import { formatNodeName } from './util'
20
+
21
+ export default class Elements extends Tool {
22
+ constructor() {
23
+ super()
24
+
25
+ this._style = evalCss(require('./Elements.scss'))
26
+
27
+ this.name = 'elements'
28
+ this._selectElement = false
29
+ this._observeElement = true
30
+ this._history = []
31
+
32
+ Emitter.mixin(this)
33
+ }
34
+ init($el, container) {
35
+ super.init($el)
36
+
37
+ this._container = container
38
+
39
+ this._initTpl()
40
+ this._htmlEl = document.documentElement
41
+ this._detail = new Detail(this._$detail, container)
42
+ this.config = this._detail.config
43
+ this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
44
+ this._splitMode = this._splitMediaQuery.isMatch()
45
+ this._domViewer = new LunaDomViewer(this._$domViewer.get(0), {
46
+ node: this._htmlEl,
47
+ ignore: (node) => isErudaEl(node) || isChobitsuEl(node),
48
+ })
49
+ this._domViewer.expand()
50
+ this._bindEvent()
51
+ chobitsu.domain('Overlay').enable()
52
+
53
+ nextTick(() => this._updateHistory())
54
+ }
55
+ show() {
56
+ super.show()
57
+ this._isShow = true
58
+
59
+ if (!this._curNode) {
60
+ this.select(document.body)
61
+ } else if (this._splitMode) {
62
+ this._showDetail()
63
+ }
64
+ }
65
+ hide() {
66
+ super.hide()
67
+ this._isShow = false
68
+
69
+ chobitsu.domain('Overlay').hideHighlight()
70
+ }
71
+ select(node) {
72
+ this._domViewer.select(node)
73
+ this._setNode(node)
74
+ this.emit('change', node)
75
+ return this
76
+ }
77
+ destroy() {
78
+ super.destroy()
79
+
80
+ emitter.off(emitter.SCALE, this._updateScale)
81
+ evalCss.remove(this._style)
82
+ this._detail.destroy()
83
+ chobitsu
84
+ .domain('Overlay')
85
+ .off('inspectNodeRequested', this._inspectNodeRequested)
86
+ chobitsu.domain('Overlay').disable()
87
+ this._splitMediaQuery.removeAllListeners()
88
+ }
89
+ _updateButtons() {
90
+ const $control = this._$control
91
+ const $showDetail = $control.find(c('.show-detail'))
92
+ const $copyNode = $control.find(c('.copy-node'))
93
+ const $deleteNode = $control.find(c('.delete-node'))
94
+ const iconDisabled = c('icon-disabled')
95
+
96
+ $showDetail.addClass(iconDisabled)
97
+ $copyNode.addClass(iconDisabled)
98
+ $deleteNode.addClass(iconDisabled)
99
+
100
+ const node = this._curNode
101
+
102
+ if (!node || isShadowRoot(node)) {
103
+ return
104
+ }
105
+
106
+ if (node !== document.documentElement && node !== document.body) {
107
+ $deleteNode.rmClass(iconDisabled)
108
+ }
109
+ $copyNode.rmClass(iconDisabled)
110
+
111
+ if (node.nodeType === Node.ELEMENT_NODE) {
112
+ $showDetail.rmClass(iconDisabled)
113
+ }
114
+ }
115
+ _showDetail = () => {
116
+ if (!this._isShow || !this._curNode) {
117
+ return
118
+ }
119
+ if (this._curNode.nodeType === Node.ELEMENT_NODE) {
120
+ this._detail.show(this._curNode)
121
+ } else {
122
+ this._detail.show(this._curNode.parentNode || this._curNode.host)
123
+ }
124
+ }
125
+ _initTpl() {
126
+ const $el = this._$el
127
+
128
+ $el.html(
129
+ c(`<div class="elements">
130
+ <div class="control">
131
+ <span class="icon icon-select select"></span>
132
+ <span class="icon icon-eye show-detail"></span>
133
+ <span class="icon icon-copy copy-node"></span>
134
+ <span class="icon icon-delete delete-node"></span>
135
+ </div>
136
+ <div class="dom-viewer-container">
137
+ <div class="dom-viewer"></div>
138
+ </div>
139
+ <div class="crumbs"></div>
140
+ </div>
141
+ <div class="detail"></div>`)
142
+ )
143
+
144
+ this._$detail = $el.find(c('.detail'))
145
+ this._$domViewer = $el.find(c('.dom-viewer'))
146
+ this._$control = $el.find(c('.control'))
147
+ this._$crumbs = $el.find(c('.crumbs'))
148
+ }
149
+ _renderCrumbs() {
150
+ const crumbs = getCrumbs(this._curNode)
151
+ let html = ''
152
+ if (!isEmpty(crumbs)) {
153
+ html = map(crumbs, ({ text, idx }) => {
154
+ return `<li class="${c('crumb')}" data-idx="${idx}">${text}</div></li>`
155
+ }).join('')
156
+ }
157
+ this._$crumbs.html(html)
158
+ }
159
+ _back = () => {
160
+ if (this._curNode === this._htmlEl) return
161
+
162
+ const parentQueue = this._curParentQueue
163
+ let parent = parentQueue.shift()
164
+
165
+ while (!isElExist(parent)) {
166
+ parent = parentQueue.shift()
167
+ }
168
+
169
+ this.set(parent)
170
+ }
171
+ _bindEvent() {
172
+ const self = this
173
+
174
+ this._$el.on('click', c('.crumb'), function () {
175
+ let idx = toNum($(this).data('idx'))
176
+ let node = self._curNode
177
+
178
+ while (idx-- && node.parentElement) {
179
+ node = node.parentElement
180
+ }
181
+
182
+ if (isElExist(node)) {
183
+ self.select(node)
184
+ }
185
+ })
186
+
187
+ this._$control
188
+ .on('click', c('.select'), this._toggleSelect)
189
+ .on('click', c('.show-detail'), this._showDetail)
190
+ .on('click', c('.copy-node'), this._copyNode)
191
+ .on('click', c('.delete-node'), this._deleteNode)
192
+
193
+ this._domViewer.on('select', this._setNode).on('deselect', this._back)
194
+
195
+ chobitsu
196
+ .domain('Overlay')
197
+ .on('inspectNodeRequested', this._inspectNodeRequested)
198
+
199
+ this._splitMediaQuery.on('match', () => {
200
+ this._splitMode = true
201
+ this._showDetail()
202
+ })
203
+ this._splitMediaQuery.on('unmatch', () => {
204
+ this._splitMode = false
205
+ this._detail.hide()
206
+ })
207
+
208
+ emitter.on(emitter.SCALE, this._updateScale)
209
+ }
210
+ _updateScale = (scale) => {
211
+ this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
212
+ }
213
+ _deleteNode = () => {
214
+ const node = this._curNode
215
+
216
+ if (node.parentNode) {
217
+ node.parentNode.removeChild(node)
218
+ }
219
+ }
220
+ _copyNode = () => {
221
+ const node = this._curNode
222
+
223
+ if (node.nodeType === Node.ELEMENT_NODE) {
224
+ copy(node.outerHTML)
225
+ } else {
226
+ copy(node.nodeValue)
227
+ }
228
+
229
+ this._container.notify('Copied', { icon: 'success' })
230
+ }
231
+ _toggleSelect = () => {
232
+ this._$el.find(c('.select')).toggleClass(c('active'))
233
+ this._selectElement = !this._selectElement
234
+
235
+ if (this._selectElement) {
236
+ chobitsu.domain('Overlay').setInspectMode({
237
+ mode: 'searchForNode',
238
+ highlightConfig: {
239
+ showInfo: !isMobile(),
240
+ showRulers: false,
241
+ showAccessibilityInfo: !isMobile(),
242
+ showExtensionLines: false,
243
+ contrastAlgorithm: 'aa',
244
+ contentColor: 'rgba(111, 168, 220, .66)',
245
+ paddingColor: 'rgba(147, 196, 125, .55)',
246
+ borderColor: 'rgba(255, 229, 153, .66)',
247
+ marginColor: 'rgba(246, 178, 107, .66)',
248
+ },
249
+ })
250
+ this._container.hide()
251
+ } else {
252
+ chobitsu.domain('Overlay').setInspectMode({
253
+ mode: 'none',
254
+ })
255
+ chobitsu.domain('Overlay').hideHighlight()
256
+ }
257
+ }
258
+ _inspectNodeRequested = ({ backendNodeId }) => {
259
+ this._container.show()
260
+ this._toggleSelect()
261
+ try {
262
+ const { node } = chobitsu.domain('DOM').getNode({ nodeId: backendNodeId })
263
+ this.select(node)
264
+ } catch {
265
+ // No op
266
+ }
267
+ }
268
+ _setNode = (node) => {
269
+ if (node === this._curNode) return
270
+
271
+ this._curNode = node
272
+ this._renderCrumbs()
273
+
274
+ const parentQueue = []
275
+
276
+ let parent = node.parentNode
277
+ while (parent) {
278
+ parentQueue.push(parent)
279
+ parent = parent.parentNode
280
+ }
281
+ this._curParentQueue = parentQueue
282
+
283
+ if (this._splitMode) {
284
+ this._showDetail()
285
+ }
286
+ this._updateButtons()
287
+ this._updateHistory()
288
+ }
289
+ _updateHistory() {
290
+ const console = this._container.get('console')
291
+ if (!console) return
292
+
293
+ const history = this._history
294
+ history.unshift(this._curNode)
295
+ if (history.length > 5) history.pop()
296
+ for (let i = 0; i < 5; i++) {
297
+ console.setGlobal(`$${i}`, history[i])
298
+ }
299
+ }
300
+ }
301
+
302
+ const isElExist = (val) => isEl(val) && val.parentNode
303
+
304
+ function getCrumbs(el) {
305
+ const ret = []
306
+ let i = 0
307
+
308
+ while (el) {
309
+ ret.push({
310
+ text: formatNodeName(el, { noAttr: true }),
311
+ idx: i++,
312
+ })
313
+
314
+ if (isShadowRoot(el)) {
315
+ el = el.host
316
+ }
317
+ if (!el.parentElement && isShadowRoot(el.parentNode)) {
318
+ el = el.parentNode
319
+ } else {
320
+ el = el.parentElement
321
+ }
322
+ }
323
+
324
+ return ret.reverse()
325
+ }
src/Elements/Elements.scss ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #elements {
5
+ .elements {
6
+ @include absolute();
7
+ padding-top: 40px;
8
+ padding-bottom: 24px;
9
+ font-size: 14px;
10
+ }
11
+ .control {
12
+ padding: 10px 0;
13
+ @include control();
14
+ .icon-eye {
15
+ right: 0;
16
+ }
17
+ .icon-copy {
18
+ right: 23px;
19
+ }
20
+ .icon-delete {
21
+ right: 46px;
22
+ }
23
+ }
24
+ .dom-viewer-container {
25
+ @include overflow-auto();
26
+ height: 100%;
27
+ padding: 5px 0;
28
+ }
29
+ .crumbs {
30
+ @include absolute(100%, 24px);
31
+ top: initial;
32
+ line-height: 24px;
33
+ bottom: 0;
34
+ border-top: 1px solid var(--border);
35
+ background: var(--darker-background);
36
+ color: var(--primary);
37
+ font-size: $font-size-s;
38
+ white-space: nowrap;
39
+ overflow: hidden;
40
+ text-overflow: ellipsis;
41
+ li {
42
+ cursor: pointer;
43
+ padding: 0 7px;
44
+ display: inline-block;
45
+ &:hover,
46
+ &:last-child {
47
+ background: var(--highlight);
48
+ }
49
+ }
50
+ .icon-arrow-right {
51
+ font-size: $font-size-s;
52
+ position: relative;
53
+ top: 1px;
54
+ }
55
+ }
56
+ .detail {
57
+ @include absolute();
58
+ z-index: 10;
59
+ padding-top: 40px;
60
+ display: none;
61
+ background: var(--background);
62
+ .control {
63
+ padding: 10px 35px;
64
+ .element-name {
65
+ font-size: $font-size-s;
66
+ overflow: hidden;
67
+ white-space: nowrap;
68
+ text-overflow: ellipsis;
69
+ width: 100%;
70
+ display: inline-block;
71
+ }
72
+ .icon-arrow-left {
73
+ left: 0;
74
+ }
75
+ .icon-refresh {
76
+ right: 0;
77
+ }
78
+ }
79
+ .element {
80
+ @include overflow-auto(y);
81
+ height: 100%;
82
+ }
83
+ }
84
+ .section {
85
+ border-bottom: 1px solid var(--border);
86
+ color: var(--foreground);
87
+ margin: 10px 0;
88
+ h2 {
89
+ color: var(--primary);
90
+ background: var(--darker-background);
91
+ border-top: 1px solid var(--border);
92
+ padding: $padding;
93
+ line-height: 18px;
94
+ font-size: $font-size;
95
+ transition: background-color $anim-duration;
96
+ @include right-btn();
97
+ &.active-effect {
98
+ cursor: pointer;
99
+ }
100
+ &.active-effect:active {
101
+ background: var(--highlight);
102
+ color: var(--select-foreground);
103
+ }
104
+ }
105
+ }
106
+ .attributes {
107
+ font-size: $font-size-s;
108
+ a {
109
+ color: var(--link-color);
110
+ }
111
+ .table-wrapper {
112
+ @include overflow-auto(x);
113
+ }
114
+ table {
115
+ td {
116
+ padding: 5px 10px;
117
+ }
118
+ }
119
+ }
120
+ .text-content {
121
+ background: #fff;
122
+ .content {
123
+ @include overflow-auto(x);
124
+ padding: $padding;
125
+ }
126
+ }
127
+ .style-color {
128
+ position: relative;
129
+ top: 1px;
130
+ width: 10px;
131
+ height: 10px;
132
+ border-radius: 50%;
133
+ margin-right: 2px;
134
+ border: 1px solid var(--border);
135
+ display: inline-block;
136
+ }
137
+ .box-model {
138
+ @include overflow-auto(x);
139
+ padding: $padding;
140
+ text-align: center;
141
+ border-bottom: 1px solid var(--color);
142
+ }
143
+ .computed-style {
144
+ font-size: $font-size-s;
145
+ a {
146
+ color: var(--link-color);
147
+ }
148
+ .table-wrapper {
149
+ @include overflow-auto(y);
150
+ max-height: 200px;
151
+ border-top: 1px solid var(--border);
152
+ }
153
+ table {
154
+ td {
155
+ padding: 5px 10px;
156
+ &.key {
157
+ white-space: nowrap;
158
+ color: var(--var-color);
159
+ }
160
+ }
161
+ }
162
+ }
163
+ .styles {
164
+ font-size: $font-size-s;
165
+ .style-wrapper {
166
+ padding: $padding;
167
+ .style-rules {
168
+ border: 1px solid var(--border);
169
+ padding: $padding;
170
+ margin-bottom: 10px;
171
+ .rule {
172
+ padding-left: 2em;
173
+ word-break: break-all;
174
+ a {
175
+ color: var(--link-color);
176
+ }
177
+ span {
178
+ color: var(--var-color);
179
+ }
180
+ }
181
+ &:last-child {
182
+ margin-bottom: 0;
183
+ }
184
+ }
185
+ }
186
+ }
187
+ .listeners {
188
+ font-size: $font-size-s;
189
+ .listener-wrapper {
190
+ padding: $padding;
191
+ .listener {
192
+ margin-bottom: 10px;
193
+ overflow: hidden;
194
+ border: 1px solid var(--border);
195
+ .listener-type {
196
+ padding: $padding;
197
+ background: var(--darker-background);
198
+ color: var(--primary);
199
+ }
200
+ .listener-content {
201
+ li {
202
+ @include overflow-auto(x);
203
+ padding: $padding;
204
+ border-top: none;
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ .safe-area #elements {
213
+ .elements {
214
+ @include safe-area(padding-bottom, 24px);
215
+ }
216
+ .crumbs {
217
+ @include safe-area(height, 24px);
218
+ }
219
+ .element {
220
+ @include safe-area(padding-bottom, 0px);
221
+ }
222
+ }
223
+
224
+ @media screen and (min-width: 680px) {
225
+ #elements {
226
+ .elements {
227
+ width: 50%;
228
+ .control {
229
+ .icon-eye {
230
+ display: none;
231
+ }
232
+ .icon-copy {
233
+ right: 0;
234
+ }
235
+ .icon-delete {
236
+ right: 23px;
237
+ }
238
+ }
239
+ }
240
+ .detail {
241
+ width: 50%;
242
+ left: initial;
243
+ right: 0;
244
+ border-left: 1px solid var(--border);
245
+ .control {
246
+ padding-left: 10px;
247
+ .icon-arrow-left {
248
+ display: none;
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
src/Elements/util.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import each from 'licia/each'
2
+ import isStr from 'licia/isStr'
3
+ import isShadowRoot from 'licia/isShadowRoot'
4
+ import { classPrefix as c } from '../lib/util'
5
+
6
+ export function formatNodeName(node, { noAttr = false } = {}) {
7
+ if (node.nodeType === Node.TEXT_NODE) {
8
+ return `<span class="${c('tag-name-color')}">(text)</span>`
9
+ } else if (node.nodeType === Node.COMMENT_NODE) {
10
+ return `<span class="${c('tag-name-color')}"><!--></span>`
11
+ } else if (isShadowRoot(node)) {
12
+ return `<span class="${c('tag-name-color')}">#shadow-root</span>`
13
+ }
14
+
15
+ const { id, className, attributes } = node
16
+
17
+ let ret = `<span class="eruda-tag-name-color">${node.tagName.toLowerCase()}</span>`
18
+
19
+ if (id !== '') ret += `<span class="eruda-function-color">#${id}</span>`
20
+
21
+ if (isStr(className)) {
22
+ let classes = ''
23
+ each(className.split(/\s+/g), (val) => {
24
+ if (val.trim() === '') return
25
+ classes += `.${val}`
26
+ })
27
+ ret += `<span class="eruda-attribute-name-color">${classes}</span>`
28
+ }
29
+
30
+ if (!noAttr) {
31
+ each(attributes, (attr) => {
32
+ const name = attr.name
33
+ if (name === 'id' || name === 'class' || name === 'style') return
34
+ ret += ` <span class="eruda-attribute-name-color">${name}</span><span class="eruda-operator-color">="</span><span class="eruda-string-color">${attr.value}</span><span class="eruda-operator-color">"</span>`
35
+ })
36
+ }
37
+
38
+ return ret
39
+ }
src/EntryBtn/EntryBtn.js ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import emitter from '../lib/emitter'
2
+ import Settings from '../Settings/Settings'
3
+ import Emitter from 'licia/Emitter'
4
+ import $ from 'licia/$'
5
+ import nextTick from 'licia/nextTick'
6
+ import orientation from 'licia/orientation'
7
+ import pointerEvent from 'licia/pointerEvent'
8
+ import { pxToNum, classPrefix as c, eventClient } from '../lib/util'
9
+ import evalCss from '../lib/evalCss'
10
+
11
+ const $document = $(document)
12
+
13
+ export default class EntryBtn extends Emitter {
14
+ constructor($container) {
15
+ super()
16
+
17
+ this._style = evalCss(require('./EntryBtn.scss'))
18
+
19
+ this._$container = $container
20
+ this._initTpl()
21
+ this._bindEvent()
22
+ this._registerListener()
23
+ }
24
+ hide() {
25
+ this._$el.hide()
26
+ }
27
+ show() {
28
+ this._$el.show()
29
+ }
30
+ setPos(pos) {
31
+ if (this._isOutOfRange(pos)) {
32
+ pos = this._getDefPos()
33
+ }
34
+
35
+ this._$el.css({
36
+ left: pos.x,
37
+ top: pos.y,
38
+ })
39
+
40
+ this.config.set('pos', pos)
41
+ }
42
+ getPos() {
43
+ return this.config.get('pos')
44
+ }
45
+ destroy() {
46
+ evalCss.remove(this._style)
47
+ this._unregisterListener()
48
+ this._$el.remove()
49
+ }
50
+ _isOutOfRange(pos) {
51
+ pos = pos || this.config.get('pos')
52
+ const defPos = this._getDefPos()
53
+
54
+ return (
55
+ pos.x > defPos.x + 10 || pos.x < 0 || pos.y < 0 || pos.y > defPos.y + 10
56
+ )
57
+ }
58
+ _registerListener() {
59
+ this._scaleListener = () =>
60
+ nextTick(() => {
61
+ if (this._isOutOfRange()) this._resetPos()
62
+ })
63
+ emitter.on(emitter.SCALE, this._scaleListener)
64
+ }
65
+ _unregisterListener() {
66
+ emitter.off(emitter.SCALE, this._scaleListener)
67
+ }
68
+ _initTpl() {
69
+ const $container = this._$container
70
+
71
+ $container.append(
72
+ c('<div class="entry-btn"><span class="icon-tool"></span></div>')
73
+ )
74
+ this._$el = $container.find('.eruda-entry-btn')
75
+ }
76
+ _resetPos(orientationChanged) {
77
+ const cfg = this.config
78
+ let pos = cfg.get('pos')
79
+ const defPos = this._getDefPos()
80
+
81
+ if (!cfg.get('rememberPos') || orientationChanged) {
82
+ pos = defPos
83
+ }
84
+
85
+ this.setPos(pos)
86
+ }
87
+ _onDragStart = (e) => {
88
+ const $el = this._$el
89
+ $el.addClass(c('active'))
90
+
91
+ this._isClick = true
92
+ e = e.origEvent
93
+ this._startX = eventClient('x', e)
94
+ this._oldX = pxToNum($el.css('left'))
95
+ this._oldY = pxToNum($el.css('top'))
96
+ this._startY = eventClient('y', e)
97
+ $document.on(pointerEvent('move'), this._onDragMove)
98
+ $document.on(pointerEvent('up'), this._onDragEnd)
99
+ }
100
+ _onDragMove = (e) => {
101
+ const btnSize = this._$el.get(0).offsetWidth
102
+ const maxWidth = this._$container.get(0).offsetWidth
103
+ const maxHeight = this._$container.get(0).offsetHeight
104
+
105
+ e = e.origEvent
106
+ const deltaX = eventClient('x', e) - this._startX
107
+ const deltaY = eventClient('y', e) - this._startY
108
+ if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) {
109
+ this._isClick = false
110
+ }
111
+ let newX = this._oldX + deltaX
112
+ let newY = this._oldY + deltaY
113
+ if (newX < 0) {
114
+ newX = 0
115
+ } else if (newX > maxWidth - btnSize) {
116
+ newX = maxWidth - btnSize
117
+ }
118
+ if (newY < 0) {
119
+ newY = 0
120
+ } else if (newY > maxHeight - btnSize) {
121
+ newY = maxHeight - btnSize
122
+ }
123
+ this._$el.css({
124
+ left: newX,
125
+ top: newY,
126
+ })
127
+ }
128
+ _onDragEnd = (e) => {
129
+ const $el = this._$el
130
+
131
+ if (this._isClick) {
132
+ this.emit('click')
133
+ }
134
+
135
+ this._onDragMove(e)
136
+ $document.off(pointerEvent('move'), this._onDragMove)
137
+ $document.off(pointerEvent('up'), this._onDragEnd)
138
+
139
+ const cfg = this.config
140
+
141
+ if (cfg.get('rememberPos')) {
142
+ cfg.set('pos', {
143
+ x: pxToNum($el.css('left')),
144
+ y: pxToNum($el.css('top')),
145
+ })
146
+ }
147
+
148
+ $el.rmClass('eruda-active')
149
+ }
150
+ _bindEvent() {
151
+ const $el = this._$el
152
+
153
+ $el.on(pointerEvent('down'), this._onDragStart)
154
+
155
+ orientation.on('change', () => this._resetPos(true))
156
+ window.addEventListener('resize', () => this._resetPos())
157
+ }
158
+ initCfg(settings) {
159
+ const cfg = (this.config = Settings.createCfg('entry-button', {
160
+ rememberPos: true,
161
+ pos: this._getDefPos(),
162
+ }))
163
+
164
+ settings.switch(cfg, 'rememberPos', 'Remember Entry Button Position')
165
+
166
+ this._resetPos()
167
+ }
168
+ _getDefPos() {
169
+ const minWidth = this._$el.get(0).offsetWidth + 10
170
+
171
+ return {
172
+ x: window.innerWidth - minWidth,
173
+ y: window.innerHeight - minWidth,
174
+ }
175
+ }
176
+ }
src/EntryBtn/EntryBtn.scss ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .container {
2
+ .entry-btn {
3
+ touch-action: none;
4
+ width: 40px;
5
+ height: 40px;
6
+ display: flex;
7
+ background: #000;
8
+ opacity: 0.3;
9
+ border-radius: 10px;
10
+ position: relative;
11
+ z-index: 1000;
12
+ transition: opacity 0.3s;
13
+ color: #fff;
14
+ font-size: 25px;
15
+ align-items: center;
16
+ justify-content: center;
17
+ &.active,
18
+ &:active {
19
+ opacity: 0.8;
20
+ }
21
+ }
22
+ }
src/Info/Info.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import defInfo from './defInfo'
3
+ import each from 'licia/each'
4
+ import isFn from 'licia/isFn'
5
+ import isUndef from 'licia/isUndef'
6
+ import cloneDeep from 'licia/cloneDeep'
7
+ import evalCss from '../lib/evalCss'
8
+ import map from 'licia/map'
9
+ import escape from 'licia/escape'
10
+ import copy from 'licia/copy'
11
+ import $ from 'licia/$'
12
+ import { classPrefix as c } from '../lib/util'
13
+
14
+ export default class Info extends Tool {
15
+ constructor() {
16
+ super()
17
+
18
+ this._style = evalCss(require('./Info.scss'))
19
+
20
+ this.name = 'info'
21
+ this._infos = []
22
+ }
23
+ init($el, container) {
24
+ super.init($el)
25
+ this._container = container
26
+
27
+ this._addDefInfo()
28
+ this._bindEvent()
29
+ }
30
+ destroy() {
31
+ super.destroy()
32
+
33
+ evalCss.remove(this._style)
34
+ }
35
+ add(name, val) {
36
+ const infos = this._infos
37
+ let isUpdate = false
38
+
39
+ each(infos, (info) => {
40
+ if (name !== info.name) return
41
+
42
+ info.val = val
43
+ isUpdate = true
44
+ })
45
+
46
+ if (!isUpdate) infos.push({ name, val })
47
+
48
+ this._render()
49
+
50
+ return this
51
+ }
52
+ get(name) {
53
+ const infos = this._infos
54
+
55
+ if (isUndef(name)) {
56
+ return cloneDeep(infos)
57
+ }
58
+
59
+ let result
60
+
61
+ each(infos, (info) => {
62
+ if (name === info.name) result = info.val
63
+ })
64
+
65
+ return result
66
+ }
67
+ remove(name) {
68
+ const infos = this._infos
69
+
70
+ for (let i = infos.length - 1; i >= 0; i--) {
71
+ if (infos[i].name === name) infos.splice(i, 1)
72
+ }
73
+
74
+ this._render()
75
+
76
+ return this
77
+ }
78
+ clear() {
79
+ this._infos = []
80
+
81
+ this._render()
82
+
83
+ return this
84
+ }
85
+ _addDefInfo() {
86
+ each(defInfo, (info) => this.add(info.name, info.val))
87
+ }
88
+ _render() {
89
+ const infos = []
90
+
91
+ each(this._infos, ({ name, val }) => {
92
+ if (isFn(val)) val = val()
93
+
94
+ infos.push({ name, val })
95
+ })
96
+
97
+ const html = `<ul>${map(
98
+ infos,
99
+ (info) =>
100
+ `<li><h2 class="${c('title')}">${escape(info.name)}<span class="${c(
101
+ 'icon-copy copy'
102
+ )}"></span></h2><div class="${c('content')}">${info.val}</div></li>`
103
+ ).join('')}</ul>`
104
+
105
+ this._renderHtml(html)
106
+ }
107
+ _bindEvent() {
108
+ const container = this._container
109
+
110
+ this._$el.on('click', c('.copy'), function () {
111
+ const $li = $(this).parent().parent()
112
+ const name = $li.find(c('.title')).text()
113
+ const content = $li.find(c('.content')).text()
114
+ copy(`${name}: ${content}`)
115
+ container.notify('Copied', { icon: 'success' })
116
+ })
117
+ }
118
+ _renderHtml(html) {
119
+ if (html === this._lastHtml) return
120
+ this._lastHtml = html
121
+ this._$el.html(html)
122
+ }
123
+ }
src/Info/Info.scss ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #info {
5
+ @include overflow-auto(y);
6
+ li {
7
+ margin: 10px;
8
+ border: 1px solid var(--border);
9
+ .title,
10
+ .content {
11
+ padding: $padding;
12
+ }
13
+ .title {
14
+ position: relative;
15
+ padding-bottom: 0;
16
+ color: var(--accent);
17
+ .icon-copy {
18
+ position: absolute;
19
+ right: 10px;
20
+ top: 14px;
21
+ color: var(--primary);
22
+ cursor: pointer;
23
+ transition: color $anim-duration;
24
+ &:active {
25
+ color: var(--accent);
26
+ }
27
+ }
28
+ }
29
+ .content {
30
+ margin: 0;
31
+ user-select: text;
32
+ color: var(--foreground);
33
+ font-size: $font-size-s;
34
+ word-break: break-all;
35
+ table {
36
+ width: 100%;
37
+ border-collapse: collapse;
38
+ th,
39
+ td {
40
+ border: 1px solid var(--border);
41
+ padding: 10px;
42
+ }
43
+ }
44
+ * {
45
+ user-select: text;
46
+ }
47
+ a {
48
+ color: var(--link-color);
49
+ }
50
+ }
51
+ .device-key,
52
+ .system-key {
53
+ width: 100px;
54
+ }
55
+ }
56
+ }
57
+
58
+ .safe-area #info {
59
+ @include safe-area(padding-bottom, 10px);
60
+ }
src/Info/defInfo.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import detectBrowser from 'licia/detectBrowser'
2
+ import detectOs from 'licia/detectOs'
3
+ import escape from 'licia/escape'
4
+
5
+ const browser = detectBrowser()
6
+
7
+ export default [
8
+ {
9
+ name: 'Location',
10
+ val() {
11
+ return escape(location.href)
12
+ },
13
+ },
14
+ {
15
+ name: 'User Agent',
16
+ val: navigator.userAgent,
17
+ },
18
+ {
19
+ name: 'Device',
20
+ val: [
21
+ '<table><tbody>',
22
+ `<tr><td class="eruda-device-key">screen</td><td>${screen.width} * ${screen.height}</td></tr>`,
23
+ `<tr><td>viewport</td><td>${window.innerWidth} * ${window.innerHeight}</td></tr>`,
24
+ `<tr><td>pixel ratio</td><td>${window.devicePixelRatio}</td></tr>`,
25
+ '</tbody></table>',
26
+ ].join(''),
27
+ },
28
+ {
29
+ name: 'System',
30
+ val: [
31
+ '<table><tbody>',
32
+ `<tr><td class="eruda-system-key">os</td><td>${detectOs()}</td></tr>`,
33
+ `<tr><td>browser</td><td>${
34
+ browser.name + ' ' + browser.version
35
+ }</td></tr>`,
36
+ '</tbody></table>',
37
+ ].join(''),
38
+ },
39
+ {
40
+ name: 'About',
41
+ val:
42
+ '<a href="https://eruda.liriliri.io" target="_blank">Eruda v' +
43
+ VERSION +
44
+ '</a>',
45
+ },
46
+ {
47
+ name: 'Backers',
48
+ val() {
49
+ return `
50
+ <a rel="noreferrer noopener" href="https://opencollective.com/eruda" target="_blank">
51
+ <img data-exclude="true" style="width: 100%;" loading="lazy" src="https://opencollective.com/eruda/backers.svg?width=${
52
+ window.innerWidth * 1.5
53
+ }&exclude=true">
54
+ </a>`
55
+ },
56
+ },
57
+ ]
src/Network/Detail.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import trim from 'licia/trim'
2
+ import isEmpty from 'licia/isEmpty'
3
+ import map from 'licia/map'
4
+ import each from 'licia/each'
5
+ import escape from 'licia/escape'
6
+ import copy from 'licia/copy'
7
+ import isJson from 'licia/isJson'
8
+ import Emitter from 'licia/Emitter'
9
+ import truncate from 'licia/truncate'
10
+ import { classPrefix as c } from '../lib/util'
11
+
12
+ export default class Detail extends Emitter {
13
+ constructor($container, devtools) {
14
+ super()
15
+ this._$container = $container
16
+ this._devtools = devtools
17
+
18
+ this._detailData = {}
19
+ this._bindEvent()
20
+ }
21
+ show(data) {
22
+ if (data.resTxt && trim(data.resTxt) === '') {
23
+ delete data.resTxt
24
+ }
25
+ if (isEmpty(data.resHeaders)) {
26
+ delete data.resHeaders
27
+ }
28
+ if (isEmpty(data.reqHeaders)) {
29
+ delete data.reqHeaders
30
+ }
31
+
32
+ let postData = ''
33
+ if (data.data) {
34
+ postData = `<pre class="${c('data')}">${escape(data.data)}</pre>`
35
+ }
36
+
37
+ let reqHeaders = '<tr><td>Empty</td></tr>'
38
+ if (data.reqHeaders) {
39
+ reqHeaders = map(data.reqHeaders, (val, key) => {
40
+ return `<tr>
41
+ <td class="${c('key')}">${escape(key)}</td>
42
+ <td>${escape(val)}</td>
43
+ </tr>`
44
+ }).join('')
45
+ }
46
+
47
+ let resHeaders = '<tr><td>Empty</td></tr>'
48
+ if (data.resHeaders) {
49
+ resHeaders = map(data.resHeaders, (val, key) => {
50
+ return `<tr>
51
+ <td class="${c('key')}">${escape(key)}</td>
52
+ <td>${escape(val)}</td>
53
+ </tr>`
54
+ }).join('')
55
+ }
56
+
57
+ let resTxt = ''
58
+ if (data.resTxt) {
59
+ let text = data.resTxt
60
+ if (text.length > MAX_RES_LEN) {
61
+ text = truncate(text, MAX_RES_LEN)
62
+ }
63
+ resTxt = `<pre class="${c('response')}">${escape(text)}</pre>`
64
+ }
65
+
66
+ const html = `<div class="${c('control')}">
67
+ <span class="${c('icon-arrow-left back')}"></span>
68
+ <span class="${c('icon-delete back')}"></span>
69
+ <span class="${c('url')}">${escape(data.url)}</span>
70
+ <span class="${c('icon-copy copy-res')}"></span>
71
+ </div>
72
+ <div class="${c('http')}">
73
+ ${postData}
74
+ <div class="${c('section')}">
75
+ <h2>Response Headers</h2>
76
+ <table class="${c('headers')}">
77
+ <tbody>
78
+ ${resHeaders}
79
+ </tbody>
80
+ </table>
81
+ </div>
82
+ <div class="${c('section')}">
83
+ <h2>Request Headers</h2>
84
+ <table class="${c('headers')}">
85
+ <tbody>
86
+ ${reqHeaders}
87
+ </tbody>
88
+ </table>
89
+ </div>
90
+ ${resTxt}
91
+ </div>`
92
+
93
+ this._$container.html(html).show()
94
+ this._detailData = data
95
+ }
96
+ hide() {
97
+ this._$container.hide()
98
+ this.emit('hide')
99
+ }
100
+ _copyRes = () => {
101
+ const detailData = this._detailData
102
+
103
+ let data = `${detailData.method} ${detailData.url} ${detailData.status}\n`
104
+ if (!isEmpty(detailData.data)) {
105
+ data += '\nRequest Data\n\n'
106
+ data += `${detailData.data}\n`
107
+ }
108
+ if (!isEmpty(detailData.reqHeaders)) {
109
+ data += '\nRequest Headers\n\n'
110
+ each(detailData.reqHeaders, (val, key) => (data += `${key}: ${val}\n`))
111
+ }
112
+ if (!isEmpty(detailData.resHeaders)) {
113
+ data += '\nResponse Headers\n\n'
114
+ each(detailData.resHeaders, (val, key) => (data += `${key}: ${val}\n`))
115
+ }
116
+ if (detailData.resTxt) {
117
+ data += `\n${detailData.resTxt}\n`
118
+ }
119
+
120
+ copy(data)
121
+ this._devtools.notify('Copied', { icon: 'success' })
122
+ }
123
+ _bindEvent() {
124
+ const devtools = this._devtools
125
+
126
+ this._$container
127
+ .on('click', c('.back'), () => this.hide())
128
+ .on('click', c('.copy-res'), this._copyRes)
129
+ .on('click', c('.http .response'), () => {
130
+ const data = this._detailData
131
+ const resTxt = data.resTxt
132
+
133
+ if (isJson(resTxt)) {
134
+ return showSources('object', resTxt)
135
+ }
136
+
137
+ switch (data.subType) {
138
+ case 'css':
139
+ return showSources('css', resTxt)
140
+ case 'html':
141
+ return showSources('html', resTxt)
142
+ case 'javascript':
143
+ return showSources('js', resTxt)
144
+ case 'json':
145
+ return showSources('object', resTxt)
146
+ }
147
+ switch (data.type) {
148
+ case 'image':
149
+ return showSources('img', data.url)
150
+ }
151
+ })
152
+
153
+ const showSources = (type, data) => {
154
+ const sources = devtools.get('sources')
155
+ if (!sources) {
156
+ return
157
+ }
158
+
159
+ sources.set(type, data)
160
+
161
+ devtools.showTool('sources')
162
+ }
163
+ }
164
+ }
165
+
166
+ const MAX_RES_LEN = 100000
src/Network/Network.js ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import $ from 'licia/$'
3
+ import ms from 'licia/ms'
4
+ import each from 'licia/each'
5
+ import map from 'licia/map'
6
+ import Detail from './Detail'
7
+ import throttle from 'licia/throttle'
8
+ import { getFileName, classPrefix as c } from '../lib/util'
9
+ import evalCss from '../lib/evalCss'
10
+ import chobitsu from '../lib/chobitsu'
11
+ import emitter from '../lib/emitter'
12
+ import LunaDataGrid from 'luna-data-grid'
13
+ import ResizeSensor from 'licia/ResizeSensor'
14
+ import MediaQuery from 'licia/MediaQuery'
15
+ import { getType } from './util'
16
+ import copy from 'licia/copy'
17
+ import extend from 'licia/extend'
18
+ import trim from 'licia/trim'
19
+ import isNull from 'licia/isNull'
20
+ import LunaModal from 'luna-modal'
21
+ import { curlStr } from './util'
22
+
23
+ export default class Network extends Tool {
24
+ constructor() {
25
+ super()
26
+
27
+ this._style = evalCss(require('./Network.scss'))
28
+
29
+ this.name = 'network'
30
+ this._requests = {}
31
+ this._selectedRequest = null
32
+ this._isRecording = true
33
+ }
34
+ init($el, container) {
35
+ super.init($el)
36
+
37
+ this._container = container
38
+ this._initTpl()
39
+ this._detail = new Detail(this._$detail, container)
40
+ this._splitMediaQuery = new MediaQuery('screen and (min-width: 680px)')
41
+ this._splitMode = this._splitMediaQuery.isMatch()
42
+ this._requestDataGrid = new LunaDataGrid(this._$requests.get(0), {
43
+ columns: [
44
+ {
45
+ id: 'name',
46
+ title: 'Name',
47
+ sortable: true,
48
+ weight: 30,
49
+ },
50
+ {
51
+ id: 'method',
52
+ title: 'Method',
53
+ sortable: true,
54
+ weight: 14,
55
+ },
56
+ {
57
+ id: 'status',
58
+ title: 'Status',
59
+ sortable: true,
60
+ weight: 14,
61
+ },
62
+ {
63
+ id: 'type',
64
+ title: 'Type',
65
+ sortable: true,
66
+ weight: 14,
67
+ },
68
+ {
69
+ id: 'size',
70
+ title: 'Size',
71
+ sortable: true,
72
+ weight: 14,
73
+ },
74
+ {
75
+ id: 'time',
76
+ title: 'Time',
77
+ sortable: true,
78
+ weight: 14,
79
+ },
80
+ ],
81
+ })
82
+ this._resizeSensor = new ResizeSensor($el.get(0))
83
+ this._bindEvent()
84
+ }
85
+ show() {
86
+ super.show()
87
+ this._updateDataGridHeight()
88
+ }
89
+ clear() {
90
+ this._requests = {}
91
+ this._requestDataGrid.clear()
92
+ }
93
+ requests() {
94
+ const ret = []
95
+ each(this._requests, (request) => {
96
+ ret.push(request)
97
+ })
98
+ return ret
99
+ }
100
+ _updateDataGridHeight() {
101
+ const height = this._$el.offset().height - this._$control.offset().height
102
+ this._requestDataGrid.setOption({
103
+ minHeight: height,
104
+ maxHeight: height,
105
+ })
106
+ }
107
+ _reqWillBeSent = (params) => {
108
+ if (!this._isRecording) {
109
+ return
110
+ }
111
+
112
+ const request = {
113
+ name: getFileName(params.request.url),
114
+ url: params.request.url,
115
+ status: 'pending',
116
+ type: 'unknown',
117
+ subType: 'unknown',
118
+ size: 0,
119
+ data: params.request.postData,
120
+ method: params.request.method,
121
+ startTime: params.timestamp * 1000,
122
+ time: 0,
123
+ resTxt: '',
124
+ done: false,
125
+ reqHeaders: params.request.headers || {},
126
+ resHeaders: {},
127
+ }
128
+ let node
129
+ request.render = () => {
130
+ const data = {
131
+ name: request.name,
132
+ method: request.method,
133
+ status: request.status,
134
+ type: request.subType,
135
+ size: request.size,
136
+ time: request.displayTime,
137
+ }
138
+ if (node) {
139
+ node.data = data
140
+ node.render()
141
+ } else {
142
+ node = this._requestDataGrid.append(data, { selectable: true })
143
+ $(node.container).data('id', params.requestId)
144
+ }
145
+ if (request.hasErr) {
146
+ $(node.container).addClass(c('request-error'))
147
+ }
148
+ }
149
+ request.render()
150
+ this._requests[params.requestId] = request
151
+ }
152
+ _resReceivedExtraInfo = (params) => {
153
+ const request = this._requests[params.requestId]
154
+ if (!this._isRecording || !request) {
155
+ return
156
+ }
157
+
158
+ request.resHeaders = params.headers
159
+
160
+ this._updateType(request)
161
+ request.render()
162
+ }
163
+ _updateType(request) {
164
+ const contentType = request.resHeaders['content-type'] || ''
165
+ const { type, subType } = getType(contentType)
166
+ request.type = type
167
+ request.subType = subType
168
+ }
169
+ _resReceived = (params) => {
170
+ const request = this._requests[params.requestId]
171
+ if (!this._isRecording || !request) {
172
+ return
173
+ }
174
+
175
+ const { response } = params
176
+ const { status, headers } = response
177
+ request.status = status
178
+ if (status < 200 || status >= 300) {
179
+ request.hasErr = true
180
+ }
181
+ if (headers) {
182
+ request.resHeaders = headers
183
+ this._updateType(request)
184
+ }
185
+
186
+ request.render()
187
+ }
188
+ _loadingFinished = (params) => {
189
+ const request = this._requests[params.requestId]
190
+ if (!this._isRecording || !request) {
191
+ return
192
+ }
193
+
194
+ const time = params.timestamp * 1000
195
+ request.time = time - request.startTime
196
+ request.displayTime = ms(request.time)
197
+
198
+ request.size = params.encodedDataLength
199
+ request.done = true
200
+ request.resTxt = chobitsu.domain('Network').getResponseBody({
201
+ requestId: params.requestId,
202
+ }).body
203
+
204
+ request.render()
205
+ }
206
+ _loadingFailed = (params) => {
207
+ const request = this._requests[params.requestId]
208
+ if (!this._isRecording || !request) {
209
+ return
210
+ }
211
+
212
+ const time = params.timestamp * 1000
213
+ request.time = time - request.startTime
214
+ request.displayTime = ms(request.time)
215
+
216
+ request.hasErr = true
217
+ request.status = 0
218
+ request.done = true
219
+
220
+ request.render()
221
+ }
222
+ _copyCurl = () => {
223
+ const request = this._selectedRequest
224
+
225
+ copy(
226
+ curlStr({
227
+ requestMethod: request.method,
228
+ url() {
229
+ return request.url
230
+ },
231
+ requestFormData() {
232
+ return request.data
233
+ },
234
+ requestHeaders() {
235
+ const reqHeaders = request.reqHeaders || {}
236
+ extend(reqHeaders, {
237
+ 'User-Agent': navigator.userAgent,
238
+ Referer: location.href,
239
+ })
240
+
241
+ return map(reqHeaders, (value, name) => {
242
+ return {
243
+ name,
244
+ value,
245
+ }
246
+ })
247
+ },
248
+ })
249
+ )
250
+
251
+ this._container.notify('Copied', { icon: 'success' })
252
+ }
253
+ _updateButtons() {
254
+ const $control = this._$control
255
+ const $showDetail = $control.find(c('.show-detail'))
256
+ const $copyCurl = $control.find(c('.copy-curl'))
257
+ const iconDisabled = c('icon-disabled')
258
+
259
+ $showDetail.addClass(iconDisabled)
260
+ $copyCurl.addClass(iconDisabled)
261
+
262
+ if (this._selectedRequest) {
263
+ $showDetail.rmClass(iconDisabled)
264
+ $copyCurl.rmClass(iconDisabled)
265
+ }
266
+ }
267
+ _toggleRecording = () => {
268
+ this._$control.find(c('.record')).toggleClass(c('recording'))
269
+ this._isRecording = !this._isRecording
270
+ }
271
+ _showDetail = () => {
272
+ if (this._selectedRequest) {
273
+ if (this._splitMode) {
274
+ this._$network.css('width', '50%')
275
+ }
276
+ this._detail.show(this._selectedRequest)
277
+ }
278
+ }
279
+ _bindEvent() {
280
+ const $control = this._$control
281
+ const $filterText = this._$filterText
282
+ const requestDataGrid = this._requestDataGrid
283
+
284
+ const self = this
285
+
286
+ $control
287
+ .on('click', c('.clear-request'), () => this.clear())
288
+ .on('click', c('.show-detail'), this._showDetail)
289
+ .on('click', c('.copy-curl'), this._copyCurl)
290
+ .on('click', c('.record'), this._toggleRecording)
291
+ .on('click', c('.filter'), () => {
292
+ LunaModal.prompt('Filter').then((filter) => {
293
+ if (isNull(filter)) return
294
+
295
+ $filterText.text(filter)
296
+ requestDataGrid.setOption('filter', trim(filter))
297
+ })
298
+ })
299
+
300
+ requestDataGrid.on('select', (node) => {
301
+ const id = $(node.container).data('id')
302
+ const request = self._requests[id]
303
+ this._selectedRequest = request
304
+ this._updateButtons()
305
+ if (this._splitMode) {
306
+ this._showDetail()
307
+ }
308
+ })
309
+
310
+ requestDataGrid.on('deselect', () => {
311
+ this._selectedRequest = null
312
+ this._updateButtons()
313
+ this._detail.hide()
314
+ })
315
+
316
+ this._resizeSensor.addListener(
317
+ throttle(() => this._updateDataGridHeight(), 15)
318
+ )
319
+
320
+ this._splitMediaQuery.on('match', () => {
321
+ this._detail.hide()
322
+ this._splitMode = true
323
+ })
324
+ this._splitMediaQuery.on('unmatch', () => {
325
+ this._detail.hide()
326
+ this._splitMode = false
327
+ })
328
+ this._detail.on('hide', () => {
329
+ if (this._splitMode) {
330
+ this._$network.css('width', '100%')
331
+ }
332
+ })
333
+
334
+ chobitsu.domain('Network').enable()
335
+
336
+ const network = chobitsu.domain('Network')
337
+ network.on('requestWillBeSent', this._reqWillBeSent)
338
+ network.on('responseReceivedExtraInfo', this._resReceivedExtraInfo)
339
+ network.on('responseReceived', this._resReceived)
340
+ network.on('loadingFinished', this._loadingFinished)
341
+ network.on('loadingFailed', this._loadingFailed)
342
+
343
+ emitter.on(emitter.SCALE, this._updateScale)
344
+ }
345
+ _updateScale = (scale) => {
346
+ this._splitMediaQuery.setQuery(`screen and (min-width: ${680 * scale}px)`)
347
+ }
348
+ destroy() {
349
+ super.destroy()
350
+
351
+ this._resizeSensor.destroy()
352
+ evalCss.remove(this._style)
353
+ this._splitMediaQuery.removeAllListeners()
354
+
355
+ const network = chobitsu.domain('Network')
356
+ network.off('requestWillBeSent', this._reqWillBeSent)
357
+ network.off('responseReceivedExtraInfo', this._resReceivedExtraInfo)
358
+ network.off('responseReceived', this._resReceived)
359
+ network.off('loadingFinished', this._loadingFinished)
360
+
361
+ emitter.off(emitter.SCALE, this._updateScale)
362
+ }
363
+ _initTpl() {
364
+ const $el = this._$el
365
+ $el.html(
366
+ c(`<div class="network">
367
+ <div class="control">
368
+ <span class="icon-record record recording"></span>
369
+ <span class="icon-clear clear-request"></span>
370
+ <span class="icon-eye icon-disabled show-detail"></span>
371
+ <span class="icon-copy icon-disabled copy-curl"></span>
372
+ <span class="filter-text"></span>
373
+ <span class="icon-filter filter"></span>
374
+ </div>
375
+ <div class="requests"></div>
376
+ </div>
377
+ <div class="detail"></div>`)
378
+ )
379
+ this._$network = $el.find(c('.network'))
380
+ this._$detail = $el.find(c('.detail'))
381
+ this._$requests = $el.find(c('.requests'))
382
+ this._$control = $el.find(c('.control'))
383
+ this._$filterText = $el.find(c('.filter-text'))
384
+ }
385
+ }
src/Network/Network.scss ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #network {
5
+ .network {
6
+ @include absolute();
7
+ padding-top: 39px;
8
+ }
9
+ .control {
10
+ padding: 10px;
11
+ border-bottom: none;
12
+ @include control();
13
+ .title {
14
+ font-size: $font-size;
15
+ }
16
+ .icon-clear {
17
+ left: 23px;
18
+ }
19
+ .icon-eye {
20
+ right: 0;
21
+ }
22
+ .icon-copy {
23
+ right: 23px;
24
+ }
25
+ .icon-filter {
26
+ right: 46px;
27
+ }
28
+ .filter-text {
29
+ white-space: nowrap;
30
+ position: absolute;
31
+ line-height: 20px;
32
+ max-width: 80px;
33
+ overflow: hidden;
34
+ right: 88px;
35
+ font-size: $font-size;
36
+ text-overflow: ellipsis;
37
+ }
38
+ .icon-record {
39
+ left: 0;
40
+ &.recording {
41
+ color: var(--console-error-foreground);
42
+ text-shadow: 0 0 4px var(--console-error-foreground);
43
+ }
44
+ }
45
+ }
46
+ .request-error {
47
+ color: var(--console-error-foreground);
48
+ }
49
+ .luna-data-grid:focus {
50
+ .luna-data-grid-data-container {
51
+ .request-error.luna-data-grid-selected {
52
+ background: var(--console-error-background);
53
+ }
54
+ }
55
+ }
56
+ .luna-data-grid {
57
+ border-left: none;
58
+ border-right: none;
59
+ }
60
+ .detail {
61
+ @include absolute();
62
+ z-index: 10;
63
+ display: none;
64
+ padding-top: 40px;
65
+ background: var(--background);
66
+ .control {
67
+ padding: 10px 35px;
68
+ border-bottom: 1px solid var(--border);
69
+ .url {
70
+ font-size: $font-size-s;
71
+ overflow: hidden;
72
+ white-space: nowrap;
73
+ text-overflow: ellipsis;
74
+ width: 100%;
75
+ display: inline-block;
76
+ }
77
+ .icon-arrow-left {
78
+ left: 0;
79
+ }
80
+ .icon-delete {
81
+ left: 0;
82
+ display: none;
83
+ }
84
+ .icon-copy {
85
+ right: 0;
86
+ }
87
+ }
88
+ .http {
89
+ @include overflow-auto(y);
90
+ height: 100%;
91
+ .section {
92
+ border-top: 1px solid var(--border);
93
+ border-bottom: 1px solid var(--border);
94
+ margin-top: 10px;
95
+ margin-bottom: 10px;
96
+ h2 {
97
+ background: var(--darker-background);
98
+ color: var(--primary);
99
+ padding: $padding;
100
+ line-height: 18px;
101
+ font-size: $font-size;
102
+ }
103
+ table {
104
+ color: var(--foreground);
105
+ * {
106
+ user-select: text;
107
+ }
108
+ td {
109
+ font-size: $font-size-s;
110
+ padding: 5px 10px;
111
+ word-break: break-all;
112
+ }
113
+ .key {
114
+ white-space: nowrap;
115
+ font-weight: bold;
116
+ color: var(--accent);
117
+ }
118
+ }
119
+ }
120
+ .response,
121
+ .data {
122
+ user-select: text;
123
+ @include overflow-auto(x);
124
+ padding: $padding;
125
+ font-size: $font-size-s;
126
+ margin: 10px 0;
127
+ white-space: pre-wrap;
128
+ border-top: 1px solid var(--border);
129
+ color: var(--foreground);
130
+ border-bottom: 1px solid var(--border);
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ .safe-area #network {
137
+ .http {
138
+ @include safe-area(padding-bottom, 0px);
139
+ }
140
+ }
141
+
142
+ @media screen and (min-width: 680px) {
143
+ #network {
144
+ .network {
145
+ .control {
146
+ .icon-eye {
147
+ display: none;
148
+ }
149
+ .icon-copy {
150
+ right: 0;
151
+ }
152
+ .icon-filter {
153
+ right: 23px;
154
+ }
155
+ .filter-text {
156
+ right: 55px;
157
+ }
158
+ }
159
+ }
160
+ .detail {
161
+ width: 50%;
162
+ left: initial;
163
+ right: 0;
164
+ border-left: 1px solid var(--border);
165
+ .control {
166
+ .icon-arrow-left {
167
+ display: none;
168
+ }
169
+ .icon-delete {
170
+ display: block;
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
src/Network/util.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import last from 'licia/last'
2
+ import detectOs from 'licia/detectOs'
3
+ import arrToMap from 'licia/arrToMap'
4
+
5
+ export function getType(contentType) {
6
+ if (!contentType) return 'unknown'
7
+
8
+ const type = contentType.split(';')[0].split('/')
9
+
10
+ return {
11
+ type: type[0],
12
+ subType: last(type),
13
+ }
14
+ }
15
+
16
+ export function curlStr(request) {
17
+ let platform = detectOs()
18
+ if (platform === 'windows') {
19
+ platform = 'win'
20
+ }
21
+ let command = []
22
+ const ignoredHeaders = arrToMap([
23
+ 'accept-encoding',
24
+ 'host',
25
+ 'method',
26
+ 'path',
27
+ 'scheme',
28
+ 'version',
29
+ ])
30
+
31
+ function escapeStringWin(str) {
32
+ const encapsChars = /[\r\n]/.test(str) ? '^"' : '"'
33
+ return (
34
+ encapsChars +
35
+ str
36
+ .replace(/\\/g, '\\\\')
37
+ .replace(/"/g, '\\"')
38
+ .replace(/[^a-zA-Z0-9\s_\-:=+~'/.',?;()*`&]/g, '^$&')
39
+ .replace(/%(?=[a-zA-Z0-9_])/g, '%^')
40
+ .replace(/\r?\n/g, '^\n\n') +
41
+ encapsChars
42
+ )
43
+ }
44
+
45
+ function escapeStringPosix(str) {
46
+ function escapeCharacter(x) {
47
+ const code = x.charCodeAt(0)
48
+ let hexString = code.toString(16)
49
+ while (hexString.length < 4) {
50
+ hexString = '0' + hexString
51
+ }
52
+
53
+ return '\\u' + hexString
54
+ }
55
+
56
+ // eslint-disable-next-line no-control-regex
57
+ if (/[\0-\x1F\x7F-\x9F!]|'/.test(str)) {
58
+ return (
59
+ "$'" +
60
+ str
61
+ .replace(/\\/g, '\\\\')
62
+ .replace(/'/g, "\\'")
63
+ .replace(/\n/g, '\\n')
64
+ .replace(/\r/g, '\\r')
65
+ // eslint-disable-next-line no-control-regex
66
+ .replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) +
67
+ "'"
68
+ )
69
+ }
70
+ return "'" + str + "'"
71
+ }
72
+
73
+ const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix
74
+
75
+ command.push(escapeString(request.url()).replace(/[[{}\]]/g, '\\$&'))
76
+
77
+ let inferredMethod = 'GET'
78
+ const data = []
79
+ const formData = request.requestFormData()
80
+ if (formData) {
81
+ data.push('--data-raw ' + escapeString(formData))
82
+ ignoredHeaders['content-length'] = true
83
+ inferredMethod = 'POST'
84
+ }
85
+
86
+ if (request.requestMethod !== inferredMethod) {
87
+ command.push('-X ' + escapeString(request.requestMethod))
88
+ }
89
+
90
+ const requestHeaders = request.requestHeaders()
91
+ for (let i = 0; i < requestHeaders.length; i++) {
92
+ const header = requestHeaders[i]
93
+ const name = header.name.replace(/^:/, '')
94
+ if (ignoredHeaders[name.toLowerCase()]) {
95
+ continue
96
+ }
97
+ command.push('-H ' + escapeString(name + ': ' + header.value))
98
+ }
99
+ command = command.concat(data)
100
+ command.push('--compressed')
101
+
102
+ return (
103
+ 'curl ' +
104
+ command.join(
105
+ command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' '
106
+ )
107
+ )
108
+ }
src/Resources/Cookie.js ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import map from 'licia/map'
2
+ import trim from 'licia/trim'
3
+ import isNull from 'licia/isNull'
4
+ import each from 'licia/each'
5
+ import copy from 'licia/copy'
6
+ import LunaModal from 'luna-modal'
7
+ import LunaDataGrid from 'luna-data-grid'
8
+ import { setState, getState } from './util'
9
+ import chobitsu from '../lib/chobitsu'
10
+ import { classPrefix as c } from '../lib/util'
11
+
12
+ export default class Cookie {
13
+ constructor($container, devtools) {
14
+ this._$container = $container
15
+ this._devtools = devtools
16
+ this._selectedItem = null
17
+
18
+ this._initTpl()
19
+ this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
20
+ columns: [
21
+ {
22
+ id: 'key',
23
+ title: 'Key',
24
+ weight: 30,
25
+ },
26
+ {
27
+ id: 'value',
28
+ title: 'Value',
29
+ weight: 90,
30
+ },
31
+ ],
32
+ minHeight: 60,
33
+ maxHeight: 223,
34
+ })
35
+
36
+ this._bindEvent()
37
+ }
38
+ refresh() {
39
+ const $container = this._$container
40
+ const dataGrid = this._dataGrid
41
+
42
+ const { cookies } = chobitsu.domain('Network').getCookies()
43
+ const cookieData = map(cookies, ({ name, value }) => ({
44
+ key: name,
45
+ val: value,
46
+ }))
47
+
48
+ dataGrid.clear()
49
+ each(cookieData, ({ key, val }) => {
50
+ dataGrid.append(
51
+ {
52
+ key,
53
+ value: val,
54
+ },
55
+ {
56
+ selectable: true,
57
+ }
58
+ )
59
+ })
60
+
61
+ const cookieState = getState('cookie', cookieData.length)
62
+ setState($container, cookieState)
63
+ }
64
+ _initTpl() {
65
+ const $container = this._$container
66
+
67
+ $container.html(
68
+ c(`<h2 class="title">
69
+ Cookie
70
+ <div class="btn refresh-cookie">
71
+ <span class="icon-refresh"></span>
72
+ </div>
73
+ <div class="btn show-detail btn-disabled">
74
+ <span class="icon icon-eye"></span>
75
+ </div>
76
+ <div class="btn copy-cookie btn-disabled">
77
+ <span class="icon icon-copy"></span>
78
+ </div>
79
+ <div class="btn delete-cookie btn-disabled">
80
+ <span class="icon icon-delete"></span>
81
+ </div>
82
+ <div class="btn clear-cookie">
83
+ <span class="icon-clear"></span>
84
+ </div>
85
+ <div class="btn filter" data-type="cookie">
86
+ <span class="icon-filter"></span>
87
+ </div>
88
+ <div class="btn filter-text"></div>
89
+ </h2>
90
+ <div class="data-grid"></div>`)
91
+ )
92
+
93
+ this._$dataGrid = $container.find(c('.data-grid'))
94
+ this._$filterText = $container.find(c('.filter-text'))
95
+ }
96
+ _updateButtons() {
97
+ const $container = this._$container
98
+ const $showDetail = $container.find(c('.show-detail'))
99
+ const $deleteCookie = $container.find(c('.delete-cookie'))
100
+ const $copyCookie = $container.find(c('.copy-cookie'))
101
+ const btnDisabled = c('btn-disabled')
102
+
103
+ $showDetail.addClass(btnDisabled)
104
+ $deleteCookie.addClass(btnDisabled)
105
+ $copyCookie.addClass(btnDisabled)
106
+
107
+ if (this._selectedItem) {
108
+ $showDetail.rmClass(btnDisabled)
109
+ $deleteCookie.rmClass(btnDisabled)
110
+ $copyCookie.rmClass(btnDisabled)
111
+ }
112
+ }
113
+ _getVal(key) {
114
+ const { cookies } = chobitsu.domain('Network').getCookies()
115
+
116
+ for (let i = 0, len = cookies.length; i < len; i++) {
117
+ if (cookies[i].name === key) {
118
+ return cookies[i].value
119
+ }
120
+ }
121
+
122
+ return ''
123
+ }
124
+ _bindEvent() {
125
+ const devtools = this._devtools
126
+
127
+ this._$container
128
+ .on('click', c('.refresh-cookie'), () => {
129
+ devtools.notify('Refreshed', { icon: 'success' })
130
+ this.refresh()
131
+ })
132
+ .on('click', c('.clear-cookie'), () => {
133
+ chobitsu.domain('Storage').clearDataForOrigin({
134
+ storageTypes: 'cookies',
135
+ })
136
+ this.refresh()
137
+ })
138
+ .on('click', c('.delete-cookie'), () => {
139
+ const key = this._selectedItem
140
+
141
+ chobitsu.domain('Network').deleteCookies({ name: key })
142
+ this.refresh()
143
+ })
144
+ .on('click', c('.show-detail'), () => {
145
+ const key = this._selectedItem
146
+ const val = this._getVal(key)
147
+
148
+ try {
149
+ showSources('object', JSON.parse(val))
150
+ } catch {
151
+ showSources('raw', val)
152
+ }
153
+ })
154
+ .on('click', c('.copy-cookie'), () => {
155
+ const key = this._selectedItem
156
+ copy(this._getVal(key))
157
+ devtools.notify('Copied', { icon: 'success' })
158
+ })
159
+ .on('click', c('.filter'), () => {
160
+ LunaModal.prompt('Filter').then((filter) => {
161
+ if (isNull(filter)) return
162
+ filter = trim(filter)
163
+ this._filter = filter
164
+ this._$filterText.text(filter)
165
+ this._dataGrid.setOption('filter', filter)
166
+ })
167
+ })
168
+
169
+ function showSources(type, data) {
170
+ const sources = devtools.get('sources')
171
+ if (!sources) return
172
+
173
+ sources.set(type, data)
174
+
175
+ devtools.showTool('sources')
176
+
177
+ return true
178
+ }
179
+
180
+ this._dataGrid
181
+ .on('select', (node) => {
182
+ this._selectedItem = node.data.key
183
+ this._updateButtons()
184
+ })
185
+ .on('deselect', () => {
186
+ this._selectedItem = null
187
+ this._updateButtons()
188
+ })
189
+ }
190
+ }
src/Resources/Resources.js ADDED
@@ -0,0 +1,444 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import Settings from '../Settings/Settings'
3
+ import $ from 'licia/$'
4
+ import escape from 'licia/escape'
5
+ import isEmpty from 'licia/isEmpty'
6
+ import contain from 'licia/contain'
7
+ import unique from 'licia/unique'
8
+ import each from 'licia/each'
9
+ import sameOrigin from 'licia/sameOrigin'
10
+ import ajax from 'licia/ajax'
11
+ import MutationObserver from 'licia/MutationObserver'
12
+ import toArr from 'licia/toArr'
13
+ import concat from 'licia/concat'
14
+ import map from 'licia/map'
15
+ import { isErudaEl, classPrefix as c } from '../lib/util'
16
+ import evalCss from '../lib/evalCss'
17
+ import Storage from './Storage'
18
+ import Cookie from './Cookie'
19
+ import { setState, getState } from './util'
20
+
21
+ export default class Resources extends Tool {
22
+ constructor() {
23
+ super()
24
+
25
+ this._style = evalCss(require('./Resources.scss'))
26
+
27
+ this.name = 'resources'
28
+ this._hideErudaSetting = false
29
+ this._observeElement = true
30
+ }
31
+ init($el, container) {
32
+ super.init($el)
33
+
34
+ this._container = container
35
+
36
+ this._initTpl()
37
+ this._localStorage = new Storage(
38
+ this._$localStorage,
39
+ container,
40
+ this,
41
+ 'local'
42
+ )
43
+ this._sessionStorage = new Storage(
44
+ this._$sessionStorage,
45
+ container,
46
+ this,
47
+ 'session'
48
+ )
49
+ this._cookie = new Cookie(this._$cookie, container)
50
+
51
+ this._bindEvent()
52
+ this._initObserver()
53
+ this._initCfg()
54
+ }
55
+ refresh() {
56
+ return this.refreshLocalStorage()
57
+ .refreshSessionStorage()
58
+ .refreshCookie()
59
+ .refreshScript()
60
+ .refreshStylesheet()
61
+ .refreshIframe()
62
+ .refreshImage()
63
+ }
64
+ destroy() {
65
+ super.destroy()
66
+
67
+ this._localStorage.destroy()
68
+ this._sessionStorage.destroy()
69
+ this._disableObserver()
70
+ evalCss.remove(this._style)
71
+ this._rmCfg()
72
+ }
73
+ refreshScript() {
74
+ let scriptData = []
75
+
76
+ $('script').each(function () {
77
+ const src = this.src
78
+
79
+ if (src !== '') scriptData.push(src)
80
+ })
81
+
82
+ scriptData = unique(scriptData)
83
+
84
+ const scriptState = getState('script', scriptData.length)
85
+ let scriptDataHtml = '<li>Empty</li>'
86
+ if (!isEmpty(scriptData)) {
87
+ scriptDataHtml = map(scriptData, (script) => {
88
+ script = escape(script)
89
+ return `<li><a href="${script}" target="_blank" class="${c(
90
+ 'js-link'
91
+ )}">${script}</a></li>`
92
+ }).join('')
93
+ }
94
+
95
+ const scriptHtml = `<h2 class="${c('title')}">
96
+ Script
97
+ <div class="${c('btn refresh-script')}">
98
+ <span class="${c('icon-refresh')}"></span>
99
+ </div>
100
+ </h2>
101
+ <ul class="${c('link-list')}">
102
+ ${scriptDataHtml}
103
+ </ul>`
104
+
105
+ const $script = this._$script
106
+ setState($script, scriptState)
107
+ $script.html(scriptHtml)
108
+
109
+ return this
110
+ }
111
+ refreshStylesheet() {
112
+ let stylesheetData = []
113
+
114
+ $('link').each(function () {
115
+ if (this.rel !== 'stylesheet') return
116
+
117
+ stylesheetData.push(this.href)
118
+ })
119
+
120
+ stylesheetData = unique(stylesheetData)
121
+
122
+ const stylesheetState = getState('stylesheet', stylesheetData.length)
123
+ let stylesheetDataHtml = '<li>Empty</li>'
124
+ if (!isEmpty(stylesheetData)) {
125
+ stylesheetDataHtml = map(stylesheetData, (stylesheet) => {
126
+ stylesheet = escape(stylesheet)
127
+ return ` <li><a href="${stylesheet}" target="_blank" class="${c(
128
+ 'css-link'
129
+ )}">${stylesheet}</a></li>`
130
+ }).join('')
131
+ }
132
+
133
+ const stylesheetHtml = `<h2 class="${c('title')}">
134
+ Stylesheet
135
+ <div class="${c('btn refresh-stylesheet')}">
136
+ <span class="${c('icon-refresh')}"></span>
137
+ </div>
138
+ </h2>
139
+ <ul class="${c('link-list')}">
140
+ ${stylesheetDataHtml}
141
+ </ul>`
142
+
143
+ const $stylesheet = this._$stylesheet
144
+ setState($stylesheet, stylesheetState)
145
+ $stylesheet.html(stylesheetHtml)
146
+
147
+ return this
148
+ }
149
+ refreshIframe() {
150
+ let iframeData = []
151
+
152
+ $('iframe').each(function () {
153
+ const $this = $(this)
154
+ const src = $this.attr('src')
155
+
156
+ if (src) iframeData.push(src)
157
+ })
158
+
159
+ iframeData = unique(iframeData)
160
+
161
+ let iframeDataHtml = '<li>Empty</li>'
162
+ if (!isEmpty(iframeData)) {
163
+ iframeDataHtml = map(iframeData, (iframe) => {
164
+ iframe = escape(iframe)
165
+ return `<li><a href="${iframe}" target="_blank" class="${c(
166
+ 'iframe-link'
167
+ )}">${iframe}</a></li>`
168
+ }).join('')
169
+ }
170
+ const iframeHtml = `<h2 class="${c('title')}">
171
+ Iframe
172
+ <div class="${c('btn refresh-iframe')}">
173
+ <span class="${c('icon-refresh')}"></span>
174
+ </div>
175
+ </h2>
176
+ <ul class="${c('link-list')}">
177
+ ${iframeDataHtml}
178
+ </ul>`
179
+
180
+ this._$iframe.html(iframeHtml)
181
+
182
+ return this
183
+ }
184
+ refreshLocalStorage() {
185
+ this._localStorage.refresh()
186
+
187
+ return this
188
+ }
189
+ refreshSessionStorage() {
190
+ this._sessionStorage.refresh()
191
+
192
+ return this
193
+ }
194
+ refreshCookie() {
195
+ this._cookie.refresh()
196
+
197
+ return this
198
+ }
199
+ refreshImage() {
200
+ let imageData = []
201
+
202
+ const performance = (this._performance =
203
+ window.webkitPerformance || window.performance)
204
+ if (performance && performance.getEntries) {
205
+ const entries = this._performance.getEntries()
206
+ entries.forEach((entry) => {
207
+ if (entry.initiatorType === 'img' || isImg(entry.name)) {
208
+ if (contain(entry.name, 'exclude=true')) {
209
+ return
210
+ }
211
+ imageData.push(entry.name)
212
+ }
213
+ })
214
+ } else {
215
+ $('img').each(function () {
216
+ const $this = $(this)
217
+ const src = $this.attr('src')
218
+
219
+ if ($this.data('exclude') === 'true') {
220
+ return
221
+ }
222
+
223
+ imageData.push(src)
224
+ })
225
+ }
226
+
227
+ imageData = unique(imageData)
228
+ imageData.sort()
229
+
230
+ const imageState = getState('image', imageData.length)
231
+ let imageDataHtml = '<li>Empty</li>'
232
+ if (!isEmpty(imageData)) {
233
+ // prettier-ignore
234
+ imageDataHtml = map(imageData, (image) => {
235
+ return `<li class="${c('image')}">
236
+ <img src="${escape(image)}" data-exclude="true" class="${c('img-link')}"/>
237
+ </li>`
238
+ }).join('')
239
+ }
240
+
241
+ const imageHtml = `<h2 class="${c('title')}">
242
+ Image
243
+ <div class="${c('btn refresh-image')}">
244
+ <span class="${c('icon-refresh')}"></span>
245
+ </div>
246
+ </h2>
247
+ <ul class="${c('image-list')}">
248
+ ${imageDataHtml}
249
+ </ul>`
250
+
251
+ const $image = this._$image
252
+ setState($image, imageState)
253
+ $image.html(imageHtml)
254
+
255
+ return this
256
+ }
257
+ show() {
258
+ super.show()
259
+ if (this._observeElement) this._enableObserver()
260
+
261
+ return this.refresh()
262
+ }
263
+ hide() {
264
+ this._disableObserver()
265
+
266
+ return super.hide()
267
+ }
268
+ _initTpl() {
269
+ const $el = this._$el
270
+ $el.html(
271
+ c(`<div class="section local-storage"></div>
272
+ <div class="section session-storage"></div>
273
+ <div class="section cookie"></div>
274
+ <div class="section script"></div>
275
+ <div class="section stylesheet"></div>
276
+ <div class="section iframe"></div>
277
+ <div class="section image"></div>`)
278
+ )
279
+ this._$localStorage = $el.find(c('.local-storage'))
280
+ this._$sessionStorage = $el.find(c('.session-storage'))
281
+ this._$cookie = $el.find(c('.cookie'))
282
+ this._$script = $el.find(c('.script'))
283
+ this._$stylesheet = $el.find(c('.stylesheet'))
284
+ this._$iframe = $el.find(c('.iframe'))
285
+ this._$image = $el.find(c('.image'))
286
+ }
287
+ _bindEvent() {
288
+ const $el = this._$el
289
+ const container = this._container
290
+
291
+ $el
292
+ .on('click', '.eruda-refresh-script', () => {
293
+ container.notify('Refreshed', { icon: 'success' })
294
+ this.refreshScript()
295
+ })
296
+ .on('click', '.eruda-refresh-stylesheet', () => {
297
+ container.notify('Refreshed', { icon: 'success' })
298
+ this.refreshStylesheet()
299
+ })
300
+ .on('click', '.eruda-refresh-iframe', () => {
301
+ container.notify('Refreshed', { icon: 'success' })
302
+ this.refreshIframe()
303
+ })
304
+ .on('click', '.eruda-refresh-image', () => {
305
+ container.notify('Refreshed', { icon: 'success' })
306
+ this.refreshImage()
307
+ })
308
+ .on('click', '.eruda-img-link', function () {
309
+ const src = $(this).attr('src')
310
+
311
+ showSources('img', src)
312
+ })
313
+ .on('click', '.eruda-css-link', linkFactory('css'))
314
+ .on('click', '.eruda-js-link', linkFactory('js'))
315
+ .on('click', '.eruda-iframe-link', linkFactory('iframe'))
316
+
317
+ function showSources(type, data) {
318
+ const sources = container.get('sources')
319
+ if (!sources) return
320
+
321
+ sources.set(type, data)
322
+
323
+ container.showTool('sources')
324
+
325
+ return true
326
+ }
327
+
328
+ function linkFactory(type) {
329
+ return function (e) {
330
+ if (!container.get('sources')) return
331
+ e.preventDefault()
332
+
333
+ const url = $(this).attr('href')
334
+
335
+ if (type === 'iframe' || !sameOrigin(location.href, url)) {
336
+ showSources('iframe', url)
337
+ } else {
338
+ ajax({
339
+ url,
340
+ success: (data) => {
341
+ showSources(type, data)
342
+ },
343
+ dataType: 'raw',
344
+ })
345
+ }
346
+ }
347
+ }
348
+ }
349
+ _rmCfg() {
350
+ const cfg = this.config
351
+
352
+ const settings = this._container.get('settings')
353
+
354
+ if (!settings) return
355
+
356
+ settings
357
+ .remove(cfg, 'hideErudaSetting')
358
+ .remove(cfg, 'observeElement')
359
+ .remove('Resources')
360
+ }
361
+ _initCfg() {
362
+ const cfg = (this.config = Settings.createCfg('resources', {
363
+ hideErudaSetting: true,
364
+ observeElement: true,
365
+ }))
366
+
367
+ if (cfg.get('hideErudaSetting')) this._hideErudaSetting = true
368
+ if (!cfg.get('observeElement')) this._observeElement = false
369
+
370
+ cfg.on('change', (key, val) => {
371
+ switch (key) {
372
+ case 'hideErudaSetting':
373
+ this._hideErudaSetting = val
374
+ return
375
+ case 'observeElement':
376
+ this._observeElement = val
377
+ return val ? this._enableObserver() : this._disableObserver()
378
+ }
379
+ })
380
+
381
+ const settings = this._container.get('settings')
382
+ settings
383
+ .text('Resources')
384
+ .switch(cfg, 'hideErudaSetting', 'Hide Eruda Setting')
385
+ .switch(cfg, 'observeElement', 'Auto Refresh Elements')
386
+ .separator()
387
+ }
388
+ _initObserver() {
389
+ this._observer = new MutationObserver((mutations) => {
390
+ each(mutations, (mutation) => {
391
+ this._handleMutation(mutation)
392
+ })
393
+ })
394
+ }
395
+ _handleMutation(mutation) {
396
+ if (isErudaEl(mutation.target)) return
397
+
398
+ const checkEl = (el) => {
399
+ const tagName = getLowerCaseTagName(el)
400
+ switch (tagName) {
401
+ case 'script':
402
+ this.refreshScript()
403
+ break
404
+ case 'img':
405
+ this.refreshImage()
406
+ break
407
+ case 'link':
408
+ this.refreshStylesheet()
409
+ break
410
+ }
411
+ }
412
+
413
+ if (mutation.type === 'attributes') {
414
+ checkEl(mutation.target)
415
+ } else if (mutation.type === 'childList') {
416
+ checkEl(mutation.target)
417
+ let nodes = toArr(mutation.addedNodes)
418
+ nodes = concat(nodes, toArr(mutation.removedNodes))
419
+
420
+ for (const node of nodes) {
421
+ checkEl(node)
422
+ }
423
+ }
424
+ }
425
+ _enableObserver() {
426
+ this._observer.observe(document.documentElement, {
427
+ attributes: true,
428
+ childList: true,
429
+ subtree: true,
430
+ })
431
+ }
432
+ _disableObserver() {
433
+ this._observer.disconnect()
434
+ }
435
+ }
436
+
437
+ function getLowerCaseTagName(el) {
438
+ if (!el.tagName) return ''
439
+ return el.tagName.toLowerCase()
440
+ }
441
+
442
+ const regImg = /\.(jpeg|jpg|gif|png)$/
443
+
444
+ const isImg = (url) => regImg.test(url)
src/Resources/Resources.scss ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #resources {
5
+ @include overflow-auto(y);
6
+ padding: 10px;
7
+ font-size: 14px;
8
+ .section {
9
+ margin-bottom: 10px;
10
+ overflow: hidden;
11
+ border: 1px solid var(--border);
12
+ &.warn {
13
+ border: 1px solid var(--console-warn-border);
14
+ .title {
15
+ background: var(--console-warn-background);
16
+ color: var(--console-warn-foreground);
17
+ }
18
+ }
19
+ &.danger {
20
+ border: 1px solid var(--console-error-border);
21
+ .title {
22
+ background: var(--console-error-background);
23
+ color: var(--console-error-foreground);
24
+ }
25
+ }
26
+ &.local-storage,
27
+ &.session-storage,
28
+ &.cookie {
29
+ border: none;
30
+ .title {
31
+ border: 1px solid var(--border);
32
+ border-bottom: none;
33
+ }
34
+ }
35
+ }
36
+ .title {
37
+ padding: $padding;
38
+ line-height: 18px;
39
+ color: var(--primary);
40
+ background: var(--darker-background);
41
+ @include right-btn();
42
+ }
43
+ .link-list {
44
+ font-size: $font-size-s;
45
+ color: var(--foreground);
46
+ li {
47
+ padding: 10px;
48
+ word-break: break-all;
49
+ a {
50
+ color: var(--link-color) !important;
51
+ }
52
+ }
53
+ }
54
+ .image-list {
55
+ color: var(--foreground);
56
+ font-size: $font-size-s;
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ padding-left: $padding;
60
+ padding-top: $padding;
61
+ &::after {
62
+ content: '';
63
+ flex-grow: 1000;
64
+ }
65
+ li {
66
+ flex-grow: 1;
67
+ cursor: pointer;
68
+ overflow-y: hidden;
69
+ margin-right: $padding;
70
+ margin-bottom: $padding;
71
+ border: 1px solid var(--border);
72
+ &.image {
73
+ height: 100px;
74
+ font-size: 0;
75
+ }
76
+ img {
77
+ height: 100px;
78
+ min-width: 100%;
79
+ object-fit: cover;
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ .safe-area #resources {
86
+ @include safe-area(padding-bottom, 10px);
87
+ }
src/Resources/Storage.js ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import each from 'licia/each'
2
+ import isStr from 'licia/isStr'
3
+ import startWith from 'licia/startWith'
4
+ import truncate from 'licia/truncate'
5
+ import LunaModal from 'luna-modal'
6
+ import LunaDataGrid from 'luna-data-grid'
7
+ import isNull from 'licia/isNull'
8
+ import trim from 'licia/trim'
9
+ import copy from 'licia/copy'
10
+ import emitter from '../lib/emitter'
11
+ import { safeStorage, classPrefix as c } from '../lib/util'
12
+
13
+ export default class Storage {
14
+ constructor($container, devtools, resources, type) {
15
+ this._type = type
16
+ this._$container = $container
17
+ this._devtools = devtools
18
+ this._resources = resources
19
+ this._selectedItem = null
20
+ this._storeData = []
21
+
22
+ this._initTpl()
23
+ this._dataGrid = new LunaDataGrid(this._$dataGrid.get(0), {
24
+ columns: [
25
+ {
26
+ id: 'key',
27
+ title: 'Key',
28
+ weight: 30,
29
+ },
30
+ {
31
+ id: 'value',
32
+ title: 'Value',
33
+ weight: 90,
34
+ },
35
+ ],
36
+ minHeight: 60,
37
+ maxHeight: 223,
38
+ })
39
+
40
+ this._bindEvent()
41
+ }
42
+ destroy() {
43
+ emitter.off(emitter.SCALE, this._updateGridHeight)
44
+ }
45
+ refresh() {
46
+ const dataGrid = this._dataGrid
47
+
48
+ this._refreshStorage()
49
+ dataGrid.clear()
50
+
51
+ each(this._storeData, ({ key, val }) => {
52
+ dataGrid.append(
53
+ {
54
+ key,
55
+ value: val,
56
+ },
57
+ {
58
+ selectable: true,
59
+ }
60
+ )
61
+ })
62
+ }
63
+ _refreshStorage() {
64
+ const resources = this._resources
65
+
66
+ let store = safeStorage(this._type, false)
67
+
68
+ if (!store) return
69
+
70
+ const storeData = []
71
+
72
+ // Mobile safari is not able to loop through localStorage directly.
73
+ store = JSON.parse(JSON.stringify(store))
74
+
75
+ each(store, (val, key) => {
76
+ // According to issue 20, not all values are guaranteed to be string.
77
+ if (!isStr(val)) return
78
+
79
+ if (resources.config.get('hideErudaSetting')) {
80
+ if (startWith(key, 'eruda') || key === 'active-eruda') return
81
+ }
82
+
83
+ storeData.push({
84
+ key: key,
85
+ val: truncate(val, 200),
86
+ })
87
+ })
88
+
89
+ this._storeData = storeData
90
+ }
91
+ _updateButtons() {
92
+ const $container = this._$container
93
+ const $showDetail = $container.find(c('.show-detail'))
94
+ const $deleteStorage = $container.find(c('.delete-storage'))
95
+ const $copyStorage = $container.find(c('.copy-storage'))
96
+ const btnDisabled = c('btn-disabled')
97
+
98
+ $showDetail.addClass(btnDisabled)
99
+ $deleteStorage.addClass(btnDisabled)
100
+ $copyStorage.addClass(btnDisabled)
101
+
102
+ if (this._selectedItem) {
103
+ $showDetail.rmClass(btnDisabled)
104
+ $deleteStorage.rmClass(btnDisabled)
105
+ $copyStorage.rmClass(btnDisabled)
106
+ }
107
+ }
108
+ _initTpl() {
109
+ const $container = this._$container
110
+ const type = this._type
111
+
112
+ $container.html(
113
+ c(`<h2 class="title">
114
+ ${type === 'local' ? 'Local' : 'Session'} Storage
115
+ <div class="btn refresh-storage">
116
+ <span class="icon icon-refresh"></span>
117
+ </div>
118
+ <div class="btn show-detail btn-disabled">
119
+ <span class="icon icon-eye"></span>
120
+ </div>
121
+ <div class="btn copy-storage btn-disabled">
122
+ <span class="icon icon-copy"></span>
123
+ </div>
124
+ <div class="btn delete-storage btn-disabled">
125
+ <span class="icon icon-delete"></span>
126
+ </div>
127
+ <div class="btn clear-storage">
128
+ <span class="icon icon-clear"></span>
129
+ </div>
130
+ <div class="btn filter">
131
+ <span class="icon icon-filter"></span>
132
+ </div>
133
+ <div class="btn filter-text"></div>
134
+ </h2>
135
+ <div class="data-grid"></div>`)
136
+ )
137
+
138
+ this._$dataGrid = $container.find(c('.data-grid'))
139
+ this._$filterText = $container.find(c('.filter-text'))
140
+ }
141
+ _getVal(key) {
142
+ return this._type === 'local'
143
+ ? localStorage.getItem(key)
144
+ : sessionStorage.getItem(key)
145
+ }
146
+ _updateGridHeight = (scale) => {
147
+ this._dataGrid.setOption({
148
+ minHeight: 60 * scale,
149
+ maxHeight: 223 * scale,
150
+ })
151
+ }
152
+ _bindEvent() {
153
+ const type = this._type
154
+ const devtools = this._devtools
155
+
156
+ this._$container
157
+ .on('click', c('.refresh-storage'), () => {
158
+ devtools.notify('Refreshed', { icon: 'success' })
159
+ this.refresh()
160
+ })
161
+ .on('click', c('.clear-storage'), () => {
162
+ each(this._storeData, (val) => {
163
+ if (type === 'local') {
164
+ localStorage.removeItem(val.key)
165
+ } else {
166
+ sessionStorage.removeItem(val.key)
167
+ }
168
+ })
169
+ this.refresh()
170
+ })
171
+ .on('click', c('.show-detail'), () => {
172
+ const key = this._selectedItem
173
+ const val = this._getVal(key)
174
+
175
+ try {
176
+ showSources('object', JSON.parse(val))
177
+ } catch {
178
+ showSources('raw', val)
179
+ }
180
+ })
181
+ .on('click', c('.copy-storage'), () => {
182
+ const key = this._selectedItem
183
+ copy(this._getVal(key))
184
+ devtools.notify('Copied', { icon: 'success' })
185
+ })
186
+ .on('click', c('.filter'), () => {
187
+ LunaModal.prompt('Filter').then((filter) => {
188
+ if (isNull(filter)) return
189
+ filter = trim(filter)
190
+ this._$filterText.text(filter)
191
+ this._dataGrid.setOption('filter', filter)
192
+ })
193
+ })
194
+ .on('click', c('.delete-storage'), () => {
195
+ const key = this._selectedItem
196
+
197
+ if (type === 'local') {
198
+ localStorage.removeItem(key)
199
+ } else {
200
+ sessionStorage.removeItem(key)
201
+ }
202
+
203
+ this.refresh()
204
+ })
205
+
206
+ function showSources(type, data) {
207
+ const sources = devtools.get('sources')
208
+ if (!sources) return
209
+
210
+ sources.set(type, data)
211
+
212
+ devtools.showTool('sources')
213
+
214
+ return true
215
+ }
216
+
217
+ this._dataGrid
218
+ .on('select', (node) => {
219
+ this._selectedItem = node.data.key
220
+ this._updateButtons()
221
+ })
222
+ .on('deselect', () => {
223
+ this._selectedItem = null
224
+ this._updateButtons()
225
+ })
226
+
227
+ emitter.on(emitter.SCALE, this._updateGridHeight)
228
+ }
229
+ }
src/Resources/util.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { classPrefix as c } from '../lib/util'
2
+
3
+ export function setState($el, state) {
4
+ $el
5
+ .rmClass(c('ok'))
6
+ .rmClass(c('danger'))
7
+ .rmClass(c('warn'))
8
+ .addClass(c(state))
9
+ }
10
+
11
+ export function getState(type, len) {
12
+ if (len === 0) return ''
13
+
14
+ let warn = 0
15
+ let danger = 0
16
+
17
+ switch (type) {
18
+ case 'cookie':
19
+ warn = 30
20
+ danger = 60
21
+ break
22
+ case 'script':
23
+ warn = 5
24
+ danger = 10
25
+ break
26
+ case 'stylesheet':
27
+ warn = 4
28
+ danger = 8
29
+ break
30
+ case 'image':
31
+ warn = 50
32
+ danger = 100
33
+ break
34
+ }
35
+
36
+ if (len >= danger) return 'danger'
37
+ if (len >= warn) return 'warn'
38
+
39
+ return 'ok'
40
+ }
src/Settings/Settings.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import $ from 'licia/$'
3
+ import LocalStore from 'licia/LocalStore'
4
+ import uniqId from 'licia/uniqId'
5
+ import each from 'licia/each'
6
+ import filter from 'licia/filter'
7
+ import isStr from 'licia/isStr'
8
+ import contain from 'licia/contain'
9
+ import clone from 'licia/clone'
10
+ import evalCss from '../lib/evalCss'
11
+ import LunaSetting from 'luna-setting'
12
+
13
+ export default class Settings extends Tool {
14
+ constructor() {
15
+ super()
16
+
17
+ this._style = evalCss(require('./Settings.scss'))
18
+
19
+ this.name = 'settings'
20
+ this._settings = []
21
+ }
22
+ init($el) {
23
+ super.init($el)
24
+
25
+ this._setting = new LunaSetting($el.get(0))
26
+
27
+ this._bindEvent()
28
+ }
29
+ remove(config, key) {
30
+ if (isStr(config)) {
31
+ const self = this
32
+ this._$el.find('.luna-setting-item-title').each(function () {
33
+ const $this = $(this)
34
+ if ($this.text() === config) {
35
+ self._setting.remove(this.settingItem)
36
+ }
37
+ })
38
+ } else {
39
+ this._settings = filter(this._settings, (setting) => {
40
+ if (setting.config === config && setting.key === key) {
41
+ this._setting.remove(setting.item)
42
+ return false
43
+ }
44
+
45
+ return true
46
+ })
47
+ }
48
+
49
+ this._cleanSeparator()
50
+
51
+ return this
52
+ }
53
+ destroy() {
54
+ this._setting.destroy()
55
+ super.destroy()
56
+
57
+ evalCss.remove(this._style)
58
+ }
59
+ clear() {
60
+ this._settings = []
61
+ this._setting.clear()
62
+ }
63
+ switch(config, key, desc) {
64
+ const id = this._genId()
65
+
66
+ const item = this._setting.appendCheckbox(id, !!config.get(key), desc)
67
+ this._settings.push({ config, key, id, item })
68
+
69
+ return this
70
+ }
71
+ select(config, key, desc, selections) {
72
+ const id = this._genId()
73
+
74
+ const selectOptions = {}
75
+ each(selections, (selection) => (selectOptions[selection] = selection))
76
+ const item = this._setting.appendSelect(
77
+ id,
78
+ config.get(key),
79
+ '',
80
+ desc,
81
+ selectOptions
82
+ )
83
+ this._settings.push({ config, key, id, item })
84
+
85
+ return this
86
+ }
87
+ range(config, key, desc, { min = 0, max = 1, step = 0.1 }) {
88
+ const id = this._genId()
89
+
90
+ const item = this._setting.appendNumber(id, config.get(key), desc, {
91
+ max,
92
+ min,
93
+ step,
94
+ range: true,
95
+ })
96
+ this._settings.push({ config, key, min, max, step, id, item })
97
+
98
+ return this
99
+ }
100
+ button(text, handler) {
101
+ this._setting.appendButton(text, handler)
102
+
103
+ return this
104
+ }
105
+ separator() {
106
+ this._setting.appendSeparator()
107
+
108
+ return this
109
+ }
110
+ text(text) {
111
+ this._setting.appendTitle(text)
112
+
113
+ return this
114
+ }
115
+ // Merge adjacent separators
116
+ _cleanSeparator() {
117
+ const children = clone(this._$el.get(0).children)
118
+
119
+ function isSeparator(node) {
120
+ return contain(node.getAttribute('class'), 'luna-setting-item-separator')
121
+ }
122
+
123
+ for (let i = 0, len = children.length; i < len - 1; i++) {
124
+ if (isSeparator(children[i]) && isSeparator(children[i + 1])) {
125
+ $(children[i]).remove()
126
+ }
127
+ }
128
+ }
129
+ _genId() {
130
+ return uniqId('eruda-settings')
131
+ }
132
+ _getSetting(id) {
133
+ let ret
134
+
135
+ each(this._settings, (setting) => {
136
+ if (setting.id === id) ret = setting
137
+ })
138
+
139
+ return ret
140
+ }
141
+ _bindEvent() {
142
+ this._setting.on('change', (id, val) => {
143
+ const setting = this._getSetting(id)
144
+ setting.config.set(setting.key, val)
145
+ })
146
+ }
147
+ static createCfg(name, data) {
148
+ return new LocalStore('eruda-' + name, data)
149
+ }
150
+ }
src/Settings/Settings.scss ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #settings {
5
+ @include overflow-auto(y);
6
+ }
7
+
8
+ .safe-area #settings {
9
+ @include safe-area(padding-bottom, 0px);
10
+ }
src/Snippets/Snippets.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import defSnippets from './defSnippets'
3
+ import $ from 'licia/$'
4
+ import each from 'licia/each'
5
+ import escape from 'licia/escape'
6
+ import map from 'licia/map'
7
+ import remove from 'licia/remove'
8
+ import evalCss from '../lib/evalCss'
9
+ import { classPrefix as c } from '../lib/util'
10
+
11
+ export default class Snippets extends Tool {
12
+ constructor() {
13
+ super()
14
+
15
+ this._style = evalCss(require('./Snippets.scss'))
16
+
17
+ this.name = 'snippets'
18
+
19
+ this._snippets = []
20
+ }
21
+ init($el) {
22
+ super.init($el)
23
+
24
+ this._bindEvent()
25
+ this._addDefSnippets()
26
+ }
27
+ destroy() {
28
+ super.destroy()
29
+
30
+ evalCss.remove(this._style)
31
+ }
32
+ add(name, fn, desc) {
33
+ this._snippets.push({ name, fn, desc })
34
+
35
+ this._render()
36
+
37
+ return this
38
+ }
39
+ remove(name) {
40
+ remove(this._snippets, (snippet) => snippet.name === name)
41
+
42
+ this._render()
43
+
44
+ return this
45
+ }
46
+ run(name) {
47
+ const snippets = this._snippets
48
+
49
+ for (let i = 0, len = snippets.length; i < len; i++) {
50
+ if (snippets[i].name === name) this._run(i)
51
+ }
52
+
53
+ return this
54
+ }
55
+ clear() {
56
+ this._snippets = []
57
+ this._render()
58
+
59
+ return this
60
+ }
61
+ _bindEvent() {
62
+ const self = this
63
+
64
+ this._$el.on('click', '.eruda-run', function () {
65
+ const idx = $(this).data('idx')
66
+
67
+ self._run(idx)
68
+ })
69
+ }
70
+ _run(idx) {
71
+ this._snippets[idx].fn.call(null)
72
+ }
73
+ _addDefSnippets() {
74
+ each(defSnippets, (snippet) => {
75
+ this.add(snippet.name, snippet.fn, snippet.desc)
76
+ })
77
+ }
78
+ _render() {
79
+ const html = map(this._snippets, (snippet, idx) => {
80
+ return `<div class="${c('section run')}" data-idx="${idx}">
81
+ <h2 class="${c('name')}">${escape(snippet.name)}
82
+ <div class="${c('btn')}">
83
+ <span class="${c('icon-play')}"></span>
84
+ </div>
85
+ </h2>
86
+ <div class="${c('description')}">
87
+ ${escape(snippet.desc)}
88
+ </div>
89
+ </div>`
90
+ }).join('')
91
+
92
+ this._renderHtml(html)
93
+ }
94
+ _renderHtml(html) {
95
+ if (html === this._lastHtml) return
96
+ this._lastHtml = html
97
+ this._$el.html(html)
98
+ }
99
+ }
src/Snippets/Snippets.scss ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #snippets {
5
+ @include overflow-auto(y);
6
+ padding: $padding;
7
+ .section {
8
+ margin-bottom: 10px;
9
+ border: 1px solid var(--border);
10
+ overflow: hidden;
11
+ cursor: pointer;
12
+ &:active {
13
+ .name {
14
+ background: var(--highlight);
15
+ color: var(--select-foreground);
16
+ }
17
+ }
18
+ .name {
19
+ padding: $padding;
20
+ line-height: 18px;
21
+ color: var(--primary);
22
+ background: var(--darker-background);
23
+ transition: background-color $anim-duration;
24
+ .btn {
25
+ margin-left: 10px;
26
+ float: right;
27
+ text-align: center;
28
+ width: 18px;
29
+ height: 18px;
30
+ font-size: $font-size-s;
31
+ }
32
+ }
33
+ .description {
34
+ font-size: $font-size-s;
35
+ color: var(--foreground);
36
+ padding: $padding;
37
+ transition: background-color $anim-duration;
38
+ }
39
+ }
40
+ }
41
+
42
+ .safe-area #snippets {
43
+ @include safe-area(padding-bottom, 10px);
44
+ }
src/Snippets/defSnippets.js ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logger from '../lib/logger'
2
+ import emitter from '../lib/emitter'
3
+ import Url from 'licia/Url'
4
+ import now from 'licia/now'
5
+ import startWith from 'licia/startWith'
6
+ import $ from 'licia/$'
7
+ import upperFirst from 'licia/upperFirst'
8
+ import loadJs from 'licia/loadJs'
9
+ import trim from 'licia/trim'
10
+ import LunaModal from 'luna-modal'
11
+ import { isErudaEl } from '../lib/util'
12
+ import evalCss from '../lib/evalCss'
13
+
14
+ let style = null
15
+
16
+ export default [
17
+ {
18
+ name: 'Border All',
19
+ fn() {
20
+ if (style) {
21
+ evalCss.remove(style)
22
+ style = null
23
+ return
24
+ }
25
+
26
+ style = evalCss(
27
+ '* { outline: 2px dashed #707d8b; outline-offset: -3px; }',
28
+ document.head
29
+ )
30
+ },
31
+ desc: 'Add color borders to all elements',
32
+ },
33
+ {
34
+ name: 'Refresh Page',
35
+ fn() {
36
+ const url = new Url()
37
+ url.setQuery('timestamp', now())
38
+
39
+ window.location.replace(url.toString())
40
+ },
41
+ desc: 'Add timestamp to url and refresh',
42
+ },
43
+ {
44
+ name: 'Search Text',
45
+ fn() {
46
+ LunaModal.prompt('Enter the text').then((keyword) => {
47
+ if (!keyword || trim(keyword) === '') {
48
+ return
49
+ }
50
+
51
+ search(keyword)
52
+ })
53
+ },
54
+ desc: 'Highlight given text on page',
55
+ },
56
+ {
57
+ name: 'Edit Page',
58
+ fn() {
59
+ const body = document.body
60
+
61
+ body.contentEditable = body.contentEditable !== 'true'
62
+ },
63
+ desc: 'Toggle body contentEditable',
64
+ },
65
+ {
66
+ name: 'Fit Screen',
67
+ // https://achrafkassioui.com/birdview/
68
+ fn() {
69
+ const body = document.body
70
+ const html = document.documentElement
71
+ const $body = $(body)
72
+ if ($body.data('scaled')) {
73
+ window.scrollTo(0, +$body.data('scaled'))
74
+ $body.rmAttr('data-scaled')
75
+ $body.css('transform', 'none')
76
+ } else {
77
+ const documentHeight = Math.max(
78
+ body.scrollHeight,
79
+ body.offsetHeight,
80
+ html.clientHeight,
81
+ html.scrollHeight,
82
+ html.offsetHeight
83
+ )
84
+ const viewportHeight = Math.max(
85
+ document.documentElement.clientHeight,
86
+ window.innerHeight || 0
87
+ )
88
+ const scaleVal = viewportHeight / documentHeight
89
+ $body.css('transform', `scale(${scaleVal})`)
90
+ $body.data('scaled', window.scrollY)
91
+ window.scrollTo(0, documentHeight / 2 - viewportHeight / 2)
92
+ }
93
+ },
94
+ desc: 'Scale down the whole page to fit screen',
95
+ },
96
+ {
97
+ name: 'Load Vue Plugin',
98
+ fn() {
99
+ loadPlugin('vue')
100
+ },
101
+ desc: 'Vue devtools',
102
+ },
103
+ {
104
+ name: 'Load Monitor Plugin',
105
+ fn() {
106
+ loadPlugin('monitor')
107
+ },
108
+ desc: 'Display page fps, memory and dom nodes',
109
+ },
110
+ {
111
+ name: 'Load Features Plugin',
112
+ fn() {
113
+ loadPlugin('features')
114
+ },
115
+ desc: 'Browser feature detections',
116
+ },
117
+ {
118
+ name: 'Load Timing Plugin',
119
+ fn() {
120
+ loadPlugin('timing')
121
+ },
122
+ desc: 'Show performance and resource timing',
123
+ },
124
+ {
125
+ name: 'Load Code Plugin',
126
+ fn() {
127
+ loadPlugin('code')
128
+ },
129
+ desc: 'Edit and run JavaScript',
130
+ },
131
+ {
132
+ name: 'Load Benchmark Plugin',
133
+ fn() {
134
+ loadPlugin('benchmark')
135
+ },
136
+ desc: 'Run JavaScript benchmarks',
137
+ },
138
+ {
139
+ name: 'Load Geolocation Plugin',
140
+ fn() {
141
+ loadPlugin('geolocation')
142
+ },
143
+ desc: 'Test geolocation',
144
+ },
145
+ {
146
+ name: 'Load Orientation Plugin',
147
+ fn() {
148
+ loadPlugin('orientation')
149
+ },
150
+ desc: 'Test orientation api',
151
+ },
152
+ {
153
+ name: 'Load Touches Plugin',
154
+ fn() {
155
+ loadPlugin('touches')
156
+ },
157
+ desc: 'Visualize screen touches',
158
+ },
159
+ ]
160
+
161
+ evalCss(require('./searchText.scss'), document.head)
162
+
163
+ function search(text) {
164
+ const root = document.body
165
+ const regText = new RegExp(text, 'ig')
166
+
167
+ traverse(root, (node) => {
168
+ const $node = $(node)
169
+
170
+ if (!$node.hasClass('eruda-search-highlight-block')) return
171
+
172
+ return document.createTextNode($node.text())
173
+ })
174
+
175
+ traverse(root, (node) => {
176
+ if (node.nodeType !== 3) return
177
+
178
+ let val = node.nodeValue
179
+ val = val.replace(
180
+ regText,
181
+ (match) => `<span class="eruda-keyword">${match}</span>`
182
+ )
183
+ if (val === node.nodeValue) return
184
+
185
+ const $ret = $(document.createElement('div'))
186
+
187
+ $ret.html(val)
188
+ $ret.addClass('eruda-search-highlight-block')
189
+
190
+ return $ret.get(0)
191
+ })
192
+ }
193
+
194
+ function traverse(root, processor) {
195
+ const childNodes = root.childNodes
196
+
197
+ if (isErudaEl(root)) return
198
+
199
+ for (let i = 0, len = childNodes.length; i < len; i++) {
200
+ const newNode = traverse(childNodes[i], processor)
201
+ if (newNode) root.replaceChild(newNode, childNodes[i])
202
+ }
203
+
204
+ return processor(root)
205
+ }
206
+
207
+ function loadPlugin(name) {
208
+ const globalName = 'eruda' + upperFirst(name)
209
+ if (window[globalName]) return
210
+
211
+ let protocol = location.protocol
212
+ if (!startWith(protocol, 'http')) protocol = 'http:'
213
+
214
+ loadJs(
215
+ `${protocol}//cdn.jsdelivr.net/npm/eruda-${name}@${pluginVersion[name]}`,
216
+ (isLoaded) => {
217
+ if (!isLoaded || !window[globalName])
218
+ return logger.error('Fail to load plugin ' + name)
219
+
220
+ emitter.emit(emitter.ADD, window[globalName])
221
+ emitter.emit(emitter.SHOW, name)
222
+ }
223
+ )
224
+ }
225
+
226
+ const pluginVersion = {
227
+ monitor: '1.1.1',
228
+ features: '2.1.0',
229
+ timing: '2.0.1',
230
+ code: '2.2.0',
231
+ benchmark: '2.0.1',
232
+ geolocation: '2.1.0',
233
+ orientation: '2.1.1',
234
+ touches: '2.1.0',
235
+ vue: '1.1.1',
236
+ }
src/Snippets/searchText.scss ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+
3
+ .search-highlight-block {
4
+ display: inline;
5
+ .keyword {
6
+ background: var(--console-warn-background);
7
+ color: var(--console-warn-foreground);
8
+ }
9
+ }
src/Sources/Sources.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Tool from '../DevTools/Tool'
2
+ import LunaObjectViewer from 'luna-object-viewer'
3
+ import Settings from '../Settings/Settings'
4
+ import ajax from 'licia/ajax'
5
+ import each from 'licia/each'
6
+ import isStr from 'licia/isStr'
7
+ import escape from 'licia/escape'
8
+ import truncate from 'licia/truncate'
9
+ import replaceAll from 'licia/replaceAll'
10
+ import highlight from 'licia/highlight'
11
+ import LunaTextViewer from 'luna-text-viewer'
12
+ import evalCss from '../lib/evalCss'
13
+ import { classPrefix as c } from '../lib/util'
14
+
15
+ export default class Sources extends Tool {
16
+ constructor() {
17
+ super()
18
+
19
+ this._style = evalCss(require('./Sources.scss'))
20
+
21
+ this.name = 'sources'
22
+ this._showLineNum = true
23
+ }
24
+ init($el, container) {
25
+ super.init($el)
26
+
27
+ this._container = container
28
+ this._bindEvent()
29
+ this._initCfg()
30
+ }
31
+ destroy() {
32
+ super.destroy()
33
+
34
+ evalCss.remove(this._style)
35
+ this._rmCfg()
36
+ }
37
+ set(type, val) {
38
+ if (type === 'img') {
39
+ this._isFetchingData = true
40
+
41
+ const img = new Image()
42
+
43
+ const self = this
44
+
45
+ img.onload = function () {
46
+ self._isFetchingData = false
47
+ self._data = {
48
+ type: 'img',
49
+ val: {
50
+ width: this.width,
51
+ height: this.height,
52
+ src: val,
53
+ },
54
+ }
55
+
56
+ self._render()
57
+ }
58
+ img.onerror = function () {
59
+ self._isFetchingData = false
60
+ }
61
+
62
+ img.src = val
63
+
64
+ return
65
+ }
66
+
67
+ this._data = { type, val }
68
+
69
+ this._render()
70
+
71
+ return this
72
+ }
73
+ show() {
74
+ super.show()
75
+
76
+ if (!this._data && !this._isFetchingData) {
77
+ this._renderDef()
78
+ }
79
+
80
+ return this
81
+ }
82
+ _renderDef() {
83
+ if (this._html) {
84
+ this._data = {
85
+ type: 'html',
86
+ val: this._html,
87
+ }
88
+
89
+ return this._render()
90
+ }
91
+
92
+ if (this._isGettingHtml) return
93
+ this._isGettingHtml = true
94
+
95
+ ajax({
96
+ url: location.href,
97
+ success: (data) => (this._html = data),
98
+ error: () => (this._html = 'Sorry, unable to fetch source code:('),
99
+ complete: () => {
100
+ this._isGettingHtml = false
101
+ this._renderDef()
102
+ },
103
+ dataType: 'raw',
104
+ })
105
+ }
106
+ _bindEvent() {
107
+ this._container.on('showTool', (name, lastTool) => {
108
+ if (name !== this.name && lastTool.name === this.name) {
109
+ delete this._data
110
+ }
111
+ })
112
+ }
113
+ _rmCfg() {
114
+ const cfg = this.config
115
+
116
+ const settings = this._container.get('settings')
117
+
118
+ if (!settings) return
119
+
120
+ settings.remove(cfg, 'showLineNum').remove('Sources')
121
+ }
122
+ _initCfg() {
123
+ const cfg = (this.config = Settings.createCfg('sources', {
124
+ showLineNum: true,
125
+ }))
126
+
127
+ if (!cfg.get('showLineNum')) this._showLineNum = false
128
+
129
+ cfg.on('change', (key, val) => {
130
+ switch (key) {
131
+ case 'showLineNum':
132
+ this._showLineNum = val
133
+ return
134
+ }
135
+ })
136
+
137
+ const settings = this._container.get('settings')
138
+ settings
139
+ .text('Sources')
140
+ .switch(cfg, 'showLineNum', 'Show Line Numbers')
141
+ .separator()
142
+ }
143
+ _render() {
144
+ this._isInit = true
145
+
146
+ const data = this._data
147
+
148
+ switch (data.type) {
149
+ case 'html':
150
+ case 'js':
151
+ case 'css':
152
+ return this._renderCode()
153
+ case 'img':
154
+ return this._renderImg()
155
+ case 'object':
156
+ return this._renderObj()
157
+ case 'raw':
158
+ return this._renderRaw()
159
+ case 'iframe':
160
+ return this._renderIframe()
161
+ }
162
+ }
163
+ _renderImg() {
164
+ const { width, height, src } = this._data.val
165
+
166
+ this._renderHtml(`<div class="${c('image')}">
167
+ <div class="${c('breadcrumb')}">${escape(src)}</div>
168
+ <div class="${c('img-container')}" data-exclude="true">
169
+ <img src="${escape(src)}">
170
+ </div>
171
+ <div class="${c('img-info')}">${escape(width)} × ${escape(height)}</div>
172
+ </div>`)
173
+ }
174
+ _renderCode() {
175
+ const data = this._data
176
+
177
+ this._renderHtml(
178
+ `<div class="${c('code')}" data-type="${data.type}"></div>`,
179
+ false
180
+ )
181
+
182
+ let code = data.val
183
+ const len = data.val.length
184
+
185
+ if (len > MAX_RAW_LEN) {
186
+ code = truncate(code, MAX_RAW_LEN)
187
+ }
188
+
189
+ // If source code too big, don't process it.
190
+ if (len < MAX_BEAUTIFY_LEN) {
191
+ code = highlight(code, data.type, {
192
+ comment: '',
193
+ string: '',
194
+ number: '',
195
+ keyword: '',
196
+ operator: '',
197
+ })
198
+ each(['comment', 'string', 'number', 'keyword', 'operator'], (type) => {
199
+ code = replaceAll(code, `class="${type}"`, `class="${c(type)}"`)
200
+ })
201
+ } else {
202
+ code = escape(code)
203
+ }
204
+
205
+ const container = this._$el.find(c('.code')).get(0)
206
+ new LunaTextViewer(container, {
207
+ text: code,
208
+ escape: false,
209
+ wrapLongLines: true,
210
+ showLineNumbers: data.val.length < MAX_LINE_NUM_LEN && this._showLineNum,
211
+ })
212
+ }
213
+ _renderObj() {
214
+ // Using cache will keep binding events to the same elements.
215
+ this._renderHtml(`<ul class="${c('json')}"></ul>`, false)
216
+
217
+ let val = this._data.val
218
+
219
+ try {
220
+ if (isStr(val)) {
221
+ val = JSON.parse(val)
222
+ }
223
+ } catch {
224
+ // No op
225
+ }
226
+
227
+ const objViewer = new LunaObjectViewer(
228
+ this._$el.find('.eruda-json').get(0),
229
+ {
230
+ unenumerable: true,
231
+ accessGetter: true,
232
+ prototype: false,
233
+ }
234
+ )
235
+ objViewer.set(val)
236
+ }
237
+ _renderRaw() {
238
+ const data = this._data
239
+
240
+ this._renderHtml(`<div class="${c('raw-wrapper')}">
241
+ <div class="${c('raw')}"></div>
242
+ </div>`)
243
+
244
+ let val = data.val
245
+ const container = this._$el.find(c('.raw')).get(0)
246
+ if (val.length > MAX_RAW_LEN) {
247
+ val = truncate(val, MAX_RAW_LEN)
248
+ }
249
+
250
+ new LunaTextViewer(container, {
251
+ text: val,
252
+ wrapLongLines: true,
253
+ showLineNumbers: val.length < MAX_LINE_NUM_LEN && this._showLineNum,
254
+ })
255
+ }
256
+ _renderIframe() {
257
+ this._renderHtml(`<iframe src="${escape(this._data.val)}"></iframe>`)
258
+ }
259
+ _renderHtml(html, cache = true) {
260
+ if (cache && html === this._lastHtml) return
261
+ this._lastHtml = html
262
+ this._$el.html(html)
263
+ // Need setTimeout to make it work
264
+ setTimeout(() => (this._$el.get(0).scrollTop = 0), 0)
265
+ }
266
+ }
267
+
268
+ const MAX_BEAUTIFY_LEN = 30000
269
+ const MAX_LINE_NUM_LEN = 80000
270
+ const MAX_RAW_LEN = 100000
src/Sources/Sources.scss ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @use '../style/variable' as *;
2
+ @use '../style/mixin' as *;
3
+
4
+ #sources {
5
+ font-size: 0;
6
+ @include overflow-auto(y);
7
+ color: var(--foreground);
8
+ .code-wrapper,
9
+ .raw-wrapper {
10
+ @include overflow-auto(x);
11
+ width: 100%;
12
+ min-height: 100%;
13
+ }
14
+ .raw,
15
+ .code {
16
+ height: 100%;
17
+ .keyword {
18
+ color: var(--keyword-color);
19
+ }
20
+ .comment {
21
+ color: var(--comment-color);
22
+ }
23
+ .number {
24
+ color: var(--number-color);
25
+ }
26
+ .string {
27
+ color: var(--string-color);
28
+ }
29
+ .operator {
30
+ color: var(--operator-color);
31
+ }
32
+ &[data-type='html'] {
33
+ .keyword {
34
+ color: var(--tag-name-color);
35
+ }
36
+ }
37
+ }
38
+ .image {
39
+ font-size: $font-size-s;
40
+ .breadcrumb {
41
+ @include breadcrumb();
42
+ }
43
+ .img-container {
44
+ text-align: center;
45
+ img {
46
+ max-width: 100%;
47
+ }
48
+ }
49
+ .img-info {
50
+ text-align: center;
51
+ margin: 20px 0;
52
+ color: var(--foreground);
53
+ }
54
+ }
55
+ .json {
56
+ padding: 0 $padding;
57
+ * {
58
+ user-select: text;
59
+ }
60
+ }
61
+ iframe {
62
+ width: 100%;
63
+ height: 100%;
64
+ }
65
+ }
src/eruda.js ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import EntryBtn from './EntryBtn/EntryBtn'
2
+ import DevTools from './DevTools/DevTools'
3
+ import Tool from './DevTools/Tool'
4
+ import Console from './Console/Console'
5
+ import Network from './Network/Network'
6
+ import Elements from './Elements/Elements'
7
+ import Snippets from './Snippets/Snippets'
8
+ import Resources from './Resources/Resources'
9
+ import Info from './Info/Info'
10
+ import Sources from './Sources/Sources'
11
+ import Settings from './Settings/Settings'
12
+ import emitter from './lib/emitter'
13
+ import logger from './lib/logger'
14
+ import * as util from './lib/util'
15
+ import { isDarkTheme } from './lib/themes'
16
+ import themes from './lib/themes'
17
+ import isFn from 'licia/isFn'
18
+ import isNum from 'licia/isNum'
19
+ import isObj from 'licia/isObj'
20
+ import each from 'licia/each'
21
+ import isMobile from 'licia/isMobile'
22
+ import viewportScale from 'licia/viewportScale'
23
+ import detectBrowser from 'licia/detectBrowser'
24
+ import $ from 'licia/$'
25
+ import toArr from 'licia/toArr'
26
+ import upperFirst from 'licia/upperFirst'
27
+ import nextTick from 'licia/nextTick'
28
+ import isEqual from 'licia/isEqual'
29
+ import extend from 'licia/extend'
30
+ import evalCss from './lib/evalCss'
31
+ import chobitsu from './lib/chobitsu'
32
+
33
+ export default {
34
+ init({
35
+ container,
36
+ tool,
37
+ autoScale = true,
38
+ useShadowDom = true,
39
+ inline = false,
40
+ defaults = {},
41
+ } = {}) {
42
+ if (this._isInit) {
43
+ return
44
+ }
45
+
46
+ this._isInit = true
47
+ this._scale = 1
48
+
49
+ this._initContainer(container, useShadowDom)
50
+ this._initStyle()
51
+ this._initDevTools(defaults, inline)
52
+ this._initEntryBtn()
53
+ this._initSettings()
54
+ this._initTools(tool)
55
+ this._registerListener()
56
+
57
+ if (autoScale) {
58
+ this._autoScale()
59
+ }
60
+ if (inline) {
61
+ this._entryBtn.hide()
62
+ this._$el.addClass('eruda-inline')
63
+ this.show()
64
+ }
65
+ },
66
+ _isInit: false,
67
+ version: VERSION,
68
+ util: {
69
+ isErudaEl: util.isErudaEl,
70
+ evalCss,
71
+ isDarkTheme(theme) {
72
+ if (!theme) {
73
+ theme = this.getTheme()
74
+ }
75
+ return isDarkTheme(theme)
76
+ },
77
+ getTheme: () => {
78
+ const curTheme = evalCss.getCurTheme()
79
+
80
+ let result = 'Light'
81
+ each(themes, (theme, name) => {
82
+ if (isEqual(theme, curTheme)) {
83
+ result = name
84
+ }
85
+ })
86
+
87
+ return result
88
+ },
89
+ },
90
+ chobitsu,
91
+ Tool,
92
+ Console,
93
+ Elements,
94
+ Network,
95
+ Sources,
96
+ Resources,
97
+ Info,
98
+ Snippets,
99
+ Settings,
100
+ get(name) {
101
+ if (!this._checkInit()) return
102
+
103
+ if (name === 'entryBtn') return this._entryBtn
104
+
105
+ const devTools = this._devTools
106
+
107
+ return name ? devTools.get(name) : devTools
108
+ },
109
+ add(tool) {
110
+ if (!this._checkInit()) return
111
+
112
+ if (isFn(tool)) tool = tool(this)
113
+
114
+ this._devTools.add(tool)
115
+
116
+ return this
117
+ },
118
+ remove(name) {
119
+ this._devTools.remove(name)
120
+
121
+ return this
122
+ },
123
+ show(name) {
124
+ if (!this._checkInit()) return
125
+
126
+ const devTools = this._devTools
127
+
128
+ name ? devTools.showTool(name) : devTools.show()
129
+
130
+ return this
131
+ },
132
+ hide() {
133
+ if (!this._checkInit()) return
134
+
135
+ this._devTools.hide()
136
+
137
+ return this
138
+ },
139
+ destroy() {
140
+ this._devTools.destroy()
141
+ delete this._devTools
142
+ this._entryBtn.destroy()
143
+ delete this._entryBtn
144
+ this._unregisterListener()
145
+ $(this._container).remove()
146
+ evalCss.clear()
147
+ this._isInit = false
148
+ this._container = null
149
+ this._shadowRoot = null
150
+ },
151
+ scale(s) {
152
+ if (isNum(s)) {
153
+ this._scale = s
154
+ emitter.emit(emitter.SCALE, s)
155
+ return this
156
+ }
157
+
158
+ return this._scale
159
+ },
160
+ position(p) {
161
+ const entryBtn = this._entryBtn
162
+
163
+ if (isObj(p)) {
164
+ entryBtn.setPos(p)
165
+ return this
166
+ }
167
+
168
+ return entryBtn.getPos()
169
+ },
170
+ _autoScale() {
171
+ if (!isMobile()) return
172
+
173
+ this.scale(1 / viewportScale())
174
+ },
175
+ _registerListener() {
176
+ this._addListener = (...args) => this.add(...args)
177
+ this._showListener = (...args) => this.show(...args)
178
+
179
+ emitter.on(emitter.ADD, this._addListener)
180
+ emitter.on(emitter.SHOW, this._showListener)
181
+ emitter.on(emitter.SCALE, evalCss.setScale)
182
+ },
183
+ _unregisterListener() {
184
+ emitter.off(emitter.ADD, this._addListener)
185
+ emitter.off(emitter.SHOW, this._showListener)
186
+ emitter.off(emitter.SCALE, evalCss.setScale)
187
+ },
188
+ _checkInit() {
189
+ if (!this._isInit) logger.error('Please call "eruda.init()" first')
190
+ return this._isInit
191
+ },
192
+ _initContainer(container, useShadowDom) {
193
+ if (!container) {
194
+ container = document.createElement('div')
195
+ document.documentElement.appendChild(container)
196
+ }
197
+
198
+ container.id = 'eruda'
199
+ container.style.all = 'initial'
200
+ this._container = container
201
+
202
+ let shadowRoot
203
+ let el
204
+ if (useShadowDom) {
205
+ if (container.attachShadow) {
206
+ shadowRoot = container.attachShadow({ mode: 'open' })
207
+ } else if (container.createShadowRoot) {
208
+ shadowRoot = container.createShadowRoot()
209
+ }
210
+ if (shadowRoot) {
211
+ // font-face doesn't work inside shadow dom.
212
+ evalCss.container = document.head
213
+ evalCss(
214
+ require('./style/icon.css') +
215
+ require('luna-console/luna-console.css') +
216
+ require('luna-object-viewer/luna-object-viewer.css') +
217
+ require('luna-dom-viewer/luna-dom-viewer.css') +
218
+ require('luna-text-viewer/luna-text-viewer.css') +
219
+ require('luna-notification/luna-notification.css')
220
+ )
221
+
222
+ el = document.createElement('div')
223
+ shadowRoot.appendChild(el)
224
+ this._shadowRoot = shadowRoot
225
+ }
226
+ }
227
+
228
+ if (!this._shadowRoot) {
229
+ el = document.createElement('div')
230
+ container.appendChild(el)
231
+ }
232
+
233
+ extend(el, {
234
+ className: 'eruda-container __chobitsu-hide__',
235
+ contentEditable: false,
236
+ })
237
+
238
+ // http://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari
239
+ if (detectBrowser().name === 'ios') el.setAttribute('ontouchstart', '')
240
+
241
+ this._$el = $(el)
242
+ },
243
+ _initDevTools(defaults, inline) {
244
+ this._devTools = new DevTools(this._$el, {
245
+ defaults,
246
+ inline,
247
+ })
248
+ },
249
+ _initStyle() {
250
+ const className = 'eruda-style-container'
251
+ const $el = this._$el
252
+
253
+ if (this._shadowRoot) {
254
+ evalCss.container = this._shadowRoot
255
+ evalCss(':host { all: initial }')
256
+ } else {
257
+ $el.append(`<div class="${className}"></div>`)
258
+ evalCss.container = $el.find(`.${className}`).get(0)
259
+ }
260
+
261
+ evalCss(
262
+ require('./style/reset.scss') +
263
+ require('luna-object-viewer/luna-object-viewer.css') +
264
+ require('luna-console/luna-console.css') +
265
+ require('luna-notification/luna-notification.css') +
266
+ require('luna-data-grid/luna-data-grid.css') +
267
+ require('luna-dom-viewer/luna-dom-viewer.css') +
268
+ require('luna-modal/luna-modal.css') +
269
+ require('luna-tab/luna-tab.css') +
270
+ require('luna-text-viewer/luna-text-viewer.css') +
271
+ require('luna-setting/luna-setting.css') +
272
+ require('luna-box-model/luna-box-model.css') +
273
+ require('./style/style.scss') +
274
+ require('./style/icon.css')
275
+ )
276
+ },
277
+ _initEntryBtn() {
278
+ this._entryBtn = new EntryBtn(this._$el)
279
+ this._entryBtn.on('click', () => this._devTools.toggle())
280
+ },
281
+ _initSettings() {
282
+ const devTools = this._devTools
283
+ const settings = new Settings()
284
+
285
+ devTools.add(settings)
286
+
287
+ this._entryBtn.initCfg(settings)
288
+ devTools.initCfg(settings)
289
+ },
290
+ _initTools(
291
+ tool = [
292
+ 'console',
293
+ 'elements',
294
+ 'network',
295
+ 'resources',
296
+ 'sources',
297
+ 'info',
298
+ 'snippets',
299
+ ]
300
+ ) {
301
+ tool = toArr(tool)
302
+
303
+ const devTools = this._devTools
304
+
305
+ tool.forEach((name) => {
306
+ const Tool = this[upperFirst(name)]
307
+ try {
308
+ if (Tool) devTools.add(new Tool())
309
+ } catch (e) {
310
+ // Use nextTick to make sure it is possible to be caught by console panel.
311
+ nextTick(() => {
312
+ logger.error(
313
+ `Something wrong when initializing tool ${name}:`,
314
+ e.message
315
+ )
316
+ })
317
+ }
318
+ })
319
+
320
+ devTools.showTool(tool[0] || 'settings')
321
+ },
322
+ }