Chat Control Panel

Adds a small panel to the top of the chat window with a list of macros, allowing you to fire chat macros with mouse actions.

// ==UserScript==
// @name           Chat Control Panel
// @namespace      kol.interface.unfinished
// @description    Adds a small panel to the top of the chat window with a list of macros, allowing you to fire chat macros with mouse actions.
// @include        https://*kingdomofloathing.com/lchat.php*
// @include        https://*kingdomofloathing.com/mchat.php*
// @grant          GM_getValue
// @grant          GM_setValue
// @version        2.021
// ==/UserScript==

//Version 2.021
// - trying to correct some spacing issues
//Version 2.02
// - change to https
//Version 2.01
// - add @grant, convert GM_log to console.log
//Version 2.0
// - modified heavily to work with the new tabbed chat.  This 
//   changes the way the docking works, and there is now an
//   icon on the top right corner that toggles the appearance
//   of the panel instead of the bar on the right.
//Version 1.0.3
// - more minor interface changes
//   - resizing is now granular, always shrinking or growing by 
//     a full entry.
//   - the toggle bar now stays on the right of the chat control
//     panel so you can quickly toggle on/off without moving
//     the mouse.
//   - the control panel sticks to the top when moved very close
//     to the top of the chat pane.
//Version 1.0.2
// - a minor aesthetic change, using partial transparency to 
//   hide slightly less of the underlying chap panel text.
//Version 1.0.1
// - fix mouse-over message for resize dragging
//Version 1.0
// - changed the docking method, so you now click on a bar 
//   on the right to dock it on the right
// - added a pull bar next to the docking bar so you can 
//   slide it anywhere in the chat pane
// - the former docking tab (line) at the bottom is now a 
//   pull tab so you can resize it to as many entries as 
//   you want (or can fit).
//Version 0.9

var height=unitHeight(5);
var ccmds;
var playername;

var dims;

function rollHandler() {
    var cp = document.getElementById('chatcp');
    var cph = document.getElementById('chatcp_handle');
    if (cp && cph) {
        var v = cp.getAttribute('style');
        var w = cph.getAttribute('style');
        if (v.match(/display\s*:\s*none/)) {
            cp.setAttribute('style',removeStyle(v,'display'));
            cph.setAttribute('style',removeStyle(w,'display'));
            saveDock(false);
        } else {
            cp.setAttribute('style','display:none;'+v);
            cph.setAttribute('style','display:none;'+w);
            saveDock(true);
        }

    }
}


function replaceOrAddStyle(s,a,v) {
    var r = new RegExp(a+'\\s*:','i');
    var sa = s.split(';');
    for (var i=0;i<sa.length;i++) {
        var x = sa[i].match(r);
        if (x) {
            sa[i] = a+':'+v;
            return sa.join(';');
        }
    }
    return a+':'+v+';'+s;
}

function removeStyle(s,a) {
    var r = new RegExp(a+'\\s*:','i');
    var sa = s.split(';');
    for (var i=0;i<sa.length;i++) {
        var x = sa[i].match(r);
        if (x) {
            sa[i] = '';
            return sa.join(';').replace(';;',';');
        }
    }
    return s;
}

function getStyleAttr(s,a) {
    var r = new RegExp('(^|;\\s*)'+a+'\\s*:\\s*([0-9]+)','i');
    var x = s.match(r);
    if (x)
        return Number(x[2]);
    //console.log('could not find style: "' +a+'" in "'+s+'"');
    return -1;
}

function setTopBorder(s) {
    return replaceOrAddStyle(s,'border-top','1px solid black');
}

function removeTopBorder(s) {
    return removeStyle(s,'border-top');
}

function setWidths(e) {
    getMaxDimensions();
    var cp = document.getElementById('chatcp');
    var cph = document.getElementById('chatcp_handle');
    var cpd = document.getElementById('chatcp_dock');
    if (cp && cph && cpd) {
        var w = (dims.headerw) ? dims.headerw : dims.width; //document.body.clientWidth-19;
        //console.log("Setting width to "+w);
        cph.setAttribute('style',cph.getAttribute('style').replace(/width\s*:\s*[0-9;px]*/,'width:'+(w+1)+'px;'));
        cp.setAttribute('style',cp.getAttribute('style').replace(/width\s*:\s*[0-9;px]*/,'width:'+(w+1)+'px;'));
        if (dims.headerw) 
            cpd.setAttribute('style',cpd.getAttribute('style').replace(/left\s*:\s*[0-9;px]*/,'left:'+(dims.headerw-15)+'px;'));
        else
            cpd.setAttribute('style',cpd.getAttribute('style').replace(/left\s*:\s*[0-9;px]*/,'left:'+(w-15)+'px;'));
        var texts = document.getElementsByClassName('chatcp_text');
        for (var i=0;i<texts.length;i++) {
            texts[i].setAttribute('style',texts[i].getAttribute('style').replace(/width\s*:\s*[0-9;px]*/,'width:'+(w-56)+'px;'));
        }
    }
}

function tHandler(e) {
    var ci = this.getAttribute('cmdidx');
    ccmds[ci] = this.value;
    saveTable(ccmds);
    if (e.keyCode==13)
        insertCmd(ccmds[ci]);
}


function insertCmd(c) {
    if (c)
        unsafeWindow.submitchat(c);
}

function bHandler() {
    insertCmd(ccmds[this.getAttribute('cmdidx')]);
}


function addPanel() {
    height = restoreHeight();
    ccmds = restoreTable();
    var f = document.getElementById('ChatWindow');
    var cp = document.getElementById('chatcp');
    if (f && !cp) {
        placeDockIcon(f);
        cp = document.createElement('div');
        var w = (dims.headerw) ? dims.headerw : dims.width;//document.body.clientWidth-19;
        var p = restorePos();
        cp.setAttribute('style','height:'+height+'px;width:'+w+'px;'+((p>0)?'border-top:1px solid black;':'')+'background-image:-moz-linear-gradient(center top , white 50%, transparent 100%);z-index:7;font-size:12px;position:absolute;font-family:arial;left:2px;top:'+p+'px;');
        cp.setAttribute('id','chatcp');

        var t = document.createElement('table');
        t.setAttribute('cellspacing','0');
        for(var i=0;unitHeight(i)<height;i++) {
            newEntry(i,w-40,t);
        }

        var tt = document.createElement('table');
        tt.setAttribute('style','vertical-align:top;');
        var tr = document.createElement('tr');
        var td = document.createElement('td');
        tt.setAttribute('cellspacing','0');
        tt.setAttribute('cellpadding','0');
        td.setAttribute('style','vertical-align:top;');
        td.appendChild(t);
        tr.appendChild(td);
        td = document.createElement('td');
        var span = document.createElement('div');
        span.setAttribute('style','background:lightgray;height:'+(height-4)+'px;width:15px;position:relative;top:2px;');
        span.setAttribute('title','Click and drag to position the chat control panel.');
        span.setAttribute('class','chatcpbar');
        td.appendChild(span);
        tr.appendChild(td);
        tt.appendChild(tr);

        cp.appendChild(tt);

        var handle = document.createElement('div');
        handle.setAttribute('style','height:7px;width:'+(w+1)+'px;text-align:center;vertical-align:middle;border-bottom:1px solid black;background-color:transparent;z-index:8;font-size:12px;position:absolute;font-family:arial;left:2px;top:'+(p+height-3)+'px;');
        handle.setAttribute('id','chatcp_handle');
        handle.setAttribute('title','Click and drag to resize the chat control panel.');
        var a = document.createElement('img');
        a.setAttribute('src',"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%1E%00%00%00%05%08%02%00%00%00%DDC%CB%AD%00%00%00%01sRGB%00%AE%CE%1C%E9%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%00%07tIME%07%DA%0C%17%002%18%89S%DB%E9%00%00%00%19tEXtComment%00Created%20with%20GIMPW%81%0E%17%00%00%00%16IDAT%18%D3c%60%A0%19%60%FC%FF%FF%3F%C3%90%03C%D3%D5%00W%E9%05%FD%13%F9%A0%92%00%00%00%00IEND%AEB%60%82");
        handle.appendChild(a);

        //handle.addEventListener('click',rollHandler,false);
        handle.addEventListener('mousedown',dragStartSize,false);
        cp.addEventListener('mousedown',dragStart,true);
        
        f.parentNode.insertBefore(cp,f);
        f.parentNode.insertBefore(handle,f);

        //cp.appendChild(handle);
        window.addEventListener('resize',setWidths,false);

        if (restoreDock())
            rollHandler();
        setWidths();
    }
}

function unitHeight(u) {
    return u*27+5;
}

function heightUnit(h) {
    return Math.floor((h-5)/27);
}

function saveTable(t) {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        GM_setValue(pn+'_cmds',t.join('#####'));
    }
}

function saveHeight(h) {
    height = h;
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        GM_setValue(pn+'_cpheight',h);
    }
}

function restoreHeight() {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        return Math.ceil(Number(GM_getValue(pn+'_cpheight',unitHeight(5))));
    }
    return Math.ceil(unitHeight(5));
}

function saveDock(b) {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        GM_setValue(pn+'_docked',b);
    }
}

function restoreDock() {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        return Boolean(GM_getValue(pn+'_docked',false));
    }
}

function restoreTable() {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        var t = GM_getValue(pn+'_cmds','');
        if (t) {
            t = t.split('#####');
            return t;
        }
    }
    return ['Good morning clan-mates!', '/unequip all', '', '', ''];
}


function savePos(y) {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        GM_setValue(pn+'_pos',y);
    }
}

function restorePos() {
    var pn = getPlayerNameFromCharpane();
    if (pn) {
        var t = Number(GM_getValue(pn+'_pos',2));
        return t;
    }
    return 2;
}


// utility to get player name; hacked/stolen from Antimarty's fortune cookie script
function getPlayerNameFromCharpane() {
    var somef=window.parent.frames;
    var goo;
    for(var j=0;j<somef.length;j++) {
        if (somef[j].name=="charpane") {
            goo=somef[j];
            var username = goo.document.getElementsByTagName("b");
            if (!username || username.length < 1) return playername;
            username = username[0];
            if (!username) return playername;
            username = username.firstChild;
            if (!username) return playername;
            // in full mode the link is <a><b>Name</b></a>
            // in compact mode it's <b><a>Name</a></b>
            // so have to handle this, and also can use it to tell
            // whether it's in compact mode or not.
            var fullmode = true;
            while (username && username.nodeType == 1)
                {
                    username = username.firstChild;
                    fullmode = false;
                }
            if (!username) return playername;
            username = username.nodeValue;
            if (!username) return playername;
            username = username.toLowerCase();
            playername = username;
            return username;
        }
    }
}


// code to drag the panel around
var d;
function dragStart(event, id) {
    var cp = document.getElementById('chatcp');
    var cph = document.getElementById('chatcp_handle');
    if (cp && cph) {
        var v = getStyleAttr(cp.getAttribute('style'),'width');
        if (v>=0) {
            if (event.clientX<v-50)
                return;
        }

        d = new Object();
        d.cp = cp;
        d.cph = cph;
        var x = event.clientX + window.scrollX;
        var y = event.clientY + window.scrollY;

        // Save starting positions of cursor and element.
        d.cursorStartX = x;
        d.cursorStartY = y;
        d.elStartLeft = parseInt(cp.style.left, 10);
        d.elStartTop = parseInt(cp.style.top, 10);

        d.baseH = getStyleAttr(cp.getAttribute('style'),'height');

        if (isNaN(d.elStartLeft)) d.elStartLeft = 2;
        if (isNaN(d.elStartTop)) d.elStartTop = 2;

        document.addEventListener("mousemove", dragGo, true);
        document.addEventListener("mouseup", dragStop, true);
        event.preventDefault();
    }
}

// drag handler for positioning the panel
function dragGo(event)  {
    var x, y;
    // Get cursor position with respect to the page.
    y = event.clientY + window.scrollY;

    //d.elNode.style.left = (d.elStartLeft + x - d.cursorStartX) + "px";
    if ((d.elStartTop + y - d.cursorStartY)>=(dims.headerw? (dims.headert+16) : (dims.top+16)) && (d.elStartTop + y - d.cursorStartY)<(dims.height+(dims.headerw? (dims.headert+16) : (dims.top+16)))-height) {
        if ((d.elStartTop + y - d.cursorStartY)<=3)
            y = d.cursorStartY - d.elStartTop;
        d.cp.style.top = (d.elStartTop + y - d.cursorStartY) + "px";
        if ((d.elStartTop + y - d.cursorStartY)>0)
            d.cp.setAttribute('style',setTopBorder(d.cp.getAttribute('style')));
        else
            d.cp.setAttribute('style',removeTopBorder(d.cp.getAttribute('style')));
        d.cph.style.top = (d.elStartTop + y - d.cursorStartY + d.baseH - 3) + "px";
        savePos(d.elStartTop + y - d.cursorStartY);
    }
    event.preventDefault();
}

// terminate dragging to position the panel
function dragStop(event)  {
    // Stop capturing mousemove and mouseup events.
    document.removeEventListener("mousemove", dragGo, true);
    document.removeEventListener("mouseup", dragStop, true);
    d = null;
}


// code to resize the panel
function dragStartSize(event, id) {
    var cp = document.getElementById('chatcp');
    var cph = document.getElementById('chatcp_handle');
    if (cp && cph) {
        d = new Object();
        d.cp = cp;
        d.cph = cph;
        var y = event.clientY + window.scrollY;
        d.baseH = getStyleAttr(cp.getAttribute('style'),'height');
        d.baseO = getStyleAttr(cph.getAttribute('style'),'top');

        // Save starting positions of cursor and element.
        d.cursorStartY = y;
        d.elStartLeft = parseInt(cp.style.left, 10);
        d.elStartTop = parseInt(cp.style.top, 10);

        if (isNaN(d.elStartLeft)) d.elStartLeft = 2;
        if (isNaN(d.elStartTop)) d.elStartTop = 2;

        document.addEventListener("mousemove", dragGoSize, true);
        document.addEventListener("mouseup", dragStopSize, true);
        event.preventDefault();
    }
}

// create cp index i in table t with width w
function newEntry(i,w,t) {
    var tr = document.createElement('tr');
    tr.setAttribute('id','chatcp_index'+i);
    var td = document.createElement('td');
    var i1 = document.createElement('input');
    i1.setAttribute('type','button');
    //i1.setAttribute('value','>');
    //var arrow = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAMCAYAAACEJVa/AAAAmklEQVQokaXSPQ5BURCG4YcNiGhUGpah0CpFQyOswUpElCLR24VFiJ8FqCQ6Ea7CkdwILseXvN3Mm5nJkJ0mFih8UftRcsIO9XdFuQyghBUSDJB/lky+YIwZrkE0RTEt2fzAOqyVYItyjCTNMcga/0guWKIm2GKYpW/SyaCLNkbhsAf0RaSFM/aoxAiG7uPPY5ofqaLnxYM9cgOSr1GycHbzaQAAAABJRU5ErkJggg==';
    //i1.setAttribute('style','font-size:9px;width:22px;');
    i1.setAttribute('style','background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAMCAYAAACEJVa/AAAAmklEQVQokaXSPQ5BURCG4YcNiGhUGpah0CpFQyOswUpElCLR24VFiJ8FqCQ6Ea7CkdwILseXvN3Mm5nJkJ0mFih8UftRcsIO9XdFuQyghBUSDJB/lky+YIwZrkE0RTEt2fzAOqyVYItyjCTNMcga/0guWKIm2GKYpW/SyaCLNkbhsAf0RaSFM/aoxAiG7uPPY5ofqaLnxYM9cgOSr1GycHbzaQAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: center;');
    i1.setAttribute('cmdidx',i);
    i1.addEventListener('mouseup',bHandler,false);
    var i2 = document.createElement('input');
    i2.setAttribute('type','text');
    i2.setAttribute('width','128');
    i2.setAttribute('value',((i>=ccmds.length)?'':ccmds[i]));
    i2.setAttribute('class','chatcp_text');
    i2.setAttribute('style','font-size:9px;width:'+(w-22)+'px;height:13px;');
    i2.setAttribute('cmdidx',i);
    i2.addEventListener('keyup',tHandler,true);
    i2.addEventListener('change',tHandler,true);
    td.appendChild(i1);
    td.appendChild(i2);
    tr.appendChild(td);
    t.appendChild(tr);
}

// update the interior of the panel during resizing
function updateCPSize(h) {
    var bars = document.getElementsByClassName('chatcpbar');
    for (var i=0;i<bars.length;i++) {
        var s = bars[i].getAttribute('style');
        bars[i].setAttribute('style',replaceOrAddStyle(s,'height',(h-4)+'px'));
    }
    var last = heightUnit(h);
    var ins = document.getElementById('chatcp_index'+last);
    if (!ins) {
        // we must have an entry 0
        var w = document.getElementById('chatcp_index0');
        if (w) {
            var t = w.parentNode;
            w = getStyleAttr(w.firstChild.firstChild.nextSibling.getAttribute('style'),'width');
            for (var i=1;i<last;i++) {
                ins = document.getElementById('chatcp_index'+i);
                if (!ins) {
                    newEntry(i,(w+16),t);
                }
            }
        }
    } else {
        var del = document.getElementById('chatcp_index'+last);
        while (del) {
            del.parentNode.removeChild(del);
            last++;
            del = document.getElementById('chatcp_index'+last);
        }
    }
    saveHeight(h);
}

// drag handler for resizing the panel
function dragGoSize(event)  {
    var y;
    // Get cursor position with respect to the page.
    y = event.clientY + window.scrollY;

    var delta = y - d.cursorStartY;
    if (delta!=0) {
        var newh = d.baseH + delta;
        if (newh>unitHeight(1) && newh<(dims.height+(dims.headerw? (dims.headert+16) : (dims.top+16)))) {
            newh = unitHeight(heightUnit(newh));
            delta = newh - d.baseH;
            d.cp.setAttribute('style',replaceOrAddStyle(d.cp.getAttribute('style'),'height',newh+'px'));
            d.cph.setAttribute('style',replaceOrAddStyle(d.cph.getAttribute('style'),'top',(d.baseO+delta)+'px'));
            updateCPSize(newh);
        }
    }
    event.preventDefault();
}

// terminate dragging to resize the panel
function dragStopSize(event)  {
    // Stop capturing mousemove and mouseup events.
    document.removeEventListener("mousemove", dragGoSize, true);
    document.removeEventListener("mouseup", dragStopSize, true);
    d = null;
}

function getMaxDimensions() {
    var chats = document.getElementsByClassName('chatdisplay');
    for (var i=0;i<chats.length;i++) {
        if (chats[i].style.display!='none') {
            var header = document.evaluate('.//div[@class="header"]',chats[i],null,XPathResult.FIRST_ORDERED_NODE_TYPE,null);
            dims = {top:Number(chats[i].style.top.replace(/[^0-9]+/,'')),width:Number(chats[i].clientWidth),height:Number(chats[i].style.height.replace(/[^0-9]+/,''))};
            if (header.singleNodeValue) {
                dims.headert = Number(header.singleNodeValue.style.top.replace(/[^0-9]+/,''));
                dims.headerw = Number(header.singleNodeValue.style.width.replace(/[^0-9]+/,''));
            }
            //console.log('dimensions: top='+dims.top+', width='+dims.width+', height='+dims.height+', offw='+chats[i].offsetWidth+', cw='+chats[i].clientWidth);
			chats[i].addEventListener('scroll',setWidths,false);
            return;
        }
    }
}

function placeDockIcon(f) {
    var img=document.createElement('img');
    img.setAttribute('src',"data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%0E%00%00%00%0C%08%06%00%00%00R%80%8C%DA%00%00%00%01sRGB%00%AE%CE%1C%E9%00%00%00%06bKGD%00%00%00%00%00%00%F9C%BB%7F%00%00%00%09pHYs%00%00%0B%13%00%00%0B%13%01%00%9A%9C%18%00%00%00%07tIME%07%DC%03%1E%14)%06%E8D%1D%FA%00%00%00%19tEXtComment%00Created%20with%20GIMPW%81%0E%17%00%00%00%5BIDAT(%CF%ED%8F%A1%11%C0%20%10%04%97L%8Ay%85%A0%8F%EF%80b%9E%8EPt%81%A7%10%FC%23%A228TD%D6%9C%DA%99%DB%E0%EE%CE%017%40%CE%99Z%2BsNJ)%00%98%19%00)%A5M%EA%BD%3Fb%8C%91%D6%1A%00%AA%FA%DA1%06%22%B2%C9%E1%F4%EA%C5!%7F%E3%A7%1A%17%CC%8BJ%7F%13%D8%1D%F7%00%00%00%00IEND%AEB%60%82");
    if (dims.headerw) 
        img.setAttribute('style','position:absolute;top:'+(dims.headert+1)+'px;left:'+(dims.headerw-15)+'px;z-index:7;');
    else
        img.setAttribute('style','position:absolute;top:'+(dims.top+2)+'px;left:'+(dims.width-15)+'px;z-index:7;');
    img.setAttribute('title','Click to toggle display of chat control panel.');
    img.setAttribute('id','chatcp_dock');
    img.addEventListener('click',rollHandler,false);
    f.parentNode.insertBefore(img,f);
}


function initialize() {
    getMaxDimensions();
    if (dims) {
        addPanel();
    }
}

setTimeout(initialize,50);