import { isBrowser } from '@vue-devtools/shared-utils' import type { BackendContext, DevtoolsBackend } from '@vue-devtools/app-backend-api' import type { ComponentBounds, ComponentInstance } from '@vue/devtools-api' import { JobQueue } from './util/queue' let overlay: HTMLDivElement let overlayContent: HTMLDivElement let currentInstance function createOverlay() { if (overlay || !isBrowser) { return } overlay = document.createElement('div') overlay.style.backgroundColor = 'rgba(65, 184, 131, 0.35)' overlay.style.position = 'fixed' overlay.style.zIndex = '99999999999998' overlay.style.pointerEvents = 'none' overlay.style.borderRadius = '3px' overlayContent = document.createElement('div') overlayContent.style.position = 'fixed' overlayContent.style.zIndex = '99999999999999' overlayContent.style.pointerEvents = 'none' overlayContent.style.backgroundColor = 'white' overlayContent.style.fontFamily = 'monospace' overlayContent.style.fontSize = '11px' overlayContent.style.padding = '4px 8px' overlayContent.style.borderRadius = '3px' overlayContent.style.color = '#333' overlayContent.style.textAlign = 'center' overlayContent.style.border = 'rgba(65, 184, 131, 0.5) 1px solid' overlayContent.style.backgroundClip = 'padding-box' } // Use a job queue to preserve highlight/unhighlight calls order // This prevents "sticky" highlights that are not removed because highlight is async const jobQueue = new JobQueue() export async function highlight(instance: ComponentInstance, backend: DevtoolsBackend, ctx: BackendContext) { await jobQueue.queue('highlight', async () => { if (!instance) { return } const bounds = await backend.api.getComponentBounds(instance) if (bounds) { createOverlay() // Name const name = (await backend.api.getComponentName(instance)) || 'Anonymous' const pre = document.createElement('span') pre.style.opacity = '0.6' pre.textContent = '<' const text = document.createElement('span') text.style.fontWeight = 'bold' text.style.color = '#09ab56' text.textContent = name const post = document.createElement('span') post.style.opacity = '0.6' post.textContent = '>' // Size const size = document.createElement('span') size.style.opacity = '0.5' size.style.marginLeft = '6px' size.appendChild(document.createTextNode((Math.round(bounds.width * 100) / 100).toString())) const multiply = document.createElement('span') multiply.style.marginLeft = multiply.style.marginRight = '2px' multiply.textContent = '×' size.appendChild(multiply) size.appendChild(document.createTextNode((Math.round(bounds.height * 100) / 100).toString())) currentInstance = instance await showOverlay(bounds, [pre, text, post, size]) } startUpdateTimer(backend, ctx) }) } export async function unHighlight() { await jobQueue.queue('unHighlight', async () => { overlay?.parentNode?.removeChild(overlay) overlayContent?.parentNode?.removeChild(overlayContent) currentInstance = null stopUpdateTimer() }) } function showOverlay(bounds: ComponentBounds, children: Node[] = null) { if (!isBrowser || !children.length) { return } positionOverlay(bounds) document.body.appendChild(overlay) overlayContent.innerHTML = '' children.forEach(child => overlayContent.appendChild(child)) document.body.appendChild(overlayContent) positionOverlayContent(bounds) } function positionOverlay({ width = 0, height = 0, top = 0, left = 0 }) { overlay.style.width = `${Math.round(width)}px` overlay.style.height = `${Math.round(height)}px` overlay.style.left = `${Math.round(left)}px` overlay.style.top = `${Math.round(top)}px` } function positionOverlayContent({ height = 0, top = 0, left = 0 }) { // Content position (prevents overflow) const contentWidth = overlayContent.offsetWidth const contentHeight = overlayContent.offsetHeight let contentLeft = left if (contentLeft < 0) { contentLeft = 0 } else if (contentLeft + contentWidth > window.innerWidth) { contentLeft = window.innerWidth - contentWidth } let contentTop = top - contentHeight - 2 if (contentTop < 0) { contentTop = top + height + 2 } if (contentTop < 0) { contentTop = 0 } else if (contentTop + contentHeight > window.innerHeight) { contentTop = window.innerHeight - contentHeight } overlayContent.style.left = `${~~contentLeft}px` overlayContent.style.top = `${~~contentTop}px` } async function updateOverlay(backend: DevtoolsBackend, _ctx: BackendContext) { if (currentInstance) { const bounds = await backend.api.getComponentBounds(currentInstance) if (bounds) { const sizeEl = overlayContent.children.item(3) const widthEl = sizeEl.childNodes[0] as unknown as Text widthEl.textContent = (Math.round(bounds.width * 100) / 100).toString() const heightEl = sizeEl.childNodes[2] as unknown as Text heightEl.textContent = (Math.round(bounds.height * 100) / 100).toString() positionOverlay(bounds) positionOverlayContent(bounds) } } } let updateTimer function startUpdateTimer(backend: DevtoolsBackend, ctx: BackendContext) { stopUpdateTimer() updateTimer = setInterval(() => { jobQueue.queue('updateOverlay', async () => { await updateOverlay(backend, ctx) }) }, 1000 / 30) // 30fps } function stopUpdateTimer() { clearInterval(updateTimer) }