feat(halls): immediately render hall badge in DOM upon adding/removing without requiring page refresh

This commit is contained in:
2026-09-05 01:37:41 +02:00
parent a5c45d693a
commit 30fdaf4363
3 changed files with 118 additions and 30 deletions
+112 -19
View File
@@ -3667,7 +3667,7 @@ window.cancelAnimFrame = (function () {
} }
// ── Store in item cache (stale-while-revalidate) ─────────────────────── // ── Store in item cache (stale-while-revalidate) ───────────────────────
if (html && !options.skipCache) { if (html && !options.noCacheStore) {
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() }); itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
if (itemCacheMap.size > ITEM_CACHE_MAX) { if (itemCacheMap.size > ITEM_CACHE_MAX) {
// Evict oldest entry // Evict oldest entry
@@ -10544,6 +10544,91 @@ document.addEventListener('DOMContentLoaded', () => {
// </image-modal> // </image-modal>
// Halls Management (Admin Only / My Halls for all logged-in users) // Halls Management (Admin Only / My Halls for all logged-in users)
const escapeHtml = (str) => String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
const updateHallBadgeInDom = (hallSlug, hallName, action = 'add', isUserHall = false) => {
if (!hallSlug) return;
const liveHallBtn = document.getElementById('a_hall');
const isModern = document.body.classList.contains('layout-modern') || !document.body.classList.contains('layout-legacy');
// 1. Update dataset on live #a_hall button
if (liveHallBtn) {
const datasetKey = isUserHall ? 'userHalls' : 'halls';
const existing = (liveHallBtn.dataset[datasetKey] || '').split(',').filter(Boolean);
if (action === 'add') {
if (!existing.includes(hallSlug)) existing.push(hallSlug);
if (isUserHall) liveHallBtn.dataset.currentUserHall = hallSlug;
else liveHallBtn.dataset.currentHall = hallSlug;
} else if (action === 'remove') {
const idx = existing.indexOf(hallSlug);
if (idx !== -1) existing.splice(idx, 1);
if (isUserHall && liveHallBtn.dataset.currentUserHall === hallSlug) {
liveHallBtn.dataset.currentUserHall = existing[0] || '';
} else if (!isUserHall && liveHallBtn.dataset.currentHall === hallSlug) {
liveHallBtn.dataset.currentHall = existing[0] || '';
}
}
liveHallBtn.dataset[datasetKey] = existing.join(',');
}
// 2. Main item header badge reflects site halls
if (isUserHall) return;
let container = document.getElementById('hall-badge-container');
if (!container) {
// Check for existing .hall-badge-wrap without wrapper
const existingWrap = document.querySelector('.hall-badge-wrap');
if (existingWrap) {
if (action === 'remove') {
existingWrap.remove();
return;
}
existingWrap.outerHTML = `<span class="badge hall-badge-wrap"><a href="/h/${escapeHtml(hallSlug)}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> ${escapeHtml(hallName || hallSlug)}</a></span>`;
return;
}
// Dynamically create container and insert at the appropriate place
container = document.createElement('span');
container.id = 'hall-badge-container';
if (isModern) {
const tsLink = document.querySelector('.blahlol .timestamp-link');
const tsBadge = tsLink ? tsLink.closest('.badge') : null;
if (tsBadge && tsBadge.parentNode) {
tsBadge.parentNode.insertBefore(container, tsBadge);
} else {
const blahlol = document.querySelector('.blahlol');
if (blahlol) blahlol.appendChild(container);
}
} else {
const gapRight = document.querySelector('.blahlol .gapRight');
if (gapRight && gapRight.parentNode) {
gapRight.parentNode.insertBefore(container, gapRight);
} else {
const blahlol = document.querySelector('.blahlol');
if (blahlol) blahlol.appendChild(container);
}
}
}
if (action === 'add') {
const badgeHtml = `<span class="badge hall-badge-wrap"><a href="/h/${escapeHtml(hallSlug)}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> ${escapeHtml(hallName || hallSlug)}</a></span>`;
container.innerHTML = isModern ? `${badgeHtml}` : `${badgeHtml}`;
} else if (action === 'remove') {
const remaining = liveHallBtn ? (liveHallBtn.dataset.halls || '').split(',').filter(Boolean) : [];
if (remaining.length > 0) {
const nextSlug = remaining[0];
const badgeHtml = `<span class="badge hall-badge-wrap"><a href="/h/${escapeHtml(nextSlug)}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> ${escapeHtml(nextSlug)}</a></span>`;
container.innerHTML = isModern ? `${badgeHtml}` : `${badgeHtml}`;
} else {
container.innerHTML = '';
}
}
};
document.addEventListener('click', async (e) => { document.addEventListener('click', async (e) => {
// Add to Hall // Add to Hall
const hallBtn = e.target.closest('#a_hall'); const hallBtn = e.target.closest('#a_hall');
@@ -10709,8 +10794,11 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
const hallName = (select.selectedOptions && select.selectedOptions[0]) ? select.selectedOptions[0].textContent : hallSlug;
updateHallBadgeInDom(hallSlug, hallName, 'add', false);
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_added) || 'Added to hall!'); if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_added) || 'Added to hall!');
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true }); if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
close(); close();
} else { } else {
throw new Error(data.msg || 'Failed to add to hall'); throw new Error(data.msg || 'Failed to add to hall');
@@ -10771,17 +10859,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success) { if (data.success) {
myHallsConfirmBtn.disabled = false; myHallsConfirmBtn.disabled = false;
myHallsConfirmBtn.textContent = window.f0ckI18n?.hall_add_btn || 'Add'; myHallsConfirmBtn.textContent = window.f0ckI18n?.hall_add_btn || 'Add';
// Patch the live #a_hall button NOW so next modal open pre-selects correctly const hallName = (myHallsSelect && myHallsSelect.selectedOptions && myHallsSelect.selectedOptions[0])
// (before loadItemAjax finishes re-rendering the DOM) ? myHallsSelect.selectedOptions[0].textContent.replace(/\s*🔒\s*$/, '')
const liveHallBtn = document.getElementById('a_hall'); : hallSlug;
if (liveHallBtn) { updateHallBadgeInDom(hallSlug, hallName, 'add', true);
liveHallBtn.dataset.currentUserHall = hallSlug; if (window.invalidateItemCache) window.invalidateItemCache(itemid);
const existing = (liveHallBtn.dataset.userHalls || '').split(',').filter(Boolean);
if (!existing.includes(hallSlug)) existing.push(hallSlug);
liveHallBtn.dataset.userHalls = existing.join(',');
}
if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_added) || 'Added to hall!'); if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_added) || 'Added to hall!');
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true }); if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
close(); close();
} else { } else {
throw new Error(data.message || data.msg || 'Failed to add'); throw new Error(data.message || data.msg || 'Failed to add');
@@ -10814,8 +10898,10 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
updateHallBadgeInDom(hallSlug, '', 'remove', false);
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_removed) || 'Removed from hall!'); if (window.flashMessage) window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_removed) || 'Removed from hall!');
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true }); if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
close(); close();
} else { } else {
throw new Error(data.msg || 'Failed to remove from hall'); throw new Error(data.msg || 'Failed to remove from hall');
@@ -10845,8 +10931,10 @@ document.addEventListener('DOMContentLoaded', () => {
if (data.success) { if (data.success) {
myHallsRemoveBtn.disabled = false; myHallsRemoveBtn.disabled = false;
myHallsRemoveBtn.textContent = 'Remove'; myHallsRemoveBtn.textContent = 'Remove';
updateHallBadgeInDom(slug, '', 'remove', true);
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_removed) || 'Removed from hall!'); window.flashMessage((window.f0ckI18n && window.f0ckI18n.hall_removed) || 'Removed from hall!');
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true }); if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
close(); close();
} else { } else {
throw new Error(data.message || data.msg || 'Failed to remove'); throw new Error(data.message || data.msg || 'Failed to remove');
@@ -10869,20 +10957,25 @@ document.addEventListener('DOMContentLoaded', () => {
if (!confirm('Remove from this hall?')) return; if (!confirm('Remove from this hall?')) return;
const container = document.getElementById('comments-container'); const container = document.getElementById('comments-container');
const itemid = container ? container.dataset.itemId : null; const itemid = container ? container.dataset.itemId : (document.querySelector('.id-link')?.dataset.itemId || null);
const hallSlug = removeBtn.dataset.hall; const hallSlug = removeBtn.dataset.hall;
if (!itemid || !hallSlug) return window.flashMessage('Error: Missing itemid or hallSlug', 3000, 'error'); if (!itemid || !hallSlug) return window.flashMessage('Error: Missing itemid or hallSlug', 3000, 'error');
const token = (window.f0ckSession && window.f0ckSession.csrf_token) || '';
const resp = await fetch('/mod/halls/remove', { const resp = await fetch('/mod/halls/remove', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: {
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}` 'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-Token': token
},
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}&csrf_token=${encodeURIComponent(token)}`
}); });
const data = await resp.json(); const data = await resp.json();
if (data.success) { if (data.success) {
// Refresh item view to remove hall badge updateHallBadgeInDom(hallSlug, '', 'remove', false);
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true }); if (window.invalidateItemCache) window.invalidateItemCache(itemid);
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
} else { } else {
window.flashMessage(data.msg || 'Failed to remove from hall', 3000, 'error'); window.flashMessage(data.msg || 'Failed to remove from hall', 3000, 'error');
} }
+3 -6
View File
@@ -131,14 +131,11 @@
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span> <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
@endif @endif
</span> </span>
@if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(item.expires_at) — <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>@endif@if(halls_enabled && item.primaryHall) — @endif @endif @if(!user_alternative_infobox) — <span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>@if(item.expires_at) — <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>@endif @endif
<span id="hall-badge-container">@if(halls_enabled && item.primaryHall) — <span class="badge hall-badge-wrap">
@if(halls_enabled && item.primaryHall)
<span class="badge hall-badge-wrap">
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif <a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
</span> </span>@endif</span>
@endif
<div class="gapRight"> <div class="gapRight">
+3 -5
View File
@@ -120,12 +120,10 @@
<div class="blahlol"> <div class="blahlol">
<span class="badge badge-dark"> <span class="badge badge-dark">
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span> <a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
</span>@if(halls_enabled && item.primaryHall) — @endif </span>
@if(halls_enabled && item.primaryHall) <span id="hall-badge-container">@if(halls_enabled && item.primaryHall)<span class="badge hall-badge-wrap">
<span class="badge hall-badge-wrap">
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif <a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
</span> </span> @endif</span>
@endif
<span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span> <span class="badge badge-dark"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
@if(item.expires_at) @if(item.expires_at)
<span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span> <span class="badge badge-warning" style="background: rgba(255, 170, 0, 0.2); color: #ffaa00; border: 1px solid rgba(255, 170, 0, 0.4);" tooltip="Self-destructs {{ item.expires_in }}" flow="up"><i class="fa-solid fa-clock"></i> Expiring ({{ item.expires_in }})</span>