feat(halls): immediately render hall badge in DOM upon adding/removing without requiring page refresh
This commit is contained in:
+112
-19
@@ -3667,7 +3667,7 @@ window.cancelAnimFrame = (function () {
|
||||
}
|
||||
|
||||
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
||||
if (html && !options.skipCache) {
|
||||
if (html && !options.noCacheStore) {
|
||||
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
|
||||
if (itemCacheMap.size > ITEM_CACHE_MAX) {
|
||||
// Evict oldest entry
|
||||
@@ -10544,6 +10544,91 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// </image-modal>
|
||||
|
||||
// Halls Management (Admin Only / My Halls for all logged-in users)
|
||||
const escapeHtml = (str) => String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
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) => {
|
||||
// Add to Hall
|
||||
const hallBtn = e.target.closest('#a_hall');
|
||||
@@ -10709,8 +10794,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
const data = await resp.json();
|
||||
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.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true });
|
||||
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
|
||||
close();
|
||||
} else {
|
||||
throw new Error(data.msg || 'Failed to add to hall');
|
||||
@@ -10771,17 +10859,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (data.success) {
|
||||
myHallsConfirmBtn.disabled = false;
|
||||
myHallsConfirmBtn.textContent = window.f0ckI18n?.hall_add_btn || 'Add';
|
||||
// Patch the live #a_hall button NOW so next modal open pre-selects correctly
|
||||
// (before loadItemAjax finishes re-rendering the DOM)
|
||||
const liveHallBtn = document.getElementById('a_hall');
|
||||
if (liveHallBtn) {
|
||||
liveHallBtn.dataset.currentUserHall = hallSlug;
|
||||
const existing = (liveHallBtn.dataset.userHalls || '').split(',').filter(Boolean);
|
||||
if (!existing.includes(hallSlug)) existing.push(hallSlug);
|
||||
liveHallBtn.dataset.userHalls = existing.join(',');
|
||||
}
|
||||
const hallName = (myHallsSelect && myHallsSelect.selectedOptions && myHallsSelect.selectedOptions[0])
|
||||
? myHallsSelect.selectedOptions[0].textContent.replace(/\s*🔒\s*$/, '')
|
||||
: hallSlug;
|
||||
updateHallBadgeInDom(hallSlug, hallName, 'add', true);
|
||||
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
|
||||
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();
|
||||
} else {
|
||||
throw new Error(data.message || data.msg || 'Failed to add');
|
||||
@@ -10814,8 +10898,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
const data = await resp.json();
|
||||
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.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true });
|
||||
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
|
||||
close();
|
||||
} else {
|
||||
throw new Error(data.msg || 'Failed to remove from hall');
|
||||
@@ -10845,8 +10931,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (data.success) {
|
||||
myHallsRemoveBtn.disabled = false;
|
||||
myHallsRemoveBtn.textContent = 'Remove';
|
||||
updateHallBadgeInDom(slug, '', 'remove', true);
|
||||
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
|
||||
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();
|
||||
} else {
|
||||
throw new Error(data.message || data.msg || 'Failed to remove');
|
||||
@@ -10869,20 +10957,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!confirm('Remove from this hall?')) return;
|
||||
|
||||
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;
|
||||
|
||||
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', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `id=${itemid}&hall=${encodeURIComponent(hallSlug)}`
|
||||
headers: {
|
||||
'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();
|
||||
if (data.success) {
|
||||
// Refresh item view to remove hall badge
|
||||
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true });
|
||||
updateHallBadgeInDom(hallSlug, '', 'remove', false);
|
||||
if (window.invalidateItemCache) window.invalidateItemCache(itemid);
|
||||
if (window.loadItemAjax) window.loadItemAjax(window.location.href, true, { keepMedia: true, skipCache: true });
|
||||
} else {
|
||||
window.flashMessage(data.msg || 'Failed to remove from hall', 3000, 'error');
|
||||
}
|
||||
|
||||
@@ -131,14 +131,11 @@
|
||||
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
|
||||
@endif
|
||||
</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
|
||||
|
||||
|
||||
@if(halls_enabled && item.primaryHall)
|
||||
<span class="badge hall-badge-wrap">
|
||||
<span id="hall-badge-container">@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
|
||||
</span>
|
||||
@endif
|
||||
</span>@endif</span>
|
||||
|
||||
|
||||
<div class="gapRight">
|
||||
|
||||
@@ -120,12 +120,10 @@
|
||||
<div class="blahlol">
|
||||
<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>
|
||||
</span>@if(halls_enabled && item.primaryHall) — @endif
|
||||
@if(halls_enabled && item.primaryHall)
|
||||
<span class="badge hall-badge-wrap">
|
||||
</span>
|
||||
<span id="hall-badge-container">@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
|
||||
</span> —
|
||||
@endif
|
||||
</span> — @endif</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)
|
||||
— <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>
|
||||
|
||||
Reference in New Issue
Block a user