diff --git a/client/components/wodo/adaptor.service.js b/client/components/wodo/adaptor.service.js index cc90fbc..b45fc1c 100644 --- a/client/components/wodo/adaptor.service.js +++ b/client/components/wodo/adaptor.service.js @@ -1,282 +1,283 @@ /*jslint unparam: true*/ /*global runtime, core, ops, io*/ 'use strict'; angular.module('manticoreApp') .factory('Adaptor', function () { - return (function() { - 'use strict'; - var OperationRouter = function (socket, odfContainer, errorCb) { var EVENT_BEFORESAVETOFILE = 'beforeSaveToFile', EVENT_SAVEDTOFILE = 'savedToFile', EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED = 'hasLocalUnsyncedOperationsChanged', EVENT_HASSESSIONHOSTCONNECTIONCHANGED = 'hasSessionHostConnectionChanged', EVENT_MEMBERADDED = 'memberAdded', EVENT_MEMBERCHANGED = 'memberChanged', EVENT_MEMBERREMOVED = 'memberRemoved', eventNotifier = new core.EventNotifier([ EVENT_BEFORESAVETOFILE, EVENT_SAVEDTOFILE, EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, EVENT_HASSESSIONHOSTCONNECTIONCHANGED, EVENT_MEMBERADDED, EVENT_MEMBERCHANGED, EVENT_MEMBERREMOVED, ops.OperationRouter.signalProcessingBatchStart, ops.OperationRouter.signalProcessingBatchEnd ]), operationFactory, playbackFunction, lastServerSyncHeadId = 0, sendClientOpspecsLock = false, sendClientOpspecsTask, hasSessionHostConnection = true, unplayedServerOpSpecQueue = [], unsyncedClientOpSpecQueue = [], operationTransformer = new ops.OperationTransformer(), /**@const*/sendClientOpspecsDelay = 300; function playbackOpspecs(opspecs) { var op, i; if (!opspecs.length) { return; } eventNotifier.emit(ops.OperationRouter.signalProcessingBatchStart, {}); for (i = 0; i < opspecs.length; i += 1) { op = operationFactory.create(opspecs[i]); if (op !== null) { if (!playbackFunction(op)) { eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); errorCb('opExecutionFailure'); return; } } else { eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); errorCb('Unknown opspec: ' + runtime.toJson(opspecs[i])); return; } } eventNotifier.emit(ops.OperationRouter.signalProcessingBatchEnd, {}); } function handleNewServerOpsWithUnsyncedClientOps(serverOps) { var transformResult = operationTransformer.transform(unsyncedClientOpSpecQueue, serverOps); if (!transformResult) { errorCb('Has unresolvable conflict.'); return false; } unsyncedClientOpSpecQueue = transformResult.opSpecsA; unplayedServerOpSpecQueue = unplayedServerOpSpecQueue.concat(transformResult.opSpecsB); return true; } function handleNewClientOpsWithUnplayedServerOps(clientOps) { var transformResult = operationTransformer.transform(clientOps, unplayedServerOpSpecQueue); if (!transformResult) { errorCb('Has unresolvable conflict.'); return false; } unsyncedClientOpSpecQueue = unsyncedClientOpSpecQueue.concat(transformResult.opSpecsA); unplayedServerOpSpecQueue = transformResult.opSpecsB; return true; } function receiveServerOpspecs(headId, serverOpspecs) { if (unsyncedClientOpSpecQueue.length > 0) { handleNewServerOpsWithUnsyncedClientOps(serverOpspecs); // could happen that ops from server make client ops obsolete if (unsyncedClientOpSpecQueue.length === 0) { eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, false); } } else { // apply directly playbackOpspecs(serverOpspecs); } lastServerSyncHeadId = headId; } function sendClientOpspecs() { var originalUnsyncedLength = unsyncedClientOpSpecQueue.length; if (originalUnsyncedLength) { sendClientOpspecsLock = true; socket.emit('commit_ops', { head: lastServerSyncHeadId, ops: unsyncedClientOpSpecQueue }, function (response) { if (response.conflict === true) { sendClientOpspecs(); } else { lastServerSyncHeadId = response.head; // on success no other server ops should have sneaked in meanwhile, so no need to check // got no other client ops meanwhile? if (unsyncedClientOpSpecQueue.length === originalUnsyncedLength) { unsyncedClientOpSpecQueue.length = 0; // finally apply all server ops collected while waiting for sync playbackOpspecs(unplayedServerOpSpecQueue); unplayedServerOpSpecQueue.length = 0; eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, false); sendClientOpspecsLock = false; } else { // send off the new client ops directly unsyncedClientOpSpecQueue.splice(0, originalUnsyncedLength); sendClientOpspecs(); } } }); } } this.setOperationFactory = function (f) { operationFactory = f; }; this.setPlaybackFunction = function (f) { playbackFunction = f; }; this.push = function (operations) { var clientOpspecs = [], now = Date.now(), hasLocalUnsyncedOpsBefore = (unsyncedClientOpSpecQueue.length !== 0), hasLocalUnsyncedOpsNow; operations.forEach(function(op) { var opspec = op.spec(); opspec.timestamp = now; clientOpspecs.push(opspec); }); playbackOpspecs(clientOpspecs); if (unplayedServerOpSpecQueue.length > 0) { handleNewClientOpsWithUnplayedServerOps(clientOpspecs); } else { unsyncedClientOpSpecQueue = unsyncedClientOpSpecQueue.concat(clientOpspecs); } hasLocalUnsyncedOpsNow = (unsyncedClientOpSpecQueue.length !== 0); if (hasLocalUnsyncedOpsNow !== hasLocalUnsyncedOpsBefore) { eventNotifier.emit(EVENT_HASLOCALUNSYNCEDOPERATIONSCHANGED, hasLocalUnsyncedOpsNow); } sendClientOpspecsTask.trigger(); }; this.requestReplay = function (cb) { var cbOnce = function () { eventNotifier.unsubscribe(ops.OperationRouter.signalProcessingBatchEnd, cbOnce); cb(); }; // hack: relies on at least addmember op being added for ourselves and being executed eventNotifier.subscribe(ops.OperationRouter.signalProcessingBatchEnd, cbOnce); socket.emit('replay', {}); }; this.close = function (cb) { cb(); }; this.subscribe = function (eventId, cb) { eventNotifier.subscribe(eventId, cb); }; this.unsubscribe = function (eventId, cb) { eventNotifier.unsubscribe(eventId, cb); }; this.hasLocalUnsyncedOps = function () { return unsyncedClientOpSpecQueue.length !== 0; }; this.hasSessionHostConnection = function () { return hasSessionHostConnection; }; function init() { sendClientOpspecsTask = core.Task.createTimeoutTask(function () { if (!sendClientOpspecsLock) { sendClientOpspecs(); } }, sendClientOpspecsDelay); socket.on('replay', function (data) { receiveServerOpspecs(data.head, data.ops); socket.on('new_ops', function (data) { receiveServerOpspecs(data.head, data.ops); }); }); } init(); }; var ClientAdaptor = function (documentId, documentURL, authToken, connectedCb, kickedCb, disconnectedCb) { - var memberId, + var self = this, + memberId, socket; this.getMemberId = function () { return memberId; }; this.getGenesisUrl = function () { return documentURL; }; this.createOperationRouter = function (odfContainer, errorCb) { runtime.assert(Boolean(memberId), 'You must be connected to a session before creating an operation router'); return new OperationRouter(socket, odfContainer, errorCb); }; this.joinSession = function (cb) { socket.on('join_success', function handleJoinSuccess(data) { socket.removeListener('join_success', handleJoinSuccess); memberId = data.memberId; cb(memberId); }); socket.emit('join', { documentId: documentId }); }; this.leaveSession = function (cb) { socket.emit('leave', {}, cb); socket.removeAllListeners(); }; this.getSocket = function () { return socket; }; + this.destroy = function () { + socket.disconnect(); + }; + function init() { socket = io('', { - query: 'token=' + authToken + query: 'token=' + authToken, + forceNew: true }); socket.on('connect', connectedCb); socket.on('kick', kickedCb); socket.on('disconnect', disconnectedCb); } init(); }; return ClientAdaptor; - - }()); }); diff --git a/client/components/wodo/editor.controller.js b/client/components/wodo/editor.controller.js index 8206a03..a16b851 100644 --- a/client/components/wodo/editor.controller.js +++ b/client/components/wodo/editor.controller.js @@ -1,72 +1,84 @@ 'use strict'; /*global Wodo*/ angular.module('manticoreApp') .controller('WodoCtrl', function ($scope, Auth, Adaptor) { - $scope.message = 'Hello'; - var editorInstance, clientAdaptor, editorOptions = { collabEditingEnabled: true }, onConnectCalled = false; - function closeEditing() { + function closeEditing(cb) { editorInstance.leaveSession(function () { clientAdaptor.leaveSession(function () { console.log('Closed editing, left session.'); + cb(); }); }); } function handleEditingError(error) { alert('Something went wrong!\n' + error); console.log(error); closeEditing(); } function openEditor() { Wodo.createCollabTextEditor('wodoContainer', editorOptions, function (err, editor) { editorInstance = editor; editorInstance.addEventListener(Wodo.EVENT_UNKNOWNERROR, handleEditingError); editorInstance.joinSession(clientAdaptor, function () {}); }); } function boot() { clientAdaptor = new Adaptor( - $scope.document.id, - '/api/documents/snapshot/' + _.last($scope.document.chunks), - Auth.getToken(), - function onConnect() { - console.log('onConnect'); - if (onConnectCalled) { - console.log('Reconnecting not yet supported'); - return; + $scope.document.id, + '/api/documents/snapshot/' + _.last($scope.document.chunks), + Auth.getToken(), + function onConnect() { + console.log('onConnect'); + if (onConnectCalled) { + console.log('Reconnecting not yet supported'); + return; + } + onConnectCalled = true; + + clientAdaptor.joinSession(function (memberId) { + if (!memberId) { + console.log('Could not join; memberId not received'); + } else { + console.log('Joined with memberId ' + memberId); + openEditor(); + } + }); + }, + function onKick() { + console.log('onKick'); + closeEditing(); + }, + function onDisconnect() { + console.log('onDisconnect'); } - onConnectCalled = true; + ); + } - clientAdaptor.joinSession(function (memberId) { - if (!memberId) { - console.log('Could not join; memberId not received'); - } else { - console.log('Joined with memberId ' + memberId); - openEditor(); - } - }); - }, - function onKick() { - console.log('onKick'); - closeEditing(); - }, - function onDisconnect() { - console.log('onDisconnect'); + function destroy (cb) { + if (editorInstance) { + closeEditing(cb); + } else { + if (clientAdaptor) { + clientAdaptor.leaveSession(); + clientAdaptor.destroy(); + cb(); + } } - ); } this.boot = boot; + this.destroy = destroy; }); diff --git a/client/components/wodo/editor.directive.js b/client/components/wodo/editor.directive.js index 4647c77..54ce0d3 100644 --- a/client/components/wodo/editor.directive.js +++ b/client/components/wodo/editor.directive.js @@ -1,78 +1,95 @@ 'use strict'; angular.module('manticoreApp') .directive('wodoEditor', function () { return { restrict: 'E', templateUrl: 'components/wodo/editor.html', controller: 'WodoCtrl', controllerAs: 'wodoCtrl', link: function (scope) { var wodoCtrl = scope.wodoCtrl; var usedLocale = 'C'; var wodoPrefix = '/bower_components/wodo/wodo'; + var head = document.getElementsByTagName('head')[0], + frag = document.createDocumentFragment(); + + if (navigator && navigator.language.match(/^(de)/)) { usedLocale = navigator.language.substr(0, 2); } window.dojoConfig = { locale: usedLocale, paths: { 'webodf/editor': wodoPrefix, 'dijit': wodoPrefix + '/dijit', 'dojox': wodoPrefix + '/dojox', 'dojo': wodoPrefix + '/dojo', 'resources': wodoPrefix + '/resources' } }; function loadDependencies(callback) { - var head = document.getElementsByTagName('head')[0], - frag = document.createDocumentFragment(), - link, - script; + + var link, script; // append two link and two script elements to the header link = document.createElement('link'); link.rel = 'stylesheet'; link.href = wodoPrefix + '/app/resources/app.css'; link.type = 'text/css'; link.async = false; frag.appendChild(link); link = document.createElement('link'); link.rel = 'stylesheet'; link.href = wodoPrefix + '/wodocollabpane.css'; link.type = 'text/css'; link.async = false; frag.appendChild(link); script = document.createElement('script'); script.src = wodoPrefix + '/dojo-amalgamation.js'; script['data-dojo-config'] = 'async: true'; script.charset = 'utf-8'; script.type = 'text/javascript'; script.async = false; frag.appendChild(script); script = document.createElement('script'); script.src = wodoPrefix + '/webodf.js'; script.charset = 'utf-8'; script.type = 'text/javascript'; script.async = false; frag.appendChild(script); script = document.createElement('script'); script.src = wodoPrefix + '/wodocollabtexteditor.js'; script.charset = 'utf-8'; script.type = 'text/javascript'; script.async = false; script.onload = callback; frag.appendChild(script); + _.each(frag.children, function (el) { + el.setAttribute('wodo', true); + }); head.appendChild(frag); } + function cleanUp() { + wodoCtrl.destroy(function () { + _.each(head.children, function (el) { + if (el && el.hasAttribute('wodo')) { + head.removeChild(el); + } + }); + }); + } + loadDependencies(function () { wodoCtrl.boot(); }); + + scope.$on('$destroy', cleanUp); } }; });