// ==UserScript== // @include http://*.4chan.org/*html // @include http://rs.4chan.org/* // @name 4chan UserJS for Opera // @author Anonymous // @version 02/06/09 // @description Various enhancements for browsing 4chan. // @ujs:documentation http://sector-5.net/archives/opera-userjs-4chan-enhancements/ // @ujs:download http://files.sector-5.net/4chan.js // ==/UserScript== // // History // 02/06/2009 - Added additional button to hide threads // - Minor revamps that render the buttons incompatible with fychan // 01/28/2009 - Removed forced Anonymous, // as this comes with 4chan-filter.js now. // - 'Fixed' quoting. Newline after >>POSTID now actually // works. CTRL+clicking a link will focus the comment box. // - Rewrote delete and report button insertion for DOM // compliance. // 09/28/2008 - Fixed double execution again, // also removed parts that weren't necessary anymore // like the some of the ads and cb-ws links // 06/02/2008 - Removed scroll up to thread button, since it // didn't work anymore and was useless to begin with // 05/13/2008 - Fixed double execution for Opera 9.5 // 03/21/2008 - Added new code for qksz.net ads // Started changelog (function () { const delimg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAJUExURQAAAP///////3N4pWMAAAADdFJOU///ANfKDUEAAAA+SURBVHjabI9RDgAgCEKJ+x86VyOM4u8BOgUJq4AYVlHHMjpTvD04P6b6UEfzuPdFHvPP/v89i/P++G8KMABUJACyetFd3gAAAABJRU5ErkJggg==' const repimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAJUExURQAAAP///////3N4pWMAAAADdFJOU///ANfKDUEAAAAqSURBVHjaYmBiYkAAIIeJgREBgDxkLlAAF5+BynxGEuXhfHT3o/kPIMAAVpgAuEYCAZgAAAAASUVORK5CYII="; //const upimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAJUExURQAAAP///////3N4pWMAAAADdFJOU///ANfKDUEAAAA7SURBVHjaYmBiYkAAIIeJgREBgDxkLlCAWD4DKp8BKgDlM8BUQPgMcC1gPsxIBhLsg/PR3Y/mP4AAAwBVcAC1T6T7qgAAAABJRU5ErkJggg=="; //const topimg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAJUExURQAAAP///////3N4pWMAAAADdFJOU///ANfKDUEAAAA/SURBVHjafM9BCgAgCETRr/c/dEJOplDi5g2zUNypCThWE7oZgUw3GaRRYxvtNtnn07feV8DrnuN5//hvCTAAUMgArNLCz7kAAAAASUVORK5CYII="; const hideimg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAMUExURQAAAP///////wAAADh+HboAAAADdFJOU///ANfKDUEAAAA7SURBVHjaYmBiYkYAJiYGJmZGBGBmYkDmAgWgfGYoAeXDCQgfSQzER3Dh+hmZqWwe3H3o7kfzH0CAAQCs+AFw13yv3wAAAABJRU5ErkJggg=='; const showimg = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAJUExURQAAAP///////3N4pWMAAAADdFJOU///ANfKDUEAAAA0SURBVHjaYmBiYkAAIIeJgREBgDxkLlAAxIcrh/OhcjA+TDG1+Ojmo9mP4j5096P5DyDAAEioAJoO1LJVAAAAAElFTkSuQmCC'; document.addEventListener('DOMContentLoaded', function (e) { if (!document.body) return; // Kill any iframes, just in case. iframeTags = document.getElementsByTagName("iframe"); for (i = iframeTags.length - 1; i >= 0; --i) iframeTags[i].parentNode.removeChild(iframeTags[i]); // Remove embed tags embedTags = document.getElementsByTagName("embed"); for (i = embedTags.length - 1; i >= 0; --i) embedTags[i].parentNode.removeChild(embedTags[i]); // Fix quote links var quotes = document.getElementsByClassName('quotejs'); for (var i = 0; i < quotes.length; ++i) { if (quotes[i].href.indexOf('javascript:') != 0) continue; quotes[i].removeAttribute('href'); quotes[i].onclick = function(event) { // Get quoted post const quoteId = this.textContent; var text = '>>' + quoteId; // If something was selected, add it after the quoted id var selection = window.getSelection().toString(); if (selection.length > 0) text += ' ' + selection; document.post.com.value += text + '\n'; // If CTRL was pressed, do not focus the input box if (event.ctrlKey == false) document.post.com.focus(); }; } // Add sage button for quick sage'ing // First, we find the correct input element inputEmail = document.evaluate("//input[@name='email']", document.forms[0], null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); if (inputEmail.snapshotLength > 0) { var inputTag = inputEmail.snapshotItem(0); inputTag.setAttribute('id', 'email'); inputTag.addEventListener('keyup', EmailBox_OnKeyUp, false); var fragment = document.createDocumentFragment(); // Create Noko box and label for it var nokoBox = document.createElement('input'); nokoBox.setAttribute('type', 'checkbox'); nokoBox.setAttribute('id', 'noko'); nokoBox.onchange = Noko_OnChange; if (inputTag.value.search(/^noko$/) != -1) nokoBox.checked = true; var nokoBoxLabel = document.createElement('label'); nokoBoxLabel.setAttribute('for', 'noko'); nokoBoxLabel.appendChild(document.createTextNode(' Noko ')); // Create Sage box and label for it var sageBox = document.createElement('input'); sageBox.setAttribute('type', 'checkbox'); sageBox.setAttribute('id', 'sage'); sageBox.onchange = Sage_OnChange; if (inputTag.value.search(/^sage$/) != -1) sageBox.checked = true; var sageBoxLabel = document.createElement('label'); sageBoxLabel.setAttribute('for', 'sage'); sageBoxLabel.appendChild(document.createTextNode(' Sage ')); // Insert elements into document fragment fragment.appendChild(document.createTextNode(' [ ')); fragment.appendChild(nokoBox); fragment.appendChild(nokoBoxLabel); fragment.appendChild(document.createTextNode(' ] ')); fragment.appendChild(document.createTextNode(' [ ')); fragment.appendChild(sageBox); fragment.appendChild(sageBoxLabel); fragment.appendChild(document.createTextNode(' ] ')); inputTag.parentNode.appendChild(fragment); } // Add quick del / report / scroll functionality if (!window.location.pathname.match(/^\/([\w]+)\//)) return; // Collect visible threads / posts var spanTags = document.evaluate( "//span[starts-with(@id, 'nothread')] |" + // Thread start "//span[starts-with(@id, 'norep')]", // Post in thread document.body, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); const createButton = function(img, title, postId, marginLeft) { var button = document.createElement('img'); button.setAttribute('src', img); button.setAttribute('alt', title); button.setAttribute('title', title); button.setAttribute('id', 'post-' + postId); button.style.verticalAlign = 'middle'; button.style.marginLeft = marginLeft + 'px'; return button; } for (var i = 0; i < spanTags.snapshotLength; ++i) { const postId = spanTags.snapshotItem(i).id.match( /no(thread|rep)(\d+)/)[2]; // Create buttons var deleteButton = createButton(delimg, 'Delete this post', postId, 15); deleteButton.onclick = function(e) { const inputElems = document.getElementsByTagName('input'); for (var i = 0; i < inputElems.length; ++i) { var input = inputElems[i]; if (input.type == 'checkbox' && input.name == this.id.substring(5)) { input.checked = true; return document.delform.submit(); } } } var reportButton = createButton(repimg, 'Report this post', postId, 2); reportButton.onclick = function(e) { return reppop(document.forms[0].action + '?mode=report&no=' + this.id.substring(5)); } // Only create the hide button on top-level posts (i.e. threads) if (spanTags.snapshotItem(i).id.indexOf('nothread') == 0) { var hideButton = createButton(hideimg, 'Hide this thread', 'hide-' + postId, 10); hideButton.onclick = function(e) { var postId = this.id.substring(10); hideThread(postId); } } else var hideButton = null; var fragment = document.createDocumentFragment(); // Append buttons fragment.appendChild(deleteButton); fragment.appendChild(reportButton); if (hideButton) fragment.appendChild(hideButton); spanTags.snapshotItem(i).appendChild(fragment); } // Hide threads. This must be done after the hide button has been created since // hideThread() changes the button. var threads = getHiddenThreads(); for (var i = 0; i < threads.length; ++i) hideThread(threads[i]); }, false ); /// Fix the quote function for proper newline support window.opera.defineMagicFunction('quote', function (realFunc, originalThis, text) { document.post.com.focus(); document.post.com.value += '>>' + text + '\n'; }); /// Called each time the email box changes. function EmailBox_OnKeyUp(event) { var sage = document.getElementById('sage'); var noko = document.getElementById('noko'); if (this.value.search(/^sage$/) != -1) { sage.checked = true; noko.checked = false; } else if (this.value.search(/^noko$/) != -1) { noko.checked = true; sage.checked = false; } else { sage.checked = false; noko.checked = false; } } /// Called each time the sage checkbox is checked/unchecked. function Sage_OnChange(event) { var email = document.getElementById('email') var noko = document.getElementById('noko'); if (this.checked) { noko.checked = false; email.value = 'sage' } if (this.checked == false && noko.checked == false) email.value = ''; } function Noko_OnChange(event) { var email = document.getElementById('email') var sage = document.getElementById('sage'); if (this.checked) { sage.checked = false; email.value = 'noko' } if (this.checked == false && sage.checked == false) email.value = ''; } /// Stores a setting in a local cookie. /// Originally from `Emulate Greasemonkey functions' by TarquinWJ, /// http://userjs.org/scripts/browser/enhancements/aa-gm-functions function setValue(cookieName, cookieValue, lifeTime) { if( !cookieName ) { return; } var cookie = escape( cookieName ) + "=" + escape( cookieValue ); // Cookie should be deleted if (lifeTime == "delete" ) { lifeTime = -10; cookie += ";expires=" + ( new Date( ( new Date() ).getTime() + ( 1000 * lifeTime ) ) ).toGMTString(); } // No argument given else if (lifeTime == null) { lifeTime = 31536000; cookie += ";expires=" + ( new Date( ( new Date() ).getTime() + ( 1000 * lifeTime ) ) ).toGMTString(); } else if (lifeTime > 0) { cookie += ";expires=" + ( new Date( ( new Date() ).getTime() + ( 1000 * lifeTime ) ) ).toGMTString(); } // Other case: Temporary cookie document.cookie = cookie + ";path=/"; } /// Loads a setting from local cookies. /// Originally from `Emulate Greasemonkey functions' by TarquinWJ, /// http://userjs.org/scripts/browser/enhancements/aa-gm-functions function getValue(cookieName, oDefault) { var cookieJar = document.cookie.split( "; " ); for( var x = 0; x < cookieJar.length; x++ ) { var oneCookie = cookieJar[x].split( "=" ); if( oneCookie[0] == escape( cookieName ) ) { var footm = unescape( oneCookie[1] ); if (footm == 'true') return true; else if (footm == 'false') return false; else return footm; } } return oDefault; } // List of all hidden threads function getHiddenThreads() { var str = getValue('4chan-hide', ''); if (str.length == 0) return []; else return str.split(';'); } // Stores the list function setHiddenThreads(threads) { var threads = threads.join(';') // Make sure not to over-shoot the size limit for cookies. // If the list is longer than 250 entries, shave off the oldest // 100 entries (these are stored at the beginning of the list) if (threads.length > 250) threads = threads.slice(100); setValue('4chan-hide', threads, 0) } /// Hides all thread content with a given thread id function hideThread(threadId) { var button = document.getElementById('post-hide-' + threadId); if (button != null) { // Hide elements before the current parent, up until