jquery-crate
Version:
Cratify tool that turns a division into a distributed and decentralized collaborative editor
1,405 lines (1,260 loc) • 3.92 MB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
function CloseButton(model, closeView, container){
// (TODO) remove the model
closeView.button.click(function(){
// #1 remove the view
container.remove();
// #2 disconnect the signaling server
if (model.signaling.startedSocket){
model.signaling.stopSharing();
};
// #3 disconnect the network
model.rps.leave();
});
};
module.exports = CloseButton;
},{}],2:[function(require,module,exports){
var Marker = require('../view/marker.js');
function EditorController(model, viewEditor){
var self = this, editor = viewEditor.editor;
this.viewEditor = viewEditor;
this.fromRemote = false;
// #B initialize the string within the editor
function getStringChildNode(childNode){
var result = '';
if (childNode.e !== null){ result = childNode.e; };
for (var i=0; i<childNode.children.length; ++i){
result += getStringChildNode(childNode.children[i]);
};
return result;
};
editor.setValue(getStringChildNode(model.sequence.root),1);
var insertRemoveOp = false;
editor.getSession().on('change', function(e){
switch(e.data.action){
case 'removeLines':
case 'removeText':
case 'insertLines':
case 'insertText':
insertRemoveOp = true;
}
});
editor.getSession().getSelection().on('changeCursor', function(e, sel){
if (!insertRemoveOp){
var range = sel.getRange();
model.core.caretMoved({
start: editor.getSession().getDocument().positionToIndex(range.start),
end: editor.getSession().getDocument().positionToIndex(range.end)
});
}
insertRemoveOp = false;
});
editor.getSession().on('change', function(e) {
var begin, end, text, message, j=0;
if (!self.fromRemote){
// #1 process the boundaries from range to index and text
begin = editor.getSession().getDocument().positionToIndex(
e.data.range.start);
switch (e.data.action){
case 'removeLines':
end = begin;
for (var i=0; i<e.data.lines.length;++i){
end += e.data.lines[i].length+1; // +1 because of \n
};
remoteCaretsUpdate(begin, begin-end);
break;
case 'removeText':
if (e.data.text.length === 1){
end = begin+1; //faster
} else {
end = editor.getSession().getDocument().positionToIndex(
e.data.range.end);
};
remoteCaretsUpdate(begin, begin-end);
break;
case 'insertLines':
text = '';
for (var i=0; i<e.data.lines.length;++i){
text = text + (e.data.lines[i]) + '\n';
};
end = begin + text.length;
remoteCaretsUpdate(begin, text.length);
break;
case 'insertText':
text = e.data.text;
end = editor.getSession().getDocument().positionToIndex(
e.data.range.end);
remoteCaretsUpdate(begin, text.length);
break;
};
// #2 update the underlying CRDT model and broadcast the results
for (var i=begin; i<end; ++i){
switch (e.data.action){
case 'insertText': model.core.insert(text[j], i); break;
case 'insertLines': model.core.insert(text[j], i); break;
case 'removeText': model.core.remove(begin); break;
case 'removeLines': model.core.remove(begin); break;
};
++j;
};
};
});
model.core.on('remoteInsert', function(element, index){
var aceDocument = editor.getSession().getDocument(),
delta,
tempFromRemote;
if (index!==-1){
delta = {action: 'insertText',
range: { start: aceDocument.indexToPosition(index-1),
end: aceDocument.indexToPosition(index)},
text: element},
tempFromRemote = self.fromRemote;
self.fromRemote = true;
aceDocument.applyDeltas([delta]);
remoteCaretsUpdate(index,1);
self.fromRemote = tempFromRemote;
};
});
model.core.on('remoteRemove', function(index){
var aceDocument = editor.getSession().getDocument(),
delta,
tempFromRemote;
if (index !== -1){
delta = {action: 'removeText',
range: { start: aceDocument.indexToPosition(index - 1),
end: aceDocument.indexToPosition(index)},
text: null};
tempFromRemote = self.fromRemote;
self.fromRemote = true;
aceDocument.applyDeltas([delta]);
remoteCaretsUpdate(index,-1);
self.fromRemote = tempFromRemote;
};
});
model.core.on('remoteCaretMoved', function(range, origin){
if (!origin) return;
if (editor.session.remoteCarets[origin]){
// #A update the existing cursor
var marker = editor.session.remoteCarets[origin];
marker.cursors = [range]; // save the cursors as indexes
editor.getSession()._signal('changeFrontMarker');
marker.refresh();
}else{
// #B create a new cursor
var marker = new Marker(editor.session, origin, range);
editor.session.addDynamicMarker(marker, true);
editor.session.remoteCarets[origin] = marker;
marker.refresh();
// call marker.session.removeMarker(marker.id) to remove it
// call marker.redraw after changing one of cursors
}
});
editor.session.remoteCarets = {};
function remoteCaretsUpdate(index, length){
var change = false, document = editor.session.getDocument();
for (origin in editor.session.remoteCarets){
var remoteCaret = editor.session.remoteCarets[origin];
for (i=0; i<remoteCaret.cursors.length; ++i){
var cursor = remoteCaret.cursors[i];
if (cursor.start >= index){
cursor.start += length;
change = true;
}
if (cursor.end >= index){
cursor.end += length;
change = true;
}
}
}
if (change){
editor.session._signal('changeFrontMarker');
}
};
};
module.exports = EditorController;
},{"../view/marker.js":12}],3:[function(require,module,exports){
//var markdown = require('markdown').markdown;
var marked = require('marked');
marked.setOptions({
renderer: new marked.Renderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false
});
function Preview(buttonView, editorView, previewView){
var self = this;
this.isPreviewing = false;
this.startPreviewText = '<i class="fa fa-eye"></i>';
this.startPreviewTooltip = 'switch to preview';
this.stopPreviewText = '<i class="fa fa-eye-slash"></i>';
this.stopPreviewTooltip = 'switch to editor';
this.refreshTimeout = 5000; // (TODO) configuable
this.refresh = null;
buttonView.button.click(function(){
if (!self.isPreviewing){
self.isPreviewing = true;
editorView.div.hide();
previewView.div.html(marked(editorView.editor.getValue()));
previewView.div.show();
buttonView.button.html(self.stopPreviewText);
buttonView.button.attr('title', self.stopPreviewTooltip)
.tooltip('fixTitle');
self.refresh = setInterval(function(){
previewView.div.html(marked(editorView.editor.getValue()));
}, self.refreshTimeout) ;
} else {
self.isPreviewing = false;
previewView.div.hide();
editorView.div.show();
editorView.editor.resize();
buttonView.button.html(self.startPreviewText);
buttonView.button.attr('title', self.startPreviewTooltip)
.tooltip('fixTitle');
clearTimeout(self.refresh);
self.refresh = null;
};
});
};
module.exports = Preview;
},{"marked":100}],4:[function(require,module,exports){
require('jquery-qrcode');
function StatesHeader(model, statesView, linkView, shareView){
var self = this;
this.model = model;
this.statesView = statesView;
this.startSharingText = '<i class="fa fa-link"></i>';
this.startSharingTooltip = 'start sharing';
this.stopSharingText = '<i class="fa fa-unlink"></i>';
this.stopSharingTooltip = 'stop sharing';
model.broadcast.source.on("statechange", function(state){
switch (state){
case "connect": statesView.setNetworkState('connected'); break;
case "partial": statesView.setNetworkState('partiallyConnected'); break;
case "disconnect": statesView.setNetworkState('disconnected'); break;
};
});
shareView.button.unbind("click").click( function(){
var socket, action, client;
if (model.signaling.startedSocket){
model.signaling.stopSharing();
return ; // ugly as hell
};
// #0 create the proper call to the server
socket = model.signaling.startSharing();
statesView.setSignalingState("waitSignaling");
socket.on("connect", function(){
shareView.button.removeAttr("disabled");
statesView.setSignalingState("waitJoiners");
shareView.button.html(self.stopSharingText);
shareView.button.attr('title', self.stopSharingTooltip)
.tooltip('fixTitle');
});
socket.on("disconnect", function(){
shareView.button.html(self.startSharingText);
shareView.button.attr('title', self.startSharingTooltip)
.tooltip('fixTitle');
});
shareView.button.attr("disabled","disabled");
// #1 modify the view
if (model.signaling.startedSocket){
// #A clean the address from args
var address = (window.location.href).split('?')[0];
// #B add the new argument
action = linkView.printLink(address +"?"+
model.signalingOptions.session);
client = new ZeroClipboard(action);
client.on("ready", function(event){
client.on( "copy", function( event ){
var clipboard = event.clipboardData;
clipboard.setData( "text/plain",
linkView.input.val() );
});
});
};
});
linkView.qrcode.click(function(){
var address = model.signaling.address +
"/index.html?" +
model.signalingOptions.session;
linkView.qrcodeCanvas.html("");
linkView.qrcodeCanvas.qrcode({
size:400,
text:address
});
});
};
StatesHeader.prototype.startJoining = function(signalingOptions){
var socket = this.model.signaling.startJoining(signalingOptions);
this.statesView.setSignalingState('waitSignaling');
var self = this;
socket.on('connect',
function(){ self.statesView.setSignalingState('waitSharer'); });
};
module.exports = StatesHeader;
},{"jquery-qrcode":98}],5:[function(require,module,exports){
var Model = require('./model/model.js');
var GUID = require('./model/guid.js');
var ace = require('brace');
require('brace/theme/chrome');
var VStructure = require('./view/structure.js');
var VEditor = require('./view/editor.js');
var VCloseButton = require('./view/closebutton.js');
var VLink = require('./view/link.js');
var VStatesHeader = require('./view/statesheader.js');
var VMetadata = require('./view/metadata.js');
var VRoundButton = require('./view/roundbutton.js');
var VPreview = require('./view/preview.js');
var CStatesHeader = require('./controller/statesheader.js');
var CCloseButton = require('./controller/closebutton.js');
var CEditor = require('./controller/editor.js');
var CPreview = require('./controller/preview.js');
/*!
* \brief transform the selected division into a distributed and decentralized
* collaborative editor.
* \param options {
* signalingOptions: configure the signaling service to join or share the
* document. {address: http://example.of.signaling.service.address,
* session: the-session-unique-identifier,
* connect: true|false}
* webRTCOptions: configure the STUN/TURN server to establish WebRTC
* connections.
* styleOptions: change the default styling options of the editor.
* name: the name of the document
* importFromJSON: the json object containing the aformentionned options plus
* the saved sequence. If any of the other above options are specified, the
* option in the json object are erased by them.
* }
*/
$.fn.cratify = function(options){
// #0 examine the arguments
// (TODO) apply style options
var styleOptions=$.extend({'headerBackgroundColor': '#242b32',
'headerColor': '#ececec',
'editorBackgroundColor': '#ffffff',
'editorHeight': '400px'},
(options && options.styleOptions) ||
(options && options.importFromJSON &&
options.importFromJSON.styleOptions) ||
{});
var webRTCOptions = (options && options.webRTCOptions) ||
(options && options.importFromJSON &&
options.importFromJSON.webRTCOptions) ||
{};
var signalingOptions=
$.extend(
$.extend({//server: "http://127.0.0.1:5000",
server: "https://ancient-shelf-9067.herokuapp.com",
session: GUID(),
connect: false},
(options && options.importFromJSON &&
options.importFromJSON.signalingOptions) ||
{}),
(options && options.signalingOptions) || {});
var name = (options && options.name) ||
(options && options.importFromJSON &&
options.importFromJSON.name) ||
"default";
return this.each(function(){
// #1 initialize the model
var m = new Model(signalingOptions, webRTCOptions, name,
options.importFromJSON);
// #2 initialize the view
var divId = GUID();
var vs = new VStructure(this);
var ve = new VEditor(vs.body, divId);
var vcb = new VCloseButton(vs.headerRightRightRight);
var vm = new VMetadata(m, vs.headerLeft);
var vsh = new VStatesHeader(m, vs.headerRight);
var vl = new VLink(this, divId);
var vpb = new VRoundButton(vs.headerRightRight,
'<i class="fa fa-eye"></i>',
'switch to preview');
var vp = new VPreview(vs.body);
var vsb = new VRoundButton(vs.headerRightRight,
'<i class="fa fa-link"></i>',
'start sharing');
var vset = new VRoundButton(vs.headerRightRight,
'<i class="fa fa-cogs"></i>',
'settings (disabled)');
// #3 initialize the controllers
var ccb = new CCloseButton(m, vcb, this);
var csh = new CStatesHeader(m, vsh, vl, vsb);
var ce = new CEditor(m, ve);
var cp = new CPreview(vpb, ve, vp);
// #4 grant quick access
this.header = vs.headerRightRight;
this.closeButton = vcb.button;
this.model = m;
// #5 optionnally join an editing session
if (signalingOptions.connect){
csh.startJoining(signalingOptions);
};
});
};
},{"./controller/closebutton.js":1,"./controller/editor.js":2,"./controller/preview.js":3,"./controller/statesheader.js":4,"./model/guid.js":6,"./model/model.js":7,"./view/closebutton.js":9,"./view/editor.js":10,"./view/link.js":11,"./view/metadata.js":13,"./view/preview.js":14,"./view/roundbutton.js":15,"./view/statesheader.js":16,"./view/structure.js":17,"brace":26,"brace/theme/chrome":27}],6:[function(require,module,exports){
/*
* \url https://github.com/justayak/yutils/blob/master/yutils.js
* \author justayak
*/
/*!
* \brief get a globally unique (with high probability) identifier
* \return a string being the identifier
*/
function GUID(){
var d = new Date().getTime();
var guid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return guid;
};
module.exports = GUID;
},{}],7:[function(require,module,exports){
var Core = require('crate-core');
var GUID = require('./guid.js');
var Signaling = require('./signaling.js');
function Model(signalingOptions, webRTCOptions, name, importFromJSON){
// #1A initialize internal variables
this.uid = GUID();
this.name = name;
this.date = new Date(); // (TODO) change
this.webRTCOptions = webRTCOptions;
this.core = new Core(this.uid, {config:webRTCOptions});
this.signaling = new Signaling(this.core.broadcast.source,signalingOptions);
// #1B if it is imported from an existing object, initialize it with these
if (importFromJSON){ this.core.init(importFromJSON); };
// #2 grant fast access
this.broadcast = this.core.broadcast;
this.rps = this.core.broadcast.source;
this.sequence = this.core.sequence;
this.causality = this.broadcast.causality;
this.signalingOptions = this.signaling.signalingOptions;
};
module.exports = Model;
},{"./guid.js":6,"./signaling.js":8,"crate-core":34}],8:[function(require,module,exports){
var io = require('socket.io-client');
/*!
* \brief handle the signaling server
* \param rps the random peer sampling protocol
* \param signalingOptions specific options for the signaling server(s). For
* now, it's an object { server, session, duration } where server is
* the address of the server to contact, session is the editing session to join
* or share, duration is the optional duration time during which the socket with
* the signaling server stays open.
*/
function Signaling(rps, signalingOptions){
this.rps = rps;
this.signalingOptions = signalingOptions;
this.socketIOConfig = { 'force new connection': true,
'reconnection': false };
this.startedSocket = false;
this.socket = null;
this.timeout = null; // event id of the termination
this.joiners = 0;
};
/*!
* \brief create a connection with a socket.io server and initialize the events
*/
Signaling.prototype.createSocket = function(){
var self = this;
// #A establish the dialog with the socket.io server
if(!this.startedSocket){
this.socket = io(this.signalingOptions.server, this.socketIOConfig);
this.startedSocket = true;
this.socket.on('connect', function(){
console.log('Connection to the signaling server established');
});
this.socket.on('launchResponse', function(idJoiner, offerTicket){
self.joiners = self.joiners + 1;
self.rps.answer(offerTicket, function(stampedTicket){
self.socket.emit('answer', idJoiner, stampedTicket);
});
});
this.socket.on('answerResponse', function(handshakeMessage){
self.rps.handshake(handshakeMessage);
self.socket.disconnect();
});
this.socket.on('disconnect', function(){
console.log('Disconnection from the signaling server');
self.startedSocket = false;
self.joiners = 0;
clearTimeout(this.timeout);
});
}
// #B reset timer before closing the connection
if (this.timeout!==null){ clearTimeout(this.timeout); };
// #C initialize a timer before closing the connection
if (this.signalingOptions.duration){
this.timeout = setTimeout(function(){
self.stopSharing();
}, this.signalingOptions.duration);
};
};
Signaling.prototype.startSharing = function(){
var self = this;
this.createSocket();
this.socket.on('connect', function(){
self.socket.emit('share', self.signalingOptions.session);
});
return this.socket;
};
Signaling.prototype.stopSharing = function(){
this.socket.disconnect();
this.timeout = null;
};
Signaling.prototype.startJoining = function(signalingOptions){
var self = this;
this.createSocket();
this.socket.on('connect', function(){
self.rps.launch(function(launchMessage){
self.socket.emit('launch', signalingOptions.session, launchMessage);
});
});
return this.socket;
};
module.exports = Signaling;
},{"socket.io-client":107}],9:[function(require,module,exports){
function CloseButton(container){
this.button = jQuery('<button>').appendTo(container)
.attr('type', 'button')
.addClass('close')
.css('color', 'white')
.append(jQuery('<span>')
.attr('aria-hidden', 'true')
.html(' ×'));
};
module.exports = CloseButton;
},{}],10:[function(require,module,exports){
function Editor(container, id){
this.div = jQuery('<div>').appendTo(container)
.attr('id','crate-'+id)
.css('min-height', '400px');
this.editor = ace.edit('crate-'+id);
this.editor.$blockScrolling = Infinity;
this.editor.setTheme("ace/theme/chrome");
this.editor.getSession().setUseWrapMode(true); // word wrapping
this.editor.setHighlightActiveLine(false); // not highlighting current line
this.editor.setShowPrintMargin(false); // no 80 column margin
this.editor.renderer.setShowGutter(false); // no line numbers
};
module.exports = Editor;
},{}],11:[function(require,module,exports){
function LinkView(container, id){
this.linkContainer = jQuery('<div>').appendTo(container)
.addClass('container')
.css('position', 'relative')
.css('top', '-100px')
.css('width', 'inherit')
.css('z-index', '10')
.css('opacity', '0.9')
.hide();
// #0 qr code modal
var qrCodeModal = jQuery('<div>').appendTo(container)
.attr('id', 'modalQRCode'+id)
.attr('tabindex','-1')
.attr('role','dialog')
.attr('aria-labelledby','modalQRCodeLabel')
.attr('aria-hidden', 'true')
.addClass('modal');
var qrCodeModalDialog = jQuery('<div>').appendTo(qrCodeModal)
.addClass('modal-dialog');
var qrCodeModalContent = jQuery('<div>').appendTo(qrCodeModalDialog)
.addClass('modal-content text-center');
this.qrcodeCanvas = jQuery('<div>');
qrCodeModalContent.append(jQuery('<br>'))
.append(this.qrcodeCanvas)
.append(jQuery('<br>'));
// #1 overall division
this.alert = jQuery('<div>').appendTo(this.linkContainer)
.attr('role', 'alert')
.addClass('alert alert-warning alert-dismissible');
// #2 cross to close the division
this.dismiss = jQuery('<button>').appendTo(this.alert)
.attr('type', 'button')
.addClass('close')
.html('<span aria-hidden="true">×</span><span class="sr-only"> '+
'Close </span>');
var rowContainer = jQuery('<div>').appendTo(this.alert)
.addClass('container');
var inputGroup = jQuery('<div>').appendTo(rowContainer)
.addClass('input-group');
this.input = jQuery('<input>').appendTo(inputGroup)
.attr('type', 'text')
.attr('placeholder', 'Nothing to see here, move along.')
.addClass('form-control');
var inputGroup2 = jQuery('<span>').appendTo(inputGroup)
.addClass('input-group-btn');
this.qrcode = jQuery('<button>').appendTo(inputGroup2)
.attr('aria-label', 'QR-code')
.attr('type', 'button')
.attr('data-target', '#modalQRCode'+id)
.attr('data-toggle', 'modal')
.addClass('btn btn-default')
.html('<i class="fa fa-qrcode"></i> QR-Code');
this.action = jQuery('<button>').appendTo(inputGroup2)
.attr('aria-label', 'Go!')
.attr('type', 'button')
.addClass('btn btn-default')
.html('Go!')
.css('z-index', '15');
var self = this;
this.dismiss.unbind("click").click(function(){self.linkContainer.hide();});
};
LinkView.prototype.printLink = function(link){
this.linkContainer.show();
this.alert.removeClass("alert-info").addClass("alert-warning");
this.action.html('<i class="fa fa-clipboard"></i> Copy');
this.action.attr("aria-label", "Copy to clipboard");
this.input.attr("readonly","readonly");
this.input.val(link);
this.qrcode.show();
};
LinkView.prototype.printLaunchLink = function(link){
this.printLink(link);
this.input.attr("placeholder",
"A link will appear in this field, give it to your "+
"friend!");
this.action.unbind("click");
this.qrcode.hide();
return this.action;
};
LinkView.prototype.printAnswerLink = function(link){
this.printLink(link);
this.input.attr("placeholder",
"A link will appear in this field. Please give it "+
"back to your friend.");
this.action.unbind("click");
this.qrcode.hide();
return this.action;
};
LinkView.prototype.askLink = function(){
this.linkContainer.show();
this.alert.removeClass("alert-warning").addClass("alert-info");
this.action.html('Go!');
this.action.attr("aria-label", "Stamp the ticket");
this.input.removeAttr("readonly");
this.input.val("");
this.action.unbind("click");
this.qrcode.hide();
};
LinkView.prototype.askLaunchLink = function(){
this.askLink();
this.input.attr("placeholder",
"Please, copy the ticket of your friend here to stamp "+
"it!");
this.qrcode.hide();
return this.action;
};
LinkView.prototype.askAnswerLink = function(){
this.askLink();
this.input.attr("placeholder", "Copy the stamped ticket to confirm "+
"your arrival in the network");
this.qrcode.hide();
return this.action;
};
LinkView.prototype.hide = function(){
this.linkContainer.hide();
};
module.exports = LinkView;
},{}],12:[function(require,module,exports){
var animals = require('animals');
var hash = require('string-hash');
function Marker(session, origin, range){
this.origin = origin;
this.session = session;
this.cursors = [range];
this.color = getColor(this.origin);
this.colorRGB = 'rgb('+this.color+')';
this.colorRGBLight = 'rgba('+this.color+', 0.5)';
this.animal = 'Anonymous ' +
capitalize(animals.words[hash(this.origin)%animals.words.length]);
};
// (TODO) refactor using jquery
Marker.prototype.update = function(html, markerLayer, session, config){
var start = config.firstRow, end = config.lastRow;
var cursors = this.cursors;
for (var i = 0; i < cursors.length; i++) {
var rng = {
start: session.getDocument().indexToPosition(cursors[i].start),
end: session.getDocument().indexToPosition(cursors[i].end)
};
var startScreenPos = session.documentToScreenPosition(rng.start);
var endScreenPos = session.documentToScreenPosition(rng.end);
if (startScreenPos.row === endScreenPos.row){//!range.isMultiLine()){
// only one line
var height = config.lineHeight;
var width = config.characterWidth *
(endScreenPos.column - startScreenPos.column);
var top = markerLayer.$getTop(startScreenPos.row, config);
var left = markerLayer.$padding + startScreenPos.column
* config.characterWidth;
var range = this.colorRGBLight;
if(width === 0){
range = this.colorRGB;
width = 2;
}
var code = '<div class="remoteCaret" style="' +
'background-color:' +range +';' +
'height:' + height + 'px;' +
'top:' + top + 'px;' +
'left:' + left + 'px;' +
'width:' + width + 'px">';
code += '<div class="squareCaret" style="background:' +
this.colorRGB + ';">';
code += '<div class="infoCaret" style="background:' +
this.colorRGBLight + ';">' + this.animal + '</div></div></div>';
html.push(code);
}else{
// multi-line
// first line
var height = config.lineHeight;
var top = markerLayer.$getTop(startScreenPos.row, config);
var left = markerLayer.$padding + startScreenPos.column *
config.characterWidth;
var code = "<div class='remoteCaret selection' style='" +
"background-color:" + this.colorRGBLight + ";" +
"height:" + height + "px;" +
"top:" + top + "px;" +
"left:" + left + "px;" +
"right: 0;'>";
code += '<div class="squareCaret" style="background:' +
this.colorRGB + ';">';
code += '<div class="infoCaret" style="background:' +
this.colorRGBLight + ';">' + this.animal + '</div></div></div>';
// last line
height = config.lineHeight;
top = markerLayer.$getTop(endScreenPos.row, config);
left = markerLayer.$padding;
width = config.characterWidth * endScreenPos.column;
code += "<div class='remoteCaret' style='" +
"background-color:" + this.colorRGBLight + ";" +
"height:" + height + "px;" +
"top:" + top + "px;" +
"left:" + left + "px;" +
"width:" + width + "px;'></div>";
// middle lines
if (endScreenPos.row - startScreenPos.row > 1){
height = config.lineHeight *
(endScreenPos.row - startScreenPos.row - 1);
top = markerLayer.$getTop(startScreenPos.row + 1, config);
left = markerLayer.$padding;
code += "<div class='remoteCaret' style='" +
"background-color:" + this.colorRGBLight + ";" +
"height:" + height + "px;" +
"top:" + top + "px;" +
"left:" + left + "px;" +
"right:0;'></div>";
}
html.push(code);
}
}
};
Marker.prototype.redraw = function(){
this.session._signal("changeFrontMarker");
};
Marker.prototype.refresh = function(){
var self = this;
if (this.timeout){
clearTimeout(this.timeout);
};
this.timeout = setTimeout(function(){
self.session.removeMarker(self.id);
delete self.session.remoteCarets[self.origin];
},10000);
};
Marker.prototype.addCursor = function(){
// add to this cursors
// trigger redraw
this.redraw()
}
function capitalize(s) {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function getColor(str){
var h1 = hash(str)%206;
var h2 = (h1*7)%206;
var h3 = (h1*11)%206;
return Math.floor(h1+50)+ ", "+Math.floor(h2+50)+ ", "+Math.floor(h3+50);
}
module.exports = Marker;
},{"animals":19,"string-hash":127}],13:[function(require,module,exports){
var imdata = "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";
function Metadata(model, container){
var metadataString =
'<ul style="padding: 5px;"><li><b>Session:</b> '+
model.signalingOptions.session+'</li>'+
'<li><b>Name:</b> '+ model.name+'</li>'+
'<li><b>Date:</b> '+ model.date.toString()+'</li>';
var buttonFile = jQuery('<a>').appendTo(container)
.attr('href','#')
.attr('data-trigger', 'hover').attr('data-toggle', 'popover')
.attr('data-placement', 'bottom').attr('data-html', 'true')
.attr('title','Document').attr('data-content', metadataString)
.css('color', 'black')
.css('display', 'inline-block')
.css('height', '32px')
.css('width', '32px')
.css('margin-left', '10px')
.css('background', 'data:image/png;base64,' + imdata +
'no-repeat center center')
.css('background-size', '32px 32px')
.addClass('crate-icon')
.css('height','34px').popover();
};
module.exports = Metadata;
},{}],14:[function(require,module,exports){
function Preview(container){
this.div = jQuery('<div>').appendTo(container)
.css('min-height', '400px')
.hide();
};
module.exports = Preview;
},{}],15:[function(require,module,exports){
function RoundButton(container, text, tooltip, size){
var s = 30;
var p = 6;
var b = 2;
switch (size){
case "large": s = 60; p = 11; break;
case "small": s = 12; p = 2; b = 1; break;
};
this.button = jQuery('<a>').appendTo(container)
.addClass('btn btn-default')
.css('width',s + 'px')
.css('height', s + 'px')
.css('margin-right', '10px')
.css('border-radius', '50%')
.css('border-width', b + 'px')
.css('background', 'inherit')
.css('padding', p+'px 0')
.css('color', '#ececec')
.css('vertical-align', 'middle')
.attr('data-toggle', 'tooltip')
.attr('data-placement', 'bottom')
.attr('title', tooltip)
.html(text)
.prop('disable', true)
.hover(function(){
$(this).css('background-color', '#ececec');
$(this).css('color', 'black');
}, function(){
$(this).css('background-color', 'inherit');
$(this).css('color', '#ececec');
})
.tooltip();
};
module.exports = RoundButton;
},{}],16:[function(require,module,exports){
function StatesHeader(model, container){
this.model = model;
this.red = "#cd2626";
this.yellow = "#eead0e";
this.green = "#228b22";
this.blue = "#00BFFF";
this.signalingState = jQuery('<i>').appendTo(container)
.addClass('fa fa-circle-o-notch fa-2x')
.attr('data-trigger', 'hover').attr('data-toggle', 'popover')
.attr('title', 'Signaling server status')
.attr('data-html', 'true').attr('data-content', '')
.attr('data-placement', 'bottom')
.css('margin-right', '10px')
.popover()
.hide();
this.networkState = jQuery('<i>').appendTo(container)
.addClass('fa fa-globe fa-2x')
.attr('data-trigger', 'hover').attr('data-toggle', 'popover')
.attr('title', 'Network status')
.attr('data-html', 'true')
.attr('data-content', 'Disconnected: you are currently'+
' editing <span class="alert-info">on your own</span>.')
.attr('data-placement', 'bottom')
.css('margin-right', '10px')
.css('margin-top', '2px')
.popover();
};
StatesHeader.prototype.setNetworkState = function(state){
switch (state){
case "connected":
var connectedString =
"<span class='alert-success'>Congratulations</span>"+
"! You are connected to people, and people are "+
"connected to you. <span class='alert-info'>You can start editing "+
"together</span>.";
this.networkState.css("color", this.green);
this.networkState.attr("data-content", connectedString);
break;
case "partiallyconnected":
var partiallyConnectedString =
"<span class='alert-warning'>Partially"+
" connected</span>: either you are connected to people, or people "+
"are connected to you. "+
"<i>This is an undesired intermediary state. If it persists, "+
"please consider rejoining the network.</i>";
this.networkState.css("color", this.yellow);
this.networkState.attr("data-content", partiallyConnectedString);
break;
case "disconnected":
var disconnectedString =
"<span class='alert-danger'>Disconnected</span>:"+
" you are currently editing <span class='alert-info'>on"+
" your own</span>.";
this.networkState.css("color", this.red);
this.networkState.attr("data-content", disconnectedString);
break;
};
};
StatesHeader.prototype.setSignalingState = function(state){
var self = this;
function blink(){
self.signalingState.show();
setTimeout( function(){
if (self.model.signaling.startedSocket){
blink();
} else {
self.setSignalingState("done");
};
}, 1000);
};
switch (state){
case "waitSignaling":
this.signalingState.show();
this.signalingState.removeClass("fa-spin");
this.signalingState.css("color", this.yellow);
var waitSignalingString = "<span class='alert-warning'>Connecting"+
"</span>: establishing a connection with the signaling server. "+
"The latter allows people to join the editing session by using "+
"the provided link. "+
"<i>If this state persists, consider reloading the page.</i>";
this.signalingState.attr("data-content", waitSignalingString);
blink();
break;
case "waitSharer":
this.signalingState.show();
this.signalingState.addClass("fa-spin");
this.signalingState.css("color", this.blue);
var waitSharerString = "The connection to the signaling server has "+
"been successfully established! <span class='alert-info'>Waiting "+
"for the sharer now</span>.";
this.signalingState.attr("data-content", waitSharerString);
blink();
break;
case "waitJoiners":
this.signalingState.css("color", this.blue);
this.signalingState.addClass("fa-spin");
var waitJoinersString = "The connection to the signaling server has "+
"been <span class='alert-success'>successfully</span> "+
"established! "+
"The server allows people to join the editing session by using "+
"the provided link. "+
"<span class='alert-info'>Waiting for the collaborators</span>."
this.signalingState.attr("data-content", waitJoinersString);
blink();
break;
case "done":
this.signalingState.show();
this.signalingState.removeClass("fa-spin");
var doneString = "The connection to the signaling server has been "+
"<span class='alert-info'>terminated</span>.";
this.signalingState.attr("data-content", doneString);
this.signalingState.css("color", this.green);
this.signalingState.fadeOut(6000, "linear");
break;
};
};
module.exports = StatesHeader;
},{}],17:[function(require,module,exports){
function Structure(container){
// #A create the global header
var header = jQuery('<div>').appendTo(container)
.css('width', '100%')
.css('box-shadow', '0px 1px 5px #ababab')
.css('border-top-left-radius', '4px')
.css('border-top-right-radius', '4px')
.css('color', '#ececec')
.css('background-color', '#242b32');
var headerContainer = jQuery('<div>').appendTo(header)
.addClass('container')
.css('width','inherit');
// #B Divide the header in four parts with different purposes
this.headerLeft = jQuery('<div>').appendTo(headerContainer)
.addClass('pull-left')
.css('padding-top','10px')
.css('padding-bottom','10px');
this.headerRightRightRight = jQuery('<div>').appendTo(headerContainer)
.addClass('pull-right')
.css('padding-top', '10px')
.css('padding-bottom', '10px')
.css('height', '34px');
this.headerRightRight = jQuery('<div>').appendTo(headerContainer)
.addClass('pull-right')
.css('padding-top','10px')
.css('padding-bottom','10px')
.css('height','34px')
.css('margin-top', '2px');
this.headerRight = jQuery('<div>').appendTo(headerContainer)
.addClass('pull-right')
.css('padding-top','10px')
.css('padding-bottom','10px')
.css('height','34px')
.css('margin-right', '20px');
this.body = jQuery('<div>').appendTo(container)
.css('box-shadow', '0px 1px 5px #ababab')
.css('border-bottom-left-radius', '4px')
.css('border-bottom-right-radius', '4px')
.css('margin-bottom', '20px')
.css('padding', '30px 15px')
.css('background-color', '#ffffff');
};
module.exports = Structure;
},{}],18:[function(require,module,exports){
module.exports = after
function after(count, callback, err_cb) {
var bail = false
err_cb = err_cb || noop
proxy.count = count
return (count === 0) ? callback() : proxy
function proxy(err, result) {
if (proxy.count <= 0) {
throw new Error('after called too many times')
}
--proxy.count
// after first error, rest are passed to err_cb
if (err) {
bail = true
callback(err)
// future error callbacks will go to error handler
callback = err_cb
} else if (proxy.count === 0 && !bail) {
callback(null, result)
}
}
}
function noop() {}
},{}],19:[function(require,module,exports){
'use strict';
var words = require('./words.json');
var uniqueRandom = require('unique-random')(0, words.length - 1);
module.exports = function () {
return words[uniqueRandom()];
};
module.exports.words = words;
},{"./words.json":20,"unique-random":130}],20:[function(require,module,exports){
module.exports=[
"aardvark",
"albatross",
"alligator",
"alpaca",
"ant",
"anteater",
"antelope",
"ape",
"armadillo",
"donkey",
"baboon",
"badger",
"barracuda",
"bat",
"bear",
"beaver",
"bee",
"bison",
"boar",
"buffalo",
"butterfly",
"camel",
"capybara",
"caribou",
"cassowary",
"cat",
"caterpillar",
"cattle",
"chamois",
"cheetah",
"chicken",
"chimpanzee",
"chinchilla",
"chough",
"clam",
"cobra",
"cockroach",
"cod",
"cormorant",
"coyote",
"crab",
"crane",
"crocodile",
"crow",
"curlew",
"deer",
"dinosaur",
"dog",
"dogfish",
"dolphin",
"donkey",
"dotterel",
"dove",
"dragonfly",
"duck",
"dugong",
"dunlin",
"eagle",
"echidna",
"eel",
"eland",
"elephant",
"elephant-seal",
"elk",
"emu",
"falcon",
"ferret",
"finch",
"fish",
"flamingo",
"fly",
"fox",
"frog",
"gaur",
"gazelle",
"gerbil",
"giant-panda",
"giraffe",
"gnat",
"gnu",
"goat",
"goose",
"goldfinch",
"goldfish",
"gorilla",
"goshawk",
"grasshopper",
"grouse",
"guanaco",
"guinea-fowl",
"guinea-pig",
"gull",
"hamster",
"hare",
"hawk",
"hedgehog",
"heron",
"herring",
"hippopotamus",
"hornet",
"horse",
"human",
"hummingbird",
"hyena",
"ibex",
"ibis",
"jackal",
"jaguar",
"jay",
"jellyfish",
"kangaroo",
"kingfisher",
"koala",
"komodo-dragon",
"kookabura",
"kouprey",
"kudu",
"lapwing",
"lark",
"lemur",
"leopard",
"lion",
"llama",
"lobster",
"locust",
"loris",
"louse",
"lyrebird",
"magpie",
"mallard",
"manatee",
"mandrill",
"mantis",
"marten",
"meerkat",
"mink",
"mole",
"mongoose",
"monkey",
"moose",
"mouse",
"mosquito",
"mule",
"narwhal",
"newt",
"nightingale",
"octopus",
"okapi",
"opossum",
"oryx",
"ostrich",
"otter",
"owl",
"ox",
"oyster",
"panther",
"parrot",
"partridge",
"peafowl",
"pelican",
"penguin",
"pheasant",
"pig",
"pigeon",
"polar-bear",
"pony",
"porcupine",
"porpoise",
"prairie-dog",
"quail",
"quelea",
"quetzal",
"rabbit",
"raccoon",
"rail",
"ram",
"rat",
"raven",
"red-deer",
"red-panda",
"reindeer",
"rhinoceros",
"rook",
"salamander",
"salmon",
"sand-dollar",
"sandpiper",
"sardine",
"scorpion",
"sea-lion",
"sea-urchin",
"seahorse",
"seal",
"shark",
"sheep",
"shrew",
"skunk",
"snail",
"snake",
"sparrow",
"spider",
"spoonbill",
"squid",
"squirrel",
"starling",
"stingray",
"stinkbug",
"stork",
"swallow",
"swan",
"tapir",
"tarsier",
"