Merge pull request #1459 from ether/timeslider-labels

Timeslider labels test
This commit is contained in:
John McLear 2013-02-06 15:37:31 -08:00
commit 8186cc59d8
15 changed files with 452 additions and 768 deletions

View file

@ -325,17 +325,17 @@ exports.getChatHistory = function(padID, start, end, callback)
if(!start || !end) if(!start || !end)
{ {
start = 0; start = 0;
end = pad.chatHead - 1; end = pad.chatHead;
} }
if(start >= chatHead) if(start >= chatHead && chatHead > 0)
{ {
callback(new customError("start is higher or equal to the current chatHead","apierror")); callback(new customError("start is higher or equal to the current chatHead","apierror"));
return; return;
} }
if(end >= chatHead) if(end > chatHead)
{ {
callback(new customError("end is higher or equal to the current chatHead","apierror")); callback(new customError("end is higher than the current chatHead","apierror"));
return; return;
} }

View file

@ -35,11 +35,6 @@ var messageLogger = log4js.getLogger("message");
var _ = require('underscore'); var _ = require('underscore');
var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js"); var hooks = require("ep_etherpad-lite/static/js/pluginfw/hooks.js");
/**
* A associative array that saves which sessions belong to a pad
*/
var pad2sessions = {};
/** /**
* A associative array that saves informations about a session * A associative array that saves informations about a session
* key = sessionId * key = sessionId
@ -83,14 +78,11 @@ exports.handleConnect = function(client)
exports.kickSessionsFromPad = function(padID) exports.kickSessionsFromPad = function(padID)
{ {
//skip if there is nobody on this pad //skip if there is nobody on this pad
if(!pad2sessions[padID]) if(socketio.sockets.clients(padID).length == 0)
return; return;
//disconnect everyone from this pad //disconnect everyone from this pad
for(var i in pad2sessions[padID]) socketio.sockets.in(padID).json.send({disconnect:"deleted"});
{
socketio.sockets.sockets[pad2sessions[padID][i]].json.send({disconnect:"deleted"});
}
} }
/** /**
@ -100,15 +92,13 @@ exports.kickSessionsFromPad = function(padID)
exports.handleDisconnect = function(client) exports.handleDisconnect = function(client)
{ {
//save the padname of this session //save the padname of this session
var sessionPad=sessioninfos[client.id].padId; var session = sessioninfos[client.id];
//if this connection was already etablished with a handshake, send a disconnect message to the others //if this connection was already etablished with a handshake, send a disconnect message to the others
if(sessioninfos[client.id] && sessioninfos[client.id].author) if(session && session.author)
{ {
var author = sessioninfos[client.id].author;
//get the author color out of the db //get the author color out of the db
authorManager.getAuthorColorId(author, function(err, color) authorManager.getAuthorColorId(session.author, function(err, color)
{ {
ERR(err); ERR(err);
@ -121,33 +111,16 @@ exports.handleDisconnect = function(client)
"ip": "127.0.0.1", "ip": "127.0.0.1",
"colorId": color, "colorId": color,
"userAgent": "Anonymous", "userAgent": "Anonymous",
"userId": author "userId": session.author
} }
} }
}; };
//Go trough all user that are still on the pad, and send them the USER_LEAVE message //Go trough all user that are still on the pad, and send them the USER_LEAVE message
for(i in pad2sessions[sessionPad]) client.broadcast.to(session.padId).json.send(messageToTheOtherUsers);
{
var socket = socketio.sockets.sockets[pad2sessions[sessionPad][i]];
if(socket !== undefined){
socket.json.send(messageToTheOtherUsers);
}
}
}); });
} }
//Go trough all sessions of this pad, search and destroy the entry of this client
for(i in pad2sessions[sessionPad])
{
if(pad2sessions[sessionPad][i] == client.id)
{
pad2sessions[sessionPad].splice(i, 1);
break;
}
}
//Delete the sessioninfos entrys of this session //Delete the sessioninfos entrys of this session
delete sessioninfos[client.id]; delete sessioninfos[client.id];
} }
@ -228,11 +201,10 @@ exports.handleMessage = function(client, message)
function(callback) function(callback)
{ {
if(!message.padId){ // If the message has a padId we assume the client is already known to the server and needs no re-authorization
// If the message has a padId we assume the client is already known to the server and needs no re-authorization if(!message.padId)
callback(); return callback();
return;
}
// Note: message.sessionID is an entirely different kind of // Note: message.sessionID is an entirely different kind of
// session from the sessions we use here! Beware! FIXME: Call // session from the sessions we use here! Beware! FIXME: Call
// our "sessions" "connections". // our "sessions" "connections".
@ -292,9 +264,7 @@ exports.handleCustomMessage = function (padID, msg, cb) {
time: time time: time
} }
}; };
for (var i in pad2sessions[padID]) { socketio.sockets.in(padID).json.send(msg);
socketio.sockets.sockets[pad2sessions[padID][i]].json.send(msg);
}
cb(null, {}); cb(null, {});
} }
@ -352,10 +322,7 @@ function handleChatMessage(client, message)
}; };
//broadcast the chat message to everyone on the pad //broadcast the chat message to everyone on the pad
for(var i in pad2sessions[padId]) socketio.sockets.in(padId).json.send(msg);
{
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(msg);
}
callback(); callback();
} }
@ -413,23 +380,16 @@ function handleGetChatMessages(client, message)
{ {
if(ERR(err, callback)) return; if(ERR(err, callback)) return;
var infoMsg = { var infoMsg = {
type: "COLLABROOM", type: "COLLABROOM",
data: { data: {
type: "CHAT_MESSAGES", type: "CHAT_MESSAGES",
messages: chatMessages messages: chatMessages
}
};
// send the messages back to the client
for(var i in pad2sessions[padId])
{
if(pad2sessions[padId][i] == client.id)
{
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(infoMsg);
break;
} }
} };
// send the messages back to the client
client.json.send(infoMsg);
}); });
}]); }]);
} }
@ -453,14 +413,14 @@ function handleSuggestUserName(client, message)
return; return;
} }
var padId = sessioninfos[client.id].padId; var padId = sessioninfos[client.id].padId,
clients = socketio.sockets.clients(padId);
//search the author and send him this message //search the author and send him this message
for(var i in pad2sessions[padId]) for(var i = 0; i < clients.length; i++) {
{ var session = sessioninfos[clients[i].id];
if(sessioninfos[pad2sessions[padId][i]].author == message.data.payload.unnamedId) if(session && session.author == message.data.payload.unnamedId) {
{ clients[i].json.send(message);
socketio.sockets.sockets[pad2sessions[padId][i]].send(message);
break; break;
} }
} }
@ -501,7 +461,8 @@ function handleUserInfoUpdate(client, message)
type: "USER_NEWINFO", type: "USER_NEWINFO",
userInfo: { userInfo: {
userId: author, userId: author,
name: message.data.userInfo.name, //set a null name, when there is no name set. cause the client wants it null
name: message.data.userInfo.name || null,
colorId: message.data.userInfo.colorId, colorId: message.data.userInfo.colorId,
userAgent: "Anonymous", userAgent: "Anonymous",
ip: "127.0.0.1", ip: "127.0.0.1",
@ -509,20 +470,8 @@ function handleUserInfoUpdate(client, message)
} }
}; };
//set a null name, when there is no name set. cause the client wants it null
if(infoMsg.data.userInfo.name == null)
{
infoMsg.data.userInfo.name = null;
}
//Send the other clients on the pad the update message //Send the other clients on the pad the update message
for(var i in pad2sessions[padId]) client.broadcast.to(padId).json.send(infoMsg);
{
if(pad2sessions[padId][i] != client.id)
{
socketio.sockets.sockets[pad2sessions[padId][i]].json.send(infoMsg);
}
}
} }
/** /**
@ -682,90 +631,76 @@ function handleUserChanges(client, message)
exports.updatePadClients = function(pad, callback) exports.updatePadClients = function(pad, callback)
{ {
//skip this step if noone is on this pad //skip this step if noone is on this pad
if(!pad2sessions[pad.id]) var roomClients = socketio.sockets.clients(pad.id);
{ if(roomClients.length==0)
callback(); return callback();
return;
}
// since all clients usually get the same set of changesets, store them in local cache
// to remove unnecessary roundtrip to the datalayer
// TODO: in REAL world, if we're working without datalayer cache, all requests to revisions will be fired
// BEFORE first result will be landed to our cache object. The solution is to replace parallel processing
// via async.forEach with sequential for() loop. There is no real benefits of running this in parallel,
// but benefit of reusing cached revision object is HUGE
var revCache = {};
//go trough all sessions on this pad //go trough all sessions on this pad
async.forEach(pad2sessions[pad.id], function(session, callback) async.forEach(roomClients, function(client, callback)
{ {
var sid = client.id;
//https://github.com/caolan/async#whilst //https://github.com/caolan/async#whilst
//send them all new changesets //send them all new changesets
async.whilst( async.whilst(
function (){ return sessioninfos[session] && sessioninfos[session].rev < pad.getHeadRevisionNumber()}, function (){ return sessioninfos[sid] && sessioninfos[sid].rev < pad.getHeadRevisionNumber()},
function(callback) function(callback)
{ {
var author, revChangeset, currentTime; var r = sessioninfos[sid].rev + 1;
var r = sessioninfos[session].rev + 1;
async.parallel([
function (callback)
{
pad.getRevisionAuthor(r, function(err, value)
{
if(ERR(err, callback)) return;
author = value;
callback();
});
},
function (callback)
{
pad.getRevisionChangeset(r, function(err, value)
{
if(ERR(err, callback)) return;
revChangeset = value;
callback();
});
},
function (callback)
{
pad.getRevisionDate(r, function(err, date)
{
if(ERR(err, callback)) return;
currentTime = date;
callback();
});
}
], function(err)
{
if(ERR(err, callback)) return;
// next if session has not been deleted
if(sessioninfos[session] == null)
{
callback(null);
return;
}
if(author == sessioninfos[session].author)
{
socketio.sockets.sockets[session].json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
}
else
{
var forWire = Changeset.prepareForWire(revChangeset, pad.pool);
var wireMsg = {"type":"COLLABROOM",
"data":{type:"NEW_CHANGES",
newRev:r,
changeset: forWire.translated,
apool: forWire.pool,
author: author,
currentTime: currentTime,
timeDelta: currentTime - sessioninfos[session].time
}};
socketio.sockets.sockets[session].json.send(wireMsg);
}
if(sessioninfos[session] != null) async.waterfall([
{ function(callback) {
sessioninfos[session].time = currentTime; if(revCache[r])
sessioninfos[session].rev = r; callback(null, revCache[r]);
} else
pad.getRevision(r, callback);
callback(null); },
}); function(revision, callback)
{
revCache[r] = revision;
var author = revision.meta.author,
revChangeset = revision.changeset,
currentTime = revision.meta.timestamp;
// next if session has not been deleted
if(sessioninfos[sid] == null)
return callback(null);
if(author == sessioninfos[sid].author)
{
client.json.send({"type":"COLLABROOM","data":{type:"ACCEPT_COMMIT", newRev:r}});
}
else
{
var forWire = Changeset.prepareForWire(revChangeset, pad.pool);
var wireMsg = {"type":"COLLABROOM",
"data":{type:"NEW_CHANGES",
newRev:r,
changeset: forWire.translated,
apool: forWire.pool,
author: author,
currentTime: currentTime,
timeDelta: currentTime - sessioninfos[sid].time
}};
client.json.send(wireMsg);
}
sessioninfos[sid].time = currentTime;
sessioninfos[sid].rev = r;
callback(null);
}
], callback);
}, },
callback callback
); );
@ -895,23 +830,14 @@ function handleClientReady(client, message)
function(callback) function(callback)
{ {
async.parallel([ async.parallel([
//get colorId //get colorId and name
function(callback) function(callback)
{ {
authorManager.getAuthorColorId(author, function(err, value) authorManager.getAuthor(author, function(err, value)
{ {
if(ERR(err, callback)) return; if(ERR(err, callback)) return;
authorColorId = value; authorColorId = value.colorId;
callback(); authorName = value.name;
});
},
//get author name
function(callback)
{
authorManager.getAuthorName(author, function(err, value)
{
if(ERR(err, callback)) return;
authorName = value;
callback(); callback();
}); });
}, },
@ -965,21 +891,17 @@ function handleClientReady(client, message)
{ {
//Check that the client is still here. It might have disconnected between callbacks. //Check that the client is still here. It might have disconnected between callbacks.
if(sessioninfos[client.id] === undefined) if(sessioninfos[client.id] === undefined)
{ return callback();
callback();
return;
}
//Check if this author is already on the pad, if yes, kick the other sessions! //Check if this author is already on the pad, if yes, kick the other sessions!
if(pad2sessions[padIds.padId]) var roomClients = socketio.sockets.clients(padIds.padId);
{ for(var i = 0; i < roomClients.length; i++) {
for(var i in pad2sessions[padIds.padId]) var sinfo = sessioninfos[roomClients[i].id];
{ if(sinfo && sinfo.author == author) {
if(sessioninfos[pad2sessions[padIds.padId][i]] && sessioninfos[pad2sessions[padIds.padId][i]].author == author) // fix user's counter, works on page refresh or if user closes browser window and then rejoins
{ sessioninfos[roomClients[i].id] = {};
var socket = socketio.sockets.sockets[pad2sessions[padIds.padId][i]]; roomClients[i].leave(padIds.padId);
if(socket) socket.json.send({disconnect:"userdup"}); roomClients[i].json.send({disconnect:"userdup"});
}
} }
} }
@ -988,15 +910,6 @@ function handleClientReady(client, message)
sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId; sessioninfos[client.id].readOnlyPadId = padIds.readOnlyPadId;
sessioninfos[client.id].readonly = padIds.readonly; sessioninfos[client.id].readonly = padIds.readonly;
//check if there is already a pad2sessions entry, if not, create one
if(!pad2sessions[padIds.padId])
{
pad2sessions[padIds.padId] = [];
}
//Saves in pad2sessions that this session belongs to this pad
pad2sessions[padIds.padId].push(client.id);
//If this is a reconnect, we don't have to send the client the ClientVars again //If this is a reconnect, we don't have to send the client the ClientVars again
if(message.reconnect == true) if(message.reconnect == true)
{ {
@ -1044,7 +957,7 @@ function handleClientReady(client, message)
// tell the client the number of the latest chat-message, which will be // tell the client the number of the latest chat-message, which will be
// used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES) // used to request the latest 100 chat-messages later (GET_CHAT_MESSAGES)
"chatHead": pad.chatHead, "chatHead": pad.chatHead,
"numConnectedUsers": pad2sessions[padIds.padId].length, "numConnectedUsers": roomClients.length,
"isProPad": false, "isProPad": false,
"readOnlyId": padIds.readOnlyPadId, "readOnlyId": padIds.readOnlyPadId,
"readonly": padIds.readonly, "readonly": padIds.readonly,
@ -1080,6 +993,8 @@ function handleClientReady(client, message)
} }
}); });
//Join the pad and start receiving updates
client.join(padIds.padId);
//Send the clientVars to the Client //Send the clientVars to the Client
client.json.send({type: "CLIENT_VARS", data: clientVars}); client.json.send({type: "CLIENT_VARS", data: clientVars});
//Save the current revision in sessioninfos, should be the same as in clientVars //Save the current revision in sessioninfos, should be the same as in clientVars
@ -1108,74 +1023,56 @@ function handleClientReady(client, message)
{ {
messageToTheOtherUsers.data.userInfo.name = authorName; messageToTheOtherUsers.data.userInfo.name = authorName;
} }
// notify all existing users about new user
client.broadcast.to(padIds.padIds).json.send(messageToTheOtherUsers);
//Run trough all sessions of this pad //Run trough all sessions of this pad
async.forEach(pad2sessions[padIds.padId], function(sessionID, callback) async.forEach(socketio.sockets.clients(padIds.padId), function(roomClient, callback)
{ {
var author, socket, sessionAuthorName, sessionAuthorColorId; var author;
//Jump over, if this session is the connection session
if(roomClient.id == client.id)
return callback();
//Since sessioninfos might change while being enumerated, check if the //Since sessioninfos might change while being enumerated, check if the
//sessionID is still assigned to a valid session //sessionID is still assigned to a valid session
if(sessioninfos[sessionID] !== undefined && if(sessioninfos[roomClient.id] !== undefined)
socketio.sockets.sockets[sessionID] !== undefined){ author = sessioninfos[roomClient.id].author;
author = sessioninfos[sessionID].author; else // If the client id is not valid, callback();
socket = socketio.sockets.sockets[sessionID]; return callback();
}else {
// If the sessionID is not valid, callback(); async.waterfall([
callback();
return;
}
async.series([
//get the authorname & colorId //get the authorname & colorId
function(callback) function(callback)
{ {
async.parallel([ // reuse previously created cache of author's data
function(callback) if(historicalAuthorData[author])
{ callback(null, historicalAuthorData[author]);
authorManager.getAuthorColorId(author, function(err, value) else
{ authorManager.getAuthor(author, callback);
if(ERR(err, callback)) return;
sessionAuthorColorId = value;
callback();
})
},
function(callback)
{
authorManager.getAuthorName(author, function(err, value)
{
if(ERR(err, callback)) return;
sessionAuthorName = value;
callback();
})
}
],callback);
}, },
function (callback) function (authorInfo, callback)
{ {
//Jump over, if this session is the connection session //Send the new User a Notification about this other user
if(sessionID != client.id) var msg = {
{ "type": "COLLABROOM",
//Send this Session the Notification about the new user "data": {
socket.json.send(messageToTheOtherUsers); type: "USER_NEWINFO",
userInfo: {
//Send the new User a Notification about this other user "ip": "127.0.0.1",
var messageToNotifyTheClientAboutTheOthers = { "colorId": authorInfo.colorId,
"type": "COLLABROOM", "name": authorInfo.name,
"data": { "userAgent": "Anonymous",
type: "USER_NEWINFO", "userId": author
userInfo: {
"ip": "127.0.0.1",
"colorId": sessionAuthorColorId,
"name": sessionAuthorName,
"userAgent": "Anonymous",
"userId": author
}
} }
}; }
client.json.send(messageToNotifyTheClientAboutTheOthers); };
} client.json.send(msg);
} }
], callback); ], callback);
}, callback); }, callback);
} }
],function(err) ],function(err)
@ -1521,33 +1418,30 @@ function composePadChangesets(padId, startNum, endNum, callback)
* Get the number of users in a pad * Get the number of users in a pad
*/ */
exports.padUsersCount = function (padID, callback) { exports.padUsersCount = function (padID, callback) {
if (!pad2sessions[padID] || typeof pad2sessions[padID] != typeof []) { callback(null, {
callback(null, {padUsersCount: 0}); padUsersCount: socketio.sockets.clients(padId).length
} else { });
callback(null, {padUsersCount: pad2sessions[padID].length});
}
} }
/** /**
* Get the list of users in a pad * Get the list of users in a pad
*/ */
exports.padUsers = function (padID, callback) { exports.padUsers = function (padID, callback) {
if (!pad2sessions[padID] || typeof pad2sessions[padID] != typeof []) { var result = [];
callback(null, {padUsers: []});
} else { async.forEach(socketio.sockets.clients(padId), function(roomClient, callback) {
var authors = []; var s = sessioninfos[roomClient.id];
for ( var ix in sessioninfos ) { if(s) {
if ( sessioninfos[ix].padId !== padID ) { authorManager.getAuthor(s.author, function(err, author) {
continue; if(ERR(err, callback)) return;
}
var aid = sessioninfos[ix].author; author.id = s.author;
authorManager.getAuthor( aid, function ( err, author ) { result.push(author);
author.id = aid; });
authors.push( author );
if ( authors.length === pad2sessions[padID].length ) {
callback(null, {padUsers: authors});
}
} );
} }
} }, function(err) {
if(ERR(err, callback)) return;
callback(null, {padUsers: result});
});
} }

View file

@ -42,6 +42,7 @@ div.innerwrapper {
border-radius: 0 0 7px 7px; border-radius: 0 0 7px 7px;
margin-left:250px; margin-left:250px;
min-width:400px; min-width:400px;
width:100%;
} }
#wrapper { #wrapper {

View file

@ -33,19 +33,6 @@ function object(o)
f.prototype = o; f.prototype = o;
return new f(); return new f();
} }
var userAgent = (((function () {return this;})().navigator || {}).userAgent || 'node-js').toLowerCase();
// Figure out what browser is being used (stolen from jquery 1.2.1)
var browser = {
version: (userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/) || [])[1],
safari: /webkit/.test(userAgent),
opera: /opera/.test(userAgent),
msie: /msie/.test(userAgent) && !/opera/.test(userAgent),
mozilla: /mozilla/.test(userAgent) && !/(compatible|webkit)/.test(userAgent),
windows: /windows/.test(userAgent),
mobile: /mobile/.test(userAgent) || /android/.test(userAgent)
};
function getAssoc(obj, name) function getAssoc(obj, name)
{ {
@ -97,7 +84,6 @@ var noop = function(){};
exports.isNodeText = isNodeText; exports.isNodeText = isNodeText;
exports.object = object; exports.object = object;
exports.browser = browser;
exports.getAssoc = getAssoc; exports.getAssoc = getAssoc;
exports.setAssoc = setAssoc; exports.setAssoc = setAssoc;
exports.binarySearch = binarySearch; exports.binarySearch = binarySearch;

View file

@ -28,7 +28,7 @@ $ = jQuery = require('./rjquery').$;
_ = require("./underscore"); _ = require("./underscore");
var isNodeText = Ace2Common.isNodeText, var isNodeText = Ace2Common.isNodeText,
browser = Ace2Common.browser, browser = $.browser,
getAssoc = Ace2Common.getAssoc, getAssoc = Ace2Common.getAssoc,
setAssoc = Ace2Common.setAssoc, setAssoc = Ace2Common.setAssoc,
isTextNode = Ace2Common.isTextNode, isTextNode = Ace2Common.isTextNode,
@ -154,7 +154,17 @@ function Ace2Inner(){
var dmesg = noop; var dmesg = noop;
window.dmesg = noop; window.dmesg = noop;
var scheduler = parent; // Ugly hack for Firefox 18
// get the timeout and interval methods from the parent iframe
// This hack breaks IE8
try{
setTimeout = parent.setTimeout;
clearTimeout = parent.clearTimeout;
setInterval = parent.setInterval;
clearInterval = parent.clearInterval;
}catch(err){
// IE8 can panic here.
}
var textFace = 'monospace'; var textFace = 'monospace';
var textSize = 12; var textSize = 12;
@ -174,7 +184,7 @@ function Ace2Inner(){
parentDynamicCSS = makeCSSManager("dynamicsyntax", true); parentDynamicCSS = makeCSSManager("dynamicsyntax", true);
} }
var changesetTracker = makeChangesetTracker(scheduler, rep.apool, { var changesetTracker = makeChangesetTracker(rep.apool, {
withCallbacks: function(operationName, f) withCallbacks: function(operationName, f)
{ {
inCallStackIfNecessary(operationName, function() inCallStackIfNecessary(operationName, function()
@ -594,7 +604,7 @@ function Ace2Inner(){
doesWrap = newVal; doesWrap = newVal;
var dwClass = "doesWrap"; var dwClass = "doesWrap";
setClassPresence(root, "doesWrap", doesWrap); setClassPresence(root, "doesWrap", doesWrap);
scheduler.setTimeout(function() setTimeout(function()
{ {
inCallStackIfNecessary("setWraps", function() inCallStackIfNecessary("setWraps", function()
{ {
@ -634,7 +644,7 @@ function Ace2Inner(){
textFace = face; textFace = face;
root.style.fontFamily = textFace; root.style.fontFamily = textFace;
lineMetricsDiv.style.fontFamily = textFace; lineMetricsDiv.style.fontFamily = textFace;
scheduler.setTimeout(function() setTimeout(function()
{ {
setUpTrackingCSS(); setUpTrackingCSS();
}, 0); }, 0);
@ -647,7 +657,7 @@ function Ace2Inner(){
root.style.lineHeight = textLineHeight() + "px"; root.style.lineHeight = textLineHeight() + "px";
sideDiv.style.lineHeight = textLineHeight() + "px"; sideDiv.style.lineHeight = textLineHeight() + "px";
lineMetricsDiv.style.fontSize = textSize + "px"; lineMetricsDiv.style.fontSize = textSize + "px";
scheduler.setTimeout(function() setTimeout(function()
{ {
setUpTrackingCSS(); setUpTrackingCSS();
}, 0); }, 0);
@ -1085,7 +1095,7 @@ function Ace2Inner(){
{ {
if (scheduledTimeout) if (scheduledTimeout)
{ {
scheduler.clearTimeout(scheduledTimeout); clearTimeout(scheduledTimeout);
scheduledTimeout = null; scheduledTimeout = null;
} }
} }
@ -1096,7 +1106,7 @@ function Ace2Inner(){
scheduledTime = time; scheduledTime = time;
var delay = time - now(); var delay = time - now();
if (delay < 0) delay = 0; if (delay < 0) delay = 0;
scheduledTimeout = scheduler.setTimeout(callback, delay); scheduledTimeout = setTimeout(callback, delay);
} }
function callback() function callback()
@ -2817,7 +2827,6 @@ function Ace2Inner(){
rep.selStart = selectStart; rep.selStart = selectStart;
rep.selEnd = selectEnd; rep.selEnd = selectEnd;
rep.selFocusAtStart = newSelFocusAtStart; rep.selFocusAtStart = newSelFocusAtStart;
if (mozillaFakeArrows) mozillaFakeArrows.notifySelectionChanged();
currentCallStack.repChanged = true; currentCallStack.repChanged = true;
return true; return true;
@ -3614,7 +3623,7 @@ function Ace2Inner(){
evt.preventDefault(); evt.preventDefault();
doReturnKey(); doReturnKey();
//scrollSelectionIntoView(); //scrollSelectionIntoView();
scheduler.setTimeout(function() setTimeout(function()
{ {
outerWin.scrollBy(-100, 0); outerWin.scrollBy(-100, 0);
}, 0); }, 0);
@ -3690,11 +3699,41 @@ function Ace2Inner(){
doDeleteKey(); doDeleteKey();
specialHandled = true; specialHandled = true;
} }
if((evt.which == 33 || evt.which == 34) && type == 'keydown'){
var oldVisibleLineRange = getVisibleLineRange();
var topOffset = rep.selStart[0] - oldVisibleLineRange[0];
if(topOffset < 0 ){
topOffset = 0;
}
if (mozillaFakeArrows && mozillaFakeArrows.handleKeyEvent(evt)) var isPageDown = evt.which === 34;
{ var isPageUp = evt.which === 33;
evt.preventDefault();
specialHandled = true; setTimeout(function(){
var newVisibleLineRange = getVisibleLineRange();
var linesCount = rep.lines.length();
var newCaretRow = rep.selStart[0];
if(isPageUp){
newCaretRow = oldVisibleLineRange[0];
}
if(isPageDown){
newCaretRow = newVisibleLineRange[0] + topOffset;
}
//ensure min and max
if(newCaretRow < 0){
newCaretRow = 0;
}
if(newCaretRow >= linesCount){
newCaretRow = linesCount-1;
}
rep.selStart[0] = newCaretRow;
rep.selEnd[0] = newCaretRow;
updateBrowserSelectionFromRep();
}, 200);
} }
} }
@ -4119,6 +4158,11 @@ function Ace2Inner(){
selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset); selection.startPoint = pointFromRangeBound(range.startContainer, range.startOffset);
selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset); selection.endPoint = pointFromRangeBound(range.endContainer, range.endOffset);
selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset)); selection.focusAtStart = (((range.startContainer != range.endContainer) || (range.startOffset != range.endOffset)) && browserSelection.anchorNode && (browserSelection.anchorNode == range.endContainer) && (browserSelection.anchorOffset == range.endOffset));
if(selection.startPoint.node.ownerDocument !== window.document){
return null;
}
return selection; return selection;
} }
else return null; else return null;
@ -4722,7 +4766,7 @@ function Ace2Inner(){
}); });
scheduler.setTimeout(function() setTimeout(function()
{ {
parent.readyFunc(); // defined in code that sets up the inner iframe parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0); }, 0);
@ -5032,331 +5076,6 @@ function Ace2Inner(){
} }
editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList; editorInfo.ace_doInsertUnorderedList = doInsertUnorderedList;
editorInfo.ace_doInsertOrderedList = doInsertOrderedList; editorInfo.ace_doInsertOrderedList = doInsertOrderedList;
var mozillaFakeArrows = (browser.mozilla && (function()
{
// In Firefox 2, arrow keys are unstable while DOM-manipulating
// operations are going on. Specifically, if an operation
// (computation that ties up the event queue) is going on (in the
// call-stack of some event, like a timeout) that at some point
// mutates nodes involved in the selection, then the arrow
// keypress may (randomly) move the caret to the beginning or end
// of the document. If the operation also mutates the selection
// range, the old selection or the new selection may be used, or
// neither.
// As long as the arrow is pressed during the busy operation, it
// doesn't seem to matter that the keydown and keypress events
// aren't generated until afterwards, or that the arrow movement
// can still be stopped (meaning it hasn't been performed yet);
// Firefox must be preserving some old information about the
// selection or the DOM from when the key was initially pressed.
// However, it also doesn't seem to matter when the key was
// actually pressed relative to the time of the mutation within
// the prolonged operation. Also, even in very controlled tests
// (like a mutation followed by a long period of busyWaiting), the
// problem shows up often but not every time, with no discernable
// pattern. Who knows, it could have something to do with the
// caret-blinking timer, or DOM changes not being applied
// immediately.
// This problem, mercifully, does not show up at all in IE or
// Safari. My solution is to have my own, full-featured arrow-key
// implementation for Firefox.
// Note that the problem addressed here is potentially very subtle,
// especially if the operation is quick and is timed to usually happen
// when the user is idle.
// features:
// - 'up' and 'down' arrows preserve column when passing through shorter lines
// - shift-arrows extend the "focus" point, which may be start or end of range
// - the focus point is kept horizontally and vertically scrolled into view
// - arrows without shift cause caret to move to beginning or end of selection (left,right)
// or move focus point up or down a line (up,down)
// - command-(left,right,up,down) on Mac acts like (line-start, line-end, doc-start, doc-end)
// - takes wrapping into account when doesWrap is true, i.e. up-arrow and down-arrow move
// between the virtual lines within a wrapped line; this was difficult, and unfortunately
// requires mutating the DOM to get the necessary information
var savedFocusColumn = 0; // a value of 0 has no effect
var updatingSelectionNow = false;
function getVirtualLineView(lineNum)
{
var lineNode = rep.lines.atIndex(lineNum).lineNode;
while (lineNode.firstChild && isBlockElement(lineNode.firstChild))
{
lineNode = lineNode.firstChild;
}
return makeVirtualLineView(lineNode);
}
function markerlessLineAndChar(line, chr)
{
return [line, chr - rep.lines.atIndex(line).lineMarker];
}
function markerfulLineAndChar(line, chr)
{
return [line, chr + rep.lines.atIndex(line).lineMarker];
}
return {
notifySelectionChanged: function()
{
if (!updatingSelectionNow)
{
savedFocusColumn = 0;
}
},
handleKeyEvent: function(evt)
{
// returns "true" if handled
if (evt.type != "keypress") return false;
var keyCode = evt.keyCode;
if (keyCode < 37 || keyCode > 40) return false;
incorporateUserChanges();
if (!(rep.selStart && rep.selEnd)) return true;
// {byWord,toEnd,normal}
var moveMode = (evt.altKey ? "byWord" : (evt.ctrlKey ? "byWord" : (evt.metaKey ? "toEnd" : "normal")));
var anchorCaret = markerlessLineAndChar(rep.selStart[0], rep.selStart[1]);
var focusCaret = markerlessLineAndChar(rep.selEnd[0], rep.selEnd[1]);
var wasCaret = isCaret();
if (rep.selFocusAtStart)
{
var tmp = anchorCaret;
anchorCaret = focusCaret;
focusCaret = tmp;
}
var K_UP = 38,
K_DOWN = 40,
K_LEFT = 37,
K_RIGHT = 39;
var dontMove = false;
if (wasCaret && !evt.shiftKey)
{
// collapse, will mutate both together
anchorCaret = focusCaret;
}
else if ((!wasCaret) && (!evt.shiftKey))
{
if (keyCode == K_LEFT)
{
// place caret at beginning
if (rep.selFocusAtStart) anchorCaret = focusCaret;
else focusCaret = anchorCaret;
if (moveMode == "normal") dontMove = true;
}
else if (keyCode == K_RIGHT)
{
// place caret at end
if (rep.selFocusAtStart) focusCaret = anchorCaret;
else anchorCaret = focusCaret;
if (moveMode == "normal") dontMove = true;
}
else
{
// collapse, will mutate both together
anchorCaret = focusCaret;
}
}
if (!dontMove)
{
function lineLength(i)
{
var entry = rep.lines.atIndex(i);
return entry.text.length - entry.lineMarker;
}
function lineText(i)
{
var entry = rep.lines.atIndex(i);
return entry.text.substring(entry.lineMarker);
}
if (keyCode == K_UP || keyCode == K_DOWN)
{
var up = (keyCode == K_UP);
var canChangeLines = ((up && focusCaret[0]) || ((!up) && focusCaret[0] < rep.lines.length() - 1));
var virtualLineView, virtualLineSpot, canChangeVirtualLines = false;
if (doesWrap)
{
virtualLineView = getVirtualLineView(focusCaret[0]);
virtualLineSpot = virtualLineView.getVLineAndOffsetForChar(focusCaret[1]);
canChangeVirtualLines = ((up && virtualLineSpot.vline > 0) || ((!up) && virtualLineSpot.vline < (
virtualLineView.getNumVirtualLines() - 1)));
}
var newColByVirtualLineChange;
if (moveMode == "toEnd")
{
if (up)
{
focusCaret[0] = 0;
focusCaret[1] = 0;
}
else
{
focusCaret[0] = rep.lines.length() - 1;
focusCaret[1] = lineLength(focusCaret[0]);
}
}
else if (moveMode == "byWord")
{
// move by "paragraph", a feature that Firefox lacks but IE and Safari both have
if (up)
{
if (focusCaret[1] === 0 && canChangeLines)
{
focusCaret[0]--;
focusCaret[1] = 0;
}
else focusCaret[1] = 0;
}
else
{
var lineLen = lineLength(focusCaret[0]);
if (browser.windows)
{
if (canChangeLines)
{
focusCaret[0]++;
focusCaret[1] = 0;
}
else
{
focusCaret[1] = lineLen;
}
}
else
{
if (focusCaret[1] == lineLen && canChangeLines)
{
focusCaret[0]++;
focusCaret[1] = lineLength(focusCaret[0]);
}
else
{
focusCaret[1] = lineLen;
}
}
}
savedFocusColumn = 0;
}
else if (canChangeVirtualLines)
{
var vline = virtualLineSpot.vline;
var offset = virtualLineSpot.offset;
if (up) vline--;
else vline++;
if (savedFocusColumn > offset) offset = savedFocusColumn;
else
{
savedFocusColumn = offset;
}
var newSpot = virtualLineView.getCharForVLineAndOffset(vline, offset);
focusCaret[1] = newSpot.lineChar;
}
else if (canChangeLines)
{
if (up) focusCaret[0]--;
else focusCaret[0]++;
var offset = focusCaret[1];
if (doesWrap)
{
offset = virtualLineSpot.offset;
}
if (savedFocusColumn > offset) offset = savedFocusColumn;
else
{
savedFocusColumn = offset;
}
if (doesWrap)
{
var newLineView = getVirtualLineView(focusCaret[0]);
var vline = (up ? newLineView.getNumVirtualLines() - 1 : 0);
var newSpot = newLineView.getCharForVLineAndOffset(vline, offset);
focusCaret[1] = newSpot.lineChar;
}
else
{
var lineLen = lineLength(focusCaret[0]);
if (offset > lineLen) offset = lineLen;
focusCaret[1] = offset;
}
}
else
{
if (up) focusCaret[1] = 0;
else focusCaret[1] = lineLength(focusCaret[0]);
savedFocusColumn = 0;
}
}
else if (keyCode == K_LEFT || keyCode == K_RIGHT)
{
var left = (keyCode == K_LEFT);
if (left)
{
if (moveMode == "toEnd") focusCaret[1] = 0;
else if (focusCaret[1] > 0)
{
if (moveMode == "byWord")
{
focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false);
}
else
{
focusCaret[1]--;
}
}
else if (focusCaret[0] > 0)
{
focusCaret[0]--;
focusCaret[1] = lineLength(focusCaret[0]);
if (moveMode == "byWord")
{
focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], false);
}
}
}
else
{
var lineLen = lineLength(focusCaret[0]);
if (moveMode == "toEnd") focusCaret[1] = lineLen;
else if (focusCaret[1] < lineLen)
{
if (moveMode == "byWord")
{
focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true);
}
else
{
focusCaret[1]++;
}
}
else if (focusCaret[0] < rep.lines.length() - 1)
{
focusCaret[0]++;
focusCaret[1] = 0;
if (moveMode == "byWord")
{
focusCaret[1] = moveByWordInLine(lineText(focusCaret[0]), focusCaret[1], true);
}
}
}
savedFocusColumn = 0;
}
}
var newSelFocusAtStart = ((focusCaret[0] < anchorCaret[0]) || (focusCaret[0] == anchorCaret[0] && focusCaret[1] < anchorCaret[1]));
var newSelStart = (newSelFocusAtStart ? focusCaret : anchorCaret);
var newSelEnd = (newSelFocusAtStart ? anchorCaret : focusCaret);
updatingSelectionNow = true;
performSelectionChange(markerfulLineAndChar(newSelStart[0], newSelStart[1]), markerfulLineAndChar(newSelEnd[0], newSelEnd[1]), newSelFocusAtStart);
updatingSelectionNow = false;
currentCallStack.userChangedSelection = true;
return true;
}
};
})());
var lineNumbersShown; var lineNumbersShown;
var sideDivInner; var sideDivInner;
@ -5495,7 +5214,7 @@ function Ace2Inner(){
documentAttributeManager: documentAttributeManager documentAttributeManager: documentAttributeManager
}); });
scheduler.setTimeout(function() setTimeout(function()
{ {
parent.readyFunc(); // defined in code that sets up the inner iframe parent.readyFunc(); // defined in code that sets up the inner iframe
}, 0); }, 0);

View file

@ -23,7 +23,7 @@
var AttributePool = require('./AttributePool'); var AttributePool = require('./AttributePool');
var Changeset = require('./Changeset'); var Changeset = require('./Changeset');
function makeChangesetTracker(scheduler, apool, aceCallbacksProvider) function makeChangesetTracker(apool, aceCallbacksProvider)
{ {
// latest official text from server // latest official text from server
@ -51,7 +51,7 @@ function makeChangesetTracker(scheduler, apool, aceCallbacksProvider)
// and if there isn't a timeout already scheduled. // and if there isn't a timeout already scheduled.
if (changeCallback && changeCallbackTimeout === null) if (changeCallback && changeCallbackTimeout === null)
{ {
changeCallbackTimeout = scheduler.setTimeout(function() changeCallbackTimeout = setTimeout(function()
{ {
try try
{ {

View file

@ -30,8 +30,7 @@ var Security = require('./security');
var hooks = require('./pluginfw/hooks'); var hooks = require('./pluginfw/hooks');
var _ = require('./underscore'); var _ = require('./underscore');
var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker; var lineAttributeMarker = require('./linestylefilter').lineAttributeMarker;
var Ace2Common = require('./ace2_common'); var noop = function(){};
var noop = Ace2Common.noop;
var domline = {}; var domline = {};

View file

@ -23,27 +23,27 @@
window.html10n = (function(window, document, undefined) { window.html10n = (function(window, document, undefined) {
// fix console // fix console
var console = window.console var console = window.console;
function interceptConsole(method){ function interceptConsole(method){
if (!console) return function() {} if (!console) return function() {};
var original = console[method] var original = console[method];
// do sneaky stuff // do sneaky stuff
if (original.bind){ if (original.bind){
// Do this for normal browsers // Do this for normal browsers
return original.bind(console) return original.bind(console);
}else{ }else{
return function() { return function() {
// Do this for IE // Do this for IE
var message = Array.prototype.slice.apply(arguments).join(' ') var message = Array.prototype.slice.apply(arguments).join(' ');
original(message) original(message);
} }
} }
} }
var consoleLog = interceptConsole('log') var consoleLog = interceptConsole('log')
, consoleWarn = interceptConsole('warn') , consoleWarn = interceptConsole('warn')
, consoleError = interceptConsole('warn') , consoleError = interceptConsole('warn');
// fix Array.prototype.instanceOf in, guess what, IE! <3 // fix Array.prototype.instanceOf in, guess what, IE! <3
@ -84,14 +84,14 @@ window.html10n = (function(window, document, undefined) {
* MicroEvent - to make any js object an event emitter (server or browser) * MicroEvent - to make any js object an event emitter (server or browser)
*/ */
var MicroEvent = function(){} var MicroEvent = function(){}
MicroEvent.prototype = { MicroEvent.prototype = {
bind : function(event, fct){ bind : function(event, fct){
this._events = this._events || {}; this._events = this._events || {};
this._events[event] = this._events[event] || []; this._events[event] = this._events[event] || [];
this._events[event].push(fct); this._events[event].push(fct);
}, },
unbind : function(event, fct){ unbind : function(event, fct){
this._events = this._events || {}; this._events = this._events || {};
if( event in this._events === false ) return; if( event in this._events === false ) return;
this._events[event].splice(this._events[event].indexOf(fct), 1); this._events[event].splice(this._events[event].indexOf(fct), 1);
@ -100,7 +100,7 @@ window.html10n = (function(window, document, undefined) {
this._events = this._events || {}; this._events = this._events || {};
if( event in this._events === false ) return; if( event in this._events === false ) return;
for(var i = 0; i < this._events[event].length; i++){ for(var i = 0; i < this._events[event].length; i++){
this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)) this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1));
} }
} }
}; };
@ -122,50 +122,50 @@ window.html10n = (function(window, document, undefined) {
* and caching all necessary resources * and caching all necessary resources
*/ */
function Loader(resources) { function Loader(resources) {
this.resources = resources this.resources = resources;
this.cache = {} // file => contents this.cache = {}; // file => contents
this.langs = {} // lang => strings this.langs = {}; // lang => strings
} }
Loader.prototype.load = function(lang, cb) { Loader.prototype.load = function(lang, cb) {
if(this.langs[lang]) return cb() if(this.langs[lang]) return cb();
if (this.resources.length > 0) { if (this.resources.length > 0) {
var reqs = 0; var reqs = 0;
for (var i=0, n=this.resources.length; i < n; i++) { for (var i=0, n=this.resources.length; i < n; i++) {
this.fetch(this.resources[i], lang, function(e) { this.fetch(this.resources[i], lang, function(e) {
reqs++; reqs++;
if(e) return setTimeout(function(){ throw e }, 0) if(e) return setTimeout(function(){ throw e }, 0);
if (reqs < n) return;// Call back once all reqs are completed if (reqs < n) return;// Call back once all reqs are completed
cb && cb() cb && cb();
}) })
} }
} }
} }
Loader.prototype.fetch = function(href, lang, cb) { Loader.prototype.fetch = function(href, lang, cb) {
var that = this var that = this;
if (this.cache[href]) { if (this.cache[href]) {
this.parse(lang, href, this.cache[href], cb) this.parse(lang, href, this.cache[href], cb)
return; return;
} }
var xhr = new XMLHttpRequest() var xhr = new XMLHttpRequest();
xhr.open('GET', href, /*async: */true) xhr.open('GET', href, /*async: */true);
if (xhr.overrideMimeType) { if (xhr.overrideMimeType) {
xhr.overrideMimeType('application/json; charset=utf-8'); xhr.overrideMimeType('application/json; charset=utf-8');
} }
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status === 0) { if (xhr.status == 200 || xhr.status === 0) {
var data = JSON.parse(xhr.responseText) var data = JSON.parse(xhr.responseText);
that.cache[href] = data that.cache[href] = data;
// Pass on the contents for parsing // Pass on the contents for parsing
that.parse(lang, href, data, cb) that.parse(lang, href, data, cb);
} else { } else {
cb(new Error('Failed to load '+href)) cb(new Error('Failed to load '+href));
} }
} }
}; };
@ -174,39 +174,39 @@ window.html10n = (function(window, document, undefined) {
Loader.prototype.parse = function(lang, currHref, data, cb) { Loader.prototype.parse = function(lang, currHref, data, cb) {
if ('object' != typeof data) { if ('object' != typeof data) {
cb(new Error('A file couldn\'t be parsed as json.')) cb(new Error('A file couldn\'t be parsed as json.'));
return return;
} }
if (!data[lang]) lang = lang.substr(0, lang.indexOf('-') == -1? lang.length : lang.indexOf('-')) if (!data[lang]) lang = lang.substr(0, lang.indexOf('-') == -1? lang.length : lang.indexOf('-'));
if (!data[lang]) { if (!data[lang]) {
cb(new Error('Couldn\'t find translations for '+lang)) cb(new Error('Couldn\'t find translations for '+lang));
return return;
} }
if ('string' == typeof data[lang]) { if ('string' == typeof data[lang]) {
// Import rule // Import rule
// absolute path // absolute path
var importUrl = data[lang] var importUrl = data[lang];
// relative path // relative path
if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) { if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
importUrl = currHref+"/../"+data[lang] importUrl = currHref+"/../"+data[lang];
} }
this.fetch(importUrl, lang, cb) this.fetch(importUrl, lang, cb);
return return;
} }
if ('object' != typeof data[lang]) { if ('object' != typeof data[lang]) {
cb(new Error('Translations should be specified as JSON objects!')) cb(new Error('Translations should be specified as JSON objects!'));
return return;
} }
this.langs[lang] = data[lang] this.langs[lang] = data[lang];
// TODO: Also store accompanying langs // TODO: Also store accompanying langs
cb() cb();
} }
@ -216,11 +216,11 @@ window.html10n = (function(window, document, undefined) {
var html10n = var html10n =
{ language : null { language : null
} }
MicroEvent.mixin(html10n) MicroEvent.mixin(html10n);
html10n.macros = {} html10n.macros = {};
html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"] html10n.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"];
/** /**
* Get rules for plural forms (shared with JetPack), see: * Get rules for plural forms (shared with JetPack), see:
@ -664,14 +664,14 @@ window.html10n = (function(window, document, undefined) {
* @param langs An array of lang codes defining fallbacks * @param langs An array of lang codes defining fallbacks
*/ */
html10n.localize = function(langs) { html10n.localize = function(langs) {
var that = this var that = this;
// if only one string => create an array // if only one string => create an array
if ('string' == typeof langs) langs = [langs] if ('string' == typeof langs) langs = [langs];
this.build(langs, function(er, translations) { this.build(langs, function(er, translations) {
html10n.translations = translations html10n.translations = translations;
html10n.translateElement(translations) html10n.translateElement(translations);
that.trigger('localized') that.trigger('localized');
}) })
} }
@ -682,78 +682,78 @@ window.html10n = (function(window, document, undefined) {
* @param element A DOM element, if omitted, the document element will be used * @param element A DOM element, if omitted, the document element will be used
*/ */
html10n.translateElement = function(translations, element) { html10n.translateElement = function(translations, element) {
element = element || document.documentElement element = element || document.documentElement;
var children = element? getTranslatableChildren(element) : document.childNodes; var children = element? getTranslatableChildren(element) : document.childNodes;
for (var i=0, n=children.length; i < n; i++) { for (var i=0, n=children.length; i < n; i++) {
this.translateNode(translations, children[i]) this.translateNode(translations, children[i]);
} }
// translate element itself if necessary // translate element itself if necessary
this.translateNode(translations, element) this.translateNode(translations, element);
} }
function asyncForEach(list, iterator, cb) { function asyncForEach(list, iterator, cb) {
var i = 0 var i = 0
, n = list.length , n = list.length;
iterator(list[i], i, function each(err) { iterator(list[i], i, function each(err) {
if(err) consoleLog(err) if(err) consoleLog(err);
i++ i++;
if (i < n) return iterator(list[i],i, each); if (i < n) return iterator(list[i],i, each);
cb() cb();
}) })
} }
function getTranslatableChildren(element) { function getTranslatableChildren(element) {
if(!document.querySelectorAll) { if(!document.querySelectorAll) {
if (!element) return [] if (!element) return [];
var nodes = element.getElementsByTagName('*') var nodes = element.getElementsByTagName('*')
, l10nElements = [] , l10nElements = [];
for (var i=0, n=nodes.length; i < n; i++) { for (var i=0, n=nodes.length; i < n; i++) {
if (nodes[i].getAttribute('data-l10n-id')) if (nodes[i].getAttribute('data-l10n-id'))
l10nElements.push(nodes[i]); l10nElements.push(nodes[i]);
} }
return l10nElements return l10nElements;
} }
return element.querySelectorAll('*[data-l10n-id]') return element.querySelectorAll('*[data-l10n-id]');
} }
html10n.get = function(id, args) { html10n.get = function(id, args) {
var translations = html10n.translations var translations = html10n.translations;
if(!translations) return consoleWarn('No translations available (yet)') if(!translations) return consoleWarn('No translations available (yet)');
if(!translations[id]) return consoleWarn('Could not find string '+id) if(!translations[id]) return consoleWarn('Could not find string '+id);
// apply args // apply args
var str = substArguments(translations[id], args) var str = substArguments(translations[id], args);
// apply macros // apply macros
return substMacros(id, str, args) return substMacros(id, str, args);
// replace {{arguments}} with their values or the // replace {{arguments}} with their values or the
// associated translation string (based on its key) // associated translation string (based on its key)
function substArguments(str, args) { function substArguments(str, args) {
var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/ var reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
, match , match;
while (match = reArgs.exec(str)) { while (match = reArgs.exec(str)) {
if (!match || match.length < 2) if (!match || match.length < 2)
return str // argument key not found return str; // argument key not found
var arg = match[1] var arg = match[1]
, sub = '' , sub = '';
if (arg in args) { if (arg in args) {
sub = args[arg] sub = args[arg];
} else if (arg in translations) { } else if (arg in translations) {
sub = translations[arg] sub = translations[arg];
} else { } else {
consoleWarn('Could not find argument {{' + arg + '}}') consoleWarn('Could not find argument {{' + arg + '}}');
return str return str;
} }
str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length) str = str.substring(0, match.index) + sub + str.substr(match.index + match[0].length);
} }
return str return str;
} }
// replace {[macros]} with their values // replace {[macros]} with their values
@ -766,21 +766,21 @@ window.html10n = (function(window, document, undefined) {
// a macro has been found // a macro has been found
// Note: at the moment, only one parameter is supported // Note: at the moment, only one parameter is supported
var macroName = reMatch[1] var macroName = reMatch[1]
, paramName = reMatch[2] , paramName = reMatch[2];
if (!(macroName in gMacros)) return str if (!(macroName in gMacros)) return str;
var param var param;
if (args && paramName in args) { if (args && paramName in args) {
param = args[paramName] param = args[paramName];
} else if (paramName in translations) { } else if (paramName in translations) {
param = translations[paramName] param = translations[paramName];
} }
// there's no macro parser yet: it has to be defined in gMacros // there's no macro parser yet: it has to be defined in gMacros
var macro = html10n.macros[macroName] var macro = html10n.macros[macroName];
str = macro(translations, key, str, param) str = macro(translations, key, str, param);
return str return str;
} }
} }
@ -788,26 +788,26 @@ window.html10n = (function(window, document, undefined) {
* Applies translations to a DOM node (recursive) * Applies translations to a DOM node (recursive)
*/ */
html10n.translateNode = function(translations, node) { html10n.translateNode = function(translations, node) {
var str = {} var str = {};
// get id // get id
str.id = node.getAttribute('data-l10n-id') str.id = node.getAttribute('data-l10n-id');
if (!str.id) return if (!str.id) return;
if(!translations[str.id]) return consoleWarn('Couldn\'t find translation key '+str.id) if(!translations[str.id]) return consoleWarn('Couldn\'t find translation key '+str.id);
// get args // get args
if(window.JSON) { if(window.JSON) {
str.args = JSON.parse(node.getAttribute('data-l10n-args')) str.args = JSON.parse(node.getAttribute('data-l10n-args'));
}else{ }else{
try{ try{
str.args = eval(node.getAttribute('data-l10n-args')) str.args = eval(node.getAttribute('data-l10n-args'));
}catch(e) { }catch(e) {
consoleWarn('Couldn\'t parse args for '+str.id) consoleWarn('Couldn\'t parse args for '+str.id);
} }
} }
str.str = html10n.get(str.id, str.args) str.str = html10n.get(str.id, str.args);
// get attribute name to apply str to // get attribute name to apply str to
var prop var prop
@ -817,31 +817,31 @@ window.html10n = (function(window, document, undefined) {
, "innerHTML": 1 , "innerHTML": 1
, "alt": 1 , "alt": 1
, "textContent": 1 , "textContent": 1
} };
if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified if (index > 0 && str.id.substr(index + 1) in attrList) { // an attribute has been specified
prop = str.id.substr(index + 1) prop = str.id.substr(index + 1);
} else { // no attribute: assuming text content by default } else { // no attribute: assuming text content by default
prop = document.body.textContent ? 'textContent' : 'innerText' prop = document.body.textContent ? 'textContent' : 'innerText';
} }
// Apply translation // Apply translation
if (node.children.length === 0 || prop != 'textContent') { if (node.children.length === 0 || prop != 'textContent') {
node[prop] = str.str node[prop] = str.str;
} else { } else {
var children = node.childNodes, var children = node.childNodes,
found = false found = false;
for (var i=0, n=children.length; i < n; i++) { for (var i=0, n=children.length; i < n; i++) {
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) { if (children[i].nodeType === 3 && /\S/.test(children[i].textContent)) {
if (!found) { if (!found) {
children[i].nodeValue = str.str children[i].nodeValue = str.str;
found = true found = true;
} else { } else {
children[i].nodeValue = '' children[i].nodeValue = '';
} }
} }
} }
if (!found) { if (!found) {
consoleWarn('Unexpected error: could not translate element content for key '+str.id, node) consoleWarn('Unexpected error: could not translate element content for key '+str.id, node);
} }
} }
} }
@ -852,32 +852,32 @@ window.html10n = (function(window, document, undefined) {
*/ */
html10n.build = function(langs, cb) { html10n.build = function(langs, cb) {
var that = this var that = this
, build = {} , build = {};
asyncForEach(langs, function (lang, i, next) { asyncForEach(langs, function (lang, i, next) {
if(!lang) return next(); if(!lang) return next();
that.loader.load(lang, next) that.loader.load(lang, next);
}, function() { }, function() {
var lang var lang;
langs.reverse() langs.reverse();
// loop through priority array... // loop through priority array...
for (var i=0, n=langs.length; i < n; i++) { for (var i=0, n=langs.length; i < n; i++) {
lang = langs[i] lang = langs[i];
if(!lang || !(lang in that.loader.langs)) continue; if(!lang || !(lang in that.loader.langs)) continue;
// ... and apply all strings of the current lang in the list // ... and apply all strings of the current lang in the list
// to our build object // to our build object
for (var string in that.loader.langs[lang]) { for (var string in that.loader.langs[lang]) {
build[string] = that.loader.langs[lang][string] build[string] = that.loader.langs[lang][string];
} }
// the last applied lang will be exposed as the // the last applied lang will be exposed as the
// lang the page was translated to // lang the page was translated to
that.language = lang that.language = lang;
} }
cb(null, build) cb(null, build);
}) })
} }
@ -893,8 +893,8 @@ window.html10n = (function(window, document, undefined) {
* Returns the direction of the language returned be html10n#getLanguage * Returns the direction of the language returned be html10n#getLanguage
*/ */
html10n.getDirection = function() { html10n.getDirection = function() {
var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-')) var langCode = this.language.indexOf('-') == -1? this.language : this.language.substr(0, this.language.indexOf('-'));
return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl' return html10n.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl';
} }
/** /**
@ -903,28 +903,28 @@ window.html10n = (function(window, document, undefined) {
html10n.index = function () { html10n.index = function () {
// Find all <link>s // Find all <link>s
var links = document.getElementsByTagName('link') var links = document.getElementsByTagName('link')
, resources = [] , resources = [];
for (var i=0, n=links.length; i < n; i++) { for (var i=0, n=links.length; i < n; i++) {
if (links[i].type != 'application/l10n+json') if (links[i].type != 'application/l10n+json')
continue; continue;
resources.push(links[i].href) resources.push(links[i].href);
} }
this.loader = new Loader(resources) this.loader = new Loader(resources);
this.trigger('indexed') this.trigger('indexed');
} }
if (document.addEventListener) // modern browsers and IE9+ if (document.addEventListener) // modern browsers and IE9+
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
html10n.index() html10n.index();
}, false) }, false);
else if (window.attachEvent) else if (window.attachEvent)
window.attachEvent('onload', function() { window.attachEvent('onload', function() {
html10n.index() html10n.index();
}, false) }, false);
// gettext-like shortcut // gettext-like shortcut
if (window._ === undefined) if (window._ === undefined)
window._ = html10n.get; window._ = html10n.get;
return html10n return html10n;
})(window, document) })(window, document);

View file

@ -94,11 +94,12 @@ exports.search = function(query, cache, cb) {
if (er) return cb(er); if (er) return cb(er);
var res = {}; var res = {};
var i = 0; var i = 0;
var pattern = query.pattern.toLowerCase();
for (key in data) { // for every plugin in the data from npm for (key in data) { // for every plugin in the data from npm
if ( key.indexOf(plugins.prefix) == 0 if ( key.indexOf(plugins.prefix) == 0
&& key.indexOf(query.pattern) != -1 && key.indexOf(pattern) != -1
|| key.indexOf(plugins.prefix) == 0 || key.indexOf(plugins.prefix) == 0
&& data[key].description.indexOf(query.pattern) != -1 && data[key].description.indexOf(pattern) != -1
) { // If the name contains ep_ and the search string is in the name or description ) { // If the name contains ep_ and the search string is in the name or description
i++; i++;
if (i > query.offset if (i > query.offset

View file

@ -9,12 +9,15 @@
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div class="menu"> <div class="menu">
<h1>Etherpad lite</h1> <h1>Etherpad lite</h1>
<li><a href="admin/plugins">Plugin manager</a> </li> <ul>
<li><a href="admin/settings">Settings</a> </li> <% e.begin_block("adminMenu"); %>
<li><a href="admin/plugins/info">Troubleshooting information</a> </li> <li><a href="admin/plugins">Plugin manager</a> </li>
<li><a href="admin/settings">Settings</a> </li>
<li><a href="admin/plugins/info">Troubleshooting information</a> </li>
<% e.end_block(); %>
</ul>
</div> </div>
</div> </div>
<div id="topborder"></div> <div id="topborder"></div>
</body> </body>

View file

@ -12,9 +12,13 @@
<div id="wrapper"> <div id="wrapper">
<div class="menu"> <div class="menu">
<h1>Etherpad lite</h1> <h1>Etherpad lite</h1>
<li><a href="../plugins">Plugin manager</a> </li> <ul>
<li><a href="../settings">Settings</a> </li> <% e.begin_block("adminMenu"); %>
<li><a href="../plugins/info">Troubleshooting information</a> </li> <li><a href="../plugins">Plugin manager</a> </li>
<li><a href="../settings">Settings</a> </li>
<li><a href="../plugins/info">Troubleshooting information</a> </li>
<% e.end_block(); %>
</ul>
</div> </div>
<div class="innerwrapper"> <div class="innerwrapper">

View file

@ -19,11 +19,14 @@
<% } %> <% } %>
<div class="menu"> <div class="menu">
<h1>Etherpad lite</h1> <h1>Etherpad lite</h1>
<li><a href="plugins">Plugin manager</a> </li> <ul>
<li><a href="settings">Settings</a> </li> <% e.begin_block("adminMenu"); %>
<li><a href="plugins/info">Troubleshooting information</a> </li> <li><a href="plugins">Plugin manager</a> </li>
<li><a href="settings">Settings</a> </li>
<li><a href="plugins/info">Troubleshooting information</a> </li>
<% e.end_block(); %>
</ul>
<div id="progress"><img src="../static/img/loading.gif" alt=""/>&nbsp;&nbsp;<span class="message"></span></div> <div id="progress"><img src="../static/img/loading.gif" alt=""/>&nbsp;&nbsp;<span class="message"></span></div>
</div> </div>

View file

@ -24,9 +24,13 @@
<div class="menu"> <div class="menu">
<h1>Etherpad lite</h1> <h1>Etherpad lite</h1>
<li><a href="plugins">Plugin manager</a> </li> <ul>
<li><a href="settings">Settings</a> </li> <% e.begin_block("adminMenu"); %>
<li><a href="plugins/info">Troubleshooting information</a> </li> <li><a href="plugins">Plugin manager</a> </li>
<li><a href="settings">Settings</a> </li>
<li><a href="plugins/info">Troubleshooting information</a> </li>
<% e.end_block(); %>
</ul>
</div> </div>
<div class="innerwrapper"> <div class="innerwrapper">

View file

@ -64,7 +64,8 @@
box-shadow: 0px 1px 8px rgba(0,0,0,0.3); box-shadow: 0px 1px 8px rgba(0,0,0,0.3);
} }
#inner { #inner {
width: 300px; position:relative;
max-width: 300px;
margin: 0 auto; margin: 0 auto;
} }
#button { #button {
@ -100,6 +101,10 @@
text-shadow: 0 1px 1px #fff; text-shadow: 0 1px 1px #fff;
margin: 16px auto 0; margin: 16px auto 0;
} }
#padname{
height:38px;
max-width:280px;
}
form { form {
height: 38px; height: 38px;
background: #fff; background: #fff;
@ -115,7 +120,8 @@
border-radius: 3px; border-radius: 3px;
box-sizing: border-box; box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
padding: 0 45px 0 10px; line-height:36px; /* IE8 hack */
padding: 0px 45px 0 10px;
*padding: 0; /* IE7 hack */ *padding: 0; /* IE7 hack */
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -125,7 +131,7 @@
} }
button[type="submit"] { button[type="submit"] {
position: absolute; position: absolute;
right: 0; left:253px;
width: 45px; width: 45px;
height: 38px; height: 38px;
} }

View file

@ -0,0 +1,64 @@
describe("timeslider", function(){
//create a new pad before each test run
beforeEach(function(cb){
helper.newPad(cb);
this.timeout(60000);
});
it("Shows a date and time in the timeslider and make sure it doesn't include NaN", function(done) {
var inner$ = helper.padInner$;
var chrome$ = helper.padChrome$;
// make some changes to produce 100 revisions
var revs = 10;
this.timeout(60000);
for(var i=0; i < revs; i++) {
setTimeout(function() {
// enter 'a' in the first text element
inner$("div").first().sendkeys('a');
}, 200);
}
setTimeout(function() {
// go to timeslider
$('#iframe-container iframe').attr('src', $('#iframe-container iframe').attr('src')+'/timeslider');
setTimeout(function() {
var timeslider$ = $('#iframe-container iframe')[0].contentWindow.$;
var $sliderBar = timeslider$('#ui-slider-bar');
var latestContents = timeslider$('#padcontent').text();
// Expect the date and time to be shown
// Click somewhere on the timeslider
var e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = 45;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = 40;
$sliderBar.trigger(e);
e = new jQuery.Event('mousedown');
e.clientX = e.pageX = 150;
e.clientY = e.pageY = 50;
$sliderBar.trigger(e);
$sliderBar.trigger('mouseup')
setTimeout(function() {
//make sure the text has changed
expect( timeslider$('#timer').text() ).not.to.eql( "" );
expect( timeslider$('#revision_date').text() ).not.to.eql( "" );
expect( timeslider$('#revision_label').text() ).not.to.eql( "" );
var includesNaN = timeslider$('#revision_label').text().indexOf("NaN"); // NaN is bad. Naan ist gut
expect( includesNaN ).to.eql( -1 ); // not quite so tasty, I like curry.
done();
}, 400);
}, 2000);
}, 2000);
});
});