$.ajaxSetup({
type: 'POST',
url: 'http://'+window.location.hostname+"/index.pl",
// hostname included for Guest User on non-canonical domain with canonical base element in html head
cache: false,
dataType: 'text',
timeout: e2.timeout * 1000
});
e2.ajax = {
// legacy non-production E2AJAX.functions are in [Additional Everything2 Ajax] (in Javascript Repository)
// ======================= Utility
pending: {},
htmlcode: function(htmlcode, params, callback, pendingId){
// htmlcode is the name of an htmlcode. See [ajax update page] for security requirements.
// params is comma-separated args for htmlcode, or an object optionally containing query
// parameters (query), htmlcode args (args) and/or ajax parameters (ajax)
if (typeof params != 'object') params = {args: (params || '').toString()};
var ajax = params.ajax || {};
ajax.data = ajax.data || params.query || {};
ajax.data.args = ajax.data.args || params.args || '';
ajax.data.htmlcode = htmlcode ;
ajax.data.node_id = ajax.data.node_id || e2.node_id;
ajax.data.displaytype = 'ajaxupdate';
ajax.complete = ajax.complete || ajaxFinished;
if (pendingId && !ajax.data.ajaxIdle)
e2.ajax.pending[pendingId] = {htmlcode: htmlcode, query: ajax.data};
$.ajax(ajax);
function ajaxFinished(request, statusText){
if (pendingId) delete e2.ajax.pending[pendingId];
if (!callback) return;
var success = statusText == 'success' && (ajax.dataType=='json' ||
/<[!]-- AJAX OK/.test(request.responseText)); // [!] = fix for brain-dead GPRS network
if (success)
var str = request.responseText.replace( /<[!]-- AJAX OK[\s\S]*/ , '' );
else if (statusText == 'success')
statusText = 'incomplete response';
else if (statusText != 'timeout')try{
if (request.status==0)
throw 'sometimes even the test throws an exception';
else if (request.status >= 500)
statusText = 'server error';
}catch(e){
statusText = 'connection error';
}
callback((success ? str : request), success, statusText || '(unknown error)');
}
},
update: function(id, htmlcode, args, replaceID, callback) {
var ol = $('#'+id), nu;
if (!ol[0]) return;
if (replaceID == null) replaceID = 1;
e2.ajax.htmlcode(htmlcode, args, ajaxFinished, id);
function ajaxFinished(result, success, statusText){
if (success){
if (replaceID) {
nu = $(result);
ol.replaceWith(nu);
}else{
ol.html(result);
nu = $('*', ol);
}
e2(nu);
}
if (callback) callback(result, success, statusText);
}
},
varChange: function(name, value, callback){
// NB: name and value have to pass check in htmlcode ajaxVar
e2.ajax.htmlcode('ajaxVar', name+','+value, callback);
e2.deleteCookie(name);
},
starRateNode: function (node_rate, weight, seed, nonce) {
$.ajax({data:{
op : "starRate",
rating_node : node_rate,
rating: weight,
starrating_seed: seed,
starrating_nonce: nonce,
displaytype: 'ajaxupdate'
}});
},
// ======================= Sleep/wake
addRobot: function(x){
// first time called, set up sleep and wake. Subsequent times just add x to list.
e2.ajax.addRobot = function(y){ robots.push(y); };
e2.inactiveWindowMarker = e2.inactiveWindowMarker || '';
var	windowId = e2.getUniqueId(),
lastActive = e2.now(),
lastZzz = 'wake',
wakeEvents = 'focusin focus mouseenter mousemove mousedown keydown keypress scroll click',
wakeWatch = $(document).add(window).bind(wakeEvents, wake).blur(monitor),
robots = [new e2.periodical(monitor, 60), x]; // monitor will put itself to sleep
function monitor(){
wakeWatch.unbind(wakeEvents, wake).bind(wakeEvents, wake);
var myCookie = e2.getCookie('lastActiveWindow');
if (e2.now() - lastActive > e2.sleepAfter * 60000 ||
(!e2.isChatterlight && myCookie && myCookie != windowId)) zzz('sleep');
}
function wake(e){
// anything this event should do inside the page has been done. Once is enough for this:
e && e.stopImmediatePropagation();
wakeWatch.unbind(wakeEvents, wake); // once a minute is enough
lastActive = e2.now();
e2.setCookie('lastActiveWindow', windowId);
zzz('wake');
}
function zzz(z){
if (lastZzz == z) return;
$(robots).each(function(){this[z]();});
lastZzz = z;
titleLength = document.title.length - e2.inactiveWindowMarker.length;
document.title = (z == 'sleep' ? document.title + e2.inactiveWindowMarker
: document.title.substr(0, titleLength));
}
},
// ======================= list management
lists: {},
addList: function(listName, listSpec){
e2.ajax.lists[listName] = listSpec;
e2('#'+listName, function(){
if (!e2.ajax.lists[this.id].manager){
e2.ajax.lists[this.id].manager = new e2.ajax.listManager(this.id);
}else{
e2.ajax.lists[this.id].manager.restart();
}
});
},
listManager: function(listName){
function updateThisList(){
e2.ajax.updateList(listName);
}
e2.periodical.call(this, updateThisList,
e2.ajax.lists[listName].period || e2.defaultUpdatePeriod * 60,
e2.ajax.lists[listName].stopAfter, e2.ajax.lists[listName].die);
e2.ajax.addRobot(this);
},
updateList: function(listName, query, callback){
var container = $('#'+listName);
var list = e2.ajax.lists[listName];
if (!container[0] || !list) return;
$('.markedForRemovalNextTime', container).remove();
if (query == null) query = {ajaxIdle: 1}; // query only passed if user action causes update
var listCallback = false;
e2.ajax.htmlcode(list.getJSON,
{args: list.args, query: query, ajax:{dataType: 'json', success: doJSON}}, callback);
function doJSON(data){
for (var i = 1, keep = {}; data[i]; i++){
var id = list.idGroup + data[i].id;
keep[id] = true;
if (!$('#'+id)[0]){
var el = $(data[i].value);
if (!el.attr('id'))
el.attr('id', id);
if (data[i].timestamp)
el.addClass('timestamp_' + data[i].timestamp);
e2.ajax.insertListItem(container, el, data[i].timestamp, list.ascending);
listCallback = list.callback;
}
}
container.children().each(function(){
if (!keep[this.id] && this.id.indexOf(list.idGroup)==0 &&
(!list.preserve || !e2(list.preserve, this)[0])){
listCallback = list.callback;
e2.ajax.removeListItem(this);
}
});
if (listCallback) e2(listCallback).defer();
}
},
insertListItem: function(container, content, timestamp, ascending){
content.hide();
if (ascending || timestamp){
container.append(content);
}else{
container.prepend(content);
}
if (timestamp){
var dir = (ascending ? 1 : -1 );
// place above first younger (ascending) or older (descending) item in list
// if none found, it belongs below them all, where it is
container.children().each(function(){
var match = /\btimestamp_(\d+)\b/.exec(this.className);
if (match && dir*match[1] > dir*timestamp){
$(this).before(content);
return false;
}
});
}
// defer here & in removeListItem so it all happens at once: smoother
// e2ify content after revealing so expandable inputs have dimensions
e2(function(){
content.slideDown(e2.fxDuration, function(){e2(content);});
}).defer();
},
removeListItem: e2(
function(el){
// hide the first time, remove in updateList the next time:
// avoid incoming list reinstating dismissed item
$(el).slideUp(e2.fxDuration).addClass('markedForRemovalNextTime');
}
).defer,
dismissListItem: function(event){
var targetId = /\bdismiss\s+(\S+)/.exec(this.className)[1];
// find which list we're in:
for (var parent = this.parentNode, spec; parent; parent = parent.parentNode){
if (parent.id && (spec = e2.ajax.lists[parent.id])) break;
}
if (spec && spec.dismissItem)
e2.ajax.htmlcode(spec.dismissItem, targetId.replace(/\D/g, ''));
e2.ajax.removeListItem('#'+targetId);
event.preventDefault();
},
// ======================= Update Triggers
periodicalUpdater: function(instructions, period, lifetime, expired){
this.className = 'ajax ' + instructions ;
e2.periodical.call(this, function(){$(this).trigger('click');}, // triggerHandler() fails
period||e2.defaultUpdatePeriod * 60, lifetime, expired);
e2.ajax.updateTrigger.call(this);
e2.ajax.addRobot(this);
},
updateTrigger: function(){
if (this.disabled) return;
var tag = (this.tagName || '').toLowerCase(); // no tagName if periodical updater
var $this = $(this);
if (tag) var type = $this.attr('type');
if ($this.hasClass('instant')){
e2.ajax.triggerUpdate.call(this);
}else if (tag == 'textarea' || type == 'hidden' || type == 'text'){
$(this.form).unbind('.ajax').bind('submit.ajax', formTrigger);
this.originalValue = this.value;
}else{
var bindEvent = (tag != 'select' ? 'click' : 'change');
if (/[?&]confirmop=/.test(this.href) && tag != 'a')// links already done
$this.bind(bindEvent, e2.confirmop);
$this.bind(bindEvent, e2.ajax.triggerUpdate);
}
function formTrigger(event){
var formSpec = $(this).serializeArray();
$.each(formSpec, function(n){ // serialize() screws up encoding (1.4.3)
// put values into object later not string now to avoid encoding snarfs
formSpec[n] = this.name + '=/';
});
formSpec = formSpec.join('&');
var nothingHappened = true;
$('textarea.ajax, input[type=text].ajax, input[type=hidden].ajax', this)
.each(function(){
if (!this.value) return;
nothingHappened = e2.ajax.triggerUpdate.call(this, event, formSpec) && nothingHappened;
});
return nothingHappened;
}
},
triggerUpdate: function(event, formSpec){
if (event && event.isDefaultPrevented && event.isDefaultPrevented() && !formSpec){ // confirmop may have done this
if ( this.checked ) this.checked = false ;
if ( this.blur ) this.blur() ;
return false;
}
// get ajax instructions from class & chop them up
var params = ('[0] '+this.className).split(/\bajax\s+/);
if (!params[1]) return true; // no instructions
params = params[1].split(/\s+/)[0].replace(/\+/g,' ').split(':');
if (!params[1]) return true; // no htmlcode
// target:
var updateTarget = params[0];
var updateOnly = (updateTarget.charAt(0) == '(');
if (updateOnly) updateTarget = updateTarget.slice(1,-1);
var target = $('#'+updateTarget)
.find('.error').remove().end(); // only show most recent error
if (!target[0]) return true;
var args = params[2] || '';
// htmlcode & query:
params = params[1].split('?');
var htmlcode = params[0];
if (!params[1] && this.href) params = this.href.split('#')[0].split('?');
var q = decodeURIComponent(params[1] || formSpec || '').split(/[&=]/);
// get query values from string/form/prompt
var query = {};
for (var i=0; q[i]; i+=2){
var name = q[i], value = q[i+1];
if (value && value.charAt(0) == '/' ){
if ( value.charAt(1) != '#' ){ // get value from form
var el = this.form[value.substr(1) || name];
if (el && el.length && el[0].type=='radio') el = $(el).filter(':checked')[0];
if (el && /radio|checkbox/.test(el.type) && el.checked == false) el = null;
value = el && el.value;
} else { // get value from prompt
value = prompt(value.substr(2)+':' , query[name] || '') ;
if (!value) return false ;
}
}
if (value != null)
query[name] = (value && value.replace(
/[^ -~]/gm, function(x) {return "&#" + x.charCodeAt(0) + ";";})
) || query[name] || '';
}
// remember a couple of things for later:
var sentValue = this.value;
$(this).addClass('pending');
var hadFocus = e2.getFocus(target);
var isPeriodic = !this.tagName;
if (!isPeriodic && htmlcode != '#'){
// disable, but avoid disabled controls when returning from history...
var ersatz = target.clone().addClass('pending');
target.replaceWith(ersatz);
(updateOnly ? $ : e2)('*', ersatz) // e2 = inclusive select: include container if it's to be replaced
.addClass('pending')
.each(function(){
if (this.name){
if (target[this.name]) this.value = target[this.name].value; // still see changed values
// ... by changing name on disabled controls:
$(this).attr({disabled:'disabled', name:'xxx'});
}
this.disabled = true;
if (this.href && /\bajax\s/.test(this.className)) this.href = '#';
});
try { e2(ersatz) } catch(e){};
}
// now do it
var e = this;
if (htmlcode == '#'){
e2.ajax.updateList(updateTarget, query, doneUpdate);
}else{
if (isPeriodic) query.ajaxIdle = 1;
e2.ajax.update(updateTarget, htmlcode,
{query: query , args: args}, !updateOnly, doneUpdate);
}
if (event && event.preventDefault) event.preventDefault();
return false;
// tidy up/report error afterwards
function doneUpdate(stringIfsuccessfulOrRequestIfNot, success, statusText){
$(e).add(ersatz).removeClass('pending') ;
if (success) {
if ( e.originalHeight ) e.style.height = e.originalHeight ;
if ( e.originalValue != null && e.value == sentValue ) e.value = e.originalValue ;
} else if ( !isPeriodic ) { // periodic updater dies quietly
var tag = target.css('display') == 'block' ? 'div' : 'span';
target.append('<'+tag+' class="error"><strong>Error:</strong> '+statusText+'</'+tag+'>');
if (e.htmlcode == '#') return;
ersatz.replaceWith(target);
e2(target); // jquery unbinds all the magic when you take it out of the DOM
}
if (hadFocus && $('#'+hadFocus).length)
e2(function(){$('#'+hadFocus).focus();}).defer() ; // IE8 fails without defer
}
}
// END e2.ajax = {
};
// automation
if (!e2.guest){
e2.ajax.addList('notifications_list',{ // id of list container
getJSON: "notificationsJSON", // htmlcode for list data (required)
args: 'wrap', // htmlcode arguments for getJSON
idGroup: "notified_", // id stub for individual list items (required):
// N.B.: items sent with an id keep it. If it doesn't match the idGroup they will never be removed.
period: 45, // seconds between updates (default is above)
dismissItem: 'ajaxMarkNotificationSeen' // htmlcode run when item dismissed. arg is id from json
});
e2.ajax.addList('chatterbox_messages', {
ascending: true, // put newest at bottom (default is newest at top)
getJSON: 'showmessages',
args: ',j',
idGroup: 'message_',
preserve: 'input:checked', // don't remove list items which match or whose contents match this
period: 23,
callback: function(){ // called after update iff anything changed
if ( $('#chatterbox_messages *')[0] && !$('#formcbox hr').length )
$('#chatterbox_chatter').before('<hr width="40%">');
}
});
e2.ajax.addList('messages_messages', {
ascending: true, // put newest at bottom (default is newest at top)
getJSON: 'testshowmessages',
args: ',j',
idGroup: 'message_',
preserve: '.showwidget .open', // don't remove list items which match or whose contents match this
period: 23
});
e2.ajax.addList('chatterbox_chatter', {
ascending: true,
getJSON: 'showchatter',
args: 'json',
idGroup: 'chat_',
period: e2.autoChat ? 11 : -1, // -1 creates periodical function in stopped state
//		preserve: '.chat', // never remove chat items
callback:(function(){
// scroll down as chat updated.
// NB: Without this, slide down of chat is unreliable in IE8, even without scrollbar
var chat, userScrolled = false,
scrollChat = new e2.periodical(function(){
chat.scrollTop = chat.scrollHeight;
}, -1);
// tell scrollChat what to scroll, or not to scroll if user has scrolled up
e2('#chatterbox_chatter', function(){
scrollChat.restart(jQuery.fx.interval/1000, e2.fxDuration*3/1000);
$(this)
//				.addClass('autochat') // limits height and adds scroll bar if needed
.scroll(function(e){
userScrolled = (this.scrollHeight - this.scrollTop - this.clientHeight > 16);
});
chat = this;
});
return function(){
if (!userScrolled) scrollChat.restart();
};
})(),
stopAfter: e2.sleepAfter * 60, // seconds
die: function(){
$('#autoChat').each(function(){this.checked=false;});
$('#chatterbox *').blur(); // '#chatterbox :focus' fails if window is not focussed
e2.ajax.insertListItem($('#chatterbox_chatter'), $(
'<p id="chat_stopped"><strong>Chatterbox refresh stopped.</strong></p>'),0,1);
}
});
e2('.dismiss', 'click', e2.ajax.dismissListItem);
new e2.ajax.periodicalUpdater('newwriteups:updateNodelet:New+Writeups', 300);
new e2.ajax.periodicalUpdater('otherusers:updateNodelet:Other+Users');
}
// replace other inputs with push buttons
e2('input.replace', function (){
var label = this.parentNode ;
if ( this.name.substr(0,6)=='vote__' &&
( this.getAttribute( 'value' )=='0' || this.disabled ) )
return label.parentNode.removeChild(label) ;
var value = label.innerHTML.replace( /^(<.*>|\s)*|(<.*>|\s)*$/g , '' );
if ( this.name.substr(0,6)=='vote__' ) value = (value=='+' ? 'Up' : 'Down');
var b=$('<input type="button" class="' + this.className.replace( /\breplace\b/ , 'replaced' ) +
'" name="' + this.name + '" title="' + this.title + '" value="' + value + '">')[0];
$(label).replaceWith(b);
if (b.outerHTML) b.outerHTML=b.outerHTML; // update DOM in IE7
// tidy up around replaced inputs
e2('.vote_buttons .replaced', b).each(function(){
if (this.previousSibling && this.previousSibling.nodeType==3)
this.parentNode.removeChild(this.previousSibling);});
});
// update nodelet only, not whole page
e2('div.nodelet', function(){ // div.nodelet to filter out body on nodelet's own page
if (!(this.nodeletName = $('h2', this)[0]) ) return; // probably a failed nodelet
this.nodeletName = this.nodeletName.innerHTML;
var omo = 'ajax '+this.id+':updateNodelet:'+this.nodeletName.replace(/ /g, '+');
$('form', this).each( function(){
if (this.onsubmit || this.ajaxTrigger || this.passwd || this.node ||
(this.node_id && this.node_id.type != 'hidden')) return ;
dummy = $('<input type="hidden" name="ajaxTrigger" value="1" class="'+omo+'">');
$(this).append(dummy) ;
if (dummy.outerHTML) dummy.outerHTML=dummy.outerHTML ; // update DOM in IE7
});
if (this.nodeletName == 'Master Control') return; // need pageload to see things happen
$('a', this).each(function(){
if (this.href.indexOf(e2.pageUrl+'?')==0 &&
!/\bajax\b/.test(this.className) &&
!/[&?]displaytype=|[&?]op=logout|[&?]op=randomnode/.test(this.href))
$(this).addClass(omo);
});
});
// draggable nodelets
e2('h2.nodelet_title', 'click', function(e){
// disables nodelet collapser when nodelet is being dropped
if ($(this).hasClass('stopClick')) e.stopImmediatePropagation();
});
e2('#sidebar:not(.pagenodelets)', 'sortable', {
axis: 'y',
containment: 'document',
items: 'div.nodelet',
opacity: 0.5,
handle: 'h2.nodelet_title',
tolerance: 'pointer',
cursor: 'n-resize',
stop: function(e, ui){
// disable nodelet collapser when nodelet is being dropped
ui.item.find('h2').addClass('stopClick');
e2(function(){
ui.item.find('h2').removeClass('stopClick')
}).defer();
},
update: function(e, ui){
e2.ajax.htmlcode('movenodelet', ui.item[0].nodeletName + ',' +
ui.item.prevAll('.nodelet').length,
null, ui.item[0].nodeletName);
}
});
// activate update triggers
e2('.wuformaction.ajax', 'unbind'); // ajax makes function to fiddle with form values redundant
e2('.ajax', e2.ajax.updateTrigger);
window.onbeforeunload = function(e){ // jQuery bind() won't hack it (in version 1.3)
e = e || window.event || {}; // for IE/unknown fail
var warnings = {
// name: htmlcode; value: code to eval to get warning. false = ignore. List from [ajax update page]
// id = updateTarget, x = update parameters
ilikeit: '"Your message to the author hasn\'t arrived yet"',
writeupmessage: "'Your message is being sent'",
zenDisplayUserInfo: "'Your message is being sent'",
weblogform: "'A usergroup page is being updated'",
categoryform: "'A category is being updated'",
writeupcools: "'Your C! is being noted. You may lose it if you don\\'t let it finish'",
updateNodelet: "'The '+x.query.args+' nodelet is updating'",
coolit: "'This page is being ' +" +
"(x.query.coolme ? 'dunked in liquid helium' : 'thawed')",
bookmarkit: "(x.query.bookmark_id == e2.node_id ? 'This page' : 'A writeup') +" +
"' is being added to your bookmarks'",
favorite_noder: "opcode(x.query.op)",
voteit: "opcode(x.query.op)",
movenodelet: "'The ' + id + ' nodelet\\'s new position is being recorded'",
// these we ignore:
listnodecategories: 'false', // instant ajax for info/confirmation only
nodeletsettingswidget: 'false', // user lost interest in settings
// these shouldn't be needed:
changeroom: '', showmessages: '',
showchatter: '', displaynltext2: ''
};
var str;
function opcode(op){
if (!op) return '';
switch(op){
case 'vote': return 'Your vote is being recorded';
case 'favorite': str = 'added to';
case 'unfavorite': return $('#pageheader h1').text()  +' is being ' +
(str || 'removed from') + ' your favorite users list';
case 'hidewriteup': str = 'hidden'; break;
case 'unhidewriteup': str = 'unhidden'; break;
case 'massacre': str ='nuked'; break;
case 'insure': str = (/undo/.test($('#'+id+' a').text()) ? 'un' : '') +
'insured'; break;
default: str = 'processed';
}
return 'A writeup is being ' + str;
}
for (var id in e2.ajax.pending){
var x = e2.ajax.pending[id];
if (warnings[x.htmlcode] && (str = eval(warnings[x.htmlcode]))) break;
}
if (x && str !== false) return e.returnValue=( // was something pending, and not to be ignored
(str || 'Something on this page is being updated') +
". Please wait a moment." ) ;
};
// rename vote button: no longer needed, but can still send form
$(function(){
$('#votebutton').each(function(){
this.value = 'blab!' ;
this.title = 'send writeup message(s)' ;
});
});
// switch automatic chatter update on and off
e2('#chatterbox:not(.pending)', function(){
function onOff(on){
if (on){
e2.ajax.updateList('chatterbox_chatter');
e2.ajax.lists.chatterbox_chatter.manager.restart(11);
}else{
e2.ajax.lists.chatterbox_chatter.manager.restart(-1);
}
}
// checkbox to turn it on and off
$('#autoChat').parent().remove(); // may be one left over if update failed
$('#message_send').after(
' <label title="Keep chat up to date even if the focus/cursor is somewhere else">'+
'<input type="checkbox" value="1" name="autoChat" id="autoChat"'+
(e2.autoChat ? ' checked="checked"' : '') + '>Keep updating</label>')
.next().find('input') // click can't go on label or you get two clicks
.click(function(e){
onOff(e2.autoChat = this.checked);
if (!e2.isChatterlight) e2.setCookie('autoChat', e2.autoChat ? '1' : '0');
$('#message').focus();
});
// always update when focus is in chatterbox
$(this)
.focusin(function(){
onOff(true); // do this even if autoChat is on, to restart inactivity countdown
if (e2.getFocus(this) == 'autoChat') return; // don't check it or the click will uncheck it
$('#autoChat', this)[0].checked = e2.autoChat; // is unchecked if autoChat has died
})
.focusout(function(){
if (!e2.autoChat) onOff(false);
});
});
if (e2.isChatterlight) e2('#message', e2(function(){
$(this).css('width','').focus();
}).defer); // defer for IE

