refactor(ModsList): cache DOM elements for improved performance and readability

This commit is contained in:
Shintaro Jokagi 2025-05-29 12:40:11 +12:00
parent d7c6204b75
commit aa7b0f608d
No known key found for this signature in database
GPG key ID: 0DDF8FA44C9A0DA8
2 changed files with 164 additions and 75 deletions

View file

@ -175,17 +175,58 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
this.lastFilteredMods = null this.lastFilteredMods = null
this.lastRenderState = null this.lastRenderState = null
// Cache DOM elements for better performance and intellisense
this.elements = this.cacheElements()
this.initializeFromURL() this.initializeFromURL()
this.bindEvents() this.bindEvents()
this.renderMods() this.renderMods()
} }
cacheElements() {
const searchInput = document.getElementById('search')
const limitSelect = document.getElementById('limit')
const createdSortButton = document.getElementById('created-sort')
const updatedSortButton = document.getElementById('updated-sort')
const modsGrid = document.getElementById('mods-grid')
const noResults = document.getElementById('no-results')
const pagination = document.getElementById('pagination')
// Sort icon elements
const createdSortDefault = document.getElementById('created-sort-default')
const createdSortAsc = document.getElementById('created-sort-asc')
const createdSortDesc = document.getElementById('created-sort-desc')
const updatedSortDefault = document.getElementById('updated-sort-default')
const updatedSortAsc = document.getElementById('updated-sort-asc')
const updatedSortDesc = document.getElementById('updated-sort-desc')
return {
searchInput: searchInput instanceof HTMLInputElement ? searchInput : null,
limitSelect: limitSelect instanceof HTMLSelectElement ? limitSelect : null,
createdSortButton:
createdSortButton instanceof HTMLButtonElement ? createdSortButton : null,
updatedSortButton:
updatedSortButton instanceof HTMLButtonElement ? updatedSortButton : null,
modsGrid: modsGrid instanceof HTMLDivElement ? modsGrid : null,
noResults: noResults instanceof HTMLDivElement ? noResults : null,
pagination: pagination instanceof HTMLDivElement ? pagination : null,
sortIcons: {
createdDefault: createdSortDefault instanceof HTMLSpanElement ? createdSortDefault : null,
createdAsc: createdSortAsc instanceof HTMLSpanElement ? createdSortAsc : null,
createdDesc: createdSortDesc instanceof HTMLSpanElement ? createdSortDesc : null,
updatedDefault: updatedSortDefault instanceof HTMLSpanElement ? updatedSortDefault : null,
updatedAsc: updatedSortAsc instanceof HTMLSpanElement ? updatedSortAsc : null,
updatedDesc: updatedSortDesc instanceof HTMLSpanElement ? updatedSortDesc : null,
},
}
}
getLocalePath(path) { getLocalePath(path) {
if (this.locale && this.locale !== 'en' && !path.startsWith(`/${this.locale}`)) { if (this.locale && this.locale !== 'en' && !path.startsWith(`/${this.locale}`)) {
return `/${this.locale}${path.startsWith('/') ? '' : '/'}${path}` return `/${this.locale}${path.startsWith('/') ? '' : '/'}${path}`
} }
return path return path
} // Validation helpers - similar to Zod }
validateSortOrder(value, defaultValue = 'default') { validateSortOrder(value, defaultValue = 'default') {
const validSortOrders = ['default', 'asc', 'desc'] const validSortOrders = ['default', 'asc', 'desc']
return validSortOrders.includes(value) ? value : defaultValue return validSortOrders.includes(value) ? value : defaultValue
@ -204,7 +245,6 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
initializeFromURL() { initializeFromURL() {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
// Validate and sanitize URL parameters with fallbacks
const rawCreatedSort = params.get('created') const rawCreatedSort = params.get('created')
const rawUpdatedSort = params.get('updated') const rawUpdatedSort = params.get('updated')
const rawPage = params.get('page') const rawPage = params.get('page')
@ -218,13 +258,18 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
limit: this.validateLimit(rawLimit, 12), limit: this.validateLimit(rawLimit, 12),
} }
// Set form values // Set form values using cached elements
document.getElementById('search').value = this.state.search if (this.elements.searchInput) {
document.getElementById('limit').value = this.state.limit.toString() this.elements.searchInput.value = this.state.search
}
if (this.elements.limitSelect) {
this.elements.limitSelect.value = this.state.limit.toString()
}
} }
bindEvents() { bindEvents() {
// Search input with debouncing // Search input with debouncing
document.getElementById('search').addEventListener('input', e => { if (this.elements.searchInput) {
this.elements.searchInput.addEventListener('input', e => {
// Clear existing timeout // Clear existing timeout
if (this.searchTimeout) { if (this.searchTimeout) {
clearTimeout(this.searchTimeout) clearTimeout(this.searchTimeout)
@ -232,23 +277,36 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
// Set new timeout for debounced search // Set new timeout for debounced search
this.searchTimeout = setTimeout(() => { this.searchTimeout = setTimeout(() => {
if (e.target instanceof HTMLInputElement) {
this.setState({ search: e.target.value, page: 1 }) this.setState({ search: e.target.value, page: 1 })
}
}, 300) // 300ms debounce }, 300) // 300ms debounce
}) })
}
// Sort buttons // Sort buttons
document.getElementById('created-sort').addEventListener('click', () => { if (this.elements.createdSortButton) {
this.elements.createdSortButton.addEventListener('click', () => {
this.toggleCreatedSort() this.toggleCreatedSort()
}) })
}
document.getElementById('updated-sort').addEventListener('click', () => { if (this.elements.updatedSortButton) {
this.elements.updatedSortButton.addEventListener('click', () => {
this.toggleUpdatedSort() this.toggleUpdatedSort()
}) // Limit select })
document.getElementById('limit').addEventListener('change', e => { }
// Limit select
if (this.elements.limitSelect) {
this.elements.limitSelect.addEventListener('change', e => {
if (e.target instanceof HTMLSelectElement) {
const newLimit = this.validateLimit(e.target.value, this.state.limit) const newLimit = this.validateLimit(e.target.value, this.state.limit)
this.setState({ limit: newLimit, page: 1 }) this.setState({ limit: newLimit, page: 1 })
}
}) })
} }
}
setState(newState) { setState(newState) {
// Prevent multiple renders // Prevent multiple renders
if (this.isRendering) return if (this.isRendering) return
@ -311,27 +369,45 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
this.setState({ updatedSort: newSort, createdSort: 'default', page: 1 }) this.setState({ updatedSort: newSort, createdSort: 'default', page: 1 })
} }
updateSortIcons() { updateSortIcons() {
// Update created sort icons // Update created sort icons using cached elements
document if (this.elements.sortIcons.createdDefault) {
.getElementById('created-sort-default') this.elements.sortIcons.createdDefault.classList.toggle(
.classList.toggle('hidden', this.state.createdSort !== 'default') 'hidden',
document this.state.createdSort !== 'default'
.getElementById('created-sort-asc') )
.classList.toggle('hidden', this.state.createdSort !== 'asc') }
document if (this.elements.sortIcons.createdAsc) {
.getElementById('created-sort-desc') this.elements.sortIcons.createdAsc.classList.toggle(
.classList.toggle('hidden', this.state.createdSort !== 'desc') 'hidden',
this.state.createdSort !== 'asc'
)
}
if (this.elements.sortIcons.createdDesc) {
this.elements.sortIcons.createdDesc.classList.toggle(
'hidden',
this.state.createdSort !== 'desc'
)
}
// Update updated sort icons // Update updated sort icons using cached elements
document if (this.elements.sortIcons.updatedDefault) {
.getElementById('updated-sort-default') this.elements.sortIcons.updatedDefault.classList.toggle(
.classList.toggle('hidden', this.state.updatedSort !== 'default') 'hidden',
document this.state.updatedSort !== 'default'
.getElementById('updated-sort-asc') )
.classList.toggle('hidden', this.state.updatedSort !== 'asc') }
document if (this.elements.sortIcons.updatedAsc) {
.getElementById('updated-sort-desc') this.elements.sortIcons.updatedAsc.classList.toggle(
.classList.toggle('hidden', this.state.updatedSort !== 'desc') 'hidden',
this.state.updatedSort !== 'asc'
)
}
if (this.elements.sortIcons.updatedDesc) {
this.elements.sortIcons.updatedDesc.classList.toggle(
'hidden',
this.state.updatedSort !== 'desc'
)
}
} }
updateURL(isSearchOrFilter = false) { updateURL(isSearchOrFilter = false) {
const params = new URLSearchParams() const params = new URLSearchParams()
@ -399,12 +475,13 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
const endIndex = startIndex + this.state.limit const endIndex = startIndex + this.state.limit
const paginatedMods = filteredMods.slice(startIndex, endIndex) const paginatedMods = filteredMods.slice(startIndex, endIndex)
const modsGrid = document.getElementById('mods-grid')
const noResults = document.getElementById('no-results')
if (paginatedMods.length > 0) { if (paginatedMods.length > 0) {
noResults.classList.add('hidden') if (this.elements.noResults) {
modsGrid.classList.remove('hidden') this.elements.noResults.classList.add('hidden')
}
if (this.elements.modsGrid) {
this.elements.modsGrid.classList.remove('hidden')
}
// Check if we're in the default state // Check if we're in the default state
const isDefaultState = const isDefaultState =
@ -452,15 +529,21 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
} }
// Clear and append fragment (single reflow) // Clear and append fragment (single reflow)
modsGrid.innerHTML = '' if (this.elements.modsGrid) {
modsGrid.appendChild(fragment) this.elements.modsGrid.innerHTML = ''
this.elements.modsGrid.appendChild(fragment)
}
// Track that we've modified the content // Track that we've modified the content
this.hasBeenModified = !isDefaultState this.hasBeenModified = !isDefaultState
} }
} else { } else {
modsGrid.classList.add('hidden') if (this.elements.modsGrid) {
noResults.classList.replace('hidden', 'flex') this.elements.modsGrid.classList.add('hidden')
}
if (this.elements.noResults) {
this.elements.noResults.classList.replace('hidden', 'flex')
}
this.hasBeenModified = true this.hasBeenModified = true
} }
@ -505,11 +588,13 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
} }
renderPagination(totalPages, totalItems) { renderPagination(totalPages, totalItems) {
const pagination = document.getElementById('pagination') const paginationElement = this.elements.pagination
if (!paginationElement) return
if (totalPages <= 1) { if (totalPages <= 1) {
pagination.innerHTML = '' paginationElement.innerHTML = ''
pagination.classList.add('hidden') paginationElement.classList.add('hidden')
return return
} }
@ -539,7 +624,7 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
.replace('{totalPages}', totalPages.toString()) .replace('{totalPages}', totalPages.toString())
.replace('{totalItems}', totalItems.toString()) .replace('{totalItems}', totalItems.toString())
pagination.innerHTML = ` paginationElement.innerHTML = `
${prevButton} ${prevButton}
<form class="flex items-center gap-2" id="page-form"> <form class="flex items-center gap-2" id="page-form">
<input <input
@ -552,11 +637,15 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
<span class="text-sm">${paginationText.replace('{input}', '')}</span> <span class="text-sm">${paginationText.replace('{input}', '')}</span>
</form> </form>
${nextButton} ${nextButton}
` // Bind pagination events `
pagination.classList.replace('hidden', 'flex')
// Bind pagination events
paginationElement.classList.replace('hidden', 'flex')
const pageForm = document.getElementById('page-form') const pageForm = document.getElementById('page-form')
const pageInput = document.getElementById('page-input') const pageInput = document.getElementById('page-input')
if (pageForm && pageInput instanceof HTMLInputElement) {
pageForm.addEventListener('submit', e => { pageForm.addEventListener('submit', e => {
e.preventDefault() e.preventDefault()
const inputValue = pageInput.value const inputValue = pageInput.value
@ -570,10 +659,11 @@ const totalPages = Math.ceil(allMods.length / defaultLimit)
pageInput.value = this.state.page.toString() pageInput.value = this.state.page.toString()
} }
}) })
}
// Bind navigation links // Bind navigation links
pagination.addEventListener('click', e => { paginationElement.addEventListener('click', e => {
if (e.target.tagName === 'A' && e.target.dataset.page) { if (e.target instanceof HTMLAnchorElement && e.target.dataset.page) {
e.preventDefault() e.preventDefault()
this.navigatePage(parseInt(e.target.dataset.page, 10)) this.navigatePage(parseInt(e.target.dataset.page, 10))
} }

View file

@ -7,7 +7,6 @@ import { getLocale, getPath, getUI } from '~/utils/i18n'
export { getStaticPaths } from '~/utils/i18n' export { getStaticPaths } from '~/utils/i18n'
const locale = getLocale(Astro) const locale = getLocale(Astro)
console.log(Astro.currentLocale)
const getLocalePath = getPath(locale) const getLocalePath = getPath(locale)
const { const {
routes: { notFound }, routes: { notFound },