├── .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 |
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 |
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 |
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 |
2 |
3 |
{{ count }}
4 |
+1
5 |
-1
6 |
7 |
Your counter is {{ $store.getters.isPositive ? 'positive' : 'negative' }}
8 |
9 |
10 |
11 |
37 |
--------------------------------------------------------------------------------
/shells/dev/target/EventChild.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Emit
4 | Emit
5 | Emit
6 |
7 |
8 |
9 |
42 |
--------------------------------------------------------------------------------
/shells/dev/target/EventChild1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Emit
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/shells/dev/target/EventChildCond.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Emit from cond
4 |
5 |
6 |
7 |
28 |
--------------------------------------------------------------------------------
/shells/dev/target/Events.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Events
4 |
5 |
6 |
7 | Toggle Cond
8 |
9 |
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 |
2 |
3 |
Date: {{ date.toString() }} - Hours: {{ hours }} - Prototype: {{ date | prototypeString }}
4 |
5 |
6 | Update Date
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Vuex mutation
15 | Create large array
16 |
17 |
18 |
Set
19 |
{{ setDisplay() }}
20 |
21 |
Map
22 |
{{ mapDisplay() }}
23 |
24 |
25 | Vuex Set
26 | Vuex Map
27 | Refresh
28 |
29 |
30 |
31 |
32 |
164 |
--------------------------------------------------------------------------------
/shells/dev/target/Other.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Other {{ id }}
4 |
5 |
6 |
7 |
8 |
59 |
60 |
64 |
--------------------------------------------------------------------------------
/shells/dev/target/Page1.vue:
--------------------------------------------------------------------------------
1 |
2 | Page 1
3 |
4 |
--------------------------------------------------------------------------------
/shells/dev/target/Page2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Page 2
4 |
{{ $route.params.id }}
5 |
6 |
7 |
--------------------------------------------------------------------------------
/shells/dev/target/Router.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Router
4 |
5 | Page 1
6 | Page 2
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
23 |
24 |
--------------------------------------------------------------------------------
/shells/dev/target/Target.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{localMsg}}
4 |
Regex: {{regex.toString()}}
5 |
6 |
(Press enter to set)
7 |
8 |
Add
9 |
Remove
10 |
11 |
12 |
13 | Inspect component
19 | Mouse over
20 |
21 |
22 |
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 |
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 |
2 |
5 |
6 |
7 |
99 |
--------------------------------------------------------------------------------
/src/devtools/components/ScrollPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
48 |
49 |
76 |
--------------------------------------------------------------------------------
/src/devtools/components/SplitPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
26 |
27 |
28 |
106 |
107 |
161 |
--------------------------------------------------------------------------------
/src/devtools/components/StateInspector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
13 |
17 | {{ toDisplayType(dataType) }}
18 |
19 |
23 |
24 |
32 |
33 |
34 |
42 |
43 |
44 |
45 |
46 |
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 |
2 |
3 |
7 |
8 | <
9 | {{ targetName }}
10 | >
11 |
12 |
13 |
14 |
18 |
19 |
25 |
26 | Inspect DOM
27 |
28 |
34 |
35 | Open in editor
36 |
37 |
38 |
39 |
43 | Select a component instance to inspect.
44 |
45 |
49 |
This instance has no reactive state.
50 |
51 |
57 |
58 |
59 |
60 |
61 |
125 |
126 |
132 |
--------------------------------------------------------------------------------
/src/devtools/views/components/ComponentInstance.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
19 |
20 |
21 |
22 |
27 |
31 |
32 |
33 | <
34 |
35 | {{ displayName }}
36 |
37 |
41 | key ={{ instance.renderKey }}
42 |
43 |
44 | >
45 |
46 |
51 | = {{ instance.consoleId }}
52 |
53 |
57 | router-view{{ instance.matchedRouteSegment ? ': ' + instance.matchedRouteSegment : null }}
58 |
59 |
63 | fragment
64 |
65 |
69 | inactive
70 |
71 |
72 |
73 |
74 |
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
203 |
204 |
310 |
--------------------------------------------------------------------------------
/src/devtools/views/components/ComponentTree.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
14 |
15 |
21 |
22 | Select
23 |
24 |
30 |
31 | Format
32 |
33 |
34 |
38 |
45 |
46 |
47 |
48 |
49 |
201 |
202 |
214 |
--------------------------------------------------------------------------------
/src/devtools/views/components/ComponentsTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
12 |
13 |
14 |
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 |
2 |
3 |
7 |
8 |
9 |
14 | No event selected
15 |
16 |
17 |
18 |
19 |
50 |
51 |
74 |
--------------------------------------------------------------------------------
/src/devtools/views/events/EventsHistory.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
14 |
15 |
21 |
25 | Clear
26 |
27 |
32 |
37 | {{ enabled ? 'Recording' : 'Paused' }}
38 |
39 |
40 |
44 |
48 | No events found (Recording is paused)
49 |
50 |
51 |
59 | {{ event.eventName }}
60 | {{ event.type }}
61 |
62 | by
63 | <
64 | {{ displayComponentName(event.instanceName) }}
65 | >
66 |
67 | {{ event.timestamp | formatTime }}
68 |
69 |
70 |
71 |
72 |
73 |
74 |
167 |
168 |
206 |
--------------------------------------------------------------------------------
/src/devtools/views/events/EventsTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 |
8 |
9 |
15 |
16 |
21 |
26 | {{ enabled ? 'Recording' : 'Paused' }}
27 |
28 |
29 |
33 |
41 |
{{ entry.mutation.type }}
42 |
43 |
49 |
53 | Time Travel
54 |
55 |
56 |
60 | {{ entry.timestamp | formatTime }}
61 |
62 |
active
66 |
inspected
70 |
71 |
72 |
73 |
74 |
75 |
182 |
183 |
275 |
--------------------------------------------------------------------------------
/src/devtools/views/vuex/VuexStateInspector.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
16 |
17 | Export
18 |
19 |
23 | (Copied to clipboard!)
24 |
25 |
26 |
27 |
32 |
33 | Import
34 |
35 |
36 |
40 |
45 |
49 | INVALID JSON!
50 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
63 |
173 |
174 |
227 |
--------------------------------------------------------------------------------
/src/devtools/views/vuex/VuexTab.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
28 |
29 |
37 |
--------------------------------------------------------------------------------
/src/devtools/views/vuex/actions.js:
--------------------------------------------------------------------------------
1 | import { parse, stringify } from 'src/util'
2 |
3 | export function commitAll ({ commit, state }) {
4 | if (state.history.length > 0) {
5 | commit('COMMIT_ALL')
6 | travelTo(state, commit, -1)
7 | }
8 | }
9 |
10 | export function revertAll ({ commit, state }) {
11 | if (state.history.length > 0) {
12 | commit('REVERT_ALL')
13 | travelTo(state, commit, -1)
14 | }
15 | }
16 |
17 | export function commit ({ commit, state }, entry) {
18 | const index = state.history.indexOf(entry)
19 | if (index > -1) {
20 | commit('COMMIT', index)
21 | travelTo(state, commit, -1)
22 | }
23 | }
24 |
25 | export function revert ({ commit, state }, entry) {
26 | const index = state.history.indexOf(entry)
27 | if (index > -1) {
28 | commit('REVERT', index)
29 | travelTo(state, commit, state.history.length - 1)
30 | }
31 | }
32 |
33 | export function inspect ({ commit, getters }, entryOrIndex) {
34 | let index = typeof entryOrIndex === 'number'
35 | ? entryOrIndex
36 | : getters.filteredHistory.indexOf(entryOrIndex)
37 | if (index < -1) index = -1
38 | if (index >= getters.filteredHistory.length) index = getters.filteredHistory.length - 1
39 | commit('INSPECT', index)
40 | }
41 |
42 | export function timeTravelTo ({ state, commit }, entry) {
43 | travelTo(state, commit, state.history.indexOf(entry))
44 | }
45 |
46 | export function toggleRecording ({ state, commit }) {
47 | commit('TOGGLE')
48 | bridge.send('vuex:toggle-recording', state.enabled)
49 | }
50 |
51 | export function updateFilter ({ commit }, filter) {
52 | commit('UPDATE_FILTER', filter)
53 | }
54 |
55 | function travelTo (state, commit, index) {
56 | const { history, base, inspectedIndex } = state
57 | const targetSnapshot = index > -1 ? history[index].snapshot : base
58 |
59 | bridge.send('vuex:travel-to-state', stringify({
60 | state: parse(targetSnapshot).state,
61 | component: history[index].component
62 | }))
63 | if (index !== inspectedIndex) {
64 | commit('INSPECT', index)
65 | }
66 | commit('TIME_TRAVEL', index)
67 | }
68 |
--------------------------------------------------------------------------------
/src/devtools/views/vuex/module.js:
--------------------------------------------------------------------------------
1 | import { parse } from 'src/util'
2 | import * as actions from './actions'
3 | import storage from '../../storage'
4 |
5 | const REGEX_RE = /^\/(.*?)\/(\w*)/
6 | const ANY_RE = new RegExp('.*', 'i')
7 | const ENABLED_KEY = 'VUEX_ENABLED'
8 | const enabled = storage.get(ENABLED_KEY)
9 |
10 | const state = {
11 | enabled: enabled == null ? true : enabled,
12 | hasVuex: true,
13 | initial: null,
14 | base: null, // type Snapshot = { state: {}, getters: {} }
15 | inspectedIndex: -1,
16 | activeIndex: -1,
17 | history: [/* { mutation, timestamp, snapshot } */],
18 | initialCommit: Date.now(),
19 | lastCommit: Date.now(),
20 | filter: '',
21 | filterRegex: ANY_RE,
22 | filterRegexInvalid: false
23 | }
24 |
25 | const mutations = {
26 | 'INIT' (state, snapshot) {
27 | state.initial = state.base = snapshot
28 | state.hasVuex = true
29 | reset(state)
30 | },
31 | 'RECEIVE_MUTATION' (state, entry) {
32 | const historyWithChecksum = state.history.filter(history => {
33 | return history.checksum === entry.checksum;
34 | })
35 |
36 | if (historyWithChecksum.length > 0) {
37 | return;
38 | }
39 |
40 | state.history.push(entry)
41 | if (!state.filter) {
42 | state.inspectedIndex = state.activeIndex = state.history.length - 1
43 | }
44 | },
45 | 'COMMIT_ALL' (state) {
46 | state.base = state.history[state.history.length - 1].snapshot
47 | state.lastCommit = Date.now()
48 | reset(state)
49 | },
50 | 'REVERT_ALL' (state) {
51 | reset(state)
52 | },
53 | 'COMMIT' (state, index) {
54 | state.base = state.history[index].snapshot
55 | state.lastCommit = Date.now()
56 | state.history = state.history.slice(index + 1)
57 | state.inspectedIndex = -1
58 | },
59 | 'REVERT' (state, index) {
60 | state.history = state.history.slice(0, index)
61 | state.inspectedIndex = state.history.length - 1
62 | },
63 | 'INSPECT' (state, index) {
64 | state.inspectedIndex = index
65 | },
66 | 'TIME_TRAVEL' (state, index) {
67 | state.activeIndex = index
68 | },
69 | 'TOGGLE' (state) {
70 | storage.set(ENABLED_KEY, state.enabled = !state.enabled)
71 | },
72 | 'UPDATE_FILTER' (state, filter) {
73 | state.filter = filter
74 | const regexParts = filter.match(REGEX_RE)
75 | if (regexParts !== null) {
76 | // looks like it might be a regex -> try to compile it
77 | try {
78 | state.filterRegexInvalid = false
79 | state.filterRegex = new RegExp(regexParts[1], regexParts[2])
80 | } catch (e) {
81 | state.filterRegexInvalid = true
82 | state.filterRegex = ANY_RE
83 | }
84 | } else {
85 | // simple case-insensitve search
86 | state.filterRegexInvalid = false
87 | state.filterRegex = new RegExp(escapeStringForRegExp(filter), 'i')
88 | }
89 | }
90 | }
91 |
92 | function reset (state) {
93 | state.history = []
94 | state.inspectedIndex = state.activeIndex = -1
95 | }
96 |
97 | function escapeStringForRegExp (str) {
98 | return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
99 | }
100 |
101 | const getters = {
102 | inspectedState ({ base, inspectedIndex }, getters) {
103 | const entry = getters.filteredHistory[inspectedIndex]
104 | const res = {}
105 |
106 | if (entry) {
107 | res.mutation = {
108 | type: entry.mutation.type,
109 | payload: entry.mutation.payload ? parse(entry.mutation.payload) : undefined
110 | }
111 | }
112 |
113 | const snapshot = parse(entry ? entry.snapshot : base)
114 | if (snapshot) {
115 | res.state = snapshot.state
116 | res.getters = snapshot.getters
117 | }
118 |
119 | return res
120 | },
121 |
122 | filteredHistory ({ history, filterRegex }) {
123 | return history.filter(entry => filterRegex.test(entry.mutation.type))
124 | }
125 | }
126 |
127 | export default {
128 | namespaced: true,
129 | state,
130 | mutations,
131 | actions,
132 | getters
133 | }
134 |
--------------------------------------------------------------------------------
/src/shared-data.js:
--------------------------------------------------------------------------------
1 | // Initial state
2 | const internalSharedData = {
3 | openInEditorHost: '/',
4 | classifyComponents: true
5 | }
6 |
7 | // ---- INTERNALS ---- //
8 |
9 | let Vue
10 | let bridge
11 | // Storage API
12 | let storage = null
13 | // List of fields to persist to storage (disabled if 'false')
14 | // This should be unique to each shared data client to prevent conflicts
15 | let persist = false
16 | // For reactivity, we wrap the data in a Vue instance
17 | let vm
18 |
19 | export function init (params) {
20 | // Mandatory params
21 | bridge = params.bridge
22 | Vue = params.Vue
23 |
24 | if (params.hasOwnProperty('persist')) {
25 | persist = params.persist
26 |
27 | if (!params.hasOwnProperty('storage')) {
28 | throw new Error('Missing `storage` params.')
29 | }
30 | storage = params.storage
31 | }
32 |
33 | // Load persisted fields
34 | if (persist) {
35 | persist.forEach(key => {
36 | const value = storage.get(`shared-data:${key}`)
37 | if (value !== null) {
38 | internalSharedData[key] = value
39 | // Send to other shared data clients
40 | sendValue(key, value)
41 | }
42 | })
43 | }
44 |
45 | // Wrapper Vue instance
46 | vm = new Vue({
47 | data: internalSharedData
48 | })
49 |
50 | // Update value from other shared data clients
51 | bridge.on('shared-data:set', ({ key, value }) => {
52 | setValue(key, value)
53 | })
54 | }
55 |
56 | export function destroy () {
57 | bridge.removeAllListeners('shared-data:set')
58 | vm.$destroy()
59 | }
60 |
61 | function setValue (key, value) {
62 | // Storage
63 | if (persist && persist.includes(key)) {
64 | storage.set(`shared-data:${key}`, value)
65 | }
66 | vm[key] = value
67 | // Validate Proxy set trap
68 | return true
69 | }
70 |
71 | function sendValue (key, value) {
72 | bridge && bridge.send('shared-data:set', {
73 | key,
74 | value
75 | })
76 | }
77 |
78 | const proxy = {}
79 | Object.keys(internalSharedData).forEach(key => {
80 | Object.defineProperty(proxy, key, {
81 | configurable: false,
82 | get: () => vm && vm.$data[key],
83 | set: (value) => {
84 | sendValue(key, value)
85 | setValue(key, value)
86 | }
87 | })
88 | })
89 |
90 | export default proxy
91 |
--------------------------------------------------------------------------------
/src/transfer.js:
--------------------------------------------------------------------------------
1 | function encode (data, replacer, list, seen) {
2 | var stored, key, value, i, l
3 | var seenIndex = seen.get(data)
4 | if (seenIndex != null) {
5 | return seenIndex
6 | }
7 | var index = list.length
8 | var proto = Object.prototype.toString.call(data)
9 | if (proto === '[object Object]') {
10 | stored = {}
11 | seen.set(data, index)
12 | list.push(stored)
13 | var keys = Object.keys(data)
14 | for (i = 0, l = keys.length; i < l; i++) {
15 | key = keys[i]
16 | value = data[key]
17 | if (replacer) value = replacer.call(data, key, value)
18 | stored[key] = encode(value, replacer, list, seen)
19 | }
20 | } else if (proto === '[object Array]') {
21 | stored = []
22 | seen.set(data, index)
23 | list.push(stored)
24 | for (i = 0, l = data.length; i < l; i++) {
25 | value = data[i]
26 | if (replacer) value = replacer.call(data, i, value)
27 | stored[i] = encode(value, replacer, list, seen)
28 | }
29 | } else {
30 | list.push(data)
31 | }
32 | return index
33 | }
34 |
35 | function decode (list, reviver) {
36 | var i = list.length
37 | var j, k, data, key, value, proto
38 | while (i--) {
39 | data = list[i]
40 | proto = Object.prototype.toString.call(data)
41 | if (proto === '[object Object]') {
42 | var keys = Object.keys(data)
43 | for (j = 0, k = keys.length; j < k; j++) {
44 | key = keys[j]
45 | value = list[data[key]]
46 | if (reviver) value = reviver.call(data, key, value)
47 | data[key] = value
48 | }
49 | } else if (proto === '[object Array]') {
50 | for (j = 0, k = data.length; j < k; j++) {
51 | value = list[data[j]]
52 | if (reviver) value = reviver.call(data, j, value)
53 | data[j] = value
54 | }
55 | }
56 | }
57 | }
58 |
59 | export function stringify (data, replacer, space) {
60 | try {
61 | return arguments.length === 1
62 | ? JSON.stringify(data)
63 | : JSON.stringify(data, replacer, space)
64 | } catch (e) {
65 | return stringifyStrict(data, replacer, space)
66 | }
67 | }
68 |
69 | export function parse (data, reviver) {
70 | var hasCircular = /^\s/.test(data)
71 | if (!hasCircular) {
72 | return arguments.length === 1
73 | ? JSON.parse(data)
74 | : JSON.parse(data, reviver)
75 | } else {
76 | var list = JSON.parse(data)
77 | decode(list, reviver)
78 | return list[0]
79 | }
80 | }
81 |
82 | export function stringifyStrict (data, replacer, space) {
83 | var list = []
84 | encode(data, replacer, list, new Map())
85 | return space
86 | ? ' ' + JSON.stringify(list, null, space)
87 | : ' ' + JSON.stringify(list)
88 | }
89 |
--------------------------------------------------------------------------------