diff --git a/Gruntfile.js b/Gruntfile.js index 76b158c..1b5188b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,696 +1,696 @@ // Generated on 2015-06-18 using generator-angular-fullstack 2.0.13 'use strict'; module.exports = function (grunt) { var localConfig; try { localConfig = require('./server/config/local.env'); } catch(e) { localConfig = {}; } // Load grunt tasks automatically, when needed require('jit-grunt')(grunt, { express: 'grunt-express-server', useminPrepare: 'grunt-usemin', ngtemplates: 'grunt-angular-templates', cdnify: 'grunt-google-cdn', protractor: 'grunt-protractor-runner', injector: 'grunt-asset-injector', buildcontrol: 'grunt-build-control' }); // Time how long tasks take. Can help when optimizing build times require('time-grunt')(grunt); // Define the configuration for all the tasks grunt.initConfig({ // Project settings pkg: grunt.file.readJSON('package.json'), yeoman: { // configurable paths client: require('./bower.json').appPath || 'client', dist: 'dist' }, express: { options: { port: process.env.PORT || 9000 }, dev: { options: { script: 'server/app.js', debug: true } }, prod: { options: { script: 'dist/server/app.js' } } }, open: { server: { url: 'http://localhost:<%= express.options.port %>' } }, watch: { injectJS: { files: [ '<%= yeoman.client %>/{app,components}/**/*.js', '!<%= yeoman.client %>/{app,components}/**/*.spec.js', '!<%= yeoman.client %>/{app,components}/**/*.mock.js', '!<%= yeoman.client %>/app/app.js'], tasks: ['injector:scripts'] }, injectCss: { files: [ '<%= yeoman.client %>/{app,components}/**/*.css' ], tasks: ['injector:css'] }, mochaTest: { files: ['server/**/*.spec.js'], tasks: ['env:test', 'mochaTest'] }, jsTest: { files: [ '<%= yeoman.client %>/{app,components}/**/*.spec.js', '<%= yeoman.client %>/{app,components}/**/*.mock.js' ], tasks: ['newer:jshint:all', 'karma'] }, injectStylus: { files: [ '<%= yeoman.client %>/{app,components}/**/*.styl'], tasks: ['injector:stylus'] }, stylus: { files: [ '<%= yeoman.client %>/{app,components}/**/*.styl'], tasks: ['stylus', 'autoprefixer'] }, jade: { files: [ '<%= yeoman.client %>/{app,components}/*', '<%= yeoman.client %>/{app,components}/**/*.jade'], tasks: ['jade'] }, gruntfile: { files: ['Gruntfile.js'] }, livereload: { files: [ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css', '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html', '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', '!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js', '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js', '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' ], options: { livereload: true } }, express: { files: [ 'server/**/*.{js,json}' ], tasks: ['express:dev', 'wait'], options: { livereload: true, nospawn: true //Without this option specified express won't be reloaded } } }, // Make sure code styles are up to par and there are no obvious mistakes jshint: { options: { jshintrc: '<%= yeoman.client %>/.jshintrc', reporter: require('jshint-stylish') }, server: { options: { jshintrc: 'server/.jshintrc' }, src: [ 'server/**/*.js', '!server/**/*.spec.js' ] }, serverTest: { options: { jshintrc: 'server/.jshintrc-spec' }, src: ['server/**/*.spec.js'] }, all: [ '<%= yeoman.client %>/{app,components}/**/*.js', '!<%= yeoman.client %>/{app,components}/**/*.spec.js', '!<%= yeoman.client %>/{app,components}/**/*.mock.js' ], test: { src: [ '<%= yeoman.client %>/{app,components}/**/*.spec.js', '<%= yeoman.client %>/{app,components}/**/*.mock.js' ] } }, // Empties folders to start fresh clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/*', '!<%= yeoman.dist %>/.git*', '!<%= yeoman.dist %>/.openshift', '!<%= yeoman.dist %>/Procfile' ] }] }, server: '.tmp' }, // Add vendor prefixed styles autoprefixer: { options: { browsers: ['last 1 version'] }, dist: { files: [{ expand: true, cwd: '.tmp/', src: '{,*/}*.css', dest: '.tmp/' }] } }, // Debugging with node inspector 'node-inspector': { custom: { options: { 'web-host': 'localhost' } } }, // Use nodemon to run server in debug mode with an initial breakpoint nodemon: { debug: { script: 'server/app.js', options: { nodeArgs: ['--debug-brk'], env: { PORT: process.env.PORT || 9000 }, callback: function (nodemon) { nodemon.on('log', function (event) { console.log(event.colour); }); // opens browser on initial server start nodemon.on('config:update', function () { setTimeout(function () { require('open')('http://localhost:8080/debug?port=5858'); }, 500); }); } } } }, // Automatically inject Bower components into the app wiredep: { target: { src: '<%= yeoman.client %>/index.html', ignorePath: '<%= yeoman.client %>/', exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/', /bootstrap.css/, /font-awesome.css/ ] } }, // Renames files for browser caching purposes rev: { dist: { files: { src: [ '<%= yeoman.dist %>/public/{,*/}*.js', '<%= yeoman.dist %>/public/{,*/}*.css', '<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', '<%= yeoman.dist %>/public/assets/fonts/*' ] } } }, // Reads HTML for usemin blocks to enable smart builds that automatically // concat, minify and revision files. Creates configurations in memory so // additional tasks can operate on them useminPrepare: { html: ['<%= yeoman.client %>/index.html'], options: { dest: '<%= yeoman.dist %>/public' } }, // Performs rewrites based on rev and the useminPrepare configuration usemin: { html: ['<%= yeoman.dist %>/public/{,*/}*.html'], css: ['<%= yeoman.dist %>/public/{,*/}*.css'], js: ['<%= yeoman.dist %>/public/{,*/}*.js'], options: { assetsDirs: [ '<%= yeoman.dist %>/public', '<%= yeoman.dist %>/public/assets/images' ], // This is so we update image references in our ng-templates patterns: { js: [ [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images'] ] } } }, // The following *-min tasks produce minified files in the dist folder imagemin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.client %>/assets/images', src: '{,*/}*.{png,jpg,jpeg,gif}', dest: '<%= yeoman.dist %>/public/assets/images' }] } }, svgmin: { dist: { files: [{ expand: true, cwd: '<%= yeoman.client %>/assets/images', src: '{,*/}*.svg', dest: '<%= yeoman.dist %>/public/assets/images' }] } }, // Allow the use of non-minsafe AngularJS files. Automatically makes it // minsafe compatible so Uglify does not destroy the ng references ngAnnotate: { dist: { files: [{ expand: true, cwd: '.tmp/concat', src: '*/**.js', dest: '.tmp/concat' }] } }, // Package all the html partials into a single javascript payload ngtemplates: { options: { // This should be the name of your apps angular module module: 'manticoreApp', htmlmin: { collapseBooleanAttributes: true, collapseWhitespace: true, removeAttributeQuotes: true, removeEmptyAttributes: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true }, usemin: 'app/app.js' }, main: { cwd: '<%= yeoman.client %>', src: ['{app,components}/**/*.html'], dest: '.tmp/templates.js' }, tmp: { cwd: '.tmp', src: ['{app,components}/**/*.html'], dest: '.tmp/tmp-templates.js' } }, // Replace Google CDN references cdnify: { dist: { html: ['<%= yeoman.dist %>/public/*.html'] } }, // Copies remaining files to places other tasks can use copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= yeoman.client %>', dest: '<%= yeoman.dist %>/public', src: [ '*.{ico,png,txt}', '.htaccess', 'bower_components/**/*', 'assets/images/{,*/}*.{webp}', 'assets/fonts/**/*', 'index.html' ] }, { expand: true, cwd: '.tmp/images', dest: '<%= yeoman.dist %>/public/assets/images', src: ['generated/*'] }, { expand: true, dest: '<%= yeoman.dist %>', src: [ 'package.json', 'server/**/*' ] }] }, styles: { expand: true, cwd: '<%= yeoman.client %>', dest: '.tmp/', src: ['{app,components}/**/*.css'] } }, buildcontrol: { options: { dir: 'dist', commit: true, push: true, connectCommits: false, message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%' }, heroku: { options: { remote: 'heroku', branch: 'master' } }, openshift: { options: { remote: 'openshift', branch: 'master' } } }, // Run some tasks in parallel to speed up the build process concurrent: { server: [ 'jade', 'stylus', ], test: [ 'jade', 'stylus', ], debug: { tasks: [ 'nodemon', 'node-inspector' ], options: { logConcurrentOutput: true } }, dist: [ 'jade', 'stylus', 'imagemin', 'svgmin' ] }, // Test settings karma: { unit: { configFile: 'karma.conf.js', singleRun: true } }, mochaTest: { options: { reporter: 'spec' }, src: ['server/**/*.spec.js'] }, protractor: { options: { configFile: 'protractor.conf.js' }, chrome: { options: { args: { browser: 'chrome' } } } }, env: { test: { NODE_ENV: 'test' }, prod: { NODE_ENV: 'production' }, all: localConfig }, // Compiles Jade to html jade: { compile: { options: { data: { debug: false } }, files: [{ expand: true, cwd: '<%= yeoman.client %>', src: [ '{app,components}/**/*.jade' ], dest: '.tmp', ext: '.html' }] } }, // Compiles Stylus to CSS stylus: { server: { options: { paths: [ '<%= yeoman.client %>/bower_components', '<%= yeoman.client %>/app', '<%= yeoman.client %>/components' ], "include css": true }, files: { '.tmp/app/app.css' : '<%= yeoman.client %>/app/app.styl' } } }, injector: { options: { }, // Inject application script files into index.html (doesn't include bower) scripts: { options: { transform: function(filePath) { filePath = filePath.replace('/client/', ''); filePath = filePath.replace('/.tmp/', ''); return ''; }, starttag: '', endtag: '' }, files: { '<%= yeoman.client %>/index.html': [ ['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js', '!{.tmp,<%= yeoman.client %>}/app/app.js', '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js', '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js'] ] } }, // Inject component styl into app.styl stylus: { options: { transform: function(filePath) { filePath = filePath.replace('/client/app/', ''); filePath = filePath.replace('/client/components/', ''); return '@import \'' + filePath + '\';'; }, starttag: '// injector', endtag: '// endinjector' }, files: { '<%= yeoman.client %>/app/app.styl': [ '<%= yeoman.client %>/{app,components}/**/*.styl', '!<%= yeoman.client %>/app/app.styl' ] } }, // Inject component css into index.html css: { options: { transform: function(filePath) { filePath = filePath.replace('/client/', ''); filePath = filePath.replace('/.tmp/', ''); return ''; }, starttag: '', endtag: '' }, files: { '<%= yeoman.client %>/index.html': [ '<%= yeoman.client %>/{app,components}/**/*.css' ] } } }, }); // Used for delaying livereload until after server has restarted grunt.registerTask('wait', function () { grunt.log.ok('Waiting for server reload...'); var done = this.async(); setTimeout(function () { grunt.log.writeln('Done waiting!'); done(); }, 1500); }); grunt.registerTask('express-keepalive', 'Keep grunt running', function() { this.async(); }); grunt.registerTask('serve', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']); } if (target === 'debug') { return grunt.task.run([ 'clean:server', 'env:all', - 'injector:stylus', + 'injector:stylus', 'concurrent:server', 'injector', 'wiredep', 'autoprefixer', 'concurrent:debug' ]); } grunt.task.run([ 'clean:server', 'env:all', - 'injector:stylus', + 'injector:stylus', 'concurrent:server', 'injector', 'wiredep', 'autoprefixer', 'express:dev', 'wait', 'open', 'watch' ]); }); grunt.registerTask('server', function () { grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); grunt.task.run(['serve']); }); grunt.registerTask('test', function(target) { if (target === 'server') { return grunt.task.run([ 'env:all', 'env:test', 'mochaTest' ]); } else if (target === 'client') { return grunt.task.run([ 'clean:server', 'env:all', - 'injector:stylus', + 'injector:stylus', 'concurrent:test', 'injector', 'autoprefixer', 'karma' ]); } else if (target === 'e2e') { return grunt.task.run([ 'clean:server', 'env:all', 'env:test', - 'injector:stylus', + 'injector:stylus', 'concurrent:test', 'injector', 'wiredep', 'autoprefixer', 'express:dev', 'protractor' ]); } else grunt.task.run([ 'test:server', 'test:client' ]); }); grunt.registerTask('build', [ 'clean:dist', - 'injector:stylus', + 'injector:stylus', 'concurrent:dist', 'injector', 'wiredep', 'useminPrepare', 'autoprefixer', 'ngtemplates', 'concat', 'ngAnnotate', 'copy:dist', 'cdnify', 'cssmin', 'uglify', 'rev', 'usemin' ]); grunt.registerTask('default', [ 'newer:jshint', 'test', 'build' ]); }; diff --git a/bower.json b/bower.json index f185680..d0aaa32 100644 --- a/bower.json +++ b/bower.json @@ -1,26 +1,27 @@ { "name": "manticore", "version": "0.0.0", "dependencies": { "angular": "1.4.1", "json3": "3.3.1", "es5-shim": "4.1.7", "jquery": "1.11.0", "bootstrap": "~3.1.1", "angular-resource": "1.4.1", "angular-cookies": "1.4.1", "angular-sanitize": "1.4.1", "angular-bootstrap": "0.13.0", "font-awesome": ">=4.1.0", "lodash": "3.9.3", "angular-ui-router": "0.2.15", - "angular-file-upload": "1.1.5" + "angular-file-upload": "1.1.5", + "wodo": "http://webodf.org/download/wodocollabtexteditor-0.5.8.preview1.zip" }, "devDependencies": { "angular-mocks": "1.4.1", "angular-scenario": "1.4.1" }, "resolutions": { "angular": "1.4.1" } } diff --git a/client/app/editor/editor.controller.js b/client/app/editor/editor.controller.js deleted file mode 100644 index 757844e..0000000 --- a/client/app/editor/editor.controller.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; - -angular.module('manticoreApp') - .controller('EditorCtrl', function ($scope) { - $scope.message = 'Hello'; - }); diff --git a/client/app/editor/editor.jade b/client/app/editor/editor.jade index ec056f8..dfe5988 100644 --- a/client/app/editor/editor.jade +++ b/client/app/editor/editor.jade @@ -1,3 +1,2 @@ div(ng-include='"components/navbar/navbar.html"') -.container - | This is the editor view. +wodo-editor diff --git a/client/app/editor/editor.js b/client/app/editor/editor.js index da10e49..067854c 100644 --- a/client/app/editor/editor.js +++ b/client/app/editor/editor.js @@ -1,11 +1,18 @@ 'use strict'; angular.module('manticoreApp') .config(function ($stateProvider) { $stateProvider .state('editor', { url: '/document/:id', + resolve: { + document: function ($stateParams, $http) { + return $http.get('/api/documents/' + $stateParams.id); + } + }, templateUrl: 'app/editor/editor.html', - controller: 'EditorCtrl' + controller: function ($scope, document) { + $scope.document = document; + } }); }); diff --git a/client/app/editor/editor.styl b/client/app/editor/editor.styl index e69de29..574b044 100644 --- a/client/app/editor/editor.styl +++ b/client/app/editor/editor.styl @@ -0,0 +1,4 @@ +#wodoContainer + width 100% + height 500px + padding 0 diff --git a/client/app/main/main.controller.spec.js b/client/app/main/main.controller.spec.js index 65a1e3e..a24e473 100644 --- a/client/app/main/main.controller.spec.js +++ b/client/app/main/main.controller.spec.js @@ -1,28 +1,23 @@ 'use strict'; describe('Controller: MainCtrl', function () { // load the controller's module beforeEach(module('manticoreApp')); var MainCtrl, scope, $httpBackend; // Initialize the controller and a mock scope beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('/api/documents') .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']); scope = $rootScope.$new(); MainCtrl = $controller('MainCtrl', { $scope: scope }); })); - - it('should attach a list of documents to the scope', function () { - $httpBackend.flush(); - expect(scope.documents.length).toBe(4); - }); }); diff --git a/client/components/mongoose-error/mongoose-error.directive.js b/client/components/mongoose-error/mongoose-error.directive.js index 77babaf..fe75281 100644 --- a/client/components/mongoose-error/mongoose-error.directive.js +++ b/client/components/mongoose-error/mongoose-error.directive.js @@ -1,17 +1,17 @@ 'use strict'; /** * Removes server error when user updates input */ angular.module('manticoreApp') .directive('mongooseError', function () { return { restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ngModel) { element.on('keydown', function() { return ngModel.$setValidity('mongoose', true); }); } }; - }); \ No newline at end of file + }); diff --git a/client/components/wodo/adaptor.service.js b/client/components/wodo/adaptor.service.js new file mode 100644 index 0000000..cc90fbc --- /dev/null +++ b/client/components/wodo/adaptor.service.js @@ -0,0 +1,282 @@ +/*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, + 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; + }; + + function init() { + socket = io('', { + query: 'token=' + authToken + }); + 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 new file mode 100644 index 0000000..8206a03 --- /dev/null +++ b/client/components/wodo/editor.controller.js @@ -0,0 +1,72 @@ +'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() { + editorInstance.leaveSession(function () { + clientAdaptor.leaveSession(function () { + console.log('Closed editing, left session.'); + }); + }); + } + + 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; + } + 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'); + } + ); + } + + this.boot = boot; +}); diff --git a/client/app/editor/editor.controller.spec.js b/client/components/wodo/editor.controller.spec.js similarity index 100% rename from client/app/editor/editor.controller.spec.js rename to client/components/wodo/editor.controller.spec.js diff --git a/client/components/wodo/editor.directive.js b/client/components/wodo/editor.directive.js new file mode 100644 index 0000000..4647c77 --- /dev/null +++ b/client/components/wodo/editor.directive.js @@ -0,0 +1,78 @@ +'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'; + + 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; + + // 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); + head.appendChild(frag); + } + + loadDependencies(function () { + wodoCtrl.boot(); + }); + } + }; +}); diff --git a/client/components/wodo/editor.jade b/client/components/wodo/editor.jade new file mode 100644 index 0000000..810f8e9 --- /dev/null +++ b/client/components/wodo/editor.jade @@ -0,0 +1 @@ +#wodoContainer diff --git a/client/index.html b/client/index.html index d53f82b..d58fa45 100644 --- a/client/index.html +++ b/client/index.html @@ -1,72 +1,75 @@
+ - + + + diff --git a/package.json b/package.json index 34be3dd..9142a8a 100644 --- a/package.json +++ b/package.json @@ -1,92 +1,94 @@ { "name": "manticore", "version": "0.0.0", "main": "server/app.js", "dependencies": { "express": "~4.0.0", "morgan": "~1.0.0", "body-parser": "~1.5.0", "method-override": "~1.0.0", "serve-favicon": "~2.0.1", "cookie-parser": "~1.0.1", "express-session": "~1.0.2", "errorhandler": "~1.0.0", "compression": "~1.0.1", "lodash": "~2.4.1", "jade": "~1.2.0", "mongoose": "~3.8.8", "gridfs-stream": "1.1.1", "jsonwebtoken": "^0.3.0", "express-jwt": "^0.1.3", "passport": "~0.2.0", "passport-local": "~0.1.6", "composable-middleware": "^0.3.0", "connect-mongo": "^0.4.1", - "multer": "0.1.8" + "multer": "0.1.8", + "socket.io": "1.3.5", + "socketio-jwt": "4.2.0" }, "devDependencies": { "grunt": "~0.4.4", "grunt-autoprefixer": "~0.7.2", "grunt-wiredep": "~1.8.0", "grunt-concurrent": "~0.5.0", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.4.0", "grunt-contrib-copy": "~0.5.0", "grunt-contrib-cssmin": "~0.9.0", "grunt-contrib-htmlmin": "~0.2.0", "grunt-contrib-imagemin": "~0.7.1", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-uglify": "~0.4.0", "grunt-contrib-watch": "~0.6.1", "grunt-contrib-jade": "^0.11.0", "grunt-google-cdn": "~0.4.0", "grunt-newer": "~0.7.0", "grunt-ng-annotate": "^0.2.3", "grunt-rev": "~0.1.0", "grunt-svgmin": "~0.4.0", "grunt-usemin": "~2.1.1", "grunt-env": "~0.4.1", "grunt-node-inspector": "~0.1.5", "grunt-nodemon": "~0.2.0", "grunt-angular-templates": "^0.5.4", "grunt-dom-munger": "^3.4.0", "grunt-protractor-runner": "^1.1.0", "grunt-asset-injector": "^0.1.0", "grunt-karma": "~0.8.2", "grunt-build-control": "DaftMonk/grunt-build-control", "grunt-mocha-test": "~0.10.2", "grunt-contrib-stylus": "latest", "jit-grunt": "^0.5.0", "time-grunt": "~0.3.1", "grunt-express-server": "~0.4.17", "grunt-open": "~0.2.3", "open": "~0.0.4", "jshint-stylish": "~0.1.5", "connect-livereload": "~0.4.0", "karma-ng-scenario": "~0.1.0", "karma-firefox-launcher": "~0.1.3", "karma-script-launcher": "~0.1.0", "karma-html2js-preprocessor": "~0.1.0", "karma-ng-jade2js-preprocessor": "^0.1.2", "karma-jasmine": "~0.1.5", "karma-chrome-launcher": "~0.1.3", "requirejs": "~2.1.11", "karma-requirejs": "~0.2.1", "karma-coffee-preprocessor": "~0.2.1", "karma-jade-preprocessor": "0.0.11", "karma-phantomjs-launcher": "~0.1.4", "karma": "~0.12.9", "karma-ng-html2js-preprocessor": "~0.1.0", "supertest": "~0.11.0", "should": "~3.3.1" }, "engines": { "node": ">=0.10.0" }, "scripts": { "start": "node server/app.js", "test": "grunt test", "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" }, "private": true } diff --git a/server/app.js b/server/app.js index 82acdba..4580da9 100644 --- a/server/app.js +++ b/server/app.js @@ -1,32 +1,36 @@ /** * Main application file */ 'use strict'; // Set default node environment to development process.env.NODE_ENV = process.env.NODE_ENV || 'development'; var express = require('express'); var mongoose = require('mongoose'); var config = require('./config/environment'); // Connect to database mongoose.connect(config.mongo.uri, config.mongo.options); // Populate DB with sample data if(config.seedDB) { require('./config/seed'); } // Setup server var app = express(); var server = require('http').createServer(app); +var socketio = require('socket.io')(server, { + path: '/socket.io' +}); +require('./config/socketio')(socketio); require('./config/express')(app); require('./routes')(app); // Start server server.listen(config.port, config.ip, function () { console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); }); // Expose app -exports = module.exports = app; \ No newline at end of file +exports = module.exports = app; diff --git a/server/config/socketio.js b/server/config/socketio.js new file mode 100644 index 0000000..ad1d3b7 --- /dev/null +++ b/server/config/socketio.js @@ -0,0 +1,23 @@ +/** + * Socket.io configuration + */ + +'use strict'; + +var config = require('./environment'); + +module.exports = function (socketio) { + // socket.io (v1.x.x) is powered by debug. + // In order to see all the debug output, set DEBUG (in server/config/local.env.js) to including the desired scope. + // + // ex: DEBUG: "http*,socket.io:socket" + + // We can authenticate socket.io users and access their token through socket.handshake.decoded_token + // + // You will need to send the token in `client/components/socket/socket.service.js` + // + socketio.use(require('socketio-jwt').authorize({ + secret: config.secrets.session, + handshake: true + })); +};