2
* Copyright 2009 Google Inc.
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
* you may not use this file except in compliance with the License.
6
* You may obtain a copy of the License at
8
* http://www.apache.org/licenses/LICENSE-2.0
10
* Unless required by applicable law or agreed to in writing, software
11
* distributed under the License is distributed on an "AS-IS" BASIS,
12
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
* See the License for the specific language governing permissions and
14
* limitations under the License.
18
var paduserlist = (function() {
20
var rowManager = (function() {
21
// The row manager handles rendering rows of the user list and animating
22
// their insertion, removal, and reordering. It manipulates TD height
25
function nextRowId() {
26
return "usertr"+(nextRowId.counter++);
28
nextRowId.counter = 1;
29
// objects are shared; fields are "domId","data","animationStep"
30
var rowsFadingOut = []; // unordered set
31
var rowsFadingIn = []; // unordered set
32
var rowsPresent = []; // in order
34
var ANIMATION_START = -12; // just starting to fade in
35
var ANIMATION_END = 12; // just finishing fading out
36
function getAnimationHeight(step, power) {
37
var a = Math.abs(step/12);
38
if (power == 2) a = a*a;
39
else if (power == 3) a = a*a*a;
40
else if (power == 4) a = a*a*a*a;
41
else if (power >= 5) a = a*a*a*a*a;
42
return Math.round(26*(1-a));
44
var OPACITY_STEPS = 6;
46
var ANIMATION_STEP_TIME = 20;
47
var LOWER_FRAMERATE_FACTOR = 2;
48
var scheduleAnimation = padutils.makeAnimationScheduler(animateStep, ANIMATION_STEP_TIME,
49
LOWER_FRAMERATE_FACTOR).scheduleAnimation;
53
// we do lots of manipulation of table rows and stuff that JQuery makes ok, despite
54
// IE's poor handling when manipulating the DOM directly.
56
function getEmptyRowHtml(height) {
57
return '<td colspan="'+NUMCOLS+'" style="border:0;height:'+height+'px"><!-- --></td>';
59
function isNameEditable(data) {
60
return (! data.name) && (data.status != 'Disconnected');
62
function replaceUserRowContents(tr, height, data) {
63
var tds = getUserRowHtml(height, data).match(/<td.*?<\/td>/gi);
64
if (isNameEditable(data) && tr.find("td.usertdname input:enabled").length > 0) {
65
// preserve input field node
66
for(var i=0; i<tds.length; i++) {
67
var oldTd = $(tr.find("td").get(i));
68
if (! oldTd.hasClass('usertdname')) {
69
oldTd.replaceWith(tds[i]);
74
tr.html(tds.join(''));
78
function getUserRowHtml(height, data) {
80
var isGuest = (data.id.charAt(0) != 'p');
82
nameHtml = padutils.escapeHtml(data.name);
83
if (isGuest && pad.getIsProPad()) {
84
nameHtml += ' (Guest)';
88
nameHtml = '<input type="text" class="editempty newinput" value="unnamed" '+
89
(isNameEditable(data) ? '' : 'disabled="disabled" ')+
93
return ['<td style="height:',height,'px" class="usertdswatch"><div class="swatch" style="background:'+data.color+'"> </div></td>',
94
'<td style="height:',height,'px" class="usertdname">',nameHtml,'</td>',
95
'<td style="height:',height,'px" class="usertdstatus">',padutils.escapeHtml(data.status),'</td>',
96
'<td style="height:',height,'px" class="activity">',padutils.escapeHtml(data.activity),'</td>'].join('');
98
function getRowHtml(id, innerHtml) {
99
return '<tr id="'+id+'">'+innerHtml+'</tr>';
101
function rowNode(row) {
102
return $("#"+row.domId);
104
function handleRowData(row) {
105
if (row.data && row.data.status == 'Disconnected') {
112
function handleRowNode(tr, data) {
113
if (data.titleText) {
114
tr.attr('title', data.titleText);
117
tr.removeAttr('title');
120
function handleOtherUserInputs() {
121
// handle 'INPUT' elements for naming other unnamed users
122
$("#otheruserstable input.newinput").each(function() {
124
var tr = input.closest("tr");
126
var index = tr.parent().children().index(tr);
128
var userId = rowsPresent[index].data.id;
129
rowManagerMakeNameEditor($(this), userId);
132
}).removeClass('newinput');
135
// animationPower is 0 to skip animation, 1 for linear, 2 for quadratic, etc.
136
function insertRow(position, data, animationPower) {
137
position = Math.max(0, Math.min(rowsPresent.length, position));
138
animationPower = (animationPower === undefined ? 4 : animationPower);
140
var domId = nextRowId();
141
var row = {data: data, animationStep: ANIMATION_START, domId: domId,
142
animationPower: animationPower};
144
rowsPresent.splice(position, 0, row);
146
if (animationPower == 0) {
147
tr = $(getRowHtml(domId, getUserRowHtml(getAnimationHeight(0), data)));
148
row.animationStep = 0;
151
rowsFadingIn.push(row);
152
tr = $(getRowHtml(domId, getEmptyRowHtml(getAnimationHeight(ANIMATION_START))));
154
handleRowNode(tr, data);
156
$("table#otheruserstable").prepend(tr);
159
rowNode(rowsPresent[position-1]).after(tr);
162
if (animationPower != 0) {
166
handleOtherUserInputs();
171
function updateRow(position, data) {
172
var row = rowsPresent[position];
176
if (row.animationStep == 0) {
177
// not currently animating
178
var tr = rowNode(row);
179
replaceUserRowContents(tr, getAnimationHeight(0), row.data).find(
180
"td").css('opacity', (row.opacity === undefined ? 1 : row.opacity));
181
handleRowNode(tr, data);
182
handleOtherUserInputs();
187
function removeRow(position, animationPower) {
188
animationPower = (animationPower === undefined ? 4 : animationPower);
189
var row = rowsPresent[position];
191
rowsPresent.splice(position, 1); // remove
192
if (animationPower == 0) {
193
rowNode(row).remove();
196
row.animationStep = - row.animationStep; // use symmetry
197
row.animationPower = animationPower;
198
rowsFadingOut.push(row);
204
// newPosition is position after the row has been removed
205
function moveRow(oldPosition, newPosition, animationPower) {
206
animationPower = (animationPower === undefined ? 1 : animationPower); // linear is best
207
var row = rowsPresent[oldPosition];
208
if (row && oldPosition != newPosition) {
209
var rowData = row.data;
210
removeRow(oldPosition, animationPower);
211
insertRow(newPosition, rowData, animationPower);
215
function animateStep() {
216
// animation must be symmetrical
217
for(var i=rowsFadingIn.length-1;i>=0;i--) { // backwards to allow removal
218
var row = rowsFadingIn[i];
219
var step = ++row.animationStep;
220
var animHeight = getAnimationHeight(step, row.animationPower);
221
var node = rowNode(row);
222
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
223
if (step <= -OPACITY_STEPS) {
224
node.find("td").height(animHeight);
226
else if (step == -OPACITY_STEPS+1) {
227
node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
228
'opacity', baseOpacity*1/OPACITY_STEPS);
229
handleRowNode(node, row.data);
232
node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS-(-step))/OPACITY_STEPS).height(animHeight);
234
else if (step == 0) {
235
// set HTML in case modified during animation
236
node.html(getUserRowHtml(animHeight, row.data)).find("td").css(
237
'opacity', baseOpacity*1).height(animHeight);
238
handleRowNode(node, row.data);
239
rowsFadingIn.splice(i, 1); // remove from set
242
for(var i=rowsFadingOut.length-1;i>=0;i--) { // backwards to allow removal
243
var row = rowsFadingOut[i];
244
var step = ++row.animationStep;
245
var node = rowNode(row);
246
var animHeight = getAnimationHeight(step, row.animationPower);
247
var baseOpacity = (row.opacity === undefined ? 1 : row.opacity);
248
if (step < OPACITY_STEPS) {
249
node.find("td").css('opacity', baseOpacity*(OPACITY_STEPS - step)/OPACITY_STEPS).height(animHeight);
251
else if (step == OPACITY_STEPS) {
252
node.html(getEmptyRowHtml(animHeight));
254
else if (step <= ANIMATION_END) {
255
node.find("td").height(animHeight);
258
rowsFadingOut.splice(i, 1); // remove from set
263
handleOtherUserInputs();
265
return (rowsFadingIn.length > 0) || (rowsFadingOut.length > 0); // is more to do
269
insertRow: insertRow,
270
removeRow: removeRow,
275
}()); ////////// rowManager
278
var otherUsersInfo = [];
279
var otherUsersData = [];
280
var colorPickerOpen = false;
282
function rowManagerMakeNameEditor(jnode, userId) {
283
setUpEditable(jnode, function() {
284
var existingIndex = findExistingIndex(userId);
285
if (existingIndex >= 0) {
286
return otherUsersInfo[existingIndex].name || '';
291
}, function(newName) {
293
jnode.addClass("editempty");
294
jnode.val("unnamed");
297
jnode.attr('disabled', 'disabled');
298
pad.suggestUserName(userId, newName);
303
function renderMyUserInfo() {
304
if (myUserInfo.name) {
305
$("#myusernameedit").removeClass("editempty").val(
309
$("#myusernameedit").addClass("editempty").val(
310
"< enter your name >");
312
if (colorPickerOpen) {
313
$("#myswatchbox").addClass('myswatchboxunhoverable').removeClass(
314
'myswatchboxhoverable');
317
$("#myswatchbox").addClass('myswatchboxhoverable').removeClass(
318
'myswatchboxunhoverable');
320
$("#myswatch").css('background', pad.getColorPalette()[myUserInfo.colorId]);
323
function findExistingIndex(userId) {
324
var existingIndex = -1;
325
for(var i=0;i<otherUsersInfo.length;i++) {
326
if (otherUsersInfo[i].userId == userId) {
331
return existingIndex;
334
function setUpEditable(jqueryNode, valueGetter, valueSetter) {
335
jqueryNode.bind('focus', function(evt) {
336
var oldValue = valueGetter();
337
if (jqueryNode.val() !== oldValue) {
338
jqueryNode.val(oldValue);
340
jqueryNode.addClass("editactive").removeClass("editempty");
342
jqueryNode.bind('blur', function(evt) {
343
var newValue = jqueryNode.removeClass("editactive").val();
344
valueSetter(newValue);
346
padutils.bindEnterAndEscape(jqueryNode, function onEnter() {
348
}, function onEscape() {
349
jqueryNode.val(valueGetter()).blur();
351
jqueryNode.removeAttr('disabled').addClass('editable');
354
function showColorPicker() {
355
if (! colorPickerOpen) {
356
var palette = pad.getColorPalette();
357
for(var i=0;i<palette.length;i++) {
358
$("#mycolorpicker .n"+(i+1)+" .pickerswatch").css(
359
'background', palette[i]);
361
$("#mycolorpicker").css('display', 'block');
362
colorPickerOpen = true;
365
// this part happens even if color picker is already open
366
$("#mycolorpicker .pickerswatchouter").removeClass('picked');
367
$("#mycolorpicker .pickerswatchouter:eq("+(myUserInfo.colorId||0)+")").
370
function getColorPickerSwatchIndex(jnode) {
371
return Number(jnode.get(0).className.match(/\bn([0-9]+)\b/)[1])-1;
373
function closeColorPicker(accept) {
375
var newColorId = getColorPickerSwatchIndex($("#mycolorpicker .picked"));
376
if (newColorId >= 0) { // fails on NaN
377
myUserInfo.colorId = newColorId;
378
pad.notifyChangeColor(newColorId);
381
colorPickerOpen = false;
382
$("#mycolorpicker").css('display', 'none');
386
function updateInviteNotice() {
387
if (otherUsersInfo.length == 0) {
388
$("#otheruserstable").hide();
389
$("#nootherusers").show();
392
$("#nootherusers").hide();
393
$("#otheruserstable").show();
397
var knocksToIgnore = {};
398
var guestPromptFlashState = 0;
399
var guestPromptFlash = padutils.makeAnimationScheduler(
401
var prompts = $("#guestprompts .guestprompt");
402
if (prompts.length == 0) {
403
return false; // no more to do
406
guestPromptFlashState = 1 - guestPromptFlashState;
407
if (guestPromptFlashState) {
408
prompts.css('background', '#ffa');
411
prompts.css('background', '#ffe');
418
init: function(myInitialUserInfo) {
419
self.setMyUserInfo(myInitialUserInfo);
421
$("#otheruserstable tr").remove();
423
if (pad.getUserIsGuest()) {
424
$("#myusernameedit").addClass('myusernameedithoverable');
425
setUpEditable($("#myusernameedit"),
427
return myUserInfo.name || '';
430
myUserInfo.name = newValue;
431
pad.notifyChangeName(newValue);
432
// wrap with setTimeout to do later because we get
433
// a double "blur" fire in IE...
434
window.setTimeout(function() {
441
$("#myswatchbox").click(showColorPicker);
442
$("#mycolorpicker .pickerswatchouter").click(function() {
443
$("#mycolorpicker .pickerswatchouter").removeClass('picked');
444
$(this).addClass('picked');
446
$("#mycolorpickersave").click(function() {
447
closeColorPicker(true);
449
$("#mycolorpickercancel").click(function() {
450
closeColorPicker(false);
455
setMyUserInfo: function(info) {
456
myUserInfo = $.extend({}, info);
460
userJoinOrUpdate: function(info) {
461
if ((! info.userId) || (info.userId == myUserInfo.userId)) {
462
// not sure how this would happen
467
userData.color = pad.getColorPalette()[info.colorId];
468
userData.name = info.name;
469
userData.status = '';
470
userData.activity = '';
471
userData.id = info.userId;
472
// Firefox ignores \n in title text; Safari does a linebreak
473
userData.titleText = [info.userAgent||'', info.ip||''].join(' \n');
475
var existingIndex = findExistingIndex(info.userId);
477
var numUsersBesides = otherUsersInfo.length;
478
if (existingIndex >= 0) {
481
var newIndex = padutils.binarySearch(numUsersBesides, function(n) {
482
if (existingIndex >= 0 && n >= existingIndex) {
483
// pretend existingIndex isn't there
486
var infoN = otherUsersInfo[n];
487
var nameN = (infoN.name||'').toLowerCase();
488
var nameThis = (info.name||'').toLowerCase();
489
var idN = infoN.userId;
490
var idThis = info.userId;
491
return (nameN > nameThis) || (nameN == nameThis &&
495
if (existingIndex >= 0) {
497
if (existingIndex == newIndex) {
498
otherUsersInfo[existingIndex] = info;
499
otherUsersData[existingIndex] = userData;
500
rowManager.updateRow(existingIndex, userData);
503
otherUsersInfo.splice(existingIndex, 1);
504
otherUsersData.splice(existingIndex, 1);
505
otherUsersInfo.splice(newIndex, 0, info);
506
otherUsersData.splice(newIndex, 0, userData);
507
rowManager.updateRow(existingIndex, userData);
508
rowManager.moveRow(existingIndex, newIndex);
512
otherUsersInfo.splice(newIndex, 0, info);
513
otherUsersData.splice(newIndex, 0, userData);
514
rowManager.insertRow(newIndex, userData);
517
updateInviteNotice();
519
userLeave: function(info) {
520
var existingIndex = findExistingIndex(info.userId);
521
if (existingIndex >= 0) {
522
var userData = otherUsersData[existingIndex];
523
userData.status = 'Disconnected';
524
rowManager.updateRow(existingIndex, userData);
525
if (userData.leaveTimer) {
526
window.clearTimeout(userData.leaveTimer);
528
// set up a timer that will only fire if no leaves,
529
// joins, or updates happen for this user in the
530
// next N seconds, to remove the user from the list.
531
var thisUserId = info.userId;
532
var thisLeaveTimer = window.setTimeout(function() {
533
var newExistingIndex = findExistingIndex(thisUserId);
534
if (newExistingIndex >= 0) {
535
var newUserData = otherUsersData[newExistingIndex];
536
if (newUserData.status == 'Disconnected' &&
537
newUserData.leaveTimer == thisLeaveTimer) {
538
otherUsersInfo.splice(newExistingIndex, 1);
539
otherUsersData.splice(newExistingIndex, 1);
540
rowManager.removeRow(newExistingIndex);
541
updateInviteNotice();
544
}, 8000); // how long to wait
545
userData.leaveTimer = thisLeaveTimer;
547
updateInviteNotice();
549
showGuestPrompt: function(userId, displayName) {
550
if (knocksToIgnore[userId]) {
554
var encodedUserId = padutils.encodeUserId(userId);
556
var actionName = 'hide-guest-prompt-'+encodedUserId;
557
padutils.cancelActions(actionName);
559
var box = $("#guestprompt-"+encodedUserId);
560
if (box.length == 0) {
561
// make guest prompt box
562
box = $('<div id="guestprompt-'+encodedUserId+'" class="guestprompt"><div class="choices"><a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',false))">Deny</a> <a href="javascript:void(paduserlist.answerGuestPrompt(\''+encodedUserId+'\',true))">Approve</a></div><div class="guestname"><strong>Guest:</strong> '+padutils.escapeHtml(displayName)+'</div></div>');
563
$("#guestprompts").append(box);
566
// update display name
567
box.find(".guestname").html('<strong>Guest:</strong> '+padutils.escapeHtml(displayName));
569
var hideLater = padutils.getCancellableAction(actionName, function() {
570
self.removeGuestPrompt(userId);
572
window.setTimeout(hideLater, 15000); // time-out with no knock
574
guestPromptFlash.scheduleAnimation();
576
removeGuestPrompt: function(userId) {
577
var box = $("#guestprompt-"+padutils.encodeUserId(userId));
578
// remove ID now so a new knock by same user gets new, unfaded box
579
box.removeAttr('id').fadeOut("fast", function() {
583
knocksToIgnore[userId] = true;
584
window.setTimeout(function() {
585
delete knocksToIgnore[userId];
588
answerGuestPrompt: function(encodedUserId, approve) {
589
var guestId = padutils.decodeUserId(encodedUserId);
593
authId: pad.getUserId(),
595
answer: (approve ? "approved" : "denied")
597
pad.sendClientMessage(msg);
599
self.removeGuestPrompt(guestId);