您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden.
当前为
// ==UserScript== // @name mydealz Manager // @namespace http://tampermonkey.net/ // @version 1.12.0 // @description Deals gezielt ausblenden mittels X Button, Filtern nach Händlern und Wörtern im Titel. Teure und kalte Deals ausblenden. // @author Moritz Baumeister (https://www.mydealz.de/profile/BobBaumeister) (https://github.com/grapefruit89) & Flo (https://www.mydealz.de/profile/Basics0119) (https://github.com/9jS2PL5T) // @license MIT // @match https://www.mydealz.de/* // @match https://www.preisjaeger.at/* // @icon https://www.google.com/s2/favicons?sz=64&domain=mydealz.de // @grant none // ==/UserScript== // Versions-Änderungen: // NEU: mydealz Manager passt sich nun an das dunkle Design der Seite an. // Einbinden von Font Awesome für Icons const fontAwesomeLink = document.createElement('link'); fontAwesomeLink.rel = 'stylesheet'; fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css'; document.head.appendChild(fontAwesomeLink); // Add constant for touch detection const IS_TOUCH_DEVICE = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); // Dark mode color constants const THEME_COLORS = { light: { background: '#f9f9f9', border: '#ccc', text: '#333', buttonBg: '#f0f0f0', buttonBorder: '#ccc', inputBg: '#fff', itemBg: '#f0f0f0', itemHoverBg: '#e8e8e8' }, dark: { background: '#1f1f1f', border: '#2d2d2d', text: '#ffffff', buttonBg: '#2d2d2d', buttonBorder: '#3d3d3d', inputBg: '#2d2d2d', itemBg: '#2d2d2d', itemHoverBg: '#3d3d3d' } }; const themeObserver = new MutationObserver(() => { if (isSettingsOpen || activeSubUI) { updateUITheme(); } }); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); // --- 1. Initial Setup --- const EXCLUDE_WORDS_KEY = 'excludeWords'; const EXCLUDE_MERCHANTS_KEY = 'excludeMerchantIDs'; const HIDDEN_DEALS_KEY = 'hiddenDeals'; const EXCLUDE_MERCHANTS_DATA_KEY = 'excludeMerchantsData'; const MERCHANT_PAGE_SELECTOR = '.merchant-banner'; const HIDE_COLD_DEALS_KEY = 'hideColdDeals'; const MAX_PRICE_KEY = 'maxPrice'; // Load data immediately let excludeWords, excludeMerchantIDs, hiddenDeals; let suggestedWords = []; let initialSuggestionListCreated = false; let activeSubUI = null; let dealThatOpenedSettings = null; let settingsDiv = null; let merchantListDiv = null; let wordsListDiv = null; let uiClickOutsideHandler = null; let isSettingsOpen = false; let hideColdDeals = localStorage.getItem(HIDE_COLD_DEALS_KEY) === 'true'; try { excludeWords = JSON.parse(localStorage.getItem(EXCLUDE_WORDS_KEY)) || []; excludeMerchantIDs = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || []; hiddenDeals = JSON.parse(localStorage.getItem(HIDDEN_DEALS_KEY)) || []; } catch (error) { excludeWords = []; excludeMerchantIDs = []; hiddenDeals = []; } // Add to init section after other load statements let maxPrice = parseFloat(localStorage.getItem(MAX_PRICE_KEY)) || 0; // Add after other global vars let suggestionClickHandler = null; function isDarkMode() { const isLight = document.documentElement.classList.contains('light'); return !isLight; } const hideButtonThemeObserver = new MutationObserver(() => { requestAnimationFrame(() => { document.querySelectorAll('.vote-button').forEach(button => { if (button) { const isDark = !document.documentElement.classList.contains('light'); const bgColor = isDark ? 'rgba(45, 45, 45, 0.9)' : 'rgba(255, 255, 255, 0.9)'; button.style.setProperty('background', bgColor, 'important'); button.style.setProperty('border-radius', '50%', 'important'); } }); }); }); hideButtonThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); function initUIContainers() { settingsDiv = document.createElement('div'); merchantListDiv = document.createElement('div'); wordsListDiv = document.createElement('div'); } // --- 1. Core Functions --- function processArticles() { const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher'); deals.forEach(deal => { const dealId = deal.getAttribute('id'); // Check if manually hidden if (hiddenDeals.includes(dealId)) { hideDeal(deal); return; } // Check if should be hidden by rules if (shouldExcludeArticle(deal)) { hideDeal(deal); return; } // Show deal if not excluded deal.style.display = 'block'; deal.style.opacity = '1'; }); } function getThemeColors() { const isLight = document.documentElement.classList.contains('light'); return isLight ? THEME_COLORS.light : THEME_COLORS.dark; } // Update word/merchant item styles in list creation function updateItemStyles(item, colors) { item.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px; `; } // Update createMerchantListUI and createExcludeWordsUI function updateListStyles(listDiv, colors) { // Apply styles to list items listDiv.querySelectorAll('.merchant-item, .word-item').forEach(item => { updateItemStyles(item, colors); }); // Update search input const searchInput = listDiv.querySelector('input[type="text"]'); if (searchInput) { searchInput.style.cssText = ` width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px; `; } // Update clear button const clearButton = listDiv.querySelector('[id*="clear"]'); if (clearButton) { clearButton.style.cssText = ` width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px; `; } } function updateUITheme() { const colors = getThemeColors(); [settingsDiv, merchantListDiv, wordsListDiv].forEach(div => { if (div?.parentNode) { div.style.background = colors.background; div.style.border = `1px solid ${colors.border}`; div.style.color = colors.text; // Update all buttons and inputs div.querySelectorAll('button:not([id*="close"])').forEach(btn => { btn.style.background = colors.buttonBg; btn.style.border = `1px solid ${colors.buttonBorder}`; btn.style.color = colors.text; }); div.querySelectorAll('input').forEach(input => { input.style.background = colors.inputBg; input.style.border = `1px solid ${colors.border}`; input.style.color = colors.text; }); } }); } // Define observer const observer = new MutationObserver(throttle((mutations) => { processArticles(); addSettingsButton(); addHideButtons(); }, 250)); function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => { inThrottle = false; return false; }, limit); } } } // Initialize everything (function init() { processArticles(); addSettingsButton(); addHideButtons(); observer.observe(document.body, { childList: true, subtree: true }); })(); // --- 2. Hilfsfunktionen --- function shouldExcludeArticle(article) { // Add temperature check if (hideColdDeals) { const tempElement = article.querySelector('.cept-vote-temp .overflow--wrap-off'); if (tempElement) { const temp = parseInt(tempElement.textContent); if (!isNaN(temp) && temp < 0) { return true; } } } const titleElement = article.querySelector('.thread-title'); const merchantLink = article.querySelector('a[href*="merchant-id="]'); // Add price check if (maxPrice > 0) { // Try multiple price selectors const priceSelectors = [ '.threadItemCard-price', '.thread-price', '[class*="price"]', '.cept-tp' ]; let priceElement; for (const selector of priceSelectors) { priceElement = article.querySelector(selector); if (priceElement) break; } if (priceElement) { try { const priceText = priceElement.textContent.trim(); // Extract price with euro symbol const priceMatch = priceText.match(/(\d+(?:[,.]\d{1,2})?)\s*€/); if (priceMatch) { const priceStr = priceMatch[1].replace(',', '.'); const price = parseFloat(priceStr); if (!isNaN(price) && price > maxPrice) { return true; } } } catch (error) { } } } // Verbesserte Titelprüfung mit HTML Entity Dekodierung if (titleElement) { const normalizedTitle = decodeHtml(titleElement.innerHTML) .replace(/ /g, ' ') .replace(/\s+/g, ' ') .toLowerCase() .trim(); // Check for exact word matches using word boundaries if (excludeWords.some(word => { const escapedWord = word.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const wordRegex = new RegExp(`\\b${escapedWord}\\b`, 'i'); return wordRegex.test(normalizedTitle); })) { return true; } } if (merchantLink) { const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/); if (merchantIDMatch) { const merchantID = merchantIDMatch[1]; if (excludeMerchantIDs.includes(merchantID)) { return true; } } } return false; } function hideDeal(deal) { deal.style.display = 'none'; } // Funktion zum Speichern der ausgeblendeten Deals function saveHiddenDeals() { localStorage.setItem(HIDDEN_DEALS_KEY, JSON.stringify(hiddenDeals)); } // Speichern der `excludeWords` und `excludeMerchantIDs` function saveExcludeWords(words) { localStorage.setItem('excludeWords', JSON.stringify(words)); } function loadExcludeWords() { return JSON.parse(localStorage.getItem('excludeWords')) || []; } // Update save function to handle merchant data objects function saveExcludeMerchants(merchantsData) { // Filter out invalid entries const validMerchants = merchantsData.filter(m => m && typeof m.id !== 'undefined' && m.id !== null && typeof m.name !== 'undefined' && m.name !== null ); // Extract IDs for backwards compatibility const ids = validMerchants.map(m => m.id); localStorage.setItem(EXCLUDE_MERCHANTS_KEY, JSON.stringify(ids)); localStorage.setItem(EXCLUDE_MERCHANTS_DATA_KEY, JSON.stringify(validMerchants)); // Update global array excludeMerchantIDs = ids; } // Load function for merchant data function loadExcludeMerchants() { const merchantsData = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_DATA_KEY)) || []; const legacyIds = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || []; // Filter out invalid entries const validMerchants = merchantsData.filter(m => m && typeof m.id !== 'undefined' && m.id !== null && typeof m.name !== 'undefined' && m.name !== null ); // Convert legacy IDs if needed if (validMerchants.length === 0 && legacyIds.length > 0) { return legacyIds .filter(id => id && typeof id !== 'undefined') .map(id => ({ id, name: id })); } return validMerchants; } // Clean up existing data on script init (function cleanupMerchantData() { const merchants = loadExcludeMerchants(); saveExcludeMerchants(merchants); })(); // Fügt Event Listener hinzu, um Auto-Speichern zu ermöglichen function addAutoSaveListeners() { // Event Listener für Eingabefelder const excludeWordsInput = document.getElementById('excludeWordsInput'); excludeWordsInput.addEventListener('input', () => { const newWords = excludeWordsInput.value.split('\n').map(w => w.trim()).filter(Boolean); saveExcludeWords(newWords); excludeWords = newWords; processArticles(); }); const excludeMerchantIDsInput = document.getElementById('excludeMerchantIDsInput'); excludeMerchantIDsInput.addEventListener('input', () => { const newMerchantIDs = excludeMerchantIDsInput.value.split('\n').map(id => id.trim()).filter(Boolean); saveExcludeMerchants(newMerchantIDs); excludeMerchantIDs = newMerchantIDs; processArticles(); }); } function addSettingsButton() { const deals = document.querySelectorAll('article.thread--deal, article.thread--voucher'); deals.forEach(deal => { if (deal.hasAttribute('data-settings-added')) return; const footer = deal.querySelector('.threadListCard-footer, .threadCardLayout-footer'); if (!footer) return; // Create settings button const settingsBtn = document.createElement('button'); settingsBtn.className = 'flex--shrink-0 button button--type-text button--mode-secondary button--square'; settingsBtn.title = 'mydealz Manager Einstellungen'; settingsBtn.setAttribute('data-t', 'mdmSettings'); settingsBtn.style.cssText = ` display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 6px !important; border: none !important; background: transparent !important; cursor: pointer !important; margin: 0 4px !important; min-width: 32px !important; min-height: 32px !important; position: relative !important; z-index: 2 !important; `; settingsBtn.innerHTML = ` <span class="flex--inline boxAlign-ai--all-c"> <svg width="20" height="20" class="icon icon--gear"> <use xlink:href="/assets/img/ico_707ed.svg#gear"></use> </svg> </span> `; // Insert at correct position (before comments button) const commentsBtn = footer.querySelector('[href*="comments"]'); if (commentsBtn) { commentsBtn.parentNode.insertBefore(settingsBtn, commentsBtn); } else { footer.prepend(settingsBtn); } deal.setAttribute('data-settings-added', 'true'); // Rest of click handler code remains unchanged settingsBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (isSettingsOpen) { if (dealThatOpenedSettings === deal) { cleanup(); } else { dealThatOpenedSettings = deal; const colors = getThemeColors(); // Move colors definition here // Update merchant button const merchantButton = document.getElementById('hideMerchantButton'); const merchantLink = deal.querySelector('a[data-t="merchantLink"]'); if (merchantButton) { merchantButton.remove(); } if (merchantLink) { const merchantName = merchantLink.textContent.trim(); const merchantButtonHtml = ` <button id="hideMerchantButton" style=" width: 100%; margin-top: 5px; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-store-slash"></i> Alle Deals von <span style="font-weight: bold">${merchantName}</span> ausblenden </button> `; // Update settings content const settingsContent = settingsDiv.querySelector('div'); if (settingsContent) { settingsContent.insertAdjacentHTML('afterbegin', merchantButtonHtml); } // Re-attach click handler const newMerchantButton = document.getElementById('hideMerchantButton'); if (newMerchantButton) { newMerchantButton.addEventListener('click', merchantButtonHandler); } } // Reset word suggestions suggestedWords = []; const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Reset word input const newWordInput = document.getElementById('newWordInput'); if (newWordInput) { newWordInput.value = ''; } } } else { dealThatOpenedSettings = deal; createSettingsUI(); } return false; }; }); } // Add to document ready handler document.addEventListener('DOMContentLoaded', () => { processArticles(); addSettingsButton(); addMerchantPageHideButton(); initObserver(); injectMaxPriceFilter(); // Add this line }); function addHideButtons() { const deals = document.querySelectorAll('article:not([data-button-added])'); deals.forEach(deal => { if (deal.hasAttribute('data-button-added')) return; // Check for expired status const isExpired = deal.querySelector('.color--text-TranslucentSecondary .size--all-s')?.textContent.includes('Abgelaufen'); // Get temperature container const voteTemp = deal.querySelector('.cept-vote-temp'); if (!voteTemp) return; // Remove popover const popover = voteTemp.querySelector('.popover-origin'); if (popover) popover.remove(); // Find temperature span for expired deals const tempSpan = isExpired ? voteTemp.querySelector('span') : null; const targetElement = isExpired ? tempSpan : voteTemp; if (!targetElement) return; const hideButtonContainer = document.createElement('div'); hideButtonContainer.style.cssText = ` position: absolute; left: 0; top: 0; width: 100%; height: 100%; display: none; z-index: 10001; pointer-events: none; `; const hideButton = document.createElement('button'); hideButton.innerHTML = '❌'; hideButton.className = 'vote-button overflow--visible'; hideButton.title = 'Deal verbergen'; hideButton.style.cssText = ` position: absolute !important; left: 50% !important; top: 50% !important; transform: translate(-50%, -50%) !important; z-index: 10002 !important; background: ${isDarkMode() ? 'rgba(45, 45, 45, 0.9)' : 'rgba(255, 255, 255, 0.9)'} !important; border: none !important; border-radius: 50% !important; cursor: pointer !important; padding: 8px !important; width: 32px !important; height: 32px !important; display: flex !important; align-items: center !important; justify-content: center !important; pointer-events: all !important; box-shadow: none !important; `; // Position relative to container if (!targetElement.style.position) { targetElement.style.position = 'relative'; } if (IS_TOUCH_DEVICE) { let buttonVisible = false; // Add scroll handler to hide button const scrollHandler = () => { if (buttonVisible) { buttonVisible = false; hideButtonContainer.style.display = 'none'; } }; // Add scroll listener window.addEventListener('scroll', scrollHandler, { passive: true }); targetElement.addEventListener('touchstart', (e) => { e.preventDefault(); e.stopPropagation(); if (!buttonVisible) { buttonVisible = true; hideButtonContainer.style.display = 'block'; } else { const dealId = deal.getAttribute('id'); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); // Remove scroll listener after hiding deal window.removeEventListener('scroll', scrollHandler); } }, true); // Clean up scroll listener when deal is removed const dealObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.type === 'childList' && mutation.removedNodes.length) { window.removeEventListener('scroll', scrollHandler); dealObserver.disconnect(); } }); }); dealObserver.observe(deal.parentNode, { childList: true }); targetElement.addEventListener('touchend', () => { if (!buttonVisible) { hideButtonContainer.style.display = 'none'; } }, true); } else { targetElement.addEventListener('mouseenter', () => { hideButtonContainer.style.display = 'block'; }, true); targetElement.addEventListener('mouseleave', () => { hideButtonContainer.style.display = 'none'; }, true); hideButton.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const dealId = deal.getAttribute('id'); hiddenDeals.push(dealId); saveHiddenDeals(); hideDeal(deal); return false; }; } hideButtonContainer.appendChild(hideButton); targetElement.appendChild(hideButtonContainer); deal.setAttribute('data-button-added', 'true'); }); } // Verbesserte HTML Decoder Funktion function decodeHtml(html) { const txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; } // --- 3. Backup- und Restore-Funktionen --- function backupData() { const backup = { excludeWords: excludeWords, excludeMerchantIDs: excludeMerchantIDs, merchantsData: loadExcludeMerchants() }; const now = new Date(); const timestamp = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + '_' + String(now.getHours()).padStart(2, '0') + '.' + String(now.getMinutes()).padStart(2, '0'); const blob = new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `mydealz_backup_${timestamp}.json`; a.click(); URL.revokeObjectURL(url); } // Restore-Funktion function restoreData(event) { const file = event.target.files[0]; if (file && file.type === 'application/json') { const reader = new FileReader(); reader.onload = function(e) { const restoredData = JSON.parse(e.target.result); if (restoredData.excludeWords && (restoredData.excludeMerchantIDs || restoredData.merchantsData)) { // Restore words saveExcludeWords(restoredData.excludeWords); excludeWords = restoredData.excludeWords; // Restore merchant data const merchantsData = restoredData.merchantsData || restoredData.excludeMerchantIDs.map(id => ({ id, name: id })); saveExcludeMerchants(merchantsData); // Update UI document.getElementById('excludeWordsInput').value = excludeWords.join('\n'); // Refresh deals processArticles(); } else { alert('Die Backup-Datei ist nicht im richtigen Format.'); } }; reader.readAsText(file); } else { alert('Bitte wählen Sie eine gültige JSON-Datei aus.'); } } // --- 4. Benutzeroberfläche (UI) --- function getSubUIPosition() { if (IS_TOUCH_DEVICE) { return ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); `; } return ` position: fixed; top: 50%; left: calc(50% + 310px); transform: translate(-50%, -50%); `; } function createMerchantListUI() { const colors = getThemeColors(); merchantListDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1001; width: 300px; color: ${colors.text}; `; const currentMerchants = loadExcludeMerchants(); const merchantListHTML = currentMerchants.map(merchant => ` <div class="merchant-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <span>${merchant.name}</span> <button class="delete-merchant" data-id="${merchant.id}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); merchantListDiv.innerHTML = ` <h4 style="margin-bottom: 10px;">Ausgeblendete Händler</h4> <input type="text" id="merchantSearch" placeholder="Händler suchen..." style=" width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px;"> <div style="margin-bottom: 15px;"> <div id="merchantList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;"> ${merchantListHTML} </div> <button id="clearMerchantListButton" style=" width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px;"> <i class="fas fa-trash"></i> Alle Händler entfernen </button> </div> <div style="text-align: right;"> <button id="closeMerchantListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen"> <i class="fas fa-times"></i> </button> </div> `; // Add the div to the document body document.body.appendChild(merchantListDiv); setupClickOutsideHandler(); // Add search functionality const searchInput = document.getElementById('merchantSearch'); searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); document.querySelectorAll('.merchant-item').forEach(item => { const merchantName = item.querySelector('span').textContent.toLowerCase(); item.style.display = merchantName.includes(searchTerm) ? 'flex' : 'none'; }); }); // Add clear all button handler document.getElementById('clearMerchantListButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Händler aus der Liste entfernen?')) { saveExcludeMerchants([]); document.getElementById('merchantList').innerHTML = ''; processArticles(); } }); // Update delete button handlers document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', function(e) { // Prevent event bubbling e.preventDefault(); e.stopPropagation(); // Get merchant ID to delete const deleteButton = e.target.closest('.delete-merchant'); if (!deleteButton) return; const idToDelete = deleteButton.dataset.id; // Update merchant data const merchantsData = loadExcludeMerchants(); const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete); // Save updated data saveExcludeMerchants(updatedMerchants); // Remove from UI deleteButton.closest('.merchant-item').remove(); // Refresh deals processArticles(); }); }); // Update close button handlers in createMerchantListUI document.getElementById('closeMerchantListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } function createExcludeWordsUI() { const colors = getThemeColors(); wordsListDiv.style.cssText = ` ${getSubUIPosition()} padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1001; width: 300px; color: ${colors.text}; `; const currentWords = loadExcludeWords(); const wordsListHTML = currentWords.map(word => ` <div class="word-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); wordsListDiv.innerHTML = ` <h4 style="margin-bottom: 10px;">Ausgeblendete Wörter</h4> <input type="text" id="wordSearch" placeholder="Wörter suchen..." style=" width: 100%; padding: 5px; margin-bottom: 10px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px;"> <div style="margin-bottom: 15px;"> <div id="wordsList" style="margin-bottom: 10px; max-height: 200px; overflow-y: auto; padding-right: 5px;"> ${wordsListHTML} </div> <button id="clearWordsListButton" style=" width: 100%; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; color: ${colors.text}; border-radius: 3px; cursor: pointer; margin-top: 10px;"> <i class="fas fa-trash"></i> Alle Wörter entfernen </button> </div> <div style="text-align: right;"> <button id="closeWordsListButton" style="padding: 8px 12px; background: none; border: none; cursor: pointer;" title="Schließen"> <i class="fas fa-times"></i> </button> </div> `; // Add the div to the document body document.body.appendChild(wordsListDiv); setupClickOutsideHandler(); // Add search functionality const searchInput = document.getElementById('wordSearch'); searchInput.addEventListener('input', (e) => { const searchTerm = e.target.value.toLowerCase(); document.querySelectorAll('.word-item').forEach(item => { const word = item.querySelector('span').textContent.toLowerCase(); item.style.display = word.includes(searchTerm) ? 'flex' : 'none'; }); }); // Add clear all button handler document.getElementById('clearWordsListButton').addEventListener('click', () => { if (confirm('Möchten Sie wirklich alle Wörter aus der Liste entfernen?')) { saveExcludeWords([]); document.getElementById('wordsList').innerHTML = ''; excludeWords = []; processArticles(); } }); // Add delete handlers document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', (e) => { // Prevent event bubbling e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-word'); if (!deleteButton) return; const wordToDelete = deleteButton.dataset.word; excludeWords = excludeWords.filter(word => word !== wordToDelete); saveExcludeWords(excludeWords); // Remove only the specific word item deleteButton.closest('.word-item').remove(); // Update deals without closing UI processArticles(); }); }); // Update close button handlers in createExcludeWordsUI document.getElementById('closeWordsListButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling closeActiveSubUI(); }); } function getWordsFromTitle(dealElement) { if (!dealElement) return []; const titleElement = dealElement.querySelector('.thread-title'); if (!titleElement) return []; // Extrahiere nur den sichtbaren Text aus dem Title const visibleTitle = titleElement.textContent; // Decode HTML entities and normalize spaces const titleText = decodeHtml(visibleTitle) .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); // Verbesserte Wortextraktion mit Beibehaltung von Sonderzeichen const words = titleText // Split at spaces, commas and forward slashes .split(/[\s,/]+/) .map(word => word.trim()) // Mindestens 2 Zeichen und keine HTML/URL-Fragmente .filter(word => word.match(/[\p{L}\p{N}]{2,}/u) && // Unicode-aware matching !word.includes('=') && !word.startsWith('class') && !word.startsWith('title') ) // Nur führende/endende Sonderzeichen entfernen .map(word => word.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '')); // Duplikate case-insensitive entfernen const uniqueWords = []; const seenWords = new Set(); for (const word of words) { const lowerWord = word.toLowerCase(); if (!seenWords.has(lowerWord)) { seenWords.add(lowerWord); uniqueWords.push(word); } } return uniqueWords; } // Add debug logging to settings UI creation function createSettingsUI() { if (isSettingsOpen) return; isSettingsOpen = true; // Initialize containers initUIContainers(); const colors = getThemeColors(); settingsDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; background: ${colors.background}; border: 1px solid ${colors.border}; border-radius: 5px; z-index: 1000; width: 300px; max-height: 90vh; overflow: visible; color: ${colors.text}; `; // Get merchant info from current deal let merchantName = null; let showMerchantButton = false; if (dealThatOpenedSettings) { const merchantLink = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]'); if (merchantLink) { merchantName = merchantLink.textContent.trim(); showMerchantButton = true; } } // Process articles when opening settings processArticles(); const currentExcludeWords = JSON.parse(localStorage.getItem(EXCLUDE_WORDS_KEY)) || []; const currentExcludeMerchantIDs = JSON.parse(localStorage.getItem(EXCLUDE_MERCHANTS_KEY)) || []; const dealWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : []; // Conditional merchant button HTML - only show if merchant exists const merchantButtonHtml = showMerchantButton ? ` <button id="hideMerchantButton" style=" width: 100%; margin-top: 5px; padding: 5px 10px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-store-slash"></i> Alle Deals von <span style="font-weight: bold">${merchantName}</span> ausblenden </button> ` : ''; // Datalist Element entfernen, wird nicht benötigt const wordInputSection = ` <div style="margin-bottom: 20px;"> <div style="display: flex; align-items: center; gap: 5px;"> <input id="newWordInput" autocomplete="off" placeholder="Neues Wort..." title="Deals mit hier eingetragenen Wörtern im Titel werden ausgeblendet." style=" flex: 1; padding: 8px; background: ${colors.inputBg}; border: 1px solid ${colors.border}; border-radius: 3px; color: ${colors.text}; "> <button id="addWordButton" style=" padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text}; "> <i class="fas fa-plus"></i> </button> </div> </div>`; // Update the settingsDiv HTML to include the new wordInputSection settingsDiv.innerHTML = ` <h4 style="margin-bottom: 15px; color: ${colors.text}">Einstellungen zum Ausblenden</h4> ${wordInputSection} ${merchantButtonHtml} <!-- List Management Section --> <div style="margin-top: 20px; display: flex; flex-direction: column; gap: 10px;"> <button id="showWordsListButton" style=" width: 100%; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text};"> <i class="fas fa-list"></i> Wortfilter verwalten </button> <button id="showMerchantListButton" style=" width: 100%; padding: 8px; background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 3px; cursor: pointer; color: ${colors.text};"> <i class="fas fa-store"></i> Händlerfilter verwalten </button> </div> <!-- Action Buttons --> <div style="margin-top: 20px; text-align: right; display: flex; justify-content: flex-end; gap: 5px;"> <button id="createBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Backup erstellen"> <i class="fas fa-file-export"></i> </button> <button id="restoreBackupButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Wiederherstellen"> <i class="fas fa-file-import"></i> </button> <input type="file" id="restoreFileInput" style="display: none;" /> <button id="closeSettingsButton" style="padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text};" title="Schließen"> <i class="fas fa-times"></i> </button> </div>`; // Explicitly add to DOM document.body.appendChild(settingsDiv); setupClickOutsideHandler(); updateUITheme(); const actionButtons = settingsDiv.querySelectorAll('#closeSettingsButton, #createBackupButton, #restoreBackupButton'); actionButtons.forEach(btn => { btn.style.cssText = ` padding: 8px; background: none; border: none; cursor: pointer; color: ${colors.text}; `; }); // Add word input handler const addWordButton = document.getElementById('addWordButton'); if (addWordButton) { addWordButton.addEventListener('click', () => { const newWordInput = document.getElementById('newWordInput'); const newWord = newWordInput.value.trim(); if (newWord && !excludeWords.includes(newWord)) { excludeWords.unshift(newWord); saveExcludeWords(excludeWords); newWordInput.value = ''; processArticles(); //updateActiveLists(); // Liste aktualisieren cleanup(); // Close settings UI suggestedWords = []; const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } } }); } // Add enter key handler for input document.getElementById('newWordInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { document.getElementById('addWordButton').click(); } }); // Only add merchant button listener if button exists const hideMerchantButton = document.getElementById('hideMerchantButton'); if (hideMerchantButton && showMerchantButton) { hideMerchantButton.addEventListener('click', () => { if (!dealThatOpenedSettings) return; const merchantLink = dealThatOpenedSettings.querySelector('a[href*="merchant-id="]'); if (!merchantLink) return; const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/); if (!merchantIDMatch) return; const merchantID = merchantIDMatch[1]; const merchantName = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]').textContent.trim(); const merchantsData = loadExcludeMerchants(); if (!merchantsData.some(m => m.id === merchantID)) { merchantsData.unshift({ id: merchantID, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); cleanup(); // Close settings UI // Aktualisiere Listen wenn UI offen if (activeSubUI === 'merchant') { updateActiveLists(); } } }); } // Add merchant list button listener document.getElementById('showMerchantListButton').addEventListener('click', () => { const btn = document.getElementById('showMerchantListButton'); if (btn.hasAttribute('data-processing')) return; btn.setAttribute('data-processing', 'true'); if (activeSubUI === 'merchant') { closeActiveSubUI(); btn.innerHTML = '<i class="fas fa-store"></i> Händlerfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createMerchantListUI(); activeSubUI = 'merchant'; btn.innerHTML = '<i class="fas fa-times"></i> Händlerfilter ausblenden'; } btn.removeAttribute('data-processing'); }); // Add words list button listener document.getElementById('showWordsListButton').addEventListener('click', () => { const btn = document.getElementById('showWordsListButton'); if (activeSubUI === 'words') { closeActiveSubUI(); btn.innerHTML = '<i class="fas fa-list"></i> Wortfilter verwalten'; activeSubUI = null; } else { closeActiveSubUI(); createExcludeWordsUI(); activeSubUI = 'words'; btn.innerHTML = '<i class="fas fa-times"></i> Wortfilter ausblenden'; } }); // Always ensure close button works document.getElementById('closeSettingsButton').addEventListener('click', (e) => { e.stopPropagation(); // Prevent event bubbling cleanup(); }); // Backup/Restore Event Listeners document.getElementById('createBackupButton').addEventListener('click', backupData); document.getElementById('restoreBackupButton').addEventListener('click', () => { document.getElementById('restoreFileInput').click(); }); document.getElementById('restoreFileInput').addEventListener('change', restoreData); // Add event listeners only if newWordInput exists const newWordInput = document.getElementById('newWordInput'); if (newWordInput) { // Unified focus handler newWordInput.addEventListener('focus', () => { // Get fresh words from current deal if none exist if (suggestedWords.length === 0) { suggestedWords = getWordsFromTitle(dealThatOpenedSettings); } // Always show suggestion list if words exist if (suggestedWords.length > 0) { updateSuggestionList(); } }, { once: false }); // Allow multiple focus events } // Click Outside Handler anpassen createSuggestionClickHandler(); // Cleanup bei UI-Schließung document.getElementById('closeSettingsButton').addEventListener('click', () => { document.removeEventListener('click', suggestionClickHandler); }); // Add cleanup to window unload window.addEventListener('unload', cleanup); // Add to createSettingsUI after other event listeners const maxPriceInput = document.getElementById('maxPriceFilterInput'); // Note the correct ID if (maxPriceInput) { maxPriceInput.addEventListener('change', (e) => { const price = parseFloat(e.target.value); if (!isNaN(price) && price >= 0) { saveMaxPrice(price); processArticles(); } }); } // Get initial word suggestions suggestedWords = dealThatOpenedSettings ? getWordsFromTitle(dealThatOpenedSettings) : []; } function updateSuggestionList() { // Save scroll position if list exists const oldList = document.getElementById('wordSuggestionList'); const scrollPosition = oldList?.scrollTop || 0; // Remove old list if exists if (oldList) oldList.remove(); // Filter and check for words suggestedWords = suggestedWords.filter(word => !excludeWords.includes(word)); if (!suggestedWords.length) return; const inputField = document.getElementById('newWordInput'); const inputRect = inputField.getBoundingClientRect(); const colors = getThemeColors(); // Create suggestion list with fixed positioning const wordSuggestionList = document.createElement('div'); wordSuggestionList.id = 'wordSuggestionList'; wordSuggestionList.style.cssText = ` position: fixed; top: ${inputRect.bottom}px; left: ${inputRect.left}px; width: ${inputRect.width}px; max-height: 200px; overflow-y: auto; background: ${colors.background}; border: 1px solid ${colors.border}; color: ${colors.text}; border-radius: 3px; z-index: 1002; box-shadow: 0 2px 5px rgba(0,0,0,0.1); display: block; `; // Add scroll event listener to update position const updatePosition = () => { const newRect = inputField.getBoundingClientRect(); wordSuggestionList.style.top = `${newRect.bottom}px`; wordSuggestionList.style.left = `${newRect.left}px`; }; window.addEventListener('scroll', updatePosition, true); window.addEventListener('resize', updatePosition); // Rest of the function stays the same wordSuggestionList.innerHTML = suggestedWords .map(word => ` <div class="word-suggestion-item" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; transition: background-color 0.2s;"> ${word} </div> `).join(''); document.body.appendChild(wordSuggestionList); wordSuggestionList.scrollTop = scrollPosition; // Add event listeners for items wordSuggestionList.querySelectorAll('.word-suggestion-item').forEach(item => { item.addEventListener('mouseenter', () => { item.style.backgroundColor = colors.itemBg; }); item.addEventListener('mouseleave', () => { item.style.backgroundColor = colors.background; }); item.addEventListener('click', handleWordSelection); }); // Clean up event listeners when list is removed const cleanupListeners = () => { window.removeEventListener('scroll', updatePosition, true); window.removeEventListener('resize', updatePosition); }; // Add to existing cleanup function const oldCleanup = cleanup; cleanup = () => { cleanupListeners(); oldCleanup(); }; } function initObserver() { // Disconnect existing observer if it exists if (observer) { observer.disconnect(); } // Reinitialize the observer observer.observe(document.body, { childList: true, subtree: true }); // Process any existing articles processArticles(); addSettingsButton(); addHideButtons(); } // Update addMerchantToList function function addMerchantToList(merchant, merchantList) { const div = document.createElement('div'); div.className = 'merchant-item'; div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;'; div.innerHTML = ` <span style="font-weight: bold;">${merchant.name}</span> <button class="delete-merchant" data-id="${merchant.id}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> `; // Insert at beginning of list merchantList.insertBefore(div, merchantList.firstChild); } // Update word list UI - add new item at top function addWordToList(word, wordsList) { const div = document.createElement('div'); div.className = 'word-item'; div.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;'; div.innerHTML = ` <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> `; // Insert at beginning of list wordsList.insertBefore(div, wordsList.firstChild); } function setupClickOutsideHandler() { if (uiClickOutsideHandler) { document.removeEventListener('click', uiClickOutsideHandler); } uiClickOutsideHandler = (e) => { // Early exit for clicks on UI controls if (e.target.closest('.settings-button') || e.target.closest('#showMerchantListButton') || e.target.closest('#showWordsListButton')) { return; } // Get current UI states const settingsOpen = settingsDiv?.parentNode; const merchantsOpen = merchantListDiv?.parentNode; const wordsOpen = wordsListDiv?.parentNode; // Check if click was outside all UIs const clickedOutside = (!settingsOpen || !settingsDiv.contains(e.target)) && (!merchantsOpen || !merchantListDiv.contains(e.target)) && (!wordsOpen || !wordsListDiv.contains(e.target)); if (clickedOutside) { cleanup(); // Explicit cleanup of UI elements if (settingsDiv?.parentNode) settingsDiv.remove(); if (merchantListDiv?.parentNode) merchantListDiv.remove(); if (wordsListDiv?.parentNode) wordsListDiv.remove(); // Reset states isSettingsOpen = false; activeSubUI = null; // Remove handler document.removeEventListener('click', uiClickOutsideHandler); uiClickOutsideHandler = null; } }; // Add with delay to prevent immediate trigger setTimeout(() => { document.addEventListener('click', uiClickOutsideHandler); }, 100); } // Add helper function to close sub-UIs function closeActiveSubUI() { if (activeSubUI === 'merchant') { merchantListDiv?.remove(); const btn = document.getElementById('showMerchantListButton'); if (btn) { btn.innerHTML = '<i class="fas fa-store"></i> Händlerfilter verwalten'; btn.removeAttribute('data-processing'); } } else if (activeSubUI === 'words') { wordsListDiv?.remove(); const btn = document.getElementById('showWordsListButton'); if (btn) { btn.innerHTML = '<i class="fas fa-list"></i> Wortfilter verwalten'; } } activeSubUI = null; } // Add new function to handle merchant pages function addMerchantPageHideButton() { // Check if we're on a merchant page const urlParams = new URLSearchParams(window.location.search); const merchantId = urlParams.get('merchant-id'); if (!merchantId) return; // Find merchant header const merchantBanner = document.querySelector(MERCHANT_PAGE_SELECTOR); if (!merchantBanner) return; // Get merchant name from page const merchantName = document.querySelector('.merchant-banner__title')?.textContent.trim(); if (!merchantName) return; // Create hide button container const hideButtonContainer = document.createElement('div'); hideButtonContainer.style.cssText = ` display: inline-flex; align-items: center; margin-left: 10px; `; // Create hide button const hideButton = document.createElement('button'); hideButton.innerHTML = '<i class="fas fa-store-slash"></i>'; hideButton.title = `Alle Deals von ${merchantName} ausblenden`; hideButton.style.cssText = ` padding: 8px; background: #f0f0f0; border: 1px solid #ccc; border-radius: 3px; cursor: pointer; `; // Add click handler hideButton.addEventListener('click', () => { const merchantsData = loadExcludeMerchants(); // Check if ID already exists if (!merchantsData.some(m => m.id === merchantId)) { // Add new merchant at start of array merchantsData.unshift({ id: merchantId, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); } }); // Add button to page hideButtonContainer.appendChild(hideButton); merchantBanner.appendChild(hideButtonContainer); } // Funktion zum Aktualisieren der aktiven Listen function updateActiveLists() { const colors = getThemeColors(); if (activeSubUI === 'merchant' && merchantListDiv) { const merchantList = document.getElementById('merchantList'); if (merchantList) { const currentMerchants = loadExcludeMerchants(); merchantList.innerHTML = currentMerchants.map(merchant => ` <div class="merchant-item" style=" display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: ${colors.itemBg}; color: ${colors.text}; border: 1px solid ${colors.border}; border-radius: 3px;"> <div style="display: flex; flex-direction: column;"> <span>${merchant.name}</span> <span style="color: ${colors.text}; opacity: 0.7; font-size: 0.8em;">ID: ${merchant.id}</span> </div> <button class="delete-merchant" data-id="${merchant.id}" style=" background: none; border: none; cursor: pointer; color: ${colors.text};"> <i class="fas fa-times"></i> </button> </div> `).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-merchant').forEach(button => { button.addEventListener('click', handleMerchantDelete); }); } } else if (activeSubUI === 'words' && wordsListDiv) { const wordsList = document.getElementById('wordsList'); if (wordsList) { const currentWords = loadExcludeWords(); wordsList.innerHTML = currentWords.map(word => ` <div class="word-item" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding: 5px; background: #f0f0f0; border-radius: 3px;"> <span style="word-break: break-word;">${word}</span> <button class="delete-word" data-word="${word}" style="background: none; border: none; cursor: pointer; color: #666;"> <i class="fas fa-times"></i> </button> </div> `).join(''); // Event Listener neu hinzufügen document.querySelectorAll('.delete-word').forEach(button => { button.addEventListener('click', handleWordDelete); }); } } } // Handler Funktionen definieren function handleMerchantDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-merchant'); if (!deleteButton) return; const idToDelete = deleteButton.dataset.id; const merchantsData = loadExcludeMerchants(); const updatedMerchants = merchantsData.filter(m => m.id !== idToDelete); saveExcludeMerchants(updatedMerchants); deleteButton.closest('.merchant-item').remove(); processArticles(); } function handleWordDelete(e) { e.preventDefault(); e.stopPropagation(); const deleteButton = e.target.closest('.delete-word'); if (!deleteButton) return; const wordToDelete = deleteButton.dataset.word; excludeWords = excludeWords.filter(word => word !== wordToDelete); saveExcludeWords(excludeWords); deleteButton.closest('.word-item').remove(); processArticles(); } // Add new save function function saveMaxPrice(price) { localStorage.setItem(MAX_PRICE_KEY, price.toString()); maxPrice = price; } // Add after existing global functions function merchantButtonHandler() { if (!dealThatOpenedSettings) return; const merchantLink = dealThatOpenedSettings.querySelector('a[href*="merchant-id="]'); if (!merchantLink) return; const merchantIDMatch = merchantLink.getAttribute('href').match(/merchant-id=(\d+)/); if (!merchantIDMatch) return; const merchantID = merchantIDMatch[1]; const merchantName = dealThatOpenedSettings.querySelector('a[data-t="merchantLink"]').textContent.trim(); const merchantsData = loadExcludeMerchants(); if (!merchantsData.some(m => m.id === merchantID)) { merchantsData.unshift({ id: merchantID, name: merchantName }); saveExcludeMerchants(merchantsData); processArticles(); cleanup(); // Close settings UI // Update lists if UI open if (activeSubUI === 'merchant') { updateActiveLists(); } } } // Add after other utility functions function handleWordSelection(e) { e.preventDefault(); e.stopPropagation(); const wordSuggestionList = document.getElementById('wordSuggestionList'); const scrollPosition = wordSuggestionList.scrollTop; // Save scroll position const word = e.target.textContent.trim(); const newWordInput = document.getElementById('newWordInput'); const currentValue = newWordInput.value.trim(); newWordInput.value = currentValue ? `${currentValue} ${word}` : word; suggestedWords = suggestedWords.filter(w => w !== word); updateSuggestionList(); newWordInput.focus(); // Restore scroll position after list update const updatedList = document.getElementById('wordSuggestionList'); if (updatedList) { updatedList.scrollTop = scrollPosition; } } // Add after other global functions function cleanup() { // Remove settings UI if (settingsDiv?.parentNode) { settingsDiv.remove(); isSettingsOpen = false; } // Add word suggestion list cleanup const suggestionList = document.getElementById('wordSuggestionList'); if (suggestionList) { suggestionList.remove(); } // Close merchant & words lists if (merchantListDiv?.parentNode) merchantListDiv.remove(); if (wordsListDiv?.parentNode) wordsListDiv.remove(); // Reset UI states if (activeSubUI === 'merchant' || activeSubUI === 'words') { const btn = document.getElementById(`show${activeSubUI === 'merchant' ? 'Merchant' : 'Words'}ListButton`); if (btn) { btn.innerHTML = activeSubUI === 'merchant' ? '<i class="fas fa-store"></i> Händlerfilter verwalten' : '<i class="fas fa-list"></i> Wortfilter verwalten'; btn.removeAttribute('data-processing'); } } activeSubUI = null; // Clean up handlers document.removeEventListener('click', suggestionClickHandler); document.removeEventListener('click', uiClickOutsideHandler); window.removeEventListener('unload', cleanup); uiClickOutsideHandler = null; // Reset suggestion state suggestedWords = []; // Don't disconnect the main observer // Instead, reinitialize it to ensure it's working initObserver(); } // 4. Complete UI state reset function resetUIState() { isSettingsOpen = false; activeSubUI = null; dealThatOpenedSettings = null; suggestedWords = []; initialSuggestionListCreated = false; settingsDiv?.remove(); merchantListDiv?.remove(); wordsListDiv?.remove(); } // Add after other global functions function createSuggestionClickHandler() { // Remove old handler if exists if (suggestionClickHandler) { document.removeEventListener('click', suggestionClickHandler); } suggestionClickHandler = (e) => { const list = document.getElementById('wordSuggestionList'); const input = document.getElementById('newWordInput'); if (!list?.contains(e.target) && !input?.contains(e.target)) { list?.remove(); } }; document.addEventListener('click', suggestionClickHandler); return suggestionClickHandler; } // Add function to inject max price filter function injectMaxPriceFilter() { const filterForm = document.querySelector('.subNavMenu-list form:first-of-type ul'); if (!filterForm) return; // Create filter items with optimized HTML const filterItems = document.createElement('div'); filterItems.innerHTML = ` <!-- Cold Deals Toggle --> <li class="flex boxAlign-jc--all-sb boxAlign-ai--all-c space--h-3 space--v-3 subNavMenu-item--separator"> <span class="subNavMenu-text mute--text space--r-2 overflow--wrap-off">Kalte Deals ausblenden</span> <label class="checkbox checkbox--brand checkbox--mode-special"> <input class="input checkbox-input" type="checkbox" id="hideColdDealsToggle" ${hideColdDeals ? 'checked' : ''} > <span class="tGrid-cell tGrid-cell--shrink"> <span class="checkbox-box flex--inline boxAlign-jc--all-c boxAlign-ai--all-c"> <svg width="18px" height="14px" class="icon icon--tick checkbox-tick"> <use xlink:href="/assets/img/ico_707ed.svg#tick"></use> </svg> </span> </span> </label> </li> <!-- Price Filter --> <li class="flex boxAlign-jc--all-sb boxAlign-ai--all-c space--h-3 space--v-3"> <span class="subNavMenu-text mute--text space--r-2 overflow--wrap-off"> Maximalpreis filtern </span> <input type="number" id="maxPriceFilterInput" value="${maxPrice}" min="0" step="1" placeholder="€" style=" width: 70px; padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; margin-left: auto; /* Remove spinner buttons */ -moz-appearance: textfield; &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } " > </li>`; filterForm.appendChild(filterItems); // Optimized input handling const priceInput = document.getElementById('maxPriceFilterInput'); if (priceInput) { let debounceTimer; let isActive = false; // Debounced price update const updatePrice = (value) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { const price = parseInt(value.replace(/\D/g, '')); if (!isNaN(price) && price >= 0) { saveMaxPrice(price); processArticles(); } }, 250); }; // Keep menu open const keepMenuOpen = () => { if (!isActive) return; const menu = document.querySelector('.subNavMenu-list'); if (menu) { menu.classList.add('force-open'); menu.style.cssText = 'display:block;visibility:visible;opacity:1'; } }; // Focus handler priceInput.addEventListener('focus', () => { isActive = true; keepMenuOpen(); }, { passive: true }); // Input handler priceInput.addEventListener('input', (e) => { e.stopPropagation(); updatePrice(e.target.value); if (isActive) keepMenuOpen(); }, { passive: true }); // Blur handler priceInput.addEventListener('blur', () => { isActive = false; }, { passive: true }); // Click outside handler document.addEventListener('click', (e) => { if (!isActive) return; if (!e.target.closest('.subNavMenu-list')) { e.preventDefault(); keepMenuOpen(); } }, { capture: true }); // Optional: Lightweight menu state observer const menu = document.querySelector('.subNavMenu-list'); if (menu) { const observer = new MutationObserver(() => { if (isActive) requestAnimationFrame(keepMenuOpen); }); observer.observe(menu, { attributes: true, attributeFilter: ['style', 'class'], childList: false, subtree: false }); } } // Cold deals toggle const coldDealsToggle = document.getElementById('hideColdDealsToggle'); if (coldDealsToggle) { coldDealsToggle.addEventListener('change', (e) => { e.stopPropagation(); hideColdDeals = e.target.checked; localStorage.setItem(HIDE_COLD_DEALS_KEY, hideColdDeals); processArticles(); }); } } // Add observer for filter UI changes const filterObserver = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { const filterMenu = document.querySelector('.subNavMenu-list'); if (filterMenu && !document.getElementById('maxPriceFilterInput')) { injectMaxPriceFilter(); } } }); }); // Start observing filter area filterObserver.observe(document.body, { childList: true, subtree: true });