/** * Copyright 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS-IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var paduserlist = (function() { var rowManager = (function() { // The row manager handles rendering rows of the user list and animating // their insertion, removal, and reordering. It manipulates TD height // and TD opacity. function nextRowId() { return "usertr"+(nextRowId.counter++); } nextRowId.counter = 1; // objects are shared; fields are "domId","data","animationStep" var rowsFadingOut = []; // unordered set var rowsFadingIn = []; // unordered set var rowsPresent = []; // in order var ANIMATION_START = -12; // just starting to fade in var ANIMATION_END = 12; // just finishing fading out function getAnimationHeight(step, power) { var a = Math.abs(step/12); if (power == 2) a = a*a; else if (power == 3) a = a*a*a; else if (power == 4) a = a*a*a*a; else if (power >= 5) a = a*a*a*a*a; return Math.round(26*(1-a)); } var OPACITY_STEPS = 6; var ANIMATION_STEP_TIME = 20; var LOWER_FRAMERATE_FACTOR = 2; var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR).scheduleAnimation; var NUMCOLS = 4; // we do lots of manipulation of table rows and stuff that JQuery makes ok, despite // IE's poor handling when manipulating the DOM directly. function getEmptyRowHtml(height) { return ''; } function isNameEditable(data) { return (! data.name) && (data.status != 'Disconnected'); } function replaceUserRowContents(tr, height, data) { var tds = getUserRowHtml(height, data).match(//gi); if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) { // preserve input field node for(var i=0; i'; } return ['
 
', '',nameHtml,'', '',padutils.escapeHtml(data.status),'', '',padutils.escapeHtml(data.activity),''].join(''); } function getRowHtml(id, innerHtml) { return ''+innerHtml+''; } function rowNode(row) { return $("#"+row.domId); } function handleRowData(row) { if (row.data && row.data.status == 'Disconnected') { row.opacity = 0.5; } else { delete row.opacity; } } function handleRowNode(tr, data) { if (data.titleText) { tr.attr('title', data.titleText); } else { tr.removeAttr('title'); } } function handleOtherUserInputs() { // handle 'INPUT' elements for naming other unnamed users $("#otheruserstable input.newinput").each(function() { var input = $(this); var tr = input.closest("tr"); if (tr.length > 0) { var index = tr.parent().children().index(tr); if (index >= 0) { var userId = rowsPresent[index].data.id; rowManagerMakeNameEditor($(this), userId); } } }).removeClass('newinput'); } // animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc. function insertRow(position, data, animationPower) { position = Math.max(0, Math.min(rowsPresent.length, position)); animationPower = (animationPower === undefined ? 4 : animationPower); var domId = nextRowId(); var row = {data: data, animationStep: ANIMATION_START, domId: domId, animationPower: animationPower}; handleRowData(row); rowsPresent.splice(position, 0, row); var tr; if (animationPower == 0) { tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data))); row.animationStep = 0; } else { rowsFadingIn.push(row); tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START)))); } handleRowNode(tr, data); if (position == 0) { $("table#otheruserstable").prepend(tr); } else { rowNode(rowsPresent[position-1]).after(tr); } if (animationPower != 0) { scheduleAnimation(); } handleOtherUserInputs(); return row; } function updateRow(position, data) { var row = rowsPresent[position]; if (row) { row.data = data; handleRowData(row); if (row.animationStep == 0) { // not currently animating var tr = rowNode(row); replaceUserRowContents(tr, getAnimationHeight(0), row.data).find( "td").css('opacity', (row.opacity === undefined ? 1 : row.opacity)); handleRowNode(tr, data); handleOtherUserInputs(); } } } function removeRow(position, animationPower) { animationPower = (animationPower === undefined ? 4 : animationPower); var row = rowsPresent[position]; if (row) { rowsPresent.splice(position, 1); // remove if (animationPower == 0) { rowNode(row).remove(); } else { row.animationStep = - row.animationStep; // use symmetry row.animationPower = animationPower; rowsFadingOut.push(row); scheduleAnimation(); } } } // newPosition is position after the row has been removed function moveRow(oldPosition, newPosition, animationPower) { animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best var row = rowsPresent[oldPosition]; if (row && oldPosition != newPosition) { var rowData = row.data; removeRow(oldPosition, animationPower); insertRow(newPosition, rowData, animationPower); } } function animateStep() { // animation must be symmetrical for(var i=rowsFadingIn.length-1;i>=0;i--) { // backwards to allow removal var row = rowsFadingIn[i]; var step = ++row.animationStep; var animHeight = getAnimationHeight(step, row.animationPower); var node = rowNode(row); var baseOpacity = (row.opacity === undefined ? 1 : row.opacity); if (step <= -OPACITY_STEPS) { node.find("td").height(animHeight); } else if (step == -OPACITY_STEPS+1) { node.html(getUserRowHtml(animHeight, row.data)).find("td").css( 'opacity', baseOpacity*1/OPACITY_STEPS); handleRowNode(node, row.data); } else if (step < 0) { node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS-(-step))/OPACITY_STEPS).height(animHeight); } else if (step == 0) { // set HTML in case modified during animation node.html(getUserRowHtml(animHeight, row.data)).find("td").css( 'opacity', baseOpacity*1).height(animHeight); handleRowNode(node, row.data); rowsFadingIn.splice(i, 1); // remove from set } } for(var i=rowsFadingOut.length-1;i>=0;i--) { // backwards to allow removal var row = rowsFadingOut[i]; var step = ++row.animationStep; var node = rowNode(row); var animHeight = getAnimationHeight(step, row.animationPower); var baseOpacity = (row.opacity === undefined ? 1 : row.opacity); if (step < OPACITY_STEPS) { node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS - step)/OPACITY_STEPS).height(animHeight); } else if (step == OPACITY_STEPS) { node.html(getEmptyRowHtml(animHeight)); } else if (step <= ANIMATION_END) { node.find("td").height(animHeight); } else { rowsFadingOut.splice(i, 1); // remove from set node.remove(); } } handleOtherUserInputs(); return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do } var self = { insertRow: insertRow, removeRow: removeRow, moveRow: moveRow, updateRow: updateRow }; return self; }()); ////////// rowManager var myUserInfo = {}; var otherUsersInfo = []; var otherUsersData = []; var colorPickerOpen = false; function rowManagerMakeNameEditor(jnode, userId) { setUpEditable(jnode, function() { var existingIndex = findExistingIndex(userId); if (existingIndex >= 0) { return otherUsersInfo[existingIndex].name || ''; } else { return ''; } }, function(newName) { if (! newName) { jnode.addClass("editempty"); jnode.val("unnamed"); } else { jnode.attr('disabled', 'disabled'); pad.suggestUserName(userId, newName); } }); } function renderMyUserInfo() { if (myUserInfo.name) { $("#myusernameedit").removeClass("editempty").val( myUserInfo.name); } else { $("#myusernameedit").addClass("editempty").val( "< enter your name >"); } if (colorPickerOpen) { $("#myswatchbox").addClass('myswatchboxunhoverable').removeClass( 'myswatchboxhoverable'); } else { $("#myswatchbox").addClass('myswatchboxhoverable').removeClass( 'myswatchboxunhoverable'); } $("#myswatch").css('background', pad.getColorPalette()[myUserInfo.colorId]); } function findExistingIndex(userId) { var existingIndex = -1; for(var i=0;i= 0) { // fails on NaN myUserInfo.colorId = newColorId; pad.notifyChangeColor(newColorId); } } colorPickerOpen = false; $("#mycolorpicker").css('display', 'none'); renderMyUserInfo(); } function updateInviteNotice() { if (otherUsersInfo.length == 0) { $("#otheruserstable").hide(); $("#nootherusers").show(); } else { $("#nootherusers").hide(); $("#otheruserstable").show(); } } var knocksToIgnore = {}; var guestPromptFlashState = 0; var guestPromptFlash = padutils.makeAnimationScheduler( function () { var prompts = $("#guestprompts .guestprompt"); if (prompts.length == 0) { return false; // no more to do } guestPromptFlashState = 1 - guestPromptFlashState; if (guestPromptFlashState) { prompts.css('background', '#ffa'); } else { prompts.css('background', '#ffe'); } return true; }, 1000); var self = { init: function(myInitialUserInfo) { self.setMyUserInfo(myInitialUserInfo); $("#otheruserstable tr").remove(); if (pad.getUserIsGuest()) { $("#myusernameedit").addClass('myusernameedithoverable'); setUpEditable($("#myusernameedit"), function() { return myUserInfo.name || ''; }, function(newValue) { myUserInfo.name = newValue; pad.notifyChangeName(newValue); // wrap with setTimeout to do later because we get // a double "blur" fire in IE... window.setTimeout(function() { renderMyUserInfo(); }, 0); }); } // color picker $("#myswatchbox").click(showColorPicker); $("#mycolorpicker .pickerswatchouter").click(function() { $("#mycolorpicker .pickerswatchouter").removeClass('picked'); $(this).addClass('picked'); }); $("#mycolorpickersave").click(function() { closeColorPicker(true); }); $("#mycolorpickercancel").click(function() { closeColorPicker(false); }); // }, setMyUserInfo: function(info) { myUserInfo = $.extend({}, info); renderMyUserInfo(); }, userJoinOrUpdate: function(info) { if ((! info.userId) || (info.userId == myUserInfo.userId)) { // not sure how this would happen return; } var userData = {}; userData.color = pad.getColorPalette()[info.colorId]; userData.name = info.name; userData.status = ''; userData.activity = ''; userData.id = info.userId; // Firefox ignores \n in title text; Safari does a linebreak userData.titleText = [info.userAgent||'', info.ip||''].join(' \n'); var existingIndex = findExistingIndex(info.userId); var numUsersBesides = otherUsersInfo.length; if (existingIndex >= 0) { numUsersBesides--; } var newIndex = padutils.binarySearch(numUsersBesides, function(n) { if (existingIndex >= 0 && n >= existingIndex) { // pretend existingIndex isn't there n++; } var infoN = otherUsersInfo[n]; var nameN = (infoN.name||'').toLowerCase(); var nameThis = (info.name||'').toLowerCase(); var idN = infoN.userId; var idThis = info.userId; return (nameN > nameThis) || (nameN == nameThis && idN > idThis); }); if (existingIndex >= 0) { // update if (existingIndex == newIndex) { otherUsersInfo[existingIndex] = info; otherUsersData[existingIndex] = userData; rowManager.updateRow(existingIndex, userData); } else { otherUsersInfo.splice(existingIndex, 1); otherUsersData.splice(existingIndex, 1); otherUsersInfo.splice(newIndex, 0, info); otherUsersData.splice(newIndex, 0, userData); rowManager.updateRow(existingIndex, userData); rowManager.moveRow(existingIndex, newIndex); } } else { otherUsersInfo.splice(newIndex, 0, info); otherUsersData.splice(newIndex, 0, userData); rowManager.insertRow(newIndex, userData); } updateInviteNotice(); }, userLeave: function(info) { var existingIndex = findExistingIndex(info.userId); if (existingIndex >= 0) { var userData = otherUsersData[existingIndex]; userData.status = 'Disconnected'; rowManager.updateRow(existingIndex, userData); if (userData.leaveTimer) { window.clearTimeout(userData.leaveTimer); } // set up a timer that will only fire if no leaves, // joins, or updates happen for this user in the // next N seconds, to remove the user from the list. var thisUserId = info.userId; var thisLeaveTimer = window.setTimeout(function() { var newExistingIndex = findExistingIndex(thisUserId); if (newExistingIndex >= 0) { var newUserData = otherUsersData[newExistingIndex]; if (newUserData.status == 'Disconnected' && newUserData.leaveTimer == thisLeaveTimer) { otherUsersInfo.splice(newExistingIndex, 1); otherUsersData.splice(newExistingIndex, 1); rowManager.removeRow(newExistingIndex); updateInviteNotice(); } } }, 8000); // how long to wait userData.leaveTimer = thisLeaveTimer; } updateInviteNotice(); }, showGuestPrompt: function(userId, displayName) { if (knocksToIgnore[userId]) { return; } var encodedUserId = padutils.encodeUserId(userId); var actionName = 'hide-guest-prompt-'+encodedUserId; padutils.cancelActions(actionName); var box = $("#guestprompt-"+encodedUserId); if (box.length == 0) { // make guest prompt box box = $('
Guest: '+padutils.escapeHtml(displayName)+'
'); $("#guestprompts").append(box); } else { // update display name box.find(".guestname").html('Guest: '+padutils.escapeHtml(displayName)); } var hideLater = padutils.getCancellableAction(actionName, function() { self.removeGuestPrompt(userId); }); window.setTimeout(hideLater, 15000); // time-out with no knock guestPromptFlash.scheduleAnimation(); }, removeGuestPrompt: function(userId) { var box = $("#guestprompt-"+padutils.encodeUserId(userId)); // remove ID now so a new knock by same user gets new, unfaded box box.removeAttr('id').fadeOut("fast", function() { box.remove(); }); knocksToIgnore[userId] = true; window.setTimeout(function() { delete knocksToIgnore[userId]; }, 5000); }, answerGuestPrompt: function(encodedUserId, approve) { var guestId = padutils.decodeUserId(encodedUserId); var msg = { type: 'guestanswer', authId: pad.getUserId(), guestId: guestId, answer: (approve ? "approved" : "denied") }; pad.sendClientMessage(msg); self.removeGuestPrompt(guestId); } }; return self; }());