UNPKG

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
(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('&nbsp;&nbsp;&times')); }; 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">&times;</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", "