There are 2 previous versions of this script.
Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)
// ==UserScript==
// @name Facepunch: Alternate Quick Reply Box
// @description Adds a floating reply box to Facepunch forums
// @author Vampired (http://forums.facepunchstudios.com/Vampired)
// @include http://forums.facepunchstudios.com/showthread.php*
// @version 1.0.2
// ==/UserScript==
/*
Features
# Hotkey to open
# Follows window while you scroll.
# Emote and tags list
# Press tab to insert a tab (4 spaces?)
# Edit and post replies without leaving the page
# Quote multiple posts at once without leaving the page
# Confirmation to clear textbox before exitting page
# Button to pop up a preview window
# Does nothing if the thread is locked.
# Reply buttons still work if you want to open in a new tab or window.
# The window fucking slides from the bottom of the screen!
*/
// Helper functions
// These are a few functions to make life easier
function $(el)
{
if(typeof el == "string")
return document.getElementById(el);
return el;
}
function $x(xpath)
{
var item, arr = [], xpr = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for(var i = 0; item = xpr.snapshotItem(i); i++)
arr.push(item);
return arr;
}
function $X(xpath)
{
var arr = $x(xpath);
return (arr.length == 0 ? null : (arr.length == 1 ? arr[0] : arr));
}
function $A(iterable)
{
if (!iterable)
return [];
if (iterable.toArray)
return iterable.toArray();
var length = iterable.length || 0, results = new Array(length);
while (length--)
results[length] = iterable[length];
return results;
}
Function.prototype.bind = function()
{
var __method = this, args = $A(arguments), object = args.shift();
return function()
{
return __method.apply(object, args.concat($A(arguments)));
}
}
// Easy adding events and DOM elements
var Event = {
add: function(t, ev, fn)
{
$(t).addEventListener(ev, fn, false)
},
remove: function(t, ev, fn)
{
$(t).removeEventListener(ev, fn, false)
}
};
var Dom = {
add: function(dest, type)
{
var el = document.createElement(type);
var dest = $(dest);
dest.appendChild(el);
return el;
},
addBefore: function(dest, type)
{
var dest = $(dest);
var parent = dest.parentNode;
var newel = document.createElement(type);
parent.insertBefore(newel, dest)
return newel;
},
addAfter: function(dest, type)
{
var dest = $(dest);
var parent = dest.parentNode;
var newel = document.createElement( type );
parent.insertBefore(newel, dest.nextSibling)
return newel;
},
addText: function(dest, text)
{
var node = document.createTextNode(text);
$(dest).appendChild(node);
return node;
},
remove: function(el)
{
var el = $(el);
el.parentNode.removeChild(el);
},
removeAll: function(el)
{
el = $(el);
if(el.hasChildNodes())
{
while(el.childNodes.length >= 1)
{
Dom.remove(el.firstChild);
}
}
}
}
function getChildrenByTagName(el, tagname)
{
var children = [];
for(var i = 0; i < el.childNodes.length; i++)
{
if(el.childNodes[i].nodeName.toLowerCase() == tagname.toLowerCase())
{
children.push(el.childNodes[i]);
}
}
return children;
}
function log(str)
{
if(unsafeWindow.console)
unsafeWindow.console.log(str);
}
function trim(str)
{
return str.replace(/^[\s]+/g, "").replace(/[\s]+$/g, "");
}
function left(hay, needle)
{
return hay.substr(0, needle.length) == needle;
}
function quad_in(p, start, end)
{
return (end-start)*p*p + start;
}
function quad_out(p, start, end)
{
return -(end-start)*p*(p-2) + start
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Convert images to base64 strings
// Thanks, http://www.famfamfam.com/lab/icons/silk/
// And, http://www.greywyvern.com/code/php/binary2base64
var bg = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAIklEQVQ4jWNkYGB4xkBFwERNw0YNHDVw1MBRA0cNHEoGAgAxHgEO2fqKUQAAAABJRU5ErkJggg%3D%3D"
var closebtn = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIhSURBVDjLlZPrThNRFIWJicmJz6BWiYbIkYDEG0JbBiitDQgm0PuFXqSAtKXtpE2hNuoPTXwSnwtExd6w0pl2OtPlrphKLSXhx07OZM769qy19wwAGLhM1ddC184+d18QMzoq3lfsD3LZ7Y3XbE5DL6Atzuyilc5Ciyd7IHVfgNcDYTQ2tvDr5crn6uLSvX+Av2Lk36FFpSVENDe3OxDZu8apO5rROJDLo30+Nlvj5RnTlVNAKs1aCVFr7b4BPn6Cls21AWgEQlz2+Dl1h7IdA+i97A/geP65WhbmrnZZ0GIJpr6OqZqYAd5/gJpKox4Mg7pD2YoC2b0/54rJQuJZdm6Izcgma4TW1WZ0h+y8BfbyJMwBmSxkjw+VObNanp5h/adwGhaTXF4NWbLj9gEONyCmUZmd10pGgf1/vwcgOT3tUQE0DdicwIod2EmSbwsKE1P8QoDkcHPJ5YESjgBJkYQpIEZ2KEB51Y6y3ojvY+P8XEDN7uKS0w0ltA7QGCWHCxSWWpwyaCeLy0BkA7UXyyg8fIzDoWHeBaDN4tQdSvAVdU1Aok+nsNTipIEVnkywo/FHatVkBoIhnFisOBoZxcGtQd4B0GYJNZsDSiAEadUBCkstPtN3Avs2Msa+Dt9XfxoFSNYF/Bh9gP0bOqHLAm2WUF1YQskwrVFYPWkf3h1iXwbvqGfFPSGW9Eah8HSS9fuZDnS32f71m8KFY7xs/QZyu6TH2+2+FAAAAABJRU5ErkJggg%3D%3D";
var loadingbtn = "data:image/gif;base64,R0lGODlhEAAQAPIAAAAAAP///zw8PLy8vP///5ycnHx8fGxsbCH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA%3D%3D";
var codetag = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAEXSURBVDjLY/j//z8DJZhhmBpg2POQn2wDDDof8HvOe3osYtXzDzCxuM2vP3gvfn4MJIfXAP22e0Ies58eK9r2+r//3Kf3YOIhq17eK9v95j9ITrv2jhBWA/Ra7kVEr375vXDrq/9+s57eUy+4IY0kJx2w6Nk9kFzE0uffgXIRKAboNtxlC1/+/GPljjdABc9+q+ZcM0Z3qmb5LWOQXOmml/8DZz7+qJB0hQ3FBerFNyNC5z/9nrXqxX+Pvgf35OMuSSPJSXtPfXQPJBc089F3oFwE1jBQTLkiZNtw51jq4qf/XVvuwsPAa9Kjexkrnv8HyclFXxTCGwsyERf4LctvHvPuvAePBf8pDz/Y1N45BpIbKUmZFAwAR3nW32nUrY0AAAAASUVORK5CYII%3D";
var emote = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJcSURBVDjLpZPNS9RRFIaf+5tx/JqxUZGKQtEkyJBKw1ZhG0mSFkW1aVW7Wgpusv4CoX20KIgIKowgZSgwrIVghFQbM2swQRM/xmx05jf33nNaBJNWtKizPJzzcDjv+xpV5X8q+mvDPTdxVXpV6BahQxVUGBchpcL1xEnNbp43my9wz02nCrdMfV+jSXagsRpQj+aW8JkJwg8DaREuVJ/S0d8AbsSc0mTnYLDjPFqxDb8xiYTLqMuDKSMoa8bYgPDzA8KF0dN15/RREWBHTI0qM5HWe3EJ1vD5NNhviAtRn/8BEcWU1hMpbeXr2MWsCA07z+tKAKBCb9DQH9cYSJgGt87I4yWQAvgCL56B2Cxu7R0+/EjFvv64eHoBAgARekz1ESSfRl0OFcuxE+WoWNRbjnZmUQlRm8Wtviaa3It4eooqiNBCaRWSW2X6bTWYBJhf5FFF3Qq7a54CId7TWASokFOXj+EdNQ3tVNTuwQSRrfviWV94g64NgSoiRH5e4EljcwchRmU8R2J7M0G0ZAvA2xC/PIwJKhAfxYZMb/7BkF2awMTqCcJR7MYcsNmhil37CMv3iVYdILc0gy0wVJQxM2jqRPgUP3InLvY9bn2GSLKHkmQbqCdcfElh9iaR8l2YquPMPLmUdZamQ1d0sWikxfvmTLSu60FZ/WmIfMNlXuFzc6jLY6IJIomDeJrITA6TmUqdbevXh79Zef6u6RLPjcqWvsZY7X6CkgDE4ULP+vwUX8YG0s5yuf2qpv6YBYDZ2yYunj4Rur2nwxXAWcadJeUsA4ev/SVM/1LfAVOtXW4xpbWpAAAAAElFTkSuQmCC";
var pencil = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAFUSURBVDjLrZM/SAJxGIZdWwuDlnCplkAEm1zkaIiGFFpyMIwGK5KGoK2lphDKkMDg3LLUSIJsSKhIi+684CokOtTiMizCGuzEU5K3vOEgKvtBDe/2Pc8H3x8NAM1fQlx4H9M3pcOWp6TXWmM8A7j0629v1nraiAVC0IrrwATKIgs5xyG5QiE+Z4iQdoeU2oAsnqCSO1NSTu+D9VhqRLD8nIB8F0Q2MgmJDyipCzjvYJkIfpN2UBLG8MpP4dxvQ3ZzGuyyBQ2H+AnOOCBd9aL6soh81A5hyYSGWyCFvxUcerqI4S+CvYVOFPMHxLAq8I3qdHVY5LbBhJzEsCrwutpRFBlUHy6wO2tEYtWAzLELPN2P03kjfj3luqDycV2F8AgefWbEnVqEHa2IznSD6BdsVDNStB0lfh0FPoQjdx8RrAqGzC0YprSgxzsUMOY2bf37N/6Ud1Vc9yYcH50CAAAAAElFTkSuQmCC";
var comment_add = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGkSURBVDjLrZJPKINhHMd3UsrJTcpBDi6UC+3ookQ5OOBAaCh2cFQ40GqTzURTihI54DRp8dqBg3BQw5BtNmxe/4flT9re5+v3vJvF2l6Kp749Pe/7fj7P7/09jwqA6i9R/ZugVr+cSZmlvFOgEIGSl0xgnVt3IRyRoDSWtn1c4qakxQW0yKBEJMbw+MpwHWIQnxgCDwxnQQbvHYP7RoLnJirvntrkkuKvghytZU1+eUWg+MjgJ/j0nuEkBh9dSTgQo4KB+R0uqEgquCD4PBiDbxlc11HYSfBuILUg/gu8fB/t6rmVcEzw4aWEfYIdAS6IyILe6S0uUCdtIpd8Hbwah1+SxQlNTE91jJHPI5tcPoiLrBsL6BxrQOtQFep0pc/lXYU9P14kkngugy/onxlF30ITlpwWOEQB5tV21JgLUNKRZVSCTeM2J6/kuV5fFrbuD8N6OCJXY7S3wGxv44K3VHAuxUvR8HVldxFszolvvVncs3DB7+67Wpv9Nig0Qy80yrB+pVG5gsTQh7pqYz5Mgkbemc98rdiDJBIDJcTLjs0G/vwDCw/6dFwBuzsAAAAASUVORK5CYII%3D";
var facepunch = "http://forums.facepunchstudios.com/";
var MODE_NONE = 0;
var MODE_POST = 1;
var MODE_EDIT = 2;
// Code begins.
var QuickReply = {
// Vars ////////////////////
versionString: "1.0.2, 2009-01-02",
awaitingAJAX: false,
posting: false,
scrolledEnough: false,
open: false, // window open?
threadid: -1,
postid: -1, // if editting post
postmode: MODE_NONE,
generatedEmotes: false,
generatedTags: false,
window: null, // main window
textbox: null, // textarea
loading: null, // loading image, to show and hide
posticon: null, // posting or editting icon.
form: null, // form element
previewWin: null,
// ajax
ajax_lastpost: null, // time in seconds of last post (from unsafeWindow)
ajax_allow: true, // if we're not on the last page (from unsafeWindow)
ajax_sucks: false, // if errors, resubmit form with this set to true
// animation
anim_starttime: 0, // start time
anim_startval: 0, // starting value
anim_current: 0, // current value
anim_endval: 0, // end value
anim_length: 2000, // time in ms to slide
anim_func: null, // function to use (percent, startval, endval)
anim_timeout: null, // timeout object(?)
options: {
popuponscroll: false,
keycommand: 114, // r
tabtotab: true,
},
// Functions ///////////////
init: function()
{
// qr_threadid is a hidden element of the quick reply form, its value stores
// the thread id. Lets nab it.
this.threadid = $("qr_threadid").value;
// Grab the lastpost time from document in seconds
this.ajax_lastpost = unsafeWindow.ajax_last_post;
this.ajax_allow = unsafeWindow.allow_ajax_qr;
// Add new reply button after all existing reply buttons
this.setupReplyButtons();
// Setup page
this.setupPage();
this.createReplyWindow();
// Confirmation box to exit page.
Event.add(window, "beforeunload", this.event_beforeUnload.bind(this));
Event.add(window, "keypress", this.event_keypress.bind(this));
Event.add(window, "mousemove", this.event_mousemove.bind(this));
if(this.options.popuponscroll)
Event.add(window, "scroll", this.event_scroll.bind(this));
},
setupReplyButtons: function()
{
// XPath yum yum :) divs where class = postbit####
var posts = $x("//div[starts-with(@class,'postbit')]");
for(var i=0; i < posts.length; i++)
{
// When new posts are added from AJAX, this function is run again.
// We don't want to mess with posts we've already gone through.
// Just assign a temp var to the object and ignore if it's already set.
if(posts[i].v_alternateQuickReply)
continue;
this.insertNewReplyButton(posts[i]);
posts[i].v_alternateQuickReply = true;
}
},
setupPage: function()
{
var div = Dom.addBefore("qrform", "div");
div.style.borderLeft = div.style.borderRight = "1px solid #888888";
div.style.textAlign = "center";
div.className = "panelsurround";
var input = Dom.add(div, "input");
input.type = "button";
input.value = "Quick Reply (Ctrl-"+String.fromCharCode(this.options.keycommand)+")";
input.setAttribute("style", "margin: 20px auto; height: 50px; background-color: #88CCFF; width: 90%");
Event.add(input, "click", this.event_replyclick.bind(this));
document.body.style.marginBottom = "100px";
// hide default form.
$("qrform").style.display = "none";
},
insertNewReplyButton: function(post)
{
// Post id
var id = post.id.substr(4); // Should be a string of numbers
// Get div children of this element.
// There should be 4 with classnames
// header, userinfo, message, footer respectively.
// todo: do this with xpath?
var children = getChildrenByTagName(post, "div");
// Now we loop through all the divs of footer.
var buttons = getChildrenByTagName(children[3], "div");
for(var i=0; i<buttons.length; i++)
{
var btn = buttons[i];
// ignore divs which aren't cbutton class
if(btn.className != "cbutton") continue;
if(btn.firstChild.href.indexOf("editpost.php") > -1) // edit button :D
{
// Remove 'onclick= blah blah'
// but keep the a href link so we can permalink to it or whatever
btn.setAttribute("onclick", "");
Event.add(btn, "click", function(ev)
{
QuickReply.editPost(id);
ev.stopPropagation();
ev.preventDefault();
});
}
else if(btn.firstChild.href.indexOf("newreply.php") > -1) // reply
{
btn.setAttribute("onclick", "");
// When we click reply button...
Event.add(btn, "click", function(ev)
{
QuickReply.quotePost(id);
ev.stopPropagation();
ev.preventDefault();
});
}
}
},
generateEmoteBar: function()
{
this.generatedEmotes = true;
var emotebar = $("qr2_emotebar");
var container = Dom.add(emotebar, "div");
for(var i=0; i<emotes.length; i++)
{
var a = Dom.add(container, "a");
if(emotes[i][2])
{
a.style.backgroundColor = "#ffffff";
a.style.padding = "1px";
}
a.href = "javascript:qr2_insertText('"+emotes[i][0]+"');";
var img = Dom.add(a, "img");
img.src = "/images/smilies/"+emotes[i][1];
img.title = emotes[i][0];
}
},
generateTagBar: function()
{
this.generatedTags = true;
var tagbar = $("qr2_tagbar");
var container = Dom.add(tagbar, "div");
for(var i=0; i<bbcode.length; i++)
{
var tag = bbcode[i].replace(/&/, "");
var tagdisplay = bbcode[i].replace(/&(.)/, "<u>$1</u>");
var a = Dom.add(container, "a");
a.innerHTML = tagdisplay;
a.href = "javascript:qr2_insertTag('"+tag+"');";
}
},
createReplyWindow: function()
{
this.window = Dom.add(document.body, "div");
this.window.id = "qr2_main";
GM_addStyle([
"#qr2_main{ padding:10px; background:transparent url('"+bg+"') repeat scroll 0px 0px; position:fixed; bottom:-300px; left:0px; width:100%; z-index:50; border-top:2px solid #666666; }",
"#qr2_main .container{ width:92%; margin:5px auto; }",
"#qr2_main td{ font-family: Tahoma; font-size: 11px; color: #666666; }",
"#qr2_textbox{ width:100%; margin:-4px; }",
"#qr2_submit{ float:left; }",
"#qr2_orbtn { margin-left: 5px; float:left; color:#AAAAAA }",
"#qr2_orbtn a{ color:#CCCCCC; text-decoration:underline }",
"#qr2_loading{ visibility:hidden; margin-top:5px; }",
"#qr2_posticon{ margin-top:5px; }",
"#qr2_tabs{ float:right }",
"#qr2_tabs a{ padding: 4px; margin: 0px 4px }",
"#qr2_emotebar{ padding:5px; display:none; margin-bottom: 2px; padding:1px; overflow:hidden; }",
"#qr2_emotebar > div { text-align:center; width:4000px; position:relative }",
"#qr2_emotebar a{ margin:4px; }",
"#qr2_tagbar{ text-align:center; display:none; margin-bottom: 5px; }",
"#qr2_tagbar a{ padding: 5px; margin: 1px; color:#AAAAAA; }",
"#qr2_tagbar a:hover{ background-color: #CCCCCC; color:#000000; }",
"#qr2_tagpanel ul{ margin:0px; padding:0px; }",
"#qr2_tagpanel li{ list-style: none; float: left; width: 70px; margin:1px; padding:1px; }",
"#qr2_tagpanel li a{ width:100%; display:block; color:#666666; }",
].join(""));
this.window.innerHTML = [
"<div class=\"container\">",
"<div id=\"qr2_emotebar\"></div>",
"<div id=\"qr2_tagbar\"></div>",
"<form action=\"\" method=\"post\" id=\"qr2_form\">",
"<input type=\"hidden\" id=\"qr2_do\" name=\"do\" value=\"?\">",
"<input type=\"hidden\" id=\"qr2_t\" name=\"t\" value=\"?\">",
"<input type=\"hidden\" id=\"qr2_p\" name=\"p\" value=\"?\">",
"<input type=\"hidden\" name=\"fromquickreply\" value=\"1\">",
"<table width=\"100%\" cellspacing=\"0\" cellpadding=\"5\" border=\"0\">",
"<tr>",
"<td width=\"16\" rowspan=\"2\" valign=\"top\">",
"<a href=\"javascript:;\" onclick=\"closeReplyWindow()\"><img src=\""+closebtn+"\"></a><br>",
"<img src=\"\" id=\"qr2_posticon\">",
"<img src=\""+loadingbtn+"\" id=\"qr2_loading\">",
"</td>",
"<td width=\"70%\" rowspan=\"2\">",
"<textarea id=\"qr2_textbox\" name=\"message\" rows=\"8\"></textarea>",
"</td>",
"<td valign=\"top\">",
"<div id=\"qr2_defaultpanel\">",
"<a href=\"http://forums.facepunchstudios.com/member.php?u=5035\">Vampired's</a> alternate quick reply<br><br>",
"Version "+this.versionString+"",
"</div>",
"</td>",
"</tr>",
"<tr>",
"<td valign=\"top\" height=\"34\">",
"<input type=\"submit\" value=\"Post Reply\" id=\"qr2_submit\">",
"<span id=\"qr2_orbtn\">or <a href=\"javascript:;\" id=\"qr2_preview\">preview</a></span>",
"<div id=\"qr2_tabs\">",
"<a href=\"javascript:;\" onclick=\"qr2_openBar(0)\"><img src=\""+emote+"\"></a>",
"<a href=\"javascript:;\" onclick=\"qr2_openBar(1)\"><img src=\""+codetag+"\"></a>",
"</div>",
"</td>",
"</tr>",
"</table>",
"</form>",
"</div>"].join("");
this.textbox = $("qr2_textbox");
this.loading = $("qr2_loading");
this.posticon = $("qr2_posticon");
this.form = $("qr2_form");
//this.generateEmoteTab();
//this.generateTagTab();
// Move it off screen, ready for animations
this.anim_current = -this.window.clientHeight-5;
this.window.style.bottom = this.anim_current+"px";
Event.add(this.form, "submit", this.event_submit.bind(this));
Event.add(this.textbox, "keypress", this.event_textboxkeypress.bind(this));
Event.add("qr2_preview", "click", this.event_previewClick.bind(this));
},
insertText: function(text)
{
// When value is modified the textbox will scroll to the top.
// Store the current scroll values and apply them afterwards.
var sLeft = this.textbox.scrollLeft;
var sTop = this.textbox.scrollTop;
var left = this.textbox.value.substr(0, this.textbox.selectionStart);
var right = this.textbox.value.substr(this.textbox.selectionEnd);
this.textbox.value = left + text + right;
this.textbox.selectionStart = this.textbox.selectionEnd = left.length + text.length;
this.textbox.scrollLeft = sLeft;
this.textbox.scrollTop = sTop;
// And focus
this.textbox.focus();
},
insertTag: function(tag)
{
var oldtext = this.textbox.value;
var selstart = this.textbox.selectionStart;
var selend = this.textbox.selectionEnd;
// left<middle>right where middle is selected
var left = oldtext.substr(0, selstart);
var middle = oldtext.substr(selstart, selend-selstart);
var right = oldtext.substr(selend);
var tagleft = "["+tag+"]";
var tagright = "[/"+tag+"]";
// Special case for list tag.
// It sucks. most of it is trial and error.
if(tag == "list")
{
var parts = trim(middle).split("\n");
middle = "\n";
for(var i in parts)
{
middle += "[*]"+parts[i]+"\n";
}
// Cursor outside of a [list] tag?
if(left.lastIndexOf(tagleft) <= left.lastIndexOf(tagright))
{
// ...[list]...[/list]...<middle>...
//middle = tagleft + middle + tagright;
this.textbox.value = left + tagleft + middle + tagright + right;
/*this.textbox.selectionStart = left.length + tagleft.length;
this.textbox.selectionEnd = left.length + tagleft.length + middle.length;*/
this.textbox.selectionStart = this.textbox.selectionEnd = left.length + tagleft.length + middle.length;
this.textbox.focus();
}
else
{
middle = trim(middle);
this.textbox.value = left + middle + right;
this.textbox.selectionStart = this.textbox.selectionEnd = left.length + middle.length;
this.textbox.focus();
}
return;
}
// left[tagleft]middle[tagright]right
this.textbox.value = left + tagleft + middle + tagright + right;
this.textbox.selectionStart = left.length + tagleft.length;
this.textbox.selectionEnd = left.length + tagleft.length + middle.length;
this.textbox.focus();
},
setLoadingImage: function(bool)
{
this.loading.style.visibility = bool ? "visible" : "hidden";
},
openBar: function(id)
{
var bars = [$("qr2_emotebar"), $("qr2_tagbar")];
var newbar = bars[id];
var drink = (newbar.style.display != "none" && newbar.style.display != "");
if(id == 0 && !this.generatedEmotes) this.generateEmoteBar();
if(id == 1 && !this.generatedTags) this.generateTagBar();
bars[id].style.display = drink ? "none" : "block";
bars[1-id].style.display = "none";
},
toggleReplyWindow: function()
{
// Toggle window, return true if window will be open, false if close
if(this.open)
return this.closeReplyWindow();
else
return this.openReplyWindow();
},
openReplyWindow: function()
{
// Return true if window is opened
if(this.open)
return;
this.open = true;
this.textbox.focus();
//this.window.style.display = "block";
this.animate(
0, // to zero px (bottom of screen)
500, // in 500 ms
quad_out // using quad_out equation
);
return true;
},
closeReplyWindow: function()
{
// Return false if window is closed
if(!this.open)
return false;
if(!this.posting && this.textbox.value.length > 0)
if(!confirm("Closing this will remove current reply text"))
return true;
this.open = false;
this.animate(
-this.window.clientHeight-5, // to minus its height (off screen)
500, // in 500 ms
quad_in // using quad_in equation
);
this.textbox.value = "";
this.textbox.blur();
this.setPostingMode(MODE_NONE);
return false;
},
setPostingMode: function(mode, text)
{
this.postmode = mode;
this.posticon.src = (mode == MODE_EDIT) ? pencil : comment_add;
this.posticon.title = text;
},
editPost: function(postid)
{
this.postid = postid;
this.setPostingMode(MODE_EDIT, "Editing post "+this.postid);
this.syncEditText(postid);
this.openReplyWindow();
},
quotePost: function(postid)
{
// This allows us to quote shit while editting our post.
if(this.postmode != MODE_EDIT)
this.setPostingMode(MODE_POST, "Replying to thread "+this.threadid);
this.syncQuoteText(postid);
this.openReplyWindow();
},
showPreview: function(text)
{
// Fuck you valid HTML
var html = ["<html>",
"<head>",
"<title>Post Preview</title>",
"<style>",
// http://forums.facepunchstudios.com/styles.css
"body { font-family: Tahoma; font-size: 13px; font-size-adjust: none; font-style: normal; font-variant: normal; font-weight: normal; line-height: normal; }",
".quote { background:#CCDDEE none repeat scroll 0%; border:1px dotted #AAAAAA; color:#333344; font-size:10px; padding:5px; }",
".quotename { background:#667788 none repeat scroll 0%; border-color:#AAAAAA rgb(170, 170, 170) -moz-use-text-color; border-style:dotted dotted none; border-width:1px 1px 0px; color:#FFFFFF; font-size:10px; padding:2px; }",
"pre.code, div.code { background-color:#EEEEEE; border:1px dashed #AAAAAA; font-family:Fixed,monospace; font-size:11px; overflow-x:auto; overflow-y:hidden; padding:15px 5px 20px; }",
".highlight { color:#FF0000; font-weight:bold; }",
".inlineimg { vertical-align:middle; }",
"img { border:0pt none; vertical-align:middle; }",
"img.bbimg { background-color:#FFFFAA; border:1px solid black; }",
"#post { color: #222222; margin: 20px; }",
"</style>",
"</head>",
"<body>",
"<div id=\"post\">",
text,
"</div>",
"</body>",
"</html>"].join("");
if(this.previewWin)
{
this.previewWin.close();
}
this.previewWin = document.open("about:blank", "qr2_previewWindow", "width=800,height=200,directories=no,menubar=no,location=no,resizable=yes,scrollbars=yes,status=yes");
if(!this.previewWin)
{
alert("Your browser has blocked windows openening :(");
return;
}
this.previewWin.document.write(html);
this.previewWin.document.close();
},
syncPreview: function()
{
if(this.awaitingAJAX)
{
alert("Busy");
return;
}
var threadid = this.threadid;
var message = this.textbox.value;
GM_xmlhttpRequest({
method: "POST",
headers: {"Content-type":"application/x-www-form-urlencoded"},
url: facepunch+"newreply.php",
data: "preview=1&wysiwyg=0&parseurl=1&s=&signature=1&fromquickreply=1&p=who+cares&do=postreply&message="+encodeURIComponent(message)+"&t="+threadid,
onload: this.callback_syncPreview.bind(this),
});
this.awaitingAJAX = true;
this.setLoadingImage(true);
},
syncEditText: function(postid)
{
if(this.awaitingAJAX)
{
alert("Busy");
return;
}
GM_xmlhttpRequest({
method: "GET",
url: facepunch+"editpost.php?do=editpost&p="+postid,
onload: this.callback_syncEdit.bind(this),
});
this.awaitingAJAX = true;
this.setLoadingImage(true);
},
syncQuoteText: function(postid)
{
if(this.awaitingAJAX)
{
alert("Busy");
return;
}
GM_xmlhttpRequest({
method: "GET",
url: facepunch+"newreply.php?do=newreply&p="+postid,
onload: this.callback_syncQuote.bind(this),
});
this.awaitingAJAX = true;
this.setLoadingImage(true);
},
sendPost: function(threadid, message)
{
GM_xmlhttpRequest({
method: "POST",
headers: {"Content-type":"application/x-www-form-urlencoded"},
url: facepunch+"newreply.php",
data: "wysiwyg=0&ajax_lastpost="+this.ajax_lastpost+"&parseurl=1&s=&signature=1&fromquickreply=1&ajax=1&p=who+cares&do=postreply&message="+encodeURIComponent(message)+"&t="+threadid,
onload: this.callback_sendPost.bind(this),
});
this.setLoadingImage(true);
return false;
},
setupFormItems: function()
{
var editting = this.postmode == MODE_EDIT;
$("qr2_form").setAttribute("action", editting ? "editpost.php" : "newreply.php");
$("qr2_p").value = editting ? this.postid : "who cares";
$("qr2_do").value = editting ? "updatepost" : "postreply";
$("qr2_t").value = this.threadid;
},
// Callbacks ////////////////
//{{{
callback_sendPost: function(response)
{
this.setLoadingImage(false);
var text = response.responseText;
if(text.match(/^<!-- postbit ok -->/))
{
// The server returns a time value. This is the time when we last got posts.
// So if we were to request again, we send this time value and it would retrieve
// posts from this value to the current time.
var time_match;
if(time_match = text.match(/^<!-- postbit ok --><!-- time ([0-9]+)/))
{
// Store it as global.
this.ajax_lastpost = time_match[1];
}
// VBulletin has a <div id="lastpost"> in the HTML which is for
// adding new posts before it.
var newdiv = Dom.addBefore("lastpost", "div");
newdiv.innerHTML = text;
// Call the setup reply buttons function.
// This makes the reply and/or edit buttons into Javascript links that
// open the quick reply box.
this.setupReplyButtons();
}
else
{
// This will be run if the server gives us an error.
// Maybe if thread is locked or some other forum related error??
log("callback error");
log(text);
this.ajax_sucks = true;
this.form.submit();
}
// Not posting any more?
// (closeReplyWindow would otherwise say Are you sure?)
this.posting = false;
// todo: make sure we really want to clear textbox;
this.textbox.value = "";
this.closeReplyWindow();
},
callback_syncPreview: function(response)
{
// Scrape the HTML from the preview page.
this.awaitingAJAX = false;
this.setLoadingImage(false);
/*
<td class="alt1">
<!-- icon and title -->
<div class="smallfont">
<strong>Re: Title</strong>
</div>
<hr size="1" style="color:#888" />
<!-- / icon and title -->
Words
</td>
*/
var text = response.responseText;
// \s is whitespace. \S is not whitespace
var m = text.match(/<!-- \/ icon and title -->[\s]*([\s\S]+?)[\s]*<\/td>[\s]*<\/tr>[\s]*<\/table>/);
if(!m)
{
alert("Can't find preview text, maybe script needs updating?");
log(text);
return;
}
this.showPreview(m[1]);
},
callback_syncEdit: function(response)
{
// This is very poor function.
// Scrape the page and find whatever text would be there if
// we were to actually go to the page in our browser.
this.awaitingAJAX = false;
this.setLoadingImage(false);
var text = response.responseText.substr(14000); // cut out loads
var m;
if(m = text.match(/<textarea name=\"message\".+?>([\s\S]+?)<\/textarea>/))
{
// Quotes, angle brackets and amperstands are encoded.
var str = m[1];
var f = [/</g, />/g, /"/g, /&/g];
var r = ["<", ">", "\"", "&"];
for(var i=0; i<f.length; i++)
str = str.replace(f[i], r[i]);
text = str;
}
else
{
alert("Can't find edit text, maybe script needs updating?");
text = "";
}
this.textbox.value = text;
// scroll to top (nah)
//this.textbox.scrollTop = 0;
},
callback_syncQuote: function(response)
{
// This is very poor function.
// Scrape the page and find whatever text would be there if
// we were to actually go to the page in our browser.
var text = response.responseText.substr(14000); // cut out loads
var m;
if(m = text.match(/<textarea name=\"message\".+?>([\s\S]+?)<\/textarea>/))
{
// Quotes, angle brackets and amperstands are encoded.
var str = m[1];
var f = [/</g, />/g, /"/g, /&/g];
var r = ["<", ">", "\"", "&"];
for(var i=0; i<f.length; i++)
str = str.replace(f[i], r[i]);
text = str;
}
else
{
alert("Can't find quote text, maybe script needs updating?");
text = "";
}
// If we quote a few posts spread them out with two new lines.
var old = trim(this.textbox.value);
this.textbox.value = (old.length > 0 ? old + "\n\n" : "") + text;
// scroll to bottom
this.textbox.scrollTop = 9999;
this.awaitingAJAX = false;
this.setLoadingImage(false);
},
//}}}
// Events ///////////////////
//{{{
event_submit: function(ev)
{
try
{
if(this.postmode == MODE_NONE)
{
log("post mode = none");
ev.preventDefault();
return;
}
if(!this.open)
{
// Maybe we pressed the submit button using enter or something
alert("Failed to send message: reply box is closed.");
ev.preventDefault();
return;
}
if(this.posting)
{
alert("Busy");
ev.preventDefault();
return;
}
// Make sure all items are setup correctly, depending on postting/editting
this.setupFormItems();
// COMMENCE POSTING INITIALIZATION MODE: GO!
this.posting = true;
if(this.postmode == MODE_EDIT)
{
// Post through HTML
return;
}
if(this.ajax_sucks)
{
alert("Ajax sucks");
// Post through HTML
return;
}
if(!this.ajax_allow)
{
// Post through HTML
return;
}
var message = this.textbox.value;
if(!this.sendPost(this.threadid, message))
ev.preventDefault();
else
return; // Post through HTML
}
catch(e)
{
// If something errors, best not to send the form :(
ev.preventDefault();
log("E: "+e);
}
},
event_previewClick: function(ev)
{
this.syncPreview();
},
event_beforeUnload: function(ev)
{
// If sending AJAX
if(this.awaitingAJAX)
{
ev.returnValue = "Currently sending AJAX request. Continuing may cause bad things to happen.";
return;
}
// If we're actually submitting the stuff.
if(!this.posting && this.textbox.value.length > 0)
{
ev.returnValue = "You have started a reply, changes will be lost if you exit this page.";
return;
}
// meh
this.closeReplyWindow();
},
event_scroll: function(ev)
{
if(window.scrollY > window.scrollMaxY-50)
{
if(!this.scrolledEnough)
{
if(this.postmode != MODE_EDIT)
this.setPostingMode(MODE_POST, "Replying to thread "+this.threadid);
this.openReplyWindow();
}
this.scrolledEnough = true;
}
else
{
this.scrolledEnough = false;
}
},
event_keypress: function(ev)
{
if(ev.keyCode == 27 && this.open) // esc
{
this.closeReplyWindow();
ev.preventDefault();
return;
}
if(ev.ctrlKey)
{
if(ev.charCode == this.options.keycommand)
{
if(this.toggleReplyWindow())
{
if(this.postmode == MODE_EDIT)
this.textbox.value = "";
this.setPostingMode(MODE_POST, "Replying to thread "+this.threadid);
}
ev.preventDefault();
}
}
},
event_textboxkeypress: function(ev)
{
if(ev.ctrlKey)
{
switch(String.fromCharCode(ev.charCode).toLowerCase())
{
case "b": this.insertTag("b"); break;
case "i": this.insertTag("i"); break;
case "u": this.insertTag("u"); break;
case "q": this.insertTag("quote"); break;
default: return;
}
ev.preventDefault();
}
if(ev.keyCode == 9) // tab
{
if(this.options.tabtotab)
{
this.insertText(" ");
ev.preventDefault();
}
}
//log(ev.keyCode);
},
event_replyclick: function(ev)
{
this.openReplyWindow();
if(this.postmode == MODE_EDIT)
this.textbox.value = "";
this.setPostingMode(MODE_POST, "Replying to thread "+this.threadid);
},
event_mousemove: function(ev)
{
var emotebar = $("qr2_emotebar");
if(emotebar && emotebar.style.display == "block" && emotebar.firstChild)
{
var p = (ev.clientX-emotebar.offsetLeft) / emotebar.clientWidth;
p = Math.min(Math.max(p, 0), 1);
var v = -p*(4000-emotebar.clientWidth);
emotebar.firstChild.style.left = (v || 0) + "px";
}
},
//}}}
// Animation /////////////
//{{{
animate: function(target, time, func)
{
var now = Number(new Date());
//if(now < this.anim_starttime + this.anim_length)
if(this.anim_timeout)
{
// animation playing
clearTimeout(this.anim_timeout);
//return;
}
this.anim_starttime = now;
this.anim_length = time;
this.anim_startval = this.anim_current; //start;
this.anim_endval = target;
this.anim_func = func;
this.anim_current = this.anim_startval;
this.animationLoop();
},
animationLoop: function()
{
var now = Number(new Date());
if(now >= this.anim_starttime)
{
var percent = (now - this.anim_starttime) / this.anim_length;
if(percent < 1)
{
this.anim_current = this.anim_func(percent, this.anim_startval, this.anim_endval)
this.window.style.bottom = this.anim_current + "px";
this.anim_timeout = setTimeout(this.animationLoop.bind(this), 33); // 30 fps
}
else
{
this.anim_current = this.anim_endval;
this.window.style.bottom = this.anim_current + "px";
clearTimeout(this.anim_timeout);
}
}
},
//}}}
}
//unsafeWindow.openReplyWindow = QuickReply.openReplyWindow.bind(QuickReply);
unsafeWindow.closeReplyWindow = QuickReply.closeReplyWindow.bind(QuickReply);
unsafeWindow.toggleReplyWindow = QuickReply.toggleReplyWindow.bind(QuickReply);
unsafeWindow.qr2_insertText = QuickReply.insertText.bind(QuickReply);
unsafeWindow.qr2_insertTag = QuickReply.insertTag.bind(QuickReply);
unsafeWindow.qr2_openBar = QuickReply.openBar.bind(QuickReply);
//// Emotes ///////////////////////////////////
var bbcode = [
"&b", "&i", "&u", ""e", "lua", "code", "highlight", "sp", "indent", "noparse", "url", "img", "img_thumb", "release", "list"
];
// The extra 'true' means use a white background, otherwise they look shit on black.
// #109
var emotes = [
[":(", "frown.png"],
[":)", "smile.png"],
[":04:", "emot-04.gif", true],
[":10bux:", "emot-10bux.gif"],
[":20bux:", "emot-20bux.gif"],
[":3:", "3.gif"],
[":4chan:", "4chan.gif"],
[":911:", "emot-911.gif"],
[":D", "biggrin.png"],
[":ahaw:", "emot-haw.gif"],
[":argh:", "emot-argh.gif"],
[":banjo:", "emot-banjo.gif"],
[":beerbear:", "emot-beerbear.png"],
[":bees:", "panic.gif"],
[":britain:", "emot-britain.gif"],
[":burger:", "emot-burger.gif"],
[":c00l:", "emot-c00l.gif"],
[":canada:", "emot-canada.gif"],
[":cheers:", "emot-cheers.png"],
[":chef:", "emot-chef.png"],
[":chicago:", "emot-chicago.gif"],
[":china:", "emot-china.gif"],
[":clint:", "emot-clint.gif"],
[":coal:", "emot-coal.png"],
[":comeback:", "emot-comeback.gif"],
[":confused:", "confused.png"],
[":cool:", "cool.png"],
[":cop:", "emot-cop.gif"],
[":cow:", "emot-cow.gif"],
[":crying:", "emot-crying.gif"],
[":cthulhu:", "emot-cthulhu.gif"],
[":dance:", "emot-dance.gif"],
[":denmark:", "emot-denmark.gif"],
[":derp:", "emot-derp.gif", true],
[":dog:", "emot-dog.png"],
[":downs:", "emot-downs.png"],
[":eek:", "emot-eek.png"],
[":emo:", "emot-emo.gif"],
[":eng101:", "emot-eng101.png"],
[":ese:", "emot-ese.gif"],
[":excited:", "emotboingpn7.gif"],
[":fap:", "emot-fap.gif"],
[":foxnews:", "emot-foxnews.gif"],
[":freeman:", "emot-freeman.gif"],
[":geno:", "emot-geno.png"],
[":ghost:", "emot-ghost.gif"],
[":girlv:", "emot-j.gif"],
[":goatse:", "emot-goatse.gif"],
[":ham:", "emot-ham.png"],
[":hellnaw:", "emot-hellnaw.gif", true],
[":holy:", "emot-holy.gif"],
[":howdy:", "howdy.gif"],
[":huh:", "emot-huh.gif"],
[":jewish:", "emot-jewish.png"],
[":jihad:", "emot-jihad.png"],
[":keke:", "emot-keke.png"],
[":krad:", "emot-krad2.gif"],
[":laffo:", "emot-laffo.gif", true],
[":laugh:", "emot-laugh.gif"],
[":lepard:", "leperdancenb0.gif"],
[":lol:", "emot-lol.gif", true],
[":loleyes:", "emot-loleyes.gif", true],
[":love:", "emot-love.gif"],
[":mad:", "mad.png"],
[":mario:", "emot-mario.gif"],
[":meatwad:", "emot-meatwad.gif"],
[":megaman:", "emot-megaman.gif"],
[":megamonoc:", "emot-megamonocle.gif"],
[":metis:", "emot-metis.gif"],
[":mmph:", "emotowvr9.png"],
[":monocle:", "emot-monocle.gif"],
[":moore:", "emot-moore.gif"],
[":ninja:", "emot-ninja.gif"],
[":nws:", "emot-nws.gif", true],
[":nyd:", "emot-nyd.gif"],
[":o", "redface.png"],
[":patriot:", "emot-patriot.png"],
[":pirate:", "emot-pirate.gif"],
[":pwn:", "emot-pwn.png"],
[":q:", "emot-q.png"],
[":quagmire:", "emot-quagmire.gif"],
[":question:", "emot-question.gif"],
[":raise:", "emot-raise.gif"],
[":rock:", "emot-rock.gif"],
[":rolleye:", "emot-rolleye.png"],
[":sax:", "emot-sax.png"],
[":scax:", "emot-scax.png"],
[":science:", "emot-science.gif"],
[":sg:", "emot-sg.gif"],
[":sigh:", "emot-sigh.gif"],
[":silent:", "emot-silent.png"],
[":siren:", "emot-siren.gif", true],
[":specialschool:", "emotengdownsdancevd9.gif"],
[":ssh:", "emot-ssh.png"],
[":ssj:", "emot-ssj.gif"],
[":tama:", "rk8yfp.gif"],
[":tinfoil:", "emot-tinfoil.gif"],
[":tizzy:", "emot-tizzy.gif"],
[":ussr:", "emot-ussr.gif"],
[":v:", "emot-v.png"],
[":weed:", "emot-weed.png"],
[":witch:", "emot-witch.png"],
[":wooow:", "wooow.gif"],
[":words:", "emot-words.gif"],
[":wtf:", "emot-wtf.gif", true],
[":xbox:", "emot-xbox.png"],
[":xd:", "emot-xd.png"],
[":yarr:", "emot-yarr.png"],
[":yoshi:", "emot-yoshi.gif"],
[":zoid:", "emot-zoid.gif"],
[";)", "wink.png"],
];
//// Start Code ///////////////////////////////
// If qrform doesn't exist then the thread is locked and we can't reply anyway.
if(document.body && $("qrform"))
{
QuickReply.init();
}