├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── media └── screenshot-shadow.png ├── package.json ├── release.js ├── shells ├── chrome │ ├── devtools-background.html │ ├── devtools.html │ ├── icons │ │ ├── 128-gray.png │ │ ├── 128.png │ │ ├── 16-gray.png │ │ ├── 16.png │ │ ├── 48-gray.png │ │ └── 48.png │ ├── manifest.json │ ├── popups │ │ ├── disabled.html │ │ ├── enabled.html │ │ └── not-found.html │ ├── src │ │ ├── backend.js │ │ ├── background.js │ │ ├── detector.js │ │ ├── devtools-background.js │ │ ├── devtools.js │ │ ├── hook.js │ │ └── proxy.js │ └── webpack.config.js ├── createConfig.js └── dev │ ├── index.html │ ├── src │ ├── backend.js │ ├── devtools.js │ └── hook.js │ ├── target-electron.html │ ├── target.html │ ├── target │ ├── Counter.vue │ ├── EventChild.vue │ ├── EventChild1.vue │ ├── EventChildCond.vue │ ├── Events.vue │ ├── MyClass.js │ ├── NativeTypes.vue │ ├── Other.vue │ ├── Page1.vue │ ├── Page2.vue │ ├── Router.vue │ ├── Target.vue │ ├── index.js │ ├── router.js │ └── store.js │ └── webpack.config.js ├── src ├── .eslintrc.js ├── backend │ ├── component-selector.js │ ├── events.js │ ├── highlighter.js │ ├── hook.js │ ├── index.js │ ├── router.js │ ├── toast.js │ ├── utils.js │ └── vuex.js ├── bridge.js ├── devtools │ ├── .eslintrc.js │ ├── App.vue │ ├── assets │ │ ├── MaterialIcons-Regular.woff2 │ │ ├── Roboto-Regular.woff2 │ │ └── logo.png │ ├── base-styles │ │ ├── animation.styl │ │ ├── base.styl │ │ ├── button.styl │ │ ├── colors.styl │ │ ├── imports.styl │ │ ├── md-colors.styl │ │ ├── mixins.styl │ │ ├── tabs.styl │ │ ├── tooltip.styl │ │ └── vars.styl │ ├── components │ │ ├── ActionHeader.vue │ │ ├── DataField.vue │ │ ├── ScrollPane.vue │ │ ├── SplitPane.vue │ │ └── StateInspector.vue │ ├── env.js │ ├── global.styl │ ├── index.js │ ├── locales │ │ └── en.js │ ├── mixins │ │ ├── data-field-edit.js │ │ ├── entry-list.js │ │ └── keyboard.js │ ├── plugins.js │ ├── plugins │ │ ├── global-refs.js │ │ ├── i18n.js │ │ └── responsive.js │ ├── router.js │ ├── storage.js │ ├── store │ │ └── index.js │ ├── transitions.styl │ ├── variables.styl │ └── views │ │ ├── components │ │ ├── ComponentInspector.vue │ │ ├── ComponentInstance.vue │ │ ├── ComponentTree.vue │ │ ├── ComponentsTab.vue │ │ └── module.js │ │ ├── events │ │ ├── EventInspector.vue │ │ ├── EventsHistory.vue │ │ ├── EventsTab.vue │ │ └── module.js │ │ └── vuex │ │ ├── VuexHistory.vue │ │ ├── VuexStateInspector.vue │ │ ├── VuexTab.vue │ │ ├── actions.js │ │ └── module.js ├── shared-data.js ├── transfer.js └── util.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'root': true, 3 | 'env': { 4 | 'browser': true 5 | }, 6 | 'extends': [ 7 | 'standard', 8 | 'plugin:vue/recommended' 9 | ], 10 | 'globals': { 11 | 'bridge': true, 12 | 'chrome': true, 13 | 'localStorage': true, 14 | 'HTMLDocument': true 15 | }, 16 | 'rules': { 17 | 'vue/html-closing-bracket-newline': ['error', { 18 | 'singleline': 'never', 19 | 'multiline': 'always' 20 | }] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | build 4 | *.zip 5 | *.xpi 6 | tests_output 7 | selenium-debug.log 8 | TODOs.md 9 | .idea 10 | .web-extension-id 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Marcel Pociot 4 | Copyright (c) 2014-2020 Evan You 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Livewire Devtools 2 | 3 | Debug your Livewire component state from within your browser. 4 | 5 |

screenshot

6 | 7 | ### Installation 8 | 9 | This extension does not yet have a stable version publicly available. 10 | You can download the [pre-release](https://github.com/beyondcode/livewire-devtools/releases) version and manually install it. 11 | 12 | 13 | ### Manual Installation 14 | 15 | 1. Clone this repo 16 | 2. `npm install` (Or `yarn install` if you are using yarn as the package manager) 17 | 3. `npm run build` 18 | 4. Open Chrome extension page (chrome://extensions) 19 | 5. Check "developer mode" 20 | 6. Click "load unpacked extension", and choose `shells/chrome`. 21 | 22 | ### Hacking 23 | 24 | 1. Clone this repo 25 | 2. `npm install` 26 | 3. `npm run dev` 27 | 4. A plain shell with a test app will be available at `localhost:8080`. 28 | 29 | ### Testing as Firefox addon 30 | 31 | 1. Install `web-ext` 32 | 33 | ~~~~ 34 | $ npm install --global web-ext 35 | ~~~~ 36 | 37 | Or, for Yarn: 38 | 39 | ~~~~ 40 | $ yarn global add web-ext 41 | ~~~~ 42 | 43 | Also, make sure `PATH` is set up. Something like this in `~/.bash_profile`: 44 | 45 | ~~~~ 46 | $ PATH=$PATH:$(yarn global bin) 47 | ~~~~ 48 | 49 | 2. Build and run in Firefox 50 | 51 | ~~~~ 52 | $ npm run build 53 | $ npm run run:firefox 54 | ~~~~ 55 | 56 | When using Yarn, just replace `npm` with `yarn`. 57 | 58 | 59 | ### License 60 | 61 | Thanks goes out to Vue devtools, which were used as a starting point for this. 62 | 63 | [MIT](http://opensource.org/licenses/MIT) 64 | -------------------------------------------------------------------------------- /media/screenshot-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/media/screenshot-shadow.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "livewire-devtools", 3 | "version": "1.0.0", 4 | "description": "devtools for Livewire!", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "cross-env PORT=8100 npm run dev:shell", 8 | "dev:shell": "cd shells/dev && webpack-dev-server --inline --hot --no-info", 9 | "dev:chrome": "cd shells/chrome && webpack --watch --hide-modules", 10 | "lint": "eslint src --ext=js,vue && eslint shells/chrome/src && eslint shells/dev/src && eslint shells/electron/src", 11 | "build": "cd shells/chrome && cross-env NODE_ENV=production webpack --progress --hide-modules", 12 | "run:firefox": "web-ext run -s shells/chrome -a dist -i src", 13 | "zip": "npm run zip:chrome && npm run zip:firefox", 14 | "zip:chrome": "cd shells && zip -r -FS ../dist/chrome.zip chrome -x *src/* -x *webpack.config.js", 15 | "zip:firefox": "web-ext build -s shells/chrome -a dist -i src --overwrite-dest", 16 | "sign:firefox": "cross-env web-ext sign --api-key=$LIVEWIRE_DEVTOOLS_AMO_KEY --api-secret=$LIVEWIRE_DEVTOOLS_AMO_SECRET -s shells/chrome -a dist -i src --id='{b432f069-95af-4ac7-8306-a5476489c281}'", 17 | "release": "node release.js && npm run test && npm run build && npm run zip", 18 | "release:beta": "cross-env RELEASE_CHANNEL=beta npm run release && npm run sign:firefox", 19 | "test": "npm run lint && npm run test:e2e", 20 | "test:e2e": "cross-env PORT=4040 start-server-and-test dev:shell http://localhost:4040 test:e2e:run", 21 | "test:e2e:run": "cypress run --config baseUrl=http://localhost:4040", 22 | "test:open": "cypress open --config baseUrl=http://localhost:8100" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/beyondcode/livewire-devtools.git" 27 | }, 28 | "keywords": [ 29 | "livewire", 30 | "devtools" 31 | ], 32 | "author": "Marcel Pociot", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/beyondcode/livewire-devtools/issues" 36 | }, 37 | "homepage": "https://github.com/beyondcode/livewire-devtools#readme", 38 | "dependencies": { 39 | "@vue/ui": "^0.5.1", 40 | "circular-json-es6": "^2.0.1", 41 | "lodash.debounce": "^4.0.8", 42 | "lodash.groupby": "^4.6.0", 43 | "uglifyjs-webpack-plugin": "^1.1.4", 44 | "vue": "^2.5.13", 45 | "vue-router": "^3.0.1", 46 | "vuex": "^3.0.1" 47 | }, 48 | "devDependencies": { 49 | "buble": "^0.19.0", 50 | "buble-loader": "^0.4.1", 51 | "cross-env": "^5.1.3", 52 | "css-loader": "^0.28.7", 53 | "cypress": "^3.0.2", 54 | "eslint": "^5.2.0", 55 | "eslint-config-standard": "^11.0.0", 56 | "eslint-plugin-cypress": "^2.0.1", 57 | "eslint-plugin-import": "^2.13.0", 58 | "eslint-plugin-node": "^7.0.1", 59 | "eslint-plugin-promise": "^3.8.0", 60 | "eslint-plugin-standard": "^3.1.0", 61 | "eslint-plugin-vue": "next", 62 | "file-loader": "^1.1.6", 63 | "friendly-errors-webpack-plugin": "^1.6.1", 64 | "inquirer": "^5.0.0", 65 | "launch-editor-middleware": "^2.1.0", 66 | "raw-loader": "^0.5.1", 67 | "semver": "^5.4.1", 68 | "start-server-and-test": "^1.5.0", 69 | "stylus": "^0.54.5", 70 | "stylus-loader": "^3.0.1", 71 | "url-loader": "^0.6.2", 72 | "vue-loader": "^15.0.0-beta.1", 73 | "vue-template-compiler": "^2.5.13", 74 | "webpack": "^3.10.0", 75 | "webpack-dev-server": "^2.9.7", 76 | "webpack-merge": "^4.1.2" 77 | }, 78 | "engines": { 79 | "node": ">=8.10" 80 | } 81 | } -------------------------------------------------------------------------------- /release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const inquirer = require('inquirer') 3 | const semver = require('semver') 4 | const pkg = require('./package.json') 5 | const manifest = require('./shells/chrome/manifest.json') 6 | 7 | const curVersion = pkg.version 8 | 9 | ;(async () => { 10 | const { newVersion } = await inquirer.prompt([{ 11 | type: 'input', 12 | name: 'newVersion', 13 | message: `Please provide a version (current: ${curVersion}):`, 14 | }]) 15 | 16 | if (!semver.valid(newVersion)) { 17 | console.error(`Invalid version: ${newVersion}`) 18 | process.exit(1) 19 | } 20 | 21 | if (semver.lt(newVersion, curVersion)) { 22 | console.error(`New version (${newVersion}) cannot be lower than current version (${curVersion}).`) 23 | process.exit(1) 24 | } 25 | 26 | const { yes } = await inquirer.prompt([{ 27 | name: 'yes', 28 | message: `Release ${newVersion}?`, 29 | type: 'confirm' 30 | }]) 31 | 32 | if (yes) { 33 | const isBeta = newVersion.includes('beta') 34 | pkg.version = newVersion 35 | if (isBeta) { 36 | const [, baseVersion, betaVersion] = /(.*)-beta\.(\w+)/.exec(newVersion) 37 | manifest.version = `${baseVersion}.${betaVersion}` 38 | applyIcons(manifest, '-beta') 39 | } else { 40 | manifest.version = newVersion 41 | applyIcons(manifest) 42 | } 43 | 44 | fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2)) 45 | fs.writeFileSync('./shells/chrome/manifest.json', JSON.stringify(manifest, null, 2)) 46 | } else { 47 | process.exit(1) 48 | } 49 | })() 50 | 51 | function applyIcons (manifest, suffix = '') { 52 | [16, 48, 128].forEach(size => { 53 | manifest.icons[size] = `icons/${size}${suffix}.png` 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /shells/chrome/devtools-background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /shells/chrome/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /shells/chrome/icons/128-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/128-gray.png -------------------------------------------------------------------------------- /shells/chrome/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/128.png -------------------------------------------------------------------------------- /shells/chrome/icons/16-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/16-gray.png -------------------------------------------------------------------------------- /shells/chrome/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/16.png -------------------------------------------------------------------------------- /shells/chrome/icons/48-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/48-gray.png -------------------------------------------------------------------------------- /shells/chrome/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/shells/chrome/icons/48.png -------------------------------------------------------------------------------- /shells/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Livewire devtools", 3 | "version": "1.0.0", 4 | "version_name": "1.0.0", 5 | "description": "Chrome and Firefox DevTools extension for debugging Livewire applications.", 6 | "manifest_version": 2, 7 | "icons": { 8 | "16": "icons/16.png", 9 | "48": "icons/48.png", 10 | "128": "icons/128.png" 11 | }, 12 | "browser_action": { 13 | "default_icon": { 14 | "16": "icons/16-gray.png", 15 | "48": "icons/48-gray.png", 16 | "128": "icons/128-gray.png" 17 | }, 18 | "default_title": "Livewire Devtools", 19 | "default_popup": "popups/not-found.html" 20 | }, 21 | "web_accessible_resources": [ 22 | "devtools.html", 23 | "devtools-background.html", 24 | "build/backend.js" 25 | ], 26 | "devtools_page": "devtools-background.html", 27 | "background": { 28 | "scripts": [ 29 | "build/background.js" 30 | ], 31 | "persistent": false 32 | }, 33 | "permissions": [ 34 | "http://*/*", 35 | "https://*/*", 36 | "contextMenus" 37 | ], 38 | "content_scripts": [ 39 | { 40 | "matches": [ 41 | "" 42 | ], 43 | "js": [ 44 | "build/hook.js" 45 | ], 46 | "run_at": "document_start" 47 | }, 48 | { 49 | "matches": [ 50 | "" 51 | ], 52 | "js": [ 53 | "build/detector.js" 54 | ], 55 | "run_at": "document_idle" 56 | } 57 | ] 58 | } -------------------------------------------------------------------------------- /shells/chrome/popups/disabled.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Livewire is detected on this page. 4 | Devtools inspection is not available because it's in 5 | production mode or explicitly disabled by the author. 6 |

7 | -------------------------------------------------------------------------------- /shells/chrome/popups/enabled.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Livewire is detected on this page. 4 | Open DevTools and look for the Livewire panel. 5 |

6 | -------------------------------------------------------------------------------- /shells/chrome/popups/not-found.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | Livewire not detected 4 |

5 | -------------------------------------------------------------------------------- /shells/chrome/src/backend.js: -------------------------------------------------------------------------------- 1 | // this is injected to the app page when the panel is activated. 2 | 3 | import { initBackend } from 'src/backend' 4 | import Bridge from 'src/bridge' 5 | 6 | window.addEventListener('message', handshake) 7 | 8 | function handshake (e) { 9 | if (e.data.source === 'livewire-devtools-proxy' && e.data.payload === 'init') { 10 | window.removeEventListener('message', handshake) 11 | 12 | let listeners = [] 13 | const bridge = new Bridge({ 14 | listen (fn) { 15 | var listener = evt => { 16 | if (evt.data.source === 'livewire-devtools-proxy' && evt.data.payload) { 17 | fn(evt.data.payload) 18 | } 19 | } 20 | window.addEventListener('message', listener) 21 | listeners.push(listener) 22 | }, 23 | send (data) { 24 | window.postMessage({ 25 | source: 'livewire-devtools-backend', 26 | payload: data 27 | }, '*') 28 | } 29 | }) 30 | 31 | bridge.on('shutdown', () => { 32 | listeners.forEach(l => { 33 | window.removeEventListener('message', l) 34 | }) 35 | listeners = [] 36 | }) 37 | 38 | initBackend(bridge) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /shells/chrome/src/background.js: -------------------------------------------------------------------------------- 1 | // the background script runs all the time and serves as a central message 2 | // hub for each vue devtools (panel + proxy + backend) instance. 3 | 4 | const ports = {} 5 | 6 | chrome.runtime.onConnect.addListener(port => { 7 | let tab 8 | let name 9 | if (isNumeric(port.name)) { 10 | tab = port.name 11 | name = 'devtools' 12 | installProxy(+port.name) 13 | } else { 14 | tab = port.sender.tab.id 15 | name = 'backend' 16 | } 17 | 18 | if (!ports[tab]) { 19 | ports[tab] = { 20 | devtools: null, 21 | backend: null 22 | } 23 | } 24 | ports[tab][name] = port 25 | 26 | if (ports[tab].devtools && ports[tab].backend) { 27 | doublePipe(tab, ports[tab].devtools, ports[tab].backend) 28 | } 29 | }) 30 | 31 | function isNumeric (str) { 32 | return +str + '' === str 33 | } 34 | 35 | function installProxy (tabId) { 36 | chrome.tabs.executeScript(tabId, { 37 | file: '/build/proxy.js' 38 | }, function (res) { 39 | if (!res) { 40 | ports[tabId].devtools.postMessage('proxy-fail') 41 | } else { 42 | console.log('injected proxy to tab ' + tabId) 43 | } 44 | }) 45 | } 46 | 47 | function doublePipe (id, one, two) { 48 | one.onMessage.addListener(lOne) 49 | function lOne (message) { 50 | if (message.event === 'log') { 51 | return console.log('tab ' + id, message.payload) 52 | } 53 | console.log('devtools -> backend', message) 54 | two.postMessage(message) 55 | } 56 | two.onMessage.addListener(lTwo) 57 | function lTwo (message) { 58 | if (message.event === 'log') { 59 | return console.log('tab ' + id, message.payload) 60 | } 61 | console.log('backend -> devtools', message) 62 | one.postMessage(message) 63 | } 64 | function shutdown () { 65 | console.log('tab ' + id + ' disconnected.') 66 | one.onMessage.removeListener(lOne) 67 | two.onMessage.removeListener(lTwo) 68 | one.disconnect() 69 | two.disconnect() 70 | ports[id] = null 71 | } 72 | one.onDisconnect.addListener(shutdown) 73 | two.onDisconnect.addListener(shutdown) 74 | console.log('tab ' + id + ' connected.') 75 | } 76 | 77 | chrome.runtime.onMessage.addListener((req, sender) => { 78 | if (sender.tab && req.livewireDetected) { 79 | chrome.browserAction.setIcon({ 80 | tabId: sender.tab.id, 81 | path: { 82 | 16: `icons/16.png`, 83 | 48: `icons/48.png`, 84 | 128: `icons/128.png` 85 | } 86 | }) 87 | chrome.browserAction.setPopup({ 88 | tabId: sender.tab.id, 89 | popup: req.devToolsEnabled ? `popups/enabled.html` : `popups/disabled.html` 90 | }) 91 | } 92 | }) 93 | 94 | // Right-click inspect context menu entry 95 | let activeTabId 96 | chrome.tabs.onActivated.addListener(({ tabId }) => { 97 | activeTabId = tabId 98 | }) 99 | -------------------------------------------------------------------------------- /shells/chrome/src/detector.js: -------------------------------------------------------------------------------- 1 | import { installToast } from 'src/backend/toast' 2 | import { isFirefox } from 'src/devtools/env' 3 | 4 | window.addEventListener('message', e => { 5 | if (e.source === window && e.data.livewireDetected) { 6 | chrome.runtime.sendMessage(e.data) 7 | } 8 | }) 9 | 10 | function detect (win) { 11 | setTimeout(() => { 12 | if (!win.Livewire) { 13 | return; 14 | } 15 | win.postMessage({ 16 | livewireDetected: true, 17 | devToolsEnabled: win.Livewire.devToolsEnabled || false 18 | }, '*') 19 | 20 | win.__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__.emit('init', win.Livewire); 21 | }, 100) 22 | } 23 | 24 | // inject the hook 25 | if (document instanceof HTMLDocument) { 26 | installScript(detect) 27 | installScript(installToast) 28 | } 29 | 30 | function installScript (fn) { 31 | const source = ';(' + fn.toString() + ')(window)' 32 | 33 | if (isFirefox) { 34 | // eslint-disable-next-line no-eval 35 | window.eval(source) // in Firefox, this evaluates on the content window 36 | } else { 37 | const script = document.createElement('script') 38 | script.textContent = source 39 | document.documentElement.appendChild(script) 40 | script.parentNode.removeChild(script) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /shells/chrome/src/devtools-background.js: -------------------------------------------------------------------------------- 1 | // This is the devtools script, which is called when the user opens the 2 | // Chrome devtool on a page. We check to see if we global hook has detected 3 | // Vue presence on the page. If yes, create the Vue panel; otherwise poll 4 | // for 10 seconds. 5 | 6 | let panelLoaded = false 7 | let panelShown = false 8 | let pendingAction 9 | let created = false 10 | let checkCount = 0 11 | 12 | chrome.devtools.network.onNavigated.addListener(createPanelIfHasLivewire) 13 | const checkLivewireInterval = setInterval(createPanelIfHasLivewire, 1000) 14 | createPanelIfHasLivewire() 15 | 16 | function createPanelIfHasLivewire () { 17 | if (created || checkCount++ > 10) { 18 | return 19 | } 20 | panelLoaded = false 21 | panelShown = false 22 | chrome.devtools.inspectedWindow.eval( 23 | '!!(window.__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__.Livewire)', 24 | function (hasLivewire) { 25 | if (!hasLivewire || created) { 26 | return 27 | } 28 | clearInterval(checkLivewireInterval) 29 | created = true 30 | chrome.devtools.panels.create( 31 | 'Livewire', 'icons/128.png', 'devtools.html', 32 | panel => { 33 | // panel loaded 34 | panel.onShown.addListener(onPanelShown) 35 | panel.onHidden.addListener(onPanelHidden) 36 | } 37 | ) 38 | } 39 | ) 40 | } 41 | 42 | // Runtime messages 43 | 44 | chrome.runtime.onMessage.addListener(request => { 45 | if (request === 'vue-panel-load') { 46 | onPanelLoad() 47 | } else if (request.vueToast) { 48 | toast(request.vueToast.message, request.vueToast.type) 49 | } else if (request.vueContextMenu) { 50 | onContextMenu(request.vueContextMenu) 51 | } 52 | }) 53 | 54 | // Page context menu entry 55 | 56 | function onContextMenu ({ id }) { 57 | if (id === 'inspect-instance') { 58 | const src = `window.__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__` 59 | 60 | chrome.devtools.inspectedWindow.eval(src, function (res, err) { 61 | if (err) { 62 | console.log(err) 63 | } 64 | if (typeof res !== 'undefined' && res) { 65 | panelAction(() => { 66 | chrome.runtime.sendMessage('get-context-menu-target') 67 | }, 'Open Livewire devtools to see component details') 68 | } else { 69 | pendingAction = null 70 | toast('No Livewire component was found', 'warn') 71 | } 72 | }) 73 | } 74 | } 75 | 76 | // Action that may execute immediatly 77 | // or later when the Vue panel is ready 78 | 79 | function panelAction (cb, message = null) { 80 | if (created && panelLoaded && panelShown) { 81 | cb() 82 | } else { 83 | pendingAction = cb 84 | message && toast(message) 85 | } 86 | } 87 | 88 | function executePendingAction () { 89 | pendingAction && pendingAction() 90 | pendingAction = null 91 | } 92 | 93 | // Execute pending action when Vue panel is ready 94 | 95 | function onPanelLoad () { 96 | executePendingAction() 97 | panelLoaded = true 98 | } 99 | 100 | // Manage panel visibility 101 | 102 | function onPanelShown () { 103 | chrome.runtime.sendMessage('vue-panel-shown') 104 | panelShown = true 105 | panelLoaded && executePendingAction() 106 | } 107 | 108 | function onPanelHidden () { 109 | chrome.runtime.sendMessage('vue-panel-hidden') 110 | panelShown = false 111 | } 112 | 113 | // Toasts 114 | 115 | function toast (message, type = 'normal') { 116 | const src = `(function() { 117 | __LIVEWIRE_DEVTOOLS_TOAST__(\`${message}\`, '${type}'); 118 | })()` 119 | 120 | chrome.devtools.inspectedWindow.eval(src, function (res, err) { 121 | if (err) { 122 | console.log(err) 123 | } 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /shells/chrome/src/devtools.js: -------------------------------------------------------------------------------- 1 | // this script is called when the VueDevtools panel is activated. 2 | 3 | import { initDevTools } from 'src/devtools' 4 | import Bridge from 'src/bridge' 5 | 6 | initDevTools({ 7 | 8 | /** 9 | * Inject backend, connect to background, and send back the bridge. 10 | * 11 | * @param {Function} cb 12 | */ 13 | 14 | connect (cb) { 15 | // 1. inject backend code into page 16 | injectScript(chrome.runtime.getURL('build/backend.js'), () => { 17 | // 2. connect to background to setup proxy 18 | const port = chrome.runtime.connect({ 19 | name: '' + chrome.devtools.inspectedWindow.tabId 20 | }) 21 | let disconnected = false 22 | port.onDisconnect.addListener(() => { 23 | disconnected = true 24 | }) 25 | 26 | const bridge = new Bridge({ 27 | listen (fn) { 28 | port.onMessage.addListener(fn) 29 | }, 30 | send (data) { 31 | if (!disconnected) { 32 | port.postMessage(data) 33 | } 34 | } 35 | }) 36 | // 3. send a proxy API to the panel 37 | cb(bridge) 38 | }) 39 | }, 40 | 41 | /** 42 | * Register a function to reload the devtools app. 43 | * 44 | * @param {Function} reloadFn 45 | */ 46 | 47 | onReload (reloadFn) { 48 | chrome.devtools.network.onNavigated.addListener(reloadFn) 49 | } 50 | }) 51 | 52 | /** 53 | * Inject a globally evaluated script, in the same context with the actual 54 | * user app. 55 | * 56 | * @param {String} scriptName 57 | * @param {Function} cb 58 | */ 59 | 60 | function injectScript (scriptName, cb) { 61 | const src = ` 62 | (function() { 63 | var script = document.constructor.prototype.createElement.call(document, 'script'); 64 | script.src = "${scriptName}"; 65 | document.documentElement.appendChild(script); 66 | script.parentNode.removeChild(script); 67 | })() 68 | ` 69 | chrome.devtools.inspectedWindow.eval(src, function (res, err) { 70 | if (err) { 71 | console.log(err) 72 | } 73 | cb() 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /shells/chrome/src/hook.js: -------------------------------------------------------------------------------- 1 | // This script is injected into every page. 2 | import { installHook } from 'src/backend/hook' 3 | import { isFirefox } from 'src/devtools/env' 4 | 5 | // inject the hook 6 | if (document instanceof HTMLDocument) { 7 | const source = ';(' + installHook.toString() + ')(window)' 8 | 9 | if (isFirefox) { 10 | // eslint-disable-next-line no-eval 11 | window.eval(source) // in Firefox, this evaluates on the content window 12 | } else { 13 | const script = document.createElement('script') 14 | script.textContent = source 15 | document.documentElement.appendChild(script) 16 | script.parentNode.removeChild(script) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shells/chrome/src/proxy.js: -------------------------------------------------------------------------------- 1 | // This is a content-script that is injected only when the devtools are 2 | // activated. Because it is not injected using eval, it has full privilege 3 | // to the chrome runtime API. It serves as a proxy between the injected 4 | // backend and the Vue devtools panel. 5 | 6 | const port = chrome.runtime.connect({ 7 | name: 'content-script' 8 | }) 9 | 10 | port.onMessage.addListener(sendMessageToBackend) 11 | window.addEventListener('message', sendMessageToDevtools) 12 | port.onDisconnect.addListener(handleDisconnect) 13 | 14 | sendMessageToBackend('init') 15 | 16 | function sendMessageToBackend (payload) { 17 | window.postMessage({ 18 | source: 'livewire-devtools-proxy', 19 | payload: payload 20 | }, '*') 21 | } 22 | 23 | function sendMessageToDevtools (e) { 24 | if (e.data && e.data.source === 'livewire-devtools-backend') { 25 | port.postMessage(e.data.payload) 26 | } 27 | } 28 | 29 | function handleDisconnect () { 30 | window.removeEventListener('message', sendMessageToDevtools) 31 | sendMessageToBackend('shutdown') 32 | } 33 | -------------------------------------------------------------------------------- /shells/chrome/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const createConfig = require('../createConfig') 3 | 4 | module.exports = createConfig({ 5 | entry: { 6 | hook: './src/hook.js', 7 | devtools: './src/devtools.js', 8 | background: './src/background.js', 9 | 'devtools-background': './src/devtools-background.js', 10 | backend: './src/backend.js', 11 | proxy: './src/proxy.js', 12 | detector: './src/detector.js' 13 | }, 14 | output: { 15 | path: path.join(__dirname, 'build'), 16 | filename: '[name].js' 17 | }, 18 | devtool: process.env.NODE_ENV !== 'production' 19 | ? '#inline-source-map' 20 | : false 21 | }) 22 | -------------------------------------------------------------------------------- /shells/createConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const merge = require('webpack-merge') 4 | const { VueLoaderPlugin } = require('vue-loader') 5 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 6 | 7 | module.exports = (config, target = { chrome: 52, firefox: 48 }) => { 8 | const bubleOptions = { 9 | target, 10 | objectAssign: 'Object.assign', 11 | transforms: { 12 | forOf: false, 13 | modules: false 14 | } 15 | } 16 | 17 | const baseConfig = { 18 | resolve: { 19 | alias: { 20 | src: path.resolve(__dirname, '../src'), 21 | views: path.resolve(__dirname, '../src/devtools/views'), 22 | components: path.resolve(__dirname, '../src/devtools/components') 23 | } 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.js$/, 29 | loader: 'buble-loader', 30 | exclude: /node_modules|vue\/dist|vuex\/dist/, 31 | options: bubleOptions 32 | }, 33 | { 34 | test: /\.vue$/, 35 | loader: 'vue-loader', 36 | options: { 37 | compilerOptions: { 38 | preserveWhitespace: false 39 | }, 40 | transpileOptions: bubleOptions 41 | } 42 | }, 43 | { 44 | test: /\.css$/, 45 | use: [ 46 | 'vue-style-loader', 47 | 'css-loader' 48 | ] 49 | }, 50 | { 51 | test: /\.styl(us)?$/, 52 | use: [ 53 | 'vue-style-loader', 54 | 'css-loader', 55 | 'stylus-loader' 56 | ] 57 | }, 58 | { 59 | test: /\.(png|woff2)$/, 60 | loader: 'url-loader?limit=0' 61 | } 62 | ] 63 | }, 64 | performance: { 65 | hints: false 66 | }, 67 | plugins: [ 68 | new VueLoaderPlugin(), 69 | ...(process.env.VUE_DEVTOOL_TEST ? [] : [new FriendlyErrorsPlugin()]), 70 | new webpack.DefinePlugin({ 71 | 'process.env.RELEASE_CHANNEL': JSON.stringify(process.env.RELEASE_CHANNEL || 'stable') 72 | }) 73 | ], 74 | devServer: { 75 | port: process.env.PORT 76 | } 77 | } 78 | 79 | if (process.env.NODE_ENV === 'production') { 80 | const UglifyPlugin = require('uglifyjs-webpack-plugin') 81 | baseConfig.plugins.push( 82 | new webpack.DefinePlugin({ 83 | 'process.env.NODE_ENV': '"production"' 84 | }), 85 | new UglifyPlugin() 86 | ) 87 | } 88 | 89 | return merge(baseConfig, config) 90 | } 91 | -------------------------------------------------------------------------------- /shells/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Plain Shell 6 | 7 | 30 | 31 | 32 |
Not Vue
33 | 34 |
35 |
36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /shells/dev/src/backend.js: -------------------------------------------------------------------------------- 1 | import { initBackend } from 'src/backend' 2 | import Bridge from 'src/bridge' 3 | 4 | const bridge = new Bridge({ 5 | listen (fn) { 6 | window.addEventListener('message', evt => fn(evt.data)) 7 | }, 8 | send (data) { 9 | console.log('backend -> devtools', data) 10 | window.parent.postMessage(data, '*') 11 | } 12 | }) 13 | 14 | initBackend(bridge) 15 | -------------------------------------------------------------------------------- /shells/dev/src/devtools.js: -------------------------------------------------------------------------------- 1 | import { initDevTools } from 'src/devtools' 2 | import Bridge from 'src/bridge' 3 | 4 | const target = document.getElementById('target') 5 | const targetWindow = target.contentWindow 6 | 7 | // 1. load user app 8 | target.src = 'target.html' 9 | target.onload = () => { 10 | // 2. init devtools 11 | initDevTools({ 12 | connect (cb) { 13 | // 3. called by devtools: inject backend 14 | inject('./build/backend.js', () => { 15 | // 4. send back bridge 16 | cb(new Bridge({ 17 | listen (fn) { 18 | targetWindow.parent.addEventListener('message', evt => fn(evt.data)) 19 | }, 20 | send (data) { 21 | console.log('devtools -> backend', data) 22 | targetWindow.postMessage(data, '*') 23 | } 24 | })) 25 | }) 26 | }, 27 | onReload (reloadFn) { 28 | target.onload = reloadFn 29 | } 30 | }) 31 | } 32 | 33 | function inject (src, done) { 34 | if (!src || src === 'false') { 35 | return done() 36 | } 37 | const script = target.contentDocument.createElement('script') 38 | script.src = src 39 | script.onload = done 40 | target.contentDocument.body.appendChild(script) 41 | } 42 | -------------------------------------------------------------------------------- /shells/dev/src/hook.js: -------------------------------------------------------------------------------- 1 | import { installHook } from 'src/backend/hook' 2 | 3 | installHook(window) 4 | -------------------------------------------------------------------------------- /shells/dev/target-electron.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /shells/dev/target.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /shells/dev/target/Counter.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 37 | -------------------------------------------------------------------------------- /shells/dev/target/EventChild.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 42 | -------------------------------------------------------------------------------- /shells/dev/target/EventChild1.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /shells/dev/target/EventChildCond.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | -------------------------------------------------------------------------------- /shells/dev/target/Events.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 33 | -------------------------------------------------------------------------------- /shells/dev/target/MyClass.js: -------------------------------------------------------------------------------- 1 | export default class MyClass { 2 | constructor () { 3 | this.msg = 'hi' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /shells/dev/target/NativeTypes.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 164 | -------------------------------------------------------------------------------- /shells/dev/target/Other.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 59 | 60 | 64 | -------------------------------------------------------------------------------- /shells/dev/target/Page1.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /shells/dev/target/Page2.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /shells/dev/target/Router.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | -------------------------------------------------------------------------------- /shells/dev/target/Target.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 74 | 75 | 79 | 80 | 96 | -------------------------------------------------------------------------------- /shells/dev/target/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from './store' 3 | import Target from './Target.vue' 4 | import Other from './Other.vue' 5 | import Counter from './Counter.vue' 6 | import NativeTypes from './NativeTypes.vue' 7 | import Events from './Events.vue' 8 | import MyClass from './MyClass.js' 9 | import Router from './Router.vue' 10 | import router from './router' 11 | 12 | window.VUE_DEVTOOLS_CONFIG = { 13 | openInEditorHost: '/' 14 | } 15 | 16 | const items = [] 17 | for (var i = 0; i < 100; i++) { 18 | items.push({ id: i }) 19 | } 20 | 21 | const circular = {} 22 | circular.self = circular 23 | 24 | new Vue({ 25 | store, 26 | router, 27 | render (h) { 28 | return h('div', null, [ 29 | h(Counter), 30 | h(Target, { props: { msg: 'hi', ins: new MyClass() }}), 31 | h(Other), 32 | h(Events, { key: 'foo' }), 33 | h(NativeTypes, { key: new Date() }), 34 | h(Router, { key: [] }) 35 | ]) 36 | }, 37 | data: { 38 | obj: { 39 | items: items, 40 | circular 41 | } 42 | } 43 | }).$mount('#app') 44 | 45 | // custom element instance 46 | const ce = document.querySelector('#shadow') 47 | if (ce.attachShadow) { 48 | const shadowRoot = ce.attachShadow({ mode: 'open' }) 49 | 50 | const ceVM = new Vue({ 51 | name: 'Shadow', 52 | render (h) { 53 | return h('h2', 'Inside Shadow DOM!') 54 | } 55 | }).$mount() 56 | 57 | shadowRoot.appendChild(ceVM.$el) 58 | } 59 | -------------------------------------------------------------------------------- /shells/dev/target/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import Page1 from './Page1.vue' 4 | import Page2 from './Page2.vue' 5 | 6 | Vue.use(VueRouter) 7 | 8 | const routes = [ 9 | { path: '/', name: 'page1', component: Page1 }, 10 | { path: '/page2', name: 'page2', component: Page2 } 11 | ] 12 | 13 | const router = new VueRouter({ 14 | routes 15 | }) 16 | 17 | export default router 18 | -------------------------------------------------------------------------------- /shells/dev/target/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | count: 0, 9 | date: new Date(), 10 | set: new Set(), 11 | map: new Map(), 12 | sym: Symbol('test') 13 | }, 14 | mutations: { 15 | INCREMENT: state => state.count++, 16 | DECREMENT: state => state.count--, 17 | UPDATE_DATE: state => { 18 | state.date = new Date() 19 | }, 20 | TEST_COMPONENT: state => {}, 21 | TEST_SET: state => { 22 | state.set.add(Math.random()) 23 | }, 24 | TEST_MAP: state => { 25 | state.map.set(`mykey_${state.map.size}`, state.map.size) 26 | } 27 | }, 28 | getters: { 29 | isPositive: state => state.count >= 0, 30 | hours: state => state.date.getHours() 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /shells/dev/webpack.config.js: -------------------------------------------------------------------------------- 1 | const createConfig = require('../createConfig') 2 | const openInEditor = require('launch-editor-middleware') 3 | 4 | module.exports = createConfig({ 5 | entry: { 6 | devtools: './src/devtools.js', 7 | backend: './src/backend.js', 8 | hook: './src/hook.js', 9 | target: './target/index.js' 10 | }, 11 | output: { 12 | path: __dirname + '/build', 13 | publicPath: '/build/', 14 | filename: '[name].js' 15 | }, 16 | devtool: '#cheap-module-source-map', 17 | devServer: { 18 | quiet: true, 19 | before (app) { 20 | app.use('/__open-in-editor', openInEditor()) 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-restricted-syntax': [ 4 | 'error', 5 | { 6 | selector: 'ForOfStatement', 7 | message: 'Not supported by bublé' 8 | } 9 | ] 10 | } 11 | } -------------------------------------------------------------------------------- /src/backend/component-selector.js: -------------------------------------------------------------------------------- 1 | import { highlight, unHighlight } from './highlighter' 2 | import { findRelatedComponent } from './utils' 3 | 4 | export default class ComponentSelector { 5 | constructor (bridge, instanceMap) { 6 | const self = this 7 | self.bridge = bridge 8 | self.instanceMap = instanceMap 9 | self.bindMethods() 10 | 11 | bridge.on('start-component-selector', self.startSelecting) 12 | bridge.on('stop-component-selector', self.stopSelecting) 13 | } 14 | 15 | /** 16 | * Adds event listeners for mouseover and mouseup 17 | */ 18 | startSelecting () { 19 | document.body.addEventListener('mouseover', this.elementMouseOver, true) 20 | document.body.addEventListener('click', this.elementClicked, true) 21 | document.body.addEventListener('mouseout', this.cancelEvent, true) 22 | document.body.addEventListener('mouseenter', this.cancelEvent, true) 23 | document.body.addEventListener('mouseleave', this.cancelEvent, true) 24 | document.body.addEventListener('mousedown', this.cancelEvent, true) 25 | document.body.addEventListener('mouseup', this.cancelEvent, true) 26 | } 27 | 28 | /** 29 | * Removes event listeners 30 | */ 31 | stopSelecting () { 32 | document.body.removeEventListener('mouseover', this.elementMouseOver, true) 33 | document.body.removeEventListener('click', this.elementClicked, true) 34 | document.body.removeEventListener('mouseout', this.cancelEvent, true) 35 | document.body.removeEventListener('mouseenter', this.cancelEvent, true) 36 | document.body.removeEventListener('mouseleave', this.cancelEvent, true) 37 | document.body.removeEventListener('mousedown', this.cancelEvent, true) 38 | document.body.removeEventListener('mouseup', this.cancelEvent, true) 39 | 40 | unHighlight() 41 | } 42 | 43 | /** 44 | * Highlights a component on element mouse over 45 | * @param {MouseEvent} e 46 | */ 47 | elementMouseOver (e) { 48 | this.cancelEvent(e) 49 | 50 | const el = e.target 51 | if (el) { 52 | this.selectedInstance = findRelatedComponent(el) 53 | } 54 | 55 | unHighlight() 56 | if (this.selectedInstance) { 57 | highlight(this.selectedInstance) 58 | } 59 | } 60 | 61 | /** 62 | * Selects an instance in the component view 63 | * @param {MouseEvent} e 64 | */ 65 | elementClicked (e) { 66 | this.cancelEvent(e) 67 | 68 | if (this.selectedInstance) { 69 | this.bridge.send('inspect-instance', this.selectedInstance.__LIVEWIRE_DEVTOOLS_UID__) 70 | } 71 | 72 | this.stopSelecting() 73 | } 74 | 75 | /** 76 | * Cancel a mouse event 77 | * @param {MouseEvent} e 78 | */ 79 | cancelEvent (e) { 80 | e.stopImmediatePropagation() 81 | e.preventDefault() 82 | } 83 | 84 | /** 85 | * Bind class methods to the class scope to avoid rebind for event listeners 86 | */ 87 | bindMethods () { 88 | this.startSelecting = this.startSelecting.bind(this) 89 | this.stopSelecting = this.stopSelecting.bind(this) 90 | this.elementMouseOver = this.elementMouseOver.bind(this) 91 | this.elementClicked = this.elementClicked.bind(this) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/backend/events.js: -------------------------------------------------------------------------------- 1 | import { stringify } from '../util' 2 | import { getInstanceName } from './index' 3 | 4 | export function initEventsBackend (Livewire, bridge) { 5 | let recording = true 6 | 7 | bridge.on('events:toggle-recording', enabled => { 8 | recording = enabled 9 | }) 10 | 11 | function logEvent(Livewire, type, instance, eventName, payload) { 12 | // The string check is important for compat with 1.x where the first 13 | // argument may be an object instead of a string. 14 | // this also ensures the event is only logged for direct $emit (source) 15 | // instead of by $dispatch/$broadcast 16 | if (typeof eventName === 'string') { 17 | 18 | let instanceId; 19 | let instanceName = 'unknown'; 20 | 21 | if (instance !== null) { 22 | let component = Livewire.components.componentsById[instanceId]; 23 | instanceId = instance.getAttribute('wire:id'); 24 | instanceName = component.name || component.fingerprint.name 25 | } 26 | 27 | bridge.send('event:triggered', stringify({ 28 | eventName, 29 | type, 30 | payload, 31 | instanceId, 32 | instanceName, 33 | timestamp: Date.now() 34 | })) 35 | } 36 | } 37 | 38 | function wrapEmit () { 39 | const original = Livewire.components['emit'] 40 | 41 | Livewire.components['emit'] = function (...args) { 42 | const res = original.apply(this, args) 43 | if (recording) { 44 | logEvent(Livewire, 'emit', null, args[0], args.slice(1)) 45 | } 46 | return res 47 | } 48 | } 49 | 50 | wrapEmit(); 51 | // wrap('emitUp') 52 | // wrap('emitSelf') 53 | // wrap('$broadcast') 54 | // wrap('$dispatch') 55 | } 56 | -------------------------------------------------------------------------------- /src/backend/highlighter.js: -------------------------------------------------------------------------------- 1 | import { inDoc, classify } from '../util' 2 | import { getInstanceName } from './index' 3 | import SharedData from 'src/shared-data' 4 | 5 | const overlay = document.createElement('div') 6 | overlay.style.backgroundColor = 'rgba(104, 182, 255, 0.35)' 7 | overlay.style.position = 'fixed' 8 | overlay.style.zIndex = '99999999999999' 9 | overlay.style.pointerEvents = 'none' 10 | overlay.style.display = 'flex' 11 | overlay.style.alignItems = 'center' 12 | overlay.style.justifyContent = 'center' 13 | overlay.style.borderRadius = '3px' 14 | const overlayContent = document.createElement('div') 15 | overlayContent.style.backgroundColor = 'rgba(104, 182, 255, 0.9)' 16 | overlayContent.style.fontFamily = 'monospace' 17 | overlayContent.style.fontSize = '11px' 18 | overlayContent.style.padding = '2px 3px' 19 | overlayContent.style.borderRadius = '3px' 20 | overlayContent.style.color = 'white' 21 | overlay.appendChild(overlayContent) 22 | 23 | /** 24 | * Highlight an instance. 25 | * 26 | * @param {Vue} instance 27 | */ 28 | 29 | export function highlight (instance) { 30 | if (!instance) return 31 | const rect = getInstanceRect(instance) 32 | if (rect) { 33 | let content = '' 34 | let name = instance.name || instance.fingerprint.name; 35 | if (SharedData.classifyComponents) name = classify(name) 36 | if (name) content = `<${name}>` 37 | showOverlay(rect, content) 38 | } 39 | } 40 | 41 | /** 42 | * Remove highlight overlay. 43 | */ 44 | 45 | export function unHighlight () { 46 | if (overlay.parentNode) { 47 | document.body.removeChild(overlay) 48 | } 49 | } 50 | 51 | /** 52 | * Get the client rect for an instance. 53 | * 54 | * @param {Vue} instance 55 | * @return {Object} 56 | */ 57 | 58 | export function getInstanceRect (instance) { 59 | 60 | const element = instance.el.el || instance.el; 61 | if (!inDoc(element)) { 62 | return 63 | } 64 | if (element.nodeType === 1) { 65 | return element.getBoundingClientRect() 66 | } 67 | } 68 | /** 69 | * Get the bounding rect for a text node using a Range. 70 | * 71 | * @param {Text} node 72 | * @return {Rect} 73 | */ 74 | 75 | const range = document.createRange() 76 | function getTextRect (node) { 77 | range.selectNode(node) 78 | return range.getBoundingClientRect() 79 | } 80 | 81 | /** 82 | * Display the overlay with given rect. 83 | * 84 | * @param {Rect} 85 | */ 86 | 87 | function showOverlay ({ width = 0, height = 0, top = 0, left = 0 }, content = '') { 88 | overlay.style.width = ~~width + 'px' 89 | overlay.style.height = ~~height + 'px' 90 | overlay.style.top = ~~top + 'px' 91 | overlay.style.left = ~~left + 'px' 92 | 93 | overlayContent.innerHTML = content 94 | 95 | document.body.appendChild(overlay) 96 | } 97 | 98 | /** 99 | * Get Vue's util 100 | */ 101 | 102 | function util () { 103 | return window.__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__.Vue.util 104 | } 105 | -------------------------------------------------------------------------------- /src/backend/hook.js: -------------------------------------------------------------------------------- 1 | // this script is injected into every page. 2 | 3 | /** 4 | * Install the hook on window, which is an event emitter. 5 | * Note because Chrome content scripts cannot directly modify the window object, 6 | * we are evaling this function by inserting a script tag. That's why we have 7 | * to inline the whole event emitter implementation here. 8 | * 9 | * @param {Window} window 10 | */ 11 | 12 | export function installHook (window) { 13 | let listeners = {} 14 | 15 | if (window.hasOwnProperty('__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__')) return 16 | 17 | const hook = { 18 | Livewire: null, 19 | 20 | on (event, fn) { 21 | event = '$' + event 22 | ;(listeners[event] || (listeners[event] = [])).push(fn) 23 | }, 24 | 25 | once (event, fn) { 26 | const eventAlias = event 27 | event = '$' + event 28 | function on () { 29 | this.off(eventAlias, on) 30 | fn.apply(this, arguments) 31 | } 32 | ;(listeners[event] || (listeners[event] = [])).push(on) 33 | }, 34 | 35 | off (event, fn) { 36 | event = '$' + event 37 | if (!arguments.length) { 38 | listeners = {} 39 | } else { 40 | const cbs = listeners[event] 41 | if (cbs) { 42 | if (!fn) { 43 | listeners[event] = null 44 | } else { 45 | for (let i = 0, l = cbs.length; i < l; i++) { 46 | const cb = cbs[i] 47 | if (cb === fn || cb.fn === fn) { 48 | cbs.splice(i, 1) 49 | break 50 | } 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | 57 | emit (event) { 58 | event = '$' + event 59 | let cbs = listeners[event] 60 | if (cbs) { 61 | const args = [].slice.call(arguments, 1) 62 | cbs = cbs.slice() 63 | for (let i = 0, l = cbs.length; i < l; i++) { 64 | cbs[i].apply(this, args) 65 | } 66 | } 67 | } 68 | } 69 | 70 | hook.once('init', Livewire => { 71 | hook.Livewire = Livewire 72 | }) 73 | 74 | Object.defineProperty(window, '__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__', { 75 | get () { 76 | return hook 77 | } 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /src/backend/index.js: -------------------------------------------------------------------------------- 1 | // This is the backend that is injected into the page that a Vue app lives in 2 | // when the Vue Devtools panel is activated. 3 | 4 | import { highlight, unHighlight, getInstanceRect } from './highlighter' 5 | import { initVuexBackend } from './vuex' 6 | import { initEventsBackend } from './events' 7 | import { findRelatedComponent } from './utils' 8 | import { stringify, classify, camelize, set, parse, getComponentName } from '../util' 9 | import ComponentSelector from './component-selector' 10 | import SharedData, { init as initSharedData } from 'src/shared-data' 11 | 12 | // hook should have been injected before this executes. 13 | const hook = window.__LIVEWIRE_DEVTOOLS_GLOBAL_HOOK__ 14 | const rootInstances = [] 15 | const propModes = ['default', 'sync', 'once'] 16 | 17 | export const instanceMap = window.__LIVEWIRE_DEVTOOLS_INSTANCE_MAP__ = new Map() 18 | const consoleBoundInstances = Array(5) 19 | let currentInspectedId 20 | let bridge 21 | let filter = '' 22 | let captureCount = 0 23 | let isLegacy = false 24 | let rootUID = 0 25 | 26 | export function initBackend (_bridge) { 27 | bridge = _bridge 28 | if (hook.Livewire) { 29 | connect() 30 | } else { 31 | hook.once('init', connect) 32 | } 33 | 34 | initRightClick() 35 | } 36 | 37 | function connect () { 38 | if (typeof hook.Livewire === 'undefined') { 39 | return; 40 | } 41 | 42 | if (! hook.Livewire.devToolsEnabled) { 43 | return; 44 | } 45 | 46 | hook.currentTab = 'components' 47 | bridge.on('switch-tab', tab => { 48 | hook.currentTab = tab 49 | if (tab === 'components') { 50 | flush() 51 | } 52 | }) 53 | 54 | // the backend may get injected to the same page multiple times 55 | // if the user closes and reopens the devtools. 56 | // make sure there's only one flush listener. 57 | hook.off('flush') 58 | hook.on('flush', () => { 59 | if (hook.currentTab === 'components') { 60 | flush() 61 | } 62 | }) 63 | 64 | bridge.on('select-instance', id => { 65 | currentInspectedId = id 66 | const instance = hook.Livewire.components.componentsById[id] 67 | 68 | bindToConsole(instance) 69 | flush() 70 | bridge.send('instance-selected') 71 | }) 72 | 73 | bridge.on('scroll-to-instance', id => { 74 | const instance = hook.Livewire.components.componentsById[id] 75 | instance && scrollIntoView(instance) 76 | }) 77 | 78 | bridge.on('filter-instances', _filter => { 79 | filter = _filter.toLowerCase() 80 | flush() 81 | }) 82 | 83 | bridge.on('vuex:travel-to-state', (payload) => { 84 | const parsedPayload = parse(payload); 85 | const parsedState = parsedPayload.state; 86 | 87 | Object.keys(parsedState).forEach(key => { 88 | hook.Livewire.components.componentsById[parsedPayload.component].set(key, parsedState[key]); 89 | }) 90 | }) 91 | 92 | bridge.on('refresh', scan) 93 | 94 | bridge.on('enter-instance', id => { 95 | let instance; 96 | 97 | try { 98 | instance = hook.Livewire.components.componentsById[id]; 99 | } catch (err) { 100 | return; 101 | } 102 | 103 | highlight(instance) 104 | }) 105 | 106 | bridge.on('leave-instance', unHighlight) 107 | 108 | // eslint-disable-next-line no-new 109 | new ComponentSelector(bridge, instanceMap) 110 | 111 | // Get the instance id that is targeted by context menu 112 | bridge.on('get-context-menu-target', () => { 113 | const instance = window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_TARGET__ 114 | 115 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_TARGET__ = null 116 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = false 117 | 118 | if (instance) { 119 | const id = instance.__LIVEWIRE_DEVTOOLS_UID__ 120 | if (id) { 121 | return bridge.send('inspect-instance', id) 122 | } 123 | } 124 | 125 | toast('No Vue component was found', 'warn') 126 | }) 127 | 128 | bridge.on('set-instance-data', args => { 129 | setStateValue(args) 130 | flush() 131 | }) 132 | 133 | initVuexBackend(hook, bridge) 134 | 135 | // events 136 | initEventsBackend(hook.Livewire, bridge) 137 | 138 | window.__LIVEWIRE_DEVTOOLS_INSPECT__ = inspectInstance 139 | 140 | const livewireHook = hook.Livewire.components.hooks.availableHooks.includes('responseReceived') ? 'responseReceived' : 'message.received'; 141 | 142 | bridge.log('backend ready.') 143 | bridge.send('ready', livewireHook === 'message.received' ? '2.x' : '1.x') // TODO: Detect version 144 | console.log( 145 | `%c livewire-devtools %c Detected Livewire %c`, 146 | 'background:#3182ce ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 147 | 'background:#ed64a6 ; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 148 | 'background:transparent' 149 | ) 150 | 151 | hook.Livewire.hook(livewireHook, (component, payload) => { 152 | flush(); 153 | }) 154 | 155 | scan() 156 | } 157 | 158 | /** 159 | * Scan the page for root level Vue instances. 160 | */ 161 | 162 | function scan () { 163 | rootInstances.length = 0 164 | hook.Livewire.components.components().forEach((component) => { 165 | rootInstances.push(component); 166 | }) 167 | flush() 168 | } 169 | 170 | /** 171 | * DOM walk helper 172 | * 173 | * @param {NodeList} nodes 174 | * @param {Function} fn 175 | */ 176 | 177 | function walk (node, fn) { 178 | if (node.childNodes) { 179 | for (let i = 0, l = node.childNodes.length; i < l; i++) { 180 | const child = node.childNodes[i] 181 | const stop = fn(child) 182 | if (!stop) { 183 | walk(child, fn) 184 | } 185 | } 186 | } 187 | 188 | // also walk shadow DOM 189 | if (node.shadowRoot) { 190 | walk(node.shadowRoot, fn) 191 | } 192 | } 193 | 194 | /** 195 | * Called on every Vue.js batcher flush cycle. 196 | * Capture current component tree structure and the state 197 | * of the current inspected instance (if present) and 198 | * send it to the devtools. 199 | */ 200 | 201 | function flush () { 202 | let start 203 | if (process.env.NODE_ENV !== 'production') { 204 | captureCount = 0 205 | start = window.performance.now() 206 | } 207 | const payload = stringify({ 208 | inspectedInstance: getInstanceDetails(currentInspectedId), 209 | instances: findQualifiedChildrenFromList(rootInstances) 210 | }) 211 | if (process.env.NODE_ENV !== 'production') { 212 | console.log(`[flush] serialized ${captureCount} instances, took ${window.performance.now() - start}ms.`) 213 | } 214 | bridge.send('flush', payload) 215 | } 216 | 217 | /** 218 | * Iterate through an array of instances and flatten it into 219 | * an array of qualified instances. This is a depth-first 220 | * traversal - e.g. if an instance is not matched, we will 221 | * recursively go deeper until a qualified child is found. 222 | * 223 | * @param {Array} instances 224 | * @return {Array} 225 | */ 226 | 227 | function findQualifiedChildrenFromList (instances) { 228 | return !filter 229 | ? instances.map(capture) 230 | : Array.prototype.concat.apply([], instances.map(findQualifiedChildren)) 231 | } 232 | 233 | /** 234 | * Find qualified children from a single instance. 235 | * If the instance itself is qualified, just return itself. 236 | * This is ok because [].concat works in both cases. 237 | * 238 | * @param {Vue} instance 239 | * @return {Vue|Array} 240 | */ 241 | 242 | function findQualifiedChildren (instance) { 243 | return isQualified(instance) 244 | ? capture(instance) 245 | : findQualifiedChildrenFromList(instance.$children) 246 | } 247 | 248 | /** 249 | * Check if an instance is qualified. 250 | * 251 | * @param {Vue} instance 252 | * @return {Boolean} 253 | */ 254 | 255 | function isQualified (instance) { 256 | const name = classify(getInstanceName(instance)).toLowerCase() 257 | return name.indexOf(filter) > -1 258 | } 259 | 260 | /** 261 | * Capture the meta information of an instance. (recursive) 262 | * 263 | * @param {Vue} instance 264 | * @return {Object} 265 | */ 266 | 267 | function capture (instance, _, list) { 268 | if (process.env.NODE_ENV !== 'production') { 269 | captureCount++ 270 | } 271 | // instance._uid is not reliable in devtools as there 272 | // may be 2 roots with same _uid which causes unexpected 273 | // behaviour 274 | instance.__LIVEWIRE_DEVTOOLS_UID__ = instance.id 275 | mark(instance) 276 | const ret = { 277 | id: instance.__LIVEWIRE_DEVTOOLS_UID__, 278 | name: instance.name, 279 | renderKey: null, 280 | inactive: false, 281 | isFragment: false, 282 | children: [] 283 | } 284 | // record screen position to ensure correct ordering 285 | if ((!list || list.length > 1) && !instance._inactive) { 286 | const rect = getInstanceRect(instance) 287 | ret.top = rect ? rect.top : Infinity 288 | } else { 289 | ret.top = Infinity 290 | } 291 | // check if instance is available in console 292 | const consoleId = consoleBoundInstances.indexOf(instance.__LIVEWIRE_DEVTOOLS_UID__) 293 | ret.consoleId = consoleId > -1 ? '$vm' + consoleId : null 294 | return ret 295 | } 296 | 297 | /** 298 | * Mark an instance as captured and store it in the instance map. 299 | * 300 | * @param {Vue} instance 301 | */ 302 | 303 | function mark (instance) { 304 | if (!instanceMap.has(instance.__LIVEWIRE_DEVTOOLS_UID__)) { 305 | instanceMap.set(instance.__LIVEWIRE_DEVTOOLS_UID__, instance) 306 | } 307 | } 308 | 309 | /** 310 | * Get the detailed information of an inspected instance. 311 | * 312 | * @param {Number} id 313 | */ 314 | 315 | function getInstanceDetails (id) { 316 | let instance; 317 | 318 | try { 319 | instance = hook.Livewire.components.componentsById[id] 320 | } catch (err) { 321 | return {}; 322 | } 323 | if (!instance) { 324 | return {} 325 | } else { 326 | return { 327 | id: id, 328 | name: instance.name || instance.fingerprint.name, 329 | state: getInstanceState(instance) 330 | } 331 | } 332 | } 333 | 334 | function getInstanceState (instance) { 335 | return processState(instance) 336 | } 337 | 338 | export function getCustomInstanceDetails (instance) { 339 | const state = getInstanceState(instance) 340 | return { 341 | _custom: { 342 | type: 'component', 343 | id: instance.__LIVEWIRE_DEVTOOLS_UID__, 344 | display: getInstanceName(instance), 345 | tooltip: 'Component instance', 346 | value: reduceStateList(state), 347 | fields: { 348 | abstract: true 349 | } 350 | } 351 | } 352 | } 353 | 354 | export function reduceStateList (list) { 355 | if (!list.length) { 356 | return undefined 357 | } 358 | return list.reduce((map, item) => { 359 | const key = item.type || 'data' 360 | const obj = map[key] = map[key] || {} 361 | obj[item.key] = item.value 362 | return map 363 | }, {}) 364 | } 365 | 366 | /** 367 | * Get the appropriate display name for an instance. 368 | * 369 | * @param {Vue} instance 370 | * @return {String} 371 | */ 372 | 373 | export function getInstanceName (instance) { 374 | const name = getComponentName(instance.$options) 375 | if (name) return name 376 | return instance.$root === instance 377 | ? 'Root' 378 | : 'Anonymous Component' 379 | } 380 | 381 | /** 382 | * Process state, filtering out props and "clean" the result 383 | * with a JSON dance. This removes functions which can cause 384 | * errors during structured clone used by window.postMessage. 385 | * 386 | * @param {Vue} instance 387 | * @return {Array} 388 | */ 389 | 390 | function processState (instance) { 391 | return Object.keys(instance.data) 392 | .map(key => ({ 393 | key, 394 | value: instance.data[key], 395 | editable: true 396 | })); 397 | } 398 | 399 | /** 400 | * Sroll a node into view. 401 | * 402 | * @param {Vue} instance 403 | */ 404 | 405 | function scrollIntoView (instance) { 406 | const rect = getInstanceRect(instance) 407 | if (rect) { 408 | window.scrollBy(0, rect.top + (rect.height - window.innerHeight) / 2) 409 | } 410 | } 411 | 412 | /** 413 | * Binds given instance in console as $vm0. 414 | * For compatibility reasons it also binds it as $vm. 415 | * 416 | * @param {Vue} instance 417 | */ 418 | 419 | function bindToConsole (instance) { 420 | const id = instance.__LIVEWIRE_DEVTOOLS_UID__ 421 | const index = consoleBoundInstances.indexOf(id) 422 | if (index > -1) { 423 | consoleBoundInstances.splice(index, 1) 424 | } else { 425 | consoleBoundInstances.pop() 426 | } 427 | consoleBoundInstances.unshift(id) 428 | for (var i = 0; i < 5; i++) { 429 | window['$vm' + i] = instanceMap.get(consoleBoundInstances[i]) 430 | } 431 | window.$vm = instance 432 | } 433 | 434 | /** 435 | * Returns a devtools unique id for instance. 436 | * @param {Vue} instance 437 | */ 438 | function getUniqueId (instance) { 439 | const rootVueId = instance.$root.__LIVEWIRE_DEVTOOLS_ROOT_UID__ 440 | return `${rootVueId}:${instance._uid}` 441 | } 442 | 443 | function getRenderKey (value) { 444 | if (value == null) return 445 | const type = typeof value 446 | if (type === 'number') { 447 | return value 448 | } else if (type === 'string') { 449 | return `'${value}'` 450 | } else if (Array.isArray(value)) { 451 | return 'Array' 452 | } else { 453 | return 'Object' 454 | } 455 | } 456 | 457 | /** 458 | * Display a toast message. 459 | * @param {any} message HTML content 460 | */ 461 | export function toast (message, type = 'normal') { 462 | const fn = window.__LIVEWIRE_DEVTOOLS_TOAST__ 463 | fn && fn(message, type) 464 | } 465 | 466 | export function inspectInstance (instance) { 467 | const id = instance.__LIVEWIRE_DEVTOOLS_UID__ 468 | id && bridge.send('inspect-instance', id) 469 | } 470 | 471 | function setStateValue ({ id, path, value, newKey, remove }) { 472 | let instance; 473 | 474 | try { 475 | instance = hook.Livewire.components.componentsById[id] 476 | } catch (err) { 477 | // 478 | } 479 | if (instance) { 480 | try { 481 | let parsedValue 482 | if (value) { 483 | parsedValue = parse(value, true) 484 | } 485 | instance.set(path, parsedValue); 486 | } catch (e) { 487 | console.error(e) 488 | } 489 | } 490 | } 491 | 492 | function initRightClick () { 493 | // Start recording context menu when Livewire is detected 494 | // event if Livewire devtools are not loaded yet 495 | document.addEventListener('contextmenu', event => { 496 | const el = event.target 497 | if (el) { 498 | // Search for parent that "is" a component instance 499 | const instance = findRelatedComponent(el) 500 | if (instance) { 501 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = true 502 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_TARGET__ = instance 503 | return 504 | } 505 | } 506 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = null 507 | window.__LIVEWIRE_DEVTOOLS_CONTEXT_MENU_TARGET__ = null 508 | }) 509 | } 510 | -------------------------------------------------------------------------------- /src/backend/router.js: -------------------------------------------------------------------------------- 1 | export function getCustomRouterDetails (router) { 2 | return { 3 | _custom: { 4 | type: 'router', 5 | display: 'VueRouter', 6 | value: { 7 | options: router.options, 8 | currentRoute: router.currentRoute 9 | }, 10 | fields: { 11 | abstract: true 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/backend/toast.js: -------------------------------------------------------------------------------- 1 | export function installToast (window) { 2 | let toastEl = null 3 | let toastTimer = 0 4 | 5 | const colors = { 6 | normal: '#3BA776', 7 | warn: '#DB6B00', 8 | error: '#DB2600' 9 | } 10 | 11 | window.__LIVEWIRE_DEVTOOLS_TOAST__ = (message, type) => { 12 | const color = colors[type] || colors.normal 13 | console.log(`%c vue-devtools %c ${message} %c `, 14 | 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 15 | `background: ${color}; padding: 1px; border-radius: 0 3px 3px 0; color: #fff`, 16 | 'background:transparent') 17 | if (!toastEl) { 18 | toastEl = document.createElement('div') 19 | toastEl.addEventListener('click', removeToast) 20 | toastEl.innerHTML = ` 21 |
34 |
43 |
44 |
45 |
46 | ` 47 | document.body.appendChild(toastEl) 48 | } else { 49 | toastEl.querySelector('.vue-wrapper').style.background = color 50 | } 51 | 52 | toastEl.querySelector('.vue-content').innerText = message 53 | 54 | clearTimeout(toastTimer) 55 | toastTimer = setTimeout(removeToast, 5000) 56 | } 57 | 58 | function removeToast () { 59 | clearTimeout(toastTimer) 60 | if (toastEl) { 61 | document.body.removeChild(toastEl) 62 | toastEl = null 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/backend/utils.js: -------------------------------------------------------------------------------- 1 | export function findRelatedComponent (el) { 2 | while (!el.__livewire && el.parentElement) { 3 | el = el.parentElement 4 | } 5 | return el.__livewire 6 | } 7 | -------------------------------------------------------------------------------- /src/backend/vuex.js: -------------------------------------------------------------------------------- 1 | import { stringify, parse } from 'src/util' 2 | 3 | export function initVuexBackend (hook, bridge) { 4 | const store = hook.store 5 | let recording = true 6 | 7 | // application -> devtool 8 | hook.Livewire.components.components().map((component) => { 9 | bridge.send('vuex:mutation', { 10 | checksum: null, 11 | component: component.id, 12 | mutation: { 13 | type: (component.name || component.fingerprint.name) + " - init", 14 | payload: stringify(component.data) 15 | }, 16 | timestamp: Date.now(), 17 | snapshot: stringify({ 18 | state: component.data, 19 | getters: {} 20 | }) 21 | }) 22 | }) 23 | 24 | const livewireHook = hook.Livewire.components.hooks.availableHooks.includes('responseReceived') ? 'responseReceived' : 'message.received'; 25 | 26 | if (livewireHook === 'message.received') { 27 | hook.Livewire.hook(livewireHook, (message, component) => { 28 | if (!recording) return 29 | const payload = message.response; 30 | 31 | bridge.send('vuex:mutation', { 32 | checksum: payload.checksum || payload.serverMemo.checksum, 33 | component: component.id, 34 | mutation: { 35 | type: component.name || component.fingerprint.name, 36 | payload: stringify(payload) 37 | }, 38 | timestamp: Date.now(), 39 | snapshot: stringify({ 40 | state: component.data, 41 | getters: {} 42 | }) 43 | }) 44 | }) 45 | } else { 46 | hook.Livewire.hook(livewireHook, (component, payload) => { 47 | if (!recording) return 48 | bridge.send('vuex:mutation', { 49 | checksum: payload.checksum || payload.serverMemo.checksum, 50 | component: component.id, 51 | mutation: { 52 | type: component.name || component.fingerprint.name, 53 | payload: stringify(payload) 54 | }, 55 | timestamp: Date.now(), 56 | snapshot: stringify({ 57 | state: component.data, 58 | getters: {} 59 | }) 60 | }) 61 | }) 62 | } 63 | 64 | // devtool -> application 65 | bridge.on('vuex:travel-to-state', state => { 66 | hook.emit('vuex:travel-to-state', parse(state, true)) 67 | }) 68 | 69 | bridge.on('vuex:import-state', state => { 70 | //hook.emit('vuex:travel-to-state', parse(state, true)) 71 | //bridge.send('vuex:init', getSnapshot()) 72 | }) 73 | 74 | bridge.on('vuex:toggle-recording', enabled => { 75 | recording = enabled 76 | }) 77 | } 78 | 79 | export function getCustomStoreDetails (store) { 80 | return { 81 | _custom: { 82 | type: 'store', 83 | display: 'Store', 84 | value: { 85 | state: store.state, 86 | getters: store.getters 87 | }, 88 | fields: { 89 | abstract: true 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/bridge.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | export default class Bridge extends EventEmitter { 4 | constructor (wall) { 5 | super() 6 | // Setting `this` to `self` here to fix an error in the Safari build: 7 | // ReferenceError: Cannot access uninitialized variable. 8 | // The error might be related to the webkit bug here: 9 | // https://bugs.webkit.org/show_bug.cgi?id=171543 10 | const self = this 11 | self.setMaxListeners(Infinity) 12 | self.wall = wall 13 | wall.listen(message => { 14 | if (typeof message === 'string') { 15 | self.emit(message) 16 | } else { 17 | self.emit(message.event, message.payload) 18 | } 19 | }) 20 | } 21 | 22 | /** 23 | * Send an event. 24 | * 25 | * @param {String} event 26 | * @param {*} payload 27 | */ 28 | 29 | send (event, payload) { 30 | this.wall.send({ 31 | event, 32 | payload 33 | }) 34 | } 35 | 36 | /** 37 | * Log a message to the devtools background page. 38 | * 39 | * @param {String} message 40 | */ 41 | 42 | log (message) { 43 | this.send('log', message) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/devtools/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-restricted-syntax': 'off' 4 | } 5 | } -------------------------------------------------------------------------------- /src/devtools/assets/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/src/devtools/assets/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /src/devtools/assets/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/src/devtools/assets/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /src/devtools/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/beyondcode/livewire-devtools/709cf631968da4ccfb5c9059f38cae1ccc947326/src/devtools/assets/logo.png -------------------------------------------------------------------------------- /src/devtools/base-styles/animation.styl: -------------------------------------------------------------------------------- 1 | // Focus 2 | 3 | @keyframes vue-ui-focus 4 | $color = $vue-ui-color-accent 5 | 0% 6 | border-color $color 7 | box-shadow 0 0 10px rgba($color, 1) 8 | 100% 9 | border-color rgba($color, .6) 10 | box-shadow 0 0 4px rgba($color, .5) 11 | 12 | // Slide 13 | 14 | vue-ui-slide($direction, $offset) 15 | if $direction == bottom 16 | $transform = "translateY(%s)" % $offset 17 | else if $direction == top 18 | $transform = "translateY(-%s)" % $offset 19 | else if $direction == left 20 | $transform = "translateX(-%s)" % $offset 21 | else if $direction == right 22 | $transform = "translateX(%s)" % $offset 23 | @keyframes vue-ui-slide-from-{$direction} 24 | 0% 25 | transform $transform 26 | 100% 27 | transform none 28 | @keyframes vue-ui-slide-to-{$direction} 29 | 0% 30 | transform none 31 | 100% 32 | transform $transform 33 | 34 | $offset = 10px 35 | vue-ui-slide(bottom, $offset) 36 | vue-ui-slide(top, $offset) 37 | vue-ui-slide(left, $offset) 38 | vue-ui-slide(right, $offset) 39 | 40 | // Fade 41 | 42 | .vue-ui-fade-enter-active, 43 | .vue-ui-fade-leave-active 44 | transition opacity .15s linear 45 | 46 | .vue-ui-fade-enter, 47 | .vue-ui-fade-leave-to 48 | opacity 0 49 | -------------------------------------------------------------------------------- /src/devtools/base-styles/base.styl: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,400i,700,700i') 2 | 3 | @import "./imports" 4 | @import "./animation" 5 | @import "./tooltip" 6 | 7 | html 8 | background $vue-ui-color-light 9 | 10 | body 11 | font-size 16px 12 | font-family 'Roboto', 'Avenir', Helvetica, Arial, sans-serif 13 | -webkit-font-smoothing antialiased 14 | -moz-osx-font-smoothing grayscale 15 | color $vue-ui-color-dark 16 | margin 0 17 | 18 | .vue-ui-dark-mode 19 | color $vue-ui-color-light 20 | background $vue-ui-color-darker 21 | body 22 | color @color 23 | 24 | hr 25 | border none 26 | height 1px 27 | background $vue-ui-color-light-neutral 28 | 29 | a 30 | color $vue-ui-color-primary 31 | text-decoration none 32 | .vue-ui-icon 33 | svg 34 | fill @color 35 | 36 | h1, h2, h3, h4, h5, h6 37 | font-weight lighter 38 | margin 24px 0 8px 39 | &:first-child 40 | margin-top 0 41 | 42 | h1 43 | font-size 42px 44 | 45 | h2 46 | font-size 32px 47 | 48 | h3 49 | font-size 26px 50 | 51 | h4 52 | font-size 22px 53 | 54 | h5 55 | font-size 18px 56 | 57 | h6 58 | font-size 16px 59 | 60 | p 61 | margin 0 0 8px 62 | 63 | ul 64 | margin 4px 0 65 | 66 | .vue-ui-no-scroll 67 | overflow hidden 68 | 69 | .vue-ui-spacer 70 | flex 100% 1 1 !important 71 | width 0 72 | height 0 73 | 74 | .vue-ui-empty 75 | color rgba($vue-ui-color-dark, .5) 76 | padding 24px 77 | text-align center 78 | box-sizing border-box 79 | .vue-ui-icon 80 | svg 81 | fill @color 82 | .vue-ui-dark-mode & 83 | color rgba($vue-ui-color-light, .5) 84 | 85 | .vue-ui-text 86 | &.banner 87 | padding 12px 88 | display flex 89 | align-items center 90 | border-radius $br 91 | > .vue-ui-icon 92 | &:first-child 93 | margin-right 10px 94 | &.primary 95 | vue-ui-text-colors($vue-ui-color-primary) 96 | &.accent 97 | vue-ui-text-colors($vue-ui-color-accent) 98 | &.danger 99 | vue-ui-text-colors($vue-ui-color-danger) 100 | &.warning 101 | vue-ui-text-colors($vue-ui-color-warning) 102 | &.info 103 | vue-ui-text-colors($vue-ui-color-info) 104 | &.success 105 | vue-ui-text-colors($vue-ui-color-success) 106 | 107 | .vue-ui-grid 108 | display grid 109 | &.default-gap 110 | grid-gap 12px 111 | &.big-gap 112 | grid-gap 24px 113 | for n in (1..10) 114 | &.col-{n} 115 | grid-template-columns repeat(n, 1fr) 116 | 117 | .span-{n} 118 | grid-column span n 119 | 120 | ::-webkit-scrollbar 121 | width 10px 122 | height @width 123 | ::-webkit-scrollbar-track-piece 124 | background transparent 125 | ::-webkit-scrollbar-track:hover 126 | background rgba($vue-ui-color-dark-neutral, .05) 127 | .vue-ui-dark-mode & 128 | background rgba($vue-ui-color-dark-neutral, .1) 129 | ::-webkit-scrollbar-thumb 130 | background-color lighten($vue-ui-color-dark-neutral, 60%) 131 | border 3px solid transparent 132 | background-clip padding-box 133 | border-radius 5px 134 | &:hover 135 | background-color $vue-ui-color-dark-neutral 136 | .vue-ui-dark-mode & 137 | background-color lighten($vue-ui-color-dark, 10%) 138 | &:hover 139 | background-color lighten($vue-ui-color-dark-neutral, 10%) 140 | -------------------------------------------------------------------------------- /src/devtools/base-styles/button.styl: -------------------------------------------------------------------------------- 1 | $focus-color = $vue-ui-color-dark 2 | 3 | colors($dark, $light, $invert = false) 4 | if $invert 5 | $foreground = $light 6 | $background = $dark 7 | else 8 | $foreground = $dark 9 | $background = $light 10 | button-colors($foreground, $background) 11 | &.flat 12 | button-colors($dark, transparent) 13 | &:not(.ghost) 14 | &:hover, 15 | &:active, 16 | .vue-ui-dropdown.open .dropdown-trigger & 17 | button-colors($foreground, $background) 18 | 19 | .vue-ui-button 20 | display inline-block 21 | vertical-align middle 22 | border none 23 | font-family inherit 24 | text-decoration none 25 | cursor pointer 26 | user-select none 27 | position relative 28 | box-sizing border-box 29 | border-radius $br 30 | padding 0 14px 31 | font-size 14px 32 | line-height 16px 33 | height 32px 34 | > .content 35 | height 100% 36 | h-box() 37 | box-center() 38 | > .tag-wrapper 39 | display flex 40 | box-center() 41 | margin-left 6px 42 | > .tag 43 | padding 2px 2px 0 44 | border-radius 4px 45 | font-size 10px 46 | line-height 10px 47 | // font-weight bold 48 | font-family monospace 49 | &.big 50 | padding 0 18px 51 | font-size 16px 52 | height 44px 53 | .vue-ui-icon 54 | width 24px 55 | height @width 56 | > .content 57 | > .tag-wrapper 58 | > .tag 59 | padding 2px 4px 0 60 | border-radius 7px 61 | font-size 12px 62 | line-height 12px 63 | &.round 64 | border-radius 17px 65 | // Focus 66 | &:focus:focus-visible::after 67 | border-radius (@border-radius + 1px) 68 | // Big button 69 | &.big 70 | border-radius 22px 71 | // Focus 72 | &:focus:focus-visible::after 73 | border-radius (@border-radius + 1px) 74 | &.flat 75 | button-transitions() 76 | &:not(.icon-button) 77 | > .content 78 | > .button-icon 79 | position relative 80 | &.left 81 | margin-right 6px 82 | left -2px 83 | &.right 84 | margin-left 6px 85 | left 2px 86 | > .loading-secondary 87 | margin-right 6px 88 | &.icon-button 89 | padding 0 90 | width 32px 91 | height @width 92 | &.big 93 | padding 0 94 | width 44px 95 | height @width 96 | > .content 97 | width 100% 98 | > .tag-wrapper 99 | position absolute 100 | right 2px 101 | bottom @right 102 | &.big-tag 103 | > .content 104 | > .tag-wrapper 105 | right 6px 106 | bottom @right 107 | // Round style 108 | &.ghost 109 | cursor default 110 | &.disabled:not(.tab-button) 111 | opacity .5 112 | filter grayscale(50%) 113 | &.loading 114 | > .content 115 | visibility hidden 116 | > .vue-ui-loading-indicator 117 | position absolute 118 | top 0 119 | bottom 0 120 | left 0 121 | right 0 122 | // Colors 123 | colors($vue-ui-color-dark, $vue-ui-color-light-neutral) 124 | .vue-ui-dark-mode & 125 | colors($vue-ui-color-light, $vue-ui-color-dark) 126 | &.primary 127 | colors($vue-ui-color-primary, $vue-ui-color-light, true) 128 | &.accent 129 | colors($vue-ui-color-accent, $vue-ui-color-light, true) 130 | .vue-ui-dark-mode & 131 | colors(lighten($vue-ui-color-accent, 60%), $vue-ui-color-dark, true) 132 | &.danger 133 | colors($vue-ui-color-danger, $vue-ui-color-light, true) 134 | &.warning 135 | colors($vue-ui-color-warning, $vue-ui-color-light, true) 136 | &.info 137 | colors($vue-ui-color-info, $vue-ui-color-light-neutral) 138 | .vue-ui-dark-mode & 139 | colors($vue-ui-color-info, $vue-ui-color-dark) 140 | &.success 141 | colors($vue-ui-color-success, $vue-ui-color-light-neutral) 142 | .vue-ui-dark-mode & 143 | colors($vue-ui-color-success, $vue-ui-color-dark) 144 | disable-focus-styles() 145 | // Keyboard focus style 146 | &:focus:focus-visible 147 | z-index 1 148 | &::after 149 | content '' 150 | display block 151 | position absolute 152 | top 0 153 | bottom @top 154 | left @top 155 | right @top 156 | border solid 1px 157 | border-radius ($br + 1px) 158 | animation vue-ui-focus .3s forwards 159 | 160 | 161 | .vue-ui-group-button.vue-ui-button 162 | button-transitions() 163 | &:not(.selected):not(.flat) 164 | button-colors($vue-ui-color-dark, $vue-ui-color-light-neutral) 165 | .vue-ui-dark-mode & 166 | button-colors($vue-ui-color-light, $vue-ui-color-dark) 167 | &.vue-ui-select-button 168 | button-colors($vue-ui-color-light, $vue-ui-color-dark-neutral) 169 | 170 | &.selected 171 | .vue-ui-group.has-indicator.primary & 172 | button-colors($vue-ui-color-primary, $vue-ui-color-light-neutral) 173 | .vue-ui-dark-mode & 174 | button-colors($vue-ui-color-primary, $vue-ui-color-dark) 175 | .vue-ui-group.has-indicator.accent & 176 | button-colors($vue-ui-color-accent, $vue-ui-color-light-neutral) 177 | .vue-ui-dark-mode & 178 | button-colors(lighten($vue-ui-color-accent, 60%), $vue-ui-color-dark) 179 | 180 | .vue-ui-group:not(.has-indicator) & 181 | &.selected 182 | &:not(.primary):not(.accent):not(.danger):not(.warning):not(.info):not(.success):not(.flat) 183 | button-colors($vue-ui-color-light, $vue-ui-color-dark) 184 | .vue-ui-dark-mode & 185 | button-colors($vue-ui-color-light, $vue-ui-color-dark-neutral) 186 | &.vue-ui-select-button 187 | background lighten($vue-ui-color-dark-neutral, 30%) 188 | 189 | &, 190 | &.selected 191 | .vue-ui-group.has-indicator & 192 | &.flat 193 | &, 194 | .vue-ui-dark-mode & 195 | background transparent 196 | 197 | .vue-ui-group:not(.vertical) & 198 | &:not(.flat) 199 | &:not(:first-child) 200 | border-top-left-radius 0 201 | border-bottom-left-radius 0 202 | &:not(:last-child) 203 | border-top-right-radius 0 204 | border-bottom-right-radius 0 205 | &.round 206 | &:first-child 207 | padding-left 18px 208 | &:last-child 209 | padding-right 18px 210 | &.icon-button 211 | &:first-child 212 | padding-left 12px 213 | &:last-child 214 | padding-right 12px 215 | 216 | .vue-ui-group.vertical & 217 | display flex 218 | width 100% 219 | &:not(.flat) 220 | &:not(:first-child) 221 | border-top-left-radius 0 222 | border-top-right-radius 0 223 | &:not(:last-child) 224 | border-bottom-left-radius 0 225 | border-bottom-right-radius 0 226 | &.round.selected 227 | background $vue-ui-color-light-neutral !important 228 | .vue-ui-dark-mode & 229 | background $vue-ui-color-dark !important 230 | &::before 231 | content '' 232 | display block 233 | position absolute 234 | top 0 235 | left 0 236 | width 100% 237 | height 100% 238 | z-index 0 239 | border-radius 17px 240 | > .content 241 | position relative 242 | z-index 1 -------------------------------------------------------------------------------- /src/devtools/base-styles/colors.styl: -------------------------------------------------------------------------------- 1 | $vue-ui-color-light = #fff 2 | $vue-ui-color-light-neutral = #e4f5ef 3 | $vue-ui-color-dark = #2c3e50 4 | $vue-ui-color-dark-neutral = #4f6f7f 5 | $vue-ui-color-darker = #1d2935 6 | $vue-ui-color-primary = #3182ce 7 | $vue-ui-color-accent = #6806c1 8 | $vue-ui-color-danger = #e83030 9 | $vue-ui-color-warning = #ea6e00 10 | $vue-ui-color-info = #03c2e6 11 | $vue-ui-color-success = #3182ce 12 | -------------------------------------------------------------------------------- /src/devtools/base-styles/imports.styl: -------------------------------------------------------------------------------- 1 | @import "./md-colors" 2 | @import "./colors" 3 | @import "./vars" 4 | @import "./mixins" 5 | @import "./button" 6 | @import "./tabs" -------------------------------------------------------------------------------- /src/devtools/base-styles/md-colors.styl: -------------------------------------------------------------------------------- 1 | $md-red =#f44336; 2 | $md-red-50 =#ffebee; 3 | $md-red-100 =#ffcdd2; 4 | $md-red-200 =#ef9a9a; 5 | $md-red-300 =#e57373; 6 | $md-red-400 =#ef5350; 7 | $md-red-500 =#f44336; 8 | $md-red-600 =#e53935; 9 | $md-red-700 =#d32f2f; 10 | $md-red-800 =#c62828; 11 | $md-red-900 =#b71c1c; 12 | $md-red-a100 =#ff8a80; 13 | $md-red-a200 =#ff5252; 14 | $md-red-a400 =#ff1744; 15 | $md-red-a700 =#d50000; 16 | 17 | $md-pink =#e91e63; 18 | $md-pink-50 =#fce4ec; 19 | $md-pink-100 =#f8bbd0; 20 | $md-pink-200 =#f48fb1; 21 | $md-pink-300 =#f06292; 22 | $md-pink-400 =#ec407a; 23 | $md-pink-500 =#e91e63; 24 | $md-pink-600 =#d81b60; 25 | $md-pink-700 =#c2185b; 26 | $md-pink-800 =#ad1457; 27 | $md-pink-900 =#880e4f; 28 | $md-pink-a100 =#ff80ab; 29 | $md-pink-a200 =#ff4081; 30 | $md-pink-a400 =#f50057; 31 | $md-pink-a700 =#c51162; 32 | 33 | $md-purple =#9c27b0; 34 | $md-purple-50 =#f3e5f5; 35 | $md-purple-100 =#e1bee7; 36 | $md-purple-200 =#ce93d8; 37 | $md-purple-300 =#ba68c8; 38 | $md-purple-400 =#ab47bc; 39 | $md-purple-500 =#9c27b0; 40 | $md-purple-600 =#8e24aa; 41 | $md-purple-700 =#7b1fa2; 42 | $md-purple-800 =#6a1b9a; 43 | $md-purple-900 =#4a148c; 44 | $md-purple-a100 =#ea80fc; 45 | $md-purple-a200 =#e040fb; 46 | $md-purple-a400 =#d500f9; 47 | $md-purple-a700 =#aa00ff; 48 | 49 | $md-deep-purple =#673ab7; 50 | $md-deep-purple-50 =#ede7f6; 51 | $md-deep-purple-100 =#d1c4e9; 52 | $md-deep-purple-200 =#b39ddb; 53 | $md-deep-purple-300 =#9575cd; 54 | $md-deep-purple-400 =#7e57c2; 55 | $md-deep-purple-500 =#673ab7; 56 | $md-deep-purple-600 =#5e35b1; 57 | $md-deep-purple-700 =#512da8; 58 | $md-deep-purple-800 =#4527a0; 59 | $md-deep-purple-900 =#311b92; 60 | $md-deep-purple-a100 =#b388ff; 61 | $md-deep-purple-a200 =#7c4dff; 62 | $md-deep-purple-a400 =#651fff; 63 | $md-deep-purple-a700 =#6200ea; 64 | 65 | $md-indigo =#3f51b5; 66 | $md-indigo-50 =#e8eaf6; 67 | $md-indigo-100 =#c5cae9; 68 | $md-indigo-200 =#9fa8da; 69 | $md-indigo-300 =#7986cb; 70 | $md-indigo-400 =#5c6bc0; 71 | $md-indigo-500 =#3f51b5; 72 | $md-indigo-600 =#3949ab; 73 | $md-indigo-700 =#303f9f; 74 | $md-indigo-800 =#283593; 75 | $md-indigo-900 =#1a237e; 76 | $md-indigo-a100 =#8c9eff; 77 | $md-indigo-a200 =#536dfe; 78 | $md-indigo-a400 =#3d5afe; 79 | $md-indigo-a700 =#304ffe; 80 | 81 | $md-blue =#2196f3; 82 | $md-blue-50 =#e3f2fd; 83 | $md-blue-100 =#bbdefb; 84 | $md-blue-200 =#90caf9; 85 | $md-blue-300 =#64b5f6; 86 | $md-blue-400 =#42a5f5; 87 | $md-blue-500 =#2196f3; 88 | $md-blue-600 =#1e88e5; 89 | $md-blue-700 =#1976d2; 90 | $md-blue-800 =#1565c0; 91 | $md-blue-900 =#0d47a1; 92 | $md-blue-a100 =#82b1ff; 93 | $md-blue-a200 =#448aff; 94 | $md-blue-a400 =#2979ff; 95 | $md-blue-a700 =#2962ff; 96 | 97 | $md-light-blue =#03a9f4; 98 | $md-light-blue-50 =#e1f5fe; 99 | $md-light-blue-100 =#b3e5fc; 100 | $md-light-blue-200 =#81d4fa; 101 | $md-light-blue-300 =#4fc3f7; 102 | $md-light-blue-400 =#29b6f6; 103 | $md-light-blue-500 =#03a9f4; 104 | $md-light-blue-600 =#039be5; 105 | $md-light-blue-700 =#0288d1; 106 | $md-light-blue-800 =#0277bd; 107 | $md-light-blue-900 =#01579b; 108 | $md-light-blue-a100 =#80d8ff; 109 | $md-light-blue-a200 =#40c4ff; 110 | $md-light-blue-a400 =#00b0ff; 111 | $md-light-blue-a700 =#0091ea; 112 | 113 | $md-cyan =#00bcd4; 114 | $md-cyan-50 =#e0f7fa; 115 | $md-cyan-100 =#b2ebf2; 116 | $md-cyan-200 =#80deea; 117 | $md-cyan-300 =#4dd0e1; 118 | $md-cyan-400 =#26c6da; 119 | $md-cyan-500 =#00bcd4; 120 | $md-cyan-600 =#00acc1; 121 | $md-cyan-700 =#0097a7; 122 | $md-cyan-800 =#00838f; 123 | $md-cyan-900 =#006064; 124 | $md-cyan-a100 =#84ffff; 125 | $md-cyan-a200 =#18ffff; 126 | $md-cyan-a400 =#00e5ff; 127 | $md-cyan-a700 =#00b8d4; 128 | 129 | $md-teal =#009688; 130 | $md-teal-50 =#e0f2f1; 131 | $md-teal-100 =#b2dfdb; 132 | $md-teal-200 =#80cbc4; 133 | $md-teal-300 =#4db6ac; 134 | $md-teal-400 =#26a69a; 135 | $md-teal-500 =#009688; 136 | $md-teal-600 =#00897b; 137 | $md-teal-700 =#00796b; 138 | $md-teal-800 =#00695c; 139 | $md-teal-900 =#004d40; 140 | $md-teal-a100 =#a7ffeb; 141 | $md-teal-a200 =#64ffda; 142 | $md-teal-a400 =#1de9b6; 143 | $md-teal-a700 =#00bfa5; 144 | 145 | $md-green =#4caf50; 146 | $md-green-50 =#e8f5e9; 147 | $md-green-100 =#c8e6c9; 148 | $md-green-200 =#a5d6a7; 149 | $md-green-300 =#81c784; 150 | $md-green-400 =#66bb6a; 151 | $md-green-500 =#4caf50; 152 | $md-green-600 =#43a047; 153 | $md-green-700 =#388e3c; 154 | $md-green-800 =#2e7d32; 155 | $md-green-900 =#1b5e20; 156 | $md-green-a100 =#b9f6ca; 157 | $md-green-a200 =#69f0ae; 158 | $md-green-a400 =#00e676; 159 | $md-green-a700 =#00c853; 160 | 161 | $md-light-green =#8bc34a; 162 | $md-light-green-50 =#f1f8e9; 163 | $md-light-green-100 =#dcedc8; 164 | $md-light-green-200 =#c5e1a5; 165 | $md-light-green-300 =#aed581; 166 | $md-light-green-400 =#9ccc65; 167 | $md-light-green-500 =#8bc34a; 168 | $md-light-green-600 =#7cb342; 169 | $md-light-green-700 =#689f38; 170 | $md-light-green-800 =#558b2f; 171 | $md-light-green-900 =#33691e; 172 | $md-light-green-a100 =#ccff90; 173 | $md-light-green-a200 =#b2ff59; 174 | $md-light-green-a400 =#76ff03; 175 | $md-light-green-a700 =#64dd17; 176 | 177 | $md-lime =#cddc39; 178 | $md-lime-50 =#f9fbe7; 179 | $md-lime-100 =#f0f4c3; 180 | $md-lime-200 =#e6ee9c; 181 | $md-lime-300 =#dce775; 182 | $md-lime-400 =#d4e157; 183 | $md-lime-500 =#cddc39; 184 | $md-lime-600 =#c0ca33; 185 | $md-lime-700 =#afb42b; 186 | $md-lime-800 =#9e9d24; 187 | $md-lime-900 =#827717; 188 | $md-lime-a100 =#f4ff81; 189 | $md-lime-a200 =#eeff41; 190 | $md-lime-a400 =#c6ff00; 191 | $md-lime-a700 =#aeea00; 192 | 193 | $md-yellow =#ffeb3b; 194 | $md-yellow-50 =#fffde7; 195 | $md-yellow-100 =#fff9c4; 196 | $md-yellow-200 =#fff59d; 197 | $md-yellow-300 =#fff176; 198 | $md-yellow-400 =#ffee58; 199 | $md-yellow-500 =#ffeb3b; 200 | $md-yellow-600 =#fdd835; 201 | $md-yellow-700 =#fbc02d; 202 | $md-yellow-800 =#f9a825; 203 | $md-yellow-900 =#f57f17; 204 | $md-yellow-a100 =#ffff8d; 205 | $md-yellow-a200 =#ffff00; 206 | $md-yellow-a400 =#ffea00; 207 | $md-yellow-a700 =#ffd600; 208 | 209 | $md-amber =#ffc107; 210 | $md-amber-50 =#fff8e1; 211 | $md-amber-100 =#ffecb3; 212 | $md-amber-200 =#ffe082; 213 | $md-amber-300 =#ffd54f; 214 | $md-amber-400 =#ffca28; 215 | $md-amber-500 =#ffc107; 216 | $md-amber-600 =#ffb300; 217 | $md-amber-700 =#ffa000; 218 | $md-amber-800 =#ff8f00; 219 | $md-amber-900 =#ff6f00; 220 | $md-amber-a100 =#ffe57f; 221 | $md-amber-a200 =#ffd740; 222 | $md-amber-a400 =#ffc400; 223 | $md-amber-a700 =#ffab00; 224 | 225 | $md-orange =#ff9800; 226 | $md-orange-50 =#fff3e0; 227 | $md-orange-100 =#ffe0b2; 228 | $md-orange-200 =#ffcc80; 229 | $md-orange-300 =#ffb74d; 230 | $md-orange-400 =#ffa726; 231 | $md-orange-500 =#ff9800; 232 | $md-orange-600 =#fb8c00; 233 | $md-orange-700 =#f57c00; 234 | $md-orange-800 =#ef6c00; 235 | $md-orange-900 =#e65100; 236 | $md-orange-a100 =#ffd180; 237 | $md-orange-a200 =#ffab40; 238 | $md-orange-a400 =#ff9100; 239 | $md-orange-a700 =#ff6d00; 240 | 241 | $md-deep-orange =#ff5722; 242 | $md-deep-orange-50 =#fbe9e7; 243 | $md-deep-orange-100 =#ffccbc; 244 | $md-deep-orange-200 =#ffab91; 245 | $md-deep-orange-300 =#ff8a65; 246 | $md-deep-orange-400 =#ff7043; 247 | $md-deep-orange-500 =#ff5722; 248 | $md-deep-orange-600 =#f4511e; 249 | $md-deep-orange-700 =#e64a19; 250 | $md-deep-orange-800 =#d84315; 251 | $md-deep-orange-900 =#bf360c; 252 | $md-deep-orange-a100 =#ff9e80; 253 | $md-deep-orange-a200 =#ff6e40; 254 | $md-deep-orange-a400 =#ff3d00; 255 | $md-deep-orange-a700 =#dd2c00; 256 | 257 | $md-brown =#795548; 258 | $md-brown-50 =#efebe9; 259 | $md-brown-100 =#d7ccc8; 260 | $md-brown-200 =#bcaaa4; 261 | $md-brown-300 =#a1887f; 262 | $md-brown-400 =#8d6e63; 263 | $md-brown-500 =#795548; 264 | $md-brown-600 =#6d4c41; 265 | $md-brown-700 =#5d4037; 266 | $md-brown-800 =#4e342e; 267 | $md-brown-900 =#3e2723; 268 | 269 | $md-grey =#9e9e9e; 270 | $md-grey-50 =#fafafa; 271 | $md-grey-100 =#f5f5f5; 272 | $md-grey-200 =#eeeeee; 273 | $md-grey-300 =#e0e0e0; 274 | $md-grey-400 =#bdbdbd; 275 | $md-grey-500 =#9e9e9e; 276 | $md-grey-600 =#757575; 277 | $md-grey-700 =#616161; 278 | $md-grey-800 =#424242; 279 | $md-grey-900 =#212121; 280 | 281 | $md-blue-grey =#607d8b; 282 | $md-blue-grey-50 =#eceff1; 283 | $md-blue-grey-100 =#cfd8dc; 284 | $md-blue-grey-200 =#b0bec5; 285 | $md-blue-grey-300 =#90a4ae; 286 | $md-blue-grey-400 =#78909c; 287 | $md-blue-grey-500 =#607d8b; 288 | $md-blue-grey-600 =#546e7a; 289 | $md-blue-grey-700 =#455a64; 290 | $md-blue-grey-800 =#37474f; 291 | $md-blue-grey-900 =#263238; 292 | 293 | $md-black =#000000; 294 | $md-white =#ffffff; 295 | -------------------------------------------------------------------------------- /src/devtools/base-styles/mixins.styl: -------------------------------------------------------------------------------- 1 | ellipsis() { 2 | overflow: hidden; 3 | -ms-text-overflow: ellipsis; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | 8 | bounds($distance) { 9 | top: $distance; 10 | bottom: $distance; 11 | right: $distance; 12 | left: $distance; 13 | } 14 | 15 | overlay() { 16 | position: absolute; 17 | bounds(0); 18 | } 19 | 20 | flex-box() { 21 | display: flex; 22 | 23 | & > * { 24 | flex: auto 0 0; 25 | } 26 | } 27 | 28 | h-box() { 29 | flex-box(); 30 | flex-direction: row; 31 | } 32 | 33 | v-box() { 34 | flex-box(); 35 | flex-direction: column; 36 | } 37 | 38 | flex-control() { 39 | width: 0 !important; 40 | } 41 | 42 | box-center() { 43 | align-items: center; 44 | justify-content: center; 45 | } 46 | 47 | space-between-x($margin) { 48 | margin-right: $margin; 49 | 50 | &:last-child { 51 | margin-right: 0; 52 | } 53 | } 54 | 55 | space-between-y($margin) { 56 | margin-bottom: $margin; 57 | 58 | &:last-child { 59 | margin-bottom: 0; 60 | } 61 | } 62 | 63 | // Disable noisy browser styles 64 | disable-focus-styles() 65 | outline none 66 | -webkit-tap-highlight-color rgba(255, 255, 255, 0) 67 | &::-moz-focus-inner 68 | border 0 69 | 70 | // Buttons 71 | 72 | button-colors($foreground, $background) 73 | color $foreground 74 | background $background 75 | &:not(.ghost) 76 | &:hover, 77 | .vue-ui-dropdown.open .dropdown-trigger & 78 | background lighten($background, 25%) 79 | .vue-ui-dark-mode .vue-ui-dropdown.open .dropdown-trigger & 80 | background $vue-ui-color-dark-neutral 81 | color $vue-ui-color-light 82 | > .content 83 | > .button-icon 84 | svg 85 | fill $vue-ui-color-light 86 | 87 | &:hover 88 | .vue-ui-dark-mode .popover & 89 | background $vue-ui-color-dark !important 90 | &:active 91 | background darken($background, 8%) 92 | > .content 93 | > .button-icon 94 | svg 95 | fill $foreground 96 | > .vue-ui-loading-indicator, 97 | > .content > .loading-secondary 98 | .animation 99 | border-right-color $foreground 100 | border-bottom-color $foreground 101 | > .content > .tag-wrapper > .tag 102 | background $foreground 103 | color $background 104 | if $background is transparent 105 | color $md-white 106 | &::before 107 | background $background 108 | 109 | &.vue-ui-dropdown-button 110 | background transparent 111 | &:not(:hover) 112 | color $vue-ui-color-dark 113 | .vue-ui-dark-mode & 114 | color $vue-ui-color-light !important 115 | > .content 116 | > .button-icon 117 | svg 118 | fill @color 119 | .vue-ui-dark-mode & 120 | fill $vue-ui-color-light !important 121 | 122 | button-transitions() 123 | transition background .1s, color .1s 124 | > .content 125 | > .button-icon 126 | svg 127 | transition fill .1s 128 | 129 | // Vue text 130 | 131 | vue-ui-text-colors($c) 132 | color $c 133 | .vue-ui-icon 134 | svg 135 | fill $c !important 136 | &.banner 137 | background lighten($c, 90%) 138 | .vue-ui-dark-mode & 139 | background darken($c, 60%) 140 | -------------------------------------------------------------------------------- /src/devtools/base-styles/tabs.styl: -------------------------------------------------------------------------------- 1 | .vue-ui-tabs 2 | v-box() 3 | 4 | > .tabs-content 5 | flex 100% 1 1 6 | 7 | &.animate 8 | $offset = 50px 9 | 10 | > .tabs-content 11 | position relative 12 | 13 | .vue-ui-tab-enter-active, 14 | .vue-ui-tab-leave-active 15 | transition all .15s cubic-bezier(0.0, 0.0, 0.2, 1) 16 | 17 | .vue-ui-tab-leave-active 18 | position absolute 19 | top 0 20 | left 0 21 | right 0 22 | height 0 23 | 24 | .vue-ui-tab-enter, 25 | .vue-ui-tab-leave-to 26 | opacity 0 27 | 28 | &.direction-to-right 29 | .vue-ui-tab-enter 30 | transform "translateX(%s)" % $offset 31 | 32 | .vue-ui-tab-leave-to 33 | transform "translateX(-%s)" % $offset 34 | 35 | &.direction-to-left 36 | .vue-ui-tab-enter 37 | transform "translateX(-%s)" % $offset 38 | 39 | .vue-ui-tab-leave-to 40 | transform "translateX(%s)" % $offset -------------------------------------------------------------------------------- /src/devtools/base-styles/tooltip.styl: -------------------------------------------------------------------------------- 1 | .tooltip 2 | $arrow-size = 6px 3 | $tooltip-animation-duration = .12s 4 | $background = $vue-ui-color-dark 5 | $dark-background = $vue-ui-color-light 6 | $color = $vue-ui-color-light 7 | $dark-color = $vue-ui-color-dark 8 | 9 | display block !important 10 | z-index 999999 11 | 12 | .tooltip-inner 13 | background $background 14 | color $color 15 | border-radius $br 16 | padding 7px 14px 17 | font-size .95em 18 | position relative 19 | max-width 300px 20 | box-shadow 0 2px 10px rgba(black, .1) 21 | .vue-ui-dark-mode & 22 | background $dark-background 23 | color $dark-color 24 | 25 | .tooltip-arrow 26 | width $arrow-size 27 | height $arrow-size 28 | position absolute 29 | margin $arrow-size 30 | z-index 1 31 | 32 | &::before, 33 | &::after 34 | content '' 35 | width 0 36 | height 0 37 | border-style solid 38 | position absolute 39 | border-width inherit 40 | top 0 41 | left 0 42 | 43 | &::before, 44 | &::after 45 | border-color $background 46 | .vue-ui-dark-mode & 47 | border-color $dark-background 48 | 49 | &[x-placement^='top'] 50 | margin-bottom $arrow-size 51 | 52 | .tooltip-inner 53 | bottom -1px 54 | 55 | .tooltip-arrow 56 | bottom -($arrow-size) 57 | left 'calc(50% - %s)' % $arrow-size 58 | margin-top 0 59 | margin-bottom 0 60 | 61 | &::before, 62 | &::after 63 | left (-($arrow-size) / 2) 64 | 65 | &::before 66 | top 1px 67 | 68 | &::after, 69 | &::before 70 | border-width $arrow-size $arrow-size 0 $arrow-size 71 | border-left-color transparent !important 72 | border-right-color transparent !important 73 | border-bottom-color transparent !important 74 | 75 | &[x-placement^='bottom'] 76 | margin-top $arrow-size 77 | 78 | .tooltip-inner 79 | top -1px 80 | 81 | .tooltip-arrow 82 | top -($arrow-size) 83 | left 'calc(50% - %s)' % $arrow-size 84 | margin-top 0 85 | margin-bottom 0 86 | 87 | &::before, 88 | &::after 89 | left (-($arrow-size) / 2) 90 | 91 | &::before 92 | top -1px 93 | 94 | &::after, 95 | &::before 96 | border-width 0 $arrow-size $arrow-size $arrow-size 97 | border-left-color transparent !important 98 | border-right-color transparent !important 99 | border-top-color transparent !important 100 | 101 | &[x-placement^='right'] 102 | margin-left $arrow-size 103 | 104 | .tooltip-inner 105 | left -1px 106 | 107 | .tooltip-arrow 108 | left -($arrow-size) 109 | top 'calc(50% - %s)' % $arrow-size 110 | margin-left 0 111 | margin-right 0 112 | 113 | &::before, 114 | &::after 115 | top (-($arrow-size) / 2) 116 | 117 | &::before 118 | left -1px 119 | 120 | &::after, 121 | &::before 122 | border-width $arrow-size $arrow-size $arrow-size 0 123 | border-left-color transparent !important 124 | border-top-color transparent !important 125 | border-bottom-color transparent !important 126 | 127 | &[x-placement^='left'] 128 | margin-right $arrow-size 129 | 130 | .tooltip-inner 131 | right -1px 132 | 133 | .tooltip-arrow 134 | right -($arrow-size) 135 | top 'calc(50% - %s)' % $arrow-size 136 | margin-left 0 137 | margin-right 0 138 | 139 | &::before, 140 | &::after 141 | top (-($arrow-size) / 2) 142 | 143 | &::before 144 | left 1px 145 | 146 | &::after, 147 | &::before 148 | border-width $arrow-size 0 $arrow-size $arrow-size 149 | border-top-color transparent !important 150 | border-right-color transparent !important 151 | border-bottom-color transparent !important 152 | 153 | 154 | &.popover:not(.force-tooltip) 155 | $background = $vue-ui-color-light 156 | $dark-background = lighten($vue-ui-color-darker, 3%) 157 | $color = $vue-ui-color-dark 158 | $dark-color = $vue-ui-color-light 159 | 160 | .popover-inner 161 | background $background 162 | color $color 163 | padding 4px 0 164 | border-radius $br 165 | max-width none 166 | box-shadow 0 2px 20px rgba(black, .1) 167 | .vue-ui-dark-mode & 168 | background $dark-background 169 | color $dark-color 170 | box-shadow 0 2px 20px rgba(black, .3) 171 | 172 | .popover-arrow 173 | &::after, 174 | &::before 175 | border-color $background 176 | .vue-ui-dark-mode & 177 | border-color $dark-background 178 | 179 | // Hidden 180 | &[aria-hidden='true'] 181 | visibility hidden 182 | opacity 0 183 | transition opacity $tooltip-animation-duration, visibility $tooltip-animation-duration 184 | 185 | &.popover:not(.force-tooltip) 186 | &[x-placement^='top'] 187 | > .wrapper 188 | animation vue-ui-slide-to-bottom $tooltip-animation-duration 189 | &[x-placement^='bottom'] 190 | > .wrapper 191 | animation vue-ui-slide-to-top $tooltip-animation-duration 192 | &[x-placement^='left'] 193 | > .wrapper 194 | animation vue-ui-slide-to-right $tooltip-animation-duration 195 | &[x-placement^='right'] 196 | > .wrapper 197 | animation vue-ui-slide-to-left $tooltip-animation-duration 198 | 199 | // Visible 200 | &[aria-hidden='false'] 201 | visibility visible 202 | opacity 1 203 | transition opacity $tooltip-animation-duration 204 | 205 | &.popover:not(.force-tooltip) 206 | &[x-placement^='top'] 207 | > .wrapper 208 | animation vue-ui-slide-from-bottom $tooltip-animation-duration 209 | &[x-placement^='bottom'] 210 | > .wrapper 211 | animation vue-ui-slide-from-top $tooltip-animation-duration 212 | &[x-placement^='left'] 213 | > .wrapper 214 | animation vue-ui-slide-from-right $tooltip-animation-duration 215 | &[x-placement^='right'] 216 | > .wrapper 217 | animation vue-ui-slide-from-left $tooltip-animation-duration 218 | -------------------------------------------------------------------------------- /src/devtools/base-styles/vars.styl: -------------------------------------------------------------------------------- 1 | $br = 3px 2 | -------------------------------------------------------------------------------- /src/devtools/components/ActionHeader.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 99 | -------------------------------------------------------------------------------- /src/devtools/components/ScrollPane.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | 49 | 76 | -------------------------------------------------------------------------------- /src/devtools/components/SplitPane.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 106 | 107 | 161 | -------------------------------------------------------------------------------- /src/devtools/components/StateInspector.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 122 | 123 | 166 | -------------------------------------------------------------------------------- /src/devtools/env.js: -------------------------------------------------------------------------------- 1 | export const isChrome = typeof chrome !== 'undefined' && !!chrome.devtools 2 | export const isFirefox = navigator.userAgent.indexOf('Firefox') > -1 3 | export const isWindows = navigator.platform.indexOf('Win') === 0 4 | export const isMac = navigator.platform === 'MacIntel' 5 | export const isLinux = navigator.platform.indexOf('Linux') === 0 6 | export const keys = { 7 | ctrl: isMac ? '⌘' : 'Ctrl', 8 | shift: 'Shift', 9 | alt: isMac ? '⌥' : 'Alt', 10 | del: 'Del', 11 | enter: 'Enter', 12 | esc: 'Esc' 13 | } 14 | 15 | export function initEnv (Vue) { 16 | if (Vue.prototype.hasOwnProperty('$isChrome')) return 17 | 18 | Object.defineProperties(Vue.prototype, { 19 | '$isChrome': { get: () => isChrome }, 20 | '$isFirefox': { get: () => isFirefox }, 21 | '$isWindows': { get: () => isWindows }, 22 | '$isMac': { get: () => isMac }, 23 | '$isLinux': { get: () => isLinux }, 24 | '$keys': { get: () => keys } 25 | }) 26 | 27 | if (isWindows) document.body.classList.add('platform-windows') 28 | if (isMac) document.body.classList.add('platform-mac') 29 | if (isLinux) document.body.classList.add('platform-linux') 30 | } 31 | -------------------------------------------------------------------------------- /src/devtools/global.styl: -------------------------------------------------------------------------------- 1 | @import './base-styles/base' 2 | 3 | @import './variables' 4 | @import './transitions' 5 | 6 | 7 | @font-face 8 | font-family 'Roboto' 9 | font-style normal 10 | font-weight 400 11 | src local('Roboto'), local('Roboto-Regular'), url(./assets/Roboto-Regular.woff2) format('woff2') 12 | 13 | .toggle-recording .vue-ui-icon 14 | svg 15 | fill #999 !important 16 | &.enabled 17 | border-radius 50% 18 | filter: drop-shadow(0 0 3px rgba(255, 0, 0, .4)) 19 | svg 20 | fill red !important 21 | 22 | .vue-ui-icon 23 | display inline-block 24 | width 22px 25 | height @width 26 | svg 27 | width 100% 28 | height 100% 29 | fill #999 30 | pointer-events none 31 | &.big 32 | transform scale(1.3) 33 | &.medium 34 | transform scale(0.9) 35 | &.small 36 | transform scale(0.8) 37 | 38 | html, body 39 | margin 0 40 | padding 0 41 | font-family Roboto 42 | font-size 16px 43 | color #444 44 | 45 | body 46 | overflow hidden 47 | 48 | * 49 | box-sizing border-box 50 | 51 | $arrow-color = #444 52 | 53 | .arrow 54 | display inline-block 55 | width 0 56 | height 0 57 | &.up 58 | border-left 4px solid transparent 59 | border-right 4px solid transparent 60 | border-bottom 6px solid $arrow-color 61 | &.down 62 | border-left 4px solid transparent 63 | border-right 4px solid transparent 64 | border-top 6px solid $arrow-color 65 | &.right 66 | border-top 4px solid transparent 67 | border-bottom 4px solid transparent 68 | border-left 6px solid $arrow-color 69 | &.left 70 | border-top 4px solid transparent 71 | border-bottom 4px solid transparent 72 | border-right 6px solid $arrow-color 73 | 74 | .notice 75 | display flex 76 | align-items center 77 | height 100% 78 | width 100% 79 | color #aaa 80 | div 81 | text-align center 82 | padding 0.5em 83 | margin 0 auto 84 | 85 | .selectable-item 86 | background-color $background-color 87 | &:hover 88 | background-color $hover-color 89 | &.selected, 90 | &.active 91 | background-color $active-color 92 | color #fff 93 | .arrow 94 | border-left-color #fff 95 | .item-name 96 | color #fff 97 | 98 | .vue-ui-dark-mode & 99 | background-color $dark-background-color 100 | &:hover 101 | background-color $dark-hover-color 102 | .arrow 103 | border-left-color #666 104 | &.selected, 105 | &.active 106 | background-color $active-color 107 | 108 | .list-item 109 | color #881391 110 | @extends .selectable-item 111 | 112 | .icon-button 113 | cursor pointer 114 | &:hover svg 115 | fill $green 116 | 117 | .scroll 118 | position relative 119 | 120 | .keyboard 121 | display inline-block 122 | min-width 22px 123 | text-align center 124 | background rgba($grey, .3) 125 | padding 2px 4px 0 126 | border-radius 3px 127 | margin-bottom 6px 128 | box-shadow 0 3px 0 rgba($grey, .2) 129 | .vue-ui-dark-mode & 130 | background rgba($grey, .9) 131 | box-shadow 0 3px 0 rgba($grey, .6) 132 | 133 | .mono 134 | font-family Menlo, Consolas, monospace 135 | 136 | .green 137 | color $green 138 | .vue-ui-icon svg 139 | fill @color 140 | 141 | .grey 142 | &, 143 | .material-icons 144 | color $darkerGrey 145 | 146 | .red 147 | &, 148 | .material-icons 149 | color $red 150 | 151 | .input-example 152 | background $white 153 | color $green 154 | font-size 12px 155 | padding 0px 8px 6px 156 | margin 0 2px 157 | border-radius 2px 158 | display inline-block 159 | vertical-align baseline 160 | .vue-ui-dark-mode & 161 | background $dark-background-color 162 | &, 163 | .tooltip .tooltip-inner & 164 | .vue-ui-icon 165 | top 4px 166 | margin-right 4px 167 | svg 168 | fill #444 169 | .vue-ui-dark-mode & 170 | fill #666 171 | -------------------------------------------------------------------------------- /src/devtools/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import store from './store' 5 | import './plugins' 6 | import { parse } from '../util' 7 | import { isChrome, initEnv } from './env' 8 | import SharedData, { init as initSharedData, destroy as destroySharedData } from 'src/shared-data' 9 | import storage from './storage' 10 | import ConfirmPrompt from 'inquirer/lib/prompts/confirm' 11 | 12 | // UI 13 | 14 | let panelShown = !isChrome 15 | let pendingAction = null 16 | 17 | const isDark = isChrome ? chrome.devtools.panels.themeName === 'dark' : false 18 | const isBeta = process.env.RELEASE_CHANNEL === 'beta' 19 | 20 | // Capture and log devtool errors when running as actual extension 21 | // so that we can debug it by inspecting the background page. 22 | // We do want the errors to be thrown in the dev shell though. 23 | if (isChrome) { 24 | Vue.config.errorHandler = (e, vm) => { 25 | bridge.send('ERROR', { 26 | message: e.message, 27 | stack: e.stack, 28 | component: vm.$options.name || vm.$options._componentTag || 'anonymous' 29 | }) 30 | } 31 | 32 | chrome.runtime.onMessage.addListener(request => { 33 | if (request === 'vue-panel-shown') { 34 | onPanelShown() 35 | } else if (request === 'vue-panel-hidden') { 36 | onPanelHidden() 37 | } else if (request === 'get-context-menu-target') { 38 | getContextMenuInstance() 39 | } 40 | }) 41 | } 42 | 43 | Vue.options.renderError = (h, e) => { 44 | return h('pre', { 45 | style: { 46 | backgroundColor: 'red', 47 | color: 'white', 48 | fontSize: '12px', 49 | padding: '10px' 50 | } 51 | }, e.stack) 52 | } 53 | 54 | let app = null 55 | 56 | /** 57 | * Create the main devtools app. Expects to be called with a shell interface 58 | * which implements a connect method. 59 | * 60 | * @param {Object} shell 61 | * - connect(bridge => {}) 62 | * - onReload(reloadFn) 63 | */ 64 | 65 | export function initDevTools (shell) { 66 | initApp(shell) 67 | shell.onReload(() => { 68 | if (app) { 69 | app.$destroy() 70 | } 71 | bridge.removeAllListeners() 72 | initApp(shell) 73 | }) 74 | } 75 | 76 | /** 77 | * Connect then init the app. We need to reconnect on every reload, because a 78 | * new backend will be injected. 79 | * 80 | * @param {Object} shell 81 | */ 82 | 83 | function initApp (shell) { 84 | shell.connect(bridge => { 85 | window.bridge = bridge 86 | 87 | if (Vue.prototype.hasOwnProperty('$shared')) { 88 | destroySharedData() 89 | } else { 90 | Object.defineProperty(Vue.prototype, '$shared', { 91 | get: () => SharedData 92 | }) 93 | } 94 | 95 | initSharedData({ 96 | bridge, 97 | Vue, 98 | storage, 99 | persist: [ 100 | 'classifyComponents' 101 | ] 102 | }) 103 | 104 | bridge.once('ready', version => { 105 | store.commit( 106 | 'SHOW_MESSAGE', 107 | 'Ready. Detected Livewire ' + version + '.' 108 | ) 109 | bridge.send('events:toggle-recording', store.state.events.enabled) 110 | 111 | if (isChrome) { 112 | chrome.runtime.sendMessage('vue-panel-load') 113 | } 114 | }) 115 | 116 | bridge.once('proxy-fail', () => { 117 | store.commit( 118 | 'SHOW_MESSAGE', 119 | 'Proxy injection failed.' 120 | ) 121 | }) 122 | 123 | bridge.on('flush', payload => { 124 | store.commit('components/FLUSH', parse(payload)) 125 | }) 126 | 127 | bridge.on('instance-details', details => { 128 | store.commit('components/RECEIVE_INSTANCE_DETAILS', parse(details)) 129 | }) 130 | 131 | bridge.on('toggle-instance', payload => { 132 | store.commit('components/TOGGLE_INSTANCE', parse(payload)) 133 | }) 134 | 135 | bridge.on('vuex:init', snapshot => { 136 | console.log(snapshot); 137 | store.commit('vuex/INIT', snapshot) 138 | }) 139 | 140 | bridge.on('vuex:mutation', payload => { 141 | store.commit('vuex/RECEIVE_MUTATION', payload) 142 | }) 143 | 144 | bridge.on('event:triggered', payload => { 145 | store.commit('events/RECEIVE_EVENT', parse(payload)) 146 | if (router.currentRoute.name !== 'events') { 147 | store.commit('events/INCREASE_NEW_EVENT_COUNT') 148 | } 149 | }) 150 | 151 | bridge.on('inspect-instance', id => { 152 | ensurePaneShown(() => { 153 | inspectInstance(id) 154 | }) 155 | }) 156 | 157 | initEnv(Vue) 158 | 159 | app = new Vue({ 160 | extends: App, 161 | router, 162 | store, 163 | 164 | data: { 165 | isDark, 166 | isBeta 167 | }, 168 | 169 | watch: { 170 | isDark: { 171 | handler (value) { 172 | if (value) { 173 | document.body.classList.add('vue-ui-dark-mode') 174 | } else { 175 | document.body.classList.remove('vue-ui-dark-mode') 176 | } 177 | }, 178 | immediate: true 179 | } 180 | } 181 | }).$mount('#app') 182 | }) 183 | } 184 | 185 | function getContextMenuInstance () { 186 | bridge.send('get-context-menu-target') 187 | } 188 | 189 | function inspectInstance (id) { 190 | bridge.send('select-instance', id) 191 | router.push({ name: 'components' }) 192 | const instance = store.state.components.instancesMap[id] 193 | instance && store.dispatch('components/toggleInstance', { 194 | instance, 195 | expanded: true, 196 | parent: true 197 | }) 198 | } 199 | 200 | // Pane visibility management 201 | 202 | function ensurePaneShown (cb) { 203 | if (panelShown) { 204 | cb() 205 | } else { 206 | pendingAction = cb 207 | } 208 | } 209 | 210 | function onPanelShown () { 211 | panelShown = true 212 | if (pendingAction) { 213 | pendingAction() 214 | pendingAction = null 215 | } 216 | } 217 | 218 | function onPanelHidden () { 219 | panelShown = false 220 | } 221 | -------------------------------------------------------------------------------- /src/devtools/locales/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | App: { 3 | components: { 4 | tooltip: '[[{{keys.ctrl}}]] + [[1]] Switch to Components' 5 | }, 6 | events: { 7 | tooltip: '[[{{keys.ctrl}}]] + [[3]] Switch to Events' 8 | }, 9 | refresh: { 10 | tooltip: '[[{{keys.ctrl}}]] + [[{{keys.alt}}]] + [[R]] Force Refresh' 11 | }, 12 | vuex: { 13 | tooltip: '[[{{keys.ctrl}}]] + [[2]] Switch to State' 14 | } 15 | }, 16 | StateInspector: { 17 | dataType: { 18 | tooltip: '[[{{keys.ctrl}}]] + <>: Collapse All
[[{{keys.shift}}]] + <>: Expand All' 19 | } 20 | }, 21 | DataField: { 22 | edit: { 23 | cancel: { 24 | tooltip: '[[{{keys.esc}}]] Cancel' 25 | }, 26 | submit: { 27 | tooltip: '[[{{keys.enter}}]] Submit change' 28 | } 29 | }, 30 | contextMenu: { 31 | copyValue: 'Copy Value' 32 | }, 33 | quickEdit: { 34 | number: { 35 | tooltip: `Quick Edit

36 | [[{{keys.ctrl}}]] + <>: {{operator}}5
37 | [[{{keys.shift}}]] + <>: {{operator}}10
38 | [[{{keys.alt}}]] + <>: {{operator}}100` 39 | } 40 | } 41 | }, 42 | ComponentTree: { 43 | select: { 44 | tooltip: '[[S]] Select component in the page' 45 | }, 46 | filter: { 47 | tooltip: '[[{{keys.ctrl}}]] + [[F]] Filter components by name' 48 | } 49 | }, 50 | ComponentInstance: { 51 | consoleId: { 52 | tooltip: 'Available as {{id}} in the console.' 53 | } 54 | }, 55 | ComponentInspector: { 56 | openInEditor: { 57 | tooltip: 'Open <>{{file}} in editor' 58 | } 59 | }, 60 | EventsHistory: { 61 | filter: { 62 | tooltip: '[[{{keys.ctrl}}]] + [[F]] To filter on components, type <> <MyComponent> or just <> <mycomp.' 63 | }, 64 | clear: { 65 | tooltip: '[[{{keys.ctrl}}]] + [[{{keys.del}}]] Clear Log' 66 | }, 67 | startRecording: { 68 | tooltip: '[[R]] Start recording' 69 | }, 70 | stopRecording: { 71 | tooltip: '[[R]] Stop recording' 72 | } 73 | }, 74 | VuexHistory: { 75 | filter: { 76 | tooltip: '[[{{keys.ctrl}}]] + [[F]] Filter state' 77 | }, 78 | commitAll: { 79 | tooltip: '[[{{keys.ctrl}}]] + [[{{keys.enter}}]] Commit all' 80 | }, 81 | revertAll: { 82 | tooltip: '[[{{keys.ctrl}}]] + [[{{keys.del}}]] Revert all' 83 | }, 84 | startRecording: { 85 | tooltip: '[[R]] Start recording' 86 | }, 87 | stopRecording: { 88 | tooltip: '[[R]] Stop recording' 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/devtools/mixins/data-field-edit.js: -------------------------------------------------------------------------------- 1 | import { 2 | UNDEFINED, 3 | SPECIAL_TOKENS, 4 | parse 5 | } from 'src/util' 6 | 7 | let currentEditedField = null 8 | 9 | function numberQuickEditMod (event) { 10 | let mod = 1 11 | if (event.ctrlKey || event.metaKey) { 12 | mod *= 5 13 | } 14 | if (event.shiftKey) { 15 | mod *= 10 16 | } 17 | if (event.altKey) { 18 | mod *= 100 19 | } 20 | return mod 21 | } 22 | 23 | export default { 24 | props: { 25 | editable: { 26 | type: Boolean, 27 | default: false 28 | }, 29 | removable: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | renamable: { 34 | type: Boolean, 35 | default: false 36 | } 37 | }, 38 | 39 | data () { 40 | return { 41 | editing: false, 42 | editedValue: null, 43 | editedKey: null, 44 | addingValue: false, 45 | newField: null 46 | } 47 | }, 48 | 49 | computed: { 50 | cssClass () { 51 | return { 52 | editing: this.editing 53 | } 54 | }, 55 | 56 | isEditable () { 57 | return this.editable && 58 | !this.fieldOptions.abstract && 59 | !this.fieldOptions.readOnly && 60 | ( 61 | typeof this.field.key !== 'string' || 62 | this.field.key.charAt(0) !== '$' 63 | ) 64 | }, 65 | 66 | isValueEditable () { 67 | const type = this.valueType 68 | return this.isEditable && 69 | ( 70 | type === 'null' || 71 | type === 'literal' || 72 | type === 'string' || 73 | type === 'array' || 74 | type === 'plain-object' 75 | ) 76 | }, 77 | 78 | isSubfieldsEditable () { 79 | return this.isEditable && (this.valueType === 'array' || this.valueType === 'plain-object') 80 | }, 81 | 82 | valueValid () { 83 | try { 84 | parse(this.transformSpecialTokens(this.editedValue, false)) 85 | return true 86 | } catch (e) { 87 | return false 88 | } 89 | }, 90 | 91 | duplicateKey () { 92 | return this.parentField.value.hasOwnProperty(this.editedKey) 93 | }, 94 | 95 | keyValid () { 96 | return this.editedKey && (this.editedKey === this.field.key || !this.duplicateKey) 97 | }, 98 | 99 | editValid () { 100 | return this.valueValid && (!this.renamable || this.keyValid) 101 | }, 102 | 103 | quickEdits () { 104 | if (this.isValueEditable) { 105 | const value = this.field.value 106 | const type = typeof value 107 | if (type === 'boolean') { 108 | return [ 109 | { 110 | icon: value ? 'check_box' : 'check_box_outline_blank', 111 | newValue: !value 112 | } 113 | ] 114 | } else if (type === 'number') { 115 | return [ 116 | { 117 | icon: 'remove', 118 | class: 'big', 119 | title: this.quickEditNumberTooltip('-'), 120 | newValue: event => value - numberQuickEditMod(event) 121 | }, 122 | { 123 | icon: 'add', 124 | class: 'big', 125 | title: this.quickEditNumberTooltip('+'), 126 | newValue: event => value + numberQuickEditMod(event) 127 | } 128 | ] 129 | } 130 | } 131 | return null 132 | } 133 | }, 134 | 135 | methods: { 136 | openEdit (focusKey = false) { 137 | if (this.isValueEditable) { 138 | if (currentEditedField && currentEditedField !== this) { 139 | currentEditedField.cancelEdit() 140 | } 141 | this.editedValue = this.transformSpecialTokens(JSON.stringify(this.field.value), true) 142 | this.editedKey = this.field.key 143 | this.editing = true 144 | currentEditedField = this 145 | this.$nextTick(() => { 146 | const el = this.$refs[focusKey && this.renamable ? 'keyInput' : 'editInput'] 147 | el.focus() 148 | el.setSelectionRange(0, el.value.length) 149 | }) 150 | } 151 | }, 152 | 153 | cancelEdit () { 154 | this.editing = false 155 | this.$emit('cancel-edit') 156 | currentEditedField = null 157 | }, 158 | 159 | submitEdit () { 160 | if (this.editValid) { 161 | this.editing = false 162 | const value = this.transformSpecialTokens(this.editedValue, false) 163 | const newKey = this.editedKey !== this.field.key ? this.editedKey : undefined 164 | this.sendEdit({ value, newKey }) 165 | this.$emit('submit-edit') 166 | } 167 | }, 168 | 169 | sendEdit (args) { 170 | bridge.send('set-instance-data', { 171 | id: this.inspectedInstance.id, 172 | path: this.path, 173 | ...args 174 | }) 175 | }, 176 | 177 | transformSpecialTokens (str, display) { 178 | Object.keys(SPECIAL_TOKENS).forEach(key => { 179 | const value = JSON.stringify(SPECIAL_TOKENS[key]) 180 | let search 181 | let replace 182 | if (display) { 183 | search = value 184 | replace = key 185 | } else { 186 | search = key 187 | replace = value 188 | } 189 | str = str.replace(new RegExp(search, 'g'), replace) 190 | }) 191 | return str 192 | }, 193 | 194 | quickEdit (info, event) { 195 | let newValue 196 | if (typeof info.newValue === 'function') { 197 | newValue = info.newValue(event) 198 | } else { 199 | newValue = info.newValue 200 | } 201 | this.sendEdit({ value: JSON.stringify(newValue) }) 202 | }, 203 | 204 | removeField () { 205 | this.sendEdit({ remove: true }) 206 | }, 207 | 208 | addNewValue () { 209 | let key 210 | if (this.valueType === 'array') { 211 | key = this.field.value.length 212 | } else if (this.valueType === 'plain-object') { 213 | let i = 1 214 | while (this.field.value.hasOwnProperty(key = `prop${i}`)) i++ 215 | } 216 | this.newField = { key, value: UNDEFINED } 217 | this.expanded = true 218 | this.addingValue = true 219 | this.$nextTick(() => { 220 | this.$refs.newField.openEdit(true) 221 | }) 222 | }, 223 | 224 | containsEdition () { 225 | return currentEditedField && currentEditedField.path.indexOf(this.path) === 0 226 | }, 227 | 228 | cancelCurrentEdition () { 229 | this.containsEdition() && currentEditedField.cancelEdit() 230 | }, 231 | 232 | quickEditNumberTooltip (operator) { 233 | return this.$t('DataField.quickEdit.number.tooltip', { 234 | operator 235 | }) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/devtools/mixins/entry-list.js: -------------------------------------------------------------------------------- 1 | import { scrollIntoView } from 'src/util' 2 | 3 | export default { 4 | watch: { 5 | inspectedIndex (value) { 6 | this.$nextTick(() => { 7 | const el = value === -1 ? this.$refs.baseEntry : this.$refs.entries[value] 8 | el && scrollIntoView(this.$globalRefs.leftScroll, el, false) 9 | }) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/devtools/mixins/keyboard.js: -------------------------------------------------------------------------------- 1 | export const LEFT = 'ArrowLeft' 2 | export const UP = 'ArrowUp' 3 | export const RIGHT = 'ArrowRight' 4 | export const DOWN = 'ArrowDown' 5 | export const ENTER = 'Enter' 6 | export const DEL = 'Delete' 7 | export const BACKSPACE = 'Backspace' 8 | 9 | const activeInstances = [] 10 | 11 | function processEvent (event, type) { 12 | if ( 13 | event.target.tagName === 'INPUT' || 14 | event.target.tagName === 'TEXTAREA' 15 | ) { 16 | return 17 | } 18 | const modifiers = [] 19 | if (event.ctrlKey || event.metaKey) modifiers.push('ctrl') 20 | if (event.shiftKey) modifiers.push('shift') 21 | if (event.altKey) modifiers.push('alt') 22 | const info = { 23 | key: event.key, 24 | code: event.code, 25 | modifiers: modifiers.join('+') 26 | } 27 | let result = true 28 | activeInstances.forEach(opt => { 29 | if (opt[type]) { 30 | const r = opt[type].call(opt.vm, info) 31 | if (r === false) { 32 | result = false 33 | } 34 | } 35 | }) 36 | if (!result) { 37 | event.preventDefault() 38 | } 39 | } 40 | 41 | document.addEventListener('keydown', (event) => { 42 | processEvent(event, 'onKeyDown') 43 | }) 44 | 45 | export default function (options) { 46 | return { 47 | mounted () { 48 | activeInstances.push({ 49 | vm: this, 50 | ...options 51 | }) 52 | }, 53 | destroyed () { 54 | const i = activeInstances.findIndex( 55 | o => o.vm === this 56 | ) 57 | if (i >= 0) { 58 | activeInstances.splice(i, 1) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/devtools/plugins.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueUi, { generateHtmlIcon } from '@vue/ui' 3 | import { keys } from './env' 4 | import VI18n from './plugins/i18n' 5 | import Responsive from './plugins/responsive' 6 | import GlobalRefs from './plugins/global-refs' 7 | 8 | Vue.use(VueUi) 9 | 10 | const currentLocale = 'en' 11 | const locales = require.context('./locales') 12 | const replacers = [ 13 | { reg: //g, replace: '' }, 14 | { reg: //g, replace: '' }, 15 | { reg: /<\/(input|mono)>/g, replace: '' }, 16 | { reg: /\[\[(\S+)\]\]/g, replace: '$1' }, 17 | { reg: /<<(\S+)>>/g, replace: (match, p1) => generateHtmlIcon(p1) } 18 | ] 19 | Vue.use(VI18n, { 20 | strings: locales(`./${currentLocale}`).default, 21 | defaultValues: { 22 | keys 23 | }, 24 | replacer: text => { 25 | for (const replacer of replacers) { 26 | text = text.replace(replacer.reg, replacer.replace) 27 | } 28 | return text 29 | } 30 | }) 31 | 32 | Vue.use(Responsive, { 33 | computed: { 34 | wide () { 35 | return this.width >= 1050 36 | }, 37 | tall () { 38 | return this.height >= 350 39 | } 40 | } 41 | }) 42 | 43 | Vue.use(GlobalRefs, { 44 | refs: { 45 | leftScroll: () => document.querySelector('.left .scroll'), 46 | rightScroll: () => document.querySelector('.right .scroll') 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/devtools/plugins/global-refs.js: -------------------------------------------------------------------------------- 1 | export default { 2 | install (Vue, options) { 3 | const { refs } = options 4 | const wrapper = {} 5 | Object.keys(refs).forEach(key => { 6 | const get = refs[key] 7 | Object.defineProperty(wrapper, key, { 8 | get 9 | }) 10 | }) 11 | Vue.prototype.$globalRefs = wrapper 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/devtools/plugins/i18n.js: -------------------------------------------------------------------------------- 1 | import { get } from 'src/util' 2 | 3 | const reg = /\{\{\s*([\w_.-]+)\s*\}\}/g 4 | 5 | let strings 6 | let defaultValues 7 | let replacer 8 | 9 | export function translate (path, values = {}) { 10 | values = Object.assign({}, defaultValues, values) 11 | let text = get(strings, path) 12 | text = text.replace(reg, (substring, matched) => { 13 | const value = get(values, matched) 14 | return typeof value !== 'undefined' ? value : substring 15 | }) 16 | replacer && (text = replacer(text)) 17 | return text 18 | } 19 | 20 | export default { 21 | install (Vue, options) { 22 | strings = options.strings || {} 23 | defaultValues = options.defaultValues || {} 24 | replacer = options.replacer 25 | Vue.prototype.$t = translate 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/devtools/plugins/responsive.js: -------------------------------------------------------------------------------- 1 | export let responsive 2 | 3 | export default { 4 | install (Vue, options) { 5 | const finalOptions = Object.assign({}, { 6 | computed: {} 7 | }, options) 8 | 9 | responsive = new Vue({ 10 | data () { 11 | return { 12 | width: window.innerWidth, 13 | height: window.innerHeight 14 | } 15 | }, 16 | computed: finalOptions.computed 17 | }) 18 | 19 | Object.defineProperty(Vue.prototype, '$responsive', { 20 | get: () => responsive 21 | }) 22 | 23 | window.addEventListener('resize', () => { 24 | responsive.width = window.innerWidth 25 | responsive.height = window.innerHeight 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/devtools/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import ComponentsTab from './views/components/ComponentsTab.vue' 5 | import VuexTab from './views/vuex/VuexTab.vue' 6 | import EventsTab from './views/events/EventsTab.vue' 7 | 8 | Vue.use(VueRouter) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | redirect: { name: 'components' } 14 | }, 15 | { 16 | path: '/components', 17 | name: 'components', 18 | component: ComponentsTab 19 | }, 20 | { 21 | path: '/vuex', 22 | name: 'vuex', 23 | component: VuexTab 24 | }, 25 | { 26 | path: '/events', 27 | name: 'events', 28 | component: EventsTab 29 | }, 30 | { 31 | path: '*', 32 | redirect: '/' 33 | } 34 | ] 35 | 36 | const router = new VueRouter({ 37 | routes 38 | }) 39 | 40 | export default router 41 | -------------------------------------------------------------------------------- /src/devtools/storage.js: -------------------------------------------------------------------------------- 1 | // If the users blocks 3rd party cookies and storage, 2 | // localStorage will throw. 3 | 4 | export default { 5 | get (key) { 6 | try { 7 | return JSON.parse(localStorage.getItem(key)) 8 | } catch (e) {} 9 | }, 10 | set (key, val) { 11 | try { 12 | localStorage.setItem(key, JSON.stringify(val)) 13 | } catch (e) {} 14 | }, 15 | remove (key) { 16 | try { 17 | localStorage.removeItem(key) 18 | } catch (e) {} 19 | }, 20 | clear () { 21 | try { 22 | localStorage.clear() 23 | } catch (e) {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/devtools/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import components from 'views/components/module' 4 | import vuex from 'views/vuex/module' 5 | import events from 'views/events/module' 6 | 7 | Vue.use(Vuex) 8 | 9 | const store = new Vuex.Store({ 10 | state: { 11 | message: '', 12 | view: 'vertical' 13 | }, 14 | mutations: { 15 | SHOW_MESSAGE (state, message) { 16 | state.message = message 17 | }, 18 | SWITCH_VIEW (state, view) { 19 | state.view = view 20 | }, 21 | RECEIVE_INSTANCE_DETAILS (state, instance) { 22 | state.message = 'Instance selected: ' + instance.name 23 | } 24 | }, 25 | modules: { 26 | components, 27 | vuex, 28 | events 29 | } 30 | }) 31 | 32 | export default store 33 | 34 | if (module.hot) { 35 | module.hot.accept([ 36 | 'views/components/module', 37 | 'views/vuex/module', 38 | 'views/events/module' 39 | ], () => { 40 | try { 41 | store.hotUpdate({ 42 | modules: { 43 | components: require('views/components/module').default, 44 | vuex: require('views/vuex/module').default, 45 | events: require('views/events/module').default 46 | } 47 | }) 48 | } catch (e) { 49 | console.log(e.stack) 50 | } 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /src/devtools/transitions.styl: -------------------------------------------------------------------------------- 1 | .slide-up-enter 2 | opacity 0 3 | transform translate(0, 50%) 4 | 5 | .slide-up-leave-to 6 | opacity 0 7 | transform translate(0, -50%) 8 | 9 | .slide-down-enter, .slide-down-leave-to 10 | opacity 0 11 | transform translate(0, -20px) 12 | 13 | @keyframes rotate 14 | 0% 15 | transform rotate(0deg) 16 | 100% 17 | transform rotate(360deg) 18 | 19 | @keyframes pulse 20 | 0% 21 | opacity 1 22 | 50% 23 | opacity .2 24 | 100% 25 | opacity 1 26 | -------------------------------------------------------------------------------- /src/devtools/variables.styl: -------------------------------------------------------------------------------- 1 | // Colors 2 | $blue = #44A1FF 3 | $grey = #DDDDDD 4 | $darkerGrey = #CCC 5 | $blueishGrey = #486887 6 | $green = #4857a3 7 | $darkerGreen = #4857a3 8 | $livewireBlue = #4857a3 9 | $slate = #242424 10 | $white = #FFFFFF 11 | $orange = #FF6B00 12 | $red = #c41a16 13 | $black = #222 14 | $vividBlue = #0033cc 15 | $purple = #997fff 16 | 17 | // The min-width to give icons text... 18 | $wide = 1050px 19 | 20 | // The min-height to give the tools a little more breathing room... 21 | $tall = 350px 22 | 23 | // Theme 24 | $active-color = $livewireBlue 25 | $border-color = $grey 26 | $background-color = $white 27 | $component-color = $active-color 28 | $hover-color = #E5F2FF 29 | 30 | $dark-active-color = $active-color 31 | $dark-border-color = lighten($slate, 10%) 32 | $dark-background-color = $slate 33 | $dark-component-color = $active-color 34 | $dark-hover-color = #444 35 | -------------------------------------------------------------------------------- /src/devtools/views/components/ComponentInspector.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 125 | 126 | 132 | -------------------------------------------------------------------------------- /src/devtools/views/components/ComponentInstance.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 203 | 204 | 310 | -------------------------------------------------------------------------------- /src/devtools/views/components/ComponentTree.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 201 | 202 | 214 | -------------------------------------------------------------------------------- /src/devtools/views/components/ComponentsTab.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 52 | -------------------------------------------------------------------------------- /src/devtools/views/components/module.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const state = { 4 | selected: null, 5 | inspectedInstance: {}, 6 | instances: [], 7 | instancesMap: {}, 8 | expansionMap: {}, 9 | events: [], 10 | scrollToExpanded: null 11 | } 12 | 13 | const mutations = { 14 | FLUSH (state, payload) { 15 | let start 16 | if (process.env.NODE_ENV !== 'production') { 17 | start = window.performance.now() 18 | } 19 | 20 | // Instance ID map 21 | // + add 'parent' properties 22 | const map = {} 23 | function walk (instance) { 24 | map[instance.id] = instance 25 | if (instance.children) { 26 | instance.children.forEach(child => { 27 | child.parent = instance 28 | walk(child) 29 | }) 30 | } 31 | } 32 | payload.instances.forEach(walk) 33 | 34 | // Mutations 35 | state.instances = Object.freeze(payload.instances) 36 | state.inspectedInstance = Object.freeze(payload.inspectedInstance) 37 | state.instancesMap = Object.freeze(map) 38 | 39 | if (process.env.NODE_ENV !== 'production') { 40 | Vue.nextTick(() => { 41 | console.log(`devtools render took ${window.performance.now() - start}ms.`) 42 | }) 43 | } 44 | }, 45 | RECEIVE_INSTANCE_DETAILS (state, instance) { 46 | state.inspectedInstance = Object.freeze(instance) 47 | state.scrollToExpanded = null 48 | }, 49 | TOGGLE_INSTANCE (state, { id, expanded, scrollTo = null } = {}) { 50 | Vue.set(state.expansionMap, id, expanded) 51 | state.scrollToExpanded = scrollTo 52 | } 53 | } 54 | 55 | const actions = { 56 | toggleInstance ({ commit, dispatch, state }, { instance, expanded, recursive, parent = false } = {}) { 57 | const id = instance.id 58 | 59 | commit('TOGGLE_INSTANCE', { 60 | id, 61 | expanded, 62 | scrollTo: parent ? id : null 63 | }) 64 | 65 | if (recursive) { 66 | instance.children.forEach((child) => { 67 | dispatch('toggleInstance', { 68 | instance: child, 69 | expanded, 70 | recursive 71 | }) 72 | }) 73 | } 74 | 75 | // Expand the parents 76 | if (parent) { 77 | let i = instance 78 | while (i.parent) { 79 | i = i.parent 80 | commit('TOGGLE_INSTANCE', { 81 | id: i.id, 82 | expanded: true, 83 | scrollTo: id 84 | }) 85 | } 86 | } 87 | } 88 | } 89 | 90 | export default { 91 | namespaced: true, 92 | state, 93 | mutations, 94 | actions 95 | } 96 | -------------------------------------------------------------------------------- /src/devtools/views/events/EventInspector.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 50 | 51 | 74 | -------------------------------------------------------------------------------- /src/devtools/views/events/EventsHistory.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 167 | 168 | 206 | -------------------------------------------------------------------------------- /src/devtools/views/events/EventsTab.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | -------------------------------------------------------------------------------- /src/devtools/views/events/module.js: -------------------------------------------------------------------------------- 1 | import storage from '../../storage' 2 | import { classify } from 'src/util' 3 | import SharedData from 'src/shared-data' 4 | 5 | const ENABLED_KEY = 'EVENTS_ENABLED' 6 | const enabled = storage.get(ENABLED_KEY) 7 | 8 | const state = { 9 | enabled: enabled == null ? true : enabled, 10 | events: [], 11 | inspectedIndex: -1, 12 | newEventCount: 0, 13 | filter: '' 14 | } 15 | 16 | const mutations = { 17 | 'RECEIVE_EVENT' (state, payload) { 18 | state.events.push(payload) 19 | if (!state.filter) { 20 | state.inspectedIndex = state.events.length - 1 21 | } 22 | }, 23 | 'RESET' (state) { 24 | state.events = [] 25 | state.inspectedIndex = -1 26 | }, 27 | 'INSPECT' (state, index) { 28 | state.inspectedIndex = index 29 | }, 30 | 'RESET_NEW_EVENT_COUNT' (state) { 31 | state.newEventCount = 0 32 | }, 33 | 'INCREASE_NEW_EVENT_COUNT' (state) { 34 | state.newEventCount++ 35 | }, 36 | 'UPDATE_FILTER' (state, filter) { 37 | state.filter = filter 38 | }, 39 | 'TOGGLE' (state) { 40 | storage.set(ENABLED_KEY, state.enabled = !state.enabled) 41 | bridge.send('events:toggle-recording', state.enabled) 42 | } 43 | } 44 | 45 | const getters = { 46 | activeEvent: (state, getters) => { 47 | return getters.filteredEvents[state.inspectedIndex] 48 | }, 49 | filteredEvents: (state, getters, rootState) => { 50 | const classifyComponents = SharedData.classifyComponents 51 | let searchText = state.filter.toLowerCase() 52 | const searchComponent = /<|>/g.test(searchText) 53 | if (searchComponent) { 54 | searchText = searchText.replace(/<|>/g, '') 55 | } 56 | return state.events.filter( 57 | e => (searchComponent ? (classifyComponents ? classify(e.instanceName) : e.instanceName) : e.eventName).toLowerCase().indexOf(searchText) > -1 58 | ) 59 | } 60 | } 61 | 62 | const actions = { 63 | inspect: ({ commit, getters }, index) => { 64 | if (index < 0) index = 0 65 | if (index >= getters.filteredEvents.length) index = getters.filteredEvents.length - 1 66 | commit('INSPECT', index) 67 | } 68 | } 69 | 70 | export default { 71 | namespaced: true, 72 | state, 73 | mutations, 74 | getters, 75 | actions 76 | } 77 | -------------------------------------------------------------------------------- /src/devtools/views/vuex/VuexHistory.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 182 | 183 | 275 | -------------------------------------------------------------------------------- /src/devtools/views/vuex/VuexStateInspector.vue: -------------------------------------------------------------------------------- 1 |