/* dcwebui.js */ //;(function() { "use strict"; var $ = (document.querySelectorAll ? function(s) { var r = document.querySelectorAll(s); return (s[0] === '#' && r.length === 1) ? r[0] : r; } : function(s) { // i'm not writing a selector engine... if (! s.length) return []; if (s[0] === '#') { return document.getElementById(s.slice(1)); } else if (s[0] === '.') { return document.getElementsByClassName(s.slice(1)); } else { return document.getElementsByTagName(s); } } ); var nmdc_escape = function(str) { return ( (''+str).length ? (''+str).replace(/&/g,'&').replace(/\|/g,'|').replace(/\$/g,'$') : ' ' ); }; var hesc = function(s) { var filter = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' }; return s.toString().replace(/[&<>'"]/g, function(s) { return filter[s]; }); }; var fmtBytes = function(b) { if (b == 0) { return '(nothing)'; } var k = 1024; var sizes = [' B', ' KiB', ' MiB', ' GiB', ' TiB']; var i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(3)) + sizes[i]; }; var linkify = function(str) { return str.replace( /(https?:\/\/[^\s<]+)/g, "$1" ); }; var sanitise = function(s) { return linkify(hesc(s)); }; var toggle = function($el) { $el.style.display = ($el.style.display === "block") ? "none" : "block"; }; var textContent = function($el) { if ($el.textContent) return $el.textContent; if ($el.innerText) return $el.innerText; return ""; }; // @ref https://developer.mozilla.org/en/docs/Web/API/WindowBase64/Base64_encoding_and_decoding var b64 = function(str) { return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { return String.fromCharCode('0x' + p1); })).replace(/=/g, ''); } // https://gist.github.com/eligrey/1276030 var appendInnerHTML = function($el, html) { var child = document.createElement("span"); child.innerHTML = html; var node; while ((node = child.firstChild)) { $el.appendChild(node); } }; // http://stackoverflow.com/a/5598797 function getOffsetLeft( elem ) { var offsetLeft = 0; do { if (!isNaN(elem.offsetLeft)) { offsetLeft += elem.offsetLeft; } } while (elem = elem.offsetParent); return offsetLeft; } function getOffsetTop( elem ) { var offsetTop = 0; do { if (!isNaN(elem.offsetTop)) { offsetTop += elem.offsetTop; } } while (elem = elem.offsetParent); return offsetTop; } /* Tab writers */ var write = function(tab) { var $tab = $('#inner-'+tab); return { 'cls': function() { $tab.innerHTML = ''; return this; }, 'scroll': function() { $tab.scrollTop = $tab.scrollHeight; return this; }, 'raw': function(s) { appendInnerHTML($tab, s); return this.scroll(); }, 'c': function(c, s) { return this.raw(''+s+''); }, 'time': function() { var d = new Date(); var pad = function(s) { return (s < 10) ? '0'+s : ''+s ; }; return this.raw( '['+ pad(d.getHours()) + ":" + pad(d.getMinutes())+ "] " ); }, 'system': function(s) { return this.time().c('tx-sys', sanitise(s)).raw('
'); }, 'pubnick': function(u) { return this.raw('<'+hesc(u)+'>'); }, 'pub': function(u, s) { return this.time(). pubnick(u).raw(' '). c('tx-chat', sanitise(s)).raw('
'); } }; }; /* Userlist */ var switchToPM = function(u) { writerFor(u); // create tab_set(pm_tabs[u]); // switch writerFor(u).scroll(); // scroll }; var userMenu = function(u, ev) { usermenu.hide(); usermenu = new MenuList(ev.target); usermenu.add("Send private message...", function() { switchToPM(u); }); // Usercommands for (var i = 0; i < user_usercommands.length; i++) (function(i) { var raw = user_usercommands[i].raw; usermenu.add(user_usercommands[i].title, function() { var message = usercommand_process( raw .replace(/%\[nick\]/g, u) ); sock.emit('raw', {message: message}); }); })(i); // Show usermenu.show(); // ev.preventDefault(); return false; }; var userlist = { 'add': function(u) { if (this.has(u)) return; var userlists = $(".userlist"); for (var l = 0, e = userlists.length; l !== e; ++l) { var userlist = userlists[l]; var to_add = document.createElement('li'); to_add.className = "user-" + b64(u); to_add.innerHTML = hesc(u); to_add.onclick = function() { switchToPM(u); }; to_add.oncontextmenu = function(ev) { return userMenu(u, ev); }; var users = userlist.children; var cmp = hesc(u).toUpperCase(); var found = false; for (var i = 0; i < users.length; i++) { if ((''+users[i].innerHTML).toUpperCase() > cmp) { userlist.insertBefore(to_add, users[i]); found = true; break; } } if (! found) { userlist.appendChild(to_add); } } return this; }, 'del': function(u) { var userlists = $(".userlist"); for (var l = 0, e = userlists.length; l !== e; ++l) { if (! userlists[l].children) continue; var userlist = userlists[l]; var users = userlist.children; var cmp = hesc(u).toUpperCase(); for (var i = 0; i < users.length; i++) { if ((''+users[i].innerHTML).toUpperCase() === cmp) { userlist.removeChild(users[i]); break; } } } return this; }, 'clear': function() { var userlists = $(".userlist"); for (var i in userlists) { if (! userlists[i].children) continue; var userlist = userlists[i]; var users = userlist.children; for (var j = users.length; j --> 0;) { userlist.removeChild(users[j]); } } return this; }, 'names': function() { var userlist = $(".userlist")[0].children; var ret = []; for (var i = 0, e = userlist.length; i < e; ++i) { ret.push( textContent(userlist[i]) ); } return ret; }, 'has': function(u) { return $(".user-" + b64(u)).length !== 0; /* there are two - large and non-large */ }, 'count': function() { return $(".userlist")[0].children.length; }, 'setInfo': function(nick, props) { console.log([nick, props]); var baseClass = "user-" + b64(nick); var $el = $("." + baseClass); var prop_str = []; if (props.Description.length > 0) { prop_str.push(props.Description); } if (props.Email.length > 0) { prop_str.push(props.Email); } if (props.ClientTag.length > 0) { prop_str.push(props.ClientTag + " " + props.ClientVersion); } prop_str.push("Sharing " + fmtBytes(props.ShareSize)); for (var i = 0; i < $el.length; ++i) { $el[i].title = prop_str.join("\n"); if (props.IsOperator) { $el[i].className = baseClass + " user-is-operator"; } else { $el[i].className = baseClass; // remove op flag } } } }; var submit = function() { var str = $("#chatbox").value; if (! str.length) return; if (hub_state === 0) { hub_state = 1; persistence_set("login", str); var name_parts = str.split(":", 2); hub_last_nick = name_parts[0]; sock.emit('hello', {'nick' : hub_last_nick, 'pass' : name_parts.length >= 2 ? name_parts[1] : ''}); $("#chatbox").disabled = true; write("tab-main").system("Connecting..."); } else if (hub_state === 2) { if (pm_target !== false) { sock.emit('priv', {'user': pm_target, 'message': str}); writerFor(pm_target).pub(hub_last_nick, str ); } else { sock.emit('pub', {'message' : str}); } } else { write("tab-main").system("Invalid internal state."); } $("#chatbox").value = ''; }; /* tabs */ /** * Switch active tab * * @param {String} tab Full tab ID (e.g. tab-main, tab-users, tab-ext-???) */ var tab_set = function(tab) { var tabs = $(".tabpane"); for (var i in tabs) { try { tabs[i].style.display = (tabs[i].id === tab ? 'block' : 'none'); } catch (e) {}; } var tabitems = $(".tabitem"); for (var i in tabitems) { try { tabitems[i].className = "tabitem" + (tabitems[i].getAttribute('data-tab') === tab ? ' selected' : ''); } catch (e) {}; } pm_target = false; for (var i in pm_tabs) { if (pm_tabs[i] === tab) { pm_target = i; break; } } updateTitle(); write(tab).scroll(); $("#chatbox").focus(); last_tab = tab; }; var tab_new = function(id, name) { appendInnerHTML($("#bar"), '
'+ ''+ hesc(name)+ ' '+ '×'+ '
' ); appendInnerHTML($("#extratabs"), ' ' ); tab_addHandlers(); return "tab-ext-"+id; }; var tab_free = function(id) { if (id === "tab-main") return; // remove tab item and body var el = $("#tabitem-"+id); el.parentNode.removeChild(el); var el = $("#"+id); el.parentNode.removeChild(el); // clear from PM tabs for (var i in pm_tabs) { if (pm_tabs[i] === id) { pm_tabs[i] = false; } } // maybe clear the 'new pm' warning updateTitle(); // don't leave us with no tab displayed if (last_tab === id) { tab_set("tab-main"); } }; var tab_addHandlers = function() { var tabitems = $(".tabitem"); for (var i = 0; i < tabitems.length; i++) { if (! tabitems[i]) continue; tabitems[i].onclick = function(ev) { tab_set( this.getAttribute('data-tab') ); // 360nobubble if (ev.stopPropagation) { ev.stopPropagation(); } else { ev.cancelBubble = true; // oldIE } return false; }; } var tabclosers = $(".tab-closer"); for (var i = 0; i < tabclosers.length; i++) { if (! tabclosers[i]) continue; tabclosers[i].onclick = function(ev) { tab_free( this.getAttribute('data-tab') ); // 360nobubble if (ev.stopPropagation) { ev.stopPropagation(); } else { ev.cancelBubble = true; // oldIE } return false; }; } }; /* */ var writerFor = function(username) { var tabid = ""; if (! username in pm_tabs || ! pm_tabs[username]) { tabid = tab_new(next_tabid++, username); pm_tabs[username] = tabid; } else { tabid = pm_tabs[username]; } return write(tabid); }; /* */ var tabcomplete_state = ''; var tabcompletion_start = function() { var cursor = $("#chatbox").value.replace(/^.*\s([^\s]+)$/, '$1'); if (tabcomplete_state === '') { // new tab completion tabcomplete_state = cursor; } // Find all users who start with tabcomplete_state and retrieve the first // one after cursor var users = userlist.names(); var match = []; for (var i = 0, e = users.length; i !== e; ++i) { if (users[i].toUpperCase().indexOf(tabcomplete_state.toUpperCase()) === 0) { match.push(users[i]); } } if (match.length === 0) { // no matches return; } // Is cursor in the list? var cpos = -1; for (var i = 0, e = match.length; i !== e; ++i) { if (match[i] === cursor) { cpos = i; break; } } var targetName = match[(i + 1) % match.length]; // Replace in textbox var chatprefix = $("#chatbox").value.substr(0, $("#chatbox").value.length - cursor.length); $("#chatbox").value = chatprefix + targetName; $("#chatbox").focus(); }; var tabcompletion_inactive = function() { tabcomplete_state = ''; }; /* */ var MenuList = function(el) { this.el = el; this.div = document.createElement("div"); this.div.classList.add("menu"); this.div.style.position = "absolute"; this.ul = document.createElement("ul"); this.div.appendChild(this.ul); document.body.appendChild(this.div); }; MenuList.prototype.clear = function() { while (this.ul.children.length) { this.ul.children[0].parentNode.removeChild( this.ul.children[0] ); } }; MenuList.prototype.add = function(txt, cb) { var li = document.createElement("li"); li.innerHTML = txt; li.onclick = cb; this.ul.appendChild(li); }; MenuList.prototype.show = function() { this.div.style.display = "block"; this.div.style.top = (getOffsetTop(this.el) + this.el.clientHeight)+"px"; this.div.style.left = (getOffsetLeft(this.el) - 200 + this.el.clientWidth)+"px"; }; MenuList.prototype.hide = function() { this.div.style.display = "none"; }; MenuList.prototype.toggle = function() { // ES5 strict mode sets `this` to undefined (this.div.style.display === "block" ? this.hide : this.show).call(this); }; /* */ var menu = new MenuList($("#menubutton")); menu.reset = function() { this.clear(); this.add(joinparts_getstr(), toggle_joinparts); }; var usermenu = new MenuList(document.body); /** * Process all substitutions in a usercommand. May prompt the user for input. * * @param {String} str * @returns {String} */ var usercommand_process = function(str) { var message = str .replace(/%\[mynick\]/g, hub_last_nick) ; var match = null; for(;;) { match = message.match(/%\[line:([^\]]*)\]/); if (match === null) break; var res = prompt(match[1]); if (res === null) return; //cancelled message = message.substr(0, match.index) + nmdc_escape(res) + message.substr(match.index + match[0].length) ; } return message; }; var user_usercommands = []; /** * Called when a $UserCommand is recieved from the server. Register it in all * relevant context menus. * * @param {Object} data Has properties type, context, title, raw * @returns {undefined} */ var process_usercommand = function(data) { switch(data.type) { case 0: { /* USERCOMMAND_TYPE_SEPARATOR */ // ignore } break; case 1: /* USERCOMMAND_TYPE_RAW */ case 2:/* USERCOMMAND_TYPE_NICKLIMITED */ { if (data.context & 1) { /* USERCOMMAND_CONTEXT_HUB */ menu.add(data.title, function() { sock.emit('raw', {message: usercommand_process(data.raw)}); }); } if (data.context & 2) { /* USERCOMMAND_CONTEXT_USER */ user_usercommands.push(data); } } break; case 255: { /* USERCOMMAND_TYPE_CLEARALL */ if (data.context & 1) { menu.reset(); } if (data.context & 2) { user_usercommands = []; // clear } } break; } }; var joinparts_getstr = function() { return (show_joins ? "☑" : "☐") + " Show Joins/Parts"; }; var toggle_joinparts = function(ev) { var $el = ev.target || ev.srcElement; show_joins = ! show_joins; persistence_set("show_joins", show_joins); $el.innerHTML = joinparts_getstr(); }; /* */ var updateTitle = function() { document.title = ($(".unread").length ? "[NEW PM] " : "") + "("+userlist.count()+") "+ hub_hubname; }; var sock = {}; var hub_state = 0; // [disconnected, sent-nick, connected] var hub_last_nick = ''; var hub_hubname = DCWEBUI_CONF.title; var pm_tabs = {}; // nick => tabid var next_tabid = 1; var pm_target = false; var last_tab = "tab-main"; var show_joins = false; /* */ var persistence_set = function(key, value) { if (window.localStorage) { window.localStorage[key] = JSON.stringify(value); } }; var persistence_get = function(key, fallback) { try { return JSON.parse( window.localStorage[key] ); } catch (ex) { return fallback; } }; window.onload = function() { write("tab-main").system("Communicating with server..."); $("#chatbox").value = persistence_get("login", ""); show_joins = persistence_get("show_joins", false); document.title = DCWEBUI_CONF.title; // HTML event handlers $("#form-none").onsubmit = function(ev) { submit(); // don't submit form ev.preventDefault(); return false; }; $("#chatbox").onkeydown = function(ev) { if (ev.keyCode === 9) { // tab tabcompletion_start(); ev.preventDefault(); return false; } else { tabcompletion_inactive(); } }; window.onresize = function() { if (last_tab === "tab-users" && document.documentElement.clientWidth >= 590) { tab_set("tab-main"); } menu.hide(); usermenu.hide(); }; $("#menubutton").onclick = function(ev) { menu.toggle(); ev.preventDefault(); ev.stopPropagation(); return false; }; window.onclick = function() { menu.hide(); usermenu.hide(); }; menu.reset(); tab_addHandlers(); // Hacks for WiiU user-agent if (navigator && navigator.userAgent && !!navigator.userAgent.match("Nintendo WiiU")) { document.body.className += " navigator-wiiu"; } // Socket event handlers sock = io.connect(DCWEBUI_CONF.extern); sock.on('cls', function() { write("tab-main").cls(); userlist.clear(); hub_state = 0; }); sock.on('hubname', function(s) { write("tab-main").system("Now talking on "+s); hub_hubname = s; updateTitle(); }); sock.on('sys', function(data) { write("tab-main").system(data); }); sock.on('raw', function(data) { write("tab-main").raw(data); }); sock.on('pub', function(data) { write("tab-main").pub( data.user, data.message); }); sock.on('priv', function(data) { writerFor(data.user).pub( data.user, data.message); if (last_tab !== pm_tabs[data.user] && ($("#tabitem-"+pm_tabs[data.user]).className.indexOf('unread') === -1) ) { $("#tabitem-"+pm_tabs[data.user]).className += " unread"; updateTitle(); } }); sock.on('hello', function() { hub_state = 2; $("#chatbox").disabled = false; }); sock.on('part', function(u) { userlist.del(u.user); if (show_joins) { write("tab-main").system("*** Parts: "+u.user); // sanitised } updateTitle(); }); sock.on('join', function(u) { userlist.add(u.user); if (show_joins) { write("tab-main").system("*** Joins: "+u.user); } updateTitle(); }); sock.on('info', function(u) { try { var props = JSON.parse(u.message); userlist.setInfo(u.user, props); } catch (ex) {} }); sock.on('close', function() { hub_state = 0; userlist.clear(); write("tab-main").system("Connection closed by remote host."); }); sock.on('disconnect', function() { hub_state = 0; userlist.clear(); write("tab-main").system("Lost connection to the server."); }); sock.on('usercommand', function(data) { process_usercommand(data); }); }; //})();