Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
sprint = {
    enabled:               false,
    paused:                false,
    portlets: [
        { id: 'wl', enabled: true, items: 10, refresh: 5 },
        { id: 'np', enabled: true, items: 10, refresh: 5 },
        { id: 'onp', enabled: true, items: 10, refresh: 5 },
        { id: 'rc', enabled: true, items: 10, refresh: 5 }
    ],
    ajax: [],
    ajaxPreview: null
};

sprint.modules = [
    {   id: 'wl',
        type: 'query',
        title: 'Watchlist', 
        url: "/w/api.php?format=xml&action=query&list=watchlist&wllimit=", 
        tag: 'item',
        titlelink:'/wiki/Special:Watchlist'},
    {   id: 'np',
        type: 'query', 
        title: 'New Pages',
        url: "/w/api.php?action=query&format=xml&list=recentchanges&rcshow=!bot|!redirect&rctype=new&rcnamespace=0&rcprop=title|timestamp|ids|patrolled&rclimit=", 
        tag: 'rc',
        titlelink:'/wiki/Special:NewPages'},
    {   id: 'onp',
        type: 'query',
        title: 'Oldest Unpatrolled',
        url: "/w/api.php?action=query&format=xml&list=recentchanges&rcshow=!bot|!redirect|!patrolled&rctype=new&rcnamespace=0&rcprop=title|timestamp|ids|patrolled&rcdir=newer&rclimit=", 
        tag: 'rc',
        titlelink:'/w/index.php?title=Special:NewPages&dir=prev&hidepatrolled=1' },
    {   id: 'rc',
        type: 'query',
        title: 'Recent Changes',
        url: "/w/api.php?format=xml&action=query&list=recentchanges&rclimit=", 
        tag: 'rc',
        titlelink:'/w/index.php?title=Special:RecentChanges&limit=50&hideliu=1' }
];

m_zIndex=101;

sprint.loadCookies=function() {
    sprint.enabled = (sprint.readCookie('sprint_enabled')=='true');

    var cookie = sprint.readCookie('sprint_portlets');
    if (cookie) {
        sprint.portlets = [];
        var arrPortlets = cookie.split('||');
        for (var i=0; i<arrPortlets.length; i++) {
            sprint.portlets[i]={};
            var s = arrPortlets[i].split('|');
            sprint.portlets[i].id = s[0];
            sprint.portlets[i].enabled = (s[1]=='true');
            sprint.portlets[i].items = s[2];
            sprint.portlets[i].refresh = s[3];
        }
    }

    for (var i=0; i<sprint.portlets.length; i++) {
        var p = sprint.portlets[i];
        for (var j=0; j<sprint.modules.length; j++) {
            var m = sprint.modules[j];
            if (p.id==m.id) {
                p.module = m;
            }
        }
    }
};

sprint.saveCookies=function() {
    var cend = "; expires=Tue, 31-Dec-2030 23:59:59 GMT; path=/";
    
    document.cookie = 'sprint_enabled=' + sprint.enabled.toString() + cend;
    
    var arr = [];
    for (var i=0; i<sprint.portlets.length; i++) {
        var p = sprint.portlets[i];
        arr[i] = p.id + '|' + p.enabled.toString() + '|' + p.items + '|' + p.refresh;
    }
    document.cookie = 'sprint_portlets='+arr.join('||')+cend;
};

sprint.pause = function () {
    sprint.paused=true;
    p = document.getElementById('sprint_ovl');
    if (p) {
        p.style.display='';
    }
};

sprint.refresh = function () {
    if(sprint.paused) {
        sprint.paused=false;
        p = document.getElementById('sprint_ovl');
        if (p) {
            p.style.display='none';
        }
        sprint.loadCookies();
        sprint.drawSprint();
    }
};

sprint.init = function () {
    sprint.loadCookies();
    mw.util.addPortletLink ('p-personal', '#', 'My Sidebar', 'toggleSprint');
    document.getElementById('toggleSprint').setAttribute('onclick', 'sprint.toggleSprint();return false;');
    sprint.drawSprint();

    window.onblur = sprint.pause;
    window.onfocus = sprint.refresh;
    importStylesheetURI('/skins-1.5/common/diff.css');
};

sprint.drawSprint=function() {
    sb = document.getElementById('sprint');
    if (sprint.enabled) {
        if (sb) {
            sb.style.display='';
        } else {
            //Create the main sprint bar
            sb = document.createElement('div');
            sb.id='sprint';
            sb.className='portlet';
            sb.style.width='12em';
            sb.style.top='0px';
            sb.style.right='0px';
            sb.style.bottom='0px';
            sb.style.zIndex=90;
            sb.style.margin='0';//'.1em';
            sb.style.padding='.5em';
            sb.style.position='absolute';
            sb.style.borderLeft='1px solid #bbbbbb;';
            sb.style.backgroundColor=document.defaultView.getComputedStyle(document.body, null).backgroundColor;
            sb.style.position='fixed';

            //Find the container where the sprint bar goes.
            //This differs between monobook and modern skins.
            container = document.getElementById('content');
            if (!container) {
                container = document.getElementById('mw_content');
            }
            container.appendChild(sb);

            //Create the overlay for when it is paused (out of focus)
            ovl = document.createElement('div');
            ovl.id='sprint_ovl';
            ovl.style.position='absolute';
            ovl.style.top='0px';
            ovl.style.right='0px';
            ovl.style.width='100%';
            ovl.style.height='100%';
            ovl.style.borderLeft='1px solid #bbbbbb;';
            ovl.style.backgroundColor=document.defaultView.getComputedStyle(document.body, null).backgroundColor;
            ovl.zIndex=100;
            ovl.style.display='none';
            
            p = document.createElement('p');
            p.style.position='absolute';
            p.style.top='5em';
            p.style.width='100%';
            p.style.fontSize='1em';
            p.style.lineHeight='2em';
            p.style.textAlign='center';
            p.style.color='#bbbbbb';
            p.appendChild(document.createTextNode('Updates paused'));
            p.appendChild(document.createElement('br'));
            p.appendChild(document.createTextNode('Click to resume'));
            ovl.appendChild(p);
            sb.appendChild(ovl);
        }
        
        //Set the spacer between the main body and the Sprint sidebar
        var spacer = (sb.offsetWidth)+'px';        
        topbar = document.evaluate('//div[@id="p-personal"]//div[@class="pBody"]', 
            document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        if (topbar.singleNodeValue) {
            topbar.singleNodeValue.style.marginRight=spacer;
        }
        sb.parentNode.style.marginRight=spacer;

        //Draw the portlets
        for (var i=0; i<sprint.portlets.length; i++) {
            sprint.drawPortlet(i);
        }
        
    } else { //Hide the sprint bar, if it has already been drawn
        topbar = document.evaluate('//div[@id="p-personal"]//div[@class="pBody"]', 
            document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        if (topbar.singleNodeValue) {
            topbar.singleNodeValue.style.marginRight='';
        }
        if (sb) {
            sb.parentNode.style.marginRight='';
            sb.style.display='none';       
        }
    }
};

//Draw each of the portlets. Called by drawSprint()
sprint.drawPortlet=function(index) {
    if (!sprint.enabled || sprint.paused) return false;
    var ptl = sprint.portlets[index];
    
    sprint.makePortlet(ptl.module.title, ptl.module.titlelink, 'p-sb'+index, ptl.enabled, 'sprint.toggle('+index+')');

    if (ptl.enabled) {
        if (!sprint.ajax) {
            sprint.ajax = [];
        }
        sprint.ajax[index] = sajax_init_object();
        if (sprint.ajax[index]) {
            sprint.ajax[index].onreadystatechange = function() {
                if(sprint.ajax[index].readyState == 4) {sprint.portletCallback(index);}
            };
            
            url = ptl.module.url + ptl.items;
                
            sprint.ajax[index].open("GET", url, true);
            sprint.ajax[index].send(null);
        } else {
            pBody = document.getElementById('p-sb'+index);
            while (pBody.firstChild) {
                pBody.removeChild(pBody.firstChild);
            }                    
        }
    }
};

sprint.portletCallback=function(index) {
    var url = sprint.portlets[index].module.url;
    var tag = sprint.portlets[index].module.tag;

    var items;    
    try {
        items = sprint.ajax[index].responseXML.documentElement.getElementsByTagName(tag);
    } catch (e) {
        setTimeout('sprint.drawPortlet('+index+')', sprint.portlets[index].refresh * 1000);
        return;
    }

    pBody = document.getElementById('p-sb'+index);
    while (pBody.firstChild) {
        pBody.removeChild(pBody.firstChild);
    }
    
    var patrol = (url.indexOf('patrolled')>=0);

    for (var i=0; i<items.length; i++) {
        var title = items[i].getAttribute('title');
        var rcid = items[i].getAttribute('rcid');
        var revid = items[i].getAttribute('revid');
        var type = items[i].getAttribute('type');
        var item_url;
        var not_patrolled=false;

        if (type=='new') {
            if (patrol) not_patrolled = (items[i].getAttribute('patrolled')==null);
            item_url = mw.config.get('wgScript') + '?title=' + title.replace(' ', '_', "g").replace('&', '%26', "g") + '&rcid=' + rcid;//+ '&redirect=no'; (this screws up Friendly)
        } else {
            item_url = mw.config.get('wgScript') + '?title=' + title.replace(' ', '_', "g").replace('&', '%26', "g") + '&oldid=' + revid;
        }

        var a = document.createElement('a');
        a.setAttribute('nopopup', 'true'); //TODO: Test interaction with popups
        a.href=item_url;
        a.title=title;
        a.appendChild(document.createTextNode(title));
        
        var p = sprint.createLine(pBody);
        p.appendChild(a);

        p.appendChild(document.createTextNode(' ['));
        pre = document.createElement('a');
        pre.setAttribute('nopopup', 'true');
        pre.setAttribute('href', '#');
        pre.setAttribute('onclick', 'sprint.showPreview("'+title+'", "'+item_url+'&diff=prev&action=render");return false;');
        pre.appendChild(document.createTextNode('preview'));
        p.appendChild(pre);
        p.appendChild(document.createTextNode(']'));

        if (not_patrolled) {
            p.style.fontWeight='bold';
            p.appendChild(document.createTextNode(' ['));
            ap = document.createElement('a');
            ap.setAttribute('nopopup', 'true');
            ap.setAttribute('href', '#');
            ap.setAttribute('onclick', 'sprint.patrol("'+item_url+'&action=markpatrolled");return false;');
            ap.appendChild(document.createTextNode('patrol'));
            p.appendChild(ap);
            p.appendChild(document.createTextNode(']'));
        }
    }
    
    setTimeout('sprint.drawPortlet('+index+')', sprint.portlets[index].refresh * 1000);
};

sprint.showPreview=function(title, url) {
    var aj = sajax_init_object();
    if (aj) { 
        var wnd = document.createElement('div');
        wnd.id='sprint_previewWindow';
        wnd.style.position='fixed';
        wnd.style.padding='10px';
        wnd.style.zIndex=++m_zIndex;
        wnd.style.backgroundColor='white';
        wnd.style.border='2px solid #aaaaaa';
        wnd.style.width='60em';
        wnd.style.height='30em';
        wnd.style.minWidth='20em';
        wnd.style.minHeight='10em';

//This is where we place the preview window - TODO: Check for modern skin
        var obj = document.getElementById('column-one');
        obj.appendChild(wnd);

        wnd.style.left=parseInt(window.innerWidth-wnd.clientWidth)/2 + 'px';
        wnd.style.top=parseInt(window.innerHeight-wnd.clientHeight)/2 + 'px';

        var hdr = document.createElement('div');
        hdr.style.position='relative';
        hdr.style.width='100%';
        hdr.style.height='2em';
        hdr.style.borderBottom='1px solid #aaaaaa';
        hdr.style.cursor='move';
        wnd.appendChild(hdr);

        var closeButton = document.createElement('a');
        closeButton.href='#';
        closeButton.style.position='absolute';
        closeButton.style.top='10px';
        closeButton.style.right='10px';
        closeButton.onclick = function () {wnd.parentNode.removeChild(wnd); return false;};
        closeButton.innerHTML = '<img src="http://upload.wikimedia.org/wikipedia/commons/thumb/5/52/Nuvola_apps_error.png/18px-Nuvola_apps_error.png"/>';
        wnd.appendChild(closeButton);

        var content = document.createElement('div');
        content.id='sprint_previewContent';
        content.style.position='relative';
        content.style.clear='both';
        content.style.overflow='scroll';
        content.style.height=parseInt(wnd.clientHeight - hdr.offsetHeight - parseInt(wnd.style.padding)*2)+'px';
        content.style.lineHeight='1.5em';
        wnd.appendChild(content);

        wnd.onmousedown=function(event) {
            if (wnd.style.zIndex < m_zIndex) {
                wnd.style.zIndex=++m_zIndex;
            }
        }

        hdr.onmousedown=function(event) {
            wnd.initialX = parseInt( event.clientX - wnd.offsetLeft );
            wnd.initialY = parseInt( event.clientY - wnd.offsetTop );
            window.onmouseup=function(event) {
                window.onmousemove=null;
                window.onmouseup=null;
                wnd.style.opacity='';
                content.style.display='';
            }
            window.onmousemove=function(event) {
                if (!content.style.display) {
                    wnd.style.opacity='.8';
                    content.style.display='none';
                }
                wnd.style.left=event.clientX-wnd.initialX+'px';
                wnd.style.top=event.clientY-wnd.initialY+'px';
            }
        }

        var resize = document.createElement('div');
        resize.id='sprint_previewResize';
        resize.style.position='absolute';
        resize.style.bottom='0px';
        resize.style.right='0px';
        resize.style.height='20px';
        resize.style.width='20px';
        resize.style.cursor='se-resize';
        wnd.appendChild(resize);

        resize.onmousedown=function(event) {
            wnd.initialWidth = parseInt( event.clientX - wnd.offsetWidth );
            wnd.initialHeight = parseInt( event.clientY - wnd.offsetHeight );
            window.onmouseup=function(event) {
                window.onmousemove=null;
                window.onmouseup=null;
                wnd.style.opacity='';
                content.style.height=parseInt(wnd.clientHeight - hdr.offsetHeight - parseInt(wnd.style.padding)*2)+'px';
                content.style.display='';
            }
            window.onmousemove=function(event) {
                if (!content.style.display) {
                    wnd.style.opacity='.8';
                    content.style.display='none';
                }
                wnd.style.width=event.clientX-wnd.initialWidth-parseInt(wnd.style.padding)*2+'px';
                wnd.style.height=event.clientY-wnd.initialHeight-parseInt(wnd.style.padding)*2+'px';
            }
        }

        hdr.innerHTML='<a href="/wiki/'+title+'" style="font-size:1.5em;">'+title+'</a>';
        content.innerHTML='Loading...';
    
        aj.onreadystatechange = function() {
            if(aj.readyState == 4 && aj.status == 200) {
                var htm;    
                htm = aj.responseText;
                content.innerHTML = htm;
                content.id = 'bodyContent'; //TODO: Find a better way to make the page format correctly
            }                      
        }
                
        aj.open("GET", url, true);
        aj.send(null);
    }
};

sprint.patrol=function(url) {
    var myAjax = sajax_init_object();
    myAjax.open("GET", url, true);
    myAjax.send(null);
};

sprint.makePortlet=function(title, titleLink, id, enabled, toggleFunction) {
    var sb = document.getElementById('sprint');
    var a;
    var hdr = document.getElementById('hdr' + id);
    if (hdr) {
        while (hdr.firstChild) {
            hdr.removeChild(hdr.firstChild);
        }
    } else {
        hdr = document.createElement('h5');
        hdr.id = 'hdr' + id;
        sb.appendChild(hdr);
    }
    if (titleLink) {
        var a = document.createElement('a');
        a.setAttribute('nopopup', 'true');
        a.id = 'lnk' + id;
        a.setAttribute('href', titleLink);
        a.appendChild(document.createTextNode(title));
        hdr.appendChild(a);
    } else {
        hdr.appendChild(document.createTextNode(title));
    }
    hdr.appendChild(document.createTextNode(' ['));
    a = document.createElement('a');
    a.setAttribute('nopopup', 'true');
    a.id = 'show' + id;
    a.setAttribute('href', '#');
    a.setAttribute('onclick', toggleFunction+';return false;');
    if (enabled) {
        a.appendChild(document.createTextNode('hide'));
    } else {
        a.appendChild(document.createTextNode('show'));
    }
    hdr.appendChild(a);
    hdr.appendChild(document.createTextNode(']'));
    hdr.appendChild(document.createElement('br'));

    var p = document.getElementById(id);
    if (!p) {
        p = document.createElement('div');
        p.setAttribute('class', 'pBody');
        p.setAttribute('id', id);
        sb.appendChild(p);//, document.getElementById('sprint_ovl'));
    }
    
    if (enabled) {
        p.style.display='';
    } else {
        p.style.display='none';
    }
};

sprint.createLine=function(pBody) {
    var p = document.createElement('p');
    p.style.lineHeight='1em';
    p.style.fontSize='95%';
    p.style.margin='0px';
    p.style.padding='.1em';
    pBody.appendChild(p);
    return p;
};

sprint.toggleSprint=function() {
    sprint.enabled = !sprint.enabled;
    sprint.saveCookies();
    sprint.drawSprint();
};

sprint.toggle=function(index) {
    sprint.portlets[index].enabled = !sprint.portlets[index].enabled;
    sprint.saveCookies();
    sprint.drawPortlet(index);
};

sprint.readCookie=function(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0;i < ca.length;i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') { c = c.substring(1,c.length); }
        if (c.indexOf(nameEQ) === 0) { return c.substring(nameEQ.length,c.length); }
    }
    return '';
};
$(sprint.init);