diff --git a/app/directives/listView.html b/app/directives/listView.html
index b6ee6f4a..765e2025 100644
--- a/app/directives/listView.html
+++ b/app/directives/listView.html
@@ -1,5 +1,5 @@
@@ -9,4 +9,8 @@
+
diff --git a/src/javascripts/Directives/listView.js b/src/javascripts/Directives/listView.js
index 5cdba57d..d10e922a 100644
--- a/src/javascripts/Directives/listView.js
+++ b/src/javascripts/Directives/listView.js
@@ -14,6 +14,18 @@ ngapp.directive('listView', function() {
}
});
+ngapp.filter('listViewFilter', function() {
+ return function(items, filterItems, filterOptions) {
+ if (!filterOptions.onlyShowMatches) return items;
+ if (!filterItems) return items;
+ return items.filter(item => {
+ return filterItems.reduce((b, f) => {
+ return b && f.filter(item, f.text);
+ }, true);
+ });
+ }
+});
+
ngapp.controller('listViewController', function($scope, $timeout, $element, hotkeyService, contextMenuService, contextMenuFactory, htmlHelpers) {
// initialization
$scope.parent = htmlHelpers.findParent($element[0], el => {
@@ -21,9 +33,13 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
});
$scope.listItems = $element[0].firstElementChild;
+ $scope.filteredItems = $scope.items;
+ $scope.filterOptions = {
+ onlyShowMatches: false
+ };
// helper variables
- let prevIndex = -1,
+ let firstFilteredIndex = -1,
eventListeners = {
click: e => $scope.$apply(() => $scope.onParentClick(e)),
keydown: e => $scope.$apply(() => {
@@ -31,6 +47,36 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
})
};
+ const prevIndex = {
+ _value: -1,
+ _filteredValue: -1,
+ get value() {
+ return this._value;
+ },
+ get filteredValue() {
+ return this._filteredValue;
+ },
+ set filteredValue(i) {
+ this._filteredValue = i;
+ this._value = i > -1 ? $scope.filteredItems[i].index : -1;
+ }
+ }
+
+ $scope.$watchCollection("filteredItems", function() {
+ if ($scope.filterOptions.onlyShowMatches) {
+ prevIndex.filteredValue = toFilteredIndex(prevIndex.value);
+
+ // When only matches are shown, the first index is trivially 0.
+ firstFilteredIndex = $scope.filteredItems.length > 0 ? 0 : -1;
+ } else {
+ prevIndex.filteredValue = prevIndex.value;
+ }
+
+ // Can't know if the change happened cause of onlyShowMatches or
+ // filteredItems, so call it always.
+ $scope.filterChanged();
+ });
+
// helper functions
let removeClasses = function(element) {
element.classList.remove('insert-after');
@@ -50,6 +96,18 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
index === dragData.index;
};
+ let checkFilters = function(item) {
+ return $scope.filterItems.reduce((b, f) => {
+ return b && f.filter(item, f.text);
+ }, true);
+ }
+
+ let toFilteredIndex = function(index) {
+ return $scope.filteredItems.findIndex(item => {
+ return item.index === index;
+ });
+ }
+
// inherited variables and functions
$scope.contextMenuItems = contextMenuFactory.checkboxListItems;
hotkeyService.buildOnKeyDown($scope, 'onKeyDown', 'listView');
@@ -62,12 +120,13 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
$scope.clearSelection = function(resetPrevIndex) {
$scope.items.forEach(item => item.selected = false);
- if (resetPrevIndex) prevIndex = -1;
+ if (resetPrevIndex) prevIndex.filteredValue = -1;
};
$scope.selectAll = function() {
+ if ($scope.filterOptions.onlyShowMatches) $scope.clearSelection(false);
$scope.items.forEach(item => item.selected = true);
- prevIndex = $scope.items.length - 1;
+ prevIndex.filteredValue = $scope.filteredItems.length - 1;
};
$scope.toggleSelected = function(targetValue) {
@@ -100,22 +159,22 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
};
$scope.selectItem = function(e, index, scroll = true) {
- let item = $scope.items[index];
+ let item = $scope.filteredItems[index];
if (scroll) $scope.scrollTo(index);
- if (e.shiftKey && prevIndex > -1) {
- let start = Math.min(index, prevIndex),
- end = Math.max(index, prevIndex);
+ if (e.shiftKey && prevIndex.filteredValue > -1) {
+ let start = Math.min(index, prevIndex.filteredValue),
+ end = Math.max(index, prevIndex.filteredValue);
if (!e.ctrlKey) $scope.clearSelection();
for (let i = start; i <= end; i++) {
- $scope.items[i].selected = true;
+ $scope.filteredItems[i].selected = true;
}
} else if (e.ctrlKey) {
item.selected = !item.selected;
- prevIndex = index;
+ prevIndex.filteredValue = index;
} else {
$scope.clearSelection(true);
item.selected = true;
- prevIndex = index;
+ prevIndex.filteredValue = index;
}
};
@@ -131,13 +190,13 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
};
$scope.handleUpArrow = function() {
- prevIndex = (prevIndex < 1 ? $scope.items.length : prevIndex) - 1;
- $scope.selectItem({}, prevIndex);
+ prevIndex.filteredValue = (prevIndex.filteredValue < 1 ? $scope.filteredItems.length : prevIndex.filteredValue) - 1;
+ $scope.selectItem({}, prevIndex.filteredValue);
};
$scope.handleDownArrow = function() {
- prevIndex = (prevIndex >= $scope.items.length - 1 ? -1 : prevIndex) + 1;
- $scope.selectItem({}, prevIndex);
+ prevIndex.filteredValue = (prevIndex.filteredValue >= $scope.filteredItems.length - 1 ? -1 : prevIndex.filteredValue) + 1;
+ $scope.selectItem({}, prevIndex.filteredValue);
};
$scope.onParentClick = function(e) {
@@ -149,7 +208,7 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
};
$scope.onItemMouseDown = function(e, index) {
- let item = $scope.items[index];
+ let item = $scope.filteredItems[index];
if (e.button !== 2 || !item.selected) $scope.selectItem(e, index);
if (e.button === 2) $scope.showContextMenu(e);
};
@@ -160,7 +219,7 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
element: e.target,
source: $scope.dragType,
index: index,
- getItem: () => $scope.items.splice(index, 1)[0]
+ getItem: () => $scope.items.splice($scope.filteredItems[index].index, 1)[0]
});
return true;
};
@@ -189,13 +248,31 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
if (!dragData || dragData.source !== $scope.dragType) return;
if (onSameItem(dragData, e, index)) return;
let after = e.offsetY > (e.target.offsetHeight / 2),
+ // This and adjust cannot use filteredItem's length because that
+ // array will still not be updated yet after getItem is called.
lengthBefore = $scope.items.length,
- movedItem = dragData.getItem(),
+ movedItem = dragData.getItem(), // The item is removed in-place.
adjust = lengthBefore > $scope.items.length && index > dragData.index;
removeClasses(e.target);
- $scope.items.splice(index + after - adjust, 0, movedItem);
- prevIndex = index + after - adjust;
+ // Translate the index to the one in the unfiltered items array. If the
+ // destination index is the end of the array, then there won't be an
+ // item at that index within filteredItems. In such case, the translated
+ // index should just be the last index in the items array + 1
+ // i.e. the length of the items array.
+ //
+ // It may seem like items.length is wrong to use when onlyShowMatches
+ // is true. However, when it's true, the item at that index will still
+ // exist in filteredItems because the array has not been updated yet to
+ // reflect the deletion caused by getItem(). It does reflect the change
+ // when onlyShowMatches is false because filteredItems then refers to
+ // the same array instance as `items`.
+ const spliceStart = index < $scope.filteredItems.length ? $scope.filteredItems[index].index : $scope.items.length;
+ $scope.items.splice(spliceStart + after - adjust, 0, movedItem);
$scope.$emit('itemsReordered');
+ // Unfortunately, execution order is important here.
+ // This must happen after itemsReordered is handled.
+ // The setter relies on the `index` property of the item being updated.
+ prevIndex.filteredValue = index + after - adjust;
return true;
};
@@ -221,15 +298,68 @@ ngapp.controller('listViewController', function($scope, $timeout, $element, hotk
};
$scope.filterChanged = function() {
- if (!$scope.filterItems) return;
- let index = $scope.items.findIndex(item => {
- return $scope.filterItems.reduce((b, f) => {
- return b && f.filter(item, f.text);
- }, true);
+ let prevMatches = false;
+
+ if ($scope.filterOptions.onlyShowMatches) {
+ // When only matches are shown, the first index is trivially 0.
+ firstFilteredIndex = $scope.filteredItems.length > 0 ? 0 : -1;
+
+ if (!$scope.filterItems || firstFilteredIndex === -1) return;
+
+ prevMatches = prevIndex.filteredValue !== -1;
+ } else {
+ if (!$scope.filterItems) return;
+
+ firstFilteredIndex = $scope.filteredItems.findIndex(checkFilters);
+ if (firstFilteredIndex === -1) return;
+
+ // If the first index is the previous index, then it's already known
+ // that the previous index is a match.
+ prevMatches = firstFilteredIndex === prevIndex.value || (prevIndex.value !== -1 && checkFilters($scope.items[prevIndex.value]));
+ }
+
+ // Don't select the item if the previous selection already matches,
+ // or if the found index is already selected.
+ if (!prevMatches && !$scope.filteredItems[firstFilteredIndex].selected) {
+ $scope.selectItem({}, firstFilteredIndex); // This also calls scrollTo.
+ } else {
+ // Even if the selection remains, it may have gone out of view if
+ // onlyShowMatches was toggled.
+ $scope.scrollTo(firstFilteredIndex);
+ }
+ };
+
+ $scope.selectNextFiltered = function() {
+ if (!$scope.filterItems || $scope.filteredItems.length === 0 || firstFilteredIndex === -1) return;
+
+ if ($scope.filterOptions.onlyShowMatches) {
+ // The next index doesn't need to be searched for;
+ // it's trivially +1 since only matching items are being shown.
+ $scope.handleDownArrow();
+ return;
+ }
+
+ let index = $scope.filteredItems.findIndex((item, i) => {
+ // Find the next index such that all filters are satisfied.
+ // Skip items that are at/before the previously selected index.
+ return i > prevIndex.filteredValue && checkFilters(item);
});
- if (index === -1) return;
+
+ // The end has been reached; cycle back to the start.
+ if (index === -1) index = firstFilteredIndex;
+
$scope.selectItem({}, index);
- };
+ }
+
+ $scope.onOnlyShowMatchesChanged = function() {
+ // This executes before the filter is updated so that the
+ // firstFilteredIndex is still valid.
+ if (!$scope.filterOptions.onlyShowMatches) {
+ if (firstFilteredIndex > -1) {
+ firstFilteredIndex = $scope.filteredItems[firstFilteredIndex].index;
+ }
+ }
+ }
$scope.$on('destroy', () => toggleEventListeners(false));
diff --git a/src/javascripts/Factories/hotkeyFactory.js b/src/javascripts/Factories/hotkeyFactory.js
index 29a350a5..d10d9092 100644
--- a/src/javascripts/Factories/hotkeyFactory.js
+++ b/src/javascripts/Factories/hotkeyFactory.js
@@ -282,14 +282,15 @@ ngapp.service('hotkeyFactory', function() {
}]
};
- let closeFilter = (scope, e) => {
- e.stopPropagation();
- scope.toggleFilter(false);
- };
-
this.listViewFilterHotkeys = {
- escape: closeFilter,
- enter: closeFilter,
+ escape: (scope, e) => {
+ e.stopPropagation();
+ scope.toggleFilter(false);
+ },
+ enter: (scope, e) => {
+ e.stopPropagation();
+ scope.selectNextFiltered();
+ },
a: [{
modifiers: ['ctrlKey'],
callback: (scope, e) => {