etherpad-lite/static/js/pad.js
Dominik Rodler 65ec8a1b47 UI refactoring and code clean-up
- Improved maintainability of UI (very much work still remaining)
- Improved performance of UI
- Consistent coding style
2012-02-20 23:16:25 +01:00

743 lines
No EOL
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
*
* 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.
*/
/* global $, window */
var socket;
// These jQuery things should create local references, but for now `require()`
// assigns to the global `$` and augments it with plugins.
require('/jquery');
require('/farbtastic');
require('/excanvas');
JSON = require('/json2');
require('/undo-xpopup');
require('/prefixfree');
var chat = require('/chat').chat,
getCollabClient = require('/collab_client').getCollabClient,
padconnectionstatus = require('/pad_connectionstatus').padconnectionstatus,
padcookie = require('/pad_cookie').padcookie,
paddocbar = require('/pad_docbar').paddocbar,
padeditbar = require('/pad_editbar').padeditbar,
padeditor = require('/pad_editor').padeditor,
padimpexp = require('/pad_impexp').padimpexp,
padmodals = require('/pad_modals').padmodals,
padsavedrevs = require('/pad_savedrevs').padsavedrevs,
paduserlist = require('/pad_userlist').paduserlist,
padutils = require('/pad_utils').padutils,
createCookie = require('/pad_utils').createCookie,
readCookie = require('/pad_utils').readCookie,
randomString = require('/pad_utils').randomString;
function getParams() {
var params = getUrlVars(),
showControls = params['showControls'],
showChat = params['showChat'],
userName = params['userName'],
showLineNumbers = params['showLineNumbers'],
useMonospaceFont = params['useMonospaceFont'],
IsnoColors = params['noColors'],
hideQRCode = params['hideQRCode'],
rtl = params['rtl'],
alwaysShowChat = params['alwaysShowChat'];
if (IsnoColors) {
if (IsnoColors == 'true') {
settings.noColors = true;
$('#buttonicon-clearauthorship').hide();
}
}
if (showControls) {
if (showControls == 'false') {
$('#editbar').hide();
$('#editorcontainer').css({top:0});
}
}
if (showChat) {
if (showChat == 'false') {
$('#chaticon').hide();
}
}
if (showLineNumbers) {
if (showLineNumbers == 'false')
settings.LineNumbersDisabled = true;
}
if (useMonospaceFont) {
if (useMonospaceFont == 'true')
settings.useMonospaceFontGlobal = true;
}
if (userName) {
// if the username is set as a parameter we should set a global value that we can call once we have initiated the pad
settings.globalUserName = unescape(userName);
}
if (hideQRCode) {
$('#qrcode').hide();
}
if (rtl) {
if (rtl == 'true')
settings.rtlIsTrue = true;
}
if (alwaysShowChat) {
if (alwaysShowChat == 'true')
chat.stickToScreen();
}
}
function getUrlVars() {
var vars = [], hash,
hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
for (var i=0, l=hashes.length; i < l; i++) {
hash = hashes[i].split('=');
vars.push(hash[0]);
vars[hash[0]] = hash[1];
}
return vars;
}
function savePassword() {
// set the password cookie
createCookie('password', $('#passwordinput').val(), null, document.location.pathname);
// reload
document.location = document.location;
}
function handshake() {
var loc = document.location,
port = loc.port == '' ? (loc.protocol == 'https:' ? 443 : 80) : loc.port, // get the correct port
url = loc.protocol + '//' + loc.hostname + ':' + port + '/', // create the url
resource = loc.pathname.substr(1, loc.pathname.indexOf('/p/')) + 'socket.io'; // determine current subfolder
// connect
socket = pad.socket = io.connect(url, {
resource: resource,
'max reconnection attempts': 3
});
function sendClientReady(isReconnect) {
var padId = document.location.pathname.substring(document.location.pathname.lastIndexOf('/') + 1);
padId = decodeURIComponent(padId); // unescape neccesary due to Safari and Opera interpretation of spaces
if (!isReconnect)
document.title = padId.replace(/_+/g, ' ') + " | " + document.title;
var token = readCookie('token');
if (token == null) {
token = 't.' + randomString();
createCookie('token', token, 60);
}
var sessionID = readCookie('sessionID'),
password = readCookie('password'),
msg = {
'component' : 'pad',
'type' : 'CLIENT_READY',
'padId' : padId,
'sessionID' : sessionID,
'password' : password,
'token' : token,
'protocolVersion' : 2
};
// this is a reconnect, lets tell the server our revision number
if (isReconnect == true) {
msg.client_rev=pad.collabClient.getCurrentRevisionNumber();
msg.reconnect=true;
}
socket.json.send(msg);
};
var disconnectTimeout;
socket.once('connect', function() {
sendClientReady(false);
});
socket.on('reconnect', function() {
// reconnect is before timeout, let´s stop the timeout
if (disconnectTimeout)
clearTimeout(disconnectTimeout);
pad.collabClient.setChannelState('CONNECTED');
sendClientReady(true);
});
socket.on('disconnect', function() {
function disconnectEvent() {
pad.collabClient.setChannelState('DISCONNECTED', 'reconnect_timeout');
}
pad.collabClient.setChannelState('RECONNECTING');
disconnectTimeout = setTimeout(disconnectEvent, 10000);
});
var receivedClientVars = false,
initalized = false;
socket.on('message', function(obj) {
// access was not granted, give the user a message
if (!receivedClientVars && obj.accessStatus) {
if (obj.accessStatus == 'deny')
$('#editorloadingbox').html("<b>You do not have permission to access this pad</b>");
else if (obj.accessStatus == "needPassword") {
$('#editorloadingbox').html("<b>You need a password to access this pad</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick=\"" + padutils.escapeHtml('require('+JSON.stringify(module.id)+").savePassword()") + "\">ok</button>");
} else if (obj.accessStatus == 'wrongPassword') {
$("#editorloadingbox").html("<b>You're password was wrong</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick=\"" + padutils.escapeHtml('require('+JSON.stringify(module.id)+").savePassword()") + "\">ok</button>");
}
} else if (!receivedClientVars) { // if we haven't received the clientVars yet, then this message should it be
// log the message
if (window.console)
console.log(obj);
receivedClientVars = true;
// set some client vars
clientVars = obj;
clientVars.userAgent = 'Anonymous';
clientVars.collab_client_vars.clientAgent = 'Anonymous';
// initalize the pad
pad._afterHandshake();
initalized = true;
// if LineNumbersDisabled is set to true, we need to hide the line numbers
if (settings.LineNumbersDisabled == true)
pad.changeViewOption('showLineNumbers', false);
// if noColors is set to true, we need to hide the backround colors on the ace spans
if (settings.noColors == true)
pad.changeViewOption('noColors', true);
if (settings.rtlIsTrue == true)
pad.changeViewOption('rtl', true);
// if Monospacefont is set to true, change it to monospace
if (settings.useMonospaceFontGlobal == true)
pad.changeViewOption('useMonospaceFont', true);
// if globalUserName is set, tell the server and the client about the new authorname
if (settings.globalUserName !== false) {
pad.notifyChangeName(settings.globalUserName); // notify the server
pad.myUserInfo.name = settings.globalUserName;
$('#myusernameedit').attr({"value":settings.globalUserName}); // update the current user´s UI
}
}
// handle every message after the clientVars
else {
// advice the client to disconnect
if (obj.disconnect) {
padconnectionstatus.disconnected(obj.disconnect);
socket.disconnect();
return;
} else {
pad.collabClient.handleMessageFromServer(obj);
}
}
});
// bind the colorpicker
var fb = $('#colorpicker').farbtastic({ callback: '#mycolorpickerpreview', width: 220});
}
var pad = {
// don't access these directly from outside this file, except for debugging
collabClient : null,
myUserInfo : null,
diagnosticInfo : {},
initTime : 0,
clientTimeOffset : null,
preloadedImages : false,
padOptions : {},
// these don't require init; clientVars should all go through here
getPadId : function() {
return clientVars.padId;
},
getClientIp : function() {
return clientVars.clientIp;
},
getIsProPad : function() {
return clientVars.isProPad;
},
getColorPalette : function() {
return clientVars.colorPalette;
},
getDisplayUserAgent: function() {
return padutils.uaDisplay(clientVars.userAgent);
},
getIsDebugEnabled : function() {
return clientVars.debugEnabled;
},
getPrivilege : function(name) {
return clientVars.accountPrivs[name];
},
getUserIsGuest : function() {
return clientVars.userIsGuest;
},
getUserId : function() {
return pad.myUserInfo.userId;
},
getUserName : function() {
return pad.myUserInfo.name;
},
sendClientMessage : function(msg) {
pad.collabClient.sendClientMessage(msg);
},
init : function() {
padutils.setupGlobalExceptionHandler();
$(document).ready(function() {
// start the custom js
if (typeof customStart == 'function')
customStart();
getParams();
handshake();
});
$(window).unload(function() {
pad.dispose();
});
},
_afterHandshake : function() {
pad.clientTimeOffset = new Date().getTime() - clientVars.serverTimestamp;
// initialize the chat
chat.init(this);
pad.initTime = +(new Date());
pad.padOptions = clientVars.initialOptions;
if ((!$.browser.msie) && (!($.browser.mozilla && $.browser.version.indexOf("1.8.") == 0)))
document.domain = document.domain; // for comet
// for IE
if ($.browser.msie) {
try {
doc.execCommand('BackgroundImageCache', false, true);
}
catch (e)
{}
}
// order of inits is important here:
padcookie.init(clientVars.cookiePrefsToSet, this);
$('#widthprefcheck').click(pad.toggleWidthPref);
pad.myUserInfo = {
userId : clientVars.userId,
name : clientVars.userName,
ip : pad.getClientIp(),
colorId : clientVars.userColor,
userAgent : pad.getDisplayUserAgent()
};
if (clientVars.specialKey) {
pad.myUserInfo.specialKey = clientVars.specialKey;
if (clientVars.specialKeyTranslation)
$('#specialkeyarea').html('mode: ' + String(clientVars.specialKeyTranslation).toUpperCase());
}
paddocbar.init({
isTitleEditable : pad.getIsProPad(),
initialTitle : clientVars.initialTitle,
initialPassword : clientVars.initialPassword,
guestPolicy : pad.padOptions.guestPolicy
}, this);
padimpexp.init(this);
padsavedrevs.init(clientVars.initialRevisionList, this);
padeditor.init(postAceInit, pad.padOptions.view || {}, this);
paduserlist.init(pad.myUserInfo, this);
// padchat.init(clientVars.chatHistory, pad.myUserInfo);
padconnectionstatus.init();
padmodals.init(this);
pad.collabClient = getCollabClient(padeditor.ace, clientVars.collab_client_vars, pad.myUserInfo, {
colorPalette: pad.getColorPalette()
}, pad);
pad.collabClient.setOnUserJoin(pad.handleUserJoin);
pad.collabClient.setOnUpdateUserInfo(pad.handleUserUpdate);
pad.collabClient.setOnUserLeave(pad.handleUserLeave);
pad.collabClient.setOnClientMessage(pad.handleClientMessage);
pad.collabClient.setOnServerMessage(pad.handleServerMessage);
pad.collabClient.setOnChannelStateChange(pad.handleChannelStateChange);
pad.collabClient.setOnInternalAction(pad.handleCollabAction);
function postAceInit() {
padeditbar.init();
setTimeout(function() {padeditor.ace.focus();}, 0);
}
},
dispose: function() {
padeditor.dispose();
},
notifyChangeName: function(newName) {
pad.myUserInfo.name = newName;
pad.collabClient.updateUserInfo(pad.myUserInfo);
//padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeColor: function(newColorId) {
pad.myUserInfo.colorId = newColorId;
pad.collabClient.updateUserInfo(pad.myUserInfo);
//padchat.handleUserJoinOrUpdate(pad.myUserInfo);
},
notifyChangeTitle: function(newTitle) {
pad.collabClient.sendClientMessage({
type : 'padtitle',
title : newTitle,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
notifyChangePassword: function(newPass) {
pad.collabClient.sendClientMessage({
type : 'padpassword',
password : newPass,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
changePadOption: function(key, value) {
var options = {};
options[key] = value;
pad.handleOptionsChange(options);
pad.collabClient.sendClientMessage({
type : 'padoptions',
options : options,
changedBy : pad.myUserInfo.name || 'unnamed'
});
},
changeViewOption: function(key, value) {
var options = {
view: {}
};
options.view[key] = value;
pad.handleOptionsChange(options);
},
handleOptionsChange: function(opts) {
// opts object is a full set of options or just some options to change
if (opts.view) {
if (!pad.padOptions.view)
pad.padOptions.view = {};
for (var k in opts.view) {
pad.padOptions.view[k] = opts.view[k];
}
padeditor.setViewOptions(pad.padOptions.view);
}
if (opts.guestPolicy) {
// order is important here:
pad.padOptions.guestPolicy = opts.guestPolicy;
paddocbar.setGuestPolicy(opts.guestPolicy);
}
},
getPadOptions: function() {
// caller shouldn't mutate the object
return pad.padOptions;
},
isPadPublic: function() {
return (!pad.getIsProPad()) || (pad.getPadOptions().guestPolicy == 'allow');
},
suggestUserName: function(userId, name) {
pad.collabClient.sendClientMessage({
type : 'suggestUserName',
unnamedId : userId,
newName : name
});
},
handleUserJoin: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
//padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserUpdate: function(userInfo) {
paduserlist.userJoinOrUpdate(userInfo);
//padchat.handleUserJoinOrUpdate(userInfo);
},
handleUserLeave: function(userInfo) {
paduserlist.userLeave(userInfo);
//padchat.handleUserLeave(userInfo);
},
handleClientMessage: function(msg) {
switch (msg.type) {
case 'suggestUserName':
if (msg.unnamedId == pad.myUserInfo.userId && msg.newName && !pad.myUserInfo.name) {
pad.notifyChangeName(msg.newName);
paduserlist.setMyUserInfo(pad.myUserInfo);
}
break;
case 'chat':
// padchat.receiveChat(msg);
break;
case 'padtitle':
paddocbar.changeTitle(msg.title);
break;
case 'padpassword':
paddocbar.changePassword(msg.password);
break;
case 'newRevisionList':
padsavedrevs.newRevisionList(msg.revisionList);
break;
case 'revisionLabel':
padsavedrevs.newRevisionList(msg.revisionList);
break;
case 'padoptions':
var opts = msg.options;
pad.handleOptionsChange(opts);
break;
case 'guestanswer':
// someone answered a prompt, remove it
paduserlist.removeGuestPrompt(msg.guestId);
break;
}
},
editbarClick: function(cmd) {
if (padeditbar)
padeditbar.toolbarClick(cmd);
},
dmesg: function(m) {
if (pad.getIsDebugEnabled()) {
var djs = $('#djs').get(0),
wasAtBottom = (djs.scrollTop - (djs.scrollHeight - $(djs).height()) >= -20);
$('#djs').append('<p>' + m + '</p>');
if (wasAtBottom)
djs.scrollTop = djs.scrollHeight;
}
},
handleServerMessage: function(m) {
if (m.type == 'NOTICE') {
if (m.text) {
alertBar.displayMessage(function(abar) {
abar.find('#servermsgdate').html(' (' + padutils.simpleDateTime(new Date) + ')');
abar.find('#servermsgtext').html(m.text);
});
}
if (m.js)
window['ev' + 'al'](m.js);
} else if (m.type == 'GUEST_PROMPT') {
paduserlist.showGuestPrompt(m.userId, m.displayName);
}
},
handleChannelStateChange: function(newState, message) {
var oldFullyConnected = !! padconnectionstatus.isFullyConnected(),
wasConnecting = (padconnectionstatus.getStatus().what == 'connecting');
if (newState == 'CONNECTED')
padconnectionstatus.connected();
else if (newState == 'RECONNECTING')
padconnectionstatus.reconnecting();
else if (newState == 'DISCONNECTED') {
pad.diagnosticInfo.disconnectedMessage = message;
pad.diagnosticInfo.padId = pad.getPadId();
pad.diagnosticInfo.socket = {};
// we filter non objects from the socket object and put them in the diagnosticInfo
// this ensures we have no cyclic data - this allows us to stringify the data
for (var i in socket.socket) {
var value = socket.socket[i],
type = typeof value;
if (type == 'string' || type == 'number')
pad.diagnosticInfo.socket[i] = value;
}
pad.asyncSendDiagnosticInfo();
if (typeof window.ajlog == 'string')
window.ajlog += ('Disconnected: ' + message + '\n');
padeditor.disable();
padeditbar.disable();
paddocbar.disable();
padimpexp.disable();
padconnectionstatus.disconnected(message);
}
var newFullyConnected = !! padconnectionstatus.isFullyConnected();
if (newFullyConnected != oldFullyConnected)
pad.handleIsFullyConnected(newFullyConnected, wasConnecting);
},
handleIsFullyConnected: function(isConnected, isInitialConnect) {
// load all images referenced from CSS, one at a time,
// starting one second after connection is first established.
if (isConnected && !pad.preloadedImages) {
window.setTimeout(function() {
if (!pad.preloadedImages) {
pad.preloadImages();
pad.preloadedImages = true;
}
}, 1000);
}
padsavedrevs.handleIsFullyConnected(isConnected);
// pad.determineSidebarVisibility(isConnected && !isInitialConnect);
pad.determineChatVisibility(isConnected && !isInitialConnect);
},
/* determineSidebarVisibility: function(asNowConnectedFeedback)
{
if (pad.isFullyConnected())
{
var setSidebarVisibility = padutils.getCancellableAction("set-sidebar-visibility", function()
{
// $("body").toggleClass('hidesidebar', !! padcookie.getPref('hideSidebar'));
});
window.setTimeout(setSidebarVisibility, asNowConnectedFeedback ? 3000 : 0);
}
else
{
padutils.cancelActions("set-sidebar-visibility");
$("body").removeClass('hidesidebar');
}
},
*/
determineChatVisibility: function(asNowConnectedFeedback) {
var chatVisCookie = padcookie.getPref('chatAlwaysVisible');
if (chatVisCookie) { // if the cookie is set for chat always visible
chat.stickToScreen(true); // stick chat to the screen
$('#options-stickychat').prop('checked', true); // set the checkbox to on
} else {
$('#options-stickychat').prop('checked', false); // set the checkbox for off
}
},
handleCollabAction: function(action) {
if (action == 'commitPerformed')
padeditbar.setSyncStatus('syncing');
else if (action == 'newlyIdle')
padeditbar.setSyncStatus('done');
},
hideServerMessage: function() {
alertBar.hideMessage();
},
asyncSendDiagnosticInfo: function() {
window.setTimeout(function() {
$.post({
url : '/ep/pad/connection-diagnostic-info',
data: {diagnosticInfo: JSON.stringify(pad.diagnosticInfo)}
});
}, 0);
},
forceReconnect: function() {
$('#reconnectform .padId').val(pad.getPadId());
pad.diagnosticInfo.collabDiagnosticInfo = pad.collabClient.getDiagnosticInfo();
$('#reconnectform .diagnosticInfo').val(JSON.stringify(pad.diagnosticInfo));
$('#reconnectform .missedChanges').val(JSON.stringify(pad.collabClient.getMissedChanges()));
$('#reconnectform').submit();
},
toggleWidthPref: function() {
var newValue = !padcookie.getPref('fullWidth');
padcookie.setPref('fullWidth', newValue);
$('#widthprefcheck').toggleClass('widthprefchecked', !! newValue).toggleClass('widthprefunchecked', !newValue);
pad.handleWidthChange();
},
/*
toggleSidebar: function()
{
var newValue = !padcookie.getPref('hideSidebar');
padcookie.setPref('hideSidebar', newValue);
$("#sidebarcheck").toggleClass('sidebarchecked', !newValue).toggleClass('sidebarunchecked', !! newValue);
pad.determineSidebarVisibility();
},
*/
handleWidthChange: function() {
var isFullWidth = padcookie.getPref('fullWidth');
if (isFullWidth)
$('BODY').addClass('fullwidth').removeClass('limwidth squish1width squish2width');
else {
$('BODY').addClass('limwidth').removeClass('fullwidth');
var pageWidth = $(window).width();
$('BODY').toggleClass('squish1width', (pageWidth < 912 && pageWidth > 812)).toggleClass('squish2width', (pageWidth <= 812));
}
},
// this is called from code put into a frame from the server:
handleImportExportFrameCall: function(callName, varargs) {
padimpexp.handleFrameCall.call(padimpexp, callName, Array.prototype.slice.call(arguments, 1));
},
callWhenNotCommitting: function(f) {
pad.collabClient.callWhenNotCommitting(f);
},
getCollabRevisionNumber: function() {
return pad.collabClient.getCurrentRevisionNumber();
},
isFullyConnected: function() {
return padconnectionstatus.isFullyConnected();
},
addHistoricalAuthors: function(data) {
if (!pad.collabClient) {
window.setTimeout(function() {
pad.addHistoricalAuthors(data);
}, 1000);
} else {
pad.collabClient.addHistoricalAuthors(data);
}
},
preloadImages: function() {
var images = ["../static/img/connectingbar.gif"];
function loadNextImage() {
if (images.length == 0)
return;
var img = new Image();
img.src = images.shift();
if (img.complete)
scheduleLoadNextImage();
else
$(img).bind('error load onreadystatechange', scheduleLoadNextImage);
}
function scheduleLoadNextImage() {
window.setTimeout(loadNextImage, 0);
}
scheduleLoadNextImage();
}
};
var alertBar = (function() {
var animator = padutils.makeShowHideAnimator(arriveAtAnimationState, false, 25, 400);
function arriveAtAnimationState(state) {
if (state == -1)
$('#alertbar').css('opacity', 0).css('display', 'block');
else if (state == 0)
$('#alertbar').css('opacity', 1);
else if (state == 1)
$('#alertbar').css('opacity', 0).css('display', 'none');
else if (state < 0)
$('#alertbar').css('opacity', state + 1);
else if (state > 0)
$('#alertbar').css('opacity', 1 - state);
}
var self = {
displayMessage: function(setupFunc) {
animator.show();
setupFunc($('#alertbar'));
},
hideMessage: function() {
animator.hide();
}
};
return self;
}());
function init() {
return pad.init();
}
var settings = {
LineNumbersDisabled : false,
noColors : false,
useMonospaceFontGlobal: false,
globalUserName : false,
hideQRCode : false,
rtlIsTrue : false
};
pad.settings = settings;
exports.settings = settings;
exports.createCookie = createCookie;
exports.readCookie = readCookie;
exports.randomString = randomString;
exports.getParams = getParams;
exports.getUrlVars = getUrlVars;
exports.savePassword = savePassword;
exports.handshake = handshake;
exports.pad = pad;
exports.init = init;
exports.alertBar = alertBar;