├── .gitattributes
├── .github
├── FUNDING.yml
└── CONTRIBUTING.md
├── src
├── Sources
│ ├── object.hbs
│ ├── iframe.hbs
│ ├── raw.hbs
│ ├── image.hbs
│ ├── code.hbs
│ ├── Sources.scss
│ └── Sources.js
├── Dom
│ ├── textNode.hbs
│ ├── htmlComment.hbs
│ ├── template.hbs
│ ├── upstream.json
│ ├── htmlTag.hbs
│ ├── style.scss
│ └── Dom.js
├── EntryBtn
│ ├── EntryBtn.hbs
│ ├── EntryBtn.scss
│ └── EntryBtn.js
├── Info
│ ├── Info.hbs
│ ├── Info.scss
│ ├── defInfo.js
│ └── Info.js
├── lib
│ ├── emitter.js
│ ├── logger.js
│ ├── abstract.scss
│ ├── extraUtil.js
│ ├── handlebars.js
│ ├── cssMap.js
│ └── evalCss.js
├── index.js
├── Network
│ ├── Network.hbs
│ ├── requests.hbs
│ ├── detail.hbs
│ ├── Network.scss
│ └── Network.js
├── Snippets
│ ├── searchText.scss
│ ├── Snippets.hbs
│ ├── Snippets.scss
│ ├── Snippets.js
│ └── defSnippets.js
├── style
│ ├── icon
│ │ ├── arrow-right.svg
│ │ ├── play.svg
│ │ ├── caret-right.svg
│ │ ├── caret-down.svg
│ │ ├── reset.svg
│ │ ├── warn.svg
│ │ ├── arrow-left.svg
│ │ ├── refresh.svg
│ │ ├── delete.svg
│ │ ├── search.svg
│ │ ├── select.svg
│ │ ├── compress.svg
│ │ ├── expand.svg
│ │ ├── clear.svg
│ │ ├── eye.svg
│ │ ├── error.svg
│ │ └── tool.svg
│ ├── variable.scss
│ ├── style.scss
│ ├── mixin.scss
│ ├── reset.scss
│ ├── luna.scss
│ └── icon.css
├── DevTools
│ ├── DevTools.hbs
│ ├── Tool.js
│ ├── DevTools.scss
│ ├── NavBar.scss
│ └── NavBar.js
├── Settings
│ ├── select.hbs
│ ├── switch.hbs
│ ├── color.hbs
│ ├── range.hbs
│ ├── Settings.scss
│ └── Settings.js
├── Elements
│ ├── BottomBar.hbs
│ ├── Highlight.js
│ ├── Select.js
│ ├── CssStore.js
│ ├── Elements.hbs
│ └── Elements.scss
├── Console
│ ├── Console.hbs
│ └── Console.scss
├── Resources
│ ├── Resources.scss
│ └── Resources.hbs
└── eruda.js
├── test
├── init.js
├── sources.js
├── network.js
├── elements.js
├── data.json
├── info.html
├── eruda.html
├── console.html
├── elements.html
├── settings.html
├── snippets.html
├── sources.html
├── network.html
├── resources.html
├── boot.js
├── style.css
├── resources.js
├── info.js
├── snippets.js
├── eruda.js
├── console.js
├── index.html
├── settings.js
└── manual.html
├── .prettierignore
├── .gitmodules
├── prettier.config.js
├── .gitignore
├── tsconfig.json
├── .eustia.js
├── .eslintrc.js
├── patches
├── licia-es+1.36.0.patch
├── handlebars-loader+1.7.2.patch
├── css-loader+6.7.1.patch
└── luna-console+0.3.1.patch
├── test.html
├── LICENSE
├── karma.conf.js
├── doc
├── PLUGIN.md
└── API.md
├── package.json
├── README_CN.md
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | open_collective: eruda
--------------------------------------------------------------------------------
/src/Sources/object.hbs:
--------------------------------------------------------------------------------
1 |
").concat(lines
48 | .slice(1)
49 | @@ -563,7 +589,7 @@ var Log = (function (_super) {
50 | return args;
51 | };
52 | Log.prototype.formatJs = function (val) {
53 | - return "
").concat(this.console.c((0, highlight_1.default)(val, 'js', emptyHighlightStyle)), "
");
54 | + return "
").concat(this.console.c((0, highlight_1.default)(val, 'js', emptyHighlightStyle), false), "
");
55 | };
56 | Log.prototype.formatFn = function (val) {
57 | return "
".concat(this.formatJs(val.toString()), "
");
58 | diff --git a/node_modules/luna-console/cjs/share/util.js b/node_modules/luna-console/cjs/share/util.js
59 | index 2bf94f0..8247c21 100644
60 | --- a/node_modules/luna-console/cjs/share/util.js
61 | +++ b/node_modules/luna-console/cjs/share/util.js
62 | @@ -23,7 +23,7 @@ function classPrefix(name) {
63 | return singleClass.replace(/[\w-]+/, function (match) { return "".concat(prefix).concat(match); });
64 | }).join(' ');
65 | }
66 | - return function (str) {
67 | + return function (str, isClassName = true) {
68 | if (/<[^>]*>/g.test(str)) {
69 | try {
70 | var tree = html_1.default.parse(str);
71 | @@ -38,7 +38,7 @@ function classPrefix(name) {
72 | return processClass(str);
73 | }
74 | }
75 | - return processClass(str);
76 | + return isClassName ? processClass(str) : str;
77 | };
78 | }
79 | exports.classPrefix = classPrefix;
80 |
--------------------------------------------------------------------------------
/src/style/luna.scss:
--------------------------------------------------------------------------------
1 | @import './variable';
2 |
3 | .luna-console {
4 | background: var(--background);
5 | }
6 |
7 | @mixin luna-console-highlight {
8 | .luna-console-key {
9 | color: var(--var-color);
10 | }
11 | .luna-console-number {
12 | color: var(--number-color);
13 | }
14 | .luna-console-null {
15 | color: var(--operator-color);
16 | }
17 | .luna-console-string {
18 | color: var(--string-color);
19 | }
20 | .luna-console-boolean {
21 | color: var(--keyword-color);
22 | }
23 | .luna-console-special {
24 | color: var(--operator-color);
25 | }
26 | .luna-console-keyword {
27 | color: var(--keyword-color);
28 | }
29 | .luna-console-operator {
30 | color: var(--operator-color);
31 | }
32 | .luna-console-comment {
33 | color: var(--comment-color);
34 | }
35 | }
36 |
37 | .luna-console-header {
38 | color: var(--link-color);
39 | border-bottom-color: var(--border);
40 | }
41 |
42 | .luna-console-nesting-level {
43 | border-right-color: var(--border);
44 | &::before {
45 | border-bottom-color: var(--border);
46 | }
47 | }
48 |
49 | .luna-console-log-item {
50 | border-bottom-color: var(--border);
51 | color: var(--foreground);
52 | a {
53 | color: var(--link-color) !important;
54 | }
55 | .luna-console-icon-container {
56 | .luna-console-icon {
57 | color: var(--foreground);
58 | }
59 | .luna-console-icon-error {
60 | color: #ef3842;
61 | }
62 | .luna-console-icon-warn {
63 | color: #e8a400;
64 | }
65 | }
66 | .luna-console-count {
67 | background: var(--text-color);
68 | }
69 | &.luna-console-warn {
70 | color: var(--console-warn-foreground);
71 | background: var(--console-warn-background);
72 | border-color: var(--console-warn-border);
73 | }
74 | &.luna-console-error {
75 | background: var(--console-error-background);
76 | color: var(--console-error-foreground);
77 | border-color: var(--console-error-border);
78 | .luna-console-count {
79 | background: var(--console-error-foreground);
80 | }
81 | }
82 | &.luna-console-table {
83 | table {
84 | color: var(--foreground);
85 | th {
86 | background: var(--darker-background);
87 | }
88 | th,
89 | td {
90 | border-color: var(--border);
91 | }
92 | tr:nth-child(even) {
93 | background: var(--contrast);
94 | }
95 | }
96 | }
97 | .luna-console-code {
98 | @include luna-console-highlight();
99 | }
100 | }
101 |
102 | .luna-console-abstract {
103 | @include luna-console-highlight();
104 | }
105 |
106 | .luna-object-viewer {
107 | color: var(--primary);
108 | font-size: 12px !important;
109 | & > li {
110 | padding: $padding 0 !important;
111 | }
112 | }
113 | .luna-object-viewer-null {
114 | color: var(--operator-color);
115 | }
116 | .luna-object-viewer-string,
117 | .luna-object-viewer-regexp {
118 | color: var(--string-color);
119 | }
120 | .luna-object-viewer-number {
121 | color: var(--number-color);
122 | }
123 | .luna-object-viewer-boolean {
124 | color: var(--keyword-color);
125 | }
126 | .luna-object-viewer-special {
127 | color: var(--operator-color);
128 | }
129 | .luna-object-viewer-key,
130 | .luna-object-viewer-key-lighter {
131 | color: var(--var-color);
132 | }
133 | .luna-object-viewer-expanded:before {
134 | border-color: transparent;
135 | border-top-color: var(--foreground);
136 | }
137 | .luna-object-viewer-collapsed:before {
138 | border-top-color: transparent;
139 | border-left-color: var(--foreground);
140 | }
141 |
142 | .luna-notification {
143 | pointer-events: none !important;
144 | padding: $padding;
145 | z-index: 1000;
146 | }
147 |
148 | .luna-notification-item {
149 | z-index: 500;
150 | color: var(--foreground);
151 | background: var(--background);
152 | box-shadow: none;
153 | padding: 5px 10px;
154 | border: 1px solid var(--border);
155 | }
156 |
157 | .luna-notification-upper {
158 | margin-bottom: 10px;
159 | }
160 |
161 | .luna-notification-lower {
162 | margin-top: 10px;
163 | }
164 |
--------------------------------------------------------------------------------
/src/style/icon/tool.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Eruda
6 |
7 |
8 |
9 | 一个专为手机网页前端设计的调试面板。
10 |
11 | [![NPM version][npm-image]][npm-url]
12 | [![Build status][ci-image]][ci-url]
13 | [![Test coverage][codecov-image]][codecov-url]
14 | [![Downloads][jsdelivr-image]][jsdelivr-url]
15 | [![License][license-image]][npm-url]
16 |
17 |
18 |
19 | [npm-image]: https://img.shields.io/npm/v/eruda?style=flat-square
20 | [npm-url]: https://npmjs.org/package/eruda
21 | [jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hm/eruda?style=flat-square
22 | [jsdelivr-url]: https://www.jsdelivr.com/package/npm/eruda
23 | [ci-image]: https://img.shields.io/github/workflow/status/liriliri/eruda/CI?style=flat-square
24 | [ci-url]: https://github.com/liriliri/eruda/actions/workflows/main.yml
25 | [codecov-image]: https://img.shields.io/codecov/c/github/liriliri/eruda?style=flat-square
26 | [codecov-url]: https://codecov.io/github/liriliri/eruda?branch=master
27 | [license-image]: https://img.shields.io/npm/l/eruda?style=flat-square
28 | [donate-image]: https://img.shields.io/badge/$-donate-0070ba.svg?style=flat-square
29 |
30 |

31 |
32 | ## Demo
33 |
34 | 
35 |
36 | 请扫描二维码或在手机上直接访问:[https://eruda.liriliri.io/](https://eruda.liriliri.io/)
37 |
38 | 如果想在其它页面尝试,请在浏览器地址栏上输入以下代码。
39 |
40 | ```javascript
41 | javascript:(function () { var script = document.createElement('script'); script.src="//cdn.jsdelivr.net/npm/eruda"; document.body.appendChild(script); script.onload = function () { eruda.init() } })();
42 | ```
43 |
44 | ## 功能清单
45 |
46 | 1. 按钮拖拽,面板透明度大小设置。
47 |
48 | 2. Console 面板:捕获 Console 日志,支持 log、error、info、warn、dir、time/timeEnd、clear、count、assert、table;支持占位符,包括 %c 自定义样式输出;支持按日志类型及正则表达式过滤;支持 JavaScript 脚本执行。
49 |
50 | 3. Elements 面板:查看标签内容及属性;查看应用在 Dom 上的样式;支持页面元素高亮;支持屏幕直接点击选取;查看 Dom 上绑定的各类事件。
51 |
52 | 4. Network 面板:捕获请求,查看发送数据、返回头、返回内容等信息。
53 |
54 | 5. Resources 面板:查看并清除 localStorage、sessionStorage 及 cookie;查看页面加载脚本及样式文件;查看页面加载图片。
55 |
56 | 6. Sources 面板:查看页面源码;格式化 html,css,js 代码及 json 数据。
57 |
58 | 7. Info 面板:输出 URL 及 User Agent;支持自定义输出内容。
59 |
60 | 8. Snippets 面板:页面元素添加边框;加时间戳刷新页面;支持自定义代码片段。
61 |
62 | ## 快速上手
63 |
64 | 通过CDN使用:
65 |
66 | ```html
67 |
68 |
69 | ```
70 |
71 | 通过 npm 安装:
72 |
73 | ```bash
74 | npm install eruda --save
75 | ```
76 |
77 | 在页面中加载脚本:
78 |
79 | ```html
80 |
81 |
82 | ```
83 |
84 | JS 文件对于移动端来说略重(gzip 后大概 100kb)。建议通过 url 参数来控制是否加载调试器,比如:
85 |
86 | ```javascript
87 | ;(function () {
88 | var src = '//cdn.jsdelivr.net/npm/eruda';
89 | if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
90 | document.write('
');
91 | document.write('
eruda.init();');
92 | })();
93 | ```
94 |
95 | 初始化时可以传入配置:
96 | * container: 用于插件初始化的 Dom 元素,如果不设置,默认创建 div 作为容器直接置于 html 根结点下面。
97 | * tool:指定要初始化哪些面板,默认加载所有。
98 |
99 | ```javascript
100 | let el = document.createElement('div');
101 | document.body.appendChild(el);
102 |
103 | eruda.init({
104 | container: el,
105 | tool: ['console', 'elements'],
106 | useShadowDom: true
107 | });
108 | ```
109 |
110 | ## 插件
111 |
112 | * [eruda-fps](https://github.com/liriliri/eruda-fps):展示页面的 fps 信息。
113 | * [eruda-features](https://github.com/liriliri/eruda-features):浏览器特性检测。
114 | * [eruda-timing](https://github.com/liriliri/eruda-timing):展示性能资源数据。
115 | * [eruda-memory](https://github.com/liriliri/eruda-memory):展示页面内存信息。
116 | * [eruda-code](https://github.com/liriliri/eruda-code):运行 JavaScript 代码。
117 | * [eruda-benchmark](https://github.com/liriliri/eruda-benchmark):运行 JavaScript 性能测试。
118 | * [eruda-geolocation](https://github.com/liriliri/eruda-geolocation):测试地理位置接口。
119 | * [eruda-dom](https://github.com/liriliri/eruda-dom):浏览 dom 树。
120 | * [eruda-orientation](https://github.com/liriliri/eruda-orientation):测试重力感应接口。
121 | * [eruda-touches](https://github.com/liriliri/eruda-orientation):可视化屏幕 Touch 事件触发。
122 |
123 | 如果你想要自己编写插件,可以查看这里的[教程](./PLUGIN.md)。
124 |
125 | ## 相关项目
126 |
127 | * [chii](https://github.com/liriliri/chii):远程调试工具。
128 | * [licia](https://github.com/liriliri/licia):Eruda 使用的工具库。
129 | * [eruda-webpack-plugin](https://github.com/huruji/eruda-webpack-plugin):Eruda webpack 插件。
130 |
--------------------------------------------------------------------------------
/src/Elements/Elements.hbs:
--------------------------------------------------------------------------------
1 | {{#if parents}}
2 |
3 | {{#each parents}}
4 | -
5 |
{{{text}}}
6 |
7 |
8 | {{/each}}
9 |
10 | {{/if}}
11 |
12 | {{{name}}}
13 |
14 | {{#if children}}
15 |
16 | {{#each children}}
17 | - {{{text}}}
18 | {{/each}}
19 |
20 | {{/if}}
21 |
22 |
Attributes
23 |
24 |
25 |
26 | {{#if attributes}}
27 | {{#each attributes}}
28 |
29 | | {{name}} |
30 | {{{value}}} |
31 |
32 | {{/each}}
33 | {{else}}
34 |
35 | | Empty |
36 |
37 | {{/if}}
38 |
39 |
40 |
41 |
42 | {{#if styles}}
43 |
44 |
Styles
45 |
46 | {{#each styles}}
47 |
48 |
{{selectorText}} {
49 | {{#each style}}
50 |
51 | {{@key}}: {{{.}}};
52 |
53 | {{/each}}
54 |
}
55 |
56 | {{/each}}
57 |
58 |
59 | {{/if}}
60 | {{#if computedStyle}}
61 |
62 |
63 | Computed Style
64 | {{#if rmDefComputedStyle}}
65 |
66 |
67 |
68 | {{else}}
69 |
70 |
71 |
72 | {{/if}}
73 |
74 |
75 |
76 | {{#if computedStyleSearchKeyword}}
77 |
78 | {{computedStyleSearchKeyword}}
79 |
80 | {{/if}}
81 |
82 |
83 | {{#if boxModel.position}}
84 |
position
{{boxModel.position.top}}
{{boxModel.position.left}}
{{/if}}{{!
85 | }}
86 |
margin
{{boxModel.margin.top}}
{{boxModel.margin.left}}
{{!
87 | }}
88 |
border
{{boxModel.border.top}}
{{boxModel.border.left}}
{{!
89 | }}
90 |
padding
{{boxModel.padding.top}}
{{boxModel.padding.left}}
{{!
91 | }}
92 | {{boxModel.content.width}} × {{boxModel.content.height}}
93 |
{{!
94 | }}
{{boxModel.padding.right}}
{{boxModel.padding.bottom}}
{{!
95 | }}
{{!
96 | }}
{{boxModel.border.right}}
{{boxModel.border.bottom}}
{{!
97 | }}
{{!
98 | }}
{{boxModel.margin.right}}
{{boxModel.margin.bottom}}
{{!
99 | }}
{{!
100 | }}{{#if boxModel.position}}
{{boxModel.position.right}}
{{boxModel.position.bottom}}
{{!
101 | }}
{{/if}}
102 |
103 |
104 |
105 |
106 | {{#each computedStyle}}
107 |
108 | | {{@key}} |
109 | {{{.}}} |
110 |
111 | {{/each}}
112 |
113 |
114 |
115 |
116 | {{/if}}
117 | {{#if listeners}}
118 |
119 |
Event Listeners
120 |
121 | {{#each listeners}}
122 |
123 |
{{@key}}
124 |
125 | {{#each .}}
126 | - {{listenerStr}}
127 | {{/each}}
128 |
129 |
130 | {{/each}}
131 |
132 |
133 | {{/if}}
134 |
--------------------------------------------------------------------------------
/src/style/icon.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'eruda-icon';
3 | src: url('data:application/x-font-woff;charset=utf-8;base64,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')
4 | format('woff');
5 | }
6 |
7 | [class^='icon-'],
8 | [class*=' icon-'] {
9 | font-family: 'eruda-icon' !important;
10 | font-size: 16px;
11 | font-style: normal;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | .icon-arrow-left:before {
17 | content: '\f101';
18 | }
19 | .icon-arrow-right:before {
20 | content: '\f102';
21 | }
22 | .icon-caret-down:before {
23 | content: '\f103';
24 | }
25 | .icon-caret-right:before {
26 | content: '\f104';
27 | }
28 | .icon-clear:before {
29 | content: '\f105';
30 | }
31 | .icon-compress:before {
32 | content: '\f106';
33 | }
34 | .icon-delete:before {
35 | content: '\f107';
36 | }
37 | .icon-error:before {
38 | content: '\f108';
39 | }
40 | .icon-expand:before {
41 | content: '\f109';
42 | }
43 | .icon-eye:before {
44 | content: '\f10a';
45 | }
46 | .icon-play:before {
47 | content: '\f10b';
48 | }
49 | .icon-refresh:before {
50 | content: '\f10c';
51 | }
52 | .icon-reset:before {
53 | content: '\f10d';
54 | }
55 | .icon-search:before {
56 | content: '\f10e';
57 | }
58 | .icon-select:before {
59 | content: '\f10f';
60 | }
61 | .icon-tool:before {
62 | content: '\f110';
63 | }
64 | .icon-warn:before {
65 | content: '\f111';
66 | }
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Eruda
6 |
7 |
8 |
9 | Console for Mobile Browsers.
10 |
11 | [![NPM version][npm-image]][npm-url]
12 | [![Build status][ci-image]][ci-url]
13 | [![Test coverage][codecov-image]][codecov-url]
14 | [![Downloads][jsdelivr-image]][jsdelivr-url]
15 | [![License][license-image]][npm-url]
16 |
17 |
18 |
19 | [npm-image]: https://img.shields.io/npm/v/eruda?style=flat-square
20 | [npm-url]: https://npmjs.org/package/eruda
21 | [jsdelivr-image]: https://img.shields.io/jsdelivr/npm/hm/eruda?style=flat-square
22 | [jsdelivr-url]: https://www.jsdelivr.com/package/npm/eruda
23 | [ci-image]: https://img.shields.io/github/workflow/status/liriliri/eruda/CI?style=flat-square
24 | [ci-url]: https://github.com/liriliri/eruda/actions/workflows/main.yml
25 | [codecov-image]: https://img.shields.io/codecov/c/github/liriliri/eruda?style=flat-square
26 | [codecov-url]: https://codecov.io/github/liriliri/eruda?branch=master
27 | [license-image]: https://img.shields.io/npm/l/eruda?style=flat-square
28 | [donate-image]: https://img.shields.io/badge/$-donate-0070ba.svg?style=flat-square
29 |
30 |

31 |
32 | [中文](README_CN.md)
33 |
34 | ## Demo
35 |
36 | 
37 |
38 | Browse it on your phone: [https://eruda.liriliri.io/](https://eruda.liriliri.io/)
39 |
40 | In order to try it for different sites, execute the script below on browser address bar.
41 |
42 | ```javascript
43 | javascript:(function () { var script = document.createElement('script'); script.src="//cdn.jsdelivr.net/npm/eruda"; document.body.appendChild(script); script.onload = function () { eruda.init() } })();
44 | ```
45 |
46 | ## Features
47 |
48 | * [Console](doc/TOOL_API.md#console): Display JavaScript logs.
49 | * [Elements](doc/TOOL_API.md#elements): Check dom state.
50 | * [Network](doc/TOOL_API.md#network): Show requests status.
51 | * [Resource](/doc/TOOL_API.md#resources): Show localStorage, cookie information.
52 | * [Info](doc/TOOL_API.md#info): Show url, user agent info.
53 | * [Snippets](doc/TOOL_API.md#snippets): Include snippets used most often.
54 | * [Sources](doc/TOOL_API.md#sources): Html, js, css source viewer.
55 |
56 | ## Install
57 |
58 | You can get it on npm.
59 |
60 | ```bash
61 | npm install eruda --save
62 | ```
63 |
64 | Add this script to your page.
65 |
66 | ```html
67 |
68 |
69 | ```
70 |
71 | It's also available on [jsDelivr](http://www.jsdelivr.com/projects/eruda) and [cdnjs](https://cdnjs.com/libraries/eruda).
72 |
73 | ```html
74 |
75 |
76 | ```
77 |
78 | The JavaScript file size is quite huge(about 100kb gzipped) and therefore not suitable to include in mobile pages. It's recommended to make sure eruda is loaded only when eruda is set to true on url(http://example.com/?eruda=true), for example:
79 |
80 | ```javascript
81 | ;(function () {
82 | var src = '//cdn.jsdelivr.net/npm/eruda';
83 | if (!/eruda=true/.test(window.location) && localStorage.getItem('active-eruda') != 'true') return;
84 | document.write('
');
85 | document.write('
eruda.init();');
86 | })();
87 | ```
88 |
89 | ## Configuration
90 |
91 | When initialization, a configuration object can be passed in.
92 |
93 | * container: Container element. If not set, it will append an element directly
94 | under html root element.
95 | * tool: Choose which default tools you want, by default all will be added.
96 |
97 | For more information, please check the [documentation](doc/API.md).
98 |
99 | ```javascript
100 | let el = document.createElement('div');
101 | document.body.appendChild(el);
102 |
103 | eruda.init({
104 | container: el,
105 | tool: ['console', 'elements']
106 | });
107 | ```
108 |
109 | ## Plugins
110 |
111 | * [eruda-fps](https://github.com/liriliri/eruda-fps): Display page fps info.
112 | * [eruda-features](https://github.com/liriliri/eruda-features): Browser feature detections.
113 | * [eruda-timing](https://github.com/liriliri/eruda-timing): Show performance and resource timing.
114 | * [eruda-memory](https://github.com/liriliri/eruda-memory): Display page memory info.
115 | * [eruda-code](https://github.com/liriliri/eruda-code): Run JavaScript code.
116 | * [eruda-benchmark](https://github.com/liriliri/eruda-benchmark): Run JavaScript benchmarks.
117 | * [eruda-geolocation](https://github.com/liriliri/eruda-geolocation): Test geolocation.
118 | * [eruda-dom](https://github.com/liriliri/eruda-dom): Navigate dom tree.
119 | * [eruda-orientation](https://github.com/liriliri/eruda-orientation): Test orientation api.
120 | * [eruda-touches](https://github.com/liriliri/eruda-touches): Visualize screen touches.
121 |
122 | If you want to create a plugin yourself, follow the guides [here](./doc/PLUGIN.md).
123 |
124 | ## Related Projects
125 |
126 | * [chii](https://github.com/liriliri/chii): Remote debugging tool.
127 | * [chobitsu](https://github.com/liriliri/chobitsu): Chrome devtools protocol JavaScript implementation.
128 | * [licia](https://github.com/liriliri/licia): Utility library used by eruda.
129 | * [eruda-webpack-plugin](https://github.com/huruji/eruda-webpack-plugin): Eruda webpack plugin.
130 |
131 | ## Backers
132 |
133 |

134 |
135 | ## Contribution
136 |
137 | Read [Contributing Guide](.github/CONTRIBUTING.md) for development setup instructions.
--------------------------------------------------------------------------------
/test/manual.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
Manual
10 |
11 |
12 |
13 |
14 |
15 |
16 |
56 |
180 |
184 |
185 |
186 |
--------------------------------------------------------------------------------
/src/Resources/Resources.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Local Storage
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{#if localStoreSearchKeyword}}{{localStoreSearchKeyword}}
{{/if}}
14 |
15 |
16 |
17 |
18 | {{#if localStoreData}}
19 | {{#each localStoreData}}
20 |
21 | | {{key}} |
22 | {{val}} |
23 |
24 |
25 | |
26 |
27 | {{/each}}
28 | {{else}}
29 |
30 | | Empty |
31 |
32 | {{/if}}
33 |
34 |
35 |
36 |
37 |
38 |
39 | Session Storage
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {{#if sessionStoreSearchKeyword}}{{sessionStoreSearchKeyword}}
{{/if}}
50 |
51 |
52 |
53 |
54 | {{#if sessionStoreData}}
55 | {{#each sessionStoreData}}
56 |
57 | | {{key}} |
58 | {{val}} |
59 |
60 |
61 | |
62 |
63 | {{/each}}
64 | {{else}}
65 |
66 | | Empty |
67 |
68 | {{/if}}
69 |
70 |
71 |
72 |
73 |
74 |
75 | Cookie
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | {{#if cookieSearchKeyword}}{{cookieSearchKeyword}}
{{/if}}
86 |
87 |
88 |
89 |
90 | {{#if cookieData}}
91 | {{#each cookieData}}
92 |
93 | | {{key}} |
94 | {{val}} |
95 |
96 |
97 | |
98 |
99 | {{/each}}
100 | {{else}}
101 |
102 | | Empty |
103 |
104 | {{/if}}
105 |
106 |
107 |
108 |
109 |
110 |
111 | Script
112 |
113 |
114 |
115 |
116 |
117 | {{#if scriptData}}
118 | {{#each scriptData}}
119 | -
120 | {{this}}
121 |
122 | {{/each}}
123 | {{else}}
124 | - Empty
125 | {{/if}}
126 |
127 |
128 |
129 |
130 | Stylesheet
131 |
132 |
133 |
134 |
135 |
136 | {{#if stylesheetData}}
137 | {{#each stylesheetData}}
138 | -
139 | {{this}}
140 |
141 | {{/each}}
142 | {{else}}
143 | - Empty
144 | {{/if}}
145 |
146 |
147 |
148 |
149 | Iframe
150 |
151 |
152 |
153 |
154 |
155 | {{#if iframeData}}
156 | {{#each iframeData}}
157 | -
158 | {{this}}
159 |
160 | {{/each}}
161 | {{else}}
162 | - Empty
163 | {{/if}}
164 |
165 |
166 |
167 |
168 | Image
169 |
170 |
171 |
172 |
173 |
174 | {{#if imageData}}
175 | {{#each imageData}}
176 | -
177 |
178 |
179 | {{/each}}
180 | {{else}}
181 | - Empty
182 | {{/if}}
183 |
184 |
185 |
--------------------------------------------------------------------------------
/src/Settings/Settings.scss:
--------------------------------------------------------------------------------
1 | @import '../style/variable';
2 | @import '../style/mixin';
3 |
4 | #settings {
5 | @include overflow-auto(y);
6 | .separator {
7 | height: 10px;
8 | }
9 | .text {
10 | padding: $padding;
11 | color: var(--accent);
12 | font-size: $font-size-s;
13 | }
14 | .select,
15 | .range,
16 | .color {
17 | cursor: pointer;
18 | }
19 | .select .head,
20 | .switch,
21 | .range .head,
22 | .color .head {
23 | padding: $padding;
24 | background: var(--darker-background);
25 | font-size: $font-size;
26 | border-bottom: 1px solid var(--border);
27 | border-top: 1px solid var(--border);
28 | color: var(--primary);
29 | margin-top: -1px;
30 | }
31 | .select .head,
32 | .range .head,
33 | .color .head {
34 | transition: background $anim-duration, color $anim-duration;
35 | span {
36 | float: right;
37 | }
38 | &:active {
39 | background: var(--highlight);
40 | color: var(--select-foreground);
41 | }
42 | }
43 | .color .head span {
44 | display: inline-block;
45 | border: 1px solid var(--border);
46 | width: 15px;
47 | height: 15px;
48 | }
49 | .select ul {
50 | display: none;
51 | border-bottom: 1px solid var(--border);
52 | color: var(--foreground);
53 | &.open {
54 | display: block;
55 | }
56 | li {
57 | padding: $padding;
58 | transition: background $anim-duration, color $anim-duration;
59 | &:active {
60 | background: var(--highlight);
61 | color: var(--select-foreground);
62 | }
63 | }
64 | }
65 | .color ul {
66 | display: none;
67 | padding: $padding;
68 | font-size: 0;
69 | border-bottom: 1px solid var(--border);
70 | &.open {
71 | display: block;
72 | }
73 | li {
74 | display: inline-block;
75 | width: 20px;
76 | border: 1px solid var(--border);
77 | height: 20px;
78 | margin-right: 10px;
79 | }
80 | }
81 | .range .input-container {
82 | display: none;
83 | padding: $padding;
84 | border-bottom: 1px solid var(--border);
85 | position: relative;
86 | &.open {
87 | display: block;
88 | }
89 | .range-track {
90 | height: 4px;
91 | width: 100%;
92 | padding: 0 $padding;
93 | position: absolute;
94 | left: 0;
95 | top: 16px;
96 | .range-track-bar {
97 | background: var(--darker-background);
98 | border-radius: 2px;
99 | overflow: hidden;
100 | width: 100%;
101 | height: 4px;
102 | .range-track-progress {
103 | height: 100%;
104 | background: var(--accent);
105 | width: 50%;
106 | }
107 | }
108 | }
109 | input {
110 | -webkit-appearance: none;
111 | background: transparent;
112 | height: 4px;
113 | width: 100%;
114 | position: relative;
115 | top: -3px;
116 | margin: 0 auto;
117 | outline: none;
118 | border-radius: 2px;
119 | }
120 | input::-webkit-slider-thumb {
121 | -webkit-appearance: none;
122 | position: relative;
123 | top: 0px;
124 | z-index: 1;
125 | width: 16px;
126 | border: none;
127 | height: 16px;
128 | border-radius: 10px;
129 | border: 1px solid var(--border);
130 | background: radial-gradient(
131 | circle at center,
132 | var(--dark) 0,
133 | var(--dark) 15%,
134 | var(--light) 22%,
135 | var(--light) 100%
136 | );
137 | }
138 | }
139 | .switch {
140 | .checkbox {
141 | float: right;
142 | position: relative;
143 | vertical-align: top;
144 | width: 46px;
145 | height: 20px;
146 | padding: 3px;
147 | border-radius: 18px;
148 | border: 1px solid var(--border);
149 | cursor: pointer;
150 | background-image: linear-gradient(
151 | to bottom,
152 | var(--dark),
153 | var(--light) 25px
154 | );
155 | .input {
156 | position: absolute;
157 | top: 0;
158 | left: 0;
159 | opacity: 0;
160 | }
161 | .label {
162 | pointer-events: none;
163 | position: relative;
164 | display: block;
165 | height: 12px;
166 | font-size: 10px;
167 | text-transform: uppercase;
168 | background: var(--darker-background);
169 | border-radius: inherit;
170 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.12),
171 | inset 0 0 2px rgba(0, 0, 0, 0.15);
172 | transition: 0.15s ease-out;
173 | transition-property: opacity background;
174 | &:before,
175 | &:after {
176 | position: absolute;
177 | top: 50%;
178 | margin-top: -0.5em;
179 | line-height: 1;
180 | transition: inherit;
181 | }
182 | }
183 | .input:checked ~ .label {
184 | background: var(--accent);
185 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15),
186 | inset 0 0 3px rgba(0, 0, 0, 0.2);
187 | }
188 | .input:checked ~ .label:before {
189 | opacity: 0;
190 | }
191 | .input:checked ~ .label:after {
192 | opacity: 1;
193 | }
194 | .handle {
195 | position: absolute;
196 | pointer-events: none;
197 | top: 0;
198 | left: 0;
199 | width: 18px;
200 | height: 18px;
201 | border-radius: 10px;
202 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.2);
203 | background-image: linear-gradient(
204 | to bottom,
205 | var(--light) 40%,
206 | var(--dark)
207 | );
208 | transition: left 0.15s ease-out;
209 | }
210 | .handle:before {
211 | content: '';
212 | position: absolute;
213 | top: 50%;
214 | left: 50%;
215 | margin: -6px 0 0 -6px;
216 | width: 12px;
217 | height: 12px;
218 | border-radius: 6px;
219 | box-shadow: inset 0 1px rgba(0, 0, 0, 0.02);
220 | background-image: linear-gradient(to bottom, var(--dark), var(--light));
221 | }
222 | .input:checked ~ .handle {
223 | left: 30px;
224 | box-shadow: -1px 1px 5px rgba(0, 0, 0, 0.2);
225 | }
226 | }
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/Network/Network.js:
--------------------------------------------------------------------------------
1 | import Tool from '../DevTools/Tool'
2 | import { isEmpty, $, ms, trim, each, last } from '../lib/util'
3 | import { getFileName } from '../lib/fione'
4 | import evalCss from '../lib/evalCss'
5 | import chobitsu from 'chobitsu'
6 | import style from './Network.scss'
7 | import template from './Network.hbs'
8 | import detail from './detail.hbs'
9 | import requests from './requests.hbs'
10 |
11 | export default class Network extends Tool {
12 | constructor() {
13 | super()
14 |
15 | this._style = evalCss(style)
16 |
17 | this.name = 'network'
18 | this._requests = {}
19 | this._tpl = template
20 | this._detailTpl = detail
21 | this._requestsTpl = requests
22 | this._detailData = {}
23 | }
24 | init($el, container) {
25 | super.init($el)
26 |
27 | this._container = container
28 | this._bindEvent()
29 | this._appendTpl()
30 | }
31 | show() {
32 | super.show()
33 |
34 | this._render()
35 | }
36 | clear() {
37 | this._requests = {}
38 | this._render()
39 | }
40 | requests() {
41 | const ret = []
42 | each(this._requests, (request) => {
43 | ret.push(request)
44 | })
45 | return ret
46 | }
47 | _reqWillBeSent = (params) => {
48 | this._requests[params.requestId] = {
49 | name: getFileName(params.request.url),
50 | url: params.request.url,
51 | status: 'pending',
52 | type: 'unknown',
53 | subType: 'unknown',
54 | size: 0,
55 | data: params.request.postData,
56 | method: params.request.method,
57 | startTime: params.timestamp * 1000,
58 | time: 0,
59 | resTxt: '',
60 | done: false,
61 | reqHeaders: params.request.headers || {},
62 | resHeaders: {},
63 | }
64 | }
65 | _resReceivedExtraInfo = (params) => {
66 | const target = this._requests[params.requestId]
67 | if (!target) {
68 | return
69 | }
70 |
71 | target.resHeaders = params.headers
72 |
73 | this._updateType(target)
74 | this._render()
75 | }
76 | _updateType(target) {
77 | const contentType = target.resHeaders['content-type'] || ''
78 | const { type, subType } = getType(contentType)
79 | target.type = type
80 | target.subType = subType
81 | }
82 | _resReceived = (params) => {
83 | const target = this._requests[params.requestId]
84 | if (!target) {
85 | return
86 | }
87 |
88 | const { response } = params
89 | const { status, headers } = response
90 | target.status = status
91 | if (status < 200 || status >= 300) {
92 | target.hasErr = true
93 | }
94 | if (headers) {
95 | target.resHeaders = headers
96 | this._updateType(target)
97 | }
98 |
99 | this._render()
100 | }
101 | _loadingFinished = (params) => {
102 | const target = this._requests[params.requestId]
103 | if (!target) {
104 | return
105 | }
106 |
107 | const time = params.timestamp * 1000
108 | target.time = time - target.startTime
109 | target.displayTime = ms(target.time)
110 |
111 | target.size = params.encodedDataLength
112 | target.done = true
113 | target.resTxt = chobitsu.domain('Network').getResponseBody({
114 | requestId: params.requestId,
115 | }).body
116 |
117 | this._render()
118 | }
119 | _bindEvent() {
120 | const $el = this._$el
121 | const container = this._container
122 |
123 | const self = this
124 |
125 | $el
126 | .on('click', '.eruda-request', function () {
127 | const id = $(this).data('id')
128 | const data = self._requests[id]
129 |
130 | if (!data.done) return
131 |
132 | self._showDetail(data)
133 | })
134 | .on('click', '.eruda-clear-request', () => this.clear())
135 | .on('click', '.eruda-back', () => this._hideDetail())
136 | .on('click', '.eruda-http .eruda-response', () => {
137 | const data = this._detailData
138 | const resTxt = data.resTxt
139 |
140 | switch (data.subType) {
141 | case 'css':
142 | return showSources('css', resTxt)
143 | case 'html':
144 | return showSources('html', resTxt)
145 | case 'javascript':
146 | return showSources('js', resTxt)
147 | case 'json':
148 | return showSources('object', resTxt)
149 | }
150 | switch (data.type) {
151 | case 'image':
152 | return showSources('img', data.url)
153 | }
154 | })
155 |
156 | function showSources(type, data) {
157 | const sources = container.get('sources')
158 | if (!sources) return
159 |
160 | sources.set(type, data)
161 |
162 | container.showTool('sources')
163 | }
164 |
165 | chobitsu.domain('Network').enable()
166 |
167 | const network = chobitsu.domain('Network')
168 | network.on('requestWillBeSent', this._reqWillBeSent)
169 | network.on('responseReceivedExtraInfo', this._resReceivedExtraInfo)
170 | network.on('responseReceived', this._resReceived)
171 | network.on('loadingFinished', this._loadingFinished)
172 | }
173 | destroy() {
174 | super.destroy()
175 |
176 | evalCss.remove(this._style)
177 |
178 | const network = chobitsu.domain('Network')
179 | network.off('requestWillBeSent', this._reqWillBeSent)
180 | network.off('responseReceivedExtraInfo', this._resReceivedExtraInfo)
181 | network.off('responseReceived', this._resReceived)
182 | network.off('loadingFinished', this._loadingFinished)
183 | }
184 | _showDetail(data) {
185 | if (data.resTxt && trim(data.resTxt) === '') {
186 | delete data.resTxt
187 | }
188 | if (isEmpty(data.resHeaders)) {
189 | delete data.resHeaders
190 | }
191 | if (isEmpty(data.reqHeaders)) {
192 | delete data.reqHeaders
193 | }
194 | this._$detail.html(this._detailTpl(data)).show()
195 | this._detailData = data
196 | }
197 | _hideDetail() {
198 | this._$detail.hide()
199 | }
200 | _appendTpl() {
201 | const $el = this._$el
202 | $el.html(this._tpl())
203 | this._$detail = $el.find('.eruda-detail')
204 | this._$requests = $el.find('.eruda-requests')
205 | }
206 | _render() {
207 | if (!this.active) return
208 |
209 | const renderData = {}
210 |
211 | if (!isEmpty(this._requests)) renderData.requests = this._requests
212 |
213 | this._renderHtml(this._requestsTpl(renderData))
214 | }
215 | _renderHtml(html) {
216 | if (html === this._lastHtml) return
217 | this._lastHtml = html
218 | this._$requests.html(html)
219 | }
220 | }
221 |
222 | function getType(contentType) {
223 | if (!contentType) return 'unknown'
224 |
225 | const type = contentType.split(';')[0].split('/')
226 |
227 | return {
228 | type: type[0],
229 | subType: last(type),
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/Snippets/defSnippets.js:
--------------------------------------------------------------------------------
1 | import logger from '../lib/logger'
2 | import emitter from '../lib/emitter'
3 | import {
4 | Url,
5 | now,
6 | each,
7 | isStr,
8 | startWith,
9 | $,
10 | upperFirst,
11 | loadJs,
12 | trim,
13 | } from '../lib/util'
14 | import { safeStorage } from '../lib/fione'
15 | import { isErudaEl } from '../lib/extraUtil'
16 | import evalCss from '../lib/evalCss'
17 | import searchTextStyle from './searchText.scss'
18 |
19 | let style = null
20 |
21 | export default [
22 | {
23 | name: 'Border All',
24 | fn() {
25 | if (style) {
26 | evalCss.remove(style)
27 | style = null
28 | return
29 | }
30 |
31 | style = evalCss(
32 | '* { outline: 2px dashed #707d8b; outline-offset: -3px; }',
33 | document.head
34 | )
35 | },
36 | desc: 'Add color borders to all elements',
37 | },
38 | {
39 | name: 'Refresh Page',
40 | fn() {
41 | const url = new Url()
42 | url.setQuery('timestamp', now())
43 |
44 | window.location.replace(url.toString())
45 | },
46 | desc: 'Add timestamp to url and refresh',
47 | },
48 | {
49 | name: 'Search Text',
50 | fn() {
51 | const keyword = prompt('Enter the text') || ''
52 |
53 | if (trim(keyword) === '') return
54 |
55 | search(keyword)
56 | },
57 | desc: 'Highlight given text on page',
58 | },
59 | {
60 | name: 'Edit Page',
61 | fn() {
62 | const body = document.body
63 |
64 | body.contentEditable = body.contentEditable !== 'true'
65 | },
66 | desc: 'Toggle body contentEditable',
67 | },
68 | {
69 | name: 'Fit Screen',
70 | // https://achrafkassioui.com/birdview/
71 | fn() {
72 | const body = document.body
73 | const html = document.documentElement
74 | const $body = $(body)
75 | if ($body.data('scaled')) {
76 | window.scrollTo(0, +$body.data('scaled'))
77 | $body.rmAttr('data-scaled')
78 | $body.css('transform', 'none')
79 | } else {
80 | const documentHeight = Math.max(
81 | body.scrollHeight,
82 | body.offsetHeight,
83 | html.clientHeight,
84 | html.scrollHeight,
85 | html.offsetHeight
86 | )
87 | const viewportHeight = Math.max(
88 | document.documentElement.clientHeight,
89 | window.innerHeight || 0
90 | )
91 | const scaleVal = viewportHeight / documentHeight
92 | $body.css('transform', `scale(${scaleVal})`)
93 | $body.data('scaled', window.scrollY)
94 | window.scrollTo(0, documentHeight / 2 - viewportHeight / 2)
95 | }
96 | },
97 | desc: 'Scale down the whole page to fit screen',
98 | },
99 | {
100 | name: 'Load Fps Plugin',
101 | fn() {
102 | loadPlugin('fps')
103 | },
104 | desc: 'Display page fps',
105 | },
106 | {
107 | name: 'Load Features Plugin',
108 | fn() {
109 | loadPlugin('features')
110 | },
111 | desc: 'Browser feature detections',
112 | },
113 | {
114 | name: 'Load Timing Plugin',
115 | fn() {
116 | loadPlugin('timing')
117 | },
118 | desc: 'Show performance and resource timing',
119 | },
120 | {
121 | name: 'Load Memory Plugin',
122 | fn() {
123 | loadPlugin('memory')
124 | },
125 | desc: 'Display memory',
126 | },
127 | {
128 | name: 'Load Code Plugin',
129 | fn() {
130 | loadPlugin('code')
131 | },
132 | desc: 'Edit and run JavaScript',
133 | },
134 | {
135 | name: 'Load Benchmark Plugin',
136 | fn() {
137 | loadPlugin('benchmark')
138 | },
139 | desc: 'Run JavaScript benchmarks',
140 | },
141 | {
142 | name: 'Load Geolocation Plugin',
143 | fn() {
144 | loadPlugin('geolocation')
145 | },
146 | desc: 'Test geolocation',
147 | },
148 | {
149 | name: 'Load Dom Plugin',
150 | fn() {
151 | loadPlugin('dom')
152 | },
153 | desc: 'Navigate dom tree',
154 | },
155 | {
156 | name: 'Load Orientation Plugin',
157 | fn() {
158 | loadPlugin('orientation')
159 | },
160 | desc: 'Test orientation api',
161 | },
162 | {
163 | name: 'Load Touches Plugin',
164 | fn() {
165 | loadPlugin('touches')
166 | },
167 | desc: 'Visualize screen touches',
168 | },
169 | {
170 | name: 'Restore Settings',
171 | fn() {
172 | const store = safeStorage('local')
173 |
174 | const data = JSON.parse(JSON.stringify(store))
175 |
176 | each(data, (val, key) => {
177 | if (!isStr(val)) return
178 |
179 | if (startWith(key, 'eruda')) store.removeItem(key)
180 | })
181 |
182 | window.location.reload()
183 | },
184 | desc: 'Restore defaults and reload',
185 | },
186 | ]
187 |
188 | evalCss(searchTextStyle, document.head)
189 |
190 | function search(text) {
191 | const root = document.body
192 | const regText = new RegExp(text, 'ig')
193 |
194 | traverse(root, (node) => {
195 | const $node = $(node)
196 |
197 | if (!$node.hasClass('eruda-search-highlight-block')) return
198 |
199 | return document.createTextNode($node.text())
200 | })
201 |
202 | traverse(root, (node) => {
203 | if (node.nodeType !== 3) return
204 |
205 | let val = node.nodeValue
206 | val = val.replace(
207 | regText,
208 | (match) => `
${match}`
209 | )
210 | if (val === node.nodeValue) return
211 |
212 | const $ret = $(document.createElement('div'))
213 |
214 | $ret.html(val)
215 | $ret.addClass('eruda-search-highlight-block')
216 |
217 | return $ret.get(0)
218 | })
219 | }
220 |
221 | function traverse(root, processor) {
222 | const childNodes = root.childNodes
223 |
224 | if (isErudaEl(root)) return
225 |
226 | for (let i = 0, len = childNodes.length; i < len; i++) {
227 | const newNode = traverse(childNodes[i], processor)
228 | if (newNode) root.replaceChild(newNode, childNodes[i])
229 | }
230 |
231 | return processor(root)
232 | }
233 |
234 | function loadPlugin(name) {
235 | const globalName = 'eruda' + upperFirst(name)
236 | if (window[globalName]) return
237 |
238 | let protocol = location.protocol
239 | if (!startWith(protocol, 'http')) protocol = 'http:'
240 |
241 | loadJs(
242 | `${protocol}//cdn.jsdelivr.net/npm/eruda-${name}@${pluginVersion[name]}`,
243 | (isLoaded) => {
244 | if (!isLoaded || !window[globalName])
245 | return logger.error('Fail to load plugin ' + name)
246 |
247 | emitter.emit(emitter.ADD, window[globalName])
248 | emitter.emit(emitter.SHOW, name)
249 | }
250 | )
251 | }
252 |
253 | const pluginVersion = {
254 | fps: '2.0.0',
255 | features: '2.0.0',
256 | timing: '2.0.0',
257 | memory: '2.0.0',
258 | code: '2.0.0',
259 | benchmark: '2.0.0',
260 | geolocation: '2.0.0',
261 | dom: '2.0.0',
262 | orientation: '2.0.0',
263 | touches: '2.0.0',
264 | }
265 |
--------------------------------------------------------------------------------
/src/Elements/Elements.scss:
--------------------------------------------------------------------------------
1 | @import '../style/variable';
2 | @import '../style/mixin';
3 |
4 | #elements {
5 | padding-bottom: 40px;
6 | font-size: 14px;
7 | .show-area {
8 | @include overflow-auto(y);
9 | height: 100%;
10 | }
11 | .parents {
12 | @include overflow-auto(x);
13 | background: var(--darker-background);
14 | color: var(--primary);
15 | padding: $padding;
16 | white-space: nowrap;
17 | border-bottom: 1px solid var(--border);
18 | cursor: pointer;
19 | font-size: $font-size-s;
20 | li {
21 | display: inline-block;
22 | .parent {
23 | display: inline-block;
24 | }
25 | &:last-child {
26 | margin-right: 0;
27 | }
28 | }
29 | .icon-arrow-right {
30 | font-size: 8px;
31 | position: relative;
32 | top: -1px;
33 | }
34 | }
35 | .breadcrumb {
36 | @include breadcrumb();
37 | cursor: pointer;
38 | transition: background $anim-duration, color $anim-duration;
39 | &:active {
40 | background: var(--highlight);
41 | color: var(--select-foreground);
42 | span {
43 | color: var(--select-foreground);
44 | }
45 | }
46 | }
47 | .section {
48 | border-bottom: 1px solid var(--border);
49 | color: var(--foreground);
50 | h2 {
51 | @include right-btn();
52 | color: var(--primary);
53 | background: var(--darker-background);
54 | border-top: 1px solid var(--border);
55 | padding: $padding;
56 | font-size: $font-size;
57 | transition: background $anim-duration;
58 | &.active-effect {
59 | cursor: pointer;
60 | }
61 | &.active-effect:active {
62 | background: var(--highlight);
63 | color: var(--select-foreground);
64 | }
65 | }
66 | margin-bottom: 10px;
67 | }
68 | .children {
69 | background: var(--darker-background);
70 | color: var(--foreground);
71 | margin-bottom: 10px !important;
72 | border-bottom: 1px solid var(--border);
73 | li {
74 | @include overflow-auto(x);
75 | cursor: default;
76 | padding: $padding;
77 | border-top: 1px solid var(--border);
78 | white-space: nowrap;
79 | transition: background $anim-duration, color $anim-duration;
80 | span {
81 | transition: color $anim-duration;
82 | }
83 | &.active-effect {
84 | cursor: pointer;
85 | }
86 | &.active-effect:active {
87 | background: var(--highlight);
88 | color: var(--select-foreground);
89 | span {
90 | color: var(--select-foreground);
91 | }
92 | }
93 | }
94 | }
95 | .attributes {
96 | font-size: $font-size-s;
97 | a {
98 | color: var(--link-color);
99 | }
100 | .table-wrapper {
101 | @include overflow-auto(x);
102 | }
103 | table {
104 | td {
105 | padding: 5px 10px;
106 | }
107 | }
108 | }
109 | .text-content {
110 | background: #fff;
111 | .content {
112 | @include overflow-auto(x);
113 | padding: $padding;
114 | }
115 | }
116 | .style-color {
117 | position: relative;
118 | top: 1px;
119 | width: 10px;
120 | height: 10px;
121 | border-radius: 50%;
122 | margin-right: 2px;
123 | border: 1px solid var(--border);
124 | display: inline-block;
125 | }
126 | .box-model {
127 | @include overflow-auto(x);
128 | color: #222;
129 | font-size: $font-size-s;
130 | padding: $padding;
131 | text-align: center;
132 | white-space: nowrap;
133 | border-bottom: 1px solid var(--color);
134 | .label {
135 | position: absolute;
136 | margin-left: 3px;
137 | padding: 0 2px;
138 | }
139 | .top,
140 | .left,
141 | .right,
142 | .bottom {
143 | display: inline-block;
144 | }
145 | .left,
146 | .right {
147 | vertical-align: middle;
148 | }
149 | .position,
150 | .margin,
151 | .border,
152 | .padding,
153 | .content {
154 | position: relative;
155 | background: #fff;
156 | display: inline-block;
157 | text-align: center;
158 | vertical-align: middle;
159 | padding: 3px;
160 | margin: 3px;
161 | }
162 | .position {
163 | border: 1px grey dotted;
164 | }
165 | .margin {
166 | border: 1px dashed;
167 | background: rgba(246, 178, 107, 0.66);
168 | }
169 | .border {
170 | border: 1px #000 solid;
171 | background: rgba(255, 229, 153, 0.66);
172 | }
173 | .padding {
174 | border: 1px grey dashed;
175 | background: rgba(147, 196, 125, 0.55);
176 | }
177 | .content {
178 | border: 1px grey solid;
179 | min-width: 100px;
180 | background: rgba(111, 168, 220, 0.66);
181 | }
182 | }
183 | .computed-style {
184 | font-size: $font-size-s;
185 | a {
186 | color: var(--link-color);
187 | }
188 | .table-wrapper {
189 | @include overflow-auto(y);
190 | max-height: 200px;
191 | border-top: 1px solid var(--border);
192 | }
193 | table {
194 | td {
195 | padding: 5px 10px;
196 | &.key {
197 | white-space: nowrap;
198 | color: var(--var-color);
199 | }
200 | }
201 | }
202 | }
203 | .styles {
204 | font-size: $font-size-s;
205 | .style-wrapper {
206 | padding: $padding;
207 | .style-rules {
208 | border: 1px solid var(--border);
209 | padding: $padding;
210 | margin-bottom: 10px;
211 | .rule {
212 | padding-left: 2em;
213 | word-break: break-all;
214 | a {
215 | color: var(--link-color);
216 | }
217 | span {
218 | color: var(--var-color);
219 | }
220 | }
221 | &:last-child {
222 | margin-bottom: 0;
223 | }
224 | }
225 | }
226 | }
227 | .listeners {
228 | font-size: $font-size-s;
229 | .listener-wrapper {
230 | padding: $padding;
231 | .listener {
232 | margin-bottom: 10px;
233 | overflow: hidden;
234 | border: 1px solid var(--border);
235 | .listener-type {
236 | padding: $padding;
237 | background: var(--darker-background);
238 | color: var(--primary);
239 | }
240 | .listener-content {
241 | li {
242 | @include overflow-auto(x);
243 | padding: $padding;
244 | border-top: none;
245 | }
246 | }
247 | }
248 | }
249 | }
250 | .bottom-bar {
251 | height: 40px;
252 | background: var(--darker-background);
253 | position: absolute;
254 | left: 0;
255 | bottom: 0;
256 | width: 100%;
257 | font-size: 0;
258 | border-top: 1px solid var(--border);
259 | .btn {
260 | cursor: pointer;
261 | text-align: center;
262 | color: var(--primary);
263 | font-size: 14px;
264 | line-height: 40px;
265 | width: 25%;
266 | display: inline-block;
267 | transition: background $anim-duration, color $anim-duration;
268 | &:active {
269 | background: var(--highlight);
270 | color: var(--select-foreground);
271 | }
272 | &.active {
273 | color: var(--accent);
274 | }
275 | }
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/src/Dom/Dom.js:
--------------------------------------------------------------------------------
1 | import Tool from '../DevTools/Tool'
2 | import { each, $, toArr } from '../lib/util'
3 | import evalCss from '../lib/evalCss'
4 |
5 | import style from './style.scss'
6 | import htmlTag from './htmlTag.hbs'
7 | import textNode from './textNode.hbs'
8 | import htmlComment from './htmlComment.hbs'
9 | import template from './template.hbs'
10 |
11 | export default class Dom extends Tool {
12 | constructor() {
13 | super()
14 | this.name = 'dom'
15 | this._style = evalCss(style)
16 | this._isInit = false
17 | this._htmlTagTpl = htmlTag
18 | this._textNodeTpl = textNode
19 | this._selectedEl = document.documentElement
20 | this._htmlCommentTpl = htmlComment
21 | this._elementChangeHandler = (el) => {
22 | if (this._selectedEl === el) return
23 | this.select(el)
24 | }
25 | }
26 | init($el, container) {
27 | super.init($el)
28 | this._container = container
29 | $el.html(template())
30 | this._$domTree = $el.find('.eruda-dom-tree')
31 |
32 | this._bindEvent()
33 | }
34 | show() {
35 | super.show()
36 |
37 | if (!this._isInit) this._initTree()
38 | }
39 | hide() {
40 | super.hide()
41 | }
42 | select(el) {
43 | const els = []
44 | els.push(el)
45 | while (el.parentElement) {
46 | els.unshift(el.parentElement)
47 | el = el.parentElement
48 | }
49 | while (els.length > 0) {
50 | el = els.shift()
51 | const erudaDom = el.erudaDom
52 | if (erudaDom) {
53 | if (erudaDom.close && erudaDom.open) {
54 | erudaDom.close()
55 | erudaDom.open()
56 | }
57 | } else {
58 | break
59 | }
60 | if (els.length === 0 && el.erudaDom) {
61 | el.erudaDom.select()
62 | }
63 | }
64 | }
65 | destroy() {
66 | super.destroy()
67 | evalCss.remove(this._style)
68 | const elements = this._container.get('elements')
69 | if (elements) {
70 | elements.off('change', this._elementChangeHandler)
71 | }
72 | }
73 | _bindEvent() {
74 | const container = this._container
75 |
76 | const elements = container.get('elements')
77 | if (elements) {
78 | elements.on('change', this._elementChangeHandler)
79 | }
80 |
81 | this._$el.on('click', '.eruda-inspect', () => {
82 | this._setElement(this._selectedEl)
83 | if (elements) container.showTool('elements')
84 | })
85 | }
86 | _setElement(el) {
87 | const elements = this._container.get('elements')
88 | if (!elements) return
89 |
90 | elements.set(el)
91 | }
92 | _initTree() {
93 | this._isInit = true
94 |
95 | this._renderChildren(null, this._$domTree)
96 | this.select(document.body)
97 | }
98 | _renderChildren(node, $container) {
99 | let children
100 | if (!node) {
101 | children = [document.documentElement]
102 | } else {
103 | children = toArr(node.childNodes)
104 | }
105 |
106 | const container = $container.get(0)
107 |
108 | if (node) {
109 | children.push({
110 | nodeType: 'END_TAG',
111 | node,
112 | })
113 | }
114 | each(children, (child) => this._renderChild(child, container))
115 | }
116 | _renderChild(child, container) {
117 | const $tag = createEl('li')
118 | let isEndTag = false
119 |
120 | $tag.addClass('eruda-tree-item')
121 | if (child.nodeType === child.ELEMENT_NODE) {
122 | const childCount = child.childNodes.length
123 | const expandable = childCount > 0
124 | const data = {
125 | ...getHtmlTagData(child),
126 | hasTail: expandable,
127 | }
128 | const hasOneTextNode =
129 | childCount === 1 && child.childNodes[0].nodeType === child.TEXT_NODE
130 | if (hasOneTextNode) {
131 | data.text = child.childNodes[0].nodeValue
132 | }
133 | $tag.html(this._htmlTagTpl(data))
134 | if (expandable && !hasOneTextNode) {
135 | $tag.addClass('eruda-expandable')
136 | }
137 | } else if (child.nodeType === child.TEXT_NODE) {
138 | const value = child.nodeValue
139 | if (value.trim() === '') return
140 |
141 | $tag.html(
142 | this._textNodeTpl({
143 | value,
144 | })
145 | )
146 | } else if (child.nodeType === child.COMMENT_NODE) {
147 | const value = child.nodeValue
148 | if (value.trim() === '') return
149 |
150 | $tag.html(
151 | this._htmlCommentTpl({
152 | value,
153 | })
154 | )
155 | } else if (child.nodeType === 'END_TAG') {
156 | isEndTag = true
157 | child = child.node
158 | $tag.html(
159 | `
</${child.tagName.toLocaleLowerCase()}>`
160 | )
161 | } else {
162 | return
163 | }
164 | const $children = createEl('ul')
165 | $children.addClass('eruda-children')
166 |
167 | container.appendChild($tag.get(0))
168 | container.appendChild($children.get(0))
169 |
170 | if (child.nodeType !== child.ELEMENT_NODE) return
171 |
172 | let erudaDom = {}
173 |
174 | if ($tag.hasClass('eruda-expandable')) {
175 | const open = () => {
176 | $tag.html(
177 | this._htmlTagTpl({
178 | ...getHtmlTagData(child),
179 | hasTail: false,
180 | })
181 | )
182 | $tag.addClass('eruda-expanded')
183 | this._renderChildren(child, $children)
184 | }
185 | const close = () => {
186 | $children.html('')
187 | $tag.html(
188 | this._htmlTagTpl({
189 | ...getHtmlTagData(child),
190 | hasTail: true,
191 | })
192 | )
193 | $tag.rmClass('eruda-expanded')
194 | }
195 | const toggle = () => {
196 | if ($tag.hasClass('eruda-expanded')) {
197 | close()
198 | } else {
199 | open()
200 | }
201 | }
202 | $tag.on('click', '.eruda-toggle-btn', (e) => {
203 | e.stopPropagation()
204 | toggle()
205 | })
206 | erudaDom = {
207 | open,
208 | close,
209 | }
210 | }
211 |
212 | const select = () => {
213 | this._$el.find('.eruda-selected').rmClass('eruda-selected')
214 | $tag.addClass('eruda-selected')
215 | this._selectedEl = child
216 | this._setElement(child)
217 | }
218 | $tag.on('click', select)
219 | erudaDom.select = select
220 | if (!isEndTag) child.erudaDom = erudaDom
221 | }
222 | }
223 |
224 | function getHtmlTagData(el) {
225 | const ret = {}
226 |
227 | ret.tagName = el.tagName.toLocaleLowerCase()
228 | const attributes = []
229 | each(el.attributes, (attribute) => {
230 | const { name, value } = attribute
231 | attributes.push({
232 | name,
233 | value,
234 | underline: isUrlAttribute(el, name),
235 | })
236 | })
237 | ret.attributes = attributes
238 |
239 | return ret
240 | }
241 |
242 | function isUrlAttribute(el, name) {
243 | const tagName = el.tagName
244 | if (
245 | tagName === 'SCRIPT' ||
246 | tagName === 'IMAGE' ||
247 | tagName === 'VIDEO' ||
248 | tagName === 'AUDIO'
249 | ) {
250 | if (name === 'src') return true
251 | }
252 |
253 | if (tagName === 'LINK') {
254 | if (name === 'href') return true
255 | }
256 |
257 | return false
258 | }
259 |
260 | function createEl(name) {
261 | return $(document.createElement(name))
262 | }
263 |
--------------------------------------------------------------------------------
/src/Settings/Settings.js:
--------------------------------------------------------------------------------
1 | import Tool from '../DevTools/Tool'
2 | import { $, LocalStore, uniqId, each, filter, isStr, clone } from '../lib/util'
3 | import evalCss from '../lib/evalCss'
4 | import style from './Settings.scss'
5 |
6 | import switchTpl from './switch.hbs'
7 | import selectTpl from './select.hbs'
8 | import rangeTpl from './range.hbs'
9 | import colorTpl from './color.hbs'
10 |
11 | export default class Settings extends Tool {
12 | constructor() {
13 | super()
14 |
15 | this._style = evalCss(style)
16 |
17 | this.name = 'settings'
18 | this._switchTpl = switchTpl
19 | this._selectTpl = selectTpl
20 | this._rangeTpl = rangeTpl
21 | this._colorTpl = colorTpl
22 | this._settings = []
23 | }
24 | init($el) {
25 | super.init($el)
26 |
27 | this._bindEvent()
28 | }
29 | remove(config, key) {
30 | if (isStr(config)) {
31 | this._$el.find('.eruda-text').each(function () {
32 | const $this = $(this)
33 | if ($this.text() === config) $this.remove()
34 | })
35 | } else {
36 | this._settings = filter(this._settings, (setting) => {
37 | if (setting.config === config && setting.key === key) {
38 | this._$el.find('#' + setting.id).remove()
39 | return false
40 | }
41 |
42 | return true
43 | })
44 | }
45 |
46 | this._cleanSeparator()
47 |
48 | return this
49 | }
50 | destroy() {
51 | super.destroy()
52 |
53 | evalCss.remove(this._style)
54 | }
55 | clear() {
56 | this._settings = []
57 | this._$el.html('')
58 | }
59 | switch(config, key, desc) {
60 | const id = this._genId('settings')
61 |
62 | this._settings.push({ config, key, id })
63 |
64 | this._$el.append(
65 | this._switchTpl({
66 | desc,
67 | key,
68 | id,
69 | val: config.get(key),
70 | })
71 | )
72 |
73 | return this
74 | }
75 | color(
76 | config,
77 | key,
78 | desc,
79 | colors = ['#2196f3', '#707d8b', '#f44336', '#009688', '#ffc107']
80 | ) {
81 | const id = this._genId('settings')
82 |
83 | this._settings.push({ config, key, id })
84 |
85 | this._$el.append(
86 | this._colorTpl({
87 | desc,
88 | colors,
89 | id,
90 | val: config.get(key),
91 | })
92 | )
93 |
94 | return this
95 | }
96 | select(config, key, desc, selections) {
97 | const id = this._genId('settings')
98 |
99 | this._settings.push({ config, key, id })
100 |
101 | this._$el.append(
102 | this._selectTpl({
103 | desc,
104 | selections,
105 | id,
106 | val: config.get(key),
107 | })
108 | )
109 |
110 | return this
111 | }
112 | range(config, key, desc, { min = 0, max = 1, step = 0.1 }) {
113 | const id = this._genId('settings')
114 |
115 | this._settings.push({ config, key, min, max, step, id })
116 |
117 | const val = config.get(key)
118 |
119 | this._$el.append(
120 | this._rangeTpl({
121 | desc,
122 | min,
123 | max,
124 | step,
125 | val,
126 | progress: progress(val, min, max),
127 | id,
128 | })
129 | )
130 |
131 | return this
132 | }
133 | separator() {
134 | this._$el.append('
')
135 |
136 | return this
137 | }
138 | text(text) {
139 | this._$el.append(`
${text}
`)
140 |
141 | return this
142 | }
143 | // Merge adjacent separators
144 | _cleanSeparator() {
145 | const children = clone(this._$el.get(0).children)
146 |
147 | function isSeparator(node) {
148 | return node.getAttribute('class') === 'eruda-separator'
149 | }
150 |
151 | for (let i = 0, len = children.length; i < len - 1; i++) {
152 | if (isSeparator(children[i]) && isSeparator(children[i + 1])) {
153 | $(children[i]).remove()
154 | }
155 | }
156 | }
157 | _genId() {
158 | return uniqId('eruda-settings')
159 | }
160 | _closeAll() {
161 | this._$el.find('.eruda-open').rmClass('eruda-open')
162 | }
163 | _getSetting(id) {
164 | let ret
165 |
166 | each(this._settings, (setting) => {
167 | if (setting.id === id) ret = setting
168 | })
169 |
170 | return ret
171 | }
172 | _bindEvent() {
173 | const self = this
174 |
175 | this._$el
176 | .on('click', '.eruda-checkbox', function () {
177 | const $input = $(this).find('input')
178 | const id = $input.data('id')
179 | const val = $input.get(0).checked
180 |
181 | const setting = self._getSetting(id)
182 | setting.config.set(setting.key, val)
183 | })
184 | .on('click', '.eruda-select .eruda-head', function () {
185 | const $el = $(this).parent().find('ul')
186 | const isOpen = $el.hasClass('eruda-open')
187 |
188 | self._closeAll()
189 | isOpen ? $el.rmClass('eruda-open') : $el.addClass('eruda-open')
190 | })
191 | .on('click', '.eruda-select li', function () {
192 | const $this = $(this)
193 | const $ul = $this.parent()
194 | const val = $this.text()
195 | const id = $ul.data('id')
196 | const setting = self._getSetting(id)
197 |
198 | $ul.rmClass('eruda-open')
199 | $ul.parent().find('.eruda-head span').text(val)
200 |
201 | setting.config.set(setting.key, val)
202 | })
203 | .on('click', '.eruda-range .eruda-head', function () {
204 | const $el = $(this).parent().find('.eruda-input-container')
205 | const isOpen = $el.hasClass('eruda-open')
206 |
207 | self._closeAll()
208 | isOpen ? $el.rmClass('eruda-open') : $el.addClass('eruda-open')
209 | })
210 | .on('change', '.eruda-range input', function () {
211 | const $this = $(this)
212 | const $container = $this.parent()
213 | const id = $container.data('id')
214 | const val = +$this.val()
215 | const setting = self._getSetting(id)
216 |
217 | setting.config.set(setting.key, val)
218 | })
219 | .on('input', '.eruda-range input', function () {
220 | const $this = $(this)
221 | const $container = $this.parent()
222 | const id = $container.data('id')
223 | const val = +$this.val()
224 | const setting = self._getSetting(id)
225 | const { min, max } = setting
226 |
227 | $container.parent().find('.eruda-head span').text(val)
228 | $container
229 | .find('.eruda-range-track-progress')
230 | .css('width', progress(val, min, max) + '%')
231 | })
232 | .on('click', '.eruda-color .eruda-head', function () {
233 | const $el = $(this).parent().find('ul')
234 | const isOpen = $el.hasClass('eruda-open')
235 |
236 | self._closeAll()
237 | isOpen ? $el.rmClass('eruda-open') : $el.addClass('eruda-open')
238 | })
239 | .on('click', '.eruda-color li', function () {
240 | const $this = $(this)
241 | const $ul = $this.parent()
242 | const val = $this.css('background-color')
243 | const id = $ul.data('id')
244 | const setting = self._getSetting(id)
245 |
246 | $ul.rmClass('eruda-open')
247 | $ul.parent().find('.eruda-head span').css('background-color', val)
248 |
249 | setting.config.set(setting.key, val)
250 | })
251 | }
252 | static createCfg(name, data) {
253 | return new LocalStore('eruda-' + name, data)
254 | }
255 | }
256 |
257 | const progress = (val, min, max) =>
258 | (((val - min) / (max - min)) * 100).toFixed(2)
259 |
--------------------------------------------------------------------------------
/src/eruda.js:
--------------------------------------------------------------------------------
1 | import EntryBtn from './EntryBtn/EntryBtn'
2 | import DevTools from './DevTools/DevTools'
3 | import Tool from './DevTools/Tool'
4 | import Dom from './Dom/Dom'
5 | import Console from './Console/Console'
6 | import Network from './Network/Network'
7 | import Elements from './Elements/Elements'
8 | import Snippets from './Snippets/Snippets'
9 | import Resources from './Resources/Resources'
10 | import Info from './Info/Info'
11 | import Sources from './Sources/Sources'
12 | import Settings from './Settings/Settings'
13 | import emitter from './lib/emitter'
14 | import logger from './lib/logger'
15 | import extraUtil from './lib/extraUtil'
16 | import * as util from './lib/util'
17 | import {
18 | isFn,
19 | isNum,
20 | isObj,
21 | isMobile,
22 | viewportScale,
23 | detectBrowser,
24 | $,
25 | toArr,
26 | upperFirst,
27 | nextTick,
28 | } from './lib/util'
29 | import evalCss from './lib/evalCss'
30 | import chobitsu from 'chobitsu'
31 |
32 | import style from './style/style.scss'
33 | import resetStyle from './style/reset.scss'
34 | import iconStyle from './style/icon.css'
35 | import lunaConsoleStyle from 'luna-console/luna-console.css'
36 | import lunaObjectViewerStyle from 'luna-object-viewer/luna-object-viewer.css'
37 | import lunaNotificationStyle from 'luna-notification/luna-notification.css'
38 |
39 | export default {
40 | init({
41 | container,
42 | tool,
43 | autoScale = true,
44 | useShadowDom = true,
45 | defaults = {},
46 | } = {}) {
47 | if (this._isInit) return
48 |
49 | this._isInit = true
50 | this._scale = 1
51 |
52 | this._initContainer(container, useShadowDom)
53 | this._initStyle()
54 | this._initDevTools(defaults)
55 | this._initEntryBtn()
56 | this._initSettings()
57 | this._initTools(tool)
58 | this._registerListener()
59 |
60 | if (autoScale) this._autoScale()
61 | },
62 | _isInit: false,
63 | version: VERSION,
64 | util,
65 | chobitsu,
66 | Tool,
67 | Console,
68 | Elements,
69 | Network,
70 | Sources,
71 | Resources,
72 | Info,
73 | Snippets,
74 | Dom,
75 | Settings,
76 | get(name) {
77 | if (!this._checkInit()) return
78 |
79 | if (name === 'entryBtn') return this._entryBtn
80 |
81 | const devTools = this._devTools
82 |
83 | return name ? devTools.get(name) : devTools
84 | },
85 | add(tool) {
86 | if (!this._checkInit()) return
87 |
88 | if (isFn(tool)) tool = tool(this)
89 |
90 | this._devTools.add(tool)
91 |
92 | return this
93 | },
94 | remove(name) {
95 | this._devTools.remove(name)
96 |
97 | return this
98 | },
99 | show(name) {
100 | if (!this._checkInit()) return
101 |
102 | const devTools = this._devTools
103 |
104 | name ? devTools.showTool(name) : devTools.show()
105 |
106 | return this
107 | },
108 | hide() {
109 | if (!this._checkInit()) return
110 |
111 | this._devTools.hide()
112 |
113 | return this
114 | },
115 | destroy() {
116 | this._devTools.destroy()
117 | delete this._devTools
118 | this._entryBtn.destroy()
119 | delete this._entryBtn
120 | this._unregisterListener()
121 | this._$el.remove()
122 | evalCss.clear()
123 | this._isInit = false
124 | },
125 | scale(s) {
126 | if (isNum(s)) {
127 | this._scale = s
128 | emitter.emit(emitter.SCALE, s)
129 | return this
130 | }
131 |
132 | return this._scale
133 | },
134 | position(p) {
135 | const entryBtn = this._entryBtn
136 |
137 | if (isObj(p)) {
138 | entryBtn.setPos(p)
139 | return this
140 | }
141 |
142 | return entryBtn.getPos()
143 | },
144 | _autoScale() {
145 | if (!isMobile()) return
146 |
147 | this.scale(1 / viewportScale())
148 | },
149 | _registerListener() {
150 | this._addListener = (...args) => this.add(...args)
151 | this._showListener = (...args) => this.show(...args)
152 |
153 | emitter.on(emitter.ADD, this._addListener)
154 | emitter.on(emitter.SHOW, this._showListener)
155 | emitter.on(emitter.SCALE, evalCss.setScale)
156 | },
157 | _unregisterListener() {
158 | emitter.off(emitter.ADD, this._addListener)
159 | emitter.off(emitter.SHOW, this._showListener)
160 | emitter.off(emitter.SCALE, evalCss.setScale)
161 | },
162 | _checkInit() {
163 | if (!this._isInit) logger.error('Please call "eruda.init()" first')
164 | return this._isInit
165 | },
166 | _initContainer(el, useShadowDom) {
167 | if (!el) {
168 | el = document.createElement('div')
169 | document.documentElement.appendChild(el)
170 | el.style.all = 'initial'
171 | }
172 |
173 |
174 | evalCss(['.luna-dom-highlighter { all: initial; }', '.luna-dom-highlighter * { background: initial; }'])
175 |
176 | let shadowRoot
177 | if (useShadowDom) {
178 | if (el.attachShadow) {
179 | shadowRoot = el.attachShadow({ mode: 'open' })
180 | } else if (el.createShadowRoot) {
181 | shadowRoot = el.createShadowRoot()
182 | }
183 | if (shadowRoot) {
184 | // font-face doesn't work inside shadow dom.
185 | evalCss.container = document.head
186 | evalCss([iconStyle, lunaConsoleStyle, lunaObjectViewerStyle])
187 |
188 | el = document.createElement('div')
189 | shadowRoot.appendChild(el)
190 | this._shadowRoot = shadowRoot
191 | }
192 | }
193 |
194 | Object.assign(el, {
195 | id: 'eruda',
196 | className: 'eruda-container',
197 | contentEditable: false,
198 | })
199 |
200 | // http://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari
201 | if (detectBrowser().name === 'ios') el.setAttribute('ontouchstart', '')
202 |
203 | this._$el = $(el)
204 | },
205 | _initDevTools(defaults) {
206 | this._devTools = new DevTools(this._$el, {
207 | defaults,
208 | })
209 | },
210 | _initStyle() {
211 | const className = 'eruda-style-container'
212 | const $el = this._$el
213 |
214 | if (this._shadowRoot) {
215 | evalCss.container = this._shadowRoot
216 | evalCss(':host { all: initial }')
217 | } else {
218 | $el.append(`
`)
219 | evalCss.container = $el.find(`.${className}`).get(0)
220 | }
221 |
222 | evalCss([
223 | lunaObjectViewerStyle,
224 | lunaConsoleStyle,
225 | lunaNotificationStyle,
226 | style,
227 | resetStyle,
228 | iconStyle,
229 | ])
230 | },
231 | _initEntryBtn() {
232 | this._entryBtn = new EntryBtn(this._$el)
233 | this._entryBtn.on('click', () => this._devTools.toggle())
234 | },
235 | _initSettings() {
236 | const devTools = this._devTools
237 | const settings = new Settings()
238 |
239 | devTools.add(settings)
240 |
241 | this._entryBtn.initCfg(settings)
242 | devTools.initCfg(settings)
243 | },
244 | _initTools(
245 | tool = [
246 | 'console',
247 | 'elements',
248 | 'network',
249 | 'resources',
250 | 'sources',
251 | 'info',
252 | 'snippets',
253 | 'dom',
254 | ]
255 | ) {
256 | tool = toArr(tool)
257 |
258 | const devTools = this._devTools
259 |
260 | tool.forEach((name) => {
261 | const Tool = this[upperFirst(name)]
262 | try {
263 | if (Tool) devTools.add(new Tool())
264 | } catch (e) {
265 | // Use nextTick to make sure it is possible to be caught by console panel.
266 | nextTick(() => {
267 | logger.error(
268 | `Something wrong when initializing tool ${name}:`,
269 | e.message
270 | )
271 | })
272 | }
273 | })
274 |
275 | devTools.showTool(tool[0] || 'settings')
276 | },
277 | }
278 |
279 | extraUtil(util)
280 |
--------------------------------------------------------------------------------
/src/Sources/Sources.js:
--------------------------------------------------------------------------------
1 | import Tool from '../DevTools/Tool'
2 | import LunaObjectViewer from 'luna-object-viewer'
3 | import Settings from '../Settings/Settings'
4 | import { ajax, escape, trim, isStr, highlight } from '../lib/util'
5 | import evalCss from '../lib/evalCss'
6 |
7 | import style from './Sources.scss'
8 |
9 | import codeTpl from './code.hbs'
10 | import imgTpl from './image.hbs'
11 | import objTpl from './object.hbs'
12 | import rawTpl from './raw.hbs'
13 | import iframeTpl from './iframe.hbs'
14 |
15 | export default class Sources extends Tool {
16 | constructor() {
17 | super()
18 |
19 | this._style = evalCss(style)
20 |
21 | this.name = 'sources'
22 | this._showLineNum = true
23 | this._formatCode = true
24 | this._indentSize = 4
25 |
26 | this._loadTpl()
27 | }
28 | init($el, container) {
29 | super.init($el)
30 |
31 | this._container = container
32 | this._bindEvent()
33 | this._initCfg()
34 | }
35 | destroy() {
36 | super.destroy()
37 |
38 | evalCss.remove(this._style)
39 | this._rmCfg()
40 | }
41 | set(type, val) {
42 | if (type === 'img') {
43 | this._isFetchingData = true
44 |
45 | const img = new Image()
46 |
47 | const self = this
48 |
49 | img.onload = function () {
50 | self._isFetchingData = false
51 | self._data = {
52 | type: 'img',
53 | val: {
54 | width: this.width,
55 | height: this.height,
56 | src: val,
57 | },
58 | }
59 |
60 | self._render()
61 | }
62 | img.onerror = function () {
63 | self._isFetchingData = false
64 | }
65 |
66 | img.src = val
67 |
68 | return
69 | }
70 |
71 | this._data = { type, val }
72 |
73 | this._render()
74 |
75 | return this
76 | }
77 | show() {
78 | super.show()
79 |
80 | if (!this._data && !this._isFetchingData) {
81 | this._renderDef()
82 | }
83 |
84 | return this
85 | }
86 | _renderDef() {
87 | if (this._html) {
88 | this._data = {
89 | type: 'html',
90 | val: this._html,
91 | }
92 |
93 | return this._render()
94 | }
95 |
96 | if (this._isGettingHtml) return
97 | this._isGettingHtml = true
98 |
99 | ajax({
100 | url: location.href,
101 | success: (data) => (this._html = data),
102 | error: () => (this._html = 'Sorry, unable to fetch source code:('),
103 | complete: () => {
104 | this._isGettingHtml = false
105 | this._renderDef()
106 | },
107 | dataType: 'raw',
108 | })
109 | }
110 | _bindEvent() {
111 | this._container.on('showTool', (name, lastTool) => {
112 | if (name !== this.name && lastTool.name === this.name) {
113 | delete this._data
114 | }
115 | })
116 | }
117 | _loadTpl() {
118 | this._codeTpl = codeTpl
119 | this._imgTpl = imgTpl
120 | this._objTpl = objTpl
121 | this._rawTpl = rawTpl
122 | this._iframeTpl = iframeTpl
123 | }
124 | _rmCfg() {
125 | const cfg = this.config
126 |
127 | const settings = this._container.get('settings')
128 |
129 | if (!settings) return
130 |
131 | settings
132 | .remove(cfg, 'showLineNum')
133 | .remove(cfg, 'formatCode')
134 | .remove(cfg, 'indentSize')
135 | .remove('Sources')
136 | }
137 | _initCfg() {
138 | const cfg = (this.config = Settings.createCfg('sources', {
139 | showLineNum: true,
140 | formatCode: true,
141 | indentSize: 4,
142 | }))
143 |
144 | if (!cfg.get('showLineNum')) this._showLineNum = false
145 | if (!cfg.get('formatCode')) this._formatCode = false
146 | this._indentSize = cfg.get('indentSize')
147 |
148 | cfg.on('change', (key, val) => {
149 | switch (key) {
150 | case 'showLineNum':
151 | this._showLineNum = val
152 | return
153 | case 'formatCode':
154 | this._formatCode = val
155 | return
156 | case 'indentSize':
157 | this._indentSize = +val
158 | return
159 | }
160 | })
161 |
162 | const settings = this._container.get('settings')
163 | settings
164 | .text('Sources')
165 | .switch(cfg, 'showLineNum', 'Show Line Numbers')
166 | .switch(cfg, 'formatCode', 'Beautify Code')
167 | .select(cfg, 'indentSize', 'Indent Size', ['2', '4'])
168 | .separator()
169 | }
170 | async _render() {
171 | this._isInit = true
172 |
173 | const data = this._data
174 |
175 | switch (data.type) {
176 | case 'html':
177 | case 'js':
178 | case 'css':
179 | return this._renderCode()
180 | case 'img':
181 | return this._renderImg()
182 | case 'object':
183 | return this._renderObj()
184 | case 'raw':
185 | return this._renderRaw()
186 | case 'iframe':
187 | return this._renderIframe()
188 | }
189 | }
190 | _renderImg() {
191 | this._renderHtml(this._imgTpl(this._data.val))
192 | }
193 | async _renderCode() {
194 | const data = this._data
195 | const indent_size = this._indentSize
196 |
197 | let code = data.val
198 | const len = data.val.length
199 |
200 | const beautify = await import(/* webpackIgnore: true */ 'js-beautify')
201 |
202 | // If source code too big, don't process it.
203 | if (len < MAX_BEAUTIFY_LEN && this._formatCode) {
204 | switch (data.type) {
205 | case 'html':
206 | code = beautify.html(code, { unformatted: [], indent_size })
207 | break
208 | case 'css':
209 | code = beautify.css(code, { indent_size })
210 | break
211 | case 'js':
212 | code = beautify(code, { indent_size })
213 | break
214 | }
215 |
216 | const curTheme = evalCss.getCurTheme()
217 | code = highlight(code, data.type, {
218 | keyword: `color:${curTheme.keywordColor}`,
219 | number: `color:${curTheme.numberColor}`,
220 | operator: `color:${curTheme.operatorColor}`,
221 | comment: `color:${curTheme.commentColor}`,
222 | string: `color:${curTheme.stringColor}`,
223 | })
224 | } else {
225 | code = escape(code)
226 | }
227 |
228 | if (len < MAX_LINE_NUM_LEN && this._showLineNum) {
229 | code = code.split('\n').map((line, idx) => {
230 | if (trim(line) === '') line = ' '
231 |
232 | return {
233 | idx: idx + 1,
234 | val: line,
235 | }
236 | })
237 | }
238 |
239 | this._renderHtml(
240 | this._codeTpl({
241 | code,
242 | showLineNum: len < MAX_LINE_NUM_LEN && this._showLineNum,
243 | })
244 | )
245 | }
246 | _renderObj() {
247 | // Using cache will keep binding events to the same elements.
248 | this._renderHtml(this._objTpl(), false)
249 |
250 | let val = this._data.val
251 |
252 | try {
253 | if (isStr(val)) {
254 | val = JSON.parse(val)
255 | }
256 | /* eslint-disable no-empty */
257 | } catch (e) {}
258 |
259 | const objViewer = new LunaObjectViewer(
260 | this._$el.find('.eruda-json').get(0),
261 | {
262 | unenumerable: true,
263 | accessGetter: true,
264 | }
265 | )
266 | objViewer.set(val)
267 | }
268 | _renderRaw() {
269 | this._renderHtml(this._rawTpl({ val: this._data.val }))
270 | }
271 | _renderIframe() {
272 | this._renderHtml(this._iframeTpl({ src: this._data.val }))
273 | }
274 | _renderHtml(html, cache = true) {
275 | if (cache && html === this._lastHtml) return
276 | this._lastHtml = html
277 | this._$el.html(html)
278 | // Need setTimeout to make it work
279 | setTimeout(() => (this._$el.get(0).scrollTop = 0), 0)
280 | }
281 | }
282 |
283 | const MAX_BEAUTIFY_LEN = 100000
284 | const MAX_LINE_NUM_LEN = 400000
285 |
--------------------------------------------------------------------------------