// ==UserScript==
// @name Mobile.de Ausstattungssuche mit modernem Popup & Import/Export (Generalisiertes Merging mit Merge-Konfiguration)
// @namespace https://github.com/jxnxtxan/Mobile
// @version 2.5.1
// @author jxnxtxan
// @description Sucht bestimmte Ausstattungen & Technische Daten auf mobile.de. Token-basierte Match-Engine mit Wortgrenzen, Quellen-Gewichtung (Feature-Liste vs. Beschreibung), SPA-Robustheit, Konfig-Popup mit Filter, Drag&Drop, Reset, Backup und Schema-Versionierung.
// @homepageURL https://github.com/jxnxtxan/Mobile
// @supportURL https://github.com/jxnxtxan/Mobile/issues
// @updateURL https://raw.githubusercontent.com/jxnxtxan/Mobile/main/mobile-ausstattungssuche.js
// @downloadURL https://raw.githubusercontent.com/jxnxtxan/Mobile/main/mobile-ausstattungssuche.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=mobile.de
// @match http://suchen.mobile.de/fahrzeuge/details.html*
// @match https://suchen.mobile.de/fahrzeuge/details.html*
// @match http://suchen.mobile.de/auto-inserat/*
// @match https://suchen.mobile.de/auto-inserat/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @run-at document-idle
// @noframes
// ==/UserScript==
(function() {
'use strict';
// ============================================================
// Konstanten / Schema
// ============================================================
const SCHEMA_VERSION = 6;
const STORAGE_KEYS = {
config: 'mobilede_config',
techConfig: 'mobilede_techconfig',
mergeGroups: 'mobilede_mergeGruppen',
featureFlags: 'mobilede_feature_flags',
version: 'mobilede_config_version',
backupPrefix: 'mobilede_config_backup_'
};
// ============================================================
// Feature-Flag-Registry (erweiterbar)
// - Neue Optionen einfach unten anhängen, das Config-Tab rendert
// sie automatisch und Defaults werden vorwärts-kompatibel
// in bestehende User-Configs gemerged.
// ============================================================
const FEATURE_FLAG_DEFINITIONS = [
{
key: 'mapsLink',
title: 'Standort als Google-Maps-Link',
description: 'Macht Standort-Texte auf der Detailseite (z.B. „DE-92690 Pressath") anklickbar. Ein Klick öffnet Google Maps mit der Adresse als Suche.',
default: true
}
];
function featureFlagsDefault() {
const obj = {};
FEATURE_FLAG_DEFINITIONS.forEach(d => { obj[d.key] = !!d.default; });
return obj;
}
function ladeFeatureFlags() {
const stored = ladeConfig(STORAGE_KEYS.featureFlags);
const defaults = featureFlagsDefault();
if (!stored || typeof stored !== 'object') return defaults;
// Defaults für neu hinzugekommene Flags ergänzen, unbekannte Keys
// bleiben erhalten (zukunftskompatibel).
return { ...defaults, ...stored };
}
// ============================================================
// 1) GM_*-Speicherhilfen
// ============================================================
function ladeConfig(key) {
try {
const str = GM_getValue(key, null);
if (!str) return null;
return JSON.parse(str);
} catch (e) {
console.warn('Fehler beim Laden der Konfiguration:', key, e);
return null;
}
}
function speichereConfig(key, data) {
try {
GM_setValue(key, JSON.stringify(data));
} catch (e) {
console.error('Fehler beim Speichern der Konfiguration:', key, e);
}
}
// ============================================================
// 2) Default-Konfigurationen (bereinigt)
// - umlauts und diakritika dürfen vorkommen, werden bei
// cleanText() / tokenize() normalisiert.
// - 'nurInFeatures: true' ignoriert Treffer aus Beschreibung.
// - 'compound: true' erlaubt Substring-Match an beliebiger
// Stelle im Token (selten nötig).
// ============================================================
const suchKonfigurationenDefault = [
{ begriffe: ['360 grad', '360 kamera', '360 cam', 'umfeld kamera', 'surround cam'], anzeige: '360 Grad Kamera', farbe: 'red', aktiv: true },
{ begriffe: ['scheiben abgedunk', 'abgedunk scheib'], anzeige: 'Abgedunkelte Scheiben', aktiv: true },
{ begriffe: ['anti blockiersystem', 'antiblocksicherung', 'abs brems'], anzeige: 'ABS', aktiv: false },
{ begriffe: ['tempomat abstand', 'adapt temp', 'acc'], anzeige: 'Abstandstempomat', farbe: 'orange', aktiv: true },
{ begriffe: ['abstands warn', 'distance warn'], anzeige: 'Abstandswarner', aktiv: false },
{ begriffe: ['adapt kurv licht', 'kurvenlicht adaptiv'], anzeige: 'Adaptives Kurvenlicht', aktiv: true },
{ begriffe: ['adblue technologie', 'adblue hinweis', 'scr system'], anzeige: 'AdBlue / SCR', aktiv: true },
{ begriffe: ['akustikverglasung', 'akustik verglasung', 'frontscheibe akus'], anzeige: 'Akustikverglasung', aktiv: true },
{ begriffe: ['alarmanlage', 'diebstahlwarnanlage'], anzeige: 'Alarmanlage', aktiv: true },
{ begriffe: ['4wd', 'allrad'], anzeige: 'Allrad', farbe: 'orange', aktiv: true },
{ begriffe: ['ambiente beleuchtung', 'ambiente licht', 'stimmungslicht'], anzeige: 'Ambiente-Beleuchtung', aktiv: true },
{ begriffe: ['android auto'], anzeige: 'Android Auto', aktiv: true },
{ begriffe: ['anhängevorrichtung', 'anhängerkupplung', 'ahk'], anzeige: 'Anhängerkupplung', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['anhängevorrichtung schwenkbar', 'anhängerkupplung schwenkbar'], anzeige: 'Anhängerkupplung schwenkbar', aktiv: true },
{ begriffe: ['apple carplay', 'apple car play'], anzeige: 'Apple Carplay', aktiv: true },
{ begriffe: ['armlehne'], anzeige: 'Armlehne', aktiv: false },
{ begriffe: ['aussen innen mit abblendautomat', 'aussen innenspiegel mit abblendautomatik', 'aeussen innen mit abblendautomatik'], anzeige: 'Außen-/Innenspiegel automatisch abblendend', aktiv: true },
{ begriffe: ['spiegel klappbar', 'elek spiegel klapp', 'außenspiegel anklappbar', 'außenspiegel klappbar'], anzeige: 'Außenspiegel anklappbar', aktiv: true },
{ begriffe: ['aussenspiegel mit abblendautomatik', 'aeussenspiegel mit abblendautomatik'], anzeige: 'Außenspiegel automatisch abblendend', aktiv: true },
{ begriffe: ['außenspiegel heizung', 'außenspiegel beheiz', 'außenspiegel heiz'], anzeige: 'Außenspiegel beheizbar', aktiv: true },
{ begriffe: ['außenspiegel elek verst', 'elek spiegel'], anzeige: 'Außenspiegel elektr. verstellbar', aktiv: true },
{ begriffe: ['bang & olufsen', 'b&o', 'bang olufsen'], anzeige: 'Bang & Olufsen Sound System', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['beats'], anzeige: 'Beats Sound System', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['berganfahrassist', 'berganfahr', 'hill start', 'hill hold', 'anfahrassist'], anzeige: 'Berganfahrassistent', aktiv: true },
{ begriffe: ['bi xenon', 'scheinwerfer xenon', 'xenon scheinwerfer'], anzeige: 'Bi-/Xenon-Scheinwerfer', aktiv: true },
{ begriffe: ['bluetooth', 'blue tooth'], anzeige: 'Bluetooth', aktiv: true },
{ begriffe: ['bose'], anzeige: 'BOSE Sound System', farbe: 'red', aktiv: true },
{ begriffe: ['brems assist', 'brake assist'], verboten: ['notbrems', 'not brems'], anzeige: 'Bremsassistent', aktiv: true },
{ begriffe: ['burmester'], anzeige: 'Burmester Sound System', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['business paket professional', 'business paket'], anzeige: 'Business Paket', aktiv: true },
{ begriffe: ['canton'], anzeige: 'Canton Sound System', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['dachhimmel alcantara', 'himmel alcant'], anzeige: 'Dachhimmel Alcantara', aktiv: true },
{ begriffe: ['dachhimmel anth', 'himmel anth', 'dachhimmel schwarz', 'dachhim schwarz'], anzeige: 'Dachhimmel Anthrazit / Schwarz', aktiv: true },
{ begriffe: ['elek fenst'], anzeige: 'Elektr. Fensterheber', aktiv: true },
{ begriffe: ['elek heckklappe'], anzeige: 'Elektr. Heckklappe', aktiv: true },
{ begriffe: ['sitz elek verstell', 'sitzeinstellung', 'sitz einstellung', 'elektr sitz'], anzeige: 'Elektr. Sitzeinstellung', aktiv: true },
{ begriffe: ['memory sitz', 'sitz memory', 'sitz elek verstell memory'], anzeige: 'Elektr. Sitzeinstellung mit Memory-Funktion', farbe: 'red', aktiv: true },
{ begriffe: ['elek wegfahrsperre', 'elektrisch wegfahrsper', 'wegfahrsperre elek'], anzeige: 'Elektr. Wegfahrsperre', aktiv: false },
{ begriffe: ['elektronisches stabilit', 'fahrstabilität', 'fahrstabilitaet'], anzeige: 'ESP', aktiv: true },
{ begriffe: ['blendfrei fernlicht', 'anti blend licht', 'fernlicht assist', 'auto fernlicht'], anzeige: 'Fernlicht Assistent', farbe: 'orange', aktiv: true },
{ begriffe: ['freisprecheinrichtung', 'freisprechanlage', 'hands free einricht'], anzeige: 'Freisprecheinrichtung', aktiv: true },
{ begriffe: ['garantie'], anzeige: 'Garantie', aktiv: false },
{ begriffe: ['harman kardon', 'h&k', 'harman'], anzeige: 'Harman Kardon Sound System', farbe: 'red', aktiv: true, nurInFeatures: true },
{ begriffe: ['head up', 'head-up', 'hud'], anzeige: 'Head-Up Display', farbe: 'red', aktiv: true },
{ begriffe: ['heckantrieb', 'antrieb heck'], anzeige: 'Heckantrieb', aktiv: false },
{ begriffe: ['induktiv laden', 'induktion laden', 'induktionsladen', 'wireless charge'], anzeige: 'Induktionsladeschale für Smartphone (Wireless Charging)', aktiv: false },
{ begriffe: ['innenraumfilter aktiv', 'innenraum aktivkohlefilt', 'aktivkohle geruchs', 'innenraumfilter geruch'], anzeige: 'Innenraumfilter Aktivkohle', aktiv: true },
{ begriffe: ['innenspiegel abblend', 'inne spiegel auto'], anzeige: 'Innenspiegel autom. abblendend', aktiv: true },
{ begriffe: ['klima automatik', 'klimaautomatic', 'klima autom'], anzeige: 'Klimaautomatik', aktiv: false },
{ begriffe: ['lederlenkrad', 'leder lenkrad'], anzeige: 'Lederlenkrad', aktiv: false },
{ begriffe: ['lenkradheizung', 'beheizbares lenkrad', 'lenkrad heizung', 'lenkrad beheiz'], anzeige: 'Lenkradheizung', aktiv: true },
{ begriffe: ['lichtsensor'], anzeige: 'Lichtsensor', aktiv: true },
{ begriffe: ['matrix led', 'matrix scheinwerfer', 'matrix beam', 'matrix licht'], anzeige: 'Matrix Scheinwerfer', farbe: 'red', aktiv: true },
{ begriffe: ['multifunktionslenkrad', 'multifunktion lenkrad', 'multifunk lenkr'], anzeige: 'Multifunktionslenkrad', aktiv: true },
{ begriffe: ['nebelscheinwerfer', 'nebel scheinwerfer'], anzeige: 'Nebelscheinwerfer', aktiv: false },
{ begriffe: ['panorama', 'panoramadach', 'glas dach'], anzeige: 'Panoramadach', farbe: 'orange', aktiv: true },
{ begriffe: ['panorama schiebedach', 'schiebedach panorama', 'panorama schieb'], verboten: ['ohne panorama'], anzeige: 'Panoramadach elektr. schiebbar', farbe: 'orange', aktiv: true },
{ begriffe: ['pdc', 'park dist contr'], anzeige: 'Park-Distance-Control', aktiv: true },
{ begriffe: ['park assist', 'park hilfe'], anzeige: 'Parkassistent', aktiv: true },
{ begriffe: ['porsche dynam licht', 'porsche dynamic light', 'dynam licht syst'], verboten: ['licht system plus', 'porsche dynam licht plus'], anzeige: 'Porsche Dynamic Light System (PDLS)', aktiv: true },
{ begriffe: ['pdls plus', 'porsche dynam licht plus', 'dynam licht system plus'], anzeige: 'Porsche Dynamic Light System Plus (PDLS+)', aktiv: true },
{ begriffe: ['quattro'], anzeige: 'Quattro / Allrad', farbe: 'orange', aktiv: true },
{ begriffe: ['radio dab', 'empfang dab', 'radioempfang dab', 'radio digital dab'], anzeige: 'Radio digital (DAB / DAB+)', aktiv: true },
{ begriffe: ['regensensor'], anzeige: 'Regensensor', aktiv: true },
{ begriffe: ['reifen druck', 'druck kontrolle'], anzeige: 'Reifendruck Kontrollsystem', aktiv: true },
{ begriffe: ['rückfahrkamera', 'rückfahrkamerasystem'], anzeige: 'Rückfahrkamera', aktiv: true },
{ begriffe: ['scheckheft gepflegt', 'scheckheft'], anzeige: 'Scheckheftgepflegt', farbe: 'red', aktiv: true },
{ begriffe: ['keyless', 'schlüssel frei', 'schlüssellose zentral'], anzeige: 'Schlüssellose Zentralverriegelung (Keyless)', farbe: 'orange', aktiv: true },
{ begriffe: ['seiten airbag', 'airbag seite'], anzeige: 'Seitenairbag', aktiv: false },
{ begriffe: ['seitenscheibe akus', 'türscheiben akus', 'seitenscheibe verglasung'], anzeige: 'Seitenscheiben Akustikverglasung', aktiv: true, nurInFeatures: true },
{ begriffe: ['sitzbelüftung', 'sitz belüftung', 'sitzkühlung', 'sitz kühlung'], anzeige: 'Sitzbelüftung', farbe: 'red', aktiv: true },
{ begriffe: ['sitzheizung', 'sitz heizung', 'heizung sitz'], anzeige: 'Sitzheizung', farbe: 'orange', aktiv: true },
{ begriffe: ['servoschließung tür', 'soft close', 'softclose'], verboten: ['pedal', 'virtuell'], anzeige: 'Softclose', aktiv: true },
{ begriffe: ['sonnenschutzverglasung'], anzeige: 'Sonnenschutzverglasung', aktiv: true },
{ begriffe: ['sonnenschutzverglasung abgedunkelt'], anzeige: 'Sonnenschutzverglasung abgedunkelt', aktiv: true },
{ begriffe: ['spurhalte assist', 'lane assist'], anzeige: 'Spurhalteassistent', aktiv: true },
{ begriffe: ['standbelüf'], anzeige: 'Standbelüftung', aktiv: true },
{ begriffe: ['standheizung', 'standhei'], anzeige: 'Standheizung', aktiv: true },
{ begriffe: ['start stop', 'auto stop'], anzeige: 'Start/Stopp-Automatik', aktiv: true },
{ begriffe: ['tempolimit anzeige', 'tempo limit hinwe', 'geschwind limit hinwe'], anzeige: 'Tempolimit-Anzeige', aktiv: true },
{ begriffe: ['totwinkel', 'blind spot'], anzeige: 'Totwinkel-Assistent', aktiv: true },
{ begriffe: ['traction control', 'traktio kontr', 'antischlupf', 'antrieb schlupf', 'asr'], anzeige: 'Traktionskontrolle', aktiv: false },
{ begriffe: ['verkehrszeichen', 'road sign'], anzeige: 'Verkehrszeichenerkennung', aktiv: true },
{ begriffe: ['digital cockpit', 'virtual cockpit', 'volldigit kombiinstrument', 'kombiinstrument digital'], anzeige: 'Volldigitales Kombiinstrument', aktiv: true },
{ begriffe: ['winter paket', 'kalt paket'], anzeige: 'Winterpaket', aktiv: true },
{ begriffe: ['zentral verriegelung', 'central lock', 'zentralverriegelung'], anzeige: 'Zentralverriegelung', aktiv: true }
];
const techDataKonfigurationenDefault = [
{ begriff: 'Fahrzeugzustand', aktiv: true },
{ begriff: 'Erstzulassung', aktiv: true },
{ begriff: 'Innenausstattung', aktiv: true },
{ begriff: 'Farbe (Hersteller)', aktiv: true },
{ begriff: 'Farbe', aktiv: true }
];
const mergeGruppenConfigDefault = [
{ basis: 'außenspiegel', order: ['elektr. verstellbar', 'beheizbar', 'anklappbar', 'klappbar', 'automatisch abblend.', 'auto. abblend.'] }
];
// ============================================================
// 3) Migration & Konfig-Laden
// ============================================================
/**
* Vereint die begriffe-Listen einer User-Config mit den aktuellen
* Defaults (per anzeige-Schlüssel). Neue Begriffsvarianten aus den
* Defaults werden additiv ergänzt, User-eigene begriffe bleiben.
* Andere Felder (anzeige, farbe, aktiv, verboten, …) werden NICHT
* angerührt.
*/
function unionBegriffeMitDefaults(userConfig, defaults) {
if (!Array.isArray(userConfig)) return userConfig;
const defaultByAnzeige = new Map();
defaults.forEach(d => {
if (d && d.anzeige) defaultByAnzeige.set(d.anzeige.trim().toLowerCase(), d);
});
let added = 0;
const merged = userConfig.map(item => {
const key = (item.anzeige || '').trim().toLowerCase();
const def = defaultByAnzeige.get(key);
if (!def || !Array.isArray(def.begriffe)) return item;
const existing = new Set((item.begriffe || []).map(b => String(b).toLowerCase().trim()));
const additions = def.begriffe.filter(b => !existing.has(String(b).toLowerCase().trim()));
if (additions.length === 0) return item;
added += additions.length;
return { ...item, begriffe: [...(item.begriffe || []), ...additions] };
});
if (added > 0) {
console.info(`mobilede: ${added} neue Default-Begriffe in User-Config integriert.`);
}
return merged;
}
/**
* Renamings für Anzeige-Texte zwischen Schema-Versionen.
* key (lowercase, getrimmt) -> neue Anzeige.
*/
const ANZEIGE_RENAMES = {
'seitenspiegel anklappbar': 'Außenspiegel anklappbar'
};
function applyAnzeigeRenames(userConfig) {
if (!Array.isArray(userConfig)) return userConfig;
let renamed = 0;
userConfig.forEach(item => {
const key = (item.anzeige || '').trim().toLowerCase();
if (ANZEIGE_RENAMES[key]) {
item.anzeige = ANZEIGE_RENAMES[key];
renamed++;
}
});
if (renamed > 0) console.info(`mobilede: ${renamed} Anzeige-Text(e) auf neuen Default umbenannt.`);
return userConfig;
}
/**
* Korrekturen, die false-positive Treffer in bekannten Einträgen
* verhindern. Werden additiv auf User-Configs angewandt:
* - nurInFeatures: true wird gesetzt, wenn aktuell nicht true.
* - verboten: Listen werden zur bestehenden verboten-Liste hinzugefügt
* (Duplikate bereinigt). Vom User selbst entfernte Einträge können
* so wiederkommen - das ist gewollt, damit Match-Korrekturen greifen.
* Key: lowercase, getrimmtes Anzeige-Feld.
*/
const ANZEIGE_PROPERTY_UPDATES = {
'seitenscheiben akustikverglasung': { nurInFeatures: true },
'bremsassistent': { verboten: ['notbrems', 'not brems'] }
};
function applyAnzeigePropertyUpdates(userConfig) {
if (!Array.isArray(userConfig)) return userConfig;
let touched = 0;
userConfig.forEach(item => {
const key = (item.anzeige || '').trim().toLowerCase();
const upd = ANZEIGE_PROPERTY_UPDATES[key];
if (!upd) return;
if (upd.nurInFeatures === true && item.nurInFeatures !== true) {
item.nurInFeatures = true;
touched++;
}
if (Array.isArray(upd.verboten) && upd.verboten.length > 0) {
const existing = new Set((item.verboten || []).map(v => String(v).toLowerCase().trim()));
const additions = upd.verboten.filter(v => !existing.has(String(v).toLowerCase().trim()));
if (additions.length > 0) {
item.verboten = [...(item.verboten || []), ...additions];
touched++;
}
}
});
if (touched > 0) console.info(`mobilede: ${touched} Match-Korrektur(en) auf Default-Einträge angewandt.`);
return userConfig;
}
function addMissingDefaultEntries(userConfig, defaults) {
if (!Array.isArray(userConfig)) return userConfig;
const userKeys = new Set(
userConfig.map(c => (c.anzeige || '').trim().toLowerCase()).filter(Boolean)
);
const missing = defaults.filter(d => {
const k = (d.anzeige || '').trim().toLowerCase();
return k && !userKeys.has(k);
});
if (missing.length === 0) return userConfig;
console.info(
`mobilede: ${missing.length} neue Default-Eintraege ergaenzt: `
+ missing.map(m => m.anzeige).join(', ')
);
return [...userConfig, ...missing.map(d => JSON.parse(JSON.stringify(d)))];
}
function migrateMergeGroups(userMerge, defaults) {
if (!Array.isArray(userMerge)) return userMerge;
let updated = false;
const byBasis = new Map(defaults.map(g => [g.basis.toLowerCase(), g]));
const merged = userMerge.map(g => {
const def = byBasis.get((g.basis || '').toLowerCase());
if (!def) return g;
const existingOrder = new Set((g.order || []).map(o => o.toLowerCase()));
const additions = (def.order || []).filter(o => !existingOrder.has(o.toLowerCase()));
if (additions.length === 0) return g;
updated = true;
return { ...g, order: [...(g.order || []), ...additions] };
});
if (updated) console.info('mobilede: Merge-Gruppen-Reihenfolge mit neuen Default-Modifiern ergaenzt.');
return merged;
}
function migrateIfNeeded() {
const stored = ladeConfig(STORAGE_KEYS.version);
if (stored === SCHEMA_VERSION) return;
// Ausstattungs-Config: rename, union begriffe, property-updates, add missing
const userConfig = ladeConfig(STORAGE_KEYS.config);
if (Array.isArray(userConfig)) {
let next = applyAnzeigeRenames(userConfig);
next = unionBegriffeMitDefaults(next, suchKonfigurationenDefault);
next = applyAnzeigePropertyUpdates(next);
next = addMissingDefaultEntries(next, suchKonfigurationenDefault);
speichereConfig(STORAGE_KEYS.config, next);
}
// Merge-Gruppen: neue Order-Modifier additiv ergänzen
const userMerge = ladeConfig(STORAGE_KEYS.mergeGroups);
if (Array.isArray(userMerge)) {
const next = migrateMergeGroups(userMerge, mergeGruppenConfigDefault);
speichereConfig(STORAGE_KEYS.mergeGroups, next);
}
speichereConfig(STORAGE_KEYS.version, SCHEMA_VERSION);
}
migrateIfNeeded();
let suchKonfigurationen = ladeConfig(STORAGE_KEYS.config) || suchKonfigurationenDefault;
let techDataKonfigurationen = ladeConfig(STORAGE_KEYS.techConfig) || techDataKonfigurationenDefault;
let mergeGruppenConfig = ladeConfig(STORAGE_KEYS.mergeGroups) || mergeGruppenConfigDefault;
let featureFlags = ladeFeatureFlags();
// ============================================================
// 4) Textaufbereitung & Tokenisierung
// ============================================================
function cleanText(text) {
if (!text) return '';
return text
.replace(/ä/g, 'ae').replace(/Ä/g, 'Ae')
.replace(/ö/g, 'oe').replace(/Ö/g, 'Oe')
.replace(/ü/g, 'ue').replace(/Ü/g, 'Ue')
.replace(/ß/g, 'ss')
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[–—\-]+/g, ' ')
.replace(/[\n\r\t]+/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/[,;:|()\[\]"']/g, ' ')
.replace(/\s{2,}/g, ' ')
.trim()
.toLowerCase();
}
function tokenize(text) {
const cleaned = cleanText(text);
if (!cleaned) return [];
return cleaned.split(/\s+/).filter(Boolean);
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// ============================================================
// 5) Match-Engine
// - tokenMatches: exakt, Prefix oder Suffix (>= 4 Zeichen),
// Substring nur bei compound: true.
// - matchInTokens: Sliding-Window über Trefferpositionen,
// O(n log n) statt kartesischem Produkt.
// ============================================================
const MAX_WORD_GAP = { 1: 0, 2: 3, 3: 6, 4: 10, 5: 14 };
function getMaxWordGap(parts) {
return MAX_WORD_GAP[parts] || (parts > 5 ? parts * 3 : 0);
}
function tokenMatches(token, part, compound) {
if (!token || !part) return false;
if (token === part) return true;
if (part.length < 4) return false;
if (compound) return token.includes(part);
if (token.startsWith(part) || token.endsWith(part)) return true;
// Mid-substring nur ab 5 Zeichen erlauben, um false-positives bei
// 4-Zeichen-Patterns (head, glas, heiz, ende, …) zu vermeiden.
if (part.length >= 5 && token.includes(part)) return true;
return false;
}
function findPositions(tokens, part, compound) {
const positions = [];
for (let i = 0; i < tokens.length; i++) {
if (tokenMatches(tokens[i], part, compound)) positions.push(i);
}
return positions;
}
/**
* Sucht ein Fenster in `tokens`, das alle `parts` enthält und
* dabei höchstens maxGap Wörter Differenz zwischen erstem und
* letztem getroffenen Token hat.
* Gibt {startIdx, endIdx} oder null zurück.
*/
function matchInTokens(tokens, parts, maxGap, compound) {
if (parts.length === 0 || tokens.length === 0) return null;
if (parts.length === 1) {
const pos = findPositions(tokens, parts[0], compound);
if (pos.length === 0) return null;
return { startIdx: pos[0], endIdx: pos[0] };
}
const positionLists = parts.map(p => findPositions(tokens, p, compound));
for (const list of positionLists) {
if (list.length === 0) return null;
}
// Pointer pro Liste -> sliding window
const pointers = new Array(positionLists.length).fill(0);
let best = null;
while (true) {
const current = positionLists.map((list, i) => list[pointers[i]]);
const min = Math.min(...current);
const max = Math.max(...current);
if (max - min <= maxGap) {
if (!best || (max - min) < (best.endIdx - best.startIdx)) {
best = { startIdx: min, endIdx: max };
if (max - min === parts.length - 1) return best;
}
}
// bewege den Pointer mit dem kleinsten Wert weiter
const minListIdx = current.indexOf(min);
pointers[minListIdx]++;
if (pointers[minListIdx] >= positionLists[minListIdx].length) break;
}
return best;
}
function isForbiddenInWindow(tokens, window, verboten) {
if (!verboten || verboten.length === 0) return false;
const slice = tokens.slice(window.startIdx, window.endIdx + 1).join(' ');
const pattern = new RegExp(verboten.map(escapeRegex).join('|'), 'i');
return pattern.test(slice);
}
// ============================================================
// 6) Quellen-Extraktion (lazy, mit heuristischem Fallback)
// ============================================================
function getFeatureItems() {
return Array.from(document.querySelectorAll("ul[data-testid='vip-features-list'] li"));
}
function getDescriptionEl() {
return document.querySelector("div[data-testid='vip-vehicle-description-text']");
}
function getTechDataDl() {
return document.querySelector("article[data-testid='vip-technical-data-box'] dl");
}
/**
* Heuristik-Fallback für den ehemaligen ".GOIOV fqe3L EevEz"-Block:
* sucht ein Geschwister-Element zur Beschreibung, das zusätzliche
* Texte enthält (Verkäufer-Hinweise etc.). Bei Layout-Änderung
* von mobile.de bleibt das Skript funktional.
*/
function getZusatzEl() {
const desc = getDescriptionEl();
if (!desc) return null;
const candidate = desc.parentElement && desc.parentElement.nextElementSibling;
if (candidate && candidate.textContent && candidate.textContent.trim().length > 20) {
return candidate;
}
return null;
}
/**
* Heuristik: erkennt einen Beschreibungs-Block, der in Wahrheit eine
* Komma-getrennte Feature-Liste ist (typischer mobile.de-Block). Wenn
* ja, wird der Block als 'high' confidence eingestuft, sodass auch
* Einträge mit nurInFeatures: true ihn berücksichtigen.
*/
function classifyDescription(rawText) {
if (!rawText) return 'low';
const items = rawText.split(/,/).map(s => s.trim()).filter(Boolean);
// Eindeutige Komma-Liste: viele Items → strukturierte Ausstattung.
if (items.length >= 12) return 'high';
if (items.length >= 6) {
// Anteil kurzer Items zählen statt nur Mittelwert (robuster gegen
// einzelne lange Items wie "Multi-Media-Interface MMI Navigation").
const shortRatio = items.filter(it => it.split(/\s+/).length <= 5).length / items.length;
if (shortRatio >= 0.6) return 'high';
}
return 'low';
}
/**
* Liefert eine Liste von Quellen mit confidence:
* - features -> high
* - tech -> high
* - description -> high (wenn Komma-Liste) sonst low
* - zusatz -> low
*/
function extractSources() {
const sources = [];
const featureItems = getFeatureItems();
if (featureItems.length > 0) {
const text = featureItems.map(li => li.textContent.trim()).filter(Boolean).join(' | ');
sources.push({ id: 'features', confidence: 'high', text, tokens: tokenize(text) });
}
const techDl = getTechDataDl();
if (techDl) {
const text = techDl.textContent.replace(/\s+/g, ' ').trim();
sources.push({ id: 'tech', confidence: 'high', text, tokens: tokenize(text) });
}
const desc = getDescriptionEl();
if (desc) {
const rawText = desc.textContent.replace(/\s+/g, ' ').trim();
const confidence = classifyDescription(rawText);
const text = rawText.replace(/,/g, ' ');
sources.push({ id: 'description', confidence, text, tokens: tokenize(text) });
}
const zusatz = getZusatzEl();
if (zusatz) {
const text = zusatz.textContent.trim();
sources.push({ id: 'zusatz', confidence: 'low', text, tokens: tokenize(text) });
}
return sources;
}
// ============================================================
// 7) Begriffs-Suche (ersetzt sucheBegriffe)
// ============================================================
function sucheBegriffe() {
const sources = extractSources();
if (sources.length === 0) return [];
const gefundene = [];
suchKonfigurationen.forEach(cfg => {
if (!cfg.aktiv) return;
if (!Array.isArray(cfg.begriffe) || cfg.begriffe.length === 0) return;
const onlyHigh = cfg.nurInFeatures === true;
const compound = cfg.compound === true;
for (const src of sources) {
if (onlyHigh && src.confidence !== 'high') continue;
let matched = false;
for (const begriff of cfg.begriffe) {
const parts = tokenize(begriff);
if (parts.length === 0) continue;
const maxGap = getMaxWordGap(parts.length);
const window = matchInTokens(src.tokens, parts, maxGap, compound);
if (!window) continue;
if (cfg.verboten && cfg.verboten.length > 0) {
const forbiddenParts = cfg.verboten
.map(v => cleanText(v))
.filter(Boolean);
if (isForbiddenInWindow(src.tokens, window, forbiddenParts)) {
console.debug('Verbotenes Token im Fenster für', cfg.anzeige, '→ skip');
continue;
}
}
const snippetTokens = src.tokens.slice(
Math.max(0, window.startIdx - 2),
Math.min(src.tokens.length, window.endIdx + 3)
);
gefundene.push({
anzeige: cfg.anzeige,
farbe: (cfg.farbe || '#66ff66').toLowerCase(),
source: src.id,
confidence: src.confidence,
snippet: snippetTokens.join(' '),
begriff
});
matched = true;
break;
}
if (matched) break;
}
});
// Dedup: gleiche anzeige nur einmal, dabei beste confidence behalten
const byAnzeige = new Map();
for (const item of gefundene) {
const existing = byAnzeige.get(item.anzeige);
if (!existing) { byAnzeige.set(item.anzeige, item); continue; }
const existingHigh = existing.confidence === 'high';
const itemHigh = item.confidence === 'high';
if (!existingHigh && itemHigh) byAnzeige.set(item.anzeige, item);
}
let unique = [...byAnzeige.values()];
unique.sort((a, b) => a.anzeige.localeCompare(b.anzeige));
// Substring-Dedup: kürzeren Eintrag entfernen, wenn ein längerer
// Eintrag existiert, der ALLE Tokens des kürzeren als komplette
// Tokens enthält (kein Prefix-Hack mehr).
unique = subsetDedup(unique);
// Generalisiertes Merging
unique = generalizedMergeEntries(unique, mergeGruppenConfig);
// Endgültige alphabetische Sortierung
unique.sort((a, b) => a.anzeige.localeCompare(b.anzeige));
console.debug('Gefundene Begriffe:', unique.map(i => `${i.anzeige} [${i.source}]`));
return unique;
}
function subsetDedup(entries) {
const tokenSets = entries.map(e => new Set(tokenize(e.anzeige)));
const result = [];
for (let i = 0; i < entries.length; i++) {
const a = tokenSets[i];
let dropped = false;
for (let j = 0; j < entries.length; j++) {
if (i === j) continue;
const b = tokenSets[j];
if (b.size <= a.size) continue;
let containsAll = true;
for (const t of a) {
if (!b.has(t)) { containsAll = false; break; }
}
if (containsAll) { dropped = true; break; }
}
if (!dropped) result.push(entries[i]);
}
return result;
}
function generalizedMergeEntries(entries, gruppen) {
if (!Array.isArray(gruppen) || gruppen.length === 0) return entries;
let result = [...entries];
gruppen.forEach(group => {
if (!group || !group.basis) return;
const basis = group.basis.toLowerCase();
const order = (group.order || []).map(item => item.toLowerCase());
const matching = result.filter(e => e.anzeige.toLowerCase().includes(basis));
if (matching.length <= 1) return;
result = result.filter(e => !e.anzeige.toLowerCase().includes(basis));
let modifiers = matching
.map(e => e.anzeige.toLowerCase().replace(basis, '').trim())
.filter(Boolean);
modifiers = Array.from(new Set(modifiers));
modifiers.sort((a, b) => {
let ia = order.findIndex(key => a.includes(key.replace(/\./g, '').trim()));
let ib = order.findIndex(key => b.includes(key.replace(/\./g, '').trim()));
if (ia === -1) ia = 999;
if (ib === -1) ib = 999;
return ia - ib;
});
const basisCap = group.basis.charAt(0).toUpperCase() + group.basis.slice(1);
const merged = basisCap + (modifiers.length ? ' ' + modifiers.join(', ') : '');
// beste confidence der Gruppe übernehmen
const bestConf = matching.some(e => e.confidence === 'high') ? 'high' : 'low';
const sources = [...new Set(matching.map(e => e.source))].join(',');
result.push({
anzeige: merged,
farbe: matching[0].farbe,
source: sources,
confidence: bestConf
});
});
return result;
}
// ============================================================
// 8) Suche nach Technischen Daten
// ============================================================
function sucheTechnischeDaten() {
const techDataBereich = getTechDataDl();
if (!techDataBereich) return [];
const dtElements = techDataBereich.querySelectorAll('dt');
const daten = [];
techDataKonfigurationen.forEach(cfg => {
if (!cfg.aktiv) return;
for (const dt of dtElements) {
if (dt.textContent.trim().toLowerCase() === cfg.begriff.toLowerCase()) {
const dd = dt.nextElementSibling;
if (dd && dd.tagName.toLowerCase() === 'dd') {
daten.push({ title: cfg.begriff, value: dd.textContent.trim() });
}
break;
}
}
});
return daten;
}
function technischeDatenHinzufuegen(parentElement) {
const technischeDaten = sucheTechnischeDaten();
if (technischeDaten.length === 0) return;
const techArticle = document.createElement('article');
techArticle.className = 'A3G6X lAeeF vTKPY HaBLt ku0Os mobilede-tech-article';
techArticle.style.marginBottom = '10px';
const techContainer = document.createElement('div');
Object.assign(techContainer.style, {
border: '1px solid #8a2be2',
padding: '10px',
backgroundColor: '#1e1f24',
color: 'white',
width: '100%',
textAlign: 'left',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
lineHeight: '1.5',
display: 'block'
});
const title = document.createElement('div');
title.textContent = 'Technische Daten:';
title.style.color = 'white';
title.style.marginBottom = '5px';
techContainer.appendChild(title);
const table = document.createElement('table');
table.style.width = '100%';
table.style.borderCollapse = 'collapse';
technischeDaten.forEach(d => {
const tr = document.createElement('tr');
const tdKey = document.createElement('td');
tdKey.textContent = d.title + ':';
Object.assign(tdKey.style, { color: 'white', paddingRight: '20px', whiteSpace: 'nowrap', verticalAlign: 'top' });
const tdValue = document.createElement('td');
tdValue.textContent = d.value;
Object.assign(tdValue.style, { color: 'white', width: '100%', verticalAlign: 'top' });
tr.appendChild(tdKey);
tr.appendChild(tdValue);
table.appendChild(tr);
});
techContainer.appendChild(table);
techArticle.appendChild(techContainer);
parentElement.parentNode.insertBefore(techArticle, parentElement);
}
// ============================================================
// 9) Render: Ergebnis-Article einfügen
// ============================================================
function ergebnisHinzufuegen() {
if (document.querySelector('#ergebnisBereich')) return;
const zielBereich = document.querySelector("article[data-testid='vip-key-features-box']");
if (!zielBereich) return;
const gefundeneTexte = sucheBegriffe();
const article = document.createElement('article');
article.className = 'A3G6X lAeeF vTKPY HaBLt ku0Os mobilede-result-article';
const ergebnisBereich = document.createElement('div');
ergebnisBereich.id = 'ergebnisBereich';
Object.assign(ergebnisBereich.style, {
border: '1px solid #8a2be2',
padding: '10px',
marginTop: '10px',
backgroundColor: '#1e1f24',
color: 'white',
width: '100%',
textAlign: 'left',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
fontSize: '14px',
lineHeight: '1.5',
display: 'block'
});
article.appendChild(ergebnisBereich);
const title = document.createElement('div');
title.style.color = 'white';
title.style.marginBottom = '5px';
title.style.width = '100%';
title.textContent = 'Gefundene Begriffe:';
ergebnisBereich.appendChild(title);
if (gefundeneTexte.length > 0) {
const columns = document.createElement('div');
Object.assign(columns.style, {
display: 'grid',
gridTemplateColumns: 'repeat(2, minmax(0, 1fr))',
columnGap: '24px',
alignItems: 'start'
});
const leftColumn = document.createElement('div');
const rightColumn = document.createElement('div');
[leftColumn, rightColumn].forEach(col => {
Object.assign(col.style, {
display: 'flex',
flexDirection: 'column',
gap: '2px',
minWidth: '0'
});
});
gefundeneTexte.forEach((item, index) => {
const el = document.createElement('div');
const isLow = item.confidence === 'low';
el.style.minWidth = '0';
// Tooltip + Help-Cursor liegen NUR auf dem inneren Span,
// sodass der Cursor außerhalb des Textes normal bleibt.
const span = document.createElement('span');
span.textContent = `- ${item.anzeige}${isLow ? ' *' : ''}`;
span.style.color = item.farbe;
span.style.cursor = 'help';
span.style.overflowWrap = 'anywhere';
span.style.display = 'inline-block';
span.style.paddingLeft = '0.6em';
span.style.textIndent = '-0.6em';
const sourceLabel = isLow
? `Nur in Beschreibung gefunden (Quelle: ${item.source})`
: `Quelle: ${item.source}`;
const trigger = item.begriff ? `\nTrigger: "${item.begriff}"` : '';
const snippet = item.snippet ? `\nKontext: …${item.snippet}…` : '';
span.title = sourceLabel + trigger + snippet;
if (isLow) {
span.style.fontStyle = 'italic';
span.style.opacity = '0.85';
}
el.appendChild(span);
(index % 2 === 0 ? leftColumn : rightColumn).appendChild(el);
});
columns.appendChild(leftColumn);
columns.appendChild(rightColumn);
ergebnisBereich.appendChild(columns);
const hasLow = gefundeneTexte.some(i => i.confidence === 'low');
if (hasLow) {
const legend = document.createElement('div');
legend.style.width = '100%';
legend.style.fontSize = '11px';
legend.style.opacity = '0.7';
legend.style.marginTop = '6px';
legend.textContent = '* = nur in Beschreibungstext gefunden (geringere Sicherheit)';
ergebnisBereich.appendChild(legend);
}
} else {
const keine = document.createElement('div');
keine.textContent = 'Keine der gesuchten Begriffe gefunden.';
keine.style.color = 'white';
ergebnisBereich.appendChild(keine);
}
zielBereich.parentNode.insertBefore(article, zielBereich.nextSibling);
technischeDatenHinzufuegen(article);
}
function clearResults() {
document.querySelectorAll('.mobilede-result-article, .mobilede-tech-article').forEach(el => el.remove());
}
// ============================================================
// 10) Lifecycle: Observer + SPA-Navigation
// ============================================================
let observer = null;
let triggerTimer = null;
function trigger() {
clearTimeout(triggerTimer);
triggerTimer = setTimeout(() => {
try { ergebnisHinzufuegen(); } catch (e) { console.error(e); }
try { verlinkeStandortAufGoogleMaps(); } catch (e) { console.error(e); }
}, 300);
}
/**
* Macht Standort-Texte (z.B. „DE-92690 Pressath", „AT-1010 Wien") überall
* auf der Seite klickbar: Klick öffnet Google Maps mit der Adresse.
* Robust gegen mobile.de Class-Name-Änderungen via Pattern-Match auf den
* Textinhalt einzelner Blatt-Elemente (kein Scope-Restrictor, damit auch
* Standorte außerhalb der Aktions-Box `.Va7Gr` erfasst werden, z.B. in
* `aside.iKWwq` der Verkäufer-Karte).
*
* Per Feature-Flag (`featureFlags.mapsLink`) abschaltbar – Listener werden
* mit AbortController gekoppelt und beim Ausschalten abgemeldet, damit
* keine Duplikate entstehen und die Optik beim Wiedereinschalten stimmt.
*/
function verlinkeStandortAufGoogleMaps() {
const enabled = !!(featureFlags && featureFlags.mapsLink !== false);
const re = /^[A-Z]{2}-\d{4,5}\s+\S.*$/;
const candidates = document.querySelectorAll('div, span, p, address');
const matched = [];
for (const el of candidates) {
if (!el || !el.dataset) continue;
if (el.children && el.children.length > 0) continue;
const txt = (el.textContent || '').trim();
if (txt.length < 6 || txt.length > 80) continue;
if (!re.test(txt)) continue;
if (el.closest && el.closest('#mobilede-config-popup')) continue;
matched.push({ el, txt });
}
if (!enabled) {
matched.forEach(({ el }) => {
if (el.dataset.mobiledeStandort !== '1') return;
const ctl = el._mobileDeMapsCtl;
if (ctl && typeof ctl.abort === 'function') {
try { ctl.abort(); } catch (_) { /* noop */ }
}
el._mobileDeMapsCtl = null;
el.style.cursor = '';
el.style.textDecoration = '';
el.style.textDecorationStyle = '';
el.style.textUnderlineOffset = '';
el.style.opacity = '';
el.removeAttribute('role');
el.removeAttribute('tabindex');
el.removeAttribute('title');
delete el.dataset.mobiledeStandort;
});
return;
}
matched.forEach(({ el, txt }) => {
el.style.cursor = 'pointer';
el.style.textDecoration = 'underline';
el.style.textDecorationStyle = 'dotted';
el.style.textUnderlineOffset = '3px';
el.title = 'In Google Maps öffnen: ' + txt;
el.setAttribute('role', 'link');
el.setAttribute('tabindex', '0');
const existing = el._mobileDeMapsCtl;
if (el.dataset.mobiledeStandort === '1' && existing && !existing.signal.aborted) {
return;
}
if (existing && typeof existing.abort === 'function') {
try { existing.abort(); } catch (_) { /* noop */ }
}
el.dataset.mobiledeStandort = '1';
const ac = new AbortController();
el._mobileDeMapsCtl = ac;
const opts = { signal: ac.signal };
el.addEventListener('mouseenter', () => {
if (!featureFlags || featureFlags.mapsLink === false) return;
el.style.textDecorationStyle = 'solid';
el.style.opacity = '0.85';
}, opts);
el.addEventListener('mouseleave', () => {
el.style.textDecorationStyle = 'dotted';
el.style.opacity = '';
}, opts);
const open = () => {
const url = 'https://www.google.com/maps/search/?api=1&query=' + encodeURIComponent(txt);
window.open(url, '_blank', 'noopener,noreferrer');
};
el.addEventListener(
'click',
e => {
if (!featureFlags || featureFlags.mapsLink === false) return;
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
open();
},
{ capture: true, signal: ac.signal }
);
el.addEventListener('keydown', e => {
if (!featureFlags || featureFlags.mapsLink === false) return;
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); }
}, opts);
});
}
function startObserver() {
if (observer) observer.disconnect();
// Wir lassen den Observer dauerhaft laufen; trigger() ist debounced
// und ergebnisHinzufuegen() bricht früh ab, wenn bereits eingefügt.
// Bei SPA-Re-Renders (DOM ohne URL-Wechsel) wird so neu gerendert.
observer = new MutationObserver(() => trigger());
observer.observe(document.body, { childList: true, subtree: true });
}
let lastUrl = location.href;
function onUrlChange() {
if (location.href === lastUrl) return;
lastUrl = location.href;
clearResults();
startObserver();
trigger();
// Konfig-Button neu setzen, falls Parent re-rendered wurde
setTimeout(() => {
if (!document.querySelector('#mobilede-config-btn')) erstelleKonfigButton();
}, 1500);
}
window.addEventListener('popstate', onUrlChange);
window.addEventListener('hashchange', onUrlChange);
setInterval(onUrlChange, 1000);
startObserver();
trigger();
// Hilfe-Texte für Konfig-Popup (Tabs). Statisches HTML, nur innerHTML aus diesem Map.
const KONFIG_TAB_HELP_HTML = new Map([
['aus', `
Was macht das?
Hier konfigurierst du, welche Ausstattungsbegriffe (z.B. „Sitzheizung", „Panoramadach") auf einer mobile.de-Detailseite gesucht und im Ergebnis angezeigt werden.
So bedienst du es:
Aktiv-Schalter (links): Eintrag ein-/ausschalten. Inaktive werden ignoriert.
Anzeigetext: Wie der Treffer im Ergebnisbereich erscheint (z.B. „Sitzheizung").
Farbe: Hintergrundakzent im Ergebnis. Klick auf das Quadrat öffnet einen Color-Picker; alternativ Hex-Code (#66ff66) oder Schlüsselwort (red, orange).
Nur Ausstattungsliste: Treffer werden nur in der strukturierten Ausstattungsliste / Tech-Daten gezählt. Beschreibungstext wird ignoriert. Empfohlen für sicherheitskritische Begriffe wie „Anhängerkupplung" oder Sound-Systeme.
Wortteil-Suche: Erlaubt Treffer auch mitten in zusammengesetzten Wörtern (z.B. „heizung" findet „Standheizung"). Vorsicht: kann False-Positives erzeugen.
Details [N] öffnet erweiterte Optionen mit den eigentlichen Suchbegriffen und Verboten (Komma-getrennt).
Ziehen (links das ⋮⋮-Symbol) ändert die Reihenfolge im gespeicherten Konfig – hat keinen Einfluss auf das Ergebnis (das ist alphabetisch).
Bulk-Aktionen in der Toolbar wirken auf alle bzw. die aktuell sichtbaren Einträge nach Filter.
`],
['tech', `
Was macht das?
Hier wählst du, welche technischen Datenfelder (aus dem mobile.de-Tech-Daten-Block) zusätzlich im Ergebnis angezeigt werden, z.B. „Erstzulassung" oder „Fahrzeugzustand".
So bedienst du es:
Aktiv-Schalter zum Ein-/Ausblenden.
Begriff: Muss exakt mit dem <dt>-Label aus dem mobile.de-Tech-Daten-Block übereinstimmen (Groß-/Kleinschreibung egal).
Bulk-Aktionen und Suche funktionieren wie auf der Ausstattungs-Seite.
Reihenfolge per Drag&Drop ändert die Anzeigereihenfolge im Tech-Daten-Block.
`],
['merge', `
Was macht das?
Mehrere getrennt gefundene Einträge mit gleichem Basis-Wort werden zu einer Zeile zusammengefasst. Beispiel: „Außenspiegel beheizbar", „Außenspiegel anklappbar", „Außenspiegel elektr. verstellbar" → eine Zeile Außenspiegel beheizbar, anklappbar, elektr. verstellbar.
So bedienst du es:
Basis: Das gemeinsame Wort, nach dem gruppiert wird (z.B. außenspiegel). Klein- und Großschreibung egal.
Reihenfolge: Komma-getrennte Liste der Modifizierer-Schlüsselwörter in der gewünschten Reihenfolge im zusammengefassten Eintrag (z.B. elektr. verstellbar, beheizbar, anklappbar). Treffer, die in keiner Reihenfolge auftauchen, kommen ans Ende.
`],
['ie', `
Was macht das?
Komplette Konfiguration als JSON sichern oder einspielen – praktisch zum Wechsel zwischen Browsern oder zum Verteilen einer Standardkonfiguration.
So bedienst du es:
Export aktualisieren generiert das aktuelle JSON. Kopieren legt es in die Zwischenablage; Herunterladen speichert eine Datei mobilede-config-YYYY-MM-DD.json.
Import: JSON entweder per Drag&Drop der Datei auf die Drop-Zone oder direkt in die Textarea einfügen. Importieren überschreibt die aktuelle Konfiguration; ein automatisches Backup wird vorher angelegt und kann per Rückgängig im Footer zurückgeholt werden.
`],
['config', `
Was macht das?
Hier schaltest du Zusatz-Features des Skripts global ein oder aus. Änderungen werden mit Speichern übernommen und greifen sofort – auch ohne Seiten-Reload.
So bedienst du es:
Jede Karte beschreibt ein Feature und besitzt einen Toggle.
Beim Deaktivieren werden bereits aktive Manipulationen (z.B. die Maps-Verlinkung) auf der gerade geöffneten Detailseite optisch zurückgenommen.
Neue Features werden automatisch mit ihren Standardwerten ergänzt; bestehende Einstellungen bleiben erhalten.