/** * Interactions. */ import { fetchJson } from './utils/requests'; import { $ } from './utils/dom'; const endpoints = { vote(imageId) { return `/images/${imageId}/vote`; }, fave(imageId) { return `/images/${imageId}/fave`; }, hide(imageId) { return `/images/${imageId}/hide`; }, }; const spoilerDownvoteMsg = 'Neigh! - Remove spoilered tags from your filters to downvote from thumbnails'; /* Quick helper function to less verbosely iterate a QSA */ function onImage(id, selector, cb) { [].forEach.call(document.querySelectorAll(`${selector}[data-image-id="${id}"]`), cb); } /* Since JS modifications to webpages, except form inputs, are not stored * in the browser navigation history, we store a cache of the changes in a * form to allow interactions to persist on navigation. */ function getCache() { const cacheEl = $('.js-interaction-cache'); return Object.values(JSON.parse(cacheEl.value)); } function modifyCache(callback) { const cacheEl = $('.js-interaction-cache'); cacheEl.value = JSON.stringify(callback(JSON.parse(cacheEl.value))); } function cacheStatus(imageId, interactionType, value) { modifyCache(cache => { cache[`${imageId}${interactionType}`] = { imageId, interactionType, value }; return cache; }); } function uncacheStatus(imageId, interactionType) { modifyCache(cache => { delete cache[`${imageId}${interactionType}`]; return cache; }); } function setScore(imageId, data) { onImage(imageId, '.score', el => { el.textContent = data.score; }); onImage(imageId, '.favorites', el => { el.textContent = data.faves; }); onImage(imageId, '.upvotes', el => { el.textContent = data.upvotes; }); onImage(imageId, '.downvotes', el => { el.textContent = data.downvotes; }); } /* These change the visual appearance of interaction links. * Their classes also effect their behavior due to event delegation. */ function showUpvoted(imageId) { cacheStatus(imageId, 'voted', 'up'); onImage(imageId, '.interaction--upvote', el => el.classList.add('active')); } function showDownvoted(imageId) { cacheStatus(imageId, 'voted', 'down'); onImage(imageId, '.interaction--downvote', el => el.classList.add('active')); } function showFaved(imageId) { cacheStatus(imageId, 'faved', ''); onImage(imageId, '.interaction--fave', el => el.classList.add('active')); } function showHidden(imageId) { cacheStatus(imageId, 'hidden', ''); onImage(imageId, '.interaction--hide', el => el.classList.add('active')); } function resetVoted(imageId) { uncacheStatus(imageId, 'voted'); onImage(imageId, '.interaction--upvote', el => el.classList.remove('active')); onImage(imageId, '.interaction--downvote', el => el.classList.remove('active')); } function resetFaved(imageId) { uncacheStatus(imageId, 'faved'); onImage(imageId, '.interaction--fave', el => el.classList.remove('active')); } function resetHidden(imageId) { uncacheStatus(imageId, 'hidden'); onImage(imageId, '.interaction--hide', el => el.classList.remove('active')); } function interact(type, imageId, method, data = {}) { return fetchJson(method, endpoints[type](imageId), data) .then(res => res.json()) .then(res => setScore(imageId, res)); } function displayInteractionSet(interactions) { interactions.forEach(i => { switch (i.interaction_type) { case 'faved': showFaved(i.image_id); break; case 'hidden': showHidden(i.image_id); break; default: if (i.value === 'up') showUpvoted(i.image_id); if (i.value === 'down') showDownvoted(i.image_id); } }); } function loadInteractions() { /* Set up the actual interactions */ displayInteractionSet(window.booru.interactions); displayInteractionSet(getCache()); /* Next part is only for image index pages * TODO: find a better way to do this */ if (!document.getElementById('imagelist-container')) return; /* Users will blind downvote without this */ window.booru.imagesWithDownvotingDisabled.forEach(i => { onImage(i, '.interaction--downvote', a => { // TODO Use a 'js-' class to target these instead const icon = a.querySelector('i') || a.querySelector('.oc-icon-small'); icon.setAttribute('title', spoilerDownvoteMsg); a.classList.add('disabled'); a.addEventListener( 'click', event => { event.stopPropagation(); event.preventDefault(); }, true, ); }); }); } const targets = { /* Active-state targets first */ '.interaction--upvote.active'(imageId) { interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId)); }, '.interaction--downvote.active'(imageId) { interact('vote', imageId, 'DELETE').then(() => resetVoted(imageId)); }, '.interaction--fave.active'(imageId) { interact('fave', imageId, 'DELETE').then(() => resetFaved(imageId)); }, '.interaction--hide.active'(imageId) { interact('hide', imageId, 'DELETE').then(() => resetHidden(imageId)); }, /* Inactive targets */ '.interaction--upvote:not(.active)'(imageId) { interact('vote', imageId, 'POST', { up: true }).then(() => { resetVoted(imageId); showUpvoted(imageId); }); }, '.interaction--downvote:not(.active)'(imageId) { interact('vote', imageId, 'POST', { up: false }).then(() => { resetVoted(imageId); showDownvoted(imageId); }); }, '.interaction--fave:not(.active)'(imageId) { interact('fave', imageId, 'POST').then(() => { resetVoted(imageId); showFaved(imageId); showUpvoted(imageId); }); }, '.interaction--hide:not(.active)'(imageId) { interact('hide', imageId, 'POST').then(() => { showHidden(imageId); }); }, }; function bindInteractions() { document.addEventListener('click', event => { if (event.button === 0) { // Is it a left-click? for (const target in targets) { /* Event delegation doesn't quite grab what we want here. */ const link = event.target && event.target.closest(target); if (link) { event.preventDefault(); targets[target](link.dataset.imageId); } } } }); } function loggedOutInteractions() { [].forEach.call(document.querySelectorAll('.interaction--fave,.interaction--upvote,.interaction--downvote'), a => a.setAttribute('href', '/sessions/new'), ); } function setupInteractions() { if (window.booru.userIsSignedIn) { bindInteractions(); loadInteractions(); } else { loggedOutInteractions(); } } export { setupInteractions, displayInteractionSet };