gsdfgsdf
This commit is contained in:
+66
-14
@@ -51,6 +51,12 @@
|
||||
if (!tagsContainer) return;
|
||||
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
|
||||
|
||||
const canManage = !!(
|
||||
document.querySelector('#tags[data-can-manage="true"]') ||
|
||||
(window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) ||
|
||||
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
|
||||
);
|
||||
|
||||
// Only remove existing dynamically generated tags
|
||||
[...inner.querySelectorAll(".badge")].forEach(tag => {
|
||||
// Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself
|
||||
@@ -59,34 +65,80 @@
|
||||
}
|
||||
});
|
||||
|
||||
_tags.reverse().forEach(tag => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/tag/${tag.normalized}`;
|
||||
a.style = "color: inherit !important";
|
||||
a.textContent = tag.tag;
|
||||
// Deduplicate: ensure only at most ONE rating tag exists in the tags list
|
||||
const ratingTags = _tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
|
||||
const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null;
|
||||
const cleanTags = _tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag);
|
||||
|
||||
const tagsCopy = [...cleanTags];
|
||||
tagsCopy.reverse().forEach(tag => {
|
||||
const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized);
|
||||
let contentEl;
|
||||
if (isRating) {
|
||||
contentEl = document.createElement("span");
|
||||
contentEl.className = "rating-label";
|
||||
contentEl.textContent = tag.tag;
|
||||
} else {
|
||||
contentEl = document.createElement("a");
|
||||
contentEl.href = `/tag/${tag.normalized}`;
|
||||
contentEl.style = "color: inherit !important";
|
||||
contentEl.textContent = tag.tag;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("badge");
|
||||
if (highlightTag && (tag.tag === highlightTag || tag.normalized === highlightTag)) {
|
||||
span.classList.add('new-tag-glow');
|
||||
}
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
|
||||
tag.badge.split(" ").forEach(b => span.classList.add(b));
|
||||
|
||||
const delbutton = document.createElement("a");
|
||||
delbutton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||
delbutton.href = "javascript:void(0)";
|
||||
// Class for delegation
|
||||
delbutton.classList.add("admin-deltag", "removetag");
|
||||
if (isRating) {
|
||||
span.classList.add('rating-tag', `is-${tag.normalized}`);
|
||||
span.dataset.rating = tag.normalized;
|
||||
if (canManage) {
|
||||
span.classList.add('can-cycle');
|
||||
}
|
||||
} else if (tag.display_name || tag.user) {
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
}
|
||||
|
||||
span.appendChild(a);
|
||||
span.appendChild(document.createTextNode('\u00A0'));
|
||||
span.appendChild(delbutton);
|
||||
span.appendChild(contentEl);
|
||||
|
||||
if (!isRating) {
|
||||
const delbutton = document.createElement("a");
|
||||
delbutton.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||
delbutton.href = "javascript:void(0)";
|
||||
// Class for delegation
|
||||
delbutton.classList.add("admin-deltag", "removetag");
|
||||
|
||||
span.appendChild(document.createTextNode('\u00A0'));
|
||||
span.appendChild(delbutton);
|
||||
}
|
||||
|
||||
inner.insertAdjacentElement("afterbegin", span);
|
||||
});
|
||||
|
||||
const hasRating = !!lastRatingTag;
|
||||
if (!hasRating) {
|
||||
const untaggedSpan = document.createElement("span");
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.dataset.rating = 'untagged';
|
||||
const lbl = document.createElement("span");
|
||||
lbl.className = "rating-label";
|
||||
lbl.textContent = "untagged";
|
||||
untaggedSpan.appendChild(lbl);
|
||||
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
||||
}
|
||||
|
||||
// Safeguard: remove any extra rating tags in DOM
|
||||
const allRatingEls = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
if (allRatingEls.length > 1) {
|
||||
for (let i = 1; i < allRatingEls.length; i++) {
|
||||
allRatingEls[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle show more/less toggle visibility and count
|
||||
const allBadges = [...inner.querySelectorAll(".badge")];
|
||||
const realTags = allBadges.filter(b => !b.querySelector('#a_addtag') && !b.querySelector('#a_toggle') && !b.classList.contains('tag-ac-wrapper'));
|
||||
|
||||
+292
-231
@@ -38,7 +38,7 @@ window.cancelAnimFrame = (function () {
|
||||
};
|
||||
|
||||
window.getCurrentItemId = () => {
|
||||
const idEl = document.querySelector('a.id-link[data-item-id], #a_toggle[data-item-id], #a_favo[data-item-id], #comments-container[data-item-id], [data-item-id]');
|
||||
const idEl = document.querySelector('a.id-link[data-item-id], .rating-tag[data-item-id], #a_toggle[data-item-id], #a_favo[data-item-id], #comments-container[data-item-id], [data-item-id]');
|
||||
if (idEl && idEl.dataset && idEl.dataset.itemId) {
|
||||
const parsed = parseInt(idEl.dataset.itemId, 10);
|
||||
if (!isNaN(parsed)) return String(parsed);
|
||||
@@ -1299,6 +1299,10 @@ window.cancelAnimFrame = (function () {
|
||||
'#comments-container',
|
||||
'.tag-controls',
|
||||
'.sidebar-tags-container',
|
||||
'#tags',
|
||||
'.rating-tag',
|
||||
'.rating-label',
|
||||
'.item-sidebar-left',
|
||||
'.xd-score-wrapper',
|
||||
'.user-infobox-block',
|
||||
'a',
|
||||
@@ -1762,6 +1766,15 @@ window.cancelAnimFrame = (function () {
|
||||
document.body.classList.remove('modal-open');
|
||||
return;
|
||||
}
|
||||
const tagAcWrapper = document.querySelector('.tag-ac-wrapper');
|
||||
if (tagAcWrapper || (window.TagAutocomplete && typeof window.TagAutocomplete.isOpen === 'function' && window.TagAutocomplete.isOpen())) {
|
||||
if (window.TagAutocomplete && typeof window.TagAutocomplete.destroy === 'function') {
|
||||
window.TagAutocomplete.destroy();
|
||||
} else if (tagAcWrapper) {
|
||||
tagAcWrapper.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (document.body.classList.contains('onara-modal-open')) {
|
||||
closeOnaraModal();
|
||||
}
|
||||
@@ -4530,6 +4543,20 @@ window.cancelAnimFrame = (function () {
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
|
||||
|
||||
// Intercept rating cycling immediately before any link/navigation handler can catch it
|
||||
const ratingTag = target.closest('.rating-tag') || target.closest('button#a_toggle');
|
||||
if (ratingTag && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && e.button === 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const canCycle = ratingTag.classList.contains('can-cycle') || ratingTag.id === 'a_toggle' || !!document.querySelector('#tags[data-can-manage="true"]');
|
||||
if (canCycle && window.cycleRating) {
|
||||
window.cycleRating(ratingTag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for mode selection (only applies to <a href="/mode/..."> links, not plain buttons)
|
||||
const modeBtn = target.closest('.mode-btn');
|
||||
if (modeBtn && modeBtn.href && modeBtn.href.includes('/mode/') && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
@@ -4714,10 +4741,11 @@ window.cancelAnimFrame = (function () {
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success && data.items && (data.items.slug || data.items.id)) {
|
||||
const targetKey = data.items.slug || data.items.id;
|
||||
const targetId = data.items.id;
|
||||
const targetKey = data.items.slug || data.items.id;
|
||||
const targetId = data.items.id;
|
||||
const targetPage = data.items.page || 1;
|
||||
|
||||
// Navigate immediately — don't wait for grid sync
|
||||
// Navigate immediately
|
||||
if (wUserHall && wUserHallOwner) {
|
||||
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true, { transition: 'fade-zoom' });
|
||||
} else if (wFavsUser) {
|
||||
@@ -4726,68 +4754,40 @@ window.cancelAnimFrame = (function () {
|
||||
loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' });
|
||||
}
|
||||
|
||||
// Background grid sync for Onara — fire and forget, user never waits for this
|
||||
// Background grid sync — page number already in response, no extra fetch needed
|
||||
if (isOnaraActive() && targetId) {
|
||||
const gridThumbs = Array.from(document.querySelectorAll('.posts > a:not(.notif-item)'));
|
||||
const alreadyInGrid = gridThumbs.some(a => {
|
||||
const k = a.pathname.replace(/\/$/, '').split('/').pop();
|
||||
return k == targetId || (data.items.slug && k === data.items.slug);
|
||||
});
|
||||
console.log('[random-gridsync] isOnaraActive:', true, 'alreadyInGrid:', alreadyInGrid, 'targetId:', targetId, '_onaraCurrentGridUrl:', window._onaraCurrentGridUrl);
|
||||
const postsEl = document.querySelector('.posts[data-current-page]');
|
||||
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
|
||||
|
||||
if (!alreadyInGrid) {
|
||||
const pageParams = new URLSearchParams(params);
|
||||
pageParams.set('id', targetId);
|
||||
console.log('[random-gridsync] fetching item-page:', `/api/v2/item-page?${pageParams}`);
|
||||
fetch(`/api/v2/item-page?${pageParams}`)
|
||||
.then(r => r.json())
|
||||
.then(pd => {
|
||||
console.log('[random-gridsync] item-page response:', pd);
|
||||
if (!pd.success || !pd.page) return;
|
||||
const targetPage = pd.page;
|
||||
const postsEl = document.querySelector('.posts[data-current-page]');
|
||||
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
|
||||
console.log('[random-gridsync] targetPage:', targetPage, 'currentPage:', currentPage, '_onaraCurrentGridUrl:', window._onaraCurrentGridUrl);
|
||||
if (targetPage === currentPage) return;
|
||||
if (targetPage !== currentPage) {
|
||||
// Build page URL from current grid base
|
||||
let gridBase = window._onaraCurrentGridUrl
|
||||
? window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '')
|
||||
: window.location.pathname
|
||||
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
|
||||
.replace(/\/\d+$/, '')
|
||||
.replace(/\/p\/\d+/, '')
|
||||
.replace(/\/$/, '');
|
||||
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
|
||||
|
||||
let gridBase;
|
||||
if (window._onaraCurrentGridUrl) {
|
||||
// Strip trailing slash — root '/' becomes '' so '/p/N' builds correctly
|
||||
gridBase = window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '');
|
||||
} else {
|
||||
gridBase = window.location.pathname
|
||||
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
|
||||
.replace(/\/\d+$/, '')
|
||||
.replace(/\/p\/\d+/, '')
|
||||
.replace(/\/$/, '');
|
||||
fetch(pageUrl)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
}
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
if (typeof updateOnaraActiveItem === 'function') {
|
||||
updateOnaraActiveItem(targetId, `/${targetKey}`);
|
||||
}
|
||||
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
|
||||
console.log('[random-gridsync] fetching grid page:', pageUrl);
|
||||
|
||||
fetch(pageUrl)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
console.log('[random-gridsync] grid swap: newPosts found:', !!newPosts, 'oldPosts found:', !!oldPosts);
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
}
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
// Highlight after grid swapped
|
||||
if (typeof updateOnaraActiveItem === 'function') {
|
||||
updateOnaraActiveItem(targetId, `/${targetKey}`);
|
||||
}
|
||||
})
|
||||
.catch((e) => { console.warn('[random-gridsync] grid fetch failed', e); });
|
||||
})
|
||||
.catch((e) => { console.warn('[random-gridsync] item-page fetch failed', e); });
|
||||
.catch(() => {});
|
||||
}
|
||||
} else {
|
||||
console.log('[random-gridsync] skipped: isOnaraActive=', isOnaraActive(), 'targetId=', targetId);
|
||||
}
|
||||
} else {
|
||||
window.location.href = link.href;
|
||||
@@ -4827,7 +4827,7 @@ window.cancelAnimFrame = (function () {
|
||||
const anyLink = target.closest('a');
|
||||
if (anyLink && anyLink.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && anyLink.target !== '_blank') {
|
||||
const pathname = anyLink.pathname;
|
||||
const isSpecialLink = anyLink.classList.contains('removetag') || anyLink.classList.contains('admin-deltag') || anyLink.classList.contains('mode-btn') || anyLink.classList.contains('btn-approve-async') || anyLink.classList.contains('btn-deny-async') || anyLink.getAttribute('href') === '#';
|
||||
const isSpecialLink = anyLink.classList.contains('removetag') || anyLink.classList.contains('admin-deltag') || anyLink.classList.contains('mode-btn') || anyLink.classList.contains('btn-approve-async') || anyLink.classList.contains('btn-deny-async') || anyLink.getAttribute('href') === '#' || !!anyLink.closest('.rating-tag');
|
||||
|
||||
const targetUrl = anyLink.href;
|
||||
const currentUrl = window.location.href.split('#')[0];
|
||||
@@ -5151,168 +5151,175 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
fileInput.dataset.itemId = itemId;
|
||||
fileInput.click();
|
||||
|
||||
} else if (target.closest('button#a_toggle')) {
|
||||
e.preventDefault();
|
||||
const toggleBtn = target.closest('button#a_toggle');
|
||||
const itemIdStr = toggleBtn.dataset.itemId || window.getCurrentItemId();
|
||||
if (!itemIdStr) return;
|
||||
const postid = parseInt(itemIdStr, 10);
|
||||
if (isNaN(postid)) return;
|
||||
|
||||
// Determine current rating
|
||||
let currentRating = 'sfw';
|
||||
if (toggleBtn.classList.contains('is-sfw')) currentRating = 'sfw';
|
||||
else if (toggleBtn.classList.contains('is-nsfw')) currentRating = 'nsfw';
|
||||
else if (toggleBtn.classList.contains('is-nsfl')) currentRating = 'nsfl';
|
||||
else if (toggleBtn.classList.contains('is-untagged')) currentRating = 'untagged';
|
||||
|
||||
// Cycle SFW -> NSFW -> NSFL (if enabled) -> SFW
|
||||
let nextRating = 'sfw';
|
||||
if (currentRating === 'sfw') {
|
||||
nextRating = 'nsfw';
|
||||
} else if (currentRating === 'nsfw') {
|
||||
nextRating = (window.f0ckSession && window.f0ckSession.enable_nsfl) ? 'nsfl' : 'sfw';
|
||||
}
|
||||
|
||||
const labels = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: '?' };
|
||||
const nextLabel = labels[nextRating] || nextRating.toUpperCase();
|
||||
|
||||
// Backup current state
|
||||
const oldClasses = [...toggleBtn.classList];
|
||||
const oldInnerHTML = toggleBtn.innerHTML;
|
||||
|
||||
// Track active request ID to ignore out-of-order race conditions on rapid keypresses
|
||||
const reqId = (toggleBtn._lastCycleReqId || 0) + 1;
|
||||
toggleBtn._lastCycleReqId = reqId;
|
||||
|
||||
// Increment active requests count to block incoming live SSE tag updates during cycle
|
||||
toggleBtn._activeRequestsCount = (toggleBtn._activeRequestsCount || 0) + 1;
|
||||
|
||||
// Optimistically apply new state
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${nextRating}`);
|
||||
// (icon stays — only CSS class color changes)
|
||||
|
||||
// Optimistically update the sidebar tag list
|
||||
let originalTags = [];
|
||||
if (window.renderTags) {
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||
if (inner) {
|
||||
originalTags = [...inner.querySelectorAll(".badge")].filter(badge => {
|
||||
return !badge.querySelector('#a_addtag') && !badge.querySelector('#a_toggle') && !badge.classList.contains('tag-ac-wrapper');
|
||||
}).map(badge => {
|
||||
const a = badge.querySelector('a[href*="/tag/"]');
|
||||
const tagText = a ? a.innerText.trim() : '';
|
||||
const normalized = a ? a.getAttribute('href').split('/').pop() : '';
|
||||
const badgeClasses = [...badge.classList].filter(c => c !== 'badge' && c !== 'mr-2').join(' ');
|
||||
return {
|
||||
tag: tagText,
|
||||
normalized: normalized,
|
||||
badge: badgeClasses
|
||||
};
|
||||
});
|
||||
|
||||
// Create the optimistic rating tag object
|
||||
const userStr = (window.f0ckSession && window.f0ckSession.user) || '';
|
||||
const dispName = (window.f0ckSession && window.f0ckSession.display_name) || userStr;
|
||||
let newRatingTag = null;
|
||||
if (nextRating === 'sfw') {
|
||||
newRatingTag = { id: 1, tag: 'sfw', normalized: 'sfw', badge: 'badge-success', user: userStr, display_name: dispName };
|
||||
} else if (nextRating === 'nsfw') {
|
||||
newRatingTag = { id: 2, tag: 'nsfw', normalized: 'nsfw', badge: 'badge-danger', user: userStr, display_name: dispName };
|
||||
} else if (nextRating === 'nsfl') {
|
||||
const nsfl_id = (window.f0ckSession && window.f0ckSession.nsfl_tag_id) || 3;
|
||||
newRatingTag = { id: nsfl_id, tag: 'nsfl', normalized: 'nsfl', badge: 'badge-nsfl', user: userStr, display_name: dispName };
|
||||
}
|
||||
|
||||
// Combine tags: filter out old rating tags and place the new one at the front
|
||||
const optimisticTags = [];
|
||||
if (newRatingTag) {
|
||||
optimisticTags.push(newRatingTag);
|
||||
}
|
||||
originalTags.filter(t => t.normalized !== 'sfw' && t.normalized !== 'nsfw' && t.normalized !== 'nsfl').forEach(t => {
|
||||
optimisticTags.push(t);
|
||||
});
|
||||
|
||||
window.renderTags(optimisticTags);
|
||||
}
|
||||
}
|
||||
|
||||
const flashMsg = (window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', nextLabel)) || ('RATING UPDATED: ' + nextLabel);
|
||||
window.flashMessage(flashMsg);
|
||||
|
||||
// Enqueue the request to ensure sequential server execution in correct order
|
||||
if (!toggleBtn._requestQueue) {
|
||||
toggleBtn._requestQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
toggleBtn._requestQueue = toggleBtn._requestQueue.then(() => {
|
||||
return fetch(`/api/v2/item/${postid}/rating`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ rating: nextRating })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||
|
||||
// Evict cached item HTML immediately on success so navigating back fetches fresh content,
|
||||
// even if the user already navigated to a different item while this request was in flight.
|
||||
if (res && res.success && window.invalidateItemCache) {
|
||||
window.invalidateItemCache(postid);
|
||||
}
|
||||
|
||||
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (toggleBtn._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||
if (res.success) {
|
||||
// Verify visual state and sync tags
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${res.rating}`);
|
||||
// icon stays constant — class drives the color
|
||||
|
||||
if (window.renderTags) {
|
||||
window.renderTags(res.tags);
|
||||
}
|
||||
} else {
|
||||
revert();
|
||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||
|
||||
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (toggleBtn._lastCycleReqId !== reqId) return;
|
||||
console.error('[RATING_TOGGLE_ERROR]', err);
|
||||
revert();
|
||||
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
function revert() {
|
||||
toggleBtn.className = '';
|
||||
oldClasses.forEach(cls => toggleBtn.classList.add(cls));
|
||||
toggleBtn.innerHTML = oldInnerHTML;
|
||||
if (window.renderTags && originalTags.length > 0) {
|
||||
window.renderTags(originalTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window._activeRatingCycleRequestsCount = 0;
|
||||
|
||||
window.cycleRating = function(targetEl, explicitPostId) {
|
||||
const ratingEl = targetEl ? (targetEl.closest('.rating-tag') || targetEl.closest('button#a_toggle') || targetEl) : document.querySelector('.rating-tag.can-cycle, button#a_toggle');
|
||||
if (!ratingEl) return;
|
||||
|
||||
const itemIdStr = explicitPostId || ratingEl.dataset.itemId || window.getCurrentItemId();
|
||||
if (!itemIdStr) return;
|
||||
const postid = parseInt(itemIdStr, 10);
|
||||
if (isNaN(postid)) return;
|
||||
|
||||
// Determine current rating
|
||||
let currentRating = ratingEl.dataset.rating || '';
|
||||
if (!currentRating) {
|
||||
if (ratingEl.classList.contains('is-sfw') || ratingEl.classList.contains('badge-success')) currentRating = 'sfw';
|
||||
else if (ratingEl.classList.contains('is-nsfw') || ratingEl.classList.contains('badge-danger')) currentRating = 'nsfw';
|
||||
else if (ratingEl.classList.contains('is-nsfl') || ratingEl.classList.contains('badge-nsfl')) currentRating = 'nsfl';
|
||||
else if (ratingEl.classList.contains('is-untagged') || ratingEl.classList.contains('badge-untagged')) currentRating = 'untagged';
|
||||
else {
|
||||
const txt = ratingEl.innerText.trim().toLowerCase();
|
||||
if (txt === 'sfw') currentRating = 'sfw';
|
||||
else if (txt === 'nsfw') currentRating = 'nsfw';
|
||||
else if (txt === 'nsfl') currentRating = 'nsfl';
|
||||
else currentRating = 'untagged';
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle: SFW -> NSFW -> NSFL (if enabled) -> SFW; untagged -> SFW
|
||||
let nextRating = 'sfw';
|
||||
if (currentRating === 'sfw') {
|
||||
nextRating = 'nsfw';
|
||||
} else if (currentRating === 'nsfw') {
|
||||
nextRating = (window.f0ckSession && window.f0ckSession.enable_nsfl !== false) ? 'nsfl' : 'sfw';
|
||||
} else if (currentRating === 'nsfl') {
|
||||
nextRating = 'sfw';
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
sfw: { label: 'SFW', text: 'sfw', badgeCls: 'badge-success', isCls: 'is-sfw', toast: '🛡 SFW' },
|
||||
nsfw: { label: 'NSFW', text: 'nsfw', badgeCls: 'badge-danger', isCls: 'is-nsfw', toast: '🔥 NSFW' },
|
||||
nsfl: { label: 'NSFL', text: 'nsfl', badgeCls: 'badge-nsfl', isCls: 'is-nsfl', toast: '💀 NSFL' },
|
||||
untagged: { label: '?', text: 'untagged', badgeCls: 'badge-untagged', isCls: 'is-untagged', toast: '❓ Untagged' }
|
||||
};
|
||||
const info = mapping[nextRating];
|
||||
const nextLabel = info.label;
|
||||
|
||||
// Backup current state
|
||||
const oldClasses = [...ratingEl.classList];
|
||||
const oldInnerHTML = ratingEl.innerHTML;
|
||||
const oldDatasetRating = ratingEl.dataset.rating;
|
||||
|
||||
// Track active request ID to ignore out-of-order race conditions on rapid keypresses
|
||||
const reqId = (ratingEl._lastCycleReqId || 0) + 1;
|
||||
ratingEl._lastCycleReqId = reqId;
|
||||
|
||||
// Increment active requests count to block incoming live SSE tag updates during cycle
|
||||
window._activeRatingCycleRequestsCount = (window._activeRatingCycleRequestsCount || 0) + 1;
|
||||
ratingEl._activeRequestsCount = (ratingEl._activeRequestsCount || 0) + 1;
|
||||
|
||||
// Optimistically apply new state to rating tag in-place (no full-DOM recreation)
|
||||
ratingEl.className = `badge ${info.badgeCls} rating-tag ${info.isCls} can-cycle`;
|
||||
ratingEl.dataset.rating = nextRating;
|
||||
ratingEl.removeAttribute('title');
|
||||
ratingEl.removeAttribute('tooltip');
|
||||
ratingEl.removeAttribute('flow');
|
||||
|
||||
const labelEl = ratingEl.querySelector('.rating-label') || ratingEl;
|
||||
labelEl.textContent = info.text;
|
||||
|
||||
// Immediately remove any duplicate rating tags from #tags container
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
if (tagsContainer) {
|
||||
const allRatingBadges = tagsContainer.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
allRatingBadges.forEach(b => {
|
||||
if (b !== ratingEl) b.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Also update any legacy a_toggle if present
|
||||
const toggleBtn = document.querySelector('button#a_toggle');
|
||||
if (toggleBtn && toggleBtn !== ratingEl) {
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${nextRating}`);
|
||||
}
|
||||
|
||||
// Optimistically update media-object data-mode for instant visual blur reaction
|
||||
const mediaObj = document.querySelector('.media-object');
|
||||
if (mediaObj) {
|
||||
mediaObj.dataset.mode = nextRating;
|
||||
}
|
||||
|
||||
const flashMsg = (window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', nextLabel)) || ('RATING UPDATED: ' + nextLabel);
|
||||
window.flashMessage(flashMsg);
|
||||
|
||||
// Debounce the network request by 250ms so rapid clicks only dispatch the final state to the server
|
||||
window._ratingDebounceTimers = window._ratingDebounceTimers || {};
|
||||
if (window._ratingDebounceTimers[postid]) {
|
||||
clearTimeout(window._ratingDebounceTimers[postid]);
|
||||
}
|
||||
window._activeRatingCycleRequestsCount = 1;
|
||||
ratingEl._activeRequestsCount = 1;
|
||||
|
||||
window._ratingDebounceTimers[postid] = setTimeout(() => {
|
||||
delete window._ratingDebounceTimers[postid];
|
||||
const targetRating = ratingEl.dataset.rating || nextRating;
|
||||
|
||||
fetch(`/api/v2/item/${postid}/rating`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ rating: targetRating })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (!window._ratingDebounceTimers || !window._ratingDebounceTimers[postid]) {
|
||||
window._activeRatingCycleRequestsCount = 0;
|
||||
ratingEl._activeRequestsCount = 0;
|
||||
}
|
||||
|
||||
if (res && res.success && window.invalidateItemCache) {
|
||||
window.invalidateItemCache(postid);
|
||||
}
|
||||
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||
if (res.success) {
|
||||
if (res.tags && window.renderTags) {
|
||||
window.renderTags(res.tags);
|
||||
}
|
||||
const curMedia = document.querySelector('.media-object');
|
||||
if (curMedia && res.rating) {
|
||||
curMedia.dataset.mode = res.rating;
|
||||
}
|
||||
} else {
|
||||
revert();
|
||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
window._activeRatingCycleRequestsCount = Math.max(0, (window._activeRatingCycleRequestsCount || 1) - 1);
|
||||
ratingEl._activeRequestsCount = Math.max(0, (ratingEl._activeRequestsCount || 1) - 1);
|
||||
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (ratingEl._lastCycleReqId !== reqId) return;
|
||||
console.error('[RATING_TOGGLE_ERROR]', err);
|
||||
revert();
|
||||
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
||||
});
|
||||
}, 200);
|
||||
|
||||
function revert() {
|
||||
ratingEl.className = '';
|
||||
oldClasses.forEach(cls => ratingEl.classList.add(cls));
|
||||
ratingEl.innerHTML = oldInnerHTML;
|
||||
if (oldDatasetRating !== undefined) ratingEl.dataset.rating = oldDatasetRating;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
// Swallow popstate events fired during Onara close (history.go(-N) unwinding)
|
||||
if (window._onaraSuppressPopstate) return;
|
||||
@@ -5415,7 +5422,13 @@ window.cancelAnimFrame = (function () {
|
||||
"ArrowRight": clickOnNavBinding("#next"),
|
||||
"d": clickOnNavBinding("#next"),
|
||||
"r": clickOnElementBinding("#random, #nav-random"),
|
||||
"p": clickOnElementBinding("button#a_toggle"),
|
||||
"p": (e) => {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
const ratingEl = document.querySelector('.rating-tag.can-cycle, button#a_toggle');
|
||||
if (ratingEl && window.cycleRating) {
|
||||
window.cycleRating(ratingEl);
|
||||
}
|
||||
},
|
||||
"f": () => {
|
||||
if (localStorage.getItem('fForFullscreen') === 'true') {
|
||||
const fsBtn = document.querySelector('.v0ck_fs_btn');
|
||||
@@ -9616,8 +9629,9 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
// Ignore incoming live SSE tag updates if an optimistic rating cycle is currently active on the page
|
||||
const toggleBtn = document.querySelector('button#a_toggle');
|
||||
if (toggleBtn && toggleBtn._activeRequestsCount > 0) {
|
||||
const toggleBtn = document.querySelector('button#a_toggle, .rating-tag');
|
||||
const hasPendingDebounce = !!(window._ratingDebounceTimers && window._ratingDebounceTimers[data.item_id]);
|
||||
if (hasPendingDebounce || (window._activeRatingCycleRequestsCount > 0) || (toggleBtn && toggleBtn._activeRequestsCount > 0)) {
|
||||
window.f0ckDebug("[NotificationSystem] Live Tag Update ignored - Optimistic rating cycle in progress.");
|
||||
return;
|
||||
}
|
||||
@@ -9653,21 +9667,60 @@ class NotificationSystem {
|
||||
// Cast to boolean to handle potentially numeric truthy values (1/0)
|
||||
const isAdminBySession = !!(window.f0ckSession?.is_admin || window.f0ckSession?.is_moderator);
|
||||
const hasSession = !!window.f0ckSession;
|
||||
const canManageItem = !!(
|
||||
document.querySelector('#tags[data-can-manage="true"]') ||
|
||||
isAdminBySession ||
|
||||
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
|
||||
);
|
||||
|
||||
window.f0ckDebug(`[NotificationSystem] Rendering ${data.tags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
// Deduplicate rating tags: guarantee at most one rating tag is kept
|
||||
const ratingTags = data.tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
|
||||
const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null;
|
||||
const cleanTags = data.tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag);
|
||||
|
||||
window.f0ckDebug(`[NotificationSystem] Rendering ${cleanTags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
data.tags.forEach(tag => {
|
||||
|
||||
const hasRatingTag = !!lastRatingTag;
|
||||
if (!hasRatingTag) {
|
||||
const untaggedSpan = document.createElement('span');
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManageItem ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.dataset.rating = 'untagged';
|
||||
const lbl = document.createElement('span');
|
||||
lbl.className = 'rating-label';
|
||||
lbl.textContent = 'untagged';
|
||||
untaggedSpan.appendChild(lbl);
|
||||
fragment.appendChild(untaggedSpan);
|
||||
}
|
||||
|
||||
cleanTags.forEach(tag => {
|
||||
const span = document.createElement('span');
|
||||
span.className = `badge ${tag.badge}`;
|
||||
if (hasSession) span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized);
|
||||
if (isRating) {
|
||||
span.classList.add('rating-tag', `is-${tag.normalized}`);
|
||||
span.dataset.rating = tag.normalized;
|
||||
if (canManageItem) {
|
||||
span.classList.add('can-cycle');
|
||||
}
|
||||
} else if (hasSession) {
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
}
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = `/tag/${tag.normalized}`;
|
||||
a.textContent = tag.tag;
|
||||
span.appendChild(a);
|
||||
let contentEl;
|
||||
if (isRating) {
|
||||
contentEl = document.createElement('span');
|
||||
contentEl.className = 'rating-label';
|
||||
contentEl.textContent = tag.tag;
|
||||
} else {
|
||||
contentEl = document.createElement('a');
|
||||
contentEl.href = `/tag/${tag.normalized}`;
|
||||
contentEl.textContent = tag.tag;
|
||||
}
|
||||
span.appendChild(contentEl);
|
||||
|
||||
if (isAdminBySession) {
|
||||
if (isAdminBySession && !isRating) {
|
||||
// Match template exactly: <a class="removetag admin-deltag" href="#"><i class="fa-solid fa-xmark"></i></a>
|
||||
span.insertAdjacentHTML('beforeend', ' <a class="removetag admin-deltag" href="#"><i class="fa-solid fa-xmark"></i></a>');
|
||||
}
|
||||
@@ -9681,6 +9734,14 @@ class NotificationSystem {
|
||||
} else {
|
||||
inner.appendChild(fragment);
|
||||
}
|
||||
|
||||
// Safeguard against duplicate rating tags in DOM
|
||||
const allRatingBadges = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
if (allRatingBadges.length > 1) {
|
||||
for (let i = 1; i < allRatingBadges.length; i++) {
|
||||
allRatingBadges[i].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,22 @@ window.TagAutocomplete = (() => {
|
||||
const MIN_QUERY_LEN = 1;
|
||||
|
||||
function destroy() {
|
||||
if (!activeInstance) return;
|
||||
const { wrapper } = activeInstance;
|
||||
if (wrapper && wrapper.parentElement) {
|
||||
wrapper.parentElement.removeChild(wrapper);
|
||||
if (activeInstance) {
|
||||
if (activeInstance.cleanup) {
|
||||
try { activeInstance.cleanup(); } catch {}
|
||||
}
|
||||
const { wrapper } = activeInstance;
|
||||
if (wrapper && wrapper.parentElement) {
|
||||
wrapper.parentElement.removeChild(wrapper);
|
||||
}
|
||||
activeInstance = null;
|
||||
}
|
||||
activeInstance = null;
|
||||
document.querySelectorAll('.tag-ac-wrapper').forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function isOpen() {
|
||||
return !!(activeInstance && activeInstance.wrapper && activeInstance.wrapper.parentElement) ||
|
||||
!!document.querySelector('.tag-ac-wrapper');
|
||||
}
|
||||
|
||||
function open(opts) {
|
||||
@@ -216,12 +226,23 @@ window.TagAutocomplete = (() => {
|
||||
|
||||
input.addEventListener('input', onInput);
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
const onEscapeKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addEventListener('keydown', onEscapeKey);
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onEscapeKey(e);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = dropdown.querySelectorAll('.tag-suggestion-item');
|
||||
if (!items.length || dropdown.style.display === 'none') return;
|
||||
@@ -310,6 +331,14 @@ window.TagAutocomplete = (() => {
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
document.removeEventListener('mousedown', onDocMousedown);
|
||||
document.removeEventListener('touchstart', onDocTouchstart);
|
||||
if (outsideTapTimer) clearTimeout(outsideTapTimer);
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
};
|
||||
activeInstance.cleanup = cleanup;
|
||||
|
||||
// Delay attaching to avoid capturing the opening touch.
|
||||
setTimeout(() => {
|
||||
document.addEventListener('mousedown', onDocMousedown);
|
||||
@@ -338,5 +367,5 @@ window.TagAutocomplete = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
return { open, destroy };
|
||||
return { open, destroy, isOpen };
|
||||
})();
|
||||
|
||||
+61
-28
@@ -47,6 +47,12 @@
|
||||
if (!tagsContainer) return;
|
||||
const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer;
|
||||
|
||||
const canManage = !!(
|
||||
document.querySelector('#tags[data-can-manage="true"]') ||
|
||||
(window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) ||
|
||||
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
|
||||
);
|
||||
|
||||
// Only remove existing dynamically generated tags
|
||||
[...inner.querySelectorAll(".badge")].forEach(tag => {
|
||||
// Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself
|
||||
@@ -55,24 +61,47 @@
|
||||
}
|
||||
});
|
||||
|
||||
_tags.reverse().forEach(tag => {
|
||||
const a = document.createElement("a");
|
||||
a.href = `/tag/${tag.normalized}`;
|
||||
a.style = "color: inherit !important";
|
||||
a.textContent = tag.tag;
|
||||
// Deduplicate: ensure only at most ONE rating tag exists in the tags list
|
||||
const ratingTags = _tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
|
||||
const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null;
|
||||
const cleanTags = _tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag);
|
||||
|
||||
const tagsCopy = [...cleanTags];
|
||||
tagsCopy.reverse().forEach(tag => {
|
||||
const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized);
|
||||
let contentEl;
|
||||
if (isRating) {
|
||||
contentEl = document.createElement("span");
|
||||
contentEl.className = "rating-label";
|
||||
contentEl.textContent = tag.tag;
|
||||
} else {
|
||||
contentEl = document.createElement("a");
|
||||
contentEl.href = `/tag/${tag.normalized}`;
|
||||
contentEl.style = "color: inherit !important";
|
||||
contentEl.textContent = tag.tag;
|
||||
}
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.classList.add("badge");
|
||||
if (highlightTag && (tag.tag === highlightTag || tag.normalized === highlightTag)) {
|
||||
span.classList.add('new-tag-glow');
|
||||
}
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
|
||||
tag.badge.split(" ").forEach(b => span.classList.add(b));
|
||||
|
||||
span.insertAdjacentElement("beforeend", a);
|
||||
if (isRating) {
|
||||
span.classList.add('rating-tag', `is-${tag.normalized}`);
|
||||
span.dataset.rating = tag.normalized;
|
||||
if (canManage) {
|
||||
span.classList.add('can-cycle');
|
||||
}
|
||||
} else if (tag.display_name || tag.user) {
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
}
|
||||
|
||||
if (window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) {
|
||||
span.insertAdjacentElement("beforeend", contentEl);
|
||||
|
||||
if (window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator) && !isRating) {
|
||||
const space = document.createTextNode('\u00A0'); //
|
||||
span.appendChild(space);
|
||||
|
||||
@@ -83,9 +112,29 @@
|
||||
span.insertAdjacentElement("beforeend", del);
|
||||
}
|
||||
|
||||
inner.insertAdjacentElement("afterbegin", span);
|
||||
inner.insertAdjacentElement("afterbegin", span);
|
||||
});
|
||||
|
||||
const hasRating = !!lastRatingTag;
|
||||
if (!hasRating) {
|
||||
const untaggedSpan = document.createElement("span");
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.dataset.rating = 'untagged';
|
||||
const lbl = document.createElement("span");
|
||||
lbl.className = "rating-label";
|
||||
lbl.textContent = "untagged";
|
||||
untaggedSpan.appendChild(lbl);
|
||||
inner.insertAdjacentElement("afterbegin", untaggedSpan);
|
||||
}
|
||||
|
||||
// Safeguard: remove any extra rating tags in DOM
|
||||
const allRatingEls = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
if (allRatingEls.length > 1) {
|
||||
for (let i = 1; i < allRatingEls.length; i++) {
|
||||
allRatingEls[i].remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle show more/less toggle visibility and count
|
||||
const allBadges = [...inner.querySelectorAll(".badge")];
|
||||
const realTags = allBadges.filter(b => !b.querySelector('#a_addtag') && !b.querySelector('#a_toggle') && !b.classList.contains('tag-ac-wrapper'));
|
||||
@@ -139,25 +188,9 @@
|
||||
|
||||
const toggleEvent = async (e) => {
|
||||
if (e) e.preventDefault();
|
||||
const ctx = getContext();
|
||||
if (!ctx) return;
|
||||
const { postid } = ctx;
|
||||
|
||||
const res = await (await fetch('/api/v2/tags/' + encodeURIComponent(postid) + '/toggle', {
|
||||
method: 'PUT',
|
||||
headers: { "X-CSRF-Token": window.f0ckSession?.csrf_token }
|
||||
})).json();
|
||||
|
||||
renderTags(res.tags);
|
||||
const isNsfw = res.tags.some(t => t.id == 2);
|
||||
const isUntagged = res.tags.length === 0;
|
||||
const toggleBtn = document.querySelector('button#a_toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.classList.toggle('is-nsfw', isNsfw && !isUntagged);
|
||||
toggleBtn.classList.toggle('is-sfw', !isNsfw && !isUntagged);
|
||||
toggleBtn.classList.toggle('is-untagged', isUntagged);
|
||||
const labels = { true: 'NSFW', false: 'SFW' };
|
||||
toggleBtn.textContent = isUntagged ? '?' : (isNsfw ? 'NSFW' : 'SFW');
|
||||
const ratingEl = document.querySelector('.rating-tag.can-cycle, button#a_toggle');
|
||||
if (window.cycleRating) {
|
||||
window.cycleRating(ratingEl);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user