Commit 18bea245 authored by Robert Knight's avatar Robert Knight

Modernize <simple-search> directive

 - Convert the <simple-search> directive to a modern component-style
   directive which uses one-way bindings and events rather than two-way
   bindings. This makes it consistent with other components in the app.

 - Avoid duplicating the search filter state in multiple places but
   store it only in the Redux store and update it via an action.

The tests for `simple-search` have not been updated in this commit
because they are going to be replaced with a new set using the
`createDirective()` helper in the next commit.
parent bcb55d44
...@@ -63,6 +63,7 @@ function initialState(settings) { ...@@ -63,6 +63,7 @@ function initialState(settings) {
} }
var types = { var types = {
CLEAR_SELECTION: 'CLEAR_SELECTION',
SELECT_ANNOTATIONS: 'SELECT_ANNOTATIONS', SELECT_ANNOTATIONS: 'SELECT_ANNOTATIONS',
FOCUS_ANNOTATIONS: 'FOCUS_ANNOTATIONS', FOCUS_ANNOTATIONS: 'FOCUS_ANNOTATIONS',
HIGHLIGHT_ANNOTATIONS: 'HIGHLIGHT_ANNOTATIONS', HIGHLIGHT_ANNOTATIONS: 'HIGHLIGHT_ANNOTATIONS',
...@@ -107,6 +108,11 @@ function reducer(state, action) { ...@@ -107,6 +108,11 @@ function reducer(state, action) {
state = annotationsReducer(state, action); state = annotationsReducer(state, action);
switch (action.type) { switch (action.type) {
case types.CLEAR_SELECTION:
return Object.assign({}, state, {
filterQuery: null,
selectedAnnotationMap: null,
});
case types.SELECT_ANNOTATIONS: case types.SELECT_ANNOTATIONS:
return Object.assign({}, state, {selectedAnnotationMap: action.selection}); return Object.assign({}, state, {selectedAnnotationMap: action.selection});
case types.FOCUS_ANNOTATIONS: case types.FOCUS_ANNOTATIONS:
...@@ -278,7 +284,7 @@ module.exports = function (settings) { ...@@ -278,7 +284,7 @@ module.exports = function (settings) {
/** De-select all annotations. */ /** De-select all annotations. */
clearSelectedAnnotations: function () { clearSelectedAnnotations: function () {
select({}); store.dispatch({type: 'CLEAR_SELECTION'});
}, },
/** Add annotations to the currently displayed set. */ /** Add annotations to the currently displayed set. */
......
'use strict'; 'use strict';
var angular = require('angular');
var scrollIntoView = require('scroll-into-view'); var scrollIntoView = require('scroll-into-view');
var events = require('./events'); var events = require('./events');
...@@ -131,20 +130,15 @@ module.exports = function AppController( ...@@ -131,20 +130,15 @@ module.exports = function AppController(
}; };
$scope.clearSelection = function () { $scope.clearSelection = function () {
$scope.search.query = '';
annotationUI.clearSelectedAnnotations(); annotationUI.clearSelectedAnnotations();
}; };
$scope.search = { $scope.search = {
query: $location.search().q, query: function () {
clear: function () { return annotationUI.getState().filterQuery;
$location.search('q', null);
}, },
update: function (query) { update: function (query) {
if (!angular.equals($location.search().q, query)) { annotationUI.setFilterQuery(query);
$location.search('q', query || null); },
annotationUI.clearSelectedAnnotations();
}
}
}; };
}; };
module.exports = ['$http', '$parse', ($http, $parse) -> module.exports = ->
link: (scope, elem, attr, ctrl) -> bindToController: true
button = elem.find('button') controllerAs: 'vm'
input = elem.find('input') controller: ['$element', '$http', '$scope', ($element, $http, $scope) ->
self = this
button = $element.find('button')
input = $element.find('input')[0]
form = $element.find('form')[0]
button.on('click', -> input[0].focus()) button.on('click', -> input.focus())
scope.reset = (event) -> $scope.$watch (-> $http.pendingRequests.length), (pending) ->
event.preventDefault() self.loading = (pending > 0)
scope.query = ''
scope.searchtext = ''
scope.search = (event) -> form.onsubmit = (e) ->
event.preventDefault() e.preventDefault()
scope.query = scope.searchtext self.onSearch({$query: input.value})
scope.$watch (-> $http.pendingRequests.length), (pending) -> this.inputClasses = ->
scope.loading = (pending > 0) 'is-expanded': self.alwaysExpanded || self.query.length > 0
scope.$watch 'query', (query) -> this.$onChanges = (changes) ->
return if query is undefined if changes.query
scope.searchtext = query input.value = changes.query.currentValue
if query
scope.onSearch?(query: scope.searchtext)
else
scope.onClear?()
self
]
restrict: 'E' restrict: 'E'
scope: scope:
# Specifies whether the search input field should always be expanded, # Specifies whether the search input field should always be expanded,
...@@ -32,21 +32,23 @@ module.exports = ['$http', '$parse', ($http, $parse) -> ...@@ -32,21 +32,23 @@ module.exports = ['$http', '$parse', ($http, $parse) ->
# #
# If false, it is only expanded when focused or when 'query' is non-empty # If false, it is only expanded when focused or when 'query' is non-empty
alwaysExpanded: '<' alwaysExpanded: '<'
query: '=' query: '<'
onSearch: '&' onSearch: '&'
onClear: '&'
template: ''' template: '''
<form class="simple-search-form" ng-class="!searchtext && 'simple-search-inactive'" name="searchBox" ng-submit="search($event)"> <form class="simple-search-form"
<input class="simple-search-input" type="text" ng-model="searchtext" name="searchText" name="searchForm"
placeholder="{{loading && 'Loading' || 'Search'}}…" ng-class="!vm.query && 'simple-search-inactive'">
ng-disabled="loading" <input class="simple-search-input"
ng-class="(alwaysExpanded || searchtext.length > 0) ? 'is-expanded' : ''"/> type="text"
<button type="button" class="simple-search-icon top-bar__btn" ng-hide="loading"> name="query"
placeholder="{{vm.loading && 'Loading' || 'Search'}}…"
ng-disabled="vm.loading"
ng-class="vm.inputClasses()"/>
<button type="button" class="simple-search-icon top-bar__btn" ng-hide="vm.loading">
<i class="h-icon-search"></i> <i class="h-icon-search"></i>
</button> </button>
<button type="button" class="simple-search-icon btn btn-clean" ng-show="loading" disabled> <button type="button" class="simple-search-icon btn btn-clean" ng-show="vm.loading" disabled>
<span class="btn-icon"><span class="spinner"></span></span> <span class="btn-icon"><span class="spinner"></span></span>
</button> </button>
</form> </form>
''' '''
]
...@@ -49,15 +49,9 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) { ...@@ -49,15 +49,9 @@ function RootThread($rootScope, annotationUI, searchFilter, viewFilter) {
function buildRootThread(state) { function buildRootThread(state) {
var sortFn = sortFns[state.sortKey]; var sortFn = sortFns[state.sortKey];
var filters;
var filterQuery = state.filterQuery;
if (filterQuery) {
filters = searchFilter.generateFacetedFilter(filterQuery);
}
var filterFn; var filterFn;
if (filterQuery) { if (state.filterQuery) {
var filters = searchFilter.generateFacetedFilter(state.filterQuery);
filterFn = function (annot) { filterFn = function (annot) {
return viewFilter.filter([annot], filters).length > 0; return viewFilter.filter([annot], filters).length > 0;
}; };
......
...@@ -2,13 +2,13 @@ angular = require('angular') ...@@ -2,13 +2,13 @@ angular = require('angular')
module.exports = class StreamController module.exports = class StreamController
this.$inject = [ this.$inject = [
'$scope', '$route', '$rootScope', '$routeParams', '$scope', '$location', '$route', '$rootScope', '$routeParams',
'annotationUI', 'annotationUI',
'queryParser', 'rootThread', 'searchFilter', 'store', 'queryParser', 'rootThread', 'searchFilter', 'store',
'streamer', 'streamFilter', 'annotationMapper' 'streamer', 'streamFilter', 'annotationMapper'
] ]
constructor: ( constructor: (
$scope, $route, $rootScope, $routeParams $scope, $location, $route, $rootScope, $routeParams
annotationUI, annotationUI,
queryParser, rootThread, searchFilter, store, queryParser, rootThread, searchFilter, store,
streamer, streamFilter, annotationMapper streamer, streamFilter, annotationMapper
...@@ -52,6 +52,11 @@ module.exports = class StreamController ...@@ -52,6 +52,11 @@ module.exports = class StreamController
$scope.forceVisible = (id) -> $scope.forceVisible = (id) ->
annotationUI.setForceVisible(id, true) annotationUI.setForceVisible(id, true)
Object.assign $scope.search, {
query: -> $routeParams.q || ''
update: (q) -> $location.search({q: q})
}
thread = -> thread = ->
rootThread.thread(annotationUI.getState()) rootThread.thread(annotationUI.getState())
......
...@@ -202,6 +202,12 @@ describe('annotationUI', function () { ...@@ -202,6 +202,12 @@ describe('annotationUI', function () {
annotationUI.clearSelectedAnnotations(); annotationUI.clearSelectedAnnotations();
assert.isNull(annotationUI.getState().selectedAnnotationMap); assert.isNull(annotationUI.getState().selectedAnnotationMap);
}); });
it('clears the current search query', function () {
annotationUI.setFilterQuery('foo');
annotationUI.clearSelectedAnnotations();
assert.isNull(annotationUI.getState().filterQuery);
});
}); });
describe('#setFilterQuery()', function () { describe('#setFilterQuery()', function () {
......
...@@ -96,6 +96,7 @@ describe 'StreamController', -> ...@@ -96,6 +96,7 @@ describe 'StreamController', ->
beforeEach inject (_$controller_, $rootScope) -> beforeEach inject (_$controller_, $rootScope) ->
$controller = _$controller_ $controller = _$controller_
$scope = $rootScope.$new() $scope = $rootScope.$new()
$scope.search = {}
afterEach -> afterEach ->
sandbox.restore() sandbox.restore()
......
...@@ -264,12 +264,6 @@ module.exports = function WidgetController( ...@@ -264,12 +264,6 @@ module.exports = function WidgetController(
return crossframe.frames; return crossframe.frames;
}, loadAnnotations); }, loadAnnotations);
// Watch the inputs that determine which annotations are currently
// visible and how they are sorted and rebuild the thread when they change
$scope.$watch('search.query', function (query) {
annotationUI.setFilterQuery(query);
});
$scope.setCollapsed = function (id, collapsed) { $scope.setCollapsed = function (id, collapsed) {
annotationUI.setCollapsed(id, collapsed); annotationUI.setCollapsed(id, collapsed);
}; };
......
...@@ -5,9 +5,8 @@ ...@@ -5,9 +5,8 @@
<div class="top-bar__inner content" ng-if="::!isSidebar"> <div class="top-bar__inner content" ng-if="::!isSidebar">
<simple-search <simple-search
class="simple-search" class="simple-search"
query="searchController.query" query="searchController.query()"
on-search="searchController.update(query)" on-search="searchController.update($query)"
on-clear="searchController.clear()"
always-expanded="true"> always-expanded="true">
</simple-search> </simple-search>
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
...@@ -29,9 +28,8 @@ ...@@ -29,9 +28,8 @@
<div class="top-bar__expander"></div> <div class="top-bar__expander"></div>
<simple-search <simple-search
class="simple-search" class="simple-search"
query="searchController.query" query="searchController.query()"
on-search="searchController.update(query)" on-search="searchController.update($query)"
on-clear="searchController.clear()"
title="Filter the annotation list"> title="Filter the annotation list">
</simple-search> </simple-search>
<sort-dropdown <sort-dropdown
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<search-status-bar <search-status-bar
ng-show="!isLoading()" ng-show="!isLoading()"
ng-if="!isStream" ng-if="!isStream"
filter-active="search.query" filter-active="!!search.query()"
filter-match-count="visibleCount()" filter-match-count="visibleCount()"
on-clear-selection="clearSelection()" on-clear-selection="clearSelection()"
search-query="search ? search.query : ''" search-query="search ? search.query : ''"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment