diff --git a/bower.json b/bower.json index d0aaa32..369998a 100644 --- a/bower.json +++ b/bower.json @@ -1,27 +1,28 @@ { "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", - "wodo": "http://webodf.org/download/wodocollabtexteditor-0.5.8.preview1.zip" + "wodo": "http://webodf.org/download/wodocollabtexteditor-0.5.8.preview1.zip", + "angular-file-saver": "~0.0.4" }, "devDependencies": { "angular-mocks": "1.4.1", "angular-scenario": "1.4.1" }, "resolutions": { "angular": "1.4.1" } } diff --git a/client/app/app.js b/client/app/app.js index 742c4cc..5149bbb 100644 --- a/client/app/app.js +++ b/client/app/app.js @@ -1,54 +1,55 @@ 'use strict'; angular.module('manticoreApp', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ui.router', 'ui.bootstrap', - 'angularFileUpload' + 'angularFileUpload', + 'fileSaver' ]) .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { $urlRouterProvider .otherwise('/'); $locationProvider.html5Mode(true); $httpProvider.interceptors.push('authInterceptor'); }) .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) { return { // Add authorization token to headers request: function (config) { config.headers = config.headers || {}; if ($cookieStore.get('token')) { config.headers.Authorization = 'Bearer ' + $cookieStore.get('token'); } return config; }, // Intercept 401s and redirect you to login responseError: function(response) { if(response.status === 401) { $location.path('/login'); // remove any stale tokens $cookieStore.remove('token'); return $q.reject(response); } else { return $q.reject(response); } } }; }) .run(function ($rootScope, $location, Auth) { // Redirect to login if route requires auth and you're not logged in $rootScope.$on('$stateChangeStart', function (event, next) { Auth.isLoggedInAsync(function(loggedIn) { if (next.authenticate && !loggedIn) { $location.path('/login'); } }); }); }); diff --git a/client/app/app.styl b/client/app/app.styl index f5b180a..3337e56 100644 --- a/client/app/app.styl +++ b/client/app/app.styl @@ -1,49 +1,51 @@ @import "font-awesome/css/font-awesome.css" @import "bootstrap/dist/css/bootstrap.css" // // Bootstrap Fonts // @font-face font-family: 'Glyphicons Halflings' src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot') src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); // // Font Awesome Fonts // @font-face font-family: 'FontAwesome' src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0') src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); font-weight: normal font-style: normal // // App-wide Styles // .browsehappy background #ccc color #000 margin 0.2em 0 padding 0.2em 0 // Component styles are injected through grunt // injector @import 'account/login/login.styl'; @import 'admin/admin.styl'; @import 'editor/editor.styl'; @import 'main/main.styl'; +@import 'exportButton/exportButton.styl'; @import 'import/import.styl'; @import 'modal/modal.styl'; +@import 'titleEditor/titleEditor.styl'; @import 'wodo/editor.styl'; // endinjector diff --git a/client/app/editor/editor.jade b/client/app/editor/editor.jade index e7aa789..e56d102 100644 --- a/client/app/editor/editor.jade +++ b/client/app/editor/editor.jade @@ -1,2 +1,7 @@ -div(ng-include='"components/navbar/navbar.html"') +div.toolbar + div.toolbar-item.title + title-editor + div.toolbar-item + export-button + wodo-editor.wodo diff --git a/client/app/editor/editor.styl b/client/app/editor/editor.styl index cc15fa1..e6c6f8b 100644 --- a/client/app/editor/editor.styl +++ b/client/app/editor/editor.styl @@ -1,9 +1,13 @@ -.navbar +.toolbar border none +.toolbar-item + display: inline-block; + height: 50px; + float: left; #wodoContainer width 100% height calc(100% - 50px) top 50px padding 0 position absolute diff --git a/client/components/exportButton/exportButton.controller.js b/client/components/exportButton/exportButton.controller.js new file mode 100644 index 0000000..45f6867 --- /dev/null +++ b/client/components/exportButton/exportButton.controller.js @@ -0,0 +1,25 @@ +'use strict'; + +angular.module('manticoreApp') +.controller('ExportButtonCtrl', function ($scope, SaveAs) { + $scope.items = [ + { + label: 'OpenDocument Text (.odt)', + format: 'odt' + } + ]; + + $scope.export = function (format) { + var fileName = $scope.editor.getMetadata('dc:title') || $scope.document.title, + addExtension = fileName.split('.').pop() !== 'odt'; + + if (format === 'odt') { + $scope.editor.getDocumentAsByteArray(function (err, data) { + SaveAs.download( + [data.buffer], + fileName + (addExtension ? '.odt' : ''), + { type: 'application/vnd.oasis.opendocument.text' }); + }); + } + }; +}); diff --git a/client/components/exportButton/exportButton.controller.spec.js b/client/components/exportButton/exportButton.controller.spec.js new file mode 100644 index 0000000..b79999f --- /dev/null +++ b/client/components/exportButton/exportButton.controller.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('Controller: ExportButtonCtrl', function () { + + // load the controller's module + beforeEach(module('manticoreApp')); + + var ExportButtonCtrl, scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + ExportButtonCtrl = $controller('ExportButtonCtrl', { + $scope: scope + }); + })); + + it('should ...', function () { + expect(1).toEqual(1); + }); +}); diff --git a/client/components/exportButton/exportButton.directive.js b/client/components/exportButton/exportButton.directive.js new file mode 100644 index 0000000..0a9c7e2 --- /dev/null +++ b/client/components/exportButton/exportButton.directive.js @@ -0,0 +1,10 @@ +'use strict'; + +angular.module('manticoreApp') + .directive('exportButton', function () { + return { + templateUrl: 'components/exportButton/exportButton.html', + restrict: 'E', + controller: 'ExportButtonCtrl' + }; + }); diff --git a/client/components/exportButton/exportButton.jade b/client/components/exportButton/exportButton.jade new file mode 100644 index 0000000..3854e59 --- /dev/null +++ b/client/components/exportButton/exportButton.jade @@ -0,0 +1,7 @@ +div.export-button.btn-group(dropdown) + button.btn.btn-danger(type='button' ng-click='export(items[0].format)') Download + button.btn.btn-danger.dropdown-toggle(dropdown-toggle) + span.caret + ul.dropdown-menu(role='menu') + li(ng-repeat='item in items') + a(href='#' ng-click='export(item.format)') {{item.label}}... diff --git a/client/components/exportButton/exportButton.styl b/client/components/exportButton/exportButton.styl new file mode 100644 index 0000000..6d6c501 --- /dev/null +++ b/client/components/exportButton/exportButton.styl @@ -0,0 +1,3 @@ +.export-button + padding: 10px 10px; + height: 50px; diff --git a/client/components/titleEditor/titleEditor.controller.js b/client/components/titleEditor/titleEditor.controller.js new file mode 100644 index 0000000..fd87dad --- /dev/null +++ b/client/components/titleEditor/titleEditor.controller.js @@ -0,0 +1,45 @@ +'use strict'; + +/*global Wodo*/ + +angular.module('manticoreApp') +.controller('TitleEditorCtrl', function ($scope, $timeout) { + function handleTitleChanged(changes) { + var title = changes.setProperties['dc:title']; + if (title !== undefined && title !== $scope.title) { + $timeout(function () { + $scope.title = title; + }); + } + } + + $scope.changeTitle = function () { + if ($scope.title !== $scope.editor.getMetadata('dc:title')) { + $scope.editor.setMetadata({ + 'dc:title': $scope.title + }); + } + }; + + $scope.handleEnterKey = function ($event) { + if ($event.keyCode === 13) { + $event.target.blur(); + } + }; + + $scope.$watch('joined', function (online) { + if (online === undefined) { return; } + if (online) { + $scope.editor.addEventListener(Wodo.EVENT_METADATACHANGED, handleTitleChanged); + $scope.title = $scope.editor.getMetadata('dc:title'); + } else { + $scope.editor.removeEventListener(Wodo.EVENT_METADATACHANGED, handleTitleChanged); + } + }); + + function init() { + $scope.title = $scope.document.title; + } + + init(); +}); diff --git a/client/components/titleEditor/titleEditor.controller.spec.js b/client/components/titleEditor/titleEditor.controller.spec.js new file mode 100644 index 0000000..e37f59b --- /dev/null +++ b/client/components/titleEditor/titleEditor.controller.spec.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('Controller: TitleEditorCtrl', function () { + + // load the controller's module + beforeEach(module('manticoreApp')); + + var TitleEditorCtrl, scope; + + // Initialize the controller and a mock scope + beforeEach(inject(function ($controller, $rootScope) { + scope = $rootScope.$new(); + TitleEditorCtrl = $controller('TitleEditorCtrl', { + $scope: scope + }); + })); + + it('should ...', function () { + expect(1).toEqual(1); + }); +}); diff --git a/client/components/titleEditor/titleEditor.directive.js b/client/components/titleEditor/titleEditor.directive.js new file mode 100644 index 0000000..e44bc4b --- /dev/null +++ b/client/components/titleEditor/titleEditor.directive.js @@ -0,0 +1,10 @@ +'use strict'; + +angular.module('manticoreApp') + .directive('titleEditor', function () { + return { + templateUrl: 'components/titleEditor/titleEditor.html', + restrict: 'E', + controller: 'TitleEditorCtrl' + }; + }); diff --git a/client/components/titleEditor/titleEditor.jade b/client/components/titleEditor/titleEditor.jade new file mode 100644 index 0000000..27b66fa --- /dev/null +++ b/client/components/titleEditor/titleEditor.jade @@ -0,0 +1,7 @@ +span.title-container + input.title-editor( + ng-model='title' + ng-disabled='!joined' + ng-blur='changeTitle()' + ng-keypress='handleEnterKey($event)' + placeholder='Untitled Document') diff --git a/client/components/titleEditor/titleEditor.styl b/client/components/titleEditor/titleEditor.styl new file mode 100644 index 0000000..3d04259 --- /dev/null +++ b/client/components/titleEditor/titleEditor.styl @@ -0,0 +1,20 @@ +.title-container + display: block; + max-width: 400px; + padding: 10px 10px; + height: 50px; + +.title-editor + float: left; + font-size: 16px; + line-height: 20px; + padding: 5px; + border: none; + outline: none; + text-overflow: ellipsis; + resize: horizontal; + width: 100%; + text-decoration: underline; + background-color: white; + &:hover, &:focus + background-color: #eeeeee; diff --git a/client/components/wodo/editor.controller.js b/client/components/wodo/editor.controller.js index cb0fa72..d508dd2 100644 --- a/client/components/wodo/editor.controller.js +++ b/client/components/wodo/editor.controller.js @@ -1,85 +1,92 @@ 'use strict'; /*global Wodo*/ angular.module('manticoreApp') .controller('WodoCtrl', function ($scope, Auth, Adaptor) { var editorInstance, clientAdaptor, editorOptions = { collabEditingEnabled: true, allFeaturesEnabled: true }, onConnectCalled = false; function closeEditing() { editorInstance.leaveSession(function () { + $scope.$apply(function () { + $scope.joined = false; + }); 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; - + $scope.editor = editor; editorInstance.addEventListener(Wodo.EVENT_UNKNOWNERROR, handleEditingError); - editorInstance.joinSession(clientAdaptor, function () {}); + editorInstance.joinSession(clientAdaptor, function () { + $scope.$apply(function () { + $scope.joined = true; + }); + }); }); } 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'); } ); } function destroy (cb) { if (editorInstance) { closeEditing(); editorInstance.destroy(cb); } else { if (clientAdaptor) { clientAdaptor.leaveSession(); clientAdaptor.destroy(); cb(); } } } this.boot = boot; this.destroy = destroy; }); diff --git a/client/components/wodo/editor.styl b/client/components/wodo/editor.styl index 0b52e7e..c8b4e43 100644 --- a/client/components/wodo/editor.styl +++ b/client/components/wodo/editor.styl @@ -1,26 +1,27 @@ // // Wodo/Bootstrap overrides // // Hide member list view .wodo .webodfeditor-editor width 100% !important height 100% !important border none !important .wodo .webodfeditor-members display none .wodo .dijitToolbar background-image none !important background-color #f8f8f8 !important + border-top: 1px solid lightgray; // Resolve bootstrap conflicts and hide cursors' avatar handles .wodo .webodf-caretOverlay .caret border-right none border-top none margin none height inherit !important .handle display none !important diff --git a/client/index.html b/client/index.html index d58fa45..8782027 100644 --- a/client/index.html +++ b/client/index.html @@ -1,75 +1,82 @@
+ + + + + + +