/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
/**
* 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 padutils = require('/pad_utils').padutils,
myUserInfo = {},
colorPickerOpen = false,
colorPickerSetup = false,
previousColorId = 0;
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
rowsFadingIn = [], // unordered set
rowsPresent = [], // in order
ANIMATION_START = -12, // just starting to fade in
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,
ANIMATION_STEP_TIME = 20,
LOWER_FRAMERATE_FACTOR = 2,
scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME, LOWER_FRAMERATE_FACTOR).scheduleAnimation,
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, l=tds.length; i < l; i++) {
var oldTd = $(tr.find("td").get(i));
if (!oldTd.hasClass('usertdname'))
oldTd.replaceWith(tds[i]);
}
} else {
tr.html(tds.join(''));
}
return tr;
}
function getUserRowHtml(height, data) {
var isGuest = (data.id.charAt(0) != 'p'),
nameHtml;
if (data.name) {
nameHtml = padutils.escapeHtml(data.name);
if (isGuest && pad.getIsProPad())
nameHtml += ' (Guest)';
} else {
nameHtml = '';
}
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) {
var titleText = data.titleText;
window.setTimeout(function() {
/* tr.attr('title', titleText)*/
}, 0);
} else {
tr.removeAttr('title');
}
}
function handleOtherUserInputs() {
// handle 'INPUT' elements for naming other unnamed users
$('#otheruserstable .newinput')
.each(function() {
var input = $(this),
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(),
row = {
data : data,
animationStep : ANIMATION_START,
domId : domId,
animationPower: animationPower
},
tr;
handleRowData(row);
rowsPresent.splice(position, 0, row);
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)
$('#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() {
var row, step, node, $td, animHeight, baseOpacity, i;
// animation must be symmetrical
for (i=rowsFadingIn.length - 1; i >= 0; i--) { // backwards to allow removal
row = rowsFadingIn[i];
step = ++row.animationStep;
animHeight = getAnimationHeight(step, row.animationPower);
node = rowNode(row);
$td = node.find('TD');
baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step <= -OPACITY_STEPS)
$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) {
$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 (i=rowsFadingOut.length - 1; i >= 0; i--) { // backwards to allow removal
row = rowsFadingOut[i];
step = ++row.animationStep;
node = rowNode(row);
$td = node.find('TD');
animHeight = getAnimationHeight(step, row.animationPower);
baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
if (step < OPACITY_STEPS)
$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)
$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 END #########################################################
var otherUsersInfo = [],
otherUsersData = [];
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 findExistingIndex(userId) {
var existingIndex = -1;
for (var i=0, l=otherUsersInfo.length; i < l; i++) {
if (otherUsersInfo[i].userId == userId) {
existingIndex = i;
break;
}
}
return existingIndex;
}
function setUpEditable(jqueryNode, valueGetter, valueSetter) {
jqueryNode
.bind('focus', function(evt) {
var oldValue = valueGetter();
if (jqueryNode.val() !== oldValue)
jqueryNode.val(oldValue);
jqueryNode.addClass('editactive').removeClass('editempty');
})
.bind('blur', function(evt) {
var newValue = jqueryNode.removeClass('editactive').val();
valueSetter(newValue);
});
padutils.bindEnterAndEscape(jqueryNode,
function onEnter() {jqueryNode.blur();},
function onEscape() {jqueryNode.val(valueGetter()).blur();
});
jqueryNode.removeAttr('disabled').addClass('editable');
}
function updateInviteNotice() {
if (otherUsersInfo.length == 0) {
$('#otheruserstable').hide();
$('#nootherusers').show();
} else {
$('#nootherusers').hide();
$('#otheruserstable').show();
}
}
var knocksToIgnore = {},
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 pad = undefined;
var self = {
init: function(myInitialUserInfo, _pad)
{
pad = _pad;
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()
{
self.renderMyUserInfo();
}, 0);
});
}
// color picker
$('#myswatchbox').click(showColorPicker);
$('#mycolorpickersave').click(function() {closeColorPicker(true);});
$('#mycolorpickercancel').click(function() {closeColorPicker(false);});
},
setMyUserInfo: function(info) {
//translate the colorId
if (typeof info.colorId == 'number')
info.colorId = clientVars.colorPalette[info.colorId];
myUserInfo = $.extend({}, info);
self.renderMyUserInfo();
},
userJoinOrUpdate: function(info) {
if ((!info.userId) || (info.userId == myUserInfo.userId))
return; // not sure how this would happen
var userData = {};
userData.color = typeof info.colorId == "number" ? clientVars.colorPalette[info.colorId] : 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)
n++; // pretend existingIndex isn't there
var infoN = otherUsersInfo[n],
nameN = (infoN.name || '').toLowerCase(),
nameThis = (info.name || '').toLowerCase(),
idN = infoN.userId,
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();
self.updateNumberOfOnlineUsers();
},
updateNumberOfOnlineUsers: function() {
var online = 1; // you are always online!
for (var i=0, l=otherUsersData.length; i < l; i++) {
if (otherUsersData[i].status === '')
online++;
}
$('#usericonback').text(online);
return online;
},
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();
self.updateNumberOfOnlineUsers();
},
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 = $('