diff --git a/test/client-old/spec/controllers/inventoryCtrlSpec.js b/test/client-old/spec/controllers/inventoryCtrlSpec.js index 955b6f4f0c..92f4d2818f 100644 --- a/test/client-old/spec/controllers/inventoryCtrlSpec.js +++ b/test/client-old/spec/controllers/inventoryCtrlSpec.js @@ -450,4 +450,84 @@ describe('Inventory Controller', function() { expect(scope.hasAllTimeTravelerItemsOfType('mounts')).to.eql(true); })); }); + + describe('Gear search filter', function() { + var wrap = function(text) { + return {'text': function() {return text;}}; + } + + var toText = function(list) { + return _.map(list, function(ele) { return ele.text(); }); + } + + var gearByClass, gearByType; + + beforeEach(function() { + scope.$digest(); + gearByClass = {'raw': [wrap('kale'), wrap('sashimi')], + 'cooked': [wrap('chicken'), wrap('potato')]}; + + gearByType = {'veg': [wrap('kale'), wrap('potato')], + 'not': [wrap('chicken'), wrap('sashimi')]}; + scope.gearByClass = gearByClass; + scope.gearByType = gearByType; + scope.equipmentQuery.query = 'a'; + }); + + it('filters nothing if equipmentQuery is nothing', function() { + scope.equipmentQuery.query = ''; + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken', 'sashimi']); + }); + + it('filters out gear if class gear changes', function() { + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato']); + + scope.gearByClass['raw'].push(wrap('zucchini')); + scope.gearByClass['cooked'].push(wrap('pizza')); + scope.$digest(); + expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato', 'pizza']); + }); + + it('filters out gear if typed gear changes', function() { + scope.$digest(); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi']); + + scope.gearByType['veg'].push(wrap('zucchini')); + scope.gearByType['not'].push(wrap('pizza')); + + scope.$digest(); + expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']); + expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi', 'pizza']); + }); + + it('filters out gear if filter query changes', function() { + scope.equipmentQuery.query = 'c'; + scope.$digest(); + + expect(toText(scope.filteredGearByClass['raw'])).to.eql([]); + expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken']); + expect(toText(scope.filteredGearByType['veg'])).to.eql([]); + expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken']); + }); + + it('returns the right filtered gear', function() { + var equipment = [wrap('spicy tuna'), wrap('dragon'), wrap('rainbow'), wrap('caterpillar')]; + expect(toText(scope.equipmentSearch(equipment, 'ra'))).to.eql(['dragon', 'rainbow']); + }); + + it('returns the right filtered gear if the source gear has unicode', function() { + // blue hat, red hat, red shield + var equipment = [wrap('藍色軟帽'), wrap('紅色軟帽'), wrap('紅色盾牌')]; + // searching for 'red' gives red hat, red shield + expect(toText(scope.equipmentSearch(equipment, '紅色'))).to.eql(['紅色軟帽', '紅色盾牌']); + }); + }); }); diff --git a/website/client-old/css/inventory.styl b/website/client-old/css/inventory.styl index ccaeaddd81..977174c883 100644 --- a/website/client-old/css/inventory.styl +++ b/website/client-old/css/inventory.styl @@ -31,6 +31,10 @@ vertical-align: bottom margin-right: 20px +.equipment-search + max-width: 300px + margin-right: 20px + .well.use-costume-info margin-top: 10px p:first-child diff --git a/website/client-old/js/controllers/inventoryCtrl.js b/website/client-old/js/controllers/inventoryCtrl.js index 312c6d1c9c..6644921f77 100644 --- a/website/client-old/js/controllers/inventoryCtrl.js +++ b/website/client-old/js/controllers/inventoryCtrl.js @@ -8,6 +8,7 @@ habitrpg.controller("InventoryCtrl", $scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5} $scope.selectedPotion = null; // {index: 5, name: "Red", value: 3} + $scope.equipmentQuery = {'query': ''}; _updateDropAnimalCount(user.items); @@ -85,6 +86,50 @@ habitrpg.controller("InventoryCtrl", }) }, true); + $scope.equipmentSearch = function(equipment, term) { + if (!equipment) return; + if (!angular.isString(term) || term.length == 0) { + return equipment; + } + termMatcher = new RegExp(term, 'i'); + + var result = []; + for (var i = 0; i < equipment.length; i++) { + if (termMatcher.test(equipment[i].text())) { + result.push(equipment[i]); + } + } + return result; + }; + + $scope.updateEquipment = function(gearByClass, gearByType, equipmentQuery) { + $scope.filteredGearByClass = {}; + $scope.filteredGearByType = {}; + _.forEach(gearByClass, function(value, key) { + var searchResult = $scope.equipmentSearch(value, equipmentQuery); + if (searchResult.length > 0) { + $scope.filteredGearByClass[key] = searchResult; + } + }); + _.forEach(gearByType, function(value, key) { + var searchResult = $scope.equipmentSearch(value, equipmentQuery); + if (searchResult.length > 0) { + $scope.filteredGearByType[key] = searchResult; + } + }); + } + + $scope.$watch(function(){ + return ['gearByClass', 'gearByType', 'equipmentQuery'].map(angular.bind($scope, $scope.$eval)); + }, function(updatedVals) { + var gearByClass = updatedVals[0]; + var gearByType = updatedVals[1]; + var equipmentQuery = updatedVals[2]; + $scope.updateEquipment(gearByClass, gearByType, equipmentQuery.query); + }, true); + + $scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentQuery.query); + $scope.chooseEgg = function(egg){ if ($scope.selectedEgg && $scope.selectedEgg.key == egg) { return $scope.selectedEgg = null; // clicked same egg, unselect diff --git a/website/views/options/inventory/equipment.jade b/website/views/options/inventory/equipment.jade index 1075501d00..380de2d55c 100644 --- a/website/views/options/inventory/equipment.jade +++ b/website/views/options/inventory/equipment.jade @@ -31,16 +31,22 @@ mixin equipmentButton(type) popover-append-to-body='true') mixin equipmentList(equipmentType) - menu.pets-menu(label='{{::label}}', ng-show='gearByClass[klass]', ng-if='groupingChoice === "klass"', + menu.pets-menu(label='{{::label}}', ng-show='filteredGearByClass[klass]', ng-if='groupingChoice === "klass"', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}') - div(ng-repeat='item in gearByClass[klass] | orderBy: order') + div(ng-repeat='item in filteredGearByClass[klass] | orderBy: order') +equipmentButton(equipmentType) - menu.pets-menu(label='{{::label}}', ng-show='gearByType[type]', ng-if='groupingChoice === "equipmentType"', + menu.pets-menu(label='{{::label}}', ng-show='filteredGearByType[type]', ng-if='groupingChoice === "equipmentType"', ng-repeat='(type,label) in {headAccessory:env.t("headAccessoryCapitalized"), head:env.t("headgearCapitalized"), eyewear:env.t("eyewear"), weapon:env.t("weaponCapitalized"), shield:env.t("offhandCapitalized"), armor:env.t("armorCapitalized"), body:env.t("body"), back:env.t("back")}') - div(ng-repeat='item in gearByType[type] | orderBy: order', ng-show='item.klass !== "base"') + div(ng-repeat='item in filteredGearByType[type] | orderBy: order', ng-show='item.klass !== "base"') +equipmentButton(equipmentType) .container-fluid + .row + .col-md-6 + .input-group.equipment-search + .input-group-addon + .glyphicon.glyphicon-search + input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery.query', ng-model-options='{ debounce: 250 }') .row .col-md-6.border-right(ng-controller="SortableInventoryController") h3.equipment-title.hint(popover-trigger='mouseenter',