Skip to content

Right click Inspect component + this.$inspect() #479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jan 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion shells/chrome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"permissions": [
"http://*/*",
"https://*/*",
"file:///*"
"file:///*",
"contextMenus"
],

"content_scripts": [
Expand Down
16 changes: 16 additions & 0 deletions shells/chrome/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,19 @@ chrome.runtime.onMessage.addListener((req, sender) => {
})
}
})

// Right-click inspect context menu entry

chrome.contextMenus.create({
id: 'vue-inspect-instance',
title: 'Inspect Vue component',
contexts: ['all']
})

chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.runtime.sendMessage({
vueContextMenu: {
id: info.menuItemId
}
})
})
9 changes: 8 additions & 1 deletion shells/chrome/src/detector.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { installToast } from 'src/backend/toast'

window.addEventListener('message', e => {
if (e.source === window && e.data.vueDetected) {
chrome.runtime.sendMessage(e.data)
Expand Down Expand Up @@ -29,8 +31,13 @@ function detect (win) {

// inject the hook
if (document instanceof HTMLDocument) {
installScript(detect)
installScript(installToast)
}

function installScript (fn) {
const script = document.createElement('script')
script.textContent = ';(' + detect.toString() + ')(window)'
script.textContent = ';(' + fn.toString() + ')(window)'
document.documentElement.appendChild(script)
script.parentNode.removeChild(script)
}
94 changes: 93 additions & 1 deletion shells/chrome/src/devtools-background.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
// Vue presence on the page. If yes, create the Vue panel; otherwise poll
// for 10 seconds.

let panelLoaded = false
let panelShown = false
let pendingAction
let created = false
let checkCount = 0

Expand All @@ -14,6 +17,8 @@ function createPanelIfHasVue () {
if (created || checkCount++ > 10) {
return
}
panelLoaded = false
panelShown = false
chrome.devtools.inspectedWindow.eval(
'!!(window.__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue)',
function (hasVue) {
Expand All @@ -24,10 +29,97 @@ function createPanelIfHasVue () {
created = true
chrome.devtools.panels.create(
'Vue', 'icons/128.png', 'devtools.html',
function (panel) {
panel => {
// panel loaded
panel.onShown.addListener(onPanelShown)
panel.onHidden.addListener(onPanelHidden)
}
)
}
)
}

// Runtime messages

chrome.runtime.onMessage.addListener(request => {
if (request === 'vue-panel-load') {
onPanelLoad()
} else if (request.vueToast) {
toast(request.vueToast.message, request.vueToast.type)
} else if (request.vueContextMenu) {
onContextMenu(request.vueContextMenu)
}
})

// Page context menu entry

function onContextMenu ({ id }) {
if (id === 'vue-inspect-instance') {
const src = `window.__VUE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__`

chrome.devtools.inspectedWindow.eval(src, function (res, err) {
if (err) {
console.log(err)
}
if (typeof res !== 'undefined' && res) {
panelAction(() => {
chrome.runtime.sendMessage('vue-get-context-menu-target')
}, 'Open Vue devtools to see component details')
} else {
pendingAction = null
toast('No Vue component was found', 'warn')
}
})
}
}

// Action that may execute immediatly
// or later when the Vue panel is ready

function panelAction (cb, message = null) {
if (created && panelLoaded && panelShown) {
cb()
} else {
pendingAction = cb
message && toast(message)
}
}

function executePendingAction () {
pendingAction && pendingAction()
pendingAction = null
}

// Execute pending action when Vue panel is ready

function onPanelLoad () {
executePendingAction()
panelLoaded = true
}

// Manage panel visibility

function onPanelShown () {
chrome.runtime.sendMessage('vue-panel-shown')
panelShown = true
panelLoaded && executePendingAction()
}

function onPanelHidden () {
chrome.runtime.sendMessage('vue-panel-hidden')
panelShown = false
}

// Toasts

function toast (message, type = 'normal') {
const src = `(function() {
__VUE_DEVTOOLS_TOAST__(\`${message}\`, '${type}');
})()`

chrome.devtools.inspectedWindow.eval(src, function (res, err) {
if (err) {
console.log(err)
}
})
}
1 change: 1 addition & 0 deletions shells/dev/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
</style>
</head>
<body>
<h1>Not Vue</h1>
<iframe id="target" name="target" src="target.html"></iframe>
<div id="container">
<div id="app"></div>
Expand Down
4 changes: 4 additions & 0 deletions shells/dev/target/Target.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<button class="remove" @click="rm">Remove</button>
<input v-model="localMsg">
<other v-for="item in items" :key="item"></other>
<button @click="inspect">Inspect component</button>
</div>
</template>

Expand Down Expand Up @@ -48,6 +49,9 @@ export default {
},
rm () {
this.items.pop()
},
inspect () {
this.$inspect()
}
}
}
Expand Down
18 changes: 10 additions & 8 deletions shells/dev/target/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ new Vue({

// custom element instance
const ce = document.querySelector('#shadow')
const shadowRoot = ce.attachShadow({ mode: 'open' })
if (ce.attachShadow) {
const shadowRoot = ce.attachShadow({ mode: 'open' })

const ceVM = new Vue({
name: 'Shadow',
render (h) {
return h('h2', 'Inside Shadow DOM!')
}
}).$mount()
const ceVM = new Vue({
name: 'Shadow',
render (h) {
return h('h2', 'Inside Shadow DOM!')
}
}).$mount()

shadowRoot.appendChild(ceVM.$el)
shadowRoot.appendChild(ceVM.$el)
}
25 changes: 25 additions & 0 deletions src/backend/hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ export function installHook (window) {

hook.once('init', Vue => {
hook.Vue = Vue

Vue.prototype.$inspect = function () {
const fn = window.__VUE_DEVTOOLS_INSPECT__
fn && fn(this)
}
})

hook.once('vuex:init', store => {
Expand All @@ -77,4 +82,24 @@ export function installHook (window) {
return hook
}
})

// Start recording context menu when Vue is detected
// event if Vue devtools are not loaded yet
document.addEventListener('contextmenu', event => {
let el = event.target
if (el) {
// Search for parent that "is" a component instance
while (!el.__vue__ && el.parentElement) {
el = el.parentElement
}
const instance = el.__vue__
if (instance) {
window.__VUE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = true
window.__VUE_DEVTOOLS_CONTEXT_MENU_TARGET__ = instance
return
}
}
window.__VUE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = null
window.__VUE_DEVTOOLS_CONTEXT_MENU_TARGET__ = null
})
}
34 changes: 33 additions & 1 deletion src/backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function connect () {
const instance = instanceMap.get(id)
if (instance) {
scrollIntoView(instance)
highlight(instance)
}
bindToConsole(instance)
flush()
Expand All @@ -80,6 +79,23 @@ function connect () {
bridge.on('enter-instance', id => highlight(instanceMap.get(id)))
bridge.on('leave-instance', unHighlight)

// Get the instance id that is targeted by context menu
bridge.on('get-context-menu-target', () => {
const instance = window.__VUE_DEVTOOLS_CONTEXT_MENU_TARGET__

window.__VUE_DEVTOOLS_CONTEXT_MENU_TARGET__ = null
window.__VUE_DEVTOOLS_CONTEXT_MENU_HAS_TARGET__ = false

if (instance) {
const id = instance.__VUE_DEVTOOLS_UID__
if (id) {
return bridge.send('inspect-instance', id)
}
}

toast('No Vue component was found', 'warn')
})

// vuex
if (hook.store) {
initVuexBackend(hook, bridge)
Expand All @@ -92,6 +108,8 @@ function connect () {
// events
initEventsBackend(hook.Vue, bridge)

window.__VUE_DEVTOOLS_INSPECT__ = inspectInstance

bridge.log('backend ready.')
bridge.send('ready', hook.Vue.version)
console.log(
Expand Down Expand Up @@ -621,3 +639,17 @@ function getUniqueId (instance) {
const rootVueId = instance.$root.__VUE_DEVTOOLS_ROOT_UID__
return `${rootVueId}:${instance._uid}`
}

/**
* Display a toast message.
* @param {any} message HTML content
*/
export function toast (message, type = 'normal') {
const fn = window.__VUE_DEVTOOLS_TOAST__
fn && fn(message, type)
}

export function inspectInstance (instance) {
const id = instance.__VUE_DEVTOOLS_UID__
id && bridge.send('inspect-instance', id)
}
65 changes: 65 additions & 0 deletions src/backend/toast.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export function installToast (window) {
let toastEl = null
let toastTimer = 0

const colors = {
normal: '#3BA776',
warn: '#DB6B00',
error: '#DB2600'
}

window.__VUE_DEVTOOLS_TOAST__ = (message, type) => {
const color = colors[type] || colors.normal
console.log(`%c vue-devtools %c ${message} %c `,
'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
`background: ${color}; padding: 1px; border-radius: 0 3px 3px 0; color: #fff`,
'background:transparent')
if (!toastEl) {
toastEl = document.createElement('div')
toastEl.addEventListener('click', removeToast)
toastEl.innerHTML = `
<div id="vue-devtools-toast" style="
position: fixed;
bottom: 6px;
left: 0;
right: 0;
height: 0;
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 999999999999999999999;
font-family: monospace;
font-size: 14px;
">
<div class="vue-wrapper" style="
padding: 6px 12px;
background: ${color};
color: white;
border-radius: 3px;
flex: auto 0 0;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
cursor: pointer;
">
<div class="vue-content"></div>
</div>
</div>
`
document.body.appendChild(toastEl)
} else {
toastEl.querySelector('.vue-wrapper').style.background = color
}

toastEl.querySelector('.vue-content').innerText = message

clearTimeout(toastTimer)
toastTimer = setTimeout(removeToast, 5000)
}

function removeToast () {
clearTimeout(toastTimer)
if (toastEl) {
document.body.removeChild(toastEl)
toastEl = null
}
}
}
3 changes: 3 additions & 0 deletions src/devtools/global.styl
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,6 @@ $arrow-color = #444
text-align center
padding 0.5em
margin 0 auto

.scroll
position relative
Loading