refactor: fix memory leak in auth-modal, consolidate escapeHTML helper across 11 components

This commit is contained in:
2026-02-05 15:07:18 +01:00
parent cd437f20e1
commit 08a650ea80
13 changed files with 44 additions and 131 deletions

View File

@@ -19,10 +19,12 @@ class AuthModal extends HTMLElement {
connectedCallback() {
this.render()
this.unsubscribe = i18n.subscribe(() => this.render())
this.boundHandleKeydown = this.handleKeydown.bind(this)
}
disconnectedCallback() {
if (this.unsubscribe) this.unsubscribe()
document.removeEventListener('keydown', this.boundHandleKeydown)
}
show(mode = 'login') {
@@ -209,7 +211,7 @@ class AuthModal extends HTMLElement {
this.querySelector('#confirm-saved')?.addEventListener('click', () => this.hide())
// Escape key
document.addEventListener('keydown', this.handleKeydown.bind(this))
document.addEventListener('keydown', this.boundHandleKeydown)
}
handleKeydown(e) {

View File

@@ -6,6 +6,7 @@
import { t, i18n } from '../i18n.js'
import { conversationsService } from '../services/conversations.js'
import { cryptoService } from '../services/crypto.js'
import { escapeHTML } from '../utils/helpers.js'
class ChatWidget extends HTMLElement {
static get observedAttributes() {
@@ -103,7 +104,7 @@ class ChatWidget extends HTMLElement {
this.innerHTML = /* html */`
<div class="chat-widget">
<div class="chat-header">
<span class="chat-recipient">${this.escapeHtml(this.recipientName)}</span>
<span class="chat-recipient">${escapeHTML(this.recipientName)}</span>
<span class="chat-encrypted" title="${t('chat.encrypted')}">🔒</span>
</div>
@@ -144,7 +145,7 @@ class ChatWidget extends HTMLElement {
return this.messages.map(msg => /* html */`
<div class="chat-message ${msg.isOwn ? 'own' : 'other'}">
<div class="message-bubble">
<p>${this.escapeHtml(msg.text)}</p>
<p>${escapeHTML(msg.text)}</p>
<span class="message-time">${this.formatTime(msg.timestamp)}</span>
</div>
</div>
@@ -194,11 +195,6 @@ class ChatWidget extends HTMLElement {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('chat-widget', ChatWidget)

View File

@@ -1,4 +1,5 @@
import { t } from '../i18n.js'
import { escapeHTML } from '../utils/helpers.js'
/**
* Error Boundary Component
@@ -37,7 +38,7 @@ class ErrorBoundary extends HTMLElement {
<div class="error-boundary">
<div class="error-icon">⚠️</div>
<h3 class="error-title">${t('error.title') || 'Etwas ist schiefgelaufen'}</h3>
<p class="error-message">${this.escapeHtml(errorMessage)}</p>
<p class="error-message">${escapeHTML(errorMessage)}</p>
<button class="btn btn-primary error-retry" type="button">
${t('error.retry') || 'Erneut versuchen'}
</button>
@@ -59,14 +60,6 @@ class ErrorBoundary extends HTMLElement {
this.error = null
}
escapeHtml(str) {
if (!str) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
}
customElements.define('error-boundary', ErrorBoundary)
@@ -137,7 +130,7 @@ function showErrorToast(message) {
toast.className = 'error-toast'
toast.innerHTML = /* html */`
<span class="error-toast-icon">⚠️</span>
<span class="error-toast-message">${escapeHtml(message)}</span>
<span class="error-toast-message">${escapeHTML(message)}</span>
<button class="error-toast-close" aria-label="Schließen">×</button>
`
@@ -155,15 +148,6 @@ function showErrorToast(message) {
requestAnimationFrame(() => toast.classList.add('visible'))
}
function escapeHtml(str) {
if (!str) return ''
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
// Styles
const style = document.createElement('style')
style.textContent = /* css */`

View File

@@ -3,6 +3,8 @@
* Displays a location on an OpenStreetMap map using Leaflet
*/
import { escapeHTML } from '../utils/helpers.js'
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
class LocationMap extends HTMLElement {
@@ -75,7 +77,7 @@ class LocationMap extends HTMLElement {
<div class="location-map-container">
<div class="location-map-header">
<span class="location-icon">📍</span>
<span class="location-text">${this.escapeHtml(locationText)}</span>
<span class="location-text">${escapeHTML(locationText)}</span>
</div>
<div class="location-map" id="map"></div>
</div>
@@ -168,12 +170,6 @@ class LocationMap extends HTMLElement {
}
}
escapeHtml(text) {
if (!text) return ''
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('location-map', LocationMap)

View File

@@ -1,4 +1,5 @@
import { t, i18n } from '../i18n.js'
import { escapeHTML } from '../utils/helpers.js'
const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/search'
const DEBOUNCE_MS = 400
@@ -73,8 +74,8 @@ class LocationPicker extends HTMLElement {
<div class="suggestion-item" data-index="${index}">
<span class="suggestion-icon">📍</span>
<div class="suggestion-text">
<span class="suggestion-name">${this.escapeHtml(loc.displayName)}</span>
<span class="suggestion-detail">${this.escapeHtml(loc.detail)}</span>
<span class="suggestion-name">${escapeHTML(loc.displayName)}</span>
<span class="suggestion-detail">${escapeHTML(loc.detail)}</span>
</div>
</div>
`).join('')
@@ -296,13 +297,6 @@ class LocationPicker extends HTMLElement {
}
}
escapeHtml(text) {
if (!text) return ''
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
getLocationData() {
if (!this.selectedLocation) return null
return {

View File

@@ -3,6 +3,7 @@ import { router } from '../../router.js'
import { auth } from '../../services/auth.js'
import { directus } from '../../services/directus.js'
import { SUPPORTED_CURRENCIES } from '../../services/currency.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../location-picker.js'
import '../pow-captcha.js'
import '../image-cropper.js'
@@ -219,7 +220,7 @@ class PageCreate extends HTMLElement {
class="input"
id="title"
name="title"
value="${this.escapeHtml(this.formData.title)}"
value="${escapeHTML(this.formData.title)}"
required
data-i18n-placeholder="create.titlePlaceholder"
placeholder="${t('create.titlePlaceholder')}"
@@ -323,7 +324,7 @@ class PageCreate extends HTMLElement {
required
data-i18n-placeholder="create.descriptionPlaceholder"
placeholder="${t('create.descriptionPlaceholder')}"
>${this.escapeHtml(this.formData.description)}</textarea>
>${escapeHTML(this.formData.description)}</textarea>
</div>
<div class="form-group">
@@ -347,7 +348,7 @@ class PageCreate extends HTMLElement {
class="input"
id="moneroAddress"
name="moneroAddress"
value="${this.escapeHtml(this.formData.moneroAddress)}"
value="${escapeHTML(this.formData.moneroAddress)}"
required
placeholder="${t('create.moneroPlaceholder')}"
>
@@ -665,12 +666,6 @@ class PageCreate extends HTMLElement {
this.querySelector('.form-error')?.remove()
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
generateSlug(title) {
return title
.toLowerCase()

View File

@@ -1,6 +1,7 @@
import { t, i18n } from '../../i18n.js'
import { directus } from '../../services/directus.js'
import { auth } from '../../services/auth.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../listing-card.js'
import '../skeleton-card.js'
@@ -121,10 +122,10 @@ class PageFavorites extends HTMLElement {
return /* html */`
<listing-card
listing-id="${listing.id}"
title="${this.escapeHtml(listing.title || '')}"
title="${escapeHTML(listing.title || '')}"
price="${listing.price || ''}"
currency="${listing.currency || 'EUR'}"
location="${this.escapeHtml(locationName)}"
location="${escapeHTML(locationName)}"
image="${imageUrl}"
owner-id="${listing.user_created || ''}"
></listing-card>
@@ -132,11 +133,6 @@ class PageFavorites extends HTMLElement {
}).join('')
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('page-favorites', PageFavorites)

View File

@@ -3,6 +3,7 @@ import { listingsService } from '../../services/listings.js'
import { directus } from '../../services/directus.js'
import { locationsService } from '../../services/locations.js'
import { auth } from '../../services/auth.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../listing-card.js'
import '../skeleton-card.js'
import '../search-box.js'
@@ -461,10 +462,10 @@ class PageHome extends HTMLElement {
return /* html */`
<listing-card
listing-id="${listing.id}"
title="${this.escapeHtml(listing.title || '')}"
title="${escapeHTML(listing.title || '')}"
price="${listing.price || ''}"
currency="${listing.currency || 'EUR'}"
location="${this.escapeHtml(locationName)}"
location="${escapeHTML(locationName)}"
image="${imageUrl}"
owner-id="${listing.user_created || ''}"
></listing-card>
@@ -486,12 +487,6 @@ class PageHome extends HTMLElement {
return listingsHtml + loadMoreHtml
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('page-home', PageHome)

View File

@@ -3,6 +3,7 @@ import { directus } from '../../services/directus.js'
import { auth } from '../../services/auth.js'
import { listingsService } from '../../services/listings.js'
import { getXmrRates, formatPrice as formatCurrencyPrice } from '../../services/currency.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../chat-widget.js'
import '../location-map.js'
import '../listing-card.js'
@@ -167,7 +168,7 @@ class PageListing extends HTMLElement {
<div class="listing-gallery">
<div class="listing-image-main" id="main-image">
${firstImage
? `<img src="${firstImage}" alt="${this.escapeHtml(this.listing.title)}" id="main-img">`
? `<img src="${firstImage}" alt="${escapeHTML(this.listing.title)}" id="main-img">`
: this.getPlaceholderSvg()}
</div>
${images.length > 1 ? `
@@ -183,8 +184,8 @@ class PageListing extends HTMLElement {
<!-- Header -->
<header class="listing-header">
${categoryName ? `<a href="#/search?category=${this.listing.category?.slug}" class="badge badge-primary">${this.escapeHtml(categoryName)}</a>` : ''}
<h1>${this.escapeHtml(this.listing.title)}</h1>
${categoryName ? `<a href="#/search?category=${this.listing.category?.slug}" class="badge badge-primary">${escapeHTML(categoryName)}</a>` : ''}
<h1>${escapeHTML(this.listing.title)}</h1>
<div class="listing-price-wrapper">
<p class="listing-price">${priceInfo.primary}</p>
${priceInfo.secondary ? `<p class="listing-price-secondary">${priceInfo.secondary}</p>` : ''}
@@ -214,9 +215,9 @@ class PageListing extends HTMLElement {
<section class="listing-location-section location-mobile">
<h2>${t('listing.location')}</h2>
<location-map
name="${this.escapeHtml(this.listing.location.name || '')}"
postal-code="${this.escapeHtml(this.listing.location.postal_code || '')}"
country="${this.escapeHtml(this.listing.location.country || '')}"
name="${escapeHTML(this.listing.location.name || '')}"
postal-code="${escapeHTML(this.listing.location.postal_code || '')}"
country="${escapeHTML(this.listing.location.country || '')}"
></location-map>
</section>
` : ''}
@@ -241,9 +242,9 @@ class PageListing extends HTMLElement {
<section class="listing-location-section">
<h2>${t('listing.location')}</h2>
<location-map
name="${this.escapeHtml(this.listing.location.name || '')}"
postal-code="${this.escapeHtml(this.listing.location.postal_code || '')}"
country="${this.escapeHtml(this.listing.location.country || '')}"
name="${escapeHTML(this.listing.location.name || '')}"
postal-code="${escapeHTML(this.listing.location.postal_code || '')}"
country="${escapeHTML(this.listing.location.country || '')}"
></location-map>
</section>
` : ''}
@@ -341,10 +342,10 @@ class PageListing extends HTMLElement {
return /* html */`
<listing-card
listing-id="${listing.id}"
title="${this.escapeHtml(listing.title || '')}"
title="${escapeHTML(listing.title || '')}"
price="${listing.price || ''}"
currency="${listing.currency || 'EUR'}"
location="${this.escapeHtml(locationName)}"
location="${escapeHTML(locationName)}"
image="${imageUrl}"
owner-id="${listing.user_created || ''}"
></listing-card>
@@ -560,13 +561,6 @@ class PageListing extends HTMLElement {
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>')
}
escapeHtml(text) {
if (!text) return ''
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('page-listing', PageListing)

View File

@@ -1,6 +1,7 @@
import { t, i18n } from '../../i18n.js'
import { auth } from '../../services/auth.js'
import { directus } from '../../services/directus.js'
import { escapeHTML } from '../../utils/helpers.js'
class PageMessages extends HTMLElement {
constructor() {
@@ -165,7 +166,7 @@ class PageMessages extends HTMLElement {
: `<div class="image-placeholder">📦</div>`}
</div>
<div class="conversation-info">
<h3 class="conversation-title">${this.escapeHtml(title)}</h3>
<h3 class="conversation-title">${escapeHTML(title)}</h3>
<p class="conversation-date">${dateStr}</p>
</div>
<div class="conversation-arrow">→</div>
@@ -188,11 +189,6 @@ class PageMessages extends HTMLElement {
return date.toLocaleDateString()
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('page-messages', PageMessages)

View File

@@ -1,6 +1,7 @@
import { t, i18n } from '../../i18n.js'
import { directus } from '../../services/directus.js'
import { auth } from '../../services/auth.js'
import { escapeHTML } from '../../utils/helpers.js'
import '../listing-card.js'
import '../skeleton-card.js'
@@ -144,10 +145,10 @@ class PageMyListings extends HTMLElement {
${statusBadge}
<listing-card
listing-id="${listing.id}"
title="${this.escapeHtml(listing.title || '')}"
title="${escapeHTML(listing.title || '')}"
price="${listing.price || ''}"
currency="${listing.currency || 'EUR'}"
location="${this.escapeHtml(locationName)}"
location="${escapeHTML(locationName)}"
image="${imageUrl}"
owner-id="${listing.user_created || ''}"
></listing-card>
@@ -155,12 +156,6 @@ class PageMyListings extends HTMLElement {
`
}).join('')
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
}
customElements.define('page-my-listings', PageMyListings)

View File

@@ -1,4 +1,5 @@
import { t, i18n } from '../i18n.js'
import { escapeHTML } from '../utils/helpers.js'
const CATEGORIES = {
electronics: ['phones', 'computers', 'tv_audio', 'gaming', 'appliances'],
@@ -171,7 +172,7 @@ class SearchBox extends HTMLElement {
if (this.searchQuery) {
badges.push(/* html */`
<button type="button" class="filter-badge" data-filter="query">
<span class="filter-badge-text">"${this.escapeHtml(this.searchQuery)}"</span>
<span class="filter-badge-text">"${escapeHTML(this.searchQuery)}"</span>
<svg class="filter-badge-close" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
@@ -230,12 +231,6 @@ class SearchBox extends HTMLElement {
`
}
escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
renderFilters() {
// Track which category accordion is expanded
this._expandedCategory = this._expandedCategory || ''

View File

@@ -175,31 +175,6 @@ class ConversationsService {
notifySubscribers() {
this.subscribers.forEach(cb => cb())
}
formatMessageTime(timestamp, locale = 'de-DE') {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) {
return 'Gerade eben'
}
if (diff < 3600000) {
const mins = Math.floor(diff / 60000)
return `vor ${mins} Min.`
}
if (diff < 86400000 && date.getDate() === now.getDate()) {
return date.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit' })
}
if (diff < 604800000) {
return date.toLocaleDateString(locale, { weekday: 'short', hour: '2-digit', minute: '2-digit' })
}
return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: '2-digit' })
}
}
export const conversationsService = new ConversationsService()