Repository navigation
Expand file tree
/
Copy pathdevtools_panel.js
More file actions
156 lines (133 loc) · 4.9 KB
/
Copy pathdevtools_panel.js
File metadata and controls
156 lines (133 loc) · 4.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
const saveBtn = document.getElementById("saveBtn");
const status = document.getElementById("status");
function isContextInvalidatedError(err) {
const message = err?.message || String(err || "");
return message.includes("Extension context invalidated");
}
function buildSnapshotFromHtml(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const snapshot = [];
doc.querySelectorAll("*").forEach(el => {
const { selector, index } = buildSelectorWithIndexInDoc(el, doc);
snapshot.push({
selector,
index,
outerHTML: el.outerHTML,
styleAttr: el.getAttribute("style") || ""
});
});
return snapshot;
}
function buildSelectorWithIndexInDoc(el, doc) {
const id = el.getAttribute("id");
if (id) return { selector: `#${CSS.escape(id)}`, index: 0 };
const parts = [];
let current = el;
while (current && current.tagName && current !== doc.documentElement) {
let part = current.tagName.toLowerCase();
const classAttr = current.getAttribute("class") || "";
const classes = classAttr.trim().split(/\s+/).filter(Boolean);
if (classes.length) part += "." + classes.map(c => CSS.escape(c)).join(".");
parts.unshift(part);
current = current.parentElement;
}
const selector = parts.length ? parts.join(" > ") : "html";
const matches = doc.querySelectorAll(selector);
let index = 0;
for (let i = 0; i < matches.length; i++) {
if (matches[i] === el) {
index = i;
break;
}
}
return { selector, index };
}
saveBtn.addEventListener("click", async () => {
status.textContent = "Capturing edits…";
saveBtn.disabled = true;
try {
const tabId = chrome.devtools.inspectedWindow.tabId;
let response;
try {
response = await chrome.tabs.sendMessage(tabId, { action: "captureDomSnapshot" });
} catch (err) {
if (isContextInvalidatedError(err)) {
throw new Error("Extension reloaded. Close and reopen DevTools, then try again.");
}
const message = err?.message || "Unable to access this page.";
if (message.includes("Receiving end does not exist") || message.includes("Could not establish connection")) {
throw new Error("This page does not allow extension scripts (for example chrome:// pages). Open a normal website and try again.");
}
throw err;
}
let url;
let snapshot;
if (response?.snapshot) {
({ url, snapshot } = response);
} else {
// Fallback for tabs with an older content script that does not support captureDomSnapshot.
let htmlResponse;
try {
htmlResponse = await chrome.tabs.sendMessage(tabId, { action: "getPageSnapshot" });
} catch (err) {
if (isContextInvalidatedError(err)) {
throw new Error("Extension reloaded. Close and reopen DevTools, then try again.");
}
throw err;
}
if (!htmlResponse?.html) {
throw new Error("Could not capture page snapshot. Reload the tab and try again.");
}
const tab = await chrome.tabs.get(tabId);
url = htmlResponse.url || tab.url;
snapshot = buildSnapshotFromHtml(htmlResponse.html);
}
// Load the stored "original" snapshot for this URL (if any)
const origKey = `original_${url}`;
const stored = await chrome.storage.local.get(origKey);
const original = stored[origKey] || null;
let editsToSave = [];
if (!original) {
// First time: store current as baseline — nothing to diff yet
await chrome.storage.local.set({ [origKey]: snapshot });
status.textContent = "✅ Baseline saved! Make your edits, then click Save again.";
saveBtn.disabled = false;
return;
}
// Build a map of original elements by selector+index
const origMap = {};
for (const entry of original) {
origMap[`${entry.selector}::${entry.index}`] = entry;
}
// Diff: find changed elements
for (const entry of snapshot) {
const key = `${entry.selector}::${entry.index}`;
const orig = origMap[key];
if (!orig) continue; // new element, skip for now
if (orig.outerHTML !== entry.outerHTML) {
editsToSave.push({
id: key,
selector: entry.selector,
selectorIndex: entry.index,
type: "outerHTML",
value: entry.outerHTML
});
}
}
// Save edits via background
await chrome.runtime.sendMessage({ action: "clearEdits", url });
for (const edit of editsToSave) {
await chrome.runtime.sendMessage({ action: "saveEdit", url, edit });
}
// Update baseline to current snapshot
await chrome.storage.local.set({ [origKey]: snapshot });
status.textContent = editsToSave.length
? `✅ Saved ${editsToSave.length} edit(s)!`
: "✅ No changes detected. Baseline updated.";
} catch (e) {
status.textContent = "❌ Error: " + (e?.message || "Unknown error");
console.error(e);
}
saveBtn.disabled = false;
});