├── .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 | -------------------------------------------------------------------------------- /src/Sources/iframe.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | eruda.init({ 2 | useShadowDom: false, 3 | }) 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/util.js 2 | src/lib/fione.js 3 | eruda.min.js -------------------------------------------------------------------------------- /src/Dom/textNode.hbs: -------------------------------------------------------------------------------- 1 | "{{value}}" -------------------------------------------------------------------------------- /src/Dom/htmlComment.hbs: -------------------------------------------------------------------------------- 1 | <!-- {{value}} --> -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fione"] 2 | path = fione 3 | url = https://github.com/liriliri/fione.git 4 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | tabWidth: 2, 4 | semi: false, 5 | } 6 | -------------------------------------------------------------------------------- /src/EntryBtn/EntryBtn.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /src/Sources/raw.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{val}}
3 |
4 | -------------------------------------------------------------------------------- /src/Dom/template.hbs: -------------------------------------------------------------------------------- 1 | 2 |
Inspect Selected Element
-------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | .idea/ 3 | dist/ 4 | node_modules/ 5 | test/lib/ 6 | coverage/ 7 | test/playground.html 8 | npm-debug.log 9 | package-lock.json -------------------------------------------------------------------------------- /src/Dom/upstream.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote": "https://github.com/liriliri/eruda-dom.git", 3 | "commit": "38d6ddcdc6faf0a1db914264fdf1b6e17bac693d", 4 | "branch": "master" 5 | } 6 | -------------------------------------------------------------------------------- /src/Info/Info.hbs: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/lib/emitter.js: -------------------------------------------------------------------------------- 1 | import { Emitter } from './util' 2 | 3 | const emitter = new Emitter() 4 | emitter.ADD = 'ADD' 5 | emitter.SHOW = 'SHOW' 6 | emitter.SCALE = 'SCALE' 7 | 8 | export default emitter 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './eruda' 2 | 3 | // Provide hooks for the user to extend the library. 4 | export { default as LunaConsole } from 'luna-console' 5 | export { default as EntryBtn } from './EntryBtn/EntryBtn' 6 | -------------------------------------------------------------------------------- /src/Network/Network.hbs: -------------------------------------------------------------------------------- 1 |
2 | Request 3 |
4 | 5 |
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /src/Snippets/searchText.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | 3 | .search-highlight-block { 4 | display: inline; 5 | .keyword { 6 | background: var(--console-warn-background); 7 | color: var(--console-warn-foreground); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/style/icon/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "rootDir": ".", 6 | "noEmit": true, 7 | "target": "ESNext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Sources/image.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{src}}
3 |
4 | 5 |
6 |
{{width}} × {{height}}
7 |
-------------------------------------------------------------------------------- /src/style/variable.scss: -------------------------------------------------------------------------------- 1 | $padding: 10px; 2 | 3 | $font-size: 14px; 4 | $font-size-s: 12px; 5 | $font-size-l: 16px; 6 | 7 | $font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', 8 | 'Segoe UI', Tahoma, sans-serif; 9 | 10 | $anim-duration: 0.3s; 11 | -------------------------------------------------------------------------------- /src/DevTools/DevTools.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
-------------------------------------------------------------------------------- /src/Settings/select.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{desc}} 4 | {{val}} 5 |
6 | 11 |
-------------------------------------------------------------------------------- /src/Settings/switch.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{desc}} 3 | 8 |
-------------------------------------------------------------------------------- /src/lib/logger.js: -------------------------------------------------------------------------------- 1 | import { Logger } from './util' 2 | 3 | let logger 4 | 5 | export default logger = new Logger( 6 | '[Eruda]', 7 | ENV === 'production' ? 'warn' : 'debug' 8 | ) 9 | 10 | logger.formatter = function (type, argList) { 11 | argList.unshift(this.name) 12 | 13 | return argList 14 | } 15 | -------------------------------------------------------------------------------- /src/style/icon/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Settings/color.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{desc}} 4 | 5 |
6 | 11 |
-------------------------------------------------------------------------------- /src/DevTools/Tool.js: -------------------------------------------------------------------------------- 1 | import { Class } from '../lib/util' 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 | }) 21 | -------------------------------------------------------------------------------- /src/Snippets/Snippets.hbs: -------------------------------------------------------------------------------- 1 | {{#each snippets}} 2 |
3 |

{{name}} 4 |
5 | 6 |
7 |

8 |
9 | {{desc}} 10 |
11 |
12 | {{/each}} 13 | -------------------------------------------------------------------------------- /src/style/icon/caret-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.eustia.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | fione: { 3 | library: ['fione', 'node_modules/eustia-module'], 4 | files: 'src/**/*.js', 5 | output: 'src/lib/fione.js', 6 | format: 'es', 7 | }, 8 | test: { 9 | library: ['node_modules/eustia-module'], 10 | files: ['test/*.js', 'test/*.html'], 11 | exclude: ['js'], 12 | namespace: 'util', 13 | output: 'test/util.js', 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/style/icon/caret-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/lib/abstract.scss: -------------------------------------------------------------------------------- 1 | .container .abstract { 2 | .key { 3 | color: var(--var-color); 4 | } 5 | .number { 6 | color: var(--number-color); 7 | } 8 | .null { 9 | color: var(--operator-color); 10 | } 11 | .string { 12 | color: var(--string-color); 13 | } 14 | .boolean { 15 | color: var(--keyword-color); 16 | } 17 | .special { 18 | color: var(--operator-color); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/sources.js: -------------------------------------------------------------------------------- 1 | describe('sources', function () { 2 | let tool = eruda.get('sources') 3 | let $tool = $('.eruda-sources') 4 | 5 | beforeEach(function () { 6 | eruda.show('sources') 7 | }) 8 | 9 | describe('js', function () { 10 | it('highlight', function () { 11 | tool.set('js', '/* test */') 12 | expect($tool.find('.eruda-content')).toContainHtml('/* test */') 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/network.js: -------------------------------------------------------------------------------- 1 | describe('network', function () { 2 | beforeEach(function () { 3 | eruda.show('network') 4 | }) 5 | 6 | describe('request', function () { 7 | it('xhr', function (done) { 8 | $('.eruda-clear-xhr').click() 9 | util.ajax.get(window.location.toString(), function () { 10 | expect($('.eruda-requests li')).toHaveLength(1) 11 | done() 12 | }) 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | browser: true, 5 | commonjs: true, 6 | es6: true, 7 | node: true, 8 | }, 9 | extends: 'eslint:recommended', 10 | parserOptions: { 11 | sourceType: 'module', 12 | }, 13 | globals: { 14 | VERSION: true, 15 | ENV: true, 16 | }, 17 | rules: { 18 | quotes: ['error', 'single'], 19 | 'prefer-const': 2, 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/Elements/BottomBar.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
-------------------------------------------------------------------------------- /src/lib/extraUtil.js: -------------------------------------------------------------------------------- 1 | import evalCss from './evalCss' 2 | 3 | export default function (util) { 4 | Object.assign(util, { 5 | evalCss, 6 | isErudaEl, 7 | }) 8 | } 9 | 10 | export function isErudaEl(el) { 11 | let parentNode = el.parentNode 12 | 13 | if (!parentNode) return false 14 | 15 | while (parentNode) { 16 | parentNode = parentNode.parentNode 17 | if (parentNode && parentNode.id === 'eruda') return true 18 | } 19 | 20 | return false 21 | } 22 | -------------------------------------------------------------------------------- /src/EntryBtn/EntryBtn.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | .entry-btn { 3 | width: 40px; 4 | height: 40px; 5 | display: flex; 6 | background: #000; 7 | opacity: 0.3; 8 | border-radius: 10px; 9 | position: relative; 10 | z-index: 1000; 11 | transition: opacity 0.3s; 12 | color: #fff; 13 | font-size: 25px; 14 | align-items: center; 15 | justify-content: center; 16 | &.active, 17 | &:active { 18 | opacity: 0.8; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/elements.js: -------------------------------------------------------------------------------- 1 | describe('elements', function () { 2 | let tool = eruda.get('elements') 3 | let $tool = $('.eruda-elements') 4 | 5 | beforeEach(function () { 6 | eruda.show('elements') 7 | }) 8 | 9 | describe('api', function () { 10 | it('set element', function () { 11 | tool.set(document.body) 12 | expect($tool.find('.eruda-parent')).toContainText('html') 13 | expect($tool.find('.eruda-breadcrumb')).toContainText('body') 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /src/style/icon/reset.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Settings/range.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{desc}} 4 | {{val}} 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 13 |
14 |
-------------------------------------------------------------------------------- /src/style/icon/warn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/style/icon/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/style/icon/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Network/requests.hbs: -------------------------------------------------------------------------------- 1 | {{#if requests}} 2 | {{#each requests}} 3 |
  • 4 | {{name}} 5 | {{status}} 6 | {{method}} 7 | {{subType}} 8 | {{size}} 9 | {{displayTime}} 10 |
  • 11 | {{/each}} 12 | {{else}} 13 |
  • Empty
  • 14 | {{/if}} -------------------------------------------------------------------------------- /test/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Test", 4 | "author": { 5 | "name": "Redhoodsu", 6 | "email": "surunzi@foxmail.com", 7 | "contact": [ 8 | { 9 | "location": "office", 10 | "number": 123456 11 | }, 12 | { 13 | "location": "A very very long long address!!!!!!!!!!!!", 14 | "number": 654321 15 | }, 16 | null 17 | ] 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /src/style/icon/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/DevTools/DevTools.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 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, height $anim-duration; 16 | .tools { 17 | @include overflow-auto(); 18 | height: 100%; 19 | width: 100%; 20 | position: relative; 21 | .tool { 22 | @include absolute(); 23 | overflow: hidden; 24 | display: none; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/info.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Info 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/eruda.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Features 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/console.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Console 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/elements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Elements 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Settings 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/snippets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Snippets 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/sources.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Sources 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Sources/code.hbs: -------------------------------------------------------------------------------- 1 | {{#if showLineNum}} 2 |
    3 | 4 | 5 | 6 | 11 | 16 | 17 | 18 |
    7 | {{#each code}} 8 |
    {{idx}}
    9 | {{/each}} 10 |
    12 | {{#each code}} 13 |
    {{{val}}}
    14 | {{/each}} 15 |
    19 |
    20 | {{else}} 21 |
    22 |
    {{{code}}}
    23 |
    24 | {{/if}} 25 | -------------------------------------------------------------------------------- /test/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Network 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/resources.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Resources 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Dom/htmlTag.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{! 3 | }}<{{! 4 | }}{{tagName}}{{! 5 | }}{{#each attributes}} 6 | 7 | {{name}}{{#if value}}="{{value}}"{{/if}}{{! 8 | }}{{! 9 | }}{{/each}}{{! 10 | }}>{{! 11 | }}{{! 12 | }}{{#if hasTail}}{{! 13 | }}{{#if text}}{{text}}{{else}}…{{/if}}{{! 14 | }}{{! 15 | }}<{{! 16 | }}/{{tagName}}{{! 17 | }}>{{! 18 | }} 19 | {{/if}} 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | ## Development Setup 4 | 5 | [Node.js](https://nodejs.org/en/) is needed for the development of eruda. 6 | 7 | After cloning the repo, run: 8 | 9 | ```bash 10 | # install npm dependencies. 11 | npm install 12 | # copy jasmine lib from node_modules to test folder. 13 | npm run setup 14 | ``` 15 | 16 | ## Commonly used NPM scripts 17 | 18 | ```bash 19 | # watch and auto re-build. 20 | npm run dev 21 | # build eruda.js 22 | npm run build 23 | # lint, build and test. 24 | npm run ci 25 | ``` 26 | 27 | ## Project Structure 28 | 29 | - **doc**: documents. 30 | - **build**: webpack configuration, and some other useful scripts. 31 | - **src**: source code, written in es2015. 32 | - **test**: contain pages for testing. 33 | -------------------------------------------------------------------------------- /src/style/icon/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/style/icon/select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Elements/Highlight.js: -------------------------------------------------------------------------------- 1 | import chobitsu from 'chobitsu' 2 | 3 | export default class Highlight { 4 | constructor() { 5 | this._isShow = false 6 | 7 | chobitsu.domain('Overlay').enable() 8 | } 9 | setEl(el) { 10 | this._target = el 11 | } 12 | show() { 13 | this._isShow = true 14 | const { nodeId } = chobitsu.domain('DOM').getNodeId({ node: this._target }) 15 | chobitsu.domain('Overlay').highlightNode({ 16 | nodeId, 17 | highlightConfig: { 18 | showInfo: true, 19 | contentColor: 'rgba(111, 168, 220, .66)', 20 | paddingColor: 'rgba(147, 196, 125, .55)', 21 | borderColor: 'rgba(255, 229, 153, .66)', 22 | marginColor: 'rgba(246, 178, 107, .66)', 23 | }, 24 | }) 25 | } 26 | destroy() { 27 | chobitsu.domain('Overlay').disable() 28 | } 29 | hide() { 30 | this._isShow = false 31 | chobitsu.domain('Overlay').hideHighlight() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /patches/licia-es+1.36.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/licia-es/uncaught.js b/node_modules/licia-es/uncaught.js 2 | index 81883ea..740f8f1 100644 3 | --- a/node_modules/licia-es/uncaught.js 4 | +++ b/node_modules/licia-es/uncaught.js 5 | @@ -18,20 +18,20 @@ SingleEmitter.mixin(exports); 6 | 7 | if (isBrowser) { 8 | window.addEventListener('error', event => { 9 | - callListeners(event.error); 10 | + callListeners(event.error, event); 11 | }); 12 | window.addEventListener('unhandledrejection', e => { 13 | - callListeners(e.reason); 14 | + callListeners(e.reason, e); 15 | }); 16 | } else { 17 | process.on('uncaughtException', callListeners); 18 | process.on('unhandledRejection', callListeners); 19 | } 20 | 21 | -function callListeners(err) { 22 | +function callListeners(...args) { 23 | if (!isOn) return; 24 | 25 | - exports.emit(err); 26 | + exports.emit(...args); 27 | } 28 | 29 | export default exports; 30 | -------------------------------------------------------------------------------- /src/Info/Info.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 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 | padding-bottom: 0; 15 | font-size: $font-size-l; 16 | color: var(--accent); 17 | } 18 | .content { 19 | margin: 0; 20 | user-select: text; 21 | color: var(--foreground); 22 | word-break: break-all; 23 | table { 24 | width: 100%; 25 | border-collapse: collapse; 26 | th, 27 | td { 28 | border: 1px solid var(--border); 29 | padding: 10px; 30 | } 31 | } 32 | * { 33 | user-select: text; 34 | } 35 | a { 36 | color: var(--link-color); 37 | } 38 | } 39 | .device-key, 40 | .system-key { 41 | width: 100px; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/style/icon/compress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Console/Console.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | 4 | All 5 | Error 6 | Warning 7 | Info 8 | 9 | 10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    Cancel
    20 |
    Execute
    21 |
    22 | 23 | 24 |
    25 |
    -------------------------------------------------------------------------------- /src/Snippets/Snippets.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 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 | color: var(--primary); 21 | background: var(--darker-background); 22 | transition: background $anim-duration; 23 | .btn { 24 | margin-left: 10px; 25 | float: right; 26 | text-align: center; 27 | width: 18px; 28 | height: 18px; 29 | line-height: 18px; 30 | font-size: $font-size-s; 31 | } 32 | } 33 | .description { 34 | color: var(--foreground); 35 | padding: $padding; 36 | transition: background $anim-duration; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/style/icon/expand.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/style/icon/clear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /patches/handlebars-loader+1.7.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/handlebars-loader/index.js b/node_modules/handlebars-loader/index.js 2 | index b024668..f6b8b9f 100644 3 | --- a/node_modules/handlebars-loader/index.js 4 | +++ b/node_modules/handlebars-loader/index.js 5 | @@ -374,10 +374,9 @@ module.exports = function(source) { 6 | 7 | // export as module if template is not blank 8 | var slug = template ? 9 | - 'var Handlebars = require(' + loaderUtils.stringifyRequest(loaderApi, runtimePath) + ');\n' 10 | - + 'function __default(obj) { return obj && (obj.__esModule ? obj["default"] : obj); }\n' 11 | - + 'module.exports = (Handlebars["default"] || Handlebars).template(' + template + ');' : 12 | - 'module.exports = function(){return "";};'; 13 | + 'import Handlebars from ' + loaderUtils.stringifyRequest(loaderApi, runtimePath) + ';\n' 14 | + + 'export default Handlebars.template(' + template + ');' : 15 | + 'export default function(){return "";};'; 16 | 17 | loaderAsyncCallback(null, slug); 18 | }; 19 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 19 |
    20 |

    hello world

    21 |
    22 | 27 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/boot.js: -------------------------------------------------------------------------------- 1 | function boot(name, cb) { 2 | // Need a little delay to make sure width and height of webpack dev server iframe are initialized. 3 | setTimeout(function () { 4 | let options = { 5 | useShadowDom: false, 6 | defaults: { 7 | displaySize: 50, 8 | transparency: 0.9, 9 | theme: 'Monokai Pro', 10 | }, 11 | } 12 | if (name) { 13 | options.tool = name === 'settings' ? [] : name 14 | } 15 | 16 | try { 17 | eruda.init(options) 18 | } catch (e) { 19 | alert(e) 20 | } 21 | eruda.show() 22 | 23 | cb && cb() 24 | 25 | if (name == null) return 26 | 27 | loadJs('lib/boot', function () { 28 | loadJs('lib/jasmine-jquery', function () { 29 | // This is needed to trigger jasmine initialization. 30 | loadJs(name, function () { 31 | window.onload() 32 | }) 33 | }) 34 | }) 35 | }, 500) 36 | } 37 | 38 | function loadJs(src, cb) { 39 | let script = document.createElement('script') 40 | script.src = src + '.js' 41 | script.onload = cb 42 | document.body.appendChild(script) 43 | } 44 | -------------------------------------------------------------------------------- /src/style/style.scss: -------------------------------------------------------------------------------- 1 | @import 'variable'; 2 | @import 'mixin'; 3 | @import 'luna'; 4 | 5 | .container { 6 | pointer-events: none; 7 | position: fixed; 8 | left: 0; 9 | top: 0; 10 | width: 100%; 11 | height: 100%; 12 | z-index: 1000000; 13 | color: var(--foreground); 14 | font-family: $font-family; 15 | font-size: $font-size; 16 | direction: ltr; 17 | * { 18 | box-sizing: border-box; 19 | pointer-events: all; 20 | user-select: none; 21 | -webkit-tap-highlight-color: transparent; 22 | -webkit-text-size-adjust: none; 23 | } 24 | ul { 25 | list-style: none; 26 | padding: 0; 27 | margin: 0; 28 | } 29 | h1, 30 | h2, 31 | h3, 32 | h4 { 33 | margin: 0; 34 | } 35 | } 36 | 37 | .hidden { 38 | display: none; 39 | } 40 | 41 | .tag-name-color { 42 | color: var(--tag-name-color); 43 | } 44 | 45 | .function-color { 46 | color: var(--function-color); 47 | } 48 | 49 | .attribute-name-color { 50 | color: var(--attribute-name-color); 51 | } 52 | 53 | .operator-color { 54 | color: var(--operator-color); 55 | } 56 | 57 | .string-color { 58 | color: var(--string-color); 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 22 | -------------------------------------------------------------------------------- /test/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | * { 7 | font-family: 'Avenir Next', Avenir, 'Helvetica Neue', Helvetica, 'Franklin Gothic Medium', 'Franklin Gothic', 'ITC Franklin Gothic', Arial, sans-serif; 8 | } 9 | 10 | header { 11 | position: relative; 12 | z-index: 15; 13 | background: #eda29b; 14 | text-align: center; 15 | color: #fff; 16 | padding: 10px; 17 | font-size: 30px; 18 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.05),0 1px 4px 0 rgba(0,0,0,.2), 0 3px 1px -2px rgba(0,0,0,.1); 19 | } 20 | 21 | nav ul { 22 | list-style: none; 23 | padding: 0; 24 | margin: 15px; 25 | } 26 | 27 | nav ul li { 28 | background: #f2d367; 29 | width: 50%; 30 | float: left; 31 | } 32 | 33 | nav ul li:nth-child(4n), nav ul li:nth-child(4n-3) { 34 | background: #e17555; 35 | } 36 | 37 | nav ul li a { 38 | text-align: center; 39 | width: 100%; 40 | height: 100%; 41 | box-sizing: border-box; 42 | display: block; 43 | padding: 10px; 44 | color: #e07556; 45 | font-size: 16px; 46 | text-decoration: none; 47 | } 48 | 49 | nav ul li:nth-child(4n) a, nav ul li:nth-child(4n-3) a{ 50 | color: #9c3c53; 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/style/icon/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Info/defInfo.js: -------------------------------------------------------------------------------- 1 | import { detectBrowser, detectOs, escape } from '../lib/util' 2 | 3 | const browser = detectBrowser() 4 | 5 | export default [ 6 | { 7 | name: 'Location', 8 | val() { 9 | return escape(location.href) 10 | }, 11 | }, 12 | { 13 | name: 'User Agent', 14 | val: navigator.userAgent, 15 | }, 16 | { 17 | name: 'Device', 18 | val: [ 19 | '', 20 | ``, 21 | ``, 22 | ``, 23 | '
    screen${screen.width} * ${screen.height}
    viewport${window.innerWidth} * ${window.innerHeight}
    pixel ratio${window.devicePixelRatio}
    ', 24 | ].join(''), 25 | }, 26 | { 27 | name: 'System', 28 | val: [ 29 | '', 30 | ``, 31 | ``, 34 | '
    os${detectOs()}
    browser${ 32 | browser.name + ' ' + browser.version 33 | }
    ', 35 | ].join(''), 36 | }, 37 | { 38 | name: 'About', 39 | val: 40 | 'Eruda v' + 41 | VERSION + 42 | '', 43 | }, 44 | ] 45 | -------------------------------------------------------------------------------- /src/lib/handlebars.js: -------------------------------------------------------------------------------- 1 | import handlebars from 'handlebars/lib/handlebars.runtime' 2 | 3 | // https://github.com/helpers/handlebars-helper-repeat 4 | handlebars.registerHelper('repeat', function (count = 0, options) { 5 | if (count < 1) return options.inverse(this) 6 | 7 | const step = 1 8 | const start = 0 9 | const max = count * step + start 10 | let index = start 11 | let str = '' 12 | 13 | do { 14 | const data = { 15 | index, 16 | count, 17 | start, 18 | step, 19 | first: index === start, 20 | last: index >= max - step, 21 | } 22 | const blockParams = [index, data] 23 | str += options.fn(this, { data, blockParams }) 24 | index += data.step 25 | } while (index < max) 26 | 27 | return str 28 | }) 29 | 30 | handlebars.registerHelper('class', function (value) { 31 | let classes = value.split(/\s+/) 32 | 33 | classes = classes.map((c) => `eruda-${c}`) 34 | 35 | return `class="${classes.join(' ')}"` 36 | }) 37 | 38 | handlebars.registerHelper('concat', function () { 39 | let ret = '' 40 | 41 | for (let i = 0, len = arguments.length; i < len; i++) { 42 | const arg = arguments[i] 43 | if (typeof arg === 'string') ret += arg 44 | } 45 | 46 | return ret 47 | }) 48 | 49 | export default handlebars 50 | -------------------------------------------------------------------------------- /src/Network/detail.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    {{url}}
    3 | {{#if data}} 4 |
    {{data}}
    5 | {{/if}} 6 |
    7 |

    Request Headers

    8 | 9 | 10 | {{#if reqHeaders}} 11 | {{#each reqHeaders}} 12 | 13 | 14 | 15 | 16 | {{/each}} 17 | {{else}} 18 | 19 | 20 | 21 | {{/if}} 22 | 23 |
    {{@key}}{{.}}
    Empty
    24 |

    Response Headers

    25 | 26 | 27 | {{#if resHeaders}} 28 | {{#each resHeaders}} 29 | 30 | 31 | 32 | 33 | {{/each}} 34 | {{else}} 35 | 36 | 37 | 38 | {{/if}} 39 | 40 |
    {{@key}}{{.}}
    Empty
    41 |
    42 | {{#if resTxt}} 43 |
    {{resTxt}}
    44 | {{/if}} 45 |
    46 |
    Back to the List
    -------------------------------------------------------------------------------- /src/DevTools/NavBar.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 3 | 4 | $height: 40px; 5 | 6 | .container { 7 | .nav-bar-container { 8 | @include absolute(100%, $height); 9 | z-index: 100; 10 | .nav-bar { 11 | @include overflow-auto(x); 12 | border-top: 1px solid var(--border); 13 | border-bottom: 1px solid var(--border); 14 | width: 100%; 15 | height: 100%; 16 | background: var(--darker-background); 17 | font-size: 0; 18 | white-space: nowrap; 19 | } 20 | .nav-bar-item { 21 | cursor: pointer; 22 | display: inline-block; 23 | height: $height - 2; 24 | line-height: $height - 2; 25 | padding: 0 10px; 26 | color: var(--foreground); 27 | font-size: $font-size-s; 28 | text-align: center; 29 | text-transform: capitalize; 30 | transition: all $anim-duration; 31 | &:active { 32 | background: var(--highlight); 33 | color: var(--select-foreground); 34 | } 35 | &.active { 36 | background: var(--highlight); 37 | color: var(--select-foreground); 38 | } 39 | } 40 | .bottom-bar { 41 | transition: left $anim-duration, width $anim-duration; 42 | height: 1px; 43 | background: var(--accent); 44 | position: absolute; 45 | bottom: 0; 46 | left: 0; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/cssMap.js: -------------------------------------------------------------------------------- 1 | export default { 2 | background: 'b', 3 | 'background-image': 'bi', 4 | border: 'bo', 5 | 'border-bottom': 'bb', 6 | 'border-collapse': 'bc', 7 | 'border-left-color': 'blc', 8 | 'border-right': 'br', 9 | 'border-radius': 'bra', 10 | 'border-top': 'bt', 11 | 'border-top-color': 'btc', 12 | 'box-shadow': 'bs', 13 | 'box-sizing': 'bsi', 14 | clear: 'cl', 15 | color: 'c', 16 | content: 'co', 17 | cursor: 'cu', 18 | display: 'd', 19 | flex: 'fl', 20 | 'flex-shrink': 'fsh', 21 | float: 'f', 22 | 'font-family': 'ff', 23 | 'font-size': 'fs', 24 | 'font-weight': 'fw', 25 | height: 'h', 26 | left: 'l', 27 | 'line-height': 'lh', 28 | margin: 'm', 29 | 'margin-bottom': 'mb', 30 | 'margin-left': 'ml', 31 | 'margin-top': 'mt', 32 | 'min-height': 'mh', 33 | outline: 'ou', 34 | overflow: 'o', 35 | 'overflow-x': 'ox', 36 | 'overflow-y': 'oy', 37 | padding: 'p', 38 | 'padding-bottom': 'pb', 39 | 'padding-left': 'pl', 40 | 'padding-top': 'pt', 41 | 'pointer-events': 'pe', 42 | position: 'po', 43 | 'text-align': 'ta', 44 | 'text-transform': 'tt', 45 | top: 't', 46 | transition: 'tr', 47 | 'user-select': 'us', 48 | 'vertical-aligin': 'va', 49 | visibility: 'v', 50 | width: 'w', 51 | 'will-change': 'wc', 52 | 'white-space': 'ws', 53 | '-webkit-overflow-scrolling': 'wos', 54 | 'z-index': 'z', 55 | } 56 | -------------------------------------------------------------------------------- /src/style/icon/error.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Sources/Sources.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 3 | 4 | #sources { 5 | @include overflow-auto(y); 6 | color: var(--foreground); 7 | .code-wrapper, 8 | .raw-wrapper { 9 | @include overflow-auto(x); 10 | width: 100%; 11 | min-height: 100%; 12 | } 13 | .raw { 14 | user-select: text; 15 | padding: $padding; 16 | } 17 | .code { 18 | font-size: $font-size-s; 19 | .content * { 20 | user-select: text; 21 | } 22 | } 23 | pre.code { 24 | padding: $padding; 25 | } 26 | table.code { 27 | border-collapse: collapse; 28 | .gutter { 29 | background: var(--background); 30 | color: var(--primary); 31 | } 32 | .line-num { 33 | border-right: 1px solid var(--border); 34 | padding: 0 3px 0 5px; 35 | text-align: right; 36 | } 37 | .code-line { 38 | padding: 0 4px; 39 | white-space: pre; 40 | } 41 | } 42 | .image { 43 | .breadcrumb { 44 | @include breadcrumb(); 45 | } 46 | .img-container { 47 | text-align: center; 48 | img { 49 | max-width: 100%; 50 | } 51 | } 52 | .img-info { 53 | text-align: center; 54 | margin: 20px 0; 55 | color: var(--foreground); 56 | } 57 | } 58 | .json { 59 | padding: 0 $padding; 60 | * { 61 | user-select: text; 62 | } 63 | } 64 | iframe { 65 | width: 100%; 66 | height: 100%; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/style/mixin.scss: -------------------------------------------------------------------------------- 1 | @import './variable'; 2 | 3 | @mixin absolute($width: 100%, $height: 100%) { 4 | position: absolute; 5 | width: $width; 6 | height: $height; 7 | left: 0; 8 | top: 0; 9 | } 10 | 11 | @mixin overflow-auto($direction: 'both') { 12 | @if $direction == 'both' { 13 | overflow: auto; 14 | } @else { 15 | overflow-#{$direction}: auto; 16 | } 17 | -webkit-overflow-scrolling: touch; 18 | } 19 | 20 | @mixin breadcrumb { 21 | background: var(--darker-background); 22 | color: var(--primary); 23 | user-select: text; 24 | margin-bottom: 10px; 25 | word-break: break-all; 26 | padding: $padding; 27 | font-size: $font-size-l; 28 | min-height: 40px; 29 | border-bottom: 1px solid var(--border); 30 | } 31 | 32 | @mixin clear-float { 33 | &:after { 34 | content: ''; 35 | display: block; 36 | clear: both; 37 | } 38 | } 39 | 40 | @mixin right-btn { 41 | .btn { 42 | display: flex; 43 | margin-left: 5px; 44 | float: right; 45 | color: var(--primary); 46 | width: 18px; 47 | height: 18px; 48 | justify-content: center; 49 | align-items: center; 50 | font-size: $font-size-l; 51 | cursor: pointer; 52 | transition: color $anim-duration; 53 | &.search-keyword { 54 | width: auto; 55 | max-width: 80px; 56 | font-size: $font-size; 57 | overflow: hidden; 58 | text-overflow: ellipsis; 59 | display: inline-block; 60 | } 61 | &:active { 62 | color: var(--accent); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/resources.js: -------------------------------------------------------------------------------- 1 | describe('resources', function () { 2 | let $tool = $('.eruda-resources') 3 | 4 | beforeEach(function () { 5 | eruda.show('resources') 6 | }) 7 | 8 | describe('localStorage', function () { 9 | it('show', function () { 10 | localStorage.clear() 11 | localStorage.setItem('testKey', 'testVal') 12 | $tool.find('.eruda-refresh-local-storage').click() 13 | expect($tool.find('.eruda-local-storage')).toContainText('testKey') 14 | }) 15 | 16 | it('clear', function () { 17 | $tool.find('.eruda-clear-storage[data-type="local"]').click() 18 | expect($tool.find('.eruda-local-storage')).toContainText('Empty') 19 | }) 20 | }) 21 | 22 | describe('sessionStorage', function () { 23 | it('show', function () { 24 | sessionStorage.clear() 25 | sessionStorage.setItem('testKey', 'testVal') 26 | $tool.find('.eruda-refresh-session-storage').click() 27 | expect($tool.find('.eruda-session-storage')).toContainText('testKey') 28 | }) 29 | 30 | it('clear', function () { 31 | $tool.find('.eruda-clear-storage[data-type="session"]').click() 32 | expect($tool.find('.eruda-session-storage')).toContainText('Empty') 33 | }) 34 | }) 35 | 36 | describe('cookie', function () { 37 | it('show', function () { 38 | util.cookie.set('testKey', 'testVal') 39 | $tool.find('.eruda-refresh-cookie').click() 40 | expect($tool.find('.eruda-cookie')).toContainText('testKey') 41 | }) 42 | 43 | it('clear', function () { 44 | $tool.find('.eruda-clear-cookie').click() 45 | expect($tool.find('.eruda-cookie')).toContainText('Empty') 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/info.js: -------------------------------------------------------------------------------- 1 | describe('info', function () { 2 | let tool = eruda.get('info') 3 | let $tool = $('.eruda-info') 4 | 5 | describe('default', function () { 6 | it('location', function () { 7 | expect($tool.find('.eruda-content').eq(0)).toContainText(location.href) 8 | }) 9 | 10 | it('user agent', function () { 11 | expect($tool.find('.eruda-content').eq(1)).toContainText( 12 | navigator.userAgent 13 | ) 14 | }) 15 | 16 | it('device', function () { 17 | expect($tool.find('.eruda-content').eq(2)).toContainText( 18 | window.innerWidth 19 | ) 20 | }) 21 | 22 | it('system', function () { 23 | expect($tool.find('.eruda-content').eq(3)).toContainText('os') 24 | }) 25 | 26 | it('about', function () { 27 | expect($tool.find('.eruda-content').eq(4)).toHaveText(/Eruda v[\d.]+/) 28 | }) 29 | }) 30 | 31 | it('clear', function () { 32 | tool.clear() 33 | expect($tool.find('li')).toHaveLength(0) 34 | }) 35 | 36 | it('add', function () { 37 | tool.add('test', 'eruda') 38 | expect($tool.find('.eruda-title')).toContainText('test') 39 | expect($tool.find('.eruda-content')).toContainText('eruda') 40 | tool.add('test', 'update') 41 | tool.add('test', 'update') 42 | expect($tool.find('.eruda-content')).toContainText('update') 43 | }) 44 | 45 | it('get', function () { 46 | expect(tool.get()).toEqual([{ name: 'test', val: 'update' }]) 47 | expect(tool.get('test')).toBe('update') 48 | expect(tool.get('test2')).not.toBeDefined() 49 | }) 50 | 51 | it('remove', function () { 52 | tool.remove('test') 53 | expect($tool.find('li')).toHaveLength(0) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /src/Elements/Select.js: -------------------------------------------------------------------------------- 1 | import { Emitter, isMobile } from '../lib/util' 2 | import { isErudaEl } from '../lib/extraUtil' 3 | 4 | export default class Select extends Emitter { 5 | constructor() { 6 | super() 7 | 8 | const self = this 9 | 10 | this._startListener = function (e) { 11 | if (isErudaEl(e.target)) return 12 | 13 | self._timer = setTimeout(function () { 14 | self.emit('select', e.target) 15 | }, 200) 16 | 17 | return false 18 | } 19 | 20 | this._moveListener = function () { 21 | clearTimeout(self._timer) 22 | } 23 | 24 | this._clickListener = function (e) { 25 | if (isErudaEl(e.target)) return 26 | 27 | e.preventDefault() 28 | e.stopImmediatePropagation() 29 | } 30 | } 31 | enable() { 32 | this.disable() 33 | function addEvent(type, listener) { 34 | document.body.addEventListener(type, listener, true) 35 | } 36 | if (isMobile()) { 37 | addEvent('touchstart', this._startListener) 38 | addEvent('touchmove', this._moveListener) 39 | } else { 40 | addEvent('mousedown', this._startListener) 41 | addEvent('mousemove', this._moveListener) 42 | } 43 | addEvent('click', this._clickListener) 44 | 45 | return this 46 | } 47 | disable() { 48 | function rmEvent(type, listener) { 49 | document.body.removeEventListener(type, listener, true) 50 | } 51 | if (isMobile()) { 52 | rmEvent('touchstart', this._startListener) 53 | rmEvent('touchmove', this._moveListener) 54 | } else { 55 | rmEvent('mousedown', this._startListener) 56 | rmEvent('mousemove', this._moveListener) 57 | } 58 | rmEvent('click', this._clickListener) 59 | 60 | return this 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 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: 'istanbul-instrumenter-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 | } 62 | -------------------------------------------------------------------------------- /test/snippets.js: -------------------------------------------------------------------------------- 1 | describe('snippets', function () { 2 | let tool = eruda.get('snippets') 3 | let $tool = $('.eruda-snippets') 4 | 5 | describe('default', function () { 6 | it('border all', function () { 7 | expect($tool.find('.eruda-name').eq(0)).toContainText('Border All') 8 | 9 | let $body = $('body') 10 | let $btn = $tool.find('.eruda-run').eq(0) 11 | 12 | $btn.click() 13 | expect($body).toHaveCss({ outlineWidth: '2px' }) 14 | $btn.click() 15 | expect($body).toHaveCss({ outlineWidth: '0px' }) 16 | }) 17 | 18 | it('refresh page', function () { 19 | expect($tool.find('.eruda-name').eq(1)).toContainText('Refresh Page') 20 | }) 21 | 22 | it('search text', function () { 23 | expect($tool.find('.eruda-name').eq(2)).toContainText('Search Text') 24 | }) 25 | 26 | it('edit page', function () { 27 | expect($tool.find('.eruda-name').eq(3)).toContainText('Edit Page') 28 | 29 | let $body = $('body') 30 | let $btn = $tool.find('.eruda-run').eq(3) 31 | 32 | $btn.click() 33 | expect($body).toHaveAttr('contenteditable', 'true') 34 | $btn.click() 35 | expect($body).toHaveAttr('contenteditable', 'false') 36 | }) 37 | }) 38 | 39 | it('clear', function () { 40 | tool.clear() 41 | expect($tool.find('.eruda-name')).toHaveLength(0) 42 | }) 43 | 44 | it('add', function () { 45 | tool.add( 46 | 'Test', 47 | function () { 48 | console.log('eruda') 49 | }, 50 | 'This is the description' 51 | ) 52 | expect($tool.find('.eruda-name')).toContainText('Test') 53 | expect($tool.find('.eruda-description')).toContainText( 54 | 'This is the description' 55 | ) 56 | }) 57 | 58 | it('remove', function () { 59 | tool.remove('Test') 60 | expect($tool.find('.eruda-name')).toHaveLength(0) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/style/reset.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | span, 3 | applet, 4 | object, 5 | iframe, 6 | h1, 7 | h2, 8 | h3, 9 | h4, 10 | h5, 11 | h6, 12 | p, 13 | blockquote, 14 | pre, 15 | a, 16 | abbr, 17 | acronym, 18 | address, 19 | big, 20 | cite, 21 | code, 22 | del, 23 | dfn, 24 | em, 25 | img, 26 | ins, 27 | kbd, 28 | q, 29 | s, 30 | samp, 31 | small, 32 | strike, 33 | strong, 34 | sub, 35 | sup, 36 | tt, 37 | var, 38 | b, 39 | u, 40 | i, 41 | center, 42 | dl, 43 | dt, 44 | dd, 45 | ol, 46 | ul, 47 | li, 48 | fieldset, 49 | form, 50 | label, 51 | legend, 52 | table, 53 | caption, 54 | tbody, 55 | tfoot, 56 | thead, 57 | tr, 58 | th, 59 | td, 60 | article, 61 | aside, 62 | canvas, 63 | details, 64 | embed, 65 | figure, 66 | figcaption, 67 | footer, 68 | header, 69 | hgroup, 70 | menu, 71 | nav, 72 | output, 73 | ruby, 74 | section, 75 | summary, 76 | time, 77 | mark, 78 | audio, 79 | video { 80 | margin: 0; 81 | padding: 0; 82 | border: 0; 83 | font-size: 100%; 84 | font: inherit; 85 | vertical-align: baseline; 86 | } 87 | article, 88 | aside, 89 | details, 90 | figcaption, 91 | figure, 92 | footer, 93 | header, 94 | hgroup, 95 | menu, 96 | nav, 97 | section { 98 | display: block; 99 | } 100 | body { 101 | line-height: 1; 102 | } 103 | ol, 104 | ul { 105 | list-style: none; 106 | } 107 | blockquote, 108 | q { 109 | quotes: none; 110 | } 111 | blockquote:before, 112 | blockquote:after, 113 | q:before, 114 | q:after { 115 | content: ''; 116 | content: none; 117 | } 118 | table { 119 | border-collapse: collapse; 120 | border-spacing: 0; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/eruda.js: -------------------------------------------------------------------------------- 1 | describe('devTools', function() { 2 | describe('init', function() { 3 | it('destroy', function() { 4 | eruda.destroy() 5 | 6 | expect($('#eruda')).toHaveLength(0) 7 | }) 8 | 9 | it('init', function() { 10 | let container = document.createElement('div') 11 | container.id = 'eruda' 12 | document.body.appendChild(container) 13 | 14 | eruda.init({ 15 | container: container, 16 | tool: [], 17 | useShadowDom: false 18 | }) 19 | 20 | let $eruda = $('#eruda') 21 | expect($eruda.find('.eruda-dev-tools')).toHaveLength(1) 22 | }) 23 | }) 24 | 25 | describe('tool', function() { 26 | it('add', function() { 27 | eruda.add({ 28 | name: 'test', 29 | init: function($el) { 30 | this._$el = $el 31 | $el.html('Test Plugin') 32 | } 33 | }) 34 | 35 | expect($('.eruda-test')).toContainText('Test Plugin') 36 | }) 37 | 38 | it('show', function() { 39 | let $tool = $('.eruda-test') 40 | expect($tool).toBeHidden() 41 | eruda.show('test') 42 | expect($tool).toHaveCss({ display: 'block' }) 43 | }) 44 | 45 | it('remove', function() { 46 | eruda.remove('test') 47 | expect($('.eruda-test')).toHaveLength(0) 48 | }) 49 | }) 50 | 51 | describe('display', function() { 52 | it('show', function() { 53 | eruda.show() 54 | expect($('.eruda-dev-tools')).toHaveCss({ display: 'block' }) 55 | }) 56 | 57 | it('hide', function(done) { 58 | eruda.hide() 59 | setTimeout(function() { 60 | expect($('.eruda-dev-tools')).toBeHidden() 61 | done() 62 | }, 500) 63 | }) 64 | }) 65 | 66 | describe('scale', function() { 67 | it('get', function() { 68 | eruda.scale(1) 69 | expect(eruda.scale()).toBe(1) 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/console.js: -------------------------------------------------------------------------------- 1 | describe('console', function () { 2 | let tool = eruda.get('console') 3 | tool.config.set('asyncRender', false) 4 | let $tool = $('.eruda-console') 5 | let logger = tool._logger 6 | 7 | function log(i) { 8 | return logs()[i].container 9 | } 10 | 11 | function logs() { 12 | return logger.displayLogs 13 | } 14 | 15 | beforeEach(function () { 16 | eruda.show('console') 17 | logger.clear(true) 18 | }) 19 | 20 | describe('config', function () { 21 | let config = tool.config 22 | 23 | it('override console', function () { 24 | config.set('overrideConsole', true) 25 | console.log('test') 26 | expect($(log(0))).toContainText('test') 27 | }) 28 | }) 29 | 30 | describe('ui', function () { 31 | it('clear', function () { 32 | tool.log('test') 33 | $('.eruda-clear-console').click() 34 | expect($tool.find('.eruda-logs li')).toHaveLength(0) 35 | }) 36 | 37 | it('filter', function () { 38 | tool.log('test') 39 | tool.warn('test') 40 | expect(logs()).toHaveLength(2) 41 | $('.eruda-filter[data-filter="warn"]').click() 42 | expect(logs()).toHaveLength(1) 43 | $('.eruda-filter[data-filter="all"]').click() 44 | }) 45 | }) 46 | 47 | describe('execute', function () { 48 | it('js', function () { 49 | $tool.find('textarea').val('1+2') 50 | $('.eruda-execute').click() 51 | expect($(log(1))).toContainText('3') 52 | }) 53 | }) 54 | 55 | describe('events', function () { 56 | it('log', function () { 57 | let sum = 0 58 | function add(num) { 59 | sum += num 60 | } 61 | tool.on('log', add) 62 | tool.log(5) 63 | expect(sum).toBe(5) 64 | tool.log(6) 65 | expect(sum).toBe(11) 66 | tool.off('log', add) 67 | tool.log(1) 68 | expect(sum).toBe(11) 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Eruda Test Page 10 | 11 | 12 | 13 | 14 |
    ERUDA TEST PAGE
    15 | 49 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/Info/Info.js: -------------------------------------------------------------------------------- 1 | import Tool from '../DevTools/Tool' 2 | import defInfo from './defInfo' 3 | import { each, isFn, isUndef, cloneDeep } from '../lib/util' 4 | import evalCss from '../lib/evalCss' 5 | import style from './Info.scss' 6 | import template from './Info.hbs' 7 | 8 | export default class Info extends Tool { 9 | constructor() { 10 | super() 11 | 12 | this._style = evalCss(style) 13 | 14 | this.name = 'info' 15 | this._tpl = template 16 | this._infos = [] 17 | } 18 | init($el) { 19 | super.init($el) 20 | 21 | this._addDefInfo() 22 | } 23 | destroy() { 24 | super.destroy() 25 | 26 | evalCss.remove(this._style) 27 | } 28 | add(name, val) { 29 | const infos = this._infos 30 | let isUpdate = false 31 | 32 | each(infos, (info) => { 33 | if (name !== info.name) return 34 | 35 | info.val = val 36 | isUpdate = true 37 | }) 38 | 39 | if (!isUpdate) infos.push({ name, val }) 40 | 41 | this._render() 42 | 43 | return this 44 | } 45 | get(name) { 46 | const infos = this._infos 47 | 48 | if (isUndef(name)) { 49 | return cloneDeep(infos) 50 | } 51 | 52 | let result 53 | 54 | each(infos, (info) => { 55 | if (name === info.name) result = info.val 56 | }) 57 | 58 | return result 59 | } 60 | remove(name) { 61 | const infos = this._infos 62 | 63 | for (let i = infos.length - 1; i >= 0; i--) { 64 | if (infos[i].name === name) infos.splice(i, 1) 65 | } 66 | 67 | this._render() 68 | 69 | return this 70 | } 71 | clear() { 72 | this._infos = [] 73 | 74 | this._render() 75 | 76 | return this 77 | } 78 | _addDefInfo() { 79 | each(defInfo, (info) => this.add(info.name, info.val)) 80 | } 81 | _render() { 82 | const infos = [] 83 | 84 | each(this._infos, ({ name, val }) => { 85 | if (isFn(val)) val = val() 86 | 87 | infos.push({ name, val }) 88 | }) 89 | 90 | this._renderHtml(this._tpl({ infos })) 91 | } 92 | _renderHtml(html) { 93 | if (html === this._lastHtml) return 94 | this._lastHtml = html 95 | this._$el.html(html) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Snippets/Snippets.js: -------------------------------------------------------------------------------- 1 | import Tool from '../DevTools/Tool' 2 | import defSnippets from './defSnippets' 3 | import { $, each } from '../lib/util' 4 | import evalCss from '../lib/evalCss' 5 | 6 | import style from './Snippets.scss' 7 | import template from './Snippets.hbs' 8 | 9 | export default class Snippets extends Tool { 10 | constructor() { 11 | super() 12 | 13 | this._style = evalCss(style) 14 | 15 | this.name = 'snippets' 16 | 17 | this._snippets = [] 18 | this._tpl = template 19 | } 20 | init($el) { 21 | super.init($el) 22 | 23 | this._bindEvent() 24 | this._addDefSnippets() 25 | } 26 | destroy() { 27 | super.destroy() 28 | 29 | evalCss.remove(this._style) 30 | } 31 | add(name, fn, desc) { 32 | this._snippets.push({ name, fn, desc }) 33 | 34 | this._render() 35 | 36 | return this 37 | } 38 | remove(name) { 39 | const snippets = this._snippets 40 | 41 | for (let i = 0, len = snippets.length; i < len; i++) { 42 | if (snippets[i].name === name) snippets.splice(i, 1) 43 | } 44 | 45 | this._render() 46 | 47 | return this 48 | } 49 | run(name) { 50 | const snippets = this._snippets 51 | 52 | for (let i = 0, len = snippets.length; i < len; i++) { 53 | if (snippets[i].name === name) this._run(i) 54 | } 55 | 56 | return this 57 | } 58 | clear() { 59 | this._snippets = [] 60 | this._render() 61 | 62 | return this 63 | } 64 | _bindEvent() { 65 | const self = this 66 | 67 | this._$el.on('click', '.eruda-run', function () { 68 | const idx = $(this).data('idx') 69 | 70 | self._run(idx) 71 | }) 72 | } 73 | _run(idx) { 74 | this._snippets[idx].fn.call(null) 75 | } 76 | _addDefSnippets() { 77 | each(defSnippets, (snippet) => { 78 | this.add(snippet.name, snippet.fn, snippet.desc) 79 | }) 80 | } 81 | _render() { 82 | this._renderHtml( 83 | this._tpl({ 84 | snippets: this._snippets, 85 | }) 86 | ) 87 | } 88 | _renderHtml(html) { 89 | if (html === this._lastHtml) return 90 | this._lastHtml = html 91 | this._$el.html(html) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/settings.js: -------------------------------------------------------------------------------- 1 | describe('settings', function () { 2 | let tool = eruda.get('settings') 3 | let $tool = $('.eruda-settings') 4 | 5 | let cfg = eruda.Settings.createCfg('test') 6 | cfg.set({ 7 | testSwitch: false, 8 | testSelect: '1', 9 | testRange: 1, 10 | testColor: '#fff', 11 | }) 12 | 13 | beforeEach(function () { 14 | tool.clear() 15 | }) 16 | 17 | it('switch', function () { 18 | let text = 'Test Switch' 19 | 20 | tool.switch(cfg, 'testSwitch', text) 21 | expect($tool.find('.eruda-switch')).toContainText(text) 22 | $tool.find('.eruda-checkbox').click() 23 | expect(cfg.get('testSwitch')).toBe(true) 24 | }) 25 | 26 | it('separator', function () { 27 | tool.separator() 28 | expect($tool.find('.eruda-separator').length).toEqual(1) 29 | }) 30 | 31 | it('select', function () { 32 | let text = 'Test Select' 33 | 34 | tool.select(cfg, 'testSelect', text, ['1', '2', '3']) 35 | let $el = $tool.find('.eruda-select') 36 | expect($el.find('ul li').length).toEqual(3) 37 | expect($el.find('.eruda-head')).toContainText(text) 38 | expect($el.find('.eruda-val')).toContainText('1') 39 | $el.find('.eruda-head').click() 40 | $el.find('ul li').eq(1).click() 41 | expect(cfg.get('testSelect')).toBe('2') 42 | }) 43 | it('range', function () { 44 | let text = 'Test Range' 45 | 46 | tool.range(cfg, 'testRange', text, { min: 0, max: 1, step: 0.1 }) 47 | let $el = $tool.find('.eruda-range') 48 | expect($el.find('.eruda-head')).toContainText(text) 49 | expect($el.find('input').length).toEqual(1) 50 | $el.find('.eruda-head').click() 51 | }) 52 | 53 | it('color', function () { 54 | let text = 'Test Color' 55 | 56 | tool.color(cfg, 'testColor', text, ['#000', '#fff']) 57 | let $el = $tool.find('.eruda-color') 58 | expect($el.find('.eruda-head')).toContainText(text) 59 | expect($el.find('ul li').length).toEqual(2) 60 | $el.find('.eruda-head').click() 61 | $el.find('ul li').eq(0).click() 62 | expect(cfg.get('testColor')).toBe('rgb(0, 0, 0)') 63 | }) 64 | 65 | it('remove', function () { 66 | let text = 'Test Switch' 67 | tool.switch(cfg, 'testSwitch', text) 68 | expect($tool.find('.eruda-switch')).toContainText(text) 69 | tool.remove(cfg, 'testSwitch') 70 | expect($tool.find('.eruda-switch')).toHaveLength(0) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /doc/PLUGIN.md: -------------------------------------------------------------------------------- 1 | # Writing a Plugin 2 | 3 | It is possible to enhance Eruda with more features by writing plugins, which means, creating your own custom panels. 4 | 5 | ## Creating a Plugin 6 | 7 | Adding plugins is super easy for eruda. All you have to do is passing an object with a few methods implemented. 8 | 9 | ```javascript 10 | eruda.add({ 11 | name: 'test', 12 | init($el) { 13 | $el.html('Hello, this is my first eruda plugin!'); 14 | this._$el = $el; 15 | }, 16 | show() { 17 | this._$el.show(); 18 | }, 19 | hide() { 20 | this._$el.hide(); 21 | }, 22 | destroy() {} 23 | }); 24 | ``` 25 | 26 | ## Basic Structure 27 | 28 | ### name 29 | 30 | Every plugin must have a unique name, which will be shown as the tab name on the top. 31 | 32 | ### init 33 | 34 | Called when plugin is added, and a document element used to display content is passed in. 35 | 36 | The element is wrapped as a jQuery like object, provided by the [licia](https://licia.liriliri.io/docs.html) utility library. 37 | 38 | ### show 39 | 40 | Called when switch to the panel. Usually all you need to do is to show the container element. 41 | 42 | ### hide 43 | 44 | Called when switch to other panel. You should at least hide the container element here. 45 | 46 | ### destroy 47 | 48 | Called when plugin is removed using `eruda.remove('plugin name')`. 49 | 50 | ## Worth Mentioning 51 | 52 | Apart from passing an object, you can also pass in a function that returns an object. Eruda will automatically invoke the function and use the object it returns. 53 | 54 | When writing plugins, you can use utilities exposed by Eruda, see [docs](./UTIL_API.md) here. 55 | 56 | ```javascript 57 | eruda.add(function (eruda) { 58 | // eruda.Tool implements those four methods. 59 | class Test extends eruda.Tool { 60 | constructor() { 61 | super() 62 | this.name = 'test'; 63 | this.style = eruda.util.evalCss('.eruda-test { background: #000; }'); 64 | } 65 | init($el) { 66 | super.init($el); 67 | } 68 | destroy() { 69 | super.destroy(); 70 | eruda.util.evalCss.remove(this.style); 71 | } 72 | } 73 | 74 | return new Test(); 75 | }); 76 | ``` 77 | 78 | There is also a tool for plugin initialization, check it out [here](https://github.com/liriliri/eruda-plugin). 79 | 80 | -------------------------------------------------------------------------------- /src/lib/evalCss.js: -------------------------------------------------------------------------------- 1 | import { 2 | each, 3 | filter, 4 | isStr, 5 | keys, 6 | kebabCase, 7 | defaults, 8 | escapeRegExp, 9 | } from './util' 10 | import themes from './themes' 11 | import cssMap from './cssMap' 12 | 13 | let styleList = [] 14 | let scale = 1 15 | 16 | let curTheme = themes.Light 17 | 18 | function getStyleString(css) { 19 | if (Array.isArray(css)) { 20 | return css.map(getStyleString).join('\n') 21 | } else if (isStr(css)) { 22 | return css 23 | } else if (typeof css.default === 'string') { 24 | return css.default 25 | } else { 26 | throw new Error('Unknown style type') 27 | } 28 | } 29 | 30 | const exports = function (css, container) { 31 | css = getStyleString(css) || '' 32 | 33 | for (let i = 0, len = styleList.length; i < len; i++) { 34 | if (styleList[i].css === css) return 35 | } 36 | 37 | container = container || exports.container || document.head 38 | const el = document.createElement('style') 39 | 40 | container.appendChild(el) 41 | 42 | const style = { css, el, container } 43 | resetStyle(style) 44 | styleList.push(style) 45 | 46 | return style 47 | } 48 | 49 | exports.setScale = function (s) { 50 | scale = s 51 | resetStyles() 52 | } 53 | 54 | exports.setTheme = function (theme) { 55 | if (isStr(theme)) { 56 | curTheme = themes[theme] || themes.Light 57 | } else { 58 | curTheme = defaults(theme, themes.Light) 59 | } 60 | 61 | resetStyles() 62 | } 63 | 64 | exports.getCurTheme = () => curTheme 65 | 66 | exports.getThemes = () => themes 67 | 68 | exports.clear = function () { 69 | each(styleList, ({ container, el }) => container.removeChild(el)) 70 | styleList = [] 71 | } 72 | 73 | exports.remove = function (style) { 74 | styleList = filter(styleList, (s) => s !== style) 75 | 76 | style.container.removeChild(style.el) 77 | } 78 | 79 | function resetStyles() { 80 | each(styleList, (style) => resetStyle(style)) 81 | } 82 | 83 | function resetStyle({ css, el }) { 84 | css = css.replace(/(\d+)px/g, ($0, $1) => +$1 * scale + 'px') 85 | css = css.replace(/_/g, 'eruda-') 86 | each(cssMap, (val, key) => { 87 | css = css.replace(new RegExp(escapeRegExp(`$${val}:`), 'g'), key + ':') 88 | }) 89 | const _keys = keys(themes.Light) 90 | each(_keys, (key) => { 91 | css = css.replace( 92 | new RegExp(`var\\(--${kebabCase(key)}\\)`, 'g'), 93 | curTheme[key] 94 | ) 95 | }) 96 | el.innerHTML = css 97 | } 98 | 99 | export default exports 100 | -------------------------------------------------------------------------------- /patches/css-loader+6.7.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/css-loader/dist/runtime/api.js b/node_modules/css-loader/dist/runtime/api.js 2 | index cdb89c5..d828dcd 100644 3 | --- a/node_modules/css-loader/dist/runtime/api.js 4 | +++ b/node_modules/css-loader/dist/runtime/api.js 5 | @@ -4,7 +4,7 @@ 6 | MIT License http://www.opensource.org/licenses/mit-license.php 7 | Author Tobias Koppers @sokra 8 | */ 9 | -module.exports = function (cssWithMappingToString) { 10 | +export default function (cssWithMappingToString) { 11 | var list = []; // return the list of modules as css string 12 | 13 | list.toString = function toString() { 14 | diff --git a/node_modules/css-loader/dist/runtime/getUrl.js b/node_modules/css-loader/dist/runtime/getUrl.js 15 | index a299e9b..9fe5a8c 100644 16 | --- a/node_modules/css-loader/dist/runtime/getUrl.js 17 | +++ b/node_modules/css-loader/dist/runtime/getUrl.js 18 | @@ -1,6 +1,6 @@ 19 | "use strict"; 20 | 21 | -module.exports = function (url, options) { 22 | +export default function (url, options) { 23 | if (!options) { 24 | options = {}; 25 | } 26 | diff --git a/node_modules/css-loader/dist/runtime/noSourceMaps.js b/node_modules/css-loader/dist/runtime/noSourceMaps.js 27 | index 9e967aa..0042208 100644 28 | --- a/node_modules/css-loader/dist/runtime/noSourceMaps.js 29 | +++ b/node_modules/css-loader/dist/runtime/noSourceMaps.js 30 | @@ -1,5 +1,5 @@ 31 | "use strict"; 32 | 33 | -module.exports = function (i) { 34 | +export default function (i) { 35 | return i[1]; 36 | }; 37 | \ No newline at end of file 38 | diff --git a/node_modules/css-loader/dist/runtime/sourceMaps.js b/node_modules/css-loader/dist/runtime/sourceMaps.js 39 | index fb96ae5..a47d0b1 100644 40 | --- a/node_modules/css-loader/dist/runtime/sourceMaps.js 41 | +++ b/node_modules/css-loader/dist/runtime/sourceMaps.js 42 | @@ -1,6 +1,6 @@ 43 | "use strict"; 44 | 45 | -module.exports = function (item) { 46 | +export default function (item) { 47 | var content = item[1]; 48 | var cssMapping = item[3]; 49 | 50 | diff --git a/node_modules/css-loader/dist/utils.js b/node_modules/css-loader/dist/utils.js 51 | index 4c81d60..c657aa0 100644 52 | --- a/node_modules/css-loader/dist/utils.js 53 | +++ b/node_modules/css-loader/dist/utils.js 54 | @@ -978,7 +978,7 @@ function getModuleCode(result, api, replacements, options, loaderContext) { 55 | // 5 - layer 56 | 57 | 58 | - return `${beforeCode}// Module\n___CSS_LOADER_EXPORT___.push([module.id, ${code}, ""${sourceMapValue}]);\n`; 59 | + return `${beforeCode}// Module\n___CSS_LOADER_EXPORT___.push([__filename, ${code}, ""${sourceMapValue}]);\n`; 60 | } 61 | 62 | function dashesCamelCase(str) { 63 | -------------------------------------------------------------------------------- /src/Resources/Resources.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 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 | .content { 13 | @include overflow-auto(y); 14 | max-height: 400px; 15 | } 16 | &.warn { 17 | border: 1px solid var(--console-warn-border); 18 | .title { 19 | background: var(--console-warn-background); 20 | color: var(--console-warn-foreground); 21 | } 22 | } 23 | &.danger { 24 | border: 1px solid var(--console-error-border); 25 | .title { 26 | background: var(--console-error-background); 27 | color: var(--console-error-foreground); 28 | } 29 | } 30 | } 31 | .title { 32 | @include right-btn(); 33 | padding: $padding; 34 | color: var(--primary); 35 | background: var(--darker-background); 36 | } 37 | .link-list { 38 | font-size: $font-size-s; 39 | color: var(--foreground); 40 | li { 41 | padding: 10px; 42 | word-break: break-all; 43 | a { 44 | color: var(--link-color) !important; 45 | } 46 | } 47 | } 48 | .image-list { 49 | @include clear-float(); 50 | color: var(--foreground); 51 | font-size: $font-size-s; 52 | display: flex; 53 | flex-wrap: wrap; 54 | padding: $padding !important; 55 | li { 56 | flex-grow: 1; 57 | cursor: pointer; 58 | &.image { 59 | height: 100px; 60 | font-size: 0; 61 | } 62 | overflow-y: hidden; 63 | img { 64 | height: 100px; 65 | min-width: 100%; 66 | object-fit: cover; 67 | } 68 | } 69 | } 70 | table { 71 | color: var(--foreground); 72 | border-collapse: collapse; 73 | width: 100%; 74 | font-size: $font-size-s; 75 | tr:nth-child(even) { 76 | background: var(--contrast); 77 | } 78 | td { 79 | padding: 10px; 80 | word-break: break-all; 81 | &.key { 82 | @include overflow-auto(x); 83 | white-space: nowrap; 84 | max-width: 120px; 85 | } 86 | &.control { 87 | padding: 0; 88 | font-size: 0; 89 | width: 40px; 90 | .icon-delete { 91 | cursor: pointer; 92 | color: var(--primary); 93 | font-size: $font-size; 94 | display: inline-block; 95 | width: 40px; 96 | height: 40px; 97 | text-align: center; 98 | line-height: 40px; 99 | transition: color $anim-duration; 100 | &:active { 101 | color: var(--accent); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Elements/CssStore.js: -------------------------------------------------------------------------------- 1 | import { each, sortKeys } from '../lib/util' 2 | 3 | function formatStyle(style) { 4 | const ret = {} 5 | 6 | for (let i = 0, len = style.length; i < len; i++) { 7 | const name = style[i] 8 | 9 | if (style[name] === 'initial') continue 10 | 11 | ret[name] = style[name] 12 | } 13 | 14 | return sortStyleKeys(ret) 15 | } 16 | 17 | const elProto = Element.prototype 18 | 19 | let matchesSel = function () { 20 | return false 21 | } 22 | 23 | if (elProto.webkitMatchesSelector) { 24 | matchesSel = (el, selText) => el.webkitMatchesSelector(selText) 25 | } else if (elProto.mozMatchesSelector) { 26 | matchesSel = (el, selText) => el.mozMatchesSelector(selText) 27 | } 28 | 29 | export default class CssStore { 30 | constructor(el) { 31 | this._el = el 32 | } 33 | getComputedStyle() { 34 | const computedStyle = window.getComputedStyle(this._el) 35 | 36 | return formatStyle(computedStyle) 37 | } 38 | getMatchedCSSRules() { 39 | const ret = [] 40 | 41 | each(document.styleSheets, (styleSheet) => { 42 | try { 43 | // Started with version 64, Chrome does not allow cross origin script to access this property. 44 | if (!styleSheet.cssRules) return 45 | } catch (e) { 46 | return 47 | } 48 | 49 | each(styleSheet.cssRules, (cssRule) => { 50 | let matchesEl = false 51 | 52 | // Mobile safari will throw DOM Exception 12 error, need to try catch it. 53 | try { 54 | matchesEl = this._elMatchesSel(cssRule.selectorText) 55 | /* eslint-disable no-empty */ 56 | } catch (e) {} 57 | 58 | if (!matchesEl) return 59 | 60 | ret.push({ 61 | selectorText: cssRule.selectorText, 62 | style: formatStyle(cssRule.style), 63 | }) 64 | }) 65 | }) 66 | 67 | return ret 68 | } 69 | _elMatchesSel(selText) { 70 | return matchesSel(this._el, selText) 71 | } 72 | } 73 | 74 | function sortStyleKeys(style) { 75 | return sortKeys(style, { 76 | comparator: (a, b) => { 77 | const lenA = a.length 78 | const lenB = b.length 79 | const len = lenA > lenB ? lenB : lenA 80 | 81 | for (let i = 0; i < len; i++) { 82 | const codeA = a.charCodeAt(i) 83 | const codeB = b.charCodeAt(i) 84 | const cmpResult = cmpCode(codeA, codeB) 85 | 86 | if (cmpResult !== 0) return cmpResult 87 | } 88 | 89 | if (lenA > lenB) return 1 90 | if (lenA < lenB) return -1 91 | 92 | return 0 93 | }, 94 | }) 95 | } 96 | 97 | function cmpCode(a, b) { 98 | a = transCode(a) 99 | b = transCode(b) 100 | 101 | if (a > b) return 1 102 | if (a < b) return -1 103 | return 0 104 | } 105 | 106 | function transCode(code) { 107 | // - should be placed after lowercase chars. 108 | if (code === 45) return 123 109 | return code 110 | } 111 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # Api 2 | 3 | ## init 4 | 5 | Initialize eruda. 6 | 7 | ### Options 8 | 9 | |Name |Type |Desc | 10 | |-----------------|------------|-----------------------------------------------------------------------------------------| 11 | |container |element |Container element. If not set, it will append an element directly under html root element| 12 | |tool |string array|Choose which default tools you want, by default all will be added | 13 | |autoScale=true |boolean |Auto scale eruda for different viewport settings | 14 | |useShadowDom=true|boolean |Use shadow dom for css encapsulation | 15 | |defaults |object |Default settings | 16 | 17 | Available default settings: 18 | 19 | |Name |Type |Desc | 20 | |------------|------|---------------------------------------------| 21 | |transparency|number|Transparency, 0 to 1 | 22 | |displaySize |number|Display size, 0 to 100 | 23 | |theme |string|Theme, defaults to Light or Dark in dark mode| 24 | 25 | ```javascript 26 | const el = document.createElement('div'); 27 | document.body.appendChild(el); 28 | 29 | eruda.init({ 30 | container: el, 31 | tool: ['console', 'elements'], 32 | useShadowDom: true, 33 | autoScale: true, 34 | defaults: { 35 | displaySize: 50, 36 | transparency: 0.9, 37 | theme: 'Monokai Pro' 38 | } 39 | }); 40 | ``` 41 | 42 | ## destroy 43 | 44 | Destory eruda. 45 | 46 | Note: You can call **init** method again after destruction. 47 | 48 | ```javascript 49 | eruda.destroy(); 50 | ``` 51 | 52 | ## scale 53 | 54 | Set or get scale. 55 | 56 | ```javascript 57 | eruda.scale(); // -> 1 58 | eruda.scale(1.5); 59 | ``` 60 | 61 | ## position 62 | 63 | Set or get entry button position. 64 | 65 | It will not take effect if given pos is out of range. 66 | 67 | ```javascript 68 | eruda.position({x: 20, y: 20}); 69 | eruda.position(); // -> {x: 20, y: 20} 70 | ``` 71 | 72 | ## get 73 | 74 | Get tool, eg. console, elements panels. 75 | 76 | ```javascript 77 | let console = eruda.get('console'); 78 | console.log('eruda'); 79 | ``` 80 | 81 | ## add 82 | 83 | Add tool. 84 | 85 | ```javascript 86 | eruda.add(new (eruda.Tool.extend({ 87 | name: 'test' 88 | }))); 89 | ``` 90 | 91 | ## remove 92 | 93 | Remove tool. 94 | 95 | ```javascript 96 | eruda.remove('console'); 97 | ``` 98 | 99 | ## show 100 | 101 | Show eruda panel. 102 | 103 | ```javascript 104 | eruda.show(); 105 | eruda.show('console'); 106 | ``` 107 | 108 | ## hide 109 | 110 | Hide eruda panel. 111 | 112 | ```javascript 113 | eruda.hide(); 114 | ``` -------------------------------------------------------------------------------- /src/Dom/style.scss: -------------------------------------------------------------------------------- 1 | @mixin overflow-auto($direction: 'both') { 2 | @if $direction == 'both' { 3 | overflow: auto; 4 | } @else { 5 | overflow-#{$direction}: auto; 6 | } 7 | -webkit-overflow-scrolling: touch; 8 | } 9 | 10 | .dom { 11 | padding-bottom: 40px; 12 | .dom-tree { 13 | @include overflow-auto(y); 14 | overflow-x: hidden; 15 | word-wrap: break-word; 16 | padding: 10px 10px 10px 25px; 17 | font-size: 12px; 18 | height: 100%; 19 | font-family: Consolas, Lucida Console, Monaco, MonoSpace; 20 | cursor: default; 21 | & * { 22 | user-select: text; 23 | } 24 | .tree-item { 25 | line-height: 16px; 26 | min-height: 16px; 27 | position: relative; 28 | z-index: 10; 29 | .toggle-btn { 30 | position: absolute; 31 | display: block; 32 | width: 12px; 33 | height: 12px; 34 | left: -12px; 35 | top: 2px; 36 | } 37 | .selection { 38 | position: absolute; 39 | display: none; 40 | left: -10000px; 41 | right: -10000px; 42 | top: 0; 43 | bottom: 0; 44 | z-index: -1; 45 | background: var(--contrast); 46 | } 47 | &.selected { 48 | &.expandable.expanded:before { 49 | border-left-color: transparent; 50 | } 51 | .selection { 52 | display: block; 53 | } 54 | } 55 | .html-tag { 56 | color: var(--tag-name-color); 57 | .tag-name { 58 | color: var(--tag-name-color); 59 | } 60 | .attribute-name { 61 | color: var(--attribute-name-color); 62 | } 63 | .attribute-value { 64 | color: var(--string-color); 65 | &.attribute-underline { 66 | text-decoration: underline; 67 | } 68 | } 69 | } 70 | .html-comment { 71 | color: var(--comment-color); 72 | } 73 | &.expandable:before { 74 | content: ''; 75 | width: 0; 76 | height: 0; 77 | border: 4px solid transparent; 78 | position: absolute; 79 | border-left-color: var(--foreground); 80 | left: -10px; 81 | top: 4px; 82 | } 83 | &.expandable.expanded:before { 84 | border-top-color: var(--foreground); 85 | border-left-color: transparent; 86 | left: -12px; 87 | top: 6px; 88 | } 89 | } 90 | .children { 91 | padding-left: 15px; 92 | } 93 | } 94 | .inspect { 95 | position: absolute; 96 | left: 0; 97 | bottom: 0; 98 | color: var(--foreground); 99 | border-top: 1px solid var(--border); 100 | width: 100%; 101 | background: var(--darker-background); 102 | display: block; 103 | height: 40px; 104 | line-height: 40px; 105 | text-decoration: none; 106 | text-align: center; 107 | margin-top: 10px; 108 | transition: background 0.3s; 109 | &:active { 110 | color: var(--select-foreground); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/DevTools/NavBar.js: -------------------------------------------------------------------------------- 1 | import { Emitter, $, isNum } from '../lib/util' 2 | import evalCss from '../lib/evalCss' 3 | import style from './NavBar.scss' 4 | 5 | function getToolName($component) { 6 | return $component.attr('data-name') || $component.text() 7 | } 8 | 9 | export default class NavBar extends Emitter { 10 | constructor($el) { 11 | super() 12 | 13 | this._style = evalCss(style) 14 | 15 | this._$el = $el.find('.eruda-nav-bar') 16 | this._$bottomBar = $el.find('.eruda-bottom-bar') 17 | this._len = 0 18 | 19 | this._bindEvent() 20 | } 21 | add(name, title = name) { 22 | const $el = this._$el 23 | this._len++ 24 | 25 | const $last = $el.find('.eruda-nav-bar-item').last() 26 | const html = `
    ${title}
    ` 27 | if ($last.length > 0 && getToolName($last) === 'settings') { 28 | $last.before(html) 29 | } else { 30 | $el.append(html) 31 | } 32 | this.resetBottomBar() 33 | } 34 | remove(name) { 35 | this._len-- 36 | this._$el.find('.eruda-nav-bar-item').each(function () { 37 | const $this = $(this) 38 | if (getToolName($this).toLowerCase() === name.toLowerCase()) { 39 | $this.remove() 40 | } 41 | }) 42 | this.resetBottomBar() 43 | } 44 | activateTool(name) { 45 | const self = this 46 | 47 | this._$el.find('.eruda-nav-bar-item').each(function () { 48 | const $this = $(this) 49 | 50 | if (getToolName($this) === name) { 51 | $this.addClass('eruda-active') 52 | self.resetBottomBar() 53 | self._scrollItemToView() 54 | } else { 55 | $this.rmClass('eruda-active') 56 | } 57 | }) 58 | } 59 | destroy() { 60 | evalCss.remove(this._style) 61 | this._$el.remove() 62 | } 63 | _scrollItemToView() { 64 | const $el = this._$el 65 | const li = $el.find('.eruda-active').get(0) 66 | const container = $el.get(0) 67 | 68 | const itemLeft = li.offsetLeft 69 | const itemWidth = li.offsetWidth 70 | const containerWidth = container.offsetWidth 71 | const scrollLeft = container.scrollLeft 72 | let targetScrollLeft 73 | 74 | if (itemLeft < scrollLeft) { 75 | targetScrollLeft = itemLeft 76 | } else if (itemLeft + itemWidth > containerWidth + scrollLeft) { 77 | targetScrollLeft = itemLeft + itemWidth - containerWidth 78 | } 79 | 80 | if (!isNum(targetScrollLeft)) return 81 | 82 | container.scrollLeft = targetScrollLeft 83 | } 84 | resetBottomBar() { 85 | const $bottomBar = this._$bottomBar 86 | const $el = this._$el 87 | 88 | const li = $el.find('.eruda-active').get(0) 89 | 90 | if (!li) return 91 | 92 | $bottomBar.css({ 93 | width: li.offsetWidth, 94 | left: li.offsetLeft - $el.get(0).scrollLeft, 95 | }) 96 | } 97 | _bindEvent() { 98 | const self = this 99 | 100 | this._$el 101 | .on('click', '.eruda-nav-bar-item', function () { 102 | self.emit('showTool', getToolName($(this))) 103 | }) 104 | .on('scroll', () => this.resetBottomBar()) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eruda", 3 | "version": "2.5.0", 4 | "description": "Console for Mobile Browsers", 5 | "main": "eruda.js", 6 | "types": "types/index.d.ts", 7 | "browserslist": [ 8 | "> 0.25%", 9 | "not dead" 10 | ], 11 | "scripts": { 12 | "postinstall": "patch-package", 13 | "ci": "npm run lint && npm run test && npm run build", 14 | "build": "rm -rf dist; NODE_ENV=production webpack --config build/webpack.prod.js", 15 | "watch": "rm -rf dist; NODE_ENV=development webpack --config build/webpack.prod.js --watch", 16 | "build:analyser": "webpack --config build/webpack.analyser.js", 17 | "dev": "webpack-dev-server --config build/webpack.dev.js --host 0.0.0.0", 18 | "test": "karma start", 19 | "format": "prettier *.{js,ts} src/**/*.{js,scss,css} build/*.js test/*.{js,html} --write", 20 | "lint": "eslint src/**/*.js", 21 | "lint:fix": "npm run lint -- --fix", 22 | "setup": "mkdir -p test/lib && cp node_modules/jasmine-core/lib/jasmine-core/{jasmine.css,jasmine.js,jasmine-html.js,boot.js} test/lib && cp node_modules/jasmine-jquery/lib/jasmine-jquery.js test/lib && shx cp node_modules/jquery/dist/jquery.js test/lib", 23 | "genIcon": "lsla genIcon --input src/style/icon --output src/style/icon.css --name eruda-icon && prettier src/**/*.css --write", 24 | "genUtilDoc": "eustia doc src/lib/util.js -f md -o doc/UTIL_API.md -t \"Eruda Util Documentation\"" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/liriliri/eruda.git" 29 | }, 30 | "keywords": [ 31 | "console", 32 | "mobile", 33 | "debug" 34 | ], 35 | "author": "redhoodsu", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/liriliri/eruda/issues" 39 | }, 40 | "homepage": "https://eruda.liriliri.io/", 41 | "devDependencies": { 42 | "@babel/core": "^7.18.6", 43 | "@babel/plugin-proposal-class-properties": "^7.18.6", 44 | "@babel/plugin-transform-runtime": "^7.18.6", 45 | "@babel/preset-env": "^7.18.6", 46 | "@babel/runtime": "^7.18.6", 47 | "@types/node": "^18.0.6", 48 | "autoprefixer": "^10.4.7", 49 | "babel-loader": "^8.2.5", 50 | "chobitsu": "^0.4.0", 51 | "css-loader": "^6.7.1", 52 | "draggabilly": "^2.2.0", 53 | "esbuild": "^0.14.49", 54 | "eslint": "^6.8.0", 55 | "esm-hook": "^0.1.4", 56 | "eustia-module": "^1.30.0", 57 | "handlebars": "^4.7.3", 58 | "handlebars-loader": "^1.7.1", 59 | "html-minifier": "^4.0.0", 60 | "html-minifier-loader": "^1.4.1", 61 | "istanbul-instrumenter-loader": "^3.0.1", 62 | "jasmine-core": "^2.99.1", 63 | "jasmine-jquery": "^2.1.1", 64 | "jquery": "^3.6.0", 65 | "js-beautify": "^1.10.3", 66 | "licia-es": "^1.36.0", 67 | "luna-console": "0.3.1", 68 | "luna-notification": "^0.1.4", 69 | "luna-object-viewer": "^0.2.1", 70 | "patch-package": "^6.4.7", 71 | "postcss": "^8.4.14", 72 | "postcss-clean": "^1.2.2", 73 | "postcss-loader": "^7.0.1", 74 | "postcss-prefixer": "^2.1.3", 75 | "regexp.escape": "^1.1.0", 76 | "sass-loader": "^13.0.2", 77 | "webpack": "^5.73.0", 78 | "webpack-bundle-analyzer": "^4.5.0", 79 | "webpack-cli": "^4.10.0", 80 | "webpack-dev-server": "^4.9.3" 81 | }, 82 | "resolutions": { 83 | "licia": "npm:lodash.noop", 84 | "postcss": "8.4.14" 85 | }, 86 | "dependencies": { 87 | "error-stack-parser": "^2.1.4", 88 | "path-browserify": "^1.0.1", 89 | "sass": "^1.53.0" 90 | }, 91 | "eslintIgnore": [ 92 | "dist" 93 | ] 94 | } 95 | -------------------------------------------------------------------------------- /src/EntryBtn/EntryBtn.js: -------------------------------------------------------------------------------- 1 | import Draggabilly from 'draggabilly' 2 | import emitter from '../lib/emitter' 3 | import Settings from '../Settings/Settings' 4 | import { Emitter, nextTick, orientation } from '../lib/util' 5 | import { pxToNum } from '../lib/fione' 6 | import evalCss from '../lib/evalCss' 7 | import style from './EntryBtn.scss' 8 | import template from './EntryBtn.hbs' 9 | 10 | export default class EntryBtn extends Emitter { 11 | constructor($container) { 12 | super() 13 | 14 | this._style = evalCss(style) 15 | 16 | this._$container = $container 17 | this._appendTpl() 18 | this._makeDraggable() 19 | this._bindEvent() 20 | this._registerListener() 21 | } 22 | hide() { 23 | this._$el.hide() 24 | } 25 | show() { 26 | this._$el.show() 27 | } 28 | setPos(pos) { 29 | if (this._isOutOfRange(pos)) { 30 | pos = this._getDefPos() 31 | } 32 | 33 | this._$el.css({ 34 | left: pos.x, 35 | top: pos.y, 36 | }) 37 | 38 | this.config.set('pos', pos) 39 | } 40 | getPos() { 41 | return this.config.get('pos') 42 | } 43 | destroy() { 44 | evalCss.remove(this._style) 45 | this._unregisterListener() 46 | this._$el.remove() 47 | } 48 | _isOutOfRange(pos) { 49 | pos = pos || this.config.get('pos') 50 | const defPos = this._getDefPos() 51 | 52 | return ( 53 | pos.x > defPos.x + 10 || pos.x < 0 || pos.y < 0 || pos.y > defPos.y + 10 54 | ) 55 | } 56 | _registerListener() { 57 | this._scaleListener = () => 58 | nextTick(() => { 59 | if (this._isOutOfRange()) this._resetPos() 60 | }) 61 | emitter.on(emitter.SCALE, this._scaleListener) 62 | } 63 | _unregisterListener() { 64 | emitter.off(emitter.SCALE, this._scaleListener) 65 | } 66 | _appendTpl() { 67 | const $container = this._$container 68 | 69 | $container.append(template()) 70 | this._$el = $container.find('.eruda-entry-btn') 71 | } 72 | _resetPos(orientationChanged) { 73 | const cfg = this.config 74 | let pos = cfg.get('pos') 75 | const defPos = this._getDefPos() 76 | 77 | if (!cfg.get('rememberPos') || orientationChanged) { 78 | pos = defPos 79 | } 80 | 81 | this.setPos(pos) 82 | } 83 | _bindEvent() { 84 | const draggabilly = this._draggabilly 85 | const $el = this._$el 86 | 87 | draggabilly 88 | .on('staticClick', () => this.emit('click')) 89 | .on('dragStart', () => $el.addClass('eruda-active')) 90 | 91 | draggabilly.on('dragEnd', () => { 92 | const cfg = this.config 93 | 94 | if (cfg.get('rememberPos')) { 95 | cfg.set('pos', { 96 | x: pxToNum(this._$el.css('left')), 97 | y: pxToNum(this._$el.css('top')), 98 | }) 99 | } 100 | 101 | $el.rmClass('eruda-active') 102 | }) 103 | 104 | orientation.on('change', () => this._resetPos(true)) 105 | window.addEventListener('resize', () => this._resetPos()) 106 | } 107 | _makeDraggable() { 108 | this._draggabilly = new Draggabilly(this._$el.get(0), { 109 | containment: true, 110 | }) 111 | } 112 | initCfg(settings) { 113 | const cfg = (this.config = Settings.createCfg('entry-button', { 114 | rememberPos: true, 115 | pos: this._getDefPos(), 116 | })) 117 | 118 | settings 119 | .separator() 120 | .switch(cfg, 'rememberPos', 'Remember Entry Button Position') 121 | 122 | this._resetPos() 123 | } 124 | _getDefPos() { 125 | const minWidth = this._$el.get(0).offsetWidth + 10 126 | 127 | return { 128 | x: window.innerWidth - minWidth, 129 | y: window.innerHeight - minWidth, 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Network/Network.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 3 | 4 | #network { 5 | padding-top: 36px; 6 | .title { 7 | @include absolute(100%, 36px); 8 | @include right-btn(); 9 | background: var(--darker-background); 10 | padding: $padding; 11 | color: var(--primary); 12 | height: 36px; 13 | border-bottom: 1px solid var(--border); 14 | } 15 | .requests { 16 | @include overflow-auto(y); 17 | height: 100%; 18 | border-bottom: 1px solid var(--border); 19 | margin-bottom: 10px; 20 | li { 21 | display: flex; 22 | width: 100%; 23 | cursor: pointer; 24 | border-bottom: 1px solid var(--border); 25 | height: 41px; 26 | color: var(--foreground); 27 | white-space: nowrap; 28 | &.error { 29 | span { 30 | color: var(--console-error-foreground); 31 | } 32 | } 33 | span { 34 | display: block; 35 | line-height: 40px; 36 | height: 40px; 37 | padding: 0 5px; 38 | font-size: $font-size-s; 39 | vertical-align: top; 40 | text-overflow: ellipsis; 41 | overflow: hidden; 42 | } 43 | .name { 44 | flex: 1; 45 | padding-left: $padding; 46 | } 47 | .status { 48 | width: 40px; 49 | } 50 | .method, 51 | .type { 52 | width: 50px; 53 | } 54 | .size { 55 | width: 70px; 56 | } 57 | .time { 58 | width: 60px; 59 | padding-right: $padding; 60 | } 61 | &:nth-child(even) { 62 | background: var(--contrast); 63 | } 64 | } 65 | } 66 | .detail { 67 | @include absolute(); 68 | z-index: 10; 69 | display: none; 70 | padding-bottom: 40px; 71 | background: var(--background); 72 | .http { 73 | @include overflow-auto(y); 74 | height: 100%; 75 | .breadcrumb { 76 | @include breadcrumb(); 77 | } 78 | .section { 79 | border-top: 1px solid var(--border); 80 | border-bottom: 1px solid var(--border); 81 | h2 { 82 | background: var(--darker-background); 83 | color: var(--primary); 84 | padding: $padding; 85 | font-size: $font-size; 86 | } 87 | margin-bottom: 10px; 88 | table { 89 | color: var(--foreground); 90 | * { 91 | user-select: text; 92 | } 93 | td { 94 | font-size: $font-size-s; 95 | padding: 5px 10px; 96 | word-break: break-all; 97 | } 98 | .key { 99 | white-space: nowrap; 100 | font-weight: bold; 101 | color: var(--accent); 102 | } 103 | } 104 | } 105 | .response, 106 | .data { 107 | user-select: text; 108 | @include overflow-auto(x); 109 | padding: $padding; 110 | font-size: $font-size-s; 111 | margin-bottom: 10px; 112 | white-space: pre-wrap; 113 | border-top: 1px solid var(--border); 114 | color: var(--foreground); 115 | border-bottom: 1px solid var(--border); 116 | } 117 | } 118 | .back { 119 | position: absolute; 120 | left: 0; 121 | bottom: 0; 122 | color: var(--foreground); 123 | width: 100%; 124 | border-top: 1px solid var(--border); 125 | background: var(--darker-background); 126 | display: block; 127 | height: 40px; 128 | line-height: 40px; 129 | text-decoration: none; 130 | text-align: center; 131 | margin-top: 10px; 132 | transition: background 0.3s; 133 | cursor: pointer; 134 | &:active { 135 | color: var(--select-foreground); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Console/Console.scss: -------------------------------------------------------------------------------- 1 | @import '../style/variable'; 2 | @import '../style/mixin'; 3 | 4 | .container .console-container { 5 | padding-top: 40px; 6 | padding-bottom: 24px; 7 | } 8 | 9 | .console-container { 10 | width: 100%; 11 | height: 100%; 12 | &.js-input-hidden { 13 | padding-bottom: 0; 14 | } 15 | .control { 16 | @include absolute(100%, 40px); 17 | cursor: default; 18 | font-size: 0; 19 | padding: 10px 10px 10px 35px; 20 | background: var(--darker-background); 21 | color: var(--primary); 22 | line-height: 20px; 23 | border-bottom: 1px solid var(--border); 24 | .icon-clear, 25 | .icon-search { 26 | display: inline-block; 27 | padding: 10px; 28 | font-size: $font-size-l; 29 | position: absolute; 30 | top: 1px; 31 | cursor: pointer; 32 | transition: color $anim-duration; 33 | &:active { 34 | color: var(--accent); 35 | } 36 | } 37 | .icon-clear { 38 | padding-right: 0px; 39 | left: 0; 40 | } 41 | .icon-search { 42 | right: 0; 43 | } 44 | .filter { 45 | cursor: pointer; 46 | font-size: $font-size-s; 47 | height: 20px; 48 | display: inline-block; 49 | margin: 0 2px; 50 | padding: 0 4px; 51 | line-height: 20px; 52 | transition: background $anim-duration, color $anim-duration; 53 | &.active { 54 | background: var(--highlight); 55 | color: var(--select-foreground); 56 | } 57 | } 58 | .search-keyword { 59 | position: absolute; 60 | line-height: 20px; 61 | max-width: 80px; 62 | overflow: hidden; 63 | right: 40px; 64 | font-size: $font-size; 65 | text-overflow: ellipsis; 66 | } 67 | } 68 | .js-input { 69 | pointer-events: none; 70 | position: absolute; 71 | z-index: 100; 72 | left: 0; 73 | bottom: 0; 74 | width: 100%; 75 | border-top: 1px solid var(--border); 76 | height: 24px; 77 | .icon-arrow-right { 78 | line-height: 23px; 79 | color: var(--accent); 80 | position: absolute; 81 | left: 10px; 82 | top: 0; 83 | z-index: 10; 84 | } 85 | &.active { 86 | height: 100%; 87 | padding-top: 40px; 88 | padding-bottom: 40px; 89 | border-top: none; 90 | .icon-arrow-right { 91 | display: none; 92 | } 93 | textarea { 94 | padding-left: 10px; 95 | } 96 | } 97 | .buttons { 98 | display: none; 99 | position: absolute; 100 | left: 0; 101 | bottom: 0; 102 | width: 100%; 103 | height: 40px; 104 | color: var(--primary); 105 | background: var(--darker-background); 106 | font-size: $font-size-s; 107 | border-top: 1px solid var(--border); 108 | .button { 109 | pointer-events: all; 110 | cursor: pointer; 111 | width: 50%; 112 | display: inline-block; 113 | text-align: center; 114 | border-right: 1px solid var(--border); 115 | height: 40px; 116 | line-height: 40px; 117 | &:last-child { 118 | border-right: none; 119 | } 120 | transition: background $anim-duration, color $anim-duration; 121 | &:active { 122 | color: var(--select-foreground); 123 | background: var(--highlight); 124 | } 125 | } 126 | } 127 | textarea { 128 | pointer-events: all; 129 | padding: 3px 10px; 130 | padding-left: 25px; 131 | outline: none; 132 | border: none; 133 | font-size: $font-size; 134 | width: 100%; 135 | height: 100%; 136 | user-select: text; 137 | resize: none; 138 | color: var(--primary); 139 | background: var(--background); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /patches/luna-console+0.3.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/luna-console/cjs/console/Log.js b/node_modules/luna-console/cjs/console/Log.js 2 | index 4c6835a..e21e513 100644 3 | --- a/node_modules/luna-console/cjs/console/Log.js 4 | +++ b/node_modules/luna-console/cjs/console/Log.js 5 | @@ -96,6 +96,7 @@ var linkify_1 = __importDefault(require("licia/linkify")); 6 | var highlight_1 = __importDefault(require("licia/highlight")); 7 | var util_1 = require("./util"); 8 | var stripIndent_1 = __importDefault(require("licia/stripIndent")); 9 | +var errorStackParser = __importStar(require("error-stack-parser")); 10 | var regJsUrl = /https?:\/\/([0-9.\-A-Za-z]+)(?::(\d+))?\/[A-Z.a-z0-9/]*\.js/g; 11 | var emptyHighlightStyle = { 12 | comment: '', 13 | @@ -445,9 +446,34 @@ var Log = (function (_super) { 14 | ret += "
    "); 15 | return ret; 16 | }; 17 | + 18 | + /** @param {Error} e */ 19 | + function formatLineNumberFromError(e) { 20 | + if (typeof e !== "object" || !e.stack) { 21 | + return ""; 22 | + } 23 | + 24 | + /** @type {{ columnNumber: string, lineNumber: number, fileName: string, source: string }[]} */ 25 | + const stack = errorStackParser.parse(e); 26 | + if (!stack.length) { 27 | + return ""; 28 | + } 29 | + 30 | + // https://github.com/stacktracejs/error-stack-parser/issues/81 31 | + if (stack[0].lineNumber == null) { 32 | + return ""; 33 | + } 34 | + 35 | + return `${stack[0].fileName}:${stack[0].lineNumber}`; 36 | + } 37 | + 38 | Log.prototype.formatErr = function (err) { 39 | var lines = err.stack ? err.stack.split('\n') : []; 40 | - var msg = "".concat(err.message || lines[0], "
    "); 41 | + var errMsg = err.toString() || err.message || lines[0]; 42 | + if (lines.length > 1) { 43 | + errMsg = `${errMsg}` 44 | + } 45 | + var msg = "".concat(errMsg, formatLineNumberFromError(err), "
    "); 46 | lines = lines.map(function (val) { return (0, escape_1.default)(val); }); 47 | var stack = "
    ").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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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 | ![Demo](https://eruda.liriliri.io/img/qrcode.png) 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 | 10 | {{/if}} 11 |
    12 | {{{name}}} 13 |
    14 | {{#if children}} 15 | 20 | {{/if}} 21 |
    22 |

    Attributes

    23 |
    24 | 25 | 26 | {{#if attributes}} 27 | {{#each attributes}} 28 | 29 | 30 | 31 | 32 | {{/each}} 33 | {{else}} 34 | 35 | 36 | 37 | {{/if}} 38 | 39 |
    {{name}}{{{value}}}
    Empty
    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 | 109 | 110 | 111 | {{/each}} 112 | 113 |
    {{@key}}{{{.}}}
    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,d09GRgABAAAAAAvoAAsAAAAAEZgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAQAAAAFZHb1PUY21hcAAAAYQAAACVAAACUPKX+h1nbHlmAAACHAAAB1oAAAoQydSW4mhlYWQAAAl4AAAAMQAAADYapMv4aGhlYQAACawAAAAdAAAAJAgEBBVobXR4AAAJzAAAABcAAABIRAb//GxvY2EAAAnkAAAAJgAAACYRiA/MbWF4cAAACgwAAAAfAAAAIAEjAQ1uYW1lAAAKLAAAASkAAAIWm5e+CnBvc3QAAAtYAAAAjwAAAMnZZQoFeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGQ+zjiBgZWBgamX6QwDA0M/hGZ8zWDEyAEUZWBlZsAKAtJcUxgcPjJ+FGQBcWNYmBgYgTQIMwAA9pkJ13ic7ZHJDcMwDATHtnyf6iNVpKC8Um6aUAUOV5syQmA4EEEJAgn0QBc8ggTNmwbFK6pNrXcstZ541p6kesn3HblRjnOquY3eFC8OjEzMcW9lY+fg5CJHy8A/tpo/v1PWFE2da2uQO6P9lGQ06dIb7a4MBnk0yJNBng3yYrTTshrkzeh3ZTfIh0E+DfJlkLMhfwF2lyt5AAAAeJx1FltsFNf1nntnZ/YxO7PjnZ3ZB55ld9kZ73q9750FO9hYGDDYYLB5NLwMNRgCqFFpkhqFDz6IlKCUqLSfSb7cfkDVRCoVbdWgiqqNqoJUKYR+VMpHP9JWfXzSNu2ue+7sBreV4rXOPfee93OXAME/RtnPiUJIPusEwK0buhQAQxfpR3q4Ows/VOyU0n0TvqakxhW4i/eUE+6+2f1G2EkRT54+ZavER0gA2gFw6PnuO7vgdvfwLujAqZ3do91jO3t8LE+/xe2ALoGYdcBuuo1M3WD50BoJWRwYKiWqQb+i8ksI8DUW69u4yvLrsnZLa7p1Ewz6KnIGLcOwgsiNwutaOEaYZ/cT9gkJkhixvRhtx2412yBGUZXbqJuGaUBWAd2Cetttu03OQMNH9kwPD9fg3uzva93pvScOz0wXS91fvPrk6tUn/7h0fuvExNbzl56UitMzh4/NdadrMHFsH9yrDQ9P74HLVz/++5OrnOEzzum57/nxPnufmKSKUWi6mK2AxIHdnACnnwrQVTAx9blMP8Q0tDkwKDEGQwsfiKo46fPdnw8ZCmCI8F4PX0N8nQ6/WyOKEZq/7/NN4sMHC6FBw4CvSn1MhXVqvy4fsp9hrghg+bH0JtYR2C9Xuj/o3l2BWQ/A3pXuXbYLAcz+D4HHtrZGiAACIUWyHbWIKohOGWOagAqUwTXQfx4H5lmlpiEpGDeGXYG8bloYbr09DjztXCTrUeFpJDMWHwwPUEF/OTuv0Y0F7QUqsCvGFlGIXQh93QwKhpLQL1KBdrpDzs3ji79ZPH7TGfovFN5DHX2+VzLzWjFNtQvU51sxo1ZSv+hfMQcVrv8iFYUd5/9f2kOf1e0eu0fiXt2+qD5fWNB/ilihmOpVyAr2KiTC/XW8R/eq+R0/log3M7/GsEQi5/10bf2i9hn6ff0xO0wGSJrU0DMvc8/SyXPJsmVojkPdAl0BllVoDFu8YYzTZpnCv144deJmPn/zxKmPPkcujC6Nji69zMFYorpJz43lknjom6rsUKFw6+TiraFicejW4slbhULn0z4nAngeWRKbxrKAZwMl0LVeX02ya0Tle8HOZcWYphuNehvcJit2HodCmmlqQZmWDFqGWdHUOvs1U4KZgc3kmfwddgd7imAT5bKSKIk6n9WGF2BOoTlvlJt8Zr0pljIaG3nu7UMvnTm1ZXR0y6kzL53+0sEV80Xj2JXtR2ZwmnE4Z45Mjm0pfQ9eYSNbJ8c2n17649LpzWPFwq1jE6dbpeLuPd/fs7tYSsZHkGXdjwfsARnC3aFAtkxbDc+N9V3h+WZmNJPnmuoiG9+2enf12tSlysi+uZ/M7RupcOTEjm1bqze6P7rcI0492DY1dW316InP6R5jKlm5AdOXPdLd1Wf99xf2V5LwMpFzxqGF9cNq6hZt1N22GcNpRbea45RbVyj9bUktjrTPvvbW9eV2++z16ckrNRViC513a8d32vbO40u9A26otSuT09fPttvL19967Wx7pKiWun9egNg6Dz/6PvwUd2iUJDELuSy2PjY3z8Ig5FoZrQGSKTkSg0O3z52jy+Vk1M+mOocf0nOPHu14+Mbrd5bp8rlk1FLDtx91DoH2xsMdjx7RHKqVCFlbE3wCkG+SO+QxeUo+Ix2edHAnoIXZLeNM2TzLbdc7RN4H2T5BBQVw+HCn4KNHw0ANs/+J4bB6T/wVH6zeDuLcCNLQcL3WMeu9G17GcWF5ptoufzLwRC/65qF/9qhm36okDkPb9vzhsmXqYCY8Y54GjlHT8UQc9INjbc8p29t6DteAXNwQKjfbDko7ksk+VCxtIB6SjIHBcmJjQI7QgNTaLicFNR7bbyRVlpKs0bQSFJgEzJepqCUjFQgaEUGQtKg/LEgCgN8nS8GYEknKkqlZI4mNfllhsrRhdFMiaOrpaEEXFZ8/IscLzZQWEEGkqKka1EMm06KSnIiUIlHGJKG77cz8XGmEsXr9wMLi0vz+kTKl1erc/KL8B18wvKGye0oZ8Adi+Wy9MgmSX27HtXixEtfwzc42DL8ckX+lDwmBsOwXVDWghQWRGcwnFepmVNDLTqKSZ75dsXdLB2enz9I03RfUgnHHTWlBSZYTWSOd95k0FRdC/o2yHApnpCALJMJiVA0aoQF/bWMo4leMZHMoAtQXCInBoGEN5P2iX/D7RFmK2M+le5oycSsvGoKSjNhR1UchKIY3xKImz7JghKKBalpkVAJfILLBtePJSDRlumr3edaoHTy0fHL/3lKZ0XrtwMKXFw/Mlas/tvWEHJ3YpUbjg5bCRpubxaKWlLVQolBNDPA3usUSA35xDBPqjwyoGpN84ZAcEk1JDtvVuF7Uy5viNYeJq/rrhT/NzL0IaT5qax38PmUCxd87SYJTkscFKOLPHtzemfo4AH7vGDH+hWO3zKzNb7h0/tY9rruujjDgDrPqsBvo/NoqWvjfSOXzbj7/bVayUyn734+dSduepEdjlhXrfpfDp/mWbbfyaPo/itrIyAAAeJxjYGRgYABii2PFJvH8Nl8ZuFkYQOD2wcO1MPr/3///WVhZmIBcDgYQyQAAXLENIQAAAHicY2BkYGBhAAEW1v9///9lYWVgZEAFQgBbzAQjAAAAeJxjYGBgYMGL///HK88KVvMXAFerBEQAAAAAAAAgADQAUgBwALQBAAEiAZAB3AIsAkwCkALQAxIDQATKBQgAAHicY2BkYGAQYmRkYGcAASYg5gJCBob/YD4DAAsEATIAeJxlkD1uwkAUhMdgSAJSghQpKbNVCiKZn5IDQE9Bl8KYtTGyvdZ6QaLLCXKEHCGniHKCHChj82hgLT9/M2/e7soABviFh3p5uG1qvVq4oTpxm/Qg7JOfhTvo40W4S38o3MMbpsJ9POKdO3j+HZ0BSuEW7vEh3Kb/KeyTv4Q7eMK3cJf+j3APK/wJ9/HqDdPIFLEp3FIn+yy0Z3n+rrStUlOoSTA+WwtdaBs6vVHro6oOydS5WMXW5GrOrs4yo0prdjpywda5cjYaxeIHkcmRIoJBgbipDktoJNgjQwh71b3UK6YtKvq1VpggwPgqtWCqaJIhlcaGyTWOrBUOPG1K1zGt+FrO5KS5zGreJCMr/u+6t6MT0Q+wbaZKzDDiE1/kg+YO+T89EV6oAAAAeJxti9EOgjAUQ1fYBg4Vxe/go5ZxEZPJyOUmyN+7yKt9aE+aVhXqkFP/1aFACQ0Diwo1TnBocMYFV7S44Y4OD+U8c9r6SKM0B/LrOYkLnkn6IW1zc+CvNiGS5zqk98K0rnagSEKG8pEtfRY/DyXtpJfo94ppzKPJZCOxaz6GKUekIFpSinrzPCv1BZLnLysA') 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 | ![Demo](https://eruda.liriliri.io/img/qrcode.png) 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 |
    Manual Test
    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 | 22 | 23 | 26 | 27 | {{/each}} 28 | {{else}} 29 | 30 | 31 | 32 | {{/if}} 33 | 34 |
    {{key}}{{val}} 24 | 25 |
    Empty
    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 | 58 | 59 | 62 | 63 | {{/each}} 64 | {{else}} 65 | 66 | 67 | 68 | {{/if}} 69 | 70 |
    {{key}}{{val}} 60 | 61 |
    Empty
    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 | 94 | 95 | 98 | 99 | {{/each}} 100 | {{else}} 101 | 102 | 103 | 104 | {{/if}} 105 | 106 |
    {{key}}{{val}} 96 | 97 |
    Empty
    107 |
    108 |
    109 |
    110 |

    111 | Script 112 |
    113 | 114 |
    115 |

    116 | 127 |
    128 |
    129 |

    130 | Stylesheet 131 |
    132 | 133 |
    134 |

    135 | 146 |
    147 |
    148 |

    149 | Iframe 150 |
    151 | 152 |
    153 |

    154 | 165 |
    166 |
    167 |

    168 | Image 169 |
    170 | 171 |
    172 |

    173 | 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 | --------------------------------------------------------------------------------