Skip to content

Commit 414cca4

Browse files
committed
refactor: beautify highlight style
1 parent 6504686 commit 414cca4

File tree

10 files changed

+198
-150
lines changed

10 files changed

+198
-150
lines changed

examples/components/TodoList.vue

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<template>
22
<div class="todo-list">
33
<h2>My To-Do List</h2>
4+
<div style="margin-bottom: 10px">
5+
<a href="https://github.com/linzhe141/vue-change-marker"
6+
>how to use vue-change-marker</a
7+
>
8+
</div>
49
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="New task" />
510
<div v-for="todo in todos" :key="todo.text">
611
<TodoItem :todo="todo" @delete="deleteTodo" />

examples/vite.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default defineConfig({
2020
},
2121
{
2222
find: 'vue-change-marker',
23-
replacement: join(__dirname, '../packages/core/index.ts'),
23+
replacement: join(__dirname, '../packages/core/src/index.ts'),
2424
},
2525
],
2626
},

packages/core/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# vue-change-marker
22

3+
## 0.0.2
4+
5+
### Patch Changes
6+
7+
- refactor: beautify highlight style
8+
39
## 0.0.1
410

511
### Patch Changes

packages/core/index.ts

-146
This file was deleted.

packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vue-change-marker",
3-
"version": "0.0.1",
3+
"version": "0.0.2",
44
"description": "Highlight updated vue components",
55
"publishConfig": {
66
"access": "public"

packages/core/src/canvas.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { ComponentInternalInstance } from 'vue'
2+
3+
export let ctx: CanvasRenderingContext2D | null = null
4+
const updateCounters = new WeakMap<HTMLElement, number>()
5+
const elements = new Set<HTMLElement>()
6+
7+
export function initCanvas() {
8+
const canvas = document.createElement('canvas')
9+
canvas.id = 'vue-change-marker-canvas'
10+
canvas.style.position = 'fixed'
11+
canvas.style.top = '0'
12+
canvas.style.left = '0'
13+
canvas.style.width = '100vw'
14+
canvas.style.height = '100vh'
15+
canvas.style.pointerEvents = 'none'
16+
canvas.style.zIndex = '2147483646'
17+
canvas.setAttribute('aria-hidden', 'true')
18+
19+
const canvasCtx = canvas.getContext('2d')!
20+
ctx = canvasCtx
21+
22+
document.documentElement.appendChild(canvas)
23+
24+
const dpi = window.devicePixelRatio || 1
25+
canvasCtx.canvas.width = dpi * window.innerWidth
26+
canvasCtx.canvas.height = dpi * window.innerHeight
27+
canvas.style.width = `${window.innerWidth}px`
28+
canvas.style.height = `${window.innerHeight}px`
29+
canvasCtx.resetTransform()
30+
canvasCtx.scale(dpi, dpi)
31+
}
32+
33+
export function clearCanvas(ctx: CanvasRenderingContext2D) {
34+
ctx.save()
35+
ctx.setTransform(1, 0, 0, 1, 0, 0)
36+
const width = ctx.canvas.width
37+
const height = ctx.canvas.height
38+
ctx.clearRect(0, 0, width, height)
39+
ctx.restore()
40+
}
41+
export function resetCounters() {
42+
elements.forEach((el) => {
43+
updateCounters.set(el, 0)
44+
})
45+
}
46+
47+
export function highlight(
48+
ctx: CanvasRenderingContext2D,
49+
el: HTMLElement,
50+
updatedInstance: ComponentInternalInstance,
51+
) {
52+
clearCanvas(ctx!)
53+
54+
let updateCounter = updateCounters.get(el) || 0
55+
updateCounter++
56+
57+
updateCounters.set(el, updateCounter)
58+
elements.add(el)
59+
60+
const opacity = Math.min(0.9, updateCounter * 0.2)
61+
const rect = el.getBoundingClientRect()
62+
63+
const componentName =
64+
updatedInstance.type.__name || updatedInstance.type.__file || ''
65+
66+
ctx.save()
67+
ctx.fillStyle = `rgba(255, 0, 0, ${opacity})`
68+
ctx.fillRect(rect.x, rect.y - 40, Math.max(250, rect.width), 40)
69+
ctx.fillStyle = `white`
70+
ctx.font = '14px Arial'
71+
ctx.fillText('updated component: ' + componentName, rect.x, rect.y - 25)
72+
ctx.fillText('updated times: ' + updateCounter, rect.x, rect.y - 5)
73+
ctx.restore()
74+
75+
ctx.save()
76+
ctx.strokeStyle = `rgba(255, 0, 0, ${opacity})`
77+
ctx.lineWidth = 1
78+
ctx.strokeRect(rect.x, rect.y, rect.width, rect.height)
79+
ctx.restore()
80+
}
81+
82+
export const getRect = (domNode: Element): DOMRect | null => {
83+
const style = window.getComputedStyle(domNode)
84+
if (
85+
style.display === 'none' ||
86+
style.visibility === 'hidden' ||
87+
style.opacity === '0'
88+
) {
89+
return null
90+
}
91+
92+
const rect = domNode.getBoundingClientRect()
93+
94+
const isVisible =
95+
rect.bottom > 0 &&
96+
rect.right > 0 &&
97+
rect.top < window.innerHeight &&
98+
rect.left < window.innerWidth
99+
100+
if (!isVisible || !rect.width || !rect.height) {
101+
return null
102+
}
103+
104+
return rect
105+
}

packages/core/src/index.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type ComponentInternalInstance } from 'vue'
2+
import { getRect, initCanvas } from './canvas'
3+
import { queueflush, scheduledDom } from './scheduler'
4+
5+
export function VueChangeMarker() {
6+
initCanvas()
7+
registerDevtoolsHook()
8+
}
9+
10+
function emit(event: string, ...payload: any[]) {
11+
if (event === 'component:updated') {
12+
const updatedInstance = payload[3] as ComponentInternalInstance
13+
// 不考虑fragment
14+
const type = updatedInstance.vnode.type?.toString()
15+
if (type === 'Symbol(v-fgt)') return
16+
17+
const el = updatedInstance.vnode.el! as HTMLElement | null
18+
if (!el) return
19+
// 注释节点之类的 transition 这类组件
20+
if (!el.getBoundingClientRect) return
21+
const rect = getRect(el)
22+
if (rect) {
23+
scheduledDom.push({ dom: el, updatedInstance })
24+
}
25+
queueflush()
26+
}
27+
}
28+
29+
function registerDevtoolsHook() {
30+
if (window.__VUE_DEVTOOLS_GLOBAL_HOOK__?.id !== 'vue-change-marker') {
31+
const oldEmit = window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit
32+
window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit = function (...args: any[]) {
33+
oldEmit.call(window.__VUE_DEVTOOLS_GLOBAL_HOOK__, ...args)
34+
// @ts-expect-error
35+
emit(...args)
36+
}
37+
}
38+
if (!window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
39+
window.__VUE_DEVTOOLS_GLOBAL_HOOK__ = { id: 'vue-change-marker', emit }
40+
}
41+
}

packages/core/src/scheduler.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ComponentInternalInstance } from 'vue'
2+
import { clearCanvas, ctx, highlight, resetCounters } from './canvas'
3+
4+
let isPending = false
5+
let timer: number = 0
6+
export const scheduledDom: {
7+
dom: HTMLElement
8+
updatedInstance: ComponentInternalInstance
9+
}[] = []
10+
11+
function flush() {
12+
const item = scheduledDom[0]
13+
if (item) {
14+
const { dom, updatedInstance } = item
15+
requestAnimationFrame(() => {
16+
highlight(ctx!, dom, updatedInstance)
17+
scheduledDom.shift()
18+
if (scheduledDom.length) {
19+
requestAnimationFrame(flush)
20+
} else {
21+
isPending = false
22+
clearTimeout(timer)
23+
timer = window.setTimeout(() => {
24+
clearCanvas(ctx!)
25+
resetCounters()
26+
}, 300)
27+
}
28+
})
29+
}
30+
}
31+
32+
export const queueflush = async () => {
33+
if (!isPending) {
34+
isPending = true
35+
flush()
36+
}
37+
}

packages/core/vite.config.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { resolve } from 'path'
33
export default defineConfig({
44
build: {
55
lib: {
6-
entry: './index.ts',
6+
entry: './src/index.ts',
77
},
88
minify: false,
99
rollupOptions: {

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"forceConsistentCasingInFileNames": true,
2020
"baseUrl": ".",
2121
"paths": {
22-
"vue-change-marker": ["./packages/core/index.ts"]
22+
"vue-change-marker": ["packages/core/src/index.ts"]
2323
}
2424
}
2525
}

0 commit comments

Comments
 (0)