├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh-CN.md ├── circle.yml ├── examples ├── jsonTree.css ├── jsonTree.html ├── todo.html └── todo.js ├── package.json ├── scripts ├── devtools.config.js └── inject.config.js ├── src ├── devtools-ui │ ├── agent.js │ ├── components │ │ ├── Devtools.js │ │ ├── Element.css │ │ ├── Element.js │ │ ├── Elements.css │ │ ├── Elements.js │ │ ├── Header.css │ │ ├── Header.js │ │ ├── JsonTree.css │ │ ├── JsonTree.js │ │ ├── JsonTreeProp.css │ │ ├── JsonTreeProp.js │ │ ├── Search.css │ │ ├── Search.js │ │ ├── Sidebar.css │ │ ├── Sidebar.js │ │ ├── SidebarPane.css │ │ ├── SidebarPane.js │ │ ├── SimpleJsonTree.js │ │ ├── Tabs.css │ │ └── Tabs.js │ ├── events │ │ ├── enter.js │ │ ├── index.js │ │ ├── input.js │ │ ├── mouseenter.js │ │ └── mouseleave.js │ ├── fonts │ │ └── Roboto-Regular.ttf │ ├── index.css │ ├── index.js │ ├── media │ │ ├── arrow.svg │ │ ├── at-sign.svg │ │ ├── github.svg │ │ ├── inspect.svg │ │ ├── loading.svg │ │ ├── next.svg │ │ ├── prev.svg │ │ ├── refresh.svg │ │ ├── regular.png │ │ ├── search.svg │ │ ├── target.svg │ │ └── target_active.svg │ ├── port.js │ └── utils │ │ ├── findElement.js │ │ ├── getData.js │ │ ├── getOthersData.js │ │ ├── highLighter.js │ │ ├── highlightNode.js │ │ ├── index.js │ │ ├── inspectComponent.js │ │ ├── inspectNode.js │ │ ├── isPrimitive.js │ │ ├── makeElementTree.js │ │ ├── patch.js │ │ ├── printInConsole.js │ │ ├── searchPath.js │ │ ├── showDefinitionByUUID.js │ │ ├── type.js │ │ └── updateInstance.js ├── electron │ └── .gitkeep ├── extension │ ├── background.js │ ├── content.js │ ├── devtools.html │ ├── devtools.js │ ├── hook.js │ ├── inject.js │ ├── manifest.json │ └── regular.png └── shared │ ├── circular-json.js │ └── log.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "google", 3 | "installedESLint": true, 4 | "rules": { 5 | "indent": ["error", 4], 6 | "no-negated-condition": 0, 7 | "no-implicit-coercion":0, 8 | "linebreak-style": 0, 9 | "guard-for-in": 0 10 | }, 11 | "env": { 12 | "browser": true, 13 | }, 14 | "globals": { 15 | "devtoolsModel": true, 16 | "chrome":true, 17 | "Regular":true, 18 | "CircularJSON":true, 19 | "lastSelected":true, 20 | "sidebarView":true, 21 | "componentHandler":true 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.log 3 | node_modules 4 | dist 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### v0.9.3 2017-09-15 2 | 3 | + Fix currenNode undefined bug 4 | 5 | #### v0.9.2 2017-09-14 6 | 7 | + Fix `lockHightLight` bug 8 | 9 | #### v0.9.1 2017-09-14 10 | 11 | + Improve element view UI 12 | + [Scroll into view when selecting an element in devtools(#26 )](https://github.com/regularjs/regular-devtools/issues/26) 13 | 14 | #### v0.9.0 2017-09-13 15 | 16 | + Improve UI 17 | + Add inspect DOM and focus component mode 18 | + Refactor, using rollup for bundling 19 | 20 | #### v0.1 2016-07-26 21 | 22 | #### v0.2 2016-08-24 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2012-2015 NetEase, Inc. and regularjs contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regular Developer Tools 2 | 3 | [![version][version-image]][version-url] 4 | [![build][build-image]][build-url] 5 | [![download][download-image]][download-url] 6 | [![rating][rating-image]][rating-url] 7 | 8 | > Regular Developer Tools is a Chrome Extension that allows real-time inspection of [Regular](http://regularjs.github.io/) components. 9 | 10 | *Regular Developer Tools is under active development, any feedback is welcome* :clap: 11 | 12 | ### Overview 13 | 14 | Some GIFs to show you how Regular Devtools works. You can: 15 | 16 |
17 | View component tree structure, and select a component to get its data, computed data, filters and directives 18 | devtools-demo 19 |
20 | 21 |
22 | Data changes are synchronized in a bi-direction way 23 | devtools-demo 24 |
25 | 26 |
27 | Select a component, and inspect its instance by evaluating `$r` in the console 28 | devtools-demo 29 |
30 | 31 |
32 | Inspecting mode allow user to select a DOM node and view its corresponding component in Devtools Panel 33 | devtools-demo 34 |
35 | 36 | ### Features 37 | 38 | > **New in v0.9** 39 | > Now you can enter the brand new **inspecting mode** by the hitting the "target" button in navbar. 40 | 41 | 42 | 43 | + Inspecting Regular components hierarchy tree in element view. 44 | + Inspecting data, filters, directives, animations of selected component in sidebar. 45 | + Data changes made with Regular components will be reflected in both element view and sidebar in real-time. 46 | + Sidebar data is editable, changes will be applied to the corresponding component in page. 47 | + Searching component in element view. 48 | + Included contents will be annotated with `#inc`. 49 | + Click `inspect` button in the sidebar to inspect DOM node of selected component in Elements tab. 50 | + **Inspecting mode** allow user to inspect DOM node, and the corresponding component will be focused in Developer Tool. 51 | + **Pro Tip One**: When inspecting DOM node in Elements tab, switch to Regular tab, if the DOM node you are inspecting is rendered from a Regular component, the Regular Devtools will automatically focus on that component. It's like the reverse version of the last feature. 52 | + **Pro Tip Two**: When selecting component in element view, the component instance is available as `$r` in your console. 53 | 54 | ### Prerequisition 55 | 56 | Require [regularjs](https://github.com/regularjs/regular) **v0.5.0+**. 57 | 58 | ### Installation 59 | 60 | Install from [Chrome Web Store](https://chrome.google.com/webstore/detail/regular-developer-tools/ehlcoecgkhfjffhmdhmhbjkjjpaecmam) 61 | 62 | ### Manual Installation 63 | 64 | + **Step 1** Clone this repo. 65 | + **Step 2** run `npm i && npm run build` in command line, you will get `dist` folder in current working directory 66 | + **Step 3** Open Google Chrome and navigate to `chrome://extensions/`. 67 | + **Step 4** Check Developement mode checkbox in right corner. 68 | + **Step 5** Click `Load unpacked extension` and load the `dist` folder. 69 | 70 | ### Development 71 | 72 | ```bash 73 | # Install dependencies 74 | $ npm install 75 | # Build and watch file changes 76 | $ npm run watch 77 | # Build for production 78 | $ npm run build 79 | ``` 80 | 81 | ### Changelog 82 | 83 | [CHANGELOG](CHANGELOG.md) 84 | 85 | ### License 86 | 87 | [MIT](https://github.com/regularjs/regular-devtools/blob/master/LICENSE) 88 | 89 | [build-image]: https://img.shields.io/circleci/project/regularjs/regular-devtools/master.svg?style=flat-square 90 | [build-url]: https://circleci.com/gh/regularjs/regular-devtools 91 | 92 | [version-image]: https://img.shields.io/chrome-web-store/v/ehlcoecgkhfjffhmdhmhbjkjjpaecmam.svg?style=flat-square 93 | [version-url]: https://chrome.google.com/webstore/detail/regular-developer-tools/ehlcoecgkhfjffhmdhmhbjkjjpaecmam 94 | 95 | [download-image]: https://img.shields.io/chrome-web-store/d/ehlcoecgkhfjffhmdhmhbjkjjpaecmam.svg?style=flat-square 96 | [download-url]: https://chrome.google.com/webstore/detail/regular-developer-tools/ehlcoecgkhfjffhmdhmhbjkjjpaecmam 97 | 98 | [rating-image]: https://img.shields.io/chrome-web-store/rating/ehlcoecgkhfjffhmdhmhbjkjjpaecmam.svg?style=flat-square 99 | [rating-url]: https://chrome.google.com/webstore/detail/regular-developer-tools/ehlcoecgkhfjffhmdhmhbjkjjpaecmam 100 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Regular Developer Tools [![build][build-image]][build-url] 2 | 3 | > Regular Developer Tools是一个chrome扩展,它允许你实时地观察[Regular](http://regularjs.github.io/)组件 4 | 5 | *Regular Developer Tools仍然处于公测阶段, 欢迎反馈任何问题* :clap: 6 | 7 | ### 总览 8 | 9 | 下面一些GIF会告诉你Regular Devtools是如何工作的 10 | 11 | ![rdt-demo](https://raw.githubusercontent.com/zxc0328/regular-devtools/master/gifs/rdt_demo_ss.gif) 12 | 13 | ![rdt-demo](https://raw.githubusercontent.com/zxc0328/regular-devtools/master/gifs/rdt_demo_dom_ss.gif) 14 | 15 | ### 特性 16 | 17 | + 查看Regular组件的树形结构 18 | + 查看选中组件的data、filters、directives、animations 19 | + 页面中的组件变化会实时更新到Regular Devtools中 20 | + 侧边栏的data是可编辑的,编辑后变动将自动应用到页面中 21 | + 在组件视图中搜索组件 22 | + include的内容会使用`#inc`进行标识 23 | + 点击`inspect`查看选中组件对应的DOM节点 24 | + **小贴士一**:当从Elements面板切换到Regular面板时,如果之前选中的DOM节点是由Regular组件渲染出来的,Regular Devtools会自动选中该组件,这相当于最后一条特性的相反版本 25 | + **小贴士二**:当在组件视图中选中一个组件后,你可以在console中通过`$r`取到当前组件实例的引用 26 | 27 | ### 前置条件 28 | 29 | 你的项目需要使用定制的regularjs(或使用官方0.5及以上版本的regularjs),你可以在这里找到定制版本[`/libs/regular.js`](https://github.com/regularjs/regular-devtools/blob/master/lib/regular.js) 30 | 31 | ### 安装 32 | 33 | 从[谷歌应用商店](https://chrome.google.com/webstore/detail/regular-developer-tools/ehlcoecgkhfjffhmdhmhbjkjjpaecmam)安装 34 | 35 | ### 如何开发 36 | 37 | + **步骤1** 克隆本仓库 38 | + **步骤2** 执行命令`npm i && npm run build`,会打包源码到dist目录 39 | + **步骤3** 打开Chrome浏览器,并导航至`chrome://extensions/` 40 | + **步骤4** 勾选右上角的`开发者模式` 41 | + **步骤5** 点击`加载已解压的扩展程序...`,选择刚刚生成的`dist`文件夹 42 | 43 | ### 更新日志 44 | 45 | #### v0.1 2016-07-26 46 | 47 | #### v0.2 2016-08-24 48 | 49 | ### License 50 | 51 | [MIT](https://github.com/regularjs/regular-devtools/blob/master/LICENSE) 52 | 53 | [build-image]: https://img.shields.io/circleci/project/regularjs/regular-devtools/master.svg?style=flat-square 54 | [build-url]: https://circleci.com/gh/regularjs/regular-devtools 55 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6 -------------------------------------------------------------------------------- /examples/jsonTree.css: -------------------------------------------------------------------------------- 1 | .json-tree { 2 | padding: 10px 20px; 3 | } 4 | 5 | .json-tree-data { 6 | font-size: 13px; 7 | } 8 | 9 | .json-tree-data-key .arrow { 10 | height: 10px; 11 | width: 10px; 12 | margin-left: -13px; 13 | margin-right: 3px; 14 | } 15 | 16 | .json-tree-data-key .arrow-down { 17 | transform: rotate(90deg); 18 | } 19 | 20 | .json-tree-data-key { 21 | line-height: 13px; 22 | padding-bottom:6px; 23 | white-space: nowrap; 24 | font-size: 0; 25 | cursor: default; 26 | } 27 | .json-tree-data-key .item { 28 | display: inline-block; 29 | vertical-align: middle; 30 | font-size: 13px; 31 | } 32 | 33 | .json-tree-data .key{ 34 | color:#881391; 35 | padding-right: 5px; 36 | } 37 | 38 | .json-tree-data .edit{ 39 | padding: 3px; 40 | margin-top: -3px; 41 | position: absolute; 42 | border: solid 1px #e6e6e6; 43 | box-sizing: border-box; 44 | } 45 | 46 | .json-tree-data .primitive{ 47 | color: #03c; 48 | padding-right: 15px; 49 | } 50 | 51 | .json-tree-data .string{ 52 | color: #c41a16; 53 | padding-right: 15px; 54 | } 55 | 56 | .json-tree-data .others{ 57 | color: #444; 58 | padding-right: 15px; 59 | } 60 | 61 | .json-tree-data-key .hide{ 62 | display: none; 63 | } -------------------------------------------------------------------------------- /examples/jsonTree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jsonTree 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 20 | 65 | 177 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /examples/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | regular-todo 6 | 7 | 8 | 9 |
10 | 11 | 12 | 18 | 19 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/todo.js: -------------------------------------------------------------------------------- 1 | var Todo = Regular.extend({ 2 | template: "#todo", 3 | foo() {} 4 | }); 5 | 6 | Todo.filter("test", function () {}); 7 | 8 | var TodoMVC = Regular.extend({ 9 | template: '#todomvc', // id | template string | preparsed ast 10 | // get the list; 11 | config() {} 12 | }) 13 | 14 | TodoMVC.component("custom-todo", Todo); 15 | 16 | var todos = [ 17 | { description: "sleep" }, 18 | { description: "work" } 19 | ] 20 | var app = new TodoMVC({ 21 | data: {todos: todos} 22 | }).$inject("#todoapp") 23 | 24 | new TodoMVC({ 25 | data: {todos: todos} 26 | }).$inject(document.body); 27 | new TodoMVC({ 28 | data: {todos: todos} 29 | }).$inject(document.body); 30 | new TodoMVC({ 31 | data: {todos: todos} 32 | }).$inject(document.body); 33 | new TodoMVC({ 34 | data: {todos: todos} 35 | }).$inject(document.body); 36 | new TodoMVC({ 37 | data: {todos: todos} 38 | }).$inject(document.body); 39 | new TodoMVC({ 40 | data: {todos: todos} 41 | }).$inject(document.body); 42 | new TodoMVC({ 43 | data: {todos: todos} 44 | }).$inject(document.body); 45 | new TodoMVC({ 46 | data: {todos: todos} 47 | }).$inject(document.body); 48 | new TodoMVC({ 49 | data: {todos: todos} 50 | }).$inject(document.body); 51 | new TodoMVC({ 52 | data: {todos: todos} 53 | }).$inject(document.body); 54 | new TodoMVC({ 55 | data: {todos: todos} 56 | }).$inject(document.body); 57 | new TodoMVC({ 58 | data: {todos: todos} 59 | }).$inject(document.body); 60 | new TodoMVC({ 61 | data: {todos: todos} 62 | }).$inject(document.body); 63 | new TodoMVC({ 64 | data: {todos: todos} 65 | }).$inject(document.body); 66 | new TodoMVC({ 67 | data: {todos: todos} 68 | }).$inject(document.body); 69 | new TodoMVC({ 70 | data: {todos: todos} 71 | }).$inject(document.body); 72 | new TodoMVC({ 73 | data: {todos: todos} 74 | }).$inject(document.body); 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "regular-devtools", 4 | "version": "0.9.3", 5 | "description": "Regular Developer Tools is a Chrome Extension that allows real-time inspection of [Regular](http://regularjs.github.io/) components.", 6 | "scripts": { 7 | "build": "npm run build:devtools && npm run build:inject", 8 | "build:devtools": "poi build --config scripts/devtools.config.js", 9 | "build:inject": "poi build --config scripts/devtools.config.js", 10 | "watch": "concurrently \"npm run watch:devtools\" \"npm run watch:inject\"", 11 | "watch:devtools": "poi watch --config scripts/devtools.config.js", 12 | "watch:inject": "poi watch --config scripts/inject.config.js", 13 | "test": "npm run lint", 14 | "lint": "eslint src --quiet", 15 | "precommit": "lint-staged" 16 | }, 17 | "lint-staged": { 18 | "src/**/*.js": "eslint --quiet" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/regularjs/regular-devtools.git" 23 | }, 24 | "author": "", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/regularjs/regular-devtools/issues" 28 | }, 29 | "homepage": "https://github.com/regularjs/regular-devtools#readme", 30 | "devDependencies": { 31 | "bili": "^0.14.0", 32 | "concurrently": "^3.3.0", 33 | "eslint": "^3.2.2", 34 | "eslint-config-google": "^0.6.0", 35 | "husky": "^0.14.3", 36 | "lint-staged": "^4.1.3", 37 | "material-design-lite": "^1.3.0", 38 | "mitt": "^1.1.2", 39 | "poi": "^9.3.5", 40 | "regularjs": "0.5.2" 41 | }, 42 | "dependencies": {} 43 | } 44 | -------------------------------------------------------------------------------- /scripts/devtools.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/devtools-ui/index.js', 3 | copy: [ 4 | { 5 | from: 'src/devtools-ui/media', 6 | to: 'static/media', 7 | }, 8 | ], 9 | webpack( config ) { 10 | config.resolve.alias[ 'regularjs' ] = require.resolve( 'regularjs/dist/regular.js' ); 11 | return config; 12 | }, 13 | minimize: true, 14 | sourceMap: true, 15 | extractCSS: true, 16 | vendor: true 17 | }; 18 | -------------------------------------------------------------------------------- /scripts/inject.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/extension/inject.js', 3 | filename: { 4 | js: 'inject.bundle.js' 5 | }, 6 | copy: [ 7 | { 8 | from: 'src/extension', 9 | to: './', 10 | }, 11 | ], 12 | minimize: true, 13 | sourceMap: true, 14 | html: false, 15 | vendor: false 16 | }; 17 | -------------------------------------------------------------------------------- /src/devtools-ui/agent.js: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | import port from './port'; 3 | 4 | class Agent { 5 | constructor() { 6 | Object.assign(this, mitt()); 7 | 8 | port.onMessage.addListener(message => { 9 | switch (message.type) { 10 | case 'dataUpdate': 11 | this.emit('dataUpdate'); 12 | break; 13 | case 'reRender': 14 | this.emit('reRender', message.nodes); 15 | break; 16 | case 'initNodes': 17 | this.emit('initNodes', message.nodes); 18 | break; 19 | case 'currNodeChange': 20 | this.emit('currNodeChange', message.uuid); 21 | break; 22 | case 'pageReload': 23 | this.emit('pageReload', message.tabId); 24 | break; 25 | default: 26 | // skip 27 | } 28 | }); 29 | 30 | // listen for messge when switch from element tab to regular tab 31 | window.addEventListener("message", event => { 32 | if (event.data.type === "currNodeChange") { 33 | this.emit("currNodeChange", event.data.uuid); 34 | } 35 | }, false); 36 | } 37 | 38 | injectContentScript(tabId) { 39 | port.postMessage({ 40 | tabId: tabId || chrome.devtools.inspectedWindow.tabId, 41 | file: "/content.js" 42 | }); 43 | } 44 | 45 | openInNewTab(url) { 46 | port.postMessage({url}); 47 | } 48 | } 49 | 50 | export default new Agent(); 51 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Devtools.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import Header from './Header'; 3 | import Sidebar from './Sidebar'; 4 | import Elements from './Elements'; 5 | import {searchPath, printInConsole, enter, exit} from '../utils'; 6 | 7 | // Regular components for devtools' UI 8 | const Devtools = Regular.extend({ 9 | template: ` 10 | 16 | 17 |
18 | 19 | 20 |
21 | `, 22 | data: { 23 | inspecting: false 24 | }, 25 | onVisitGithub: function() { 26 | this.$emit("openNewTab", "https://github.com/regularjs/regular-devtools"); 27 | }, 28 | onInspect: function() { 29 | if (this.data.inspecting) { 30 | this.data.inspecting = false; 31 | this.$refs.sidebarView.$emit("lockHighLight", false); 32 | exit(); 33 | } else { 34 | this.data.inspecting = true; 35 | this.$refs.sidebarView.$emit("lockHighLight", true); 36 | enter(); 37 | } 38 | this.$update(); 39 | }, 40 | onRefresh: function() { 41 | this.$emit('refresh'); 42 | }, 43 | focusNode: function(uuid) { 44 | const elementViewDOM = document.querySelector(".elementTree"); 45 | const elementView = this.$refs.elementView; 46 | const relativeOffset = elementView._children[0].group.children[0].last().getBoundingClientRect().top; 47 | // unselect last selected 48 | if (this.data.lastSelected) { 49 | this.data.lastSelected.data.selected = false; 50 | } 51 | 52 | // update sidebarView 53 | this.$emit("clickElement", uuid); 54 | 55 | // update elementView 56 | const path = []; 57 | searchPath(elementView._children, uuid, path); 58 | for (let i = 0; i < path.length; i++) { 59 | path[i].data.opened = true; 60 | } 61 | this.data.lastSelected = path[0]; 62 | path[0].data.selected = true; 63 | printInConsole(uuid); 64 | elementView.$update(); 65 | 66 | // scroll into view 67 | const currTop = path[0].group.children[0].last().getBoundingClientRect().top; 68 | console.log(currTop); 69 | elementViewDOM.scrollTop = currTop - relativeOffset; 70 | } 71 | }); 72 | 73 | Devtools.component('devtools-header', Header); 74 | Devtools.component('devtools-sidebar', Sidebar); 75 | Devtools.component('devtools-elements', Elements); 76 | 77 | export default Devtools; 78 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Element.css: -------------------------------------------------------------------------------- 1 | .element__wrapper { 2 | position: relative; 3 | } 4 | 5 | .element__joint { 6 | position: absolute; 7 | width: 0; 8 | border-right: dashed 1px #ddd; 9 | } 10 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Element.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import {findElementByUUID} from '../utils'; 3 | import './Element.css'; 4 | 5 | const Element = Regular.extend({ 6 | template: ` 7 |
8 | {#if opened} 9 |
10 | {/if} 11 |
17 |
18 | arrow 23 | < 24 | 25 | {node.name} 26 | 27 | 28 | {#if node.childNodes.length === 0} /{/if}>{#if node.isIncluded }#inc{/if} 29 | 30 |
31 | {#if opened && node.childNodes.length > 0} 32 | {#list node.childNodes as n} 33 | 34 | {/list} 35 | {/if} 36 | {#if node.childNodes.length > 0} 37 |
41 | </ 42 | {node.name} 43 | > 44 |
45 | {/if} 46 |
47 | `, 48 | data: { 49 | selected: false, 50 | opened: false 51 | }, 52 | onMouseEnter: function(uuid, inspectable) { 53 | // Communication between two components(not directly related), should refactor using eventbus. 54 | this.$root.$refs.sidebarView.highLightNode(uuid, inspectable); 55 | }, 56 | onClick: function(node) { 57 | let lastSelected = this.$root.data.lastSelected; 58 | if (lastSelected) { 59 | if (lastSelected === this) { 60 | return; 61 | } 62 | 63 | if (!findElementByUUID(this.$root.data.nodes, lastSelected.data.node.uuid)) { 64 | this.$root.data.lastSelected = null; 65 | } else { 66 | lastSelected.data.selected = false; 67 | } 68 | } 69 | this.data.selected = true; 70 | this.$root.data.lastSelected = this; 71 | this.$root.$emit("clickElement", node.uuid); 72 | } 73 | }); 74 | 75 | Element.component('element', Element); 76 | 77 | export default Element; 78 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Elements.css: -------------------------------------------------------------------------------- 1 | .elementView { 2 | position: relative; 3 | box-sizing: border-box; 4 | display: flex; 5 | flex-direction: column; 6 | width: 50%; 7 | font-size: 20px; 8 | color: #000; 9 | vertical-align: top; 10 | height: 100%; 11 | border-right: 1px solid #e3e3e3; 12 | } 13 | 14 | .elementView .element { 15 | line-height: 18px; 16 | font-size: 0; 17 | padding: 0 20px 3px; 18 | white-space: nowrap; 19 | cursor: default; 20 | } 21 | 22 | .elementView .warnning { 23 | padding: 20px; 24 | font-size: 15px; 25 | } 26 | 27 | .elementView .tag { 28 | color: rgb(168, 148, 166) 29 | } 30 | 31 | .elementView .arrow { 32 | height: 12px; 33 | width: 12px; 34 | } 35 | 36 | .elementView .ele-item { 37 | font-size: 13px; 38 | display: inline-block; 39 | vertical-align: middle; 40 | } 41 | 42 | .elementView .tagname.is-anonymous { 43 | font-style: italic; 44 | } 45 | 46 | .elementView .ele-include { 47 | font-size: 11px; 48 | margin-left: 5px; 49 | color: #999; 50 | } 51 | 52 | .elementView .arrow-down { 53 | transform: rotate(90deg); 54 | } 55 | 56 | .elementView .loading-img { 57 | width: 30px; 58 | height: 30px; 59 | position: absolute; 60 | top: 50%; 61 | left: 50%; 62 | margin-left: -15px; 63 | margin-top: -15px; 64 | } 65 | 66 | @keyframes loading-animation { 67 | from { 68 | transform: rotate(0deg); 69 | } 70 | to { 71 | transform: rotate(360deg); 72 | } 73 | } 74 | 75 | .element-tag:hover { 76 | background-color: #e5e5e5; 77 | } 78 | 79 | .selected { 80 | background-color: #44a1ff; 81 | } 82 | 83 | .flush-animation {} 84 | 85 | .elementView .selected .ele-item { 86 | color: #fff; 87 | } 88 | 89 | .elementView .selected .ele-include { 90 | color: #fff; 91 | } 92 | 93 | .elementView .hide { 94 | display: none; 95 | } 96 | 97 | .elementTree { 98 | flex: 1; 99 | width: 100%; 100 | overflow-y: auto; 101 | padding: 20px 0; 102 | box-sizing: border-box; 103 | } 104 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Elements.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import Element from './Element'; 3 | import Search from './Search'; 4 | import './Elements.css'; 5 | 6 | const Elements = Regular.extend({ 7 | template: ` 8 |
9 |
10 | {#if loading } 11 |
12 |
13 |
14 | {#else} 15 | {#if nodes.length > 0} 16 | {#list nodes as node} 17 | {#if node.hasTemplate && node.hasInjected} 18 | 19 | {/if} 20 | {/list} 21 | {#else} 22 |
There is no Regular instance detected. Please check if you are using the latest version of Regularjs. Or try reloading Regular Devtools
23 | {/if} 24 | {/if} 25 |
26 | 27 |
28 | `, 29 | data: { 30 | nodes: [], 31 | loading: true 32 | }, 33 | onMouseLeave: function() { 34 | this.$root.$refs.sidebarView.highLightNode(null, false); 35 | } 36 | }); 37 | 38 | Elements.component('element', Element); 39 | Elements.component('search', Search); 40 | 41 | export default Elements; 42 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | display: flex; 3 | width: 100%; 4 | height: 60px; 5 | border-bottom: 1px solid #e3e3e3; 6 | box-shadow: 0 0 8px rgba(0, 0, 0, .15); 7 | box-sizing: border-box; 8 | position: absolute; 9 | left: 0; 10 | top: 0; 11 | padding: 10px 20px; 12 | } 13 | 14 | .header__logo { 15 | display: flex; 16 | align-items: center; 17 | flex: 1; 18 | padding-left: 50px; 19 | background-image: url(/static/media/regular.png); 20 | background-repeat: no-repeat; 21 | background-size: auto 100%; 22 | white-space: nowrap; 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | font-size: 20px; 26 | cursor: default; 27 | } 28 | 29 | .header__toolbar { 30 | height: 70%; 31 | align-self: center; 32 | } 33 | 34 | .header__inspect, .header__refresh, .header__github { 35 | height: 100%; 36 | cursor: pointer; 37 | outline: none; 38 | margin-left: 10px; 39 | } 40 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Header.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import './Header.css'; 3 | 4 | export default Regular.extend({ 5 | template: ` 6 |
7 | 8 | 9 |
10 | 11 | 12 | 13 |
Select
14 |
Reload
15 |
GitHub
16 |
17 |
18 | `, 19 | onInspect() { 20 | this.$emit('inspect'); 21 | }, 22 | onRefresh() { 23 | this.$emit('refresh'); 24 | }, 25 | onVisitGithub() { 26 | this.$emit('visit-github'); 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /src/devtools-ui/components/JsonTree.css: -------------------------------------------------------------------------------- 1 | .json-tree { 2 | padding: 10px 20px; 3 | } 4 | -------------------------------------------------------------------------------- /src/devtools-ui/components/JsonTree.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import JsonTreeProp from './JsonTreeProp'; 3 | import './JsonTree.css'; 4 | 5 | const JsonTree = Regular.extend({ 6 | template: ` 7 |
8 | {#list Object.keys(source) as k} 9 | 10 | {/list} 11 |
12 | `, 13 | data: { 14 | source: {} 15 | }, 16 | config: function() { 17 | var self = this; 18 | 19 | function onClick(e) { 20 | self.$emit('checkClickOutside', e.target); 21 | } 22 | document.addEventListener('click', onClick, false); 23 | this.$on('$destroy', function() { 24 | document.removeEventListener('click', onClick, false); 25 | }); 26 | }, 27 | isJsonTree() { 28 | return true; 29 | } 30 | }); 31 | 32 | JsonTree.component('JsonTreeProp', JsonTreeProp); 33 | 34 | export default JsonTree; 35 | -------------------------------------------------------------------------------- /src/devtools-ui/components/JsonTreeProp.css: -------------------------------------------------------------------------------- 1 | .json-tree-data { 2 | font-size: 13px; 3 | } 4 | 5 | .json-tree-data-key .arrow { 6 | height: 10px; 7 | width: 10px; 8 | margin-left: -13px; 9 | margin-right: 3px; 10 | } 11 | 12 | .json-tree-data-key .arrow-down { 13 | transform: rotate(90deg); 14 | } 15 | 16 | .json-tree-data-key { 17 | position: relative; 18 | line-height: 13px; 19 | padding-bottom: 6px; 20 | white-space: nowrap; 21 | font-size: 0; 22 | cursor: default; 23 | } 24 | 25 | .json-tree-data-key .item { 26 | display: inline-block; 27 | vertical-align: middle; 28 | font-size: 13px; 29 | } 30 | 31 | .json-tree-data .key { 32 | color: #881391; 33 | padding-right: 5px; 34 | } 35 | 36 | .json-tree-data .edit { 37 | padding: 3px; 38 | margin-top: -3px; 39 | position: absolute; 40 | border: solid 1px #e6e6e6; 41 | box-sizing: border-box; 42 | } 43 | 44 | .json-tree-data .primitive { 45 | color: #03c; 46 | padding-right: 15px; 47 | } 48 | 49 | .json-tree-data .string { 50 | color: #c41a16; 51 | padding-right: 15px; 52 | } 53 | 54 | .json-tree-data .function { 55 | color: rgb(13, 34, 170); 56 | font-style: italic; 57 | } 58 | 59 | .json-tree-data .dom { 60 | font-style: italic; 61 | } 62 | 63 | .json-tree-data .gray { 64 | color: #999; 65 | padding-right: 15px; 66 | } 67 | 68 | .json-tree-data .others { 69 | color: #444; 70 | padding-right: 15px; 71 | } 72 | 73 | .json-tree-data-key .hide { 74 | display: none; 75 | } 76 | -------------------------------------------------------------------------------- /src/devtools-ui/components/JsonTreeProp.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import {isPrimitive, type} from '../utils'; 3 | import './JsonTreeProp.css'; 4 | 5 | const JsonTreeProp = Regular.extend({ 6 | template: ` 7 |
8 |
9 | arrow 15 | {key + ':'} 16 | 17 | 18 | {#if !editing} 19 | {#if this.isPrimitive(value)} 20 | {#if type === 'String'} 21 | {#if value === 'Function'} 22 | f() 23 | {#elseif value === '[DOM node]'} 24 | DOM Node 25 | {#elseif value === '[Circular]'} 26 | Circular Reference 27 | {#else} 28 | "{value}" 29 | {/if} 30 | {#else} 31 | {value} 32 | {/if} 33 | {#elseif type === 'Array'} 34 | Array[{value.length}] 35 | {#else} 36 | {type} 37 | {/if} 38 | {/if} 39 | 49 | 50 |
51 | {#if opened && hasChildren} 52 |
53 | {#list Object.keys(value) as k} 54 | 55 | {/list} 56 |
57 | {/if} 58 |
59 | `, 60 | data: { 61 | opened: false 62 | }, 63 | computed: { 64 | type() { 65 | return type(this.data.value); 66 | }, 67 | hasChildren() { 68 | var data = this.data; 69 | return ((type(data.value) === 'Array') || (type(data.value) === 'Object')) && 70 | ((data.value.length || Object.keys(data.value).length)); 71 | } 72 | }, 73 | config: function() { 74 | var self = this; 75 | this.$parent.$on('checkClickOutside', function(v) { 76 | if (self.$refs && self.$refs.edit && !self.$refs.edit.contains(v)) { 77 | self.data.editing = false; 78 | self.$update(); 79 | } 80 | self.$emit('checkClickOutside', v); 81 | }); 82 | }, 83 | onEdit: function() { 84 | if (this.data.value === 'function') { 85 | return; 86 | } 87 | if (!isPrimitive(this.data.value)) { 88 | return; 89 | } 90 | this.data.editing = true; 91 | // focus and selectRange after UI updated 92 | setTimeout(() => { 93 | // select all when active 94 | var $edit = this.$refs.edit; 95 | $edit.focus(); 96 | if (type(this.data.value) === "String") { 97 | $edit.setSelectionRange(1, $edit.value.length - 1); 98 | } else { 99 | $edit.setSelectionRange(0, $edit.value.length); 100 | } 101 | }, 0); 102 | }, 103 | onBlur: function(e) { 104 | this.data.editing = false; 105 | this._sync(e); 106 | }, 107 | onEnter: function(e) { 108 | // Enter 109 | if (e.which === 13) { 110 | this.$refs.edit.blur(); 111 | } 112 | }, 113 | // when editing is finished 114 | _sync: function(e) { 115 | var tmp; 116 | try { 117 | tmp = JSON.parse(e.target.value); 118 | } catch (error) { 119 | const value = this.data.value; 120 | if (type(value)) { 121 | e.target.value = JSON.stringify(value); 122 | } else { 123 | e.target.value = value; 124 | } 125 | } 126 | 127 | // if not primitive or new value equals original one, return 128 | if (!isPrimitive(tmp) || tmp === this.data.value) { 129 | return; 130 | } 131 | 132 | var parent = this.$parent; 133 | while (parent) { 134 | if (isJsonTree(parent)) { 135 | parent.$emit('change', { 136 | path: this.data.path, 137 | value: tmp, 138 | oldValue: this.data.value 139 | }); 140 | break; 141 | } 142 | parent = parent.$parent; 143 | } 144 | 145 | function isJsonTree(context) { 146 | return typeof context.isJsonTree === 'function' && context.isJsonTree() === true; 147 | } 148 | 149 | this.data.value = tmp; 150 | this.$update(); 151 | }, 152 | isPrimitive: isPrimitive 153 | }); 154 | 155 | JsonTreeProp.component('JsonTreeProp', JsonTreeProp); 156 | 157 | export default JsonTreeProp; 158 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Search.css: -------------------------------------------------------------------------------- 1 | .searchView { 2 | display: flex; 3 | width: 100%; 4 | height: 40px; 5 | border-top: 1px solid #e3e3e3; 6 | box-sizing: border-box; 7 | } 8 | 9 | .searchView-md{ 10 | width:100%; 11 | } 12 | 13 | .searchView-md-input{ 14 | padding: 0 0 0 10px; 15 | } 16 | 17 | .searchView-md-label{ 18 | top:12px; 19 | padding-left: 10px; 20 | box-sizing: border-box; 21 | } 22 | 23 | .searchView-md-label:after{ 24 | bottom:0; 25 | } 26 | 27 | .searchView-input { 28 | display: block; 29 | width: 100%; 30 | height: 100%; 31 | line-height: 40px; 32 | font-size: 11px; 33 | border: none; 34 | box-sizing: border-box; 35 | outline: none; 36 | } 37 | 38 | .searchView-btns { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | position: absolute; 43 | right: 0; 44 | padding-right: 10px; 45 | } 46 | 47 | .searchView-text { 48 | font-size: 12px; 49 | vertical-align: middle; 50 | height: 40px; 51 | line-height: 40px; 52 | white-space: nowrap; 53 | margin-right: 5px; 54 | } 55 | 56 | .searchView-btn { 57 | display: block; 58 | height: 25px; 59 | width: 25px; 60 | cursor: pointer; 61 | } 62 | 63 | .searchView-btn:not(:last-child) { 64 | margin-right: 10px; 65 | } 66 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Search.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import {findElementByName} from '../utils'; 3 | import './Search.css'; 4 | 5 | const SearchView = Regular.extend({ 6 | template: ` 7 |
8 |
9 | 10 | 11 |
12 |
13 |
14 | {#if hasSearched && !resultList.length} 15 | Not found 16 | {#else} 17 | {#if resultList.length} 18 | {index + 1}/{resultList.length} found 19 | {/if} 20 | {/if} 21 |
22 | search 23 | prev 24 | next 25 |
26 |
27 | `, 28 | data: { 29 | value: "", 30 | resultList: [], 31 | index: 0, 32 | hasSearched: false 33 | }, 34 | onInput: function() { 35 | this.data.hasSearched = false; 36 | }, 37 | onEnter: function() { 38 | if (this.data.hasSearched) { 39 | this.next(1); 40 | } else { 41 | this.search(); 42 | } 43 | }, 44 | search: function() { 45 | if (!this.data.value) { 46 | return; 47 | } 48 | var reg = new RegExp(this.data.value); 49 | this.data.resultList = []; 50 | this.data.index = 0; 51 | findElementByName(this.$root.data.nodes, reg, this.data.resultList); 52 | this.data.hasSearched = true; 53 | if (this.data.resultList.length) { 54 | this.$root.focusNode(this.data.resultList[0]); 55 | } 56 | }, 57 | next: function(dir) { 58 | var data = this.data; 59 | if (data.resultList.length) { 60 | data.index += dir; 61 | if (data.index === data.resultList.length) data.index = 0; 62 | if (data.index === -1) data.index = data.resultList.length - 1; 63 | this.$root.focusNode(data.resultList[data.index]); 64 | } 65 | }, 66 | reset: function() { 67 | this.data.value = ""; 68 | this.data.resultList = []; 69 | this.data.index = 0; 70 | this.data.hasSearched = false; 71 | this.$update(); 72 | } 73 | }); 74 | 75 | export default SearchView; 76 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | display: inline-block; 3 | width: 50%; 4 | height: 100%; 5 | } 6 | 7 | .sidebar__header { 8 | font-size: 15px; 9 | line-height: 30px; 10 | display: flex; 11 | padding: 0 10px; 12 | } 13 | 14 | .sidebar__header-left { 15 | flex-grow: 1; 16 | } 17 | 18 | .sidebar__header-right { 19 | font-size: 13px; 20 | } 21 | 22 | .sidebar__content { 23 | height: -webkit-calc(100% - 61px); 24 | overflow-y: auto; 25 | font-size: 20px; 26 | } 27 | 28 | .sidebar__tools { 29 | padding: 0 10px; 30 | } 31 | 32 | .sidebar__inspect { 33 | display: inline-block; 34 | background-image: url(/static/media/inspect.svg); 35 | background-position: 0 center; 36 | background-size: 16px auto; 37 | background-repeat: no-repeat; 38 | padding-left: 18px; 39 | font-size: 11px; 40 | color: #333; 41 | cursor: pointer; 42 | margin: 10px 0; 43 | } 44 | 45 | .sidebar__definition { 46 | display: inline-block; 47 | background-image: url(/static/media/at-sign.svg); 48 | background-position: 0 center; 49 | background-size: 14px auto; 50 | background-repeat: no-repeat; 51 | padding-left: 16px; 52 | font-size: 11px; 53 | color: #333; 54 | cursor: pointer; 55 | margin: 10px 0; 56 | } 57 | 58 | .sidebar__inspect:hover { 59 | color: #000; 60 | } 61 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import SidebarPane from './SidebarPane'; 3 | import SimpleJsonTree from './SimpleJsonTree'; 4 | import JsonTree from './JsonTree'; 5 | import Tabs from './Tabs'; 6 | import log from '../../shared/log'; 7 | import './Sidebar.css'; 8 | 9 | const Sidebar = Regular.extend({ 10 | template: ` 11 | 65 | `, 66 | config() { 67 | // defaultValue of currentNode 68 | this.data.currentNode = { 69 | name: "", 70 | uuid: "", 71 | inspectable: false, 72 | computed: {}, 73 | data: {} 74 | }; 75 | // others for currentNode 76 | this.data.others = {}; 77 | this.data.tabSource = [ 78 | {text: "Data", key: "data"}, 79 | {text: 'Others', key: 'others'} 80 | ]; 81 | // defaults to `data` pane 82 | this.data.tabSelected = 'data'; 83 | this.data.lockHighlight = false; 84 | }, 85 | computed: { 86 | currentTabIndex: { 87 | get() { 88 | const source = this.data.tabSource; 89 | for (let i = 0; i < source.length; i++) { 90 | if (this.data.tabSelected === source[i].key) { 91 | return i; 92 | } 93 | } 94 | return 0; 95 | } 96 | } 97 | }, 98 | onTabChange(key) { 99 | this.data.tabSelected = key; 100 | this.$update(); 101 | log("Tab is Changed to", key); 102 | }, 103 | onDataChange({path, value}) { 104 | const uuid = this.data.currentNode.uuid; 105 | this.$emit('dataChange', {uuid, path, value}); 106 | }, 107 | onInspectNode(uuid) { 108 | this.$emit('inspectNode', uuid); 109 | }, 110 | onShowDefinition(uuid) { 111 | this.$emit('showDefinition', uuid); 112 | }, 113 | highLightNode(uuid, inspectable) { 114 | this.$emit('highLightNode', {uuid, inspectable}); 115 | } 116 | }); 117 | 118 | Sidebar.component('SidebarPane', SidebarPane); 119 | Sidebar.component('SimpleJsonTree', SimpleJsonTree); 120 | Sidebar.component('Tabs', Tabs); 121 | Sidebar.component('JsonTree', JsonTree); 122 | 123 | export default Sidebar; 124 | -------------------------------------------------------------------------------- /src/devtools-ui/components/SidebarPane.css: -------------------------------------------------------------------------------- 1 | .sidebar-pane-header { 2 | display: flex; 3 | align-items: center; 4 | background-color: #eee; 5 | height: 20px; 6 | padding: 3px 10px; 7 | border-top: 1px solid #dadada; 8 | border-bottom: 1px solid #dadada; 9 | white-space: nowrap; 10 | overflow: hidden; 11 | color: #222; 12 | font-size: 12px; 13 | font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; 14 | } 15 | 16 | .sidebar-pane-content { 17 | font-size: 13px; 18 | } 19 | -------------------------------------------------------------------------------- /src/devtools-ui/components/SidebarPane.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import './SidebarPane.css'; 3 | 4 | const SidebarPane = Regular.extend({ 5 | template: ` 6 | 14 | ` 15 | }); 16 | 17 | export default SidebarPane; 18 | -------------------------------------------------------------------------------- /src/devtools-ui/components/SimpleJsonTree.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | 3 | const SimpleJsonTree = Regular.extend({ 4 | template: ` 5 |
6 | {#list Object.keys(source) as k} 7 |
8 |
9 | {k}{source[k] ===''? '': ':'} 10 | {source[k]} 11 |
12 |
13 | {/list} 14 |
15 | `, 16 | config() { 17 | this.data.source = this.data.source || {}; 18 | } 19 | }); 20 | 21 | export default SimpleJsonTree; 22 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Tabs.css: -------------------------------------------------------------------------------- 1 | .tabs { 2 | font-family: Consolas, Lucida Console, monospace; 3 | } 4 | 5 | .tabs-header { 6 | position: relative; 7 | overflow: hidden; 8 | background-color: #f3f3f3; 9 | border-bottom: 1px solid #ccc; 10 | font-size: 12px; 11 | user-select: none; 12 | -webkit-user-select: none; 13 | } 14 | 15 | .tab-indicator{ 16 | height: 1px; 17 | width: 60px; 18 | background-color: #3E82F7; 19 | position: absolute; 20 | left: 0; 21 | bottom: 0; 22 | transform: translateX(0); 23 | transition: transform 150ms cubic-bezier(0, 0, 0.2, 1); 24 | } 25 | 26 | .tabs-header-item { 27 | box-sizing: border-box; 28 | float: left; 29 | padding: 0 6px; 30 | width: 60px; 31 | height: 25px; 32 | border-bottom: none; 33 | line-height: 15px; 34 | white-space: nowrap; 35 | cursor: default; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | color: #5a5a5a; 40 | font-family: '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; 41 | outline: none; 42 | } 43 | 44 | .tabs-header-item:hover { 45 | color: #333; 46 | background-color: #e5e5e5; 47 | } 48 | -------------------------------------------------------------------------------- /src/devtools-ui/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import Regular from 'regularjs'; 2 | import './Tabs.css'; 3 | 4 | const Tabs = Regular.extend({ 5 | template: ` 6 |
7 |
8 |
9 | {#list source as s} 10 | {#if s.key === "data" } 11 |
12 | the data displayed here is passed from inspected page by IPC, which means only JSON-ifiable object is allowed. If you want to inspect DOM or function in data, please use $r in console 13 |
14 |
15 | { s.text } 16 |
17 | {#else} 18 |
19 | { s.text } 20 |
21 | {/if} 22 | {/list} 23 |
24 |
25 |
26 |
27 | `, 28 | onTabClick: function(key) { 29 | if (this.data.selected === key) { 30 | return; 31 | } 32 | this.$emit('change', key); 33 | } 34 | }); 35 | 36 | export default Tabs; 37 | -------------------------------------------------------------------------------- /src/devtools-ui/events/enter.js: -------------------------------------------------------------------------------- 1 | export default function(Component, Regular) { 2 | const dom = Regular.dom; 3 | 4 | Component.event('enter', function(elem, fire) { 5 | function update(ev) { 6 | if (ev.which === 13) { // ENTER key 7 | ev.preventDefault(); 8 | fire(ev); // if key is enter , we fire the event; 9 | } 10 | } 11 | dom.on(elem, "keypress", update); 12 | return function destroy() { // return a destroy function 13 | dom.off(elem, "keypress", update); 14 | }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/devtools-ui/events/index.js: -------------------------------------------------------------------------------- 1 | import enter from './enter'; 2 | import input from './input'; 3 | import mouseenter from './mouseenter'; 4 | import mouseleave from './mouseleave'; 5 | 6 | export { 7 | enter, 8 | input, 9 | mouseenter, 10 | mouseleave 11 | }; 12 | -------------------------------------------------------------------------------- /src/devtools-ui/events/input.js: -------------------------------------------------------------------------------- 1 | export default function(Component, Regular) { 2 | const dom = Regular.dom; 3 | 4 | Component.event('input', function(elem, fire) { 5 | function update(ev) { 6 | fire(ev); // if key is enter , we fire the event; 7 | } 8 | dom.on(elem, "input", update); 9 | return function destroy() { // return a destroy function 10 | dom.off(elem, "input", update); 11 | }; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools-ui/events/mouseenter.js: -------------------------------------------------------------------------------- 1 | export default function(Component, Regular) { 2 | const dom = Regular.dom; 3 | 4 | Component.event('mouseenter', function(elem, fire) { 5 | function update(ev) { 6 | fire(ev); 7 | } 8 | dom.on(elem, "mouseenter", update); 9 | return function destroy() { // return a destroy function 10 | dom.off(elem, "mouseenter", update); 11 | }; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools-ui/events/mouseleave.js: -------------------------------------------------------------------------------- 1 | export default function(Component, Regular) { 2 | const dom = Regular.dom; 3 | 4 | Regular.event('mouseleave', function(elem, fire) { 5 | function update(ev) { 6 | fire(ev); 7 | } 8 | dom.on(elem, "mouseleave", update); 9 | return function destroy() { // return a destroy function 10 | dom.off(elem, "mouseleave", update); 11 | }; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools-ui/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regularjs/regular-devtools/3782e24f1d2c79de527356f984e0a6b830edc5ea/src/devtools-ui/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/devtools-ui/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Roboto"; 3 | src:url("./fonts/Roboto-Regular.ttf"); 4 | } 5 | 6 | html { 7 | height: 100%; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | height: 100%; 13 | font-family: Consolas, Lucida Console, Menlo, monospace, '.SFNSDisplay-Regular', 'Helvetica Neue', 'Lucida Grande', sans-serif; 14 | } 15 | 16 | #app { 17 | box-sizing: border-box; 18 | padding-top: 60px; 19 | width: 100%; 20 | height: 100%; 21 | overflow: hidden; 22 | font-size: 0; 23 | } 24 | 25 | .roboto { 26 | font-family: "Roboto"; 27 | } 28 | 29 | /* main start */ 30 | 31 | .devtoolsMain { 32 | display: flex; 33 | height: 100%; 34 | } 35 | 36 | /* main end */ 37 | 38 | /* util start */ 39 | 40 | .purplee { 41 | font-size: 18px; 42 | color: rgb(136, 18, 128); 43 | } 44 | 45 | .purple { 46 | color: rgb(136, 18, 128); 47 | } 48 | 49 | .gray { 50 | color: gray; 51 | } 52 | 53 | .blue { 54 | color: blue; 55 | } 56 | 57 | .bold { 58 | font-weight: bold; 59 | } 60 | 61 | /* util end */ 62 | -------------------------------------------------------------------------------- /src/devtools-ui/index.js: -------------------------------------------------------------------------------- 1 | // the real devtools script 2 | // the UI layer of devtools 3 | import Regular from "regularjs"; 4 | import 'material-design-lite'; 5 | import CircularJSON from "../shared/circular-json"; 6 | import log from '../shared/log'; 7 | import {enter, input, mouseenter, mouseleave} from './events'; 8 | import { 9 | printInConsole, 10 | findElementByUUID, 11 | inspectNodeByUUID, 12 | showDefinitionByUUID, 13 | updateInstanceByUUIDAndPath, 14 | evalHighLightNode, 15 | getData, 16 | getOthersData, 17 | makeElementTree, 18 | patch 19 | } from './utils'; 20 | import agent from './agent'; 21 | import Devtools from './components/Devtools'; 22 | 23 | import 'material-design-lite/dist/material.min.css'; 24 | import './index.css'; 25 | 26 | // register custom events 27 | Regular.use(enter); 28 | Regular.use(input); 29 | Regular.use(mouseenter); 30 | Regular.use(mouseleave); 31 | 32 | // directive for MDL component registeration 33 | Regular.directive('r-md', function(elem, value) { 34 | componentHandler.upgradeElement(elem); 35 | }); 36 | 37 | // variables 38 | let ready = false; 39 | 40 | // devtools 41 | const devtools = new Devtools({ 42 | data: { 43 | nodes: [], 44 | lastSelected: null 45 | } 46 | }).$inject("#app"); 47 | // left element view 48 | const elementView = devtools.$refs.elementView; 49 | // right sidebar view 50 | const sidebarView = devtools.$refs.sidebarView; 51 | // searchView 52 | const searchView = elementView.$refs.searchView; 53 | 54 | function displayWarning() { 55 | if (elementView.data.loading) { 56 | elementView.data.loading = false; 57 | elementView.$update(); 58 | } 59 | } 60 | 61 | // listen for custom events 62 | devtools 63 | .$on("initNodes", function(nodesStr) { 64 | let nodes = CircularJSON.parse(nodesStr); 65 | log("initNodes", nodes); 66 | this.data.nodes = nodes; 67 | 68 | elementView.data.loading = false; 69 | elementView.data.nodes = makeElementTree(nodes, []); 70 | elementView.$update(); 71 | 72 | // init sidebar 73 | sidebarView.data.currentNode.name = nodes[0].name; 74 | sidebarView.data.currentNode.uuid = nodes[0].uuid; 75 | sidebarView.data.currentNode.inspectable = nodes[0].inspectable; 76 | sidebarView.$emit('updateData', nodes[0].uuid); 77 | ready = true; 78 | }) 79 | .$on("clickElement", function(uuid) { 80 | if (uuid !== sidebarView.data.currentNode.uuid) { 81 | let currentNode = findElementByUUID(this.data.nodes, uuid); 82 | sidebarView.data.currentNode.name = currentNode.name; 83 | sidebarView.data.currentNode.inspectable = currentNode.inspectable; 84 | sidebarView.data.currentNode.uuid = uuid; 85 | sidebarView.$emit('updateData', uuid); 86 | sidebarView.$emit('updateOthersData', uuid); 87 | } 88 | printInConsole(uuid); 89 | }) 90 | .$on("stateViewReRender", function(nodesStr) { 91 | log("On stateViewRender."); 92 | sidebarView.$emit('updateData', sidebarView.data.currentNode.uuid); 93 | }) 94 | .$on("elementViewReRender", function(nodesStr) { 95 | log("On elementViewRerender."); 96 | let nodes = CircularJSON.parse(nodesStr); 97 | this.data.nodes = nodes; 98 | // need refactor 99 | /* eslint-disable no-unused-vars */ 100 | var oldArr = elementView.data.nodes; 101 | var newArr = makeElementTree(nodes, []); 102 | elementView.data.nodes = patch(oldArr, newArr); 103 | /* eslint-enable no-unused-vars */ 104 | elementView.$update(); 105 | }) 106 | .$on("currentNodeChange", function(uuid) { 107 | log("On currentNodeChange."); 108 | if (sidebarView.data.currentNode.uuid !== uuid) { 109 | devtools.focusNode(uuid); 110 | } 111 | }) 112 | .$on("reload", function(event) { 113 | ready = false; 114 | log("On reload."); 115 | searchView.reset(); 116 | // wait for the page to fully intialize 117 | setTimeout(function() { 118 | agent.injectContentScript(event.tabId); 119 | setTimeout(displayWarning, 4000); 120 | }, 2000); 121 | }) 122 | .$on("refresh", () => { 123 | chrome.devtools.inspectedWindow.reload(); 124 | }) 125 | .$on("openNewTab", function(url) { 126 | agent.openInNewTab(url); 127 | }); 128 | 129 | sidebarView 130 | .$on("dataChange", ({uuid, path, value}) => { 131 | updateInstanceByUUIDAndPath({uuid, path, value}); 132 | }) 133 | .$on("inspectNode", uuid => { 134 | inspectNodeByUUID(uuid); 135 | }) 136 | .$on("showDefinition", uuid => { 137 | showDefinitionByUUID(uuid); 138 | }) 139 | .$on("highLightNode", ({uuid, inspectable}) => { 140 | if (!sidebarView.data.lockHighlight) { 141 | evalHighLightNode(uuid, inspectable); 142 | } 143 | }) 144 | .$on("updateData", uuid => { 145 | getData(uuid).then(data => { 146 | let currentNode = CircularJSON.parse(data); 147 | sidebarView.data.currentNode.data = currentNode.data; 148 | sidebarView.data.currentNode.computed = currentNode.computed; 149 | sidebarView.$update(); 150 | }); 151 | }) 152 | .$on("lockHighLight", flag => { 153 | sidebarView.data.lockHighlight = flag; 154 | }) 155 | .$on("updateOthersData", uuid => { 156 | getOthersData(uuid).then(data => { 157 | sidebarView.data.others = data; 158 | sidebarView.$update(); 159 | }); 160 | }); 161 | 162 | agent.on('dataUpdate', () => devtools.$emit("stateViewReRender")); 163 | agent.on('reRender', nodes => devtools.$emit("elementViewReRender", nodes)); 164 | agent.on('initNodes', nodes => devtools.$emit("initNodes", nodes)); 165 | agent.on('currNodeChange', uuid => { 166 | if (ready) { 167 | devtools.$emit("currentNodeChange", uuid); 168 | } 169 | }); 170 | agent.on('pageReload', tabId => { 171 | elementView.data.loading = true; 172 | elementView.$update(); 173 | devtools.$emit("reload", {tabId}); 174 | }); 175 | 176 | agent.injectContentScript(); 177 | 178 | // waiting 4000ms, if still loading, remove loading and show warning 179 | setTimeout(displayWarning, 4000); 180 | -------------------------------------------------------------------------------- /src/devtools-ui/media/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/devtools-ui/media/at-sign.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/devtools-ui/media/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/devtools-ui/media/inspect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/prev.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regularjs/regular-devtools/3782e24f1d2c79de527356f984e0a6b830edc5ea/src/devtools-ui/media/regular.png -------------------------------------------------------------------------------- /src/devtools-ui/media/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/devtools-ui/media/target.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/devtools-ui/media/target_active.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/devtools-ui/port.js: -------------------------------------------------------------------------------- 1 | // Create a current inspected page unique connection to the background page, by its tabId 2 | export default chrome.runtime.connect({ 3 | name: "devToBackCon_" + chrome.devtools.inspectedWindow.tabId 4 | }); 5 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/findElement.js: -------------------------------------------------------------------------------- 1 | export function findElementByUUID(nodes, uuid) { 2 | for (var i = 0; i < nodes.length; i++) { 3 | if (nodes[i].uuid === uuid) { 4 | return nodes[i]; 5 | } 6 | if (nodes[i].childNodes.length) { 7 | var result = findElementByUUID(nodes[i].childNodes, uuid); 8 | if (result) { 9 | return result; 10 | } 11 | } 12 | } 13 | } 14 | 15 | export function findElementByUUIDNonRecursive(nodes, uuid) { 16 | for (var i = 0; i < nodes.length; i++) { 17 | if (nodes[i].uuid === uuid) { 18 | return nodes[i]; 19 | } 20 | } 21 | } 22 | 23 | export function findElementByName(nodes, reg, container) { 24 | for (var i = 0; i < nodes.length; i++) { 25 | if (reg.test(nodes[i].name)) { 26 | container.push(nodes[i].uuid); 27 | } 28 | if (nodes[i].childNodes.length) { 29 | findElementByName(nodes[i].childNodes, reg, container); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/getData.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | function getData(uuid) { 4 | var node = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.ins.filter(function(n) { 5 | return n.uuid === uuid; 6 | })[0]; 7 | return window.devtoolsModel.stringify({ 8 | name: node.name || "[anonymous]", 9 | uuid: uuid, 10 | data: node.data, 11 | computed: window.devtoolsModel.fetchComputedProps(node) 12 | }); 13 | } 14 | 15 | const getDataStr = getData.toString(); 16 | 17 | export default function(uuid) { 18 | return new Promise((resolve, reject) => { 19 | chrome.devtools.inspectedWindow.eval( 20 | `(${getDataStr})(${JSON.stringify(uuid)})`, 21 | function(result, isException) { 22 | if (isException) { 23 | log("Get Data Error: ", isException); 24 | reject(isException); 25 | return; 26 | } 27 | resolve(result); 28 | } 29 | ); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/getOthersData.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | function getOthersData(uuid) { 4 | var othersNameArr = ['_directives', '_filters', '_animations']; 5 | var node = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.ins.filter(function(n) { 6 | return n.uuid === uuid; 7 | })[0]; 8 | if (node) { 9 | var constructor = node.constructor; 10 | var result = {}; 11 | for (var prop in constructor) { 12 | if (constructor.hasOwnProperty(prop) && othersNameArr.indexOf(prop) !== -1) { 13 | var tempObj = {}; 14 | var curObj = constructor[prop]; 15 | var curUI = constructor.prototype; 16 | while (curObj && curUI) { 17 | var tempArr = []; 18 | for (var key in curObj) { 19 | if (curObj.hasOwnProperty(key)) { 20 | tempArr.push(key); 21 | } 22 | } 23 | /* eslint-disable no-proto, no-loop-func */ 24 | 25 | tempArr.sort(); // same level sort 26 | tempArr.forEach(function(value) { 27 | if (!tempObj[value]) { // same command big level not show 28 | if (curUI.constructor._addProtoInheritCache) { 29 | tempObj[value] = "[regular]"; 30 | } else if (curUI.reset && !curUI.__proto__.reset && curUI.__proto__.constructor._addProtoInheritCache) { 31 | var funStr = curUI.reset.toString(); 32 | if (funStr.indexOf("this.data = {}") !== -1 && funStr.indexOf("this.config()") !== -1) { 33 | tempObj[value] = "[regular-ui]"; // very low possible be developer's Component 34 | } else { 35 | tempObj[value] = curUI.name === undefined ? '' : curUI.name; 36 | } 37 | } else { 38 | tempObj[value] = curUI.name === undefined ? '' : curUI.name; // same level same color 39 | } 40 | } 41 | }); 42 | curObj = curObj.__proto__; 43 | curUI = curUI.__proto__; 44 | 45 | /* eslint-enable no-proto, no-loop-func*/ 46 | } 47 | 48 | result[prop] = tempObj; 49 | } 50 | } 51 | return result; 52 | } 53 | } 54 | 55 | const getOthersDataStr = getOthersData.toString(); 56 | 57 | export default function(uuid) { 58 | return new Promise((resolve, reject) => { 59 | chrome.devtools.inspectedWindow.eval( 60 | `(${getOthersDataStr})(${JSON.stringify(uuid)})`, 61 | function(result, isException) { 62 | if (isException) { 63 | log("Get Others Data Error: ", isException); 64 | reject(isException); 65 | return; 66 | } 67 | resolve(result); 68 | } 69 | ); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/highLighter.js: -------------------------------------------------------------------------------- 1 | let maskNode; 2 | let labelNode; 3 | 4 | const setLabelPositon = function(node, rect) { 5 | var w = Math.max(document.documentElement.clientWidth, 6 | window.innerWidth || 0); 7 | var h = Math.max(document.documentElement.clientHeight, 8 | window.innerHeight || 0); 9 | 10 | // detect if rect resides in the viewport 11 | if (rect.top >= 0 && rect.top <= h && rect.left >= 0 && rect.left <= w) { 12 | // set vertical 13 | if (rect.top > 34) { 14 | node.style.top = window.scrollY + rect.top - 29 + "px"; 15 | } else if ((h - rect.top - rect.height) > 34) { 16 | node.style.top = window.scrollY + rect.top + rect.height + 5 + "px"; 17 | } else { 18 | node.style.top = window.scrollY + rect.top + "px"; 19 | } 20 | 21 | // set horizontal 22 | if (rect.left > 120) { 23 | node.style.left = rect.left + "px"; 24 | } else if ((h - rect.left - rect.width) > 120) { 25 | node.style.left = rect.left + "px"; 26 | } else { 27 | node.style.left = rect.left + "px"; 28 | } 29 | } else { 30 | if (rect.top < 0) { 31 | node.style.top = window.scrollY + "px"; 32 | } else if (rect.top > h) { 33 | node.style.top = window.scrollY + h - 24 + "px"; 34 | } 35 | 36 | if (rect.left < 0) { 37 | node.style.left = window.scrollX + "px"; 38 | } else if (rect.left > w) { 39 | node.style.left = window.scrollX + w - 100 + "px"; 40 | } 41 | 42 | if (!node.style.left) node.style.left = rect.left + "px"; 43 | if (!node.style.top) node.style.top = rect.top + "px"; 44 | } 45 | }; 46 | 47 | export function clearMask() { 48 | if (maskNode) { 49 | document.querySelector("body").removeChild(maskNode); 50 | document.querySelector("body").removeChild(labelNode); 51 | maskNode = null; 52 | labelNode = null; 53 | } 54 | } 55 | 56 | export function highLightNode(domNode, name) { 57 | var rect = domNode.getBoundingClientRect(); 58 | clearMask(); 59 | 60 | // draw mask 61 | maskNode = document.createElement("div"); 62 | maskNode.style.position = "absolute"; 63 | maskNode.style.left = rect.left + "px"; 64 | maskNode.style.top = rect.top + window.scrollY + "px"; 65 | maskNode.style.width = rect.width + window.scrollX + "px"; 66 | maskNode.style.height = rect.height + "px"; 67 | maskNode.style.backgroundColor = "rgba(145, 183, 228, 0.6)"; 68 | maskNode.style.zIndex = 999999; 69 | maskNode.style.pointerEvents = "none"; 70 | document.querySelector("body").appendChild(maskNode); 71 | 72 | // draw label 73 | var demensionStr = "\n" + rect.width.toFixed(0) + "×" + rect.height.toFixed(0); 74 | labelNode = document.createElement("div"); 75 | labelNode.textContent = name + demensionStr; 76 | labelNode.style.backgroundColor = "#272931"; 77 | labelNode.style.color = "#fff"; 78 | labelNode.style.position = "absolute"; 79 | labelNode.style.padding = "0 10px"; 80 | labelNode.style.height = "24px"; 81 | labelNode.style.lineHeight = "24px"; 82 | labelNode.style.fontSize = "12px"; 83 | labelNode.style.borderRadius = "2px"; 84 | labelNode.style.zIndex = 999999; 85 | setLabelPositon(labelNode, rect); 86 | document.querySelector("body").appendChild(labelNode); 87 | } 88 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/highlightNode.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | export function evalHighLightNode(uuid, inspectable) { 4 | var evalStr = inspectable ? "devtoolsModel.highLighter('" + uuid + "')" : "devtoolsModel.highLighter()"; 5 | chrome.devtools.inspectedWindow.eval( 6 | evalStr, 7 | function(result, isException) { 8 | if (isException) { 9 | log("HightLight Error: ", isException); 10 | } 11 | } 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/index.js: -------------------------------------------------------------------------------- 1 | import isPrimitive from './isPrimitive'; 2 | import type from './type.js'; 3 | import searchPath from './searchPath.js'; 4 | import printInConsole from './printInConsole.js'; 5 | import {findElementByUUID, findElementByName, findElementByUUIDNonRecursive} from './findElement.js'; 6 | import {inspectNodeByUUID} from './inspectNode'; 7 | import showDefinitionByUUID from './showDefinitionByUUID'; 8 | import {evalHighLightNode} from './highlightNode'; 9 | import {clearMask, highLightNode} from './highLighter'; 10 | import {updateInstanceByUUIDAndPath} from './updateInstance'; 11 | import {enter, exit} from './inspectComponent'; 12 | import getData from './getData'; 13 | import getOthersData from './getOthersData'; 14 | import makeElementTree from './makeElementTree'; 15 | import patch from './patch'; 16 | 17 | export { 18 | isPrimitive, 19 | type, 20 | searchPath, 21 | printInConsole, 22 | findElementByUUID, 23 | findElementByName, 24 | findElementByUUIDNonRecursive, 25 | inspectNodeByUUID, 26 | showDefinitionByUUID, 27 | updateInstanceByUUIDAndPath, 28 | evalHighLightNode, 29 | highLightNode, 30 | clearMask, 31 | getData, 32 | getOthersData, 33 | makeElementTree, 34 | patch, 35 | enter, 36 | exit 37 | }; 38 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/inspectComponent.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | function inspectComponent(type) { 4 | if (type === "enter") { 5 | window.devtoolsModel.enterInspectMode(); 6 | } else { 7 | window.devtoolsModel.exitInspectMode(); 8 | } 9 | } 10 | 11 | const funcStr = inspectComponent.toString(); 12 | 13 | export function enter() { 14 | return new Promise((resolve, reject) => { 15 | chrome.devtools.inspectedWindow.eval( 16 | `(${funcStr})('enter')`, 17 | function(result, isException) { 18 | if (isException) { 19 | log("Get Data Error: ", isException); 20 | reject(isException); 21 | return; 22 | } 23 | resolve(result); 24 | } 25 | ); 26 | }); 27 | } 28 | 29 | export function exit() { 30 | return new Promise((resolve, reject) => { 31 | chrome.devtools.inspectedWindow.eval( 32 | `(${funcStr})('exit')`, 33 | function(result, isException) { 34 | if (isException) { 35 | log("Get Data Error: ", isException); 36 | reject(isException); 37 | return; 38 | } 39 | resolve(result); 40 | } 41 | ); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/inspectNode.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | export function inspectNodeByUUID(uuid) { 4 | chrome.devtools.inspectedWindow.eval( 5 | ` 6 | var node = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.ins.filter(function(n) { 7 | return n.uuid === '${uuid}' 8 | })[0]; 9 | if (node) { 10 | inspect(node.group && node.group.children && node.group.children[0] && node.group.children[0].node && node.group.children[0].node() || node.parentNode); 11 | } 12 | `, 13 | function(result, isException) { 14 | if (isException) { 15 | log("Inspect Error: ", isException); 16 | } 17 | } 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/isPrimitive.js: -------------------------------------------------------------------------------- 1 | export default function isPrimitive(arg) { 2 | var type = typeof arg; 3 | return arg === null || (type !== "object" && type !== "function"); 4 | } 5 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/makeElementTree.js: -------------------------------------------------------------------------------- 1 | export default function makeElementTree(nodes, container) { 2 | for (var i = 0; i < nodes.length; i++) { 3 | var node = { 4 | ...nodes[i], 5 | childNodes: [] 6 | }; 7 | container.push(node); 8 | if (nodes[i].childNodes.length) { 9 | makeElementTree(nodes[i].childNodes, node.childNodes); 10 | } 11 | } 12 | return container; 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/patch.js: -------------------------------------------------------------------------------- 1 | import {findElementByUUIDNonRecursive} from "./findElement"; 2 | 3 | // reuse old nodes 4 | export default function patch(oldArr, newArr) { 5 | const container = []; 6 | for (var i = 0; i < newArr.length; i++) { 7 | var newNode = newArr[i]; 8 | var oldNode = findElementByUUIDNonRecursive(oldArr, newArr[i].uuid); 9 | if (oldNode) { 10 | if (JSON.stringify(oldNode) !== JSON.stringify(newNode)) { 11 | oldNode.name = newNode.name; 12 | oldNode.isIncluded = newNode.isIncluded; 13 | oldNode.childNodes = patch(oldNode.childNodes, newNode.childNodes, []); 14 | } 15 | container.push(oldNode); 16 | } else { 17 | container.push(newNode); 18 | } 19 | } 20 | return container; 21 | } 22 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/printInConsole.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | export default function(uuid) { 4 | chrome.devtools.inspectedWindow.eval( 5 | `devtoolsModel.print(${JSON.stringify(uuid)})`, 6 | function(result, isException) { 7 | if (isException) { 8 | log("Print Error: ", isException); 9 | } 10 | } 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/searchPath.js: -------------------------------------------------------------------------------- 1 | const searchPath = function(nodes, uuid, path) { 2 | for (var i = 0; i < nodes.length; i++) { 3 | if (nodes[i].data.node.uuid === uuid) { 4 | path.push(nodes[i]); 5 | return true; 6 | } else if (nodes[i]._children.length > 0) { 7 | if (searchPath(nodes[i]._children, uuid, path)) { 8 | path.push(nodes[i]); 9 | return true; 10 | } 11 | } 12 | } 13 | return false; 14 | }; 15 | 16 | export default function searchPathWarpper(nodes, uuid, path) { 17 | for (var i = 0; i < nodes.length; i++) { 18 | if (nodes[i].data.node && nodes[i].data.node.uuid === uuid) { 19 | path.push(nodes[i]); 20 | return path; 21 | } else if (searchPath(nodes[i]._children, uuid, path)) { 22 | path.push(nodes[i]); 23 | return path; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/showDefinitionByUUID.js: -------------------------------------------------------------------------------- 1 | import log from '../../shared/log'; 2 | 3 | export default function showDefinitionByUUID(uuid) { 4 | chrome.devtools.inspectedWindow.eval( 5 | ` 6 | var node = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.ins.filter(function(n) { 7 | return n.uuid === '${uuid}' 8 | })[0]; 9 | if (node) { 10 | var proto = node.__proto__; 11 | var hasConfig = proto.hasOwnProperty('config') && typeof proto.config === 'function'; 12 | var hasInit = proto.hasOwnProperty('init') && typeof proto.init === 'function'; 13 | 14 | var found = false 15 | if (hasConfig) { 16 | inspect(proto.config); 17 | } else if (hasInit) { 18 | inspect(proto.init); 19 | } else { 20 | for(var i in proto) { 21 | if (i !== 'constructor' && proto.hasOwnProperty(i) && typeof proto[i] === 'function') { 22 | inspect(proto[i]); 23 | found = true; 24 | break; 25 | } 26 | } 27 | 28 | if(!found){ 29 | inspect(proto.constructor); 30 | } 31 | } 32 | } 33 | `, 34 | function(result, isException) { 35 | if (isException) { 36 | log("Show definition Error: ", isException); 37 | } 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/type.js: -------------------------------------------------------------------------------- 1 | export default function type(obj) { 2 | return Object.prototype.toString.call(obj).slice(8, -1); 3 | } 4 | -------------------------------------------------------------------------------- /src/devtools-ui/utils/updateInstance.js: -------------------------------------------------------------------------------- 1 | export function updateInstanceByUUIDAndPath({uuid, path, value}) { 2 | // send message to page, update instance by uuid and path 3 | var fn = function(uuid, path, value) { 4 | window.postMessage({ 5 | type: 'FROM_CONTENT_SCRIPT', 6 | action: 'UPDATE_INSTANCE', 7 | payload: { 8 | uuid: uuid, 9 | path: path, 10 | value: value 11 | } 12 | }, '*'); 13 | }; 14 | chrome.devtools.inspectedWindow.eval( 15 | '(' + fn + ')(' + JSON.stringify(uuid) + ',' + JSON.stringify(path) + ',' + JSON.stringify(value) + ')', { 16 | useContentScriptContext: true 17 | }, 18 | function() {} 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/electron/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regularjs/regular-devtools/3782e24f1d2c79de527356f984e0a6b830edc5ea/src/electron/.gitkeep -------------------------------------------------------------------------------- /src/extension/background.js: -------------------------------------------------------------------------------- 1 | // background.js, runs presistently 2 | // inject content script for devtools page 3 | // pass message from content script to devtools page 4 | var prefix = "[Regular Devtools] "; 5 | var injectToBackConnection; 6 | 7 | // for every page's different injectToBackConnection to its devToBackConnection according to tab.id 8 | var devToBackConMap = {}; 9 | 10 | var injectListener = function(message, sender, sendResponse) { 11 | if (message.file) { 12 | // Inject a content script into the identified tab 13 | chrome.tabs.executeScript(message.tabId, { 14 | file: message.file 15 | }); 16 | console.log(prefix + "Content script injected."); 17 | } 18 | }; 19 | 20 | var openNewTabListener = function(message, sender, sendResponse) { 21 | if (message.url) { 22 | chrome.tabs.create({url: message.url}, () => {}); 23 | console.log(prefix + "Open new tab: " + message.url); 24 | } 25 | }; 26 | 27 | // cannot put in onConnect.addListener() for everyConnect add one callbackFun 28 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo) { 29 | if (changeInfo.status === "complete") { 30 | console.log(prefix + "Backend send pageReload event"); 31 | 32 | devToBackConMap[tabId].postMessage({ 33 | type: "pageReload", 34 | tabId: tabId 35 | }); 36 | } 37 | }); 38 | 39 | chrome.runtime.onConnect.addListener(function(connection) { 40 | // add the listener 41 | if (connection.name.indexOf("devToBackCon") !== -1) { 42 | var tabId = connection.name.split('_')[1]; 43 | console.log("dev2Back get connect with tabId: " + tabId); 44 | devToBackConMap[tabId] = connection; 45 | devToBackConMap[tabId].onMessage.addListener(injectListener); 46 | devToBackConMap[tabId].onMessage.addListener(openNewTabListener); 47 | return; 48 | } 49 | 50 | // Receive message from content script and relay to its devTools page according sender tab.id 51 | if (connection.name === "injectToBackCon") { 52 | injectToBackConnection = connection; 53 | console.log(prefix + "injectToBack Connection established.", connection); 54 | injectToBackConnection.onMessage.addListener(function(request, sender, sendResponse) { 55 | console.log(prefix + "Backend received message:" + request.type); 56 | console.log("inject2Back sent info with tabId: " + sender.sender.tab.id); 57 | devToBackConMap[sender.sender.tab.id].postMessage(request); 58 | return true; 59 | }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/extension/content.js: -------------------------------------------------------------------------------- 1 | // this is the content script runs when the panel is activated. 2 | // this script serves as the brigde between app page script(inject and hook) and the backend script 3 | 4 | var port = chrome.runtime.connect({ 5 | name: "injectToBackCon" 6 | }); 7 | 8 | function injectScript(file, node) { 9 | return new Promise(function(resolve, reject) { 10 | var th = document.getElementsByTagName(node)[0]; 11 | var s = document.createElement('script'); 12 | s.setAttribute('type', 'text/javascript'); 13 | s.setAttribute('src', file); 14 | th.appendChild(s); 15 | s.onload = function() { 16 | resolve(); 17 | }; 18 | }); 19 | } 20 | 21 | window.addEventListener("message", function(event) { 22 | // We only accept messages from ourselves 23 | if (event.source !== window) 24 | return; 25 | 26 | if (event.data.type && (event.data.type === "FROM_PAGE")) { 27 | port.postMessage(event.data.data); 28 | } 29 | }, false); 30 | 31 | injectScript(chrome.extension.getURL('/inject.bundle.js'), 'body'); 32 | -------------------------------------------------------------------------------- /src/extension/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Regular Developer Tools 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/extension/devtools.js: -------------------------------------------------------------------------------- 1 | // create new panel for devtools 2 | chrome.devtools.panels.create( 3 | "Regular", 4 | "/regular.png", 5 | "/index.html", 6 | function(panel) { 7 | panel.onShown.addListener(function(extPanelWindow) { 8 | chrome.devtools.inspectedWindow.eval( 9 | "window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.contain($0)", 10 | function(result, isException) { 11 | if (!isException && result) { 12 | extPanelWindow.postMessage({ 13 | type: "currNodeChange", 14 | uuid: result 15 | }, "*"); 16 | } 17 | } 18 | ); 19 | }); 20 | } 21 | ); 22 | -------------------------------------------------------------------------------- /src/extension/hook.js: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/vue-devtools/blob/master/src/backend/hook.js 2 | // this script is injected into every page. 3 | 4 | /** 5 | * Install the hook on window, which is an event emitter. 6 | * Note because Chrome content scripts cannot directly modify the window object, 7 | * we are evaling this function by inserting a script tag. That's why we have 8 | * to inline the whole event emitter implementation here. 9 | * 10 | * @param {Window} window 11 | */ 12 | 13 | function installHook(window) { 14 | var listeners = {}; 15 | 16 | var hook = { 17 | ins: [], 18 | 19 | location: { 20 | finalUUID: "", 21 | level: 0 22 | }, 23 | 24 | on: function(event, fn) { 25 | event = '$' + event; 26 | (listeners[event] || (listeners[event] = [])).push(fn); 27 | }, 28 | 29 | once: function(event, fn) { 30 | event = '$' + event; 31 | 32 | function on() { 33 | this.off(event, on); 34 | fn.apply(this, arguments); 35 | } 36 | (listeners[event] || (listeners[event] = [])).push(on); 37 | }, 38 | 39 | off: function(event, fn) { 40 | event = '$' + event; 41 | if (!arguments.length) { 42 | listeners = {}; 43 | } else { 44 | const cbs = listeners[event]; 45 | if (cbs) { 46 | if (!fn) { 47 | listeners[event] = null; 48 | } else { 49 | for (let i = 0, l = cbs.length; i < l; i++) { 50 | const cb = cbs[i]; 51 | if (cb === fn || cb.fn === fn) { 52 | cbs.splice(i, 1); 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | 61 | emit: function(event) { 62 | event = '$' + event; 63 | let cbs = listeners[event]; 64 | if (cbs) { 65 | const args = [].slice.call(arguments, 1); 66 | cbs = cbs.slice(); 67 | for (let i = 0, l = cbs.length; i < l; i++) { 68 | cbs[i].apply(this, args); 69 | } 70 | } 71 | }, 72 | 73 | contain: function(selectedNode) { 74 | this.location.finalUUID = ""; 75 | this.location.level = 0; 76 | // devtoolsModel is in global scope 77 | // TODO: rename devtoolsModel to __DevtoolsModel__ 78 | var nodeTree = devtoolsModel.getNodeTree(); 79 | for (var i = 0; i < nodeTree.length; i++) { 80 | this.containByNode(selectedNode, nodeTree[i], 0); 81 | } 82 | return this.location.finalUUID; 83 | }, 84 | containByNode: function(selectedNode, node, level) { 85 | var currLevel; 86 | if (node.node.length) { 87 | for (var i = 0; i < node.node.length; i++) { 88 | if (node.node[i].contains(selectedNode)) { 89 | if (level >= this.location.level) { 90 | this.location.finalUUID = node.uuid; 91 | this.location.level = level; 92 | } 93 | } 94 | } 95 | } 96 | if (node.childNodes) { 97 | currLevel = level + 1; 98 | for (var j = 0; j < node.childNodes.length; j++) { 99 | this.containByNode(selectedNode, 100 | node.childNodes[j], currLevel); 101 | } 102 | } 103 | } 104 | }; 105 | 106 | // debounce helper 107 | var debounce = function(func, wait, immediate) { 108 | var timeout; 109 | return function() { 110 | var context = this; 111 | var args = arguments; 112 | clearTimeout(timeout); 113 | timeout = setTimeout(function() { 114 | timeout = null; 115 | if (!immediate) func.apply(context, args); 116 | }, wait); 117 | if (immediate && !timeout) func.apply(context, args); 118 | }; 119 | }; 120 | 121 | var emitRerender = function() { 122 | hook.emit("reRender"); 123 | }; 124 | 125 | var emitStateRender = function() { 126 | hook.emit("flushMessage"); 127 | }; 128 | 129 | var reRender = debounce(emitRerender, 500); 130 | var reRenderState = debounce(emitStateRender, 500); 131 | 132 | window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__ = hook; 133 | 134 | hook.on('init', function(obj) { 135 | hook.ins.push(obj); 136 | this.emit('addNodeMessage', obj); 137 | reRender(); 138 | }); 139 | 140 | hook.on('destroy', function(obj) { 141 | hook.ins.splice(hook.ins.indexOf(obj), 1); 142 | reRender(); 143 | }); 144 | 145 | hook.on('flush', function() { 146 | reRenderState(); 147 | }); 148 | } 149 | 150 | // inject the hook 151 | var script = document.createElement('script'); 152 | script.textContent = ';(' + installHook.toString() + ')(window)'; 153 | document.documentElement.appendChild(script); 154 | script.parentNode.removeChild(script); 155 | -------------------------------------------------------------------------------- /src/extension/inject.js: -------------------------------------------------------------------------------- 1 | import CircularJSON from "../shared/circular-json"; 2 | import {findElementByUUID, highLightNode, clearMask} from '../devtools-ui/utils'; 3 | import log from '../shared/log'; 4 | 5 | // listen for message from content script 6 | // ensure only executing window.addEventListener once 7 | if (!window.devtoolsModel) { 8 | window.addEventListener('message', function(event) { 9 | // We only accept messages from ourselves 10 | if (event.source !== window) 11 | return; 12 | 13 | var data = event.data; 14 | 15 | if (data.type && (data.type === "FROM_CONTENT_SCRIPT")) { 16 | if (data.action === 'UPDATE_INSTANCE' && window.devtoolsModel) { 17 | window.devtoolsModel.updateInstance(data.payload.uuid, data.payload.path, data.payload.value); 18 | } 19 | } 20 | }, false); 21 | } 22 | 23 | // this is injected to the app page when the panel is activated. 24 | // this script serves as the model layer of the devtools 25 | // this lives in origin page context 26 | window.devtoolsModel = (function() { 27 | let Regular; 28 | let hook = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__; 29 | let ins = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.ins || []; 30 | let store = []; 31 | let fetchComputedProps; 32 | let walker; 33 | let treeGen; 34 | let getDomNode; 35 | let searchGroupChildrenForDomNode; 36 | let getNodesByUUID; 37 | let highLighter; 38 | let inspectMode = false; 39 | let inspectResult = ""; 40 | 41 | highLighter = function(uuid) { 42 | if (!uuid) { 43 | clearMask(); 44 | return; 45 | } 46 | 47 | var node = ins.filter(n => { 48 | return n.uuid === uuid; 49 | })[0]; 50 | 51 | var domNode = getNodesByUUID(uuid); 52 | domNode = domNode && domNode[0]; 53 | 54 | if (!domNode) { 55 | return; 56 | } 57 | domNode.scrollIntoView(); 58 | highLightNode(domNode, node.name || "[anonymous]"); 59 | }; 60 | 61 | function onMouseOver(e) { 62 | let result = window.__REGULAR_DEVTOOLS_GLOBAL_HOOK__.contain(e.target); 63 | if (result) { 64 | if (inspectResult !== result) { 65 | inspectResult = result; 66 | highLightNode(e.target, e.target.tagName.toLowerCase()); 67 | window.postMessage({ 68 | type: "FROM_PAGE", 69 | data: { 70 | type: "currNodeChange", 71 | uuid: inspectResult 72 | } 73 | }, "*"); 74 | } 75 | } 76 | } 77 | 78 | // reserve for future use 79 | function onClick(e) { 80 | } 81 | 82 | var stringifyStore = function(store) { 83 | return CircularJSON.stringify(store, function(key, item) { 84 | if ((item !== null && typeof item === "object")) { 85 | if (Object.prototype.toString.call(item) === "[object Object]" && !item.constructor.prototype.hasOwnProperty("isPrototypeOf")) { 86 | return "[Circular]"; 87 | } else if (isNode(item) || isElement(item)) { 88 | return "[DOM node]"; 89 | } 90 | return item; 91 | } else if (isFunction(item)) { 92 | return "Function"; 93 | } 94 | return item; 95 | }); 96 | }; 97 | 98 | fetchComputedProps = function(ins) { 99 | var computed = {}; 100 | Object.keys(ins.computed).forEach(function(v) { 101 | try { 102 | computed[v] = ins.$get(v); 103 | } catch (e) { 104 | log("Fetch computed props error:", e); 105 | } 106 | }); 107 | return computed; 108 | }; 109 | 110 | function isFunction(o) { 111 | return Object.prototype.toString.call(o) === "[object Function]"; 112 | } 113 | 114 | // Returns true if it is a DOM node 115 | function isNode(o) { 116 | return ( 117 | typeof Node === "object" ? o instanceof Node : 118 | o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string" 119 | ); 120 | } 121 | 122 | // Returns true if it is a DOM element 123 | function isElement(o) { 124 | return ( 125 | typeof HTMLElement === "object" ? o instanceof HTMLElement : 126 | o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string" 127 | ); 128 | } 129 | // setObjectByPath start 130 | // https://github.com/sindresorhus/dot-prop 131 | var isObj = function(arg) { 132 | var type = typeof arg; 133 | return arg !== null && (type === "object" || type === "function"); 134 | }; 135 | 136 | function getPathSegments(path) { 137 | var pathArr = path.split('.'); 138 | var parts = []; 139 | 140 | for (var i = 0; i < pathArr.length; i++) { 141 | var p = pathArr[i]; 142 | 143 | while (p[p.length - 1] === '\\' && pathArr[i + 1] !== undefined) { 144 | p = p.slice(0, -1) + '.'; 145 | p += pathArr[++i]; 146 | } 147 | 148 | parts.push(p); 149 | } 150 | 151 | return parts; 152 | } 153 | 154 | var setObjectByPath = function(obj, path, value) { 155 | if (!isObj(obj) || typeof path !== 'string') { 156 | return; 157 | } 158 | 159 | var pathArr = getPathSegments(path); 160 | 161 | for (var i = 0; i < pathArr.length; i++) { 162 | var p = pathArr[i]; 163 | 164 | if (!isObj(obj[p])) { 165 | obj[p] = {}; 166 | } 167 | 168 | if (i === pathArr.length - 1) { 169 | obj[p] = value; 170 | } 171 | 172 | obj = obj[p]; 173 | } 174 | }; 175 | // setObjectByPath end 176 | 177 | getDomNode = function(node) { 178 | var container = []; 179 | if (node.group) { 180 | searchGroupChildrenForDomNode(node.group, container); 181 | } 182 | return container; 183 | }; 184 | 185 | searchGroupChildrenForDomNode = function(group, container) { 186 | for (let i = 0; i < group.children.length; i++) { 187 | if (group.get(i).type === "element") { 188 | container.push(group.get(i).last()); 189 | } else if (group.get(i).children) { 190 | searchGroupChildrenForDomNode(group.get(i), container); 191 | } else if (group.get(i) instanceof Regular) { 192 | if (group.get(i).group) { 193 | searchGroupChildrenForDomNode(group.get(i).group, container); 194 | } 195 | } 196 | } 197 | }; 198 | 199 | getNodesByUUID = function(uuid) { 200 | const element = findElementByUUID(store, uuid); 201 | return element && element.node; 202 | }; 203 | 204 | function isASTElement(node) { 205 | return node.type && node.group; 206 | } 207 | 208 | function isASTGroup(node) { 209 | return node.children; 210 | } 211 | 212 | function isRegularInstance(node) { 213 | return node instanceof Regular; 214 | } 215 | 216 | walker = function(node, container, flag) { 217 | var n; 218 | var i; 219 | 220 | if (isRegularInstance(node)) { 221 | n = { 222 | uuid: node.uuid, 223 | name: node.name || findComponentRegisteredName(node) || '[anonymous]', 224 | childNodes: [], 225 | inspectable: false, 226 | hasTemplate: !!node.template, 227 | hasInjected: true 228 | }; 229 | 230 | n.node = getDomNode(node); 231 | 232 | if (node.$outer) { 233 | n.isIncluded = true; 234 | } 235 | 236 | // fetch all computed props 237 | // n.computed = fetchComputedProps(node); 238 | node.visited = true; 239 | if (node.group) { 240 | treeGen(node, n.childNodes); 241 | } 242 | 243 | if (n.node.length) { 244 | n.inspectable = true; 245 | } 246 | 247 | container.push(n); 248 | } else if (isASTElement(node)) { 249 | for (i = 0; i < node.group.children.length; i++) { 250 | walker(node.group.children[i], container); 251 | } 252 | } else if (isASTGroup(node)) { 253 | for (i = 0; i < node.children.length; i++) { 254 | walker(node.children[i], container); 255 | } 256 | } 257 | }; 258 | 259 | treeGen = function(root, container) { 260 | var tree = container || []; 261 | if (root.group) { 262 | for (var i = 0; i < root.group.children.length; i++) { 263 | walker(root.group.children[i], tree); 264 | } 265 | } 266 | return tree; 267 | }; 268 | 269 | var uuid = function() { 270 | function s4() { 271 | return Math.floor((1 + Math.random()) * 0x10000) 272 | .toString(16) 273 | .substring(1); 274 | } 275 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 276 | s4() + '-' + s4() + s4() + s4(); 277 | }; 278 | 279 | var assignUUID = function(obj) { 280 | if (!obj.uuid) { 281 | obj.uuid = uuid(); 282 | } 283 | }; 284 | 285 | var assignUUIDMany = function(arr) { 286 | for (var i = 0; i < arr.length; i++) { 287 | if (!arr[i].uuid) { 288 | assignUUID(arr[i]); 289 | } 290 | } 291 | }; 292 | 293 | var storeGen = function(flag) { 294 | var node; 295 | store = []; 296 | for (var i = 0; i < ins.length; i++) { 297 | // standalone instances 298 | if (ins[i].$root === ins[i]) { 299 | // fetch all computed props 300 | // var computed = fetchComputedProps(ins[i]); 301 | const nodes = getGroupNode(ins[i]); 302 | node = { 303 | uuid: ins[i].uuid, 304 | name: ins[i].name || "[anonymous]", 305 | childNodes: [], 306 | node: [], 307 | inspectable: !!ins[i].parentNode, 308 | hasTemplate: !!ins[i].template, 309 | hasInjected: !!nodes && !!(Array.isArray(nodes) ? nodes[0].parentNode : nodes.parentNode) 310 | }; 311 | var body = document.body; 312 | if (ins[i].parentNode) { 313 | if (ins[i].parentNode === body) { 314 | for (var j = 0; j < ins[i].group.children.length; j++) { 315 | if (ins[i].group.get(j).type) { 316 | node.node.push(ins[i].group.get(j).node()); 317 | } 318 | } 319 | } else { 320 | node.node.push(ins[i].parentNode); 321 | } 322 | } 323 | ins[i].visited = true; 324 | treeGen(ins[i], node.childNodes); 325 | store.push(node); 326 | } 327 | } 328 | return stringifyStore(store); 329 | }; 330 | 331 | // generate uuid for the first time 332 | assignUUIDMany(ins); 333 | return { 334 | init: function() { 335 | if (ins.length === 0) { 336 | return; 337 | } 338 | 339 | /* eslint-disable no-proto */ 340 | let proto = ins[0].__proto__; 341 | while (proto.__proto__ !== Object.prototype) { 342 | proto = proto.__proto__; 343 | } 344 | /* eslint-enable no-proto */ 345 | Regular = proto.constructor; 346 | 347 | hook.on("flushMessage", function() { 348 | window.postMessage({ 349 | type: "FROM_PAGE", 350 | data: { 351 | type: "dataUpdate" 352 | } 353 | }, "*"); 354 | }); 355 | 356 | hook.on("addNodeMessage", function(obj) { 357 | assignUUID(obj); 358 | }); 359 | 360 | hook.on("reRender", function(obj) { 361 | window.postMessage({ 362 | type: "FROM_PAGE", 363 | data: { 364 | type: "reRender", 365 | nodes: storeGen() 366 | } 367 | }, "*"); 368 | }); 369 | 370 | window.postMessage({ 371 | type: "FROM_PAGE", 372 | data: { 373 | type: "initNodes", 374 | nodes: storeGen() 375 | } 376 | }, "*"); 377 | }, 378 | getNodeTree: function() { 379 | return store; 380 | }, 381 | updateInstance: function(uuid, path, value) { 382 | // find instance by uuid 383 | var instance; 384 | var i; 385 | var len; 386 | 387 | for (i = 0, len = ins.length; i < len; i++) { 388 | if (ins[i].uuid === uuid) { 389 | instance = ins[i]; 390 | break; 391 | } 392 | } 393 | 394 | if (!instance) return; 395 | 396 | // update instance data by path 397 | setObjectByPath(instance.data, path, value); 398 | instance.$update(); 399 | 400 | for (i = 0, len = ins.length; i < len; i++) { 401 | if (ins[i].parentNode) { 402 | ins[i].$update(); 403 | } 404 | } 405 | }, 406 | print: function(uuid) { 407 | var i; 408 | for (i = 0; i < ins.length; i++) { 409 | if (ins[i].uuid === uuid) { 410 | window.$r = ins[i]; // console output $component as curUI component 411 | break; 412 | } 413 | } 414 | }, 415 | enterInspectMode: function() { 416 | if (inspectMode) { 417 | return; 418 | } 419 | inspectMode = true; 420 | window.document.body.addEventListener("mouseover", onMouseOver); 421 | window.document.body.addEventListener("click", onClick); 422 | }, 423 | exitInspectMode: function() { 424 | if (!inspectMode) { 425 | return; 426 | } 427 | window.document.body.removeEventListener("mouseover", onMouseOver); 428 | window.document.body.removeEventListener("click", onClick); 429 | clearMask(); 430 | inspectMode = false; 431 | }, 432 | highLighter: highLighter, 433 | stringify: stringifyStore, 434 | fetchComputedProps: fetchComputedProps 435 | }; 436 | })(); 437 | 438 | window.devtoolsModel.init(); 439 | 440 | function findComponentRegisteredName(node) { 441 | let name; 442 | 443 | if (node.$parent) { 444 | let obj = node.$parent.constructor._components; 445 | let keys = Object.keys(obj); 446 | for (let i = 0; i < keys.length; i++) { 447 | if (obj[keys[i]] === node.constructor) { 448 | name = keys[i]; 449 | break; 450 | } 451 | } 452 | } 453 | 454 | return name; 455 | } 456 | 457 | function getGroupNode(item) { 458 | let node; 459 | let nodes; 460 | if (!item) return; 461 | if (typeof item.node === "function") return item.node(); 462 | if (typeof item.nodeType === "number") return item; 463 | if (item.group) return getGroupNode(item.group); 464 | 465 | item = item.children || item; 466 | if (Array.isArray(item)) { 467 | const len = item.length; 468 | if (len === 1) { 469 | return getGroupNode(item[0]); 470 | } 471 | nodes = []; 472 | for (let i = 0, len = item.length; i < len; i++) { 473 | node = getGroupNode(item[i]); 474 | if (Array.isArray(node)) { 475 | nodes.push.apply(nodes, node); 476 | } else if (node) { 477 | nodes.push(node); 478 | } 479 | } 480 | return nodes; 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Regular Developer Tools", 3 | "description": "A Chrome extension for inspecting regularjs components", 4 | "version": "0.9.3", 5 | 6 | "manifest_version": 2, 7 | 8 | "icons": { 9 | "16": "/regular.png", 10 | "48": "/regular.png", 11 | "128": "/regular.png" 12 | }, 13 | 14 | "devtools_page": "/devtools.html", 15 | 16 | "background": { 17 | "persistent": true, 18 | "scripts": ["/background.js"] 19 | }, 20 | 21 | "content_scripts": [{ 22 | "matches": [""], 23 | "js": ["/hook.js"], 24 | "run_at": "document_start" 25 | }], 26 | 27 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 28 | 29 | "web_accessible_resources": [ 30 | "/inject.bundle.js" 31 | ], 32 | 33 | "permissions": ["", "tabs"] 34 | } 35 | -------------------------------------------------------------------------------- /src/extension/regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/regularjs/regular-devtools/3782e24f1d2c79de527356f984e0a6b830edc5ea/src/extension/regular.png -------------------------------------------------------------------------------- /src/shared/circular-json.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /*! (C) WebReflection Mit Style License */ 3 | export default (function CircularJSONCtor(e,t){function l(e,t,o){var u=[],f=[e],l=[e],c=[o?n:"[Circular]"],h=e,p=1,d;return function(e,v){return t&&(v=t.call(this,e,v)),e!==""&&(h!==this&&(d=p-a.call(f,this)-1,p-=d,f.splice(p,f.length),u.splice(p-1,u.length),h=this),typeof v=="object"&&v?(a.call(f,v)<0&&f.push(h=v),p=f.length,d=a.call(l,v),d<0?(d=l.push(v)-1,o?(u.push((""+e).replace(s,r)),c[d]=n+u.join(n)):c[d]=c[0]):v=c[d]):typeof v=="string"&&o&&(v=v.replace(r,i).replace(n,r))),v}}function c(e,t){for(var r=0,i=t.length;r