refactor: fix memory leak in auth-modal, consolidate escapeHTML helper across 11 components
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
}
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
// Styles
|
||||
const style = document.createElement('style')
|
||||
style.textContent = /* css */`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, '>')
|
||||
.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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 || ''
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user