Skip to content

Commit 344557d

Browse files
authored
feat: add native DOM hover highlight (#18)
1 parent affb647 commit 344557d

15 files changed

Lines changed: 1019 additions & 34 deletions

‎README.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Browser DevTools is excellent at inspecting one element, but finding why two sim
2121
-**Difference-first table**: changed properties are shown by default, with a `Show all` toggle for the full computed style list.
2222
-**Property search**: filter CSS properties by name while reviewing the comparison.
2323
-**One-click copy**: click a value cell to copy `property: value;`.
24+
-**Native page hover highlight**: hover a source/target DOM line in the diff table to highlight that DOM node in the inspected page, and remove the highlight immediately when the pointer leaves.
2425
-**Cross-window/tab sync**: selected element data is broadcast to other open windows/tabs, which helps compare page states side by side.
2526
-**Localized UI**: includes English and Simplified Chinese browser i18n messages.
2627

@@ -31,15 +32,27 @@ Browser DevTools is excellent at inspecting one element, but finding why two sim
3132
3233
Download the packaged zip from [Releases](https://github.com/jevin98/css-diff-devtools/releases), then install or load it manually from your browser's extensions page.
3334

35+
## Chrome Debugger Permission
36+
37+
CSS-Diff requests Chrome's `debugger` permission so the diff table can use the Chrome DevTools Protocol `Overlay.highlightNode` and `Overlay.hideHighlight` commands. This is what makes DOM line hover highlighting appear and disappear immediately in the inspected page.
38+
39+
Chrome treats this permission as sensitive:
40+
41+
- Chrome may show a permission warning when the extension is installed or updated.
42+
- Chrome may show a browser-level notice that CSS-Diff is debugging the current page while the extension is attached.
43+
- CSS-Diff uses this permission only for temporary DOM hover highlighting from the DevTools sidebar.
44+
- If the permission is denied, CSS comparison still works, but native hover highlighting in the inspected page is unavailable.
45+
3446
## Usage
3547

3648
1. Open the page you want to inspect.
3749
2. Open DevTools and switch to the Elements panel.
3850
3. Open the `CSS-Diff` sidebar.
3951
4. Select the first DOM element, then select the second DOM element.
4052
5. Review the highlighted differences, search for a property, or enable `Show all`.
41-
6. Click a left/right value cell to copy the CSS declaration.
42-
7. Click `Clear Selection` to start another comparison.
53+
6. Hover a source/target DOM line in the diff table to highlight that DOM node in the inspected page.
54+
7. Click a left/right value cell to copy the CSS declaration.
55+
8. Click `Clear Selection` to start another comparison.
4356

4457
## Local Development
4558

‎README.zh-CN.md‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
-**差异优先展示**:默认只展示有差异的属性,也可以通过 `显示全部` 查看完整 computed style 列表。
2222
-**属性搜索**:按 CSS 属性名过滤对比结果。
2323
-**一键复制**:点击任意左右侧样式值单元格,即可复制 `property: value;`
24+
-**原生页面悬停高亮**:在差异表格中悬停源/目标 DOM 行时,会在被检查页面中高亮对应 DOM 节点,鼠标移出后立即取消高亮。
2425
-**跨窗口/标签页同步**:选中的元素数据会同步广播到其他已打开的窗口/标签页,便于并排比较不同页面状态。
2526
-**本地化界面**:内置英文和简体中文浏览器 i18n 文案。
2627

@@ -31,15 +32,27 @@
3132
3233
[Releases](https://github.com/jevin98/css-diff-devtools/releases) 下载打包后的 zip 文件,然后在浏览器扩展程序页面中手动安装或加载。
3334

35+
## Chrome Debugger 权限
36+
37+
CSS-Diff 会请求 Chrome 的 `debugger` 权限,用于通过 Chrome DevTools Protocol 的 `Overlay.highlightNode``Overlay.hideHighlight` 命令实现差异表格 DOM 行的页面高亮,并在鼠标移出时立即取消高亮。
38+
39+
Chrome 会把该权限视为敏感权限:
40+
41+
- 安装或更新扩展时,Chrome 可能显示权限警告。
42+
- 扩展附加到当前页面时,Chrome 可能显示“正在调试此浏览器/页面”的提示。
43+
- CSS-Diff 仅将该权限用于 DevTools 侧边栏中的临时 DOM 悬停高亮。
44+
- 如果未授予该权限,CSS 对比功能仍可使用,但被检查页面中的原生悬停高亮不可用。
45+
3446
## 使用方法
3547

3648
1. 打开需要检查的页面。
3749
2. 打开 DevTools,并切换到 Elements 面板。
3850
3. 打开 `CSS-Diff` 侧边栏。
3951
4. 先选择第一个 DOM 元素,再选择第二个 DOM 元素。
4052
5. 查看高亮的样式差异、搜索属性,或启用 `显示全部`
41-
6. 点击左侧或右侧的样式值单元格,复制对应 CSS 声明。
42-
7. 点击 `清除选择` 开始新的比较。
53+
6. 在差异表格中悬停源/目标 DOM 行,在被检查页面中高亮对应 DOM 节点。
54+
7. 点击左侧或右侧的样式值单元格,复制对应 CSS 声明。
55+
8. 点击 `清除选择` 开始新的比较。
4356

4457
## 本地开发
4558

‎entrypoints/devtools-panel/devtools-panel.vue‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const {
3636
handleClearSelection,
3737
handleRemoveSelectedElement,
3838
handleCopyStyle,
39+
handleInspectSelectedElement,
40+
handleRestoreInspectedElement,
3941
} =useDevToolsPanel()
4042
4143
const themePreference =ref<ThemePreference>('system')
@@ -149,6 +151,20 @@ function getStyleValueLabel(value: string) {
149151
returnvalue===UNDEFINED_STYLE_VALUE?t('undefinedStyleValue') :value
150152
}
151153
154+
function handleInspectPointerLeave(event:PointerEvent, element:SelectedElType) {
155+
const relatedTarget =event.relatedTarget
156+
157+
if (relatedTargetinstanceofElement) {
158+
const nextInspectTarget =relatedTarget.closest('[data-css-diff-inspect-id]')
159+
160+
if (nextInspectTarget?.getAttribute('data-css-diff-inspect-id')) {
161+
return
162+
}
163+
}
164+
165+
handleRestoreInspectedElement(element)
166+
}
167+
152168
onMounted(() => {
153169
initializeLocale(localStorage.getItem(LOCALE_STORAGE_KEY))
154170
systemThemeQuery=window.matchMedia(THEME_MEDIA_QUERY)
@@ -403,6 +419,10 @@ const PropertyNode = defineComponent({
403419
v-for="slot in selectionSlots"
404420
:key="slot.key"
405421
class="min-w-[280px] border-l border-border px-3 py-2 font-medium"
422+
:data-css-diff-inspect-id="slot.element?.inspectId"
423+
:title="slot.element ? t('nativeHoverTooltip') : undefined"
424+
@pointerenter="slot.element && handleInspectSelectedElement(slot.element)"
425+
@pointerleave="slot.element && handleInspectPointerLeave($event, slot.element)"
406426
>
407427
<divclass="flex items-center justify-between gap-2">
408428
<divclass="min-w-0">
@@ -493,6 +513,10 @@ const PropertyNode = defineComponent({
493513
:key="`${row.property}-${slot.key}`"
494514
class="group min-w-[280px] cursor-copy border-l border-border px-3 py-2 align-top transition-colors hover:bg-accent hover:text-accent-foreground"
495515
:class="row.isDiff ? 'bg-muted/30 font-medium text-foreground' : ''"
516+
:data-css-diff-inspect-id="slot.element?.inspectId"
517+
:title="slot.element ? t('nativeHoverTooltip') : undefined"
518+
@pointerenter="slot.element && handleInspectSelectedElement(slot.element)"
519+
@pointerleave="slot.element && handleInspectPointerLeave($event, slot.element)"
496520
@click="slot.element && handleCopyStyle(row, slot.element.valueType)"
497521
>
498522
<divclass="flex items-start justify-between gap-3">

‎entrypoints/devtools-panel/hooks/useDevToolsPanel.ts‎

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useClipboard } from '@vueuse/core'
33
import{toast}from'vue-sonner'
44
import{t}from'../lang'
55
importSMfrom'../message'
6-
import{compareStyles,formatStyle,typeFormatStyleValue,getVisibleCssDiffs,UNDEFINED_STYLE_VALUE}from'../utils'
6+
import{typeCaptureSelectedElementResult,compareStyles,createCaptureSelectedElementScript,devToolsOverlayHighlighter,getVisibleCssDiffs,UNDEFINED_STYLE_VALUE}from'../utils'
77

88
exportfunctionuseDevToolsPanel(){
99
constinputValue=ref('')
@@ -12,31 +12,26 @@ export function useDevToolsPanel() {
1212
constcssDiffs: Array<CssDiffsType>=reactive([])
1313

1414
constisAllProperty=ref(false)
15-
constisLoadComplete=ref(false)
1615

1716
onMounted(()=>{
1817
browser.devtools.panels.elements.onSelectionChanged.addListener(()=>{
19-
browser.devtools.inspectedWindow.eval(
20-
`(() => document.readyState)($0)`,
21-
(readyState: Document['readyState'])=>{
22-
if(readyState==='complete'){
23-
isLoadComplete.value=true
24-
}
25-
else{
26-
isLoadComplete.value=false
27-
}
28-
},
29-
)
18+
constvalueType=getAvailableValueType()
19+
20+
if(!valueType){
21+
return
22+
}
3023

3124
browser.devtools.inspectedWindow.eval(
32-
`(${formatStyle.toString()})($0)`,
33-
(result: FormatStyleValue,isException)=>{
34-
if(!isException&&result!=null&&isLoadComplete.value){
35-
constvalueType=getAvailableValueType()
36-
37-
if(valueType){
38-
saveSelectedEl({ ...result, valueType })
39-
}
25+
createCaptureSelectedElementScript(valueType),
26+
(payload: CaptureSelectedElementResult,isException)=>{
27+
if(!isException&&payload?.readyState==='complete'&&payload.result!=null){
28+
saveSelectedEl({
29+
...payload.result,
30+
inspectId: payload.inspectId,
31+
inspectPath: payload.inspectPath,
32+
inspectTabId: browser.devtools.inspectedWindow.tabId,
33+
valueType,
34+
})
4035
}
4136
},
4237
)
@@ -55,13 +50,18 @@ export function useDevToolsPanel() {
5550

5651
if(selectedEl.length===2){
5752
compareSelectedEl()
53+
prepareSelectedElementOverlay()
5854
}
5955
else{
6056
cssDiffs.length=0
6157
}
6258
})
6359
})
6460

61+
onUnmounted(()=>{
62+
devToolsOverlayHighlighter.detach()
63+
})
64+
6565
functionsaveSelectedEl(result: SelectedElType){
6666
if(!getAvailableValueType()){
6767
return
@@ -74,12 +74,14 @@ export function useDevToolsPanel() {
7474

7575
if(selectedEl.length===2){
7676
compareSelectedEl()
77+
prepareSelectedElementOverlay()
7778
}
7879
}
7980

8081
functionhandleClearSelection(){
8182
selectedEl.length=0
8283
cssDiffs.length=0
84+
devToolsOverlayHighlighter.detach()
8385

8486
// Send selected data to other windows/tabs
8587
SM.send([])
@@ -94,6 +96,7 @@ export function useDevToolsPanel() {
9496

9597
selectedEl.splice(index,1)
9698
cssDiffs.length=0
99+
devToolsOverlayHighlighter.detach()
97100

98101
// Send selected data to other windows/tabs
99102
SM.send(selectedEl)
@@ -148,6 +151,39 @@ export function useDevToolsPanel() {
148151
toast.success(`${t('copyInfo')} > ${source}`)
149152
}
150153

154+
functionprepareSelectedElementOverlay(){
155+
constinspectedTabId=browser.devtools.inspectedWindow.tabId
156+
constlocalElement=selectedEl.find(element=>element.inspectTabId===inspectedTabId)
157+
158+
if(!localElement){
159+
return
160+
}
161+
162+
devToolsOverlayHighlighter.prepare({
163+
inspectTabId: localElement.inspectTabId,
164+
})
165+
}
166+
167+
functionhandleInspectSelectedElement(element: SelectedElType){
168+
if(!element.inspectId){
169+
return
170+
}
171+
172+
devToolsOverlayHighlighter.highlight({
173+
inspectId: element.inspectId,
174+
inspectPath: element.inspectPath,
175+
inspectTabId: element.inspectTabId,
176+
})
177+
}
178+
179+
functionhandleRestoreInspectedElement(element: SelectedElType){
180+
if(!element.inspectId){
181+
return
182+
}
183+
184+
devToolsOverlayHighlighter.hide()
185+
}
186+
151187
return{
152188
inputValue,
153189

@@ -158,5 +194,7 @@ export function useDevToolsPanel() {
158194
handleClearSelection,
159195
handleRemoveSelectedElement,
160196
handleCopyStyle,
197+
handleInspectSelectedElement,
198+
handleRestoreInspectedElement,
161199
}
162200
}

‎entrypoints/devtools-panel/lang.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type MessageKey =
3333
|'copyInfo'
3434
|'clearFilter'
3535
|'inputPlaceholder'
36+
|'nativeHoverTooltip'
3637
|'switchToDarkTheme'
3738
|'switchToLightTheme'
3839
|'waitingSelection'

‎entrypoints/devtools-panel/types.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { FormatStyleValue } from './utils'
22

33
exporttypeSelectedElType={
44
valueType: 'left'|'right'
5+
inspectId?: string
6+
inspectPath?: string
7+
inspectTabId?: number
58
}&FormatStyleValue
69

710
exportinterfaceCssDiffsType{

0 commit comments

Comments
 (0)