Yet another update/rewrite of Rob W’s script.
Improvements relative to the older version:
- no jQuery dependencies – only modern, vanilla JavaScript APIs, making it lightweight and compatible with sandboxing user-script managers (like current Greasemonkey)
- allows clicking on the separated vote counts to go back to the total score again
- usable even before page loads in full, thanks to
@run-at document-start - uses actual minus signs
Script code:
// ==UserScript==
// @name Stack Exchange: Show/hide vote counts for all
// @grant none
// @run-at document-start
// @match https://*.stackexchange.com/*
// @match https://*.superuser.com/*
// @match https://*.stackoverflow.com/*
// @match https://*.mathoverflow.net/*
// @match https://*.serverfault.com/*
// @match https://*.askubuntu.com/*
// @match https://stackapps.com/*
// @exclude https://api.stackexchange.com/*
// @exclude https://data.stackexchange.com/*
// @exclude https://openid.stackexchange.com/*
// @exclude https://contests.stackoverflow.com/*
// @exclude /^https?:\/\/winterbash\d{4,}\.stackexchange\.com\//
// ==/UserScript==
document.addEventListener('DOMContentLoaded', ev => {
document.head.insertAdjacentHTML('beforeend', `
<style type="text/css">
.js-vote-count {
cursor: pointer;
}
</style>
`);
}, { once: true });
const tsa = typeof cloneInto !== 'undefined'
? x => cloneInto(x, unsafeWindow, { cloneFunctions: true })
: x => x;
const showToast = (text, options) =>
unsafeWindow.StackExchange.helpers.showToast(text, tsa(options));
const fetchVotes = async (postId) => {
const resp = await fetch(
`https://api.stackexchange.com/2.0/posts/${postId}?${new URLSearchParams({
filter: '!)q3b*aB43Xc',
key: 'DwnkTjZvdT0qLs*o8rNDWw((',
site: location.host,
})}`
);
const json = await resp.json();
if (resp.status === 200) {
const signed = (sign, val) => 0 < val && val < 100 ? `${sign}${val}` : `${val}`;
return {
up: signed('+', json.items[0].up_vote_count),
down: signed('\u2212', json.items[0].down_vote_count),
total: json.items[0].up_vote_count - json.items[0].down_vote_count,
};
} else {
throw new Error(json.error_message);
}
};
document.addEventListener('click', async ev => {
const target = ev.target.closest('.js-vote-count');
if (!target)
return;
// do not fetch vote counts again if we are
// already fetching them (or if native scripts are)
if (target.querySelector('[aria-busy="true"]'))
return;
// when the vote counts are already fetched, hide them
if (target.querySelector('.vote-count-separator')) {
target.innerHTML = `${+target.dataset.value}`.replaceAll('-', '\u2212');
return;
}
const postNode = target.closest('.question[data-questionid], .answer[data-answerid]');
const postId = postNode.dataset.questionid ?? postNode.dataset.answerid;
target.insertAdjacentHTML('beforeend', `
<div class="s-spinner fc-black-200 s-spinner__sm" role="alert" aria-busy="true">
<div style="font-size: 0px; color: transparent; float: left;">loading...</div>
</div>
`);
const spinner = target.querySelector(':last-child');
try {
const { up, down, total } = await fetchVotes(postId);
target.dataset.value = total;
target.setAttribute('aria-label', `${up} up / ${down} down`);
target.innerHTML = `
<div class="fc-green-600">${up}</div>
<div class="vote-count-separator"></div>
<div class="fc-red-600">${down}</div>
`;
} catch (e) {
console.error("[SE:shvca] %o", e);
spinner?.remove();
showToast(`${e}`, {
type: 'danger',
transient: true,
});
}
}, false);