mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-17 09:41:28 +08:00
feat: basic element picker in content.js for CSP-restricted pages
When inspector.js can't be injected (CSP, chrome:// pages), content.js provides a basic picker using getComputedStyle + CSSOM: - startBasicPicker/stopBasicPicker message handlers - captureBasicData() with ~30 key CSS properties, box model, matched rules - Hover highlight with outline save/restore (never leaves artifacts) - Click uses e.target directly (no re-querying by selector) - Sends inspectResult with mode:'basic' for sidebar rendering - Escape key cancels picker and restores outlines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -125,8 +125,217 @@ function renderRefPanel(refs) {
|
|||||||
container.appendChild(panel);
|
container.appendChild(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Basic Inspector Picker (CSP fallback) ──────────────────
|
||||||
|
// When inspector.js can't be injected (CSP, chrome:// pages), content.js
|
||||||
|
// provides a basic element picker using getComputedStyle + CSSOM.
|
||||||
|
|
||||||
|
let basicPickerActive = false;
|
||||||
|
let basicPickerOverlay = null;
|
||||||
|
let basicPickerLastEl = null;
|
||||||
|
let basicPickerSavedOutline = '';
|
||||||
|
|
||||||
|
const BASIC_KEY_PROPERTIES = [
|
||||||
|
'display', 'position', 'top', 'right', 'bottom', 'left',
|
||||||
|
'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height',
|
||||||
|
'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
|
||||||
|
'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
|
||||||
|
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
||||||
|
'color', 'background-color', 'background-image',
|
||||||
|
'font-family', 'font-size', 'font-weight', 'line-height',
|
||||||
|
'text-align', 'text-decoration',
|
||||||
|
'overflow', 'overflow-x', 'overflow-y',
|
||||||
|
'opacity', 'z-index',
|
||||||
|
'flex-direction', 'justify-content', 'align-items', 'flex-wrap', 'gap',
|
||||||
|
'grid-template-columns', 'grid-template-rows',
|
||||||
|
'box-shadow', 'border-radius', 'transform',
|
||||||
|
];
|
||||||
|
|
||||||
|
function captureBasicData(el) {
|
||||||
|
const computed = getComputedStyle(el);
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
|
||||||
|
const computedStyles = {};
|
||||||
|
for (const prop of BASIC_KEY_PROPERTIES) {
|
||||||
|
computedStyles[prop] = computed.getPropertyValue(prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxModel = {
|
||||||
|
content: { width: rect.width, height: rect.height },
|
||||||
|
padding: {
|
||||||
|
top: parseFloat(computed.paddingTop) || 0,
|
||||||
|
right: parseFloat(computed.paddingRight) || 0,
|
||||||
|
bottom: parseFloat(computed.paddingBottom) || 0,
|
||||||
|
left: parseFloat(computed.paddingLeft) || 0,
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
top: parseFloat(computed.borderTopWidth) || 0,
|
||||||
|
right: parseFloat(computed.borderRightWidth) || 0,
|
||||||
|
bottom: parseFloat(computed.borderBottomWidth) || 0,
|
||||||
|
left: parseFloat(computed.borderLeftWidth) || 0,
|
||||||
|
},
|
||||||
|
margin: {
|
||||||
|
top: parseFloat(computed.marginTop) || 0,
|
||||||
|
right: parseFloat(computed.marginRight) || 0,
|
||||||
|
bottom: parseFloat(computed.marginBottom) || 0,
|
||||||
|
left: parseFloat(computed.marginLeft) || 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Matched CSS rules via CSSOM (same-origin only)
|
||||||
|
const matchedRules = [];
|
||||||
|
try {
|
||||||
|
for (const sheet of document.styleSheets) {
|
||||||
|
try {
|
||||||
|
const rules = sheet.cssRules || sheet.rules;
|
||||||
|
if (!rules) continue;
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.type !== CSSRule.STYLE_RULE) continue;
|
||||||
|
try {
|
||||||
|
if (el.matches(rule.selectorText)) {
|
||||||
|
const properties = [];
|
||||||
|
for (let i = 0; i < rule.style.length; i++) {
|
||||||
|
const prop = rule.style[i];
|
||||||
|
properties.push({
|
||||||
|
name: prop,
|
||||||
|
value: rule.style.getPropertyValue(prop),
|
||||||
|
priority: rule.style.getPropertyPriority(prop),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
matchedRules.push({
|
||||||
|
selector: rule.selectorText,
|
||||||
|
properties,
|
||||||
|
source: sheet.href || 'inline',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch { /* skip rules that can't be matched */ }
|
||||||
|
}
|
||||||
|
} catch { /* cross-origin sheet — silently skip */ }
|
||||||
|
}
|
||||||
|
} catch { /* CSSOM not available */ }
|
||||||
|
|
||||||
|
return { computedStyles, boxModel, matchedRules };
|
||||||
|
}
|
||||||
|
|
||||||
|
function basicBuildSelector(el) {
|
||||||
|
if (el.id) {
|
||||||
|
const sel = '#' + CSS.escape(el.id);
|
||||||
|
try { if (document.querySelectorAll(sel).length === 1) return sel; } catch {}
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
let current = el;
|
||||||
|
while (current && current !== document.body && current !== document.documentElement) {
|
||||||
|
let part = current.tagName.toLowerCase();
|
||||||
|
if (current.id) {
|
||||||
|
parts.unshift('#' + CSS.escape(current.id));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (current.className && typeof current.className === 'string') {
|
||||||
|
const classes = current.className.trim().split(/\s+/).filter(c => c.length > 0);
|
||||||
|
if (classes.length > 0) part += '.' + classes.map(c => CSS.escape(c)).join('.');
|
||||||
|
}
|
||||||
|
const parent = current.parentElement;
|
||||||
|
if (parent) {
|
||||||
|
const siblings = Array.from(parent.children).filter(s => s.tagName === current.tagName);
|
||||||
|
if (siblings.length > 1) {
|
||||||
|
part += `:nth-child(${Array.from(parent.children).indexOf(current) + 1})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.unshift(part);
|
||||||
|
current = current.parentElement;
|
||||||
|
}
|
||||||
|
return parts.join(' > ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function basicPickerHighlight(el) {
|
||||||
|
// Restore previous element
|
||||||
|
if (basicPickerLastEl && basicPickerLastEl !== el) {
|
||||||
|
basicPickerLastEl.style.outline = basicPickerSavedOutline;
|
||||||
|
}
|
||||||
|
if (el) {
|
||||||
|
basicPickerSavedOutline = el.style.outline;
|
||||||
|
el.style.outline = '2px solid rgba(59, 130, 246, 0.6)';
|
||||||
|
basicPickerLastEl = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function basicPickerCleanup() {
|
||||||
|
if (basicPickerLastEl) {
|
||||||
|
basicPickerLastEl.style.outline = basicPickerSavedOutline;
|
||||||
|
basicPickerLastEl = null;
|
||||||
|
basicPickerSavedOutline = '';
|
||||||
|
}
|
||||||
|
basicPickerActive = false;
|
||||||
|
document.removeEventListener('mousemove', onBasicMouseMove, true);
|
||||||
|
document.removeEventListener('click', onBasicClick, true);
|
||||||
|
document.removeEventListener('keydown', onBasicKeydown, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBasicMouseMove(e) {
|
||||||
|
if (!basicPickerActive) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
if (el && el !== basicPickerLastEl) {
|
||||||
|
basicPickerHighlight(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBasicClick(e) {
|
||||||
|
if (!basicPickerActive) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const el = e.target;
|
||||||
|
|
||||||
|
const basicData = captureBasicData(el);
|
||||||
|
const selector = basicBuildSelector(el);
|
||||||
|
const tagName = el.tagName.toLowerCase();
|
||||||
|
const id = el.id || null;
|
||||||
|
const classes = el.className && typeof el.className === 'string'
|
||||||
|
? el.className.trim().split(/\s+/).filter(c => c.length > 0)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
basicPickerCleanup();
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
type: 'inspectResult',
|
||||||
|
data: {
|
||||||
|
selector,
|
||||||
|
tagName,
|
||||||
|
id,
|
||||||
|
classes,
|
||||||
|
basicData,
|
||||||
|
mode: 'basic',
|
||||||
|
boxModel: basicData.boxModel,
|
||||||
|
computedStyles: basicData.computedStyles,
|
||||||
|
matchedRules: basicData.matchedRules,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBasicKeydown(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
basicPickerCleanup();
|
||||||
|
chrome.runtime.sendMessage({ type: 'pickerCancelled' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startBasicPicker() {
|
||||||
|
basicPickerActive = true;
|
||||||
|
document.addEventListener('mousemove', onBasicMouseMove, true);
|
||||||
|
document.addEventListener('click', onBasicClick, true);
|
||||||
|
document.addEventListener('keydown', onBasicKeydown, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for messages from background worker
|
// Listen for messages from background worker
|
||||||
chrome.runtime.onMessage.addListener((msg) => {
|
chrome.runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.type === 'startBasicPicker') {
|
||||||
|
startBasicPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.type === 'stopBasicPicker') {
|
||||||
|
basicPickerCleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (msg.type === 'refs' && msg.data) {
|
if (msg.type === 'refs' && msg.data) {
|
||||||
const refs = msg.data.refs || [];
|
const refs = msg.data.refs || [];
|
||||||
const mode = msg.data.mode;
|
const mode = msg.data.mode;
|
||||||
|
|||||||
Reference in New Issue
Block a user