diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html index e396195075a7cf9ebd660fcdf76133c4a029d9e6..eaf87bd581d33395a14c0c6dc65f04544ce01960 100644 --- a/vipra-ui/app/html/index.html +++ b/vipra-ui/app/html/index.html @@ -29,7 +29,10 @@ </div> <div class="row row-spaced"> <div class="col-md-12"> - <input type="text" class="form-control input-lg" placeholder="Search..." ng-model="search" ng-model-options="{debounce:500}"> + <div class="form-group has-feedback"> + <input type="text" class="form-control input-lg" placeholder="Search..." ng-model="search" ng-model-options="{debounce:500}" id="searchBox"> + <i class="form-control-feedback glyphicon glyphicon-search text-muted"></i> + </div> </div> </div> <div class="row row-spaced"> diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html index f52e82571551a8ba9abc2ede86184c4d1104d415..80951609cf22fc7ffc86b08b6cff780287604d0c 100644 --- a/vipra-ui/app/index.html +++ b/vipra-ui/app/index.html @@ -65,11 +65,15 @@ <a ui-sref="topics">Topics</a> </li> </ul> + <form class="navbar-form navbar-left" role="search" ng-hide="$state.current.name === 'index'"> + <div class="form-group has-feedback"> + <input type="text" class="form-control" placeholder="Search..." ng-model="rootModels.search" ng-enter="menubarSearch(rootModels.search)" id="menuSearchBox"> + <i class="form-control-feedback glyphicon glyphicon-search text-muted"></i> + </div> + </form> <ul class="nav navbar-nav navbar-right"> - <li> - <a data-toggle="modal" data-target="#topicModelModal"> - Models - </a> + <li ng-class="{'text-italic':rootModels.topicModel}"> + <a data-toggle="modal" data-target="#topicModelModal" ng-bind-template="{{rootModels.topicModel ? rootModels.topicModel.id : 'Models'}}" ng-attr-title="{{rootModels.topicModel.modelConfig.description}}"></a> </li> <li ui-sref-active="active"> <a ui-sref="about"> @@ -81,7 +85,7 @@ </div> </nav> <div class="main" ui-view ng-cloak></div> - <div id="topicModelModal" class="modal fade" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false"> + <div id="topicModelModal" class="modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> @@ -90,7 +94,7 @@ </div> <div class="modal-body"> <ul class="list-group" ng-show="topicModels.length"> - <button type="button" class="list-group-item" ng-repeat="topicModel in topicModels" ng-click="changeTopicModel(topicModel)" ng-class="{active:rootModels.topicModel.id===topicModel.id}"> + <button type="button" class="list-group-item topic-model" ng-repeat="topicModel in topicModels" ng-click="changeTopicModel(topicModel)" ng-class="{'active selected-model':rootModels.topicModel.id===topicModel.id}"> <span class="badge" ng-bind="topicModel.articleCount" ng-show="topicModel.articleCount" ng-attr-title="{{topicModel.articleCount + ' article(s)'}}"></span> <span class="badge" ng-bind="topicModel.topicCount" ng-show="topicModel.topicCount" ng-attr-title="{{topicModel.topicCount + ' topic(s)'}}"></span> <span ng-bind="topicModel.id"></span> diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js index fe66def8df8c2dfd03fe74a459b7f5d89b9ff3f6..d67c058af5ca1d70c4100e6e1de4cc3c9d6b2035 100644 --- a/vipra-ui/app/js/app.js +++ b/vipra-ui/app/js/app.js @@ -2,7 +2,7 @@ * Vipra Application * Main application file ******************************************************************************/ -/* globals angular, $ */ +/* globals angular */ (function() { "use strict"; @@ -11,6 +11,7 @@ 'ngResource', 'ngSanitize', 'ui.router', + 'cfp.hotkeys', 'nya.bootstrap.select', 'vipra.controllers', 'vipra.directives', @@ -27,9 +28,10 @@ // states $stateProvider.state('index', { - url: '/', + url: '/?q', templateUrl: 'html/index.html', - controller: 'IndexController' + controller: 'IndexController', + reloadOnSearch: false }); $stateProvider.state('about', { @@ -53,9 +55,10 @@ // states: articles $stateProvider.state('articles', { - url: '/articles', + url: '/articles?p', templateUrl: 'html/articles/index.html', - controller: 'ArticlesIndexController' + controller: 'ArticlesIndexController', + reloadOnSearch: false }); $stateProvider.state('articles.show', { @@ -67,9 +70,10 @@ // states: topics $stateProvider.state('topics', { - url: '/topics', + url: '/topics?p', templateUrl: 'html/topics/index.html', - controller: 'TopicsIndexController' + controller: 'TopicsIndexController', + reloadOnSearch: false }); $stateProvider.state('topics.show', { diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js index aae45f1142d6e080ed40330138b6317f8456b4c5..e2493affbc3117036ddc424c42f502a4a4a5530c 100644 --- a/vipra-ui/app/js/controllers.js +++ b/vipra-ui/app/js/controllers.js @@ -7,30 +7,17 @@ "use strict"; - var app = angular.module('vipra.controllers', [ - 'ui.router', - 'vipra.factories' - ]); + var app = angular.module('vipra.controllers', []); - app.controller('RootController', ['$scope', '$state', 'TopicModelFactory', - function($scope, $state, TopicModelFactory) { + app.controller('RootController', ['$scope', '$state', '$location', 'hotkeys', 'TopicModelFactory', + function($scope, $state, $location, hotkeys, TopicModelFactory) { $scope.$state = $state; $scope.rootModels = { - topicModel: null + topicModel: null, + search: null }; - if (localStorage.topicModel) { - try { - var topicModel = JSON.parse(localStorage.topicModel); - TopicModelFactory.get({ - id: topicModel.id - }, function(data) { - $scope.rootModels.topicModel = data; - }); - } catch (e) {} - } - TopicModelFactory.query({ fields: '_all' }, function(data) { @@ -40,29 +27,90 @@ }); $scope.chooseTopicModel = function() { + $scope.rootModels.topicModelModalOpen = true; $('#topicModelModal').modal(); + if ($scope.rootModels.topicModel) + $('.selected-model').focus(); + else + $('.topic-model').first().focus(); }; $scope.changeTopicModel = function(topicModel) { $scope.rootModels.topicModel = topicModel; - localStorage.topicModel = JSON.stringify(topicModel); $('#topicModelModal').modal('hide'); }; + $scope.menubarSearch = function(query) { + $state.transitionTo('index', { + q: query + }); + }; + + $scope.$on('$stateChangeSuccess', function() { + $scope.rootModels.search = null; + }); + + hotkeys.add({ + combo: 's', + description: 'Search for articles', + callback: function($event) { + if ($event.stopPropagation) $event.stopPropagation(); + if ($event.preventDefault) $event.preventDefault(); + if ($state.current.name === 'index') + $('#searchBox').focus(); + else + $('#menuSearchBox').focus(); + } + }); + + hotkeys.add({ + combo: 'e', + description: 'Go to explorer', + callback: function() { + if ($state.current.name !== 'explorer') + $state.transitionTo('explorer'); + } + }); + + hotkeys.add({ + combo: 'a', + description: 'Go to articles', + callback: function() { + if ($state.current.name !== 'articles') + $state.transitionTo('articles'); + } + }); + + hotkeys.add({ + combo: 't', + description: 'Go to topics', + callback: function() { + if ($state.current.name !== 'topics') + $state.transitionTo('topics'); + } + }); + + hotkeys.add({ + combo: 'm', + description: 'Choose a topic model', + callback: function() { + $scope.chooseTopicModel(); + } + }); } ]); /** * Index controller */ - app.controller('IndexController', ['$scope', '$location', 'ArticleFactory', 'TopicFactory', 'SearchFactory', - function($scope, $location, ArticleFactory, TopicFactory, SearchFactory) { + app.controller('IndexController', ['$scope', '$stateParams', '$location', 'ArticleFactory', 'TopicFactory', 'SearchFactory', + function($scope, $stateParams, $location, ArticleFactory, TopicFactory, SearchFactory) { // page was reloaded, choose topic model if (!$scope.rootModels.topicModel) $scope.chooseTopicModel(); - $scope.search = $location.search().query; + $scope.search = $stateParams.q || $scope.search; $scope.$watch('rootModels.topicModel', function() { if (!$scope.rootModels.topicModel) return; @@ -90,25 +138,29 @@ $scope.$watchGroup(['search', 'rootModels.topicModel'], function() { if ($scope.search && $scope.rootModels.topicModel) { - $location.search('query', $scope.search); - $scope.searching = true; - - SearchFactory.query({ - topicModel: $scope.rootModels.topicModel.id, - limit: 10, - query: $scope.search - }, function(data) { - $scope.searching = false; - $scope.searchResults = data; - }, function(err) { - $scope.errors = err; - }); + $location.search('q', $scope.search); + $scope.goSearch(); } else { - $location.search('query', null); + $location.search('q', null); $scope.searchResults = []; } }); + $scope.goSearch = function() { + $scope.searching = true; + + SearchFactory.query({ + topicModel: $scope.rootModels.topicModel.id, + limit: 10, + query: $scope.search + }, function(data) { + $scope.searching = false; + $scope.searchResults = data; + }, function(err) { + $scope.errors = err; + }); + }; + } ]); @@ -517,8 +569,8 @@ /** * Article Index route */ - app.controller('ArticlesIndexController', ['$scope', '$state', '$location', 'ArticleFactory', - function($scope, $state, $location, ArticleFactory) { + app.controller('ArticlesIndexController', ['$scope', '$state', 'ArticleFactory', + function($scope, $state, ArticleFactory) { // page was reloaded, choose topic model if (!$scope.rootModels.topicModel && $state.current.name === 'articles') @@ -527,7 +579,7 @@ $scope.articlesIndexModels = { sortkey: 'date', sortdir: true, - page: Math.max($location.search().page || 1, 1), + page: 1, limit: 100 }; @@ -627,8 +679,8 @@ /** * Topic Index route */ - app.controller('TopicsIndexController', ['$scope', '$state', '$location', 'TopicFactory', - function($scope, $state, $location, TopicFactory) { + app.controller('TopicsIndexController', ['$scope', '$state', 'TopicFactory', + function($scope, $state, TopicFactory) { // page was reloaded, choose topic model if (!$scope.rootModels.topicModel && $state.current.name === 'topics') @@ -637,7 +689,7 @@ $scope.topicsIndexModels = { sortkey: 'name', sortdir: true, - page: Math.max($location.search().page || 1, 1), + page: 1, limit: 100 }; @@ -832,9 +884,6 @@ app.controller('PaginationController', ['$scope', function($scope) { - if (!$scope.page) - $scope.page = 1; - $scope.calculatePages = function() { var pages = [], max = Math.ceil($scope.total / $scope.limit * 1.0), diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js index 0757812a9fdca5848a11dfa697feccb4ef3c08be..5638c5691a07065ad67920c8746cbb3d19aa52be 100644 --- a/vipra-ui/app/js/directives.js +++ b/vipra-ui/app/js/directives.js @@ -234,7 +234,19 @@ }, 10); } }; - }]); + app.directive('ngEnter', function() { + return function($scope, $elem, $attrs) { + $elem.bind("keydown keypress", function(event) { + if (event.which === 13) { + $scope.$apply(function() { + $scope.$eval($attrs.ngEnter); + }); + event.preventDefault(); + } + }); + }; + }); + })(); \ No newline at end of file diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js index e4076cbd99c5b138fade37f6f3b66c74c1cafdfe..31f9b64b4016ec220007ca70220b2990cc375541 100644 --- a/vipra-ui/app/js/factories.js +++ b/vipra-ui/app/js/factories.js @@ -9,41 +9,67 @@ var app = angular.module('vipra.factories', []); - app.factory('ArticleFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/articles/:id', {}, { - similar: { - isArray: true, - url: Vipra.config.restUrl + '/articles/:id/similar' - } - }); + app.factory('$myResource', ['$resource', function($resource) { + return function(url, paramDefaults, actions) { + actions = angular.merge({}, { + get: { + method: 'GET', + cache: true + }, + save: { + method: 'POST' + }, + query: { + method: 'GET', + isArray: true, + cache: true + }, + remove: { + method: 'DELETE' + }, + delete: { + method: 'DELETE' + }, + update: { + method: 'PUT' + } + }, actions); + return $resource(url, paramDefaults, actions); + }; }]); - app.factory('TopicFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/topics/:id', {}, { - update: { - method: 'PUT' - }, + app.factory('ArticleFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/articles/:id'); + }]); + + app.factory('TopicFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/topics/:id', {}, { articles: { + cache: true, isArray: true, url: Vipra.config.restUrl + '/topics/:id/articles' } }); }]); - app.factory('SequenceFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/sequences/:id'); + app.factory('SequenceFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/sequences/:id'); }]); - app.factory('SearchFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/search'); + app.factory('SearchFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/search', {}, { + query: { + cache: false + } + }); }]); - app.factory('InfoFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/info'); + app.factory('InfoFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/info'); }]); - app.factory('TopicModelFactory', ['$resource', function($resource) { - return $resource(Vipra.config.restUrl + '/topicmodels/:id'); + app.factory('TopicModelFactory', ['$myResource', function($myResource) { + return $myResource(Vipra.config.restUrl + '/topicmodels/:id'); }]); })(); \ No newline at end of file diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less index 6feb0298cfd1f60cdcb2253bb55efd71c904d759..28b0aceefed40d77770937a8afe4c9f33ea4130c 100644 --- a/vipra-ui/app/less/app.less +++ b/vipra-ui/app/less/app.less @@ -389,6 +389,7 @@ topic-menu { width: 19px; padding: 5px 0; vertical-align: middle; + position: relative; > i { visibility: hidden; @@ -406,6 +407,10 @@ topic-menu { } } +.text-italic { + font-style: italic; +} + @-moz-keyframes spin { 100% { -moz-transform: rotateY(360deg); @@ -460,3 +465,7 @@ topic-menu { opacity: 1; } } + +[ng\:cloak], [ng-cloak], .ng-cloak { + display: none !important; +} \ No newline at end of file diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json index f46a67d2bdca67f7e0251ab95151465d9f33a0d4..11a5e687589b3f2c35b34590fffe563712a062fc 100644 --- a/vipra-ui/bower.json +++ b/vipra-ui/bower.json @@ -30,6 +30,7 @@ "font-awesome": "^4.x", "awesome-bootstrap-checkbox": "^0.x", "randomcolor": "randomColor#^0.x", - "bootbox.js": "bootbox#^4.4.0" + "bootbox.js": "bootbox#^4.x", + "angular-hotkeys": "chieffancypants/angular-hotkeys#^1.x" } -} +} \ No newline at end of file diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js index 39729e97f35768caff4a7f46c62ad03ccca9e979..5ad351f6952a55416b56a8ab28c45f0834589a0a 100644 --- a/vipra-ui/gulpfile.js +++ b/vipra-ui/gulpfile.js @@ -15,6 +15,7 @@ var assets = { 'bower_components/angular/angular.min.js', 'bower_components/angular-resource/angular-resource.min.js', 'bower_components/angular-sanitize/angular-sanitize.min.js', + 'bower_components/angular-hotkeys/build/hotkeys.min.js', 'bower_components/angular-ui-router/release/angular-ui-router.min.js', 'bower_components/bootstrap/dist/js/bootstrap.min.js', 'bower_components/highcharts/highstock.js', @@ -29,7 +30,8 @@ var assets = { 'bower_components/font-awesome/css/font-awesome.min.css', 'bower_components/vis/dist/vis.min.css', 'bower_components/nya-bootstrap-select/dist/css/nya-bs-select.min.css', - 'bower_components/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css' + 'bower_components/awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css', + 'bower_components/angular-hotkeys/build/hotkeys.min.css' ], fonts: [ 'bower_components/bootstrap/dist/fonts/*',