// ==UserScript==
// @name Der universelle Komponenten-Analysator 🔬 (v21.4 - Verbessert)
// @namespace http://tampermonkey.net/
// @version 21.4
// @description Umfassendes Update: Optimierter Prompt-Header für bessere KI-Interaktion, erweiterte CSS-Eigenschaften für tiefere Analyse und verfeinerte HTML-Struktur-Extraktion.
// @author Assistant & Dein Name
// @match *://*/*
// @grant GM_addStyle
// @license MIT
// @grant GM_setClipboard
// @grant GM_log
// @grant GM_info
// ==/UserScript==
(function() {
'use strict';
// --- Skript-Konfiguration & Konstanten ---
const SCRIPT_VERSION = GM_info.script.version; // Version direkt aus Metadaten
const PICKER_BUTTON_SIZE = 50; // px
const NOTIFICATION_OFFSET = 20; // px
const STATUS_OFFSET_TOP = 20; // px
// Globale Zustandsvariablen in einem Objekt gekapselt
const appState = {
pickerMode: false,
pickerButton: null,
lastHoveredElement: null,
selectedElements: [],
isDragging: false,
wasDragging: false,
offsetX: 0,
offsetY: 0,
defaultStylesCache: new Map(),
notificationTimeout: null,
statusTimeout: null,
currentStatusMessage: '' // Hinzugefügt für Statusmanagement
};
// CSS-Stile für das Skript
GM_addStyle(`
#element-picker-btn {
position: fixed;
right: ${NOTIFICATION_OFFSET}px;
top: 50%;
transform: translateY(-50%);
width: ${PICKER_BUTTON_SIZE}px;
height: ${PICKER_BUTTON_SIZE}px;
background: #007185;
border: none;
border-radius: 50%;
cursor: grab;
z-index: 999999;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
}
#element-picker-btn:hover { background-color: #005a6b; }
body.picker-dragging, body.picker-dragging * { cursor: grabbing !important; }
#element-picker-btn.active { background: #dc3545; animation: pulse 1s infinite; }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(220, 53, 69, 0); } 100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); } }
.picker-cursor, .picker-cursor * { cursor: crosshair !important; }
.picker-highlight { outline: 2px solid #00A896 !important; outline-offset: 1px !important; background-color: rgba(0, 168, 150, 0.1) !important; }
.picker-selected { outline: 2px solid #007bff !important; outline-offset: 1px !important; background-color: rgba(0, 123, 255, 0.15) !important; }
.picker-notification { position: fixed; top: ${NOTIFICATION_OFFSET}px; right: ${NOTIFICATION_OFFSET}px; background: #28a745; color: white; padding: 10px 20px; border-radius: 5px; z-index: 1000000; font-family: Arial, sans-serif; font-size: 14px; animation: slideIn 0.3s ease-out; opacity: 0; transition: opacity 0.3s ease-out, transform 0.3s ease-out; }
.picker-notification.error { background-color: #dc3545; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.picker-status { position: fixed; top: ${STATUS_OFFSET_TOP}px; left: 50%; transform: translateX(-50%); background: #dc3545; color: white; padding: 8px 16px; border-radius: 5px; z-index: 1000000; font-family: Arial, sans-serif; font-size: 12px; font-weight: bold; opacity: 0; transition: opacity 0.3s ease-out; }
`);
// --- KERNFUNKTIONEN ---
function onElementClick(e) {
if (!appState.pickerMode || e.target.id === 'element-picker-btn' || e.target.closest('#element-picker-btn')) return;
// Verhindert das Standardverhalten des Klicks (z.B. Link-Navigation)
e.preventDefault();
e.stopPropagation();
const selectedElement = appState.lastHoveredElement;
if (!selectedElement) {
showNotification('Kein Element zum Auswählen gefunden. Versuche es erneut.', true);
return;
}
try {
if (e.ctrlKey) { // Multi-Pick
const index = appState.selectedElements.indexOf(selectedElement);
if (index > -1) {
selectedElement.classList.remove('picker-selected');
appState.selectedElements.splice(index, 1);
} else {
selectedElement.classList.add('picker-selected');
appState.selectedElements.push(selectedElement);
}
showStatus(`${appState.selectedElements.length} Element(e) ausgewählt. Klick ohne STRG zum Kopieren. ESC zum Beenden.`);
} else { // Finaler Klick -> Datenverarbeitung
let elementsToCopy = [...appState.selectedElements];
if (!elementsToCopy.includes(selectedElement)) {
elementsToCopy.push(selectedElement);
}
if (elementsToCopy.length > 0) {
showStatus('Analysiere Elemente... Bitte warten.', false, true); // "Bitte warten..." Meldung
// Timeout um UI-Update zu ermöglichen, bevor schwere Berechnung startet
setTimeout(() => {
try { // Zusätzlicher Try-Catch für die Datenverarbeitung selbst
const startTime = performance.now();
const globalMeta = getGlobalPageMetadata();
const elementJsonObjects = elementsToCopy.map(el => extractElementAsJson(el));
const endTime = performance.now();
const scriptDuration = ((endTime - startTime) / 1000).toFixed(2) + 's';
const metadata = {
metadata: {
url: window.location.href,
title: document.title,
timestamp_utc: new Date().toISOString(), // Präziserer Zeitstempel
html_lang: document.documentElement.lang || 'unknown', // HTML-Sprache
favicon_url: getFaviconUrl(), // Favicon-URL
url_parameters: getUrlParameters(), // URL-Parameter
...globalMeta,
selected_elements_count: elementsToCopy.length,
script_runtime: scriptDuration,
script_version: SCRIPT_VERSION // Skript-Version hier hinzufügen
}
};
const metadataLine = JSON.stringify(metadata);
const elementLines = elementJsonObjects.map(obj => JSON.stringify(obj));
const finalJsonlOutput = [metadataLine, ...elementLines].join('\n');
const promptHeader = getPromptHeader(elementsToCopy.length);
const finalReport = `${promptHeader}\n\n---\n\n${finalJsonlOutput}`;
GM_setClipboard(finalReport, 'text');
showNotification(`Semantischer Report für ${elementsToCopy.length} Element(e) kopiert!`);
deactivatePickerMode();
} catch (processingError) {
console.error(`Fehler bei der Datenverarbeitung (v${SCRIPT_VERSION}):`, processingError);
showNotification('Fehler bei der Datenverarbeitung! Siehe Konsole.', true);
deactivatePickerMode(); // Im Fehlerfall auch den Picker beenden
}
}, 50); // Kleiner Timeout
} else {
showNotification('Keine Elemente zum Kopieren ausgewählt.', true);
deactivatePickerMode();
}
}
} catch (error) {
console.error(`Allgemeiner Fehler in Picker-Skript (v${SCRIPT_VERSION}):`, error);
showNotification('Ein unerwarteter Fehler ist aufgetreten! Siehe Konsole.', true);
deactivatePickerMode(); // Im Fehlerfall auch den Picker beenden
}
}
// --- BUTTON POSITIONIERUNG ---
function loadButtonPosition() {
const savedPosition = localStorage.getItem('pickerButtonPosition');
if (savedPosition) {
const pos = JSON.parse(savedPosition);
let x = parseInt(pos.x, 10);
let y = parseInt(pos.y, 10);
const docWidth = window.innerWidth;
const docHeight = window.innerHeight;
// Stellt sicher, dass der Button im sichtbaren Bereich bleibt
x = Math.max(0, Math.min(x, docWidth - PICKER_BUTTON_SIZE));
y = Math.max(0, Math.min(y, docHeight - PICKER_BUTTON_SIZE));
appState.pickerButton.style.top = `${y}px`;
appState.pickerButton.style.left = `${x}px`;
appState.pickerButton.style.right = 'auto'; // Original 'right' überschreiben
appState.pickerButton.style.transform = 'none'; // Original 'transform' überschreiben
}
}
// --- Prompt-Header (Stark erweitert und verbessert) ---
function getPromptHeader(elementCount) {
const currentUrl = window.location.href;
const currentTitle = document.title;
const exampleJsonl = `
\`\`\`jsonl
{"metadata": {"url": "https://example.com/page", "title": "Example Page", "timestamp_utc": "2024-06-11T12:00:00.000Z", "html_lang": "en", "script_version": "21.4"}}
{"tagName": "h1", "id": "main-title", "domPath": "body > div > h1#main-title", "textContent": "Welcome!", "css": {"font-size": "32px", "color": "rgb(34, 34, 34)"}}
{"tagName": "button", "classes": ["btn", "btn-primary"], "textContent": "Click Me", "isInteractive": true, "eventListeners": ["click"], "geometry": {"x": 100, "y": 200, "width": 120, "height": 40}}
{"tagName": "img", "src": "/images/logo.png", "alt": "Company Logo", "geometry": {"width": 150, "height": 50}}
\`\`\`
`;
return `Du bist ein spezialisierter KI-Analyst für Webseiten-Komponenten. Deine Aufgabe ist es, die vom Benutzer bereitgestellten HTML-Element-Daten zu interpretieren und basierend auf einer der folgenden Rollen zu antworten.
**Kontext des Exports:**
* **Aktuelle URL:** ${currentUrl}
* **Seitentitel:** ${currentTitle}
* **Export-Datum (UTC):** ${new Date().toISOString()}
* **Skript-Version:** ${SCRIPT_VERSION}
**WICHTIGE ANWEISUNG FÜR DICH, DIE KI:**
1. **Wähle EINE Rolle:** Lies die drei Persona-Abschnitte unten. Wähle diejenige Rolle, die am besten zur nachfolgenden Benutzerfrage passt.
2. **Lösche die anderen Rollen:** Entferne die Abschnitte der beiden *nicht* gewählten Rollen vollständig aus deiner Antwort.
3. **Lösche diesen Anweisungsblock:** Auch dieser Block sollte in deiner finalen Antwort nicht mehr enthalten sein.
4. **Formuliere deine Antwort als Experte der gewählten Rolle:** Gib eine präzise, hilfsbereite und relevante Antwort auf die Benutzerfrage, basierend auf den bereitgestellten Daten und deiner gewählten Persona. Sei dabei so detailliert oder prägnant, wie es die Aufgabe erfordert.
5. **Füge KEINE zusätzlichen Informationen hinzu, die nicht in den bereitgestellten Daten enthalten sind.** Wenn eine Information nicht in den JSONL-Daten zu finden ist, weise darauf hin, anstatt zu spekulieren.
6. **Erkenne Unstimmigkeiten:** Solltest du eine Unstimmigkeit oder einen möglichen Fehler in den bereitgestellten Daten erkennen, weise bitte höflich darauf hin.
---
### Persona 1: Der Userscript-Entwickler
**Deine Rolle:** Du bist ein erfahrener JavaScript-Entwickler, spezialisiert auf die Erstellung von robusten Userscripts (z.B. für Tampermonkey oder Greasemonkey).
**Dein Ziel:** Erstelle ein funktionierendes Userscript auf Basis der bereitgestellten JSONL-Daten, das die vom Benutzer gewünschte Aufgabe automatisiert (z.B. Elemente klicken, Text extrahieren, Formulare ausfüllen).
**Dein Fokus:**
* **Stabile Selektoren:** Bevorzuge IDs, \`data-*\`-Attribute oder robuste Klassenkombinationen aus dem \`domPath\`. Nutze \`document.querySelector\` oder \`document.querySelectorAll\`. Vermeide unspezifische nth-child/nth-of-type Selektoren, wo immer möglich.
* **Struktur & Kontext:** Verstehe die Beziehungen zwischen Elementen durch \`domPath\` und \`structureTree\`.
* **Interaktion:** Beachte \`isInteractive\`, \`tagName\` (\`button\`, \`a\`, \`input\`) und \`eventListeners\` für Aktionen. Berücksichtige die \`geometry\` für sichtbare Interaktionen.
* **Code-Qualität:** Dein Code sollte sauber, kommentiert und robust gegenüber kleinen Änderungen an der Webseite sein. Nutze Warten-Funktionen (\`waitForElement\`), falls Elemente dynamisch geladen werden.
* **Fehlerbehandlung:** Implementiere grundlegende Fehlerbehandlung und Logging.
**Beispiel für eine Benutzerfrage, die du beantworten würdest:**
"Ich möchte ein Userscript erstellen, das automatisch auf den Button mit dem Text 'Weiter' klickt, sobald er erscheint. Kannst du mir den JavaScript-Code dafür geben, der die Element-ID oder eine robuste Klasse nutzt?"
---
### Persona 2: Der CSS-Stylist / Frontend-Entwickler
**Deine Rolle:** Du bist ein versierter Frontend-Entwickler mit einem scharfen Auge für CSS und Design.
**Dein Ziel:** Erstelle CSS-Code, um das Aussehen der ausgewählten Elemente nach den Wünschen des Benutzers zu verändern oder Layout-Probleme zu beheben.
**Dein Fokus:**
* **Bestehende Stile:** Analysiere das \`css\`-Feld, um die aktuellen, vom Standard abweichenden Stile zu verstehen und zu überschreiben oder zu ergänzen.
* **Präzise Selektoren:** Nutze die \`id\` und \`classes\` der Elemente, um genaue CSS-Selektoren zu erstellen. Berücksichtige auch \`domPath\` für kontextbezogene Selektoren.
* **Struktur:** Berücksichtige die \`structureTree\` und den \`domPath\` für den Einfluss von Änderungen auf Kind- oder Elternelemente (z.B. mit Kombinatoren wie \`>\` oder \`+\`).
* **Spezifität & \`!important\`:** Erstelle Regeln mit ausreichender Spezifität. Erkläre, wann und warum \`!important\` eventuell notwendig (aber zu vermeiden) ist.
* **Responsive Design:** Denke an verschiedene Bildschirmgrößen und den Einfluss deiner Änderungen auf das Layout.
**Beispiel für eine Benutzerfrage, die du beantworten würdest:**
"Wie kann ich die Schriftgröße dieses Titels auf 24px erhöhen und ihn zentrieren, ohne andere Elemente zu beeinflussen? Bitte gib mir den nötigen CSS-Code."
---
### Persona 3: Der Daten-Analyst & -Extraktor
**Deine Rolle:** Du bist ein präziser Daten-Analyst. Deine Aufgabe ist es, aus den Webseiten-Fragmenten die reinen Informationen zu extrahieren und strukturiert darzustellen.
**Dein Ziel:** Fasse den semantischen Inhalt der ausgewählten Elemente zusammen, erkenne Muster und präsentiere die extrahierten Daten in einem sauberen Format (z.B. Markdown-Tabelle, JSON).
**Dein Fokus:**
* **Inhalt:** Konzentriere dich auf das \`textContent\`-Feld, um die Kerninformationen zu gewinnen. Beachte auch \`src\` und \`alt\` für Bilder, \`href\` für Links und \`inputValue\` für Formularfelder.
* **Bedeutung:** Nutze \`domPath\`, \`tagName\`, \`ariaRole\`, \`ariaLabel\`, \`schemaOrgData\` und die Metadaten der Seite, um den Kontext und die Art der Daten zu bestimmen.
* **Gruppierung:** Fasse Elemente, die offensichtlich zusammengehören (z.B. ein Bild, eine Überschrift und ein Preis), zu einer einzigen logischen Einheit zusammen.
* **Mustererkennung:** Identifiziere wiederkehrende Muster in Listen oder Tabellen und schlage effektive Extraktionsstrategien vor.
* **Präsentation:** Gib die extrahierten Daten ohne schmückendes Beiwerk, aber sauber formatiert (z.B. als Tabelle oder strukturiertes JSON), aus.
**Beispiel für eine Benutzerfrage, die du beantworten würdest:**
"Ich habe mehrere Produktinformationen ausgewählt. Kannst du mir eine Tabelle mit den Produktnamen, Preisen und zugehörigen Links erstellen?"
---
**DATENBESTAND:**
* Es sind **${elementCount} Objekt(e)** zur Analyse vorhanden.
* Die Daten folgen dem **JSONL-Format** (ein JSON-Objekt pro Zeile).
* **Erste Zeile:** Globale Metadaten zur gesamten Seite.
* **Weitere Zeilen:** Semantisch angereicherte Daten der einzelnen, ausgewählten HTML-Elemente.
${exampleJsonl}
---
`;
}
// --- Datenextraktion ---
function extractElementAsJson(element) {
const data = { tagName: element.tagName.toLowerCase() };
data.domPath = getDomPath(element);
if (element.id) { data.id = element.id; }
// Robusterer Check für className
if (element.className && typeof element.className === 'string') {
const classes = element.className.split(' ').filter(c => !c.startsWith('picker-'));
if (classes.length > 0) { data.classes = classes; }
}
const role = element.getAttribute('role');
if (role) data.ariaRole = role;
const label = element.getAttribute('aria-label');
if (label) data.ariaLabel = label;
const hidden = element.getAttribute('aria-hidden');
if (hidden) data.ariaHidden = hidden === 'true'; // Convert to boolean
// Erweitert um data-Attribute
const dataAttributes = {};
for (const attr of element.attributes) {
if (attr.name.startsWith('data-')) {
dataAttributes[attr.name] = attr.value;
}
}
if (Object.keys(dataAttributes).length > 0) {
data.dataAttributes = dataAttributes;
}
const interactiveTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'DETAILS'];
data.isInteractive = interactiveTags.includes(element.tagName) || element.hasAttribute('onclick') || window.getComputedStyle(element).cursor === 'pointer';
// Erfassung von Event Listenern (Best-Effort, da JS keine direkte API bietet)
const eventListeners = getEventListeners(element);
if (eventListeners.length > 0) {
data.eventListeners = eventListeners;
}
const rect = element.getBoundingClientRect();
data.geometry = {
x: Math.round(rect.x), y: Math.round(rect.y),
width: Math.round(rect.width), height: Math.round(rect.height),
inViewport: isElementInViewport(rect)
};
const textContent = element.innerText?.trim();
if (textContent) { data.textContent = textContent; }
// Spezielle Attribute für bestimmte Tags
if (element.tagName === 'A' && element.href) { data.href = element.href; }
if (element.tagName === 'IMG' && element.src) { data.src = element.src; }
if (element.tagName === 'IMG' && element.alt) { data.alt = element.alt; }
if (element.tagName === 'INPUT' && element.type) { data.inputType = element.type; }
if (element.tagName === 'INPUT' && element.value !== undefined) { data.inputValue = element.value; } // Achtung bei Passwörtern!
if (element.tagName === 'FORM' && element.action) { data.formAction = element.action; }
if (element.tagName === 'FORM' && element.method) { data.formMethod = element.method; }
if (element.tagName === 'LINK' && element.rel) { data.linkRel = element.rel; }
if (element.tagName === 'LINK' && element.href) { data.linkHref = element.href; }
if (element.tagName === 'META' && element.name) { data.metaName = element.name; }
if (element.tagName === 'META' && element.property) { data.metaProperty = element.property; }
if (element.tagName === 'META' && element.content) { data.metaContent = element.content; }
if (element.tagName === 'IFRAME' && element.src) { data.iframeSrc = element.src; }
if (element.tagName === 'SVG') { data.svgViewBox = element.getAttribute('viewBox'); }
if (element.tagName === 'CANVAS') { data.canvasWidth = element.width; data.canvasHeight = element.height; }
// Strukturbaum als JSON-Objekt
data.structureTree = buildChildTree(element, 0, 2); // Max-Tiefe von 2 Ebenen
// Kompaktes HTML der direkten Kinder (nicht des gesamten Baums, um Größe zu begrenzen)
const childHtml = Array.from(element.children).map(child => child.outerHTML).join('');
data.rawChildrenHTML = compactHTML(childHtml);
data.css = getCompressedCss(element); // CSS als Objekt
return data;
}
// --- Semantische Helferfunktionen ---
function getGlobalPageMetadata() {
const globalData = {};
const description = document.querySelector('meta[name="description"]');
if (description) globalData.metaDescription = description.content.trim();
const canonical = document.querySelector('link[rel="canonical"]');
if (canonical) globalData.canonicalUrl = canonical.href;
const schemaScripts = document.querySelectorAll('script[type="application/ld+json"]');
if (schemaScripts.length > 0) {
const schemaTypes = new Set();
schemaScripts.forEach(script => {
try {
const jsonData = JSON.parse(script.textContent);
// Handle single object and array of objects
const types = (Array.isArray(jsonData) ? jsonData : [jsonData])
.map(item => item['@type'])
.flat()
.filter(Boolean); // Filtert undefined/null
types.forEach(type => schemaTypes.add(type));
} catch (e) { GM_log(`[${SCRIPT_VERSION}] Fehler beim Parsen von Schema.org JSON: ${e.message}`); }
});
if (schemaTypes.size > 0) {
globalData.schemaOrgData = Array.from(schemaTypes);
}
}
return globalData;
}
function getDomPath(element) {
if (!element || element === document.body) return element ? element.tagName.toLowerCase() : '';
const path = [];
while (element && element.tagName !== 'HTML') { // Bis zum HTML-Tag gehen
let selector = element.tagName.toLowerCase();
if (element.id) { selector += `#${element.id}`; }
// Robusterer Check für className
if (element.className && typeof element.className === 'string') {
const classes = element.className.split(' ').filter(c => c && !c.startsWith('picker-'));
if (classes.length > 0) { selector += `.${classes.join('.')}`; }
}
// Index für gleiche Geschwisterelemente hinzufügen, falls keine ID/Klasse
if (!element.id && (!element.className || (typeof element.className === 'string' && element.className.split(' ').filter(c => c && !c.startsWith('picker-')).length === 0))) {
let siblingCount = 0;
let originalIndex = 0;
if (element.parentElement) {
for (let i = 0; i < element.parentElement.children.length; i++) {
if (element.parentElement.children[i].tagName === element.tagName) {
if (element.parentElement.children[i] === element) {
originalIndex = siblingCount;
}
siblingCount++;
}
}
}
if (siblingCount > 1) { // Nur hinzufügen, wenn es mehrere Geschwister des gleichen Typs gibt
selector += `:nth-of-type(${originalIndex + 1})`;
}
}
path.unshift(selector);
element = element.parentElement;
}
return path.join(' > ');
}
function isElementInViewport(rect) {
return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) );
}
// Verbesserte CSS-Extraktion: Rückgabe als Objekt, mehr relevante Properties
function getCompressedCss(element) {
const hadHighlight = element.classList.contains('picker-highlight');
if (hadHighlight) element.classList.remove('picker-highlight');
const tagName = element.tagName;
if (!appState.defaultStylesCache.has(tagName)) {
const dummy = document.createElement(tagName);
document.body.appendChild(dummy);
appState.defaultStylesCache.set(tagName, window.getComputedStyle(dummy));
document.body.removeChild(dummy);
}
const styles = window.getComputedStyle(element);
const defaultStyles = appState.defaultStylesCache.get(tagName);
const cssProps = {};
const relevantProperties = [
'display', 'position', 'flex-direction', 'justify-content', 'align-items', 'flex-wrap', 'gap',
'width', 'height', 'min-width', 'max-width', 'min-height', 'max-height',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left',
'border-radius', 'box-sizing', // Wichtig für Layout
'font-family', 'font-size', 'font-weight', 'font-style', 'color', 'background-color',
'text-align', 'line-height', 'text-decoration', 'text-transform', 'letter-spacing', 'word-spacing',
'box-shadow', 'opacity', 'visibility', 'overflow', 'overflow-x', 'overflow-y',
'z-index', 'cursor', 'pointer-events', // Für Interaktivität
'white-space', 'word-break', 'float', 'clear',
'top', 'bottom', 'left', 'right', // Für absolute/fixed positionierte Elemente
'background-image', 'background-size', 'background-position', 'background-repeat', 'background-attachment',
'transform', 'transform-origin', 'transition', 'animation',
'outline', 'outline-offset', // Für Fokus/Interaktion
'box-shadow', 'filter', // Visuelle Effekte
'clip-path', 'mask', // Komplexere Formen
'content', // Für pseudo-Elemente
'vertical-align', // Für Inline-Elemente
'writing-mode', 'direction', // Textfluss
'grid-template-columns', 'grid-template-rows', 'grid-gap', 'grid-column', 'grid-row' // CSS Grid
];
for (const prop of relevantProperties) {
const value = styles.getPropertyValue(prop);
if (value !== defaultStyles.getPropertyValue(prop) && value !== '' && value !== 'initial' && value !== 'unset') {
cssProps[prop] = value;
}
}
if (hadHighlight) element.classList.add('picker-highlight');
return Object.keys(cssProps).length > 0 ? cssProps : null;
}
// Strukturbaum als JSON-Objekt
function buildChildTree(element, level = 0, maxLevel = 2) {
if (level >= maxLevel) return null;
const childrenData = [];
for (const child of element.children) {
// Ignoriere Skripte und Style-Tags im Strukturbaum
if (child.tagName === 'SCRIPT' || child.tagName === 'STYLE' || child.tagName === 'NOSCRIPT') continue;
const childObj = {
tagName: child.tagName.toLowerCase(),
id: child.id || undefined,
classes: (child.className && typeof child.className === 'string') ? child.className.replace(/picker-\w+/g, '').trim().split(' ').filter(c => c) : undefined
};
// Füge einen Text-Snippet hinzu, falls vorhanden und nicht zu lang
const childTextContent = child.textContent?.trim();
if (childTextContent) {
childObj.textContentSnippet = childTextContent.substring(0, 100) + (childTextContent.length > 100 ? '...' : '');
}
if (child.tagName === 'IMG' && child.src) { childObj.src = child.src; } // Bilder im Baum
if (child.tagName === 'A' && child.href) { childObj.href = child.href; } // Links im Baum
// Rekursiver Aufruf für tiefere Kinder
if (child.children.length > 0) {
const nestedChildren = buildChildTree(child, level + 1, maxLevel);
if (nestedChildren && nestedChildren.length > 0) {
childObj.children = nestedChildren;
}
}
childrenData.push(childObj);
}
return childrenData.length > 0 ? childrenData : null;
}
// HTML-Kompression: Vorsichtiger mit Whitespaces
function compactHTML(html) {
// Ersetzt mehrere Whitespaces, Zeilenumbrüche und Tabs durch ein einzelnes Leerzeichen.
// Entfernt Whitespace direkt zwischen schließenden und öffnenden Tags (z.B. `> <`).
// Entfernt führende/endende Whitespaces.
return html.replace(/(\s*\n\s*|\s{2,}|\t)/g, ' ')
.replace(/>\s+</g, '><')
.trim();
}
// --- EVENT LISTENER ERFASSUNG (Best-Effort & Heuristiken) ---
function getEventListeners(element) {
const listeners = new Set(); // Nutze Set, um Duplikate zu vermeiden
// 1. Inline-Event-Handler-Attribute
const inlineEventAttributes = [
'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmousemove', 'onmouseover', 'onmouseout',
'onmouseenter', 'onmouseleave', 'oncontextmenu', 'onfocus', 'onblur', 'onchange',
'onsubmit', 'onreset', 'onselect', 'oninput', 'onkeydown', 'onkeyup', 'onkeypress',
'onload', 'onerror', 'onresize', 'onscroll', 'oncut', 'oncopy', 'onpaste'
];
for (const attrName of inlineEventAttributes) {
if (element.hasAttribute(attrName)) {
listeners.add(attrName.substring(2)); // z.B. 'click' statt 'onclick'
}
}
// 2. Heuristik für gängige interaktive Elemente
if (element.tagName === 'A' && element.href) {
listeners.add('click');
}
if (element.tagName === 'BUTTON') {
listeners.add('click');
}
if (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA') {
listeners.add('change');
listeners.add('input');
listeners.add('focus');
listeners.add('blur');
}
// Für Formulare
if (element.tagName === 'FORM') {
listeners.add('submit');
}
// Für Medienelemente
if (element.tagName === 'AUDIO' || element.tagName === 'VIDEO') {
listeners.add('play');
listeners.add('pause');
listeners.add('ended');
}
// Drag and Drop
if (element.hasAttribute('draggable') && element.getAttribute('draggable') === 'true') {
listeners.add('dragstart');
listeners.add('dragend');
}
// 3. jQuery Events abfangen (falls jQuery auf der Seite geladen ist)
// Dies bleibt eine "Best-Effort"-Heuristik und funktioniert nicht immer zuverlässig in Userscript-Sandboxes.
try {
if (typeof jQuery === 'function' && jQuery._data && jQuery._data(element, 'events')) {
for (const eventType in jQuery._data(element, 'events')) {
listeners.add(eventType);
}
}
} catch (e) {
// Dies ist ein erwarteter Fehler, wenn jQuery nicht oder anders geladen ist. Kein GM_log hier, um Spam zu vermeiden.
}
return Array.from(listeners); // Konvertiere Set zurück in Array
}
function getFaviconUrl() {
let favicon = document.querySelector("link[rel~='icon']");
if (favicon) {
return favicon.href;
}
// Fallback für gängige Favicon-Pfade
return `${window.location.origin}/favicon.ico`;
}
function getUrlParameters() {
const params = {};
const url = new URL(window.location.href);
for (const [key, value] of url.searchParams.entries()) {
params[key] = value;
}
return Object.keys(params).length > 0 ? params : null;
}
// --- UI-HELPER ---
function onMouseOver(e) {
if (!appState.pickerMode) return;
const targetElement = e.target;
if (!targetElement || targetElement.id === 'element-picker-btn' || targetElement.closest('#element-picker-btn')) {
if (appState.lastHoveredElement) {
appState.lastHoveredElement.classList.remove('picker-highlight');
appState.lastHoveredElement = null;
}
return;
}
if (appState.lastHoveredElement !== targetElement) {
if (appState.lastHoveredElement) {
appState.lastHoveredElement.classList.remove('picker-highlight');
}
appState.lastHoveredElement = targetElement;
appState.lastHoveredElement.classList.add('picker-highlight');
}
}
function onMouseOut(e) {
// Aktuell keine spezifische Logik erforderlich
}
function activatePickerMode() {
appState.pickerMode = true;
appState.pickerButton.classList.add('active');
document.body.classList.add('picker-cursor');
showStatus('Picker aktiv. STRG+Klick für Mehrfachauswahl. ESC zum Beenden.');
document.addEventListener('click', onElementClick, true);
document.addEventListener('mouseover', onMouseOver, true);
document.addEventListener('mouseout', onMouseOut, true);
document.addEventListener('keydown', onKeyDown, true);
// Zusätzlicher Tipp für neue Nutzer
if (!localStorage.getItem('pickerModeFirstTime')) {
showNotification('Klicke auf Elemente, um sie auszuwählen!', false);
localStorage.setItem('pickerModeFirstTime', 'true');
}
}
function makeButtonDraggable(button) {
button.addEventListener('mousedown', (e) => {
if (e.button !== 0 || appState.pickerMode) return; // Nur linker Mausklick und nicht im Picker-Modus
appState.isDragging = true;
appState.wasDragging = false;
document.body.classList.add('picker-dragging');
button.style.cursor = 'grabbing';
button.style.transition = 'none'; // Übergänge beim Dragging deaktivieren
const rect = button.getBoundingClientRect();
// Setze Position absolut, damit top/left funktionieren
button.style.left = `${rect.left}px`;
button.style.top = `${rect.top}px`;
button.style.right = 'auto'; // Original "right" Attribut aufheben
button.style.transform = 'none'; // Original "transform" Attribut aufheben
appState.offsetX = e.clientX - rect.left;
appState.offsetY = e.clientY - rect.top;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
});
function onDragMove(e) {
if (!appState.isDragging) return;
e.preventDefault(); // Verhindert z.B. Textauswahl beim Ziehen
appState.wasDragging = true;
let newX = e.clientX - appState.offsetX;
let newY = e.clientY - appState.offsetY;
// Beschränkt den Button auf den sichtbaren Bereich
newX = Math.max(0, Math.min(newX, window.innerWidth - button.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - button.offsetHeight));
button.style.left = `${newX}px`;
button.style.top = `${newY}px`;
}
function onDragEnd() {
if (!appState.isDragging) return;
appState.isDragging = false;
document.body.classList.remove('picker-dragging');
button.style.cursor = 'grab';
button.style.transition = 'background-color 0.3s ease'; // Transition wieder aktivieren
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragEnd);
if (appState.wasDragging) {
// Speichere die Position nur, wenn wirklich gezogen wurde
const position = { x: button.style.left, y: button.style.top };
localStorage.setItem('pickerButtonPosition', JSON.stringify(position));
}
}
}
function togglePickerMode() {
if (appState.wasDragging) {
// Wenn der Klick nur das Ende eines Drag-Vorgangs war, nicht den Picker-Modus umschalten
appState.wasDragging = false;
return;
}
appState.pickerMode = !appState.pickerMode;
if (appState.pickerMode) {
activatePickerMode();
} else {
deactivatePickerMode();
}
}
function deactivatePickerMode() {
appState.pickerMode = false;
if(appState.pickerButton) appState.pickerButton.classList.remove('active');
document.body.classList.remove('picker-cursor');
// Styles der markierten und ausgewählten Elemente zurücksetzen
if (appState.lastHoveredElement) {
appState.lastHoveredElement.classList.remove('picker-highlight');
}
appState.selectedElements.forEach(el => el.classList.remove('picker-selected'));
appState.selectedElements = []; // Auswahl leeren
appState.lastHoveredElement = null;
removeStatus(); // Statusmeldung entfernen
// Event Listener entfernen, um Speicherlecks und Konflikte zu vermeiden
document.removeEventListener('click', onElementClick, true);
document.removeEventListener('mouseover', onMouseOver, true);
document.removeEventListener('mouseout', onMouseOut, true);
document.removeEventListener('keydown', onKeyDown, true);
}
function onKeyDown(e) {
if (!appState.pickerMode) return;
if (e.key === 'Escape') {
e.preventDefault();
deactivatePickerMode();
showNotification('Picker-Modus beendet.'); // Meldung beim Beenden
}
}
function createPipetteIcon() {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "white");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M20.71 5.63l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-3.12 3.12-1.93-1.91-1.41 1.41 1.42 1.42L3 16.25V21h4.75l8.92-8.92 1.42 1.42 1.41-1.41-1.91-1.93 3.12-3.12c.4-.4.4-1.03 0-1.41zM6.92 19L5 17.08l8.06-8.06 1.92 1.92L6.92 19z");
svg.appendChild(path);
return svg;
}
function createPickerButton() {
const button = document.createElement('button');
button.id = 'element-picker-btn';
button.title = 'Komponenten-Analysator starten';
button.appendChild(createPipetteIcon());
button.addEventListener('click', togglePickerMode);
document.body.appendChild(button);
return button;
}
function showNotification(message, isError = false) {
// Bestehende Notification entfernen, um Überlappung zu vermeiden
if (appState.notificationTimeout) {
clearTimeout(appState.notificationTimeout);
const existingNotification = document.querySelector('.picker-notification');
if (existingNotification && existingNotification.parentNode) {
existingNotification.parentNode.removeChild(existingNotification);
}
}
const n = document.createElement('div');
n.className = 'picker-notification';
if (isError) n.classList.add('error');
n.textContent = message;
document.body.appendChild(n);
// Animation und Timeout
setTimeout(() => {
n.style.opacity = '1';
}, 10); // Kleiner Delay für CSS-Transition
appState.notificationTimeout = setTimeout(() => {
n.style.opacity = '0';
setTimeout(() => {
if (n.parentNode) n.parentNode.removeChild(n);
appState.notificationTimeout = null;
}, 300); // Entspricht CSS-Transition
}, 4000); // Notification bleibt 4 Sekunden sichtbar
}
function showStatus(message, removeAfterDelay = true, isTemporary = false) {
// Wenn es eine temporäre "Bitte warten..." Nachricht ist, die vorherige entfernen, aber nicht das Timeout von dauerhaften Nachrichten.
if (isTemporary && appState.statusTimeout) {
clearTimeout(appState.statusTimeout);
}
let statusElement = document.getElementById('picker-status');
if (!statusElement) {
statusElement = document.createElement('div');
statusElement.id = 'picker-status';
statusElement.className = 'picker-status';
document.body.appendChild(statusElement);
}
// Wenn der Text der gleiche ist und es nicht temporär ist, und der Status schon sichtbar ist, nichts tun
if (statusElement.textContent === message && !isTemporary && statusElement.style.opacity === '1') {
return;
}
statusElement.textContent = message;
statusElement.style.opacity = '1'; // Sichtbarkeit sicherstellen
if (removeAfterDelay) {
// Timer für automatische Entfernung der Statusmeldung
if (appState.statusTimeout) {
clearTimeout(appState.statusTimeout);
}
appState.statusTimeout = setTimeout(() => {
removeStatus();
appState.statusTimeout = null;
}, 5000); // Statusmeldung bleibt 5 Sekunden sichtbar
} else {
// Wenn nicht removeAfterDelay, dann Timeout löschen, falls gesetzt
if (appState.statusTimeout) {
clearTimeout(appState.statusTimeout);
appState.statusTimeout = null;
}
}
appState.currentStatusMessage = message; // Aktuelle Statusmeldung speichern
}
function removeStatus() {
const e = document.getElementById('picker-status');
if (e) {
e.style.opacity = '0';
setTimeout(() => {
if (e.parentNode) e.parentNode.removeChild(e);
}, 300); // Entspricht CSS-Transition
}
appState.currentStatusMessage = ''; // Statusmeldung leeren
}
function init() {
// Verhindert doppelte Ausführung
if (document.getElementById('element-picker-btn')) return;
appState.pickerButton = createPickerButton();
makeButtonDraggable(appState.pickerButton);
loadButtonPosition();
GM_log(`Universal Component Analyzer (v${SCRIPT_VERSION}) wurde geladen.`);
// Zeigt den initialen Tooltip für den Button
if (!localStorage.getItem('pickerButtonInitialTooltipShown')) {
showNotification('Klicke auf die Pipette, um den Element-Selektor zu starten.', false);
localStorage.setItem('pickerButtonInitialTooltipShown', 'true');
}
}
// Skript starten, sobald das DOM bereit ist
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();