From 896052ecd07729e87939510861d7d2d5c642ed15 Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 01:37:30 +0100 Subject: [PATCH 1/8] Allow to use data objects instead of strings for the collection of suggestions, using a render function to convert them into strings and print them in the list --- index.html | 2 +- script/app.js | 19 +++++++--- script/autocomplete.js | 78 +++++++++++++++++++++++++++++++++--------- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/index.html b/index.html index a0569ca..7f6f147 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@

AngularJS: Allmighty-Autocomplete Demo

Search for new released movies:

- +
This is a simple autocomplete directive for AngularJS, packaged in a Angular module. You can check out the source code or download it here: diff --git a/script/app.js b/script/app.js index be9cd3a..c382723 100644 --- a/script/app.js +++ b/script/app.js @@ -12,10 +12,11 @@ app.factory('MovieRetriever', function($http, $q, $timeout){ var moreMovies = ["The Wolverine", "The Smurfs 2", "The Mortal Instruments: City of Bones", "Drinking Buddies", "All the Boys Love Mandy Lane", "The Act Of Killing", "Red 2", "Jobs", "Getaway", "Red Obsession", "2 Guns", "The World's End", "Planes", "Paranoia", "The To Do List", "Man of Steel", "The Way Way Back", "Before Midnight", "Only God Forgives", "I Give It a Year", "The Heat", "Pacific Rim", "Pacific Rim", "Kevin Hart: Let Me Explain", "A Hijacking", "Maniac", "After Earth", "The Purge", "Much Ado About Nothing", "Europa Report", "Stuck in Love", "We Steal Secrets: The Story Of Wikileaks", "The Croods", "This Is the End", "The Frozen Ground", "Turbo", "Blackfish", "Frances Ha", "Prince Avalanche", "The Attack", "Grown Ups 2", "White House Down", "Lovelace", "Girl Most Likely", "Parkland", "Passion", "Monsters University", "R.I.P.D.", "Byzantium", "The Conjuring", "The Internship"] - if(i && i.indexOf('T')!=-1) - movies=moreMovies; - else - movies=moreMovies; + movies = moreMovies; + + movies = movies.map(function(movie){ + return {name: movie, duration: 160}; + }); $timeout(function(){ moviedata.resolve(movies); @@ -46,8 +47,16 @@ app.controller('MyCtrl', function($scope, MovieRetriever){ }); } + /** + * Use this function to convert the suggested object into a suggestion string to insert into the inner text of the
  • + * @return {string} The suggestion to print + */ + $scope.renderResult = function(objSuggestion){ + return objSuggestion.name + ' ('+objSuggestion.duration+' min)'; + }; + $scope.doSomethingElse = function(suggestion){ - console.log("Suggestion selected: " + suggestion ); + console.log("Suggestion selected: " + JSON.stringify(suggestion) ); } }); diff --git a/script/autocomplete.js b/script/autocomplete.js index 377ad0f..d3b6985 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -12,6 +12,7 @@ app.directive('autocomplete', function() { suggestions: '=data', onType: '=onType', onSelect: '=onSelect', + render: '=render', autocompleteRequired: '=' }, controller: ['$scope', function($scope){ @@ -83,10 +84,10 @@ app.directive('autocomplete', function() { // selecting a suggestion with RIGHT ARROW or ENTER $scope.select = function(suggestion){ if(suggestion){ - $scope.searchParam = suggestion; - $scope.searchFilter = suggestion; + $scope.searchParam = suggestion.text; + $scope.searchFilter = suggestion.text; if($scope.onSelect) - $scope.onSelect(suggestion); + $scope.onSelect(suggestion.data); } watching = false; $scope.completing = false; @@ -94,6 +95,36 @@ app.directive('autocomplete', function() { $scope.setIndex(-1); }; + //Default render function will just output the input string: + if(typeof $scope.render !== 'function'){ + $scope.render = function(suggestion){ + if(typeof suggestion !== 'string'){ //User is trying to store objects instead of strings, so the function should be already defined by the user and binded in the `render` attribute + console.error('render function must be defined when using data object suggestions'); + return ''; + } + return suggestion; + }; + } + + /* + $scope.getUniqueId = function(){ + var alphabet = 'abcdefghikjlmnopqrstuvwzyx0123456789'; + return alphabet.split('').shuffle().slice(-12).join(''); + };*/ + + //Every time the suggestions collection changes, it will wrap the elements into the wrappedSuggestions: + $scope.wrappedSuggestions = []; + $scope.$watchCollection('suggestions', function(newSuggestions){ + if(newSuggestions instanceof Array){ + $scope.wrappedSuggestions = newSuggestions.map(function(suggestion){ + return { + text: $scope.render(suggestion), + data: suggestion + }; + }); + console.log('wrappedSuggestions defined'); + } + }); }], link: function(scope, element, attrs){ @@ -249,32 +280,47 @@ app.directive('autocomplete', function() { class="{{ attrs.inputclass }}"\ id="{{ attrs.inputid }}"\ ng-required="{{ autocompleteRequired }}" />\ -
      \ +
        \ \ + ng-click="select(wrappedSuggestion)"\ + ng-bind-html="wrappedSuggestion.text | highlight:searchParam">\
      \
  • ' }; }); + +app.filter('myFilter', function($filter){ + return function(wrappedSuggestions, searchFilter){ + if(wrappedSuggestions instanceof Array){ + searchFilter = searchFilter || ''; + return wrappedSuggestions.filter(function(wrappedSuggestion){ + var searchFilterLower = searchFilter.toLowerCase(); + return wrappedSuggestion.text.toLowerCase().indexOf(searchFilterLower) !== -1; + }); + } + }; +}); + + app.filter('highlight', ['$sce', function ($sce) { return function (input, searchParam) { if (typeof input === 'function') return ''; if (searchParam) { - var words = '(' + - searchParam.split(/\ /).join(' |') + '|' + - searchParam.split(/\ /).join('|') + - ')', - exp = new RegExp(words, 'gi'); - if (words.length) { - input = input.replace(exp, "$1"); - } + //Create a dynamic regexp to find the searchParam in the text within multiple words: + //Some charachters need to be scaped before using them in a regular expression definition: + var escapeRegexp = function(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }; + searchParamEscaped = escapeRegexp(searchParam); + var wordsPattern = '(' + searchParamEscaped.split(/\ /).join(' |') + '|' + searchParamEscaped.split(/\ /).join('|') + ')'; + var exp = new RegExp(wordsPattern, 'gi'); + input = input.replace(exp, "$1"); } return $sce.trustAsHtml(input); }; From 0ed08a181922d184a9471fc92f5281b8bbd6dcc3 Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 02:39:43 +0100 Subject: [PATCH 2/8] Enhance filtering and highlight functionality to allow multiple words and multiple highlighting words within a suggestion --- script/autocomplete.js | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/script/autocomplete.js b/script/autocomplete.js index d3b6985..f63b209 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -300,8 +300,19 @@ app.filter('myFilter', function($filter){ if(wrappedSuggestions instanceof Array){ searchFilter = searchFilter || ''; return wrappedSuggestions.filter(function(wrappedSuggestion){ - var searchFilterLower = searchFilter.toLowerCase(); - return wrappedSuggestion.text.toLowerCase().indexOf(searchFilterLower) !== -1; + var escapeRegexp = function(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }; + var words = searchFilter.replace(/\ +/g, ' ').split(/\ /g); + var escapedWords = words.map(escapeRegexp); //Make sure non alphanumeric characters are escaped properly before constructing the regexp + //It will detect if all the words of the search are present in the suggestion, no matter the order, nor the case: + var pattern = ''; + escapedWords.forEach(function(escapedWord){ + pattern += '(?=.*'+escapedWord+')'; + }); + var rePattern = new RegExp(pattern, 'gi'); + var suggestion = wrappedSuggestion.text; + return rePattern.test(suggestion); }); } }; @@ -312,15 +323,17 @@ app.filter('highlight', ['$sce', function ($sce) { return function (input, searchParam) { if (typeof input === 'function') return ''; if (searchParam) { - //Create a dynamic regexp to find the searchParam in the text within multiple words: - //Some charachters need to be scaped before using them in a regular expression definition: + //Hightlight the words or semiwords that are present in both the search and the suggestion: var escapeRegexp = function(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }; - searchParamEscaped = escapeRegexp(searchParam); - var wordsPattern = '(' + searchParamEscaped.split(/\ /).join(' |') + '|' + searchParamEscaped.split(/\ /).join('|') + ')'; - var exp = new RegExp(wordsPattern, 'gi'); - input = input.replace(exp, "$1"); + var words = searchParam.replace(/\ +/g, ' ').split(/\ /g); + var escapedWords = words.map(escapeRegexp); //Make sure non alphanumeric characters are escaped properly before constructing the regexp + escapedWords.forEach(function(escapedWord){ + var wordPattern = '(?!]*?>)('+escapedWord+')(?![^<]*?<\/span>)'; //Match the escapedWord only if it's not already wrapped within span tags + var wordRegexp = new RegExp(wordPattern, 'gi'); + input = input.replace(wordRegexp, "$1"); + }); } return $sce.trustAsHtml(input); }; From 1e35418e8c8a92aa8550ee1626adc924ac0770ba Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 02:56:49 +0100 Subject: [PATCH 3/8] Cleaning code --- .project | 17 +++++++++++++++++ .settings/.jsdtscope | 6 ++++++ .../org.eclipse.wst.jsdt.ui.superType.container | 1 + .../org.eclipse.wst.jsdt.ui.superType.name | 1 + index.html | 2 +- script/app.js | 14 +------------- script/autocomplete.js | 7 ------- 7 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 .project create mode 100644 .settings/.jsdtscope create mode 100644 .settings/org.eclipse.wst.jsdt.ui.superType.container create mode 100644 .settings/org.eclipse.wst.jsdt.ui.superType.name diff --git a/.project b/.project new file mode 100644 index 0000000..d2425db --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + allmighty-autocomplete + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + + org.eclipse.wst.jsdt.core.jsNature + + diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope new file mode 100644 index 0000000..dcec811 --- /dev/null +++ b/.settings/.jsdtscope @@ -0,0 +1,6 @@ + + + + + + diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container new file mode 100644 index 0000000..49c8cd4 --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.container @@ -0,0 +1 @@ +org.eclipse.wst.jsdt.launching.JRE_CONTAINER \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name new file mode 100644 index 0000000..11006e2 --- /dev/null +++ b/.settings/org.eclipse.wst.jsdt.ui.superType.name @@ -0,0 +1 @@ +Global \ No newline at end of file diff --git a/index.html b/index.html index 7f6f147..a0569ca 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@

    AngularJS: Allmighty-Autocomplete Demo

    Search for new released movies:

    - +
    This is a simple autocomplete directive for AngularJS, packaged in a Angular module. You can check out the source code or download it here: diff --git a/script/app.js b/script/app.js index c382723..11911fb 100644 --- a/script/app.js +++ b/script/app.js @@ -14,10 +14,6 @@ app.factory('MovieRetriever', function($http, $q, $timeout){ movies = moreMovies; - movies = movies.map(function(movie){ - return {name: movie, duration: 160}; - }); - $timeout(function(){ moviedata.resolve(movies); },1000); @@ -47,16 +43,8 @@ app.controller('MyCtrl', function($scope, MovieRetriever){ }); } - /** - * Use this function to convert the suggested object into a suggestion string to insert into the inner text of the
  • - * @return {string} The suggestion to print - */ - $scope.renderResult = function(objSuggestion){ - return objSuggestion.name + ' ('+objSuggestion.duration+' min)'; - }; - $scope.doSomethingElse = function(suggestion){ - console.log("Suggestion selected: " + JSON.stringify(suggestion) ); + console.log("Suggestion selected: " + suggestion ); } }); diff --git a/script/autocomplete.js b/script/autocomplete.js index f63b209..d67938f 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -106,12 +106,6 @@ app.directive('autocomplete', function() { }; } - /* - $scope.getUniqueId = function(){ - var alphabet = 'abcdefghikjlmnopqrstuvwzyx0123456789'; - return alphabet.split('').shuffle().slice(-12).join(''); - };*/ - //Every time the suggestions collection changes, it will wrap the elements into the wrappedSuggestions: $scope.wrappedSuggestions = []; $scope.$watchCollection('suggestions', function(newSuggestions){ @@ -122,7 +116,6 @@ app.directive('autocomplete', function() { data: suggestion }; }); - console.log('wrappedSuggestions defined'); } }); From 7ab34a951e6c9052dc1da5713e8c9164dbeaa85b Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 02:57:57 +0100 Subject: [PATCH 4/8] Clean eclipse files --- .project | 17 ----------------- .settings/.jsdtscope | 6 ------ .../org.eclipse.wst.jsdt.ui.superType.container | 1 - .../org.eclipse.wst.jsdt.ui.superType.name | 1 - 4 files changed, 25 deletions(-) delete mode 100644 .project delete mode 100644 .settings/.jsdtscope delete mode 100644 .settings/org.eclipse.wst.jsdt.ui.superType.container delete mode 100644 .settings/org.eclipse.wst.jsdt.ui.superType.name diff --git a/.project b/.project deleted file mode 100644 index d2425db..0000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - allmighty-autocomplete - - - - - - org.eclipse.wst.jsdt.core.javascriptValidator - - - - - - org.eclipse.wst.jsdt.core.jsNature - - diff --git a/.settings/.jsdtscope b/.settings/.jsdtscope deleted file mode 100644 index dcec811..0000000 --- a/.settings/.jsdtscope +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.container b/.settings/org.eclipse.wst.jsdt.ui.superType.container deleted file mode 100644 index 49c8cd4..0000000 --- a/.settings/org.eclipse.wst.jsdt.ui.superType.container +++ /dev/null @@ -1 +0,0 @@ -org.eclipse.wst.jsdt.launching.JRE_CONTAINER \ No newline at end of file diff --git a/.settings/org.eclipse.wst.jsdt.ui.superType.name b/.settings/org.eclipse.wst.jsdt.ui.superType.name deleted file mode 100644 index 11006e2..0000000 --- a/.settings/org.eclipse.wst.jsdt.ui.superType.name +++ /dev/null @@ -1 +0,0 @@ -Global \ No newline at end of file From ef24097fbdc51b94402b2cd2e56c6393d4e48456 Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 03:06:52 +0100 Subject: [PATCH 5/8] Add render attribute to the documentation --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 48cc796..05ab812 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,9 @@ You can also pass a function that receives changes with the `on-type` attribute. `on-type` : *(optional)* Pass a function that will receive changes, when somebody types something. It passes the full string for any character typed or deleted. You can use that for example to update the array that you passed in data. -`on-select` : *(optional)* Pass a function that will receive changes, when a suggestion is selected. It passes the full string of the suggestion. +`on-select` : *(optional)* Pass a function that will receive changes, when a suggestion is selected. It passes the full string of the suggestion, or the object defined as the suggestion in case you define the suggestions as an array of objects. + +`render` : *(optional)* You can use data objects instead of strings to populate the suggestions array. You only have to assign an array of objects to the suggestions collection, and then define a render function that will be used to convert these objects into strings in order for the autocomplete to print them in the list. You can retrieve these objects as the first parameter in the on-select listener. If you use string suggestions you don't have to define this render function but only if you are using data objects as suggestions. `click-activation` : *(optional)* When `true`, the suggestion box opens on click (unfortunately onfoucs is not implemented properly in most browsers right now). By default it is only activated, when you start typing something. From c1e07ac5a204791c6b8dd8f7241885b13cf34164 Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 11:26:41 +0100 Subject: [PATCH 6/8] Fix error related with undefined render function --- script/autocomplete.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/script/autocomplete.js b/script/autocomplete.js index d67938f..f61c82f 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -95,24 +95,24 @@ app.directive('autocomplete', function() { $scope.setIndex(-1); }; - //Default render function will just output the input string: - if(typeof $scope.render !== 'function'){ - $scope.render = function(suggestion){ - if(typeof suggestion !== 'string'){ //User is trying to store objects instead of strings, so the function should be already defined by the user and binded in the `render` attribute - console.error('render function must be defined when using data object suggestions'); - return ''; - } - return suggestion; - }; - } - //Every time the suggestions collection changes, it will wrap the elements into the wrappedSuggestions: $scope.wrappedSuggestions = []; $scope.$watchCollection('suggestions', function(newSuggestions){ if(newSuggestions instanceof Array){ $scope.wrappedSuggestions = newSuggestions.map(function(suggestion){ + var renderedText; + if(typeof $scope.render === 'function'){ + renderedText = $scope.render(suggestion); + } + else if(typeof suggestion !== 'string'){ + console.error('render function must be defined when using data object suggestions'); + renderedText = ''; + } + else{ + renderedText = suggestion; + } return { - text: $scope.render(suggestion), + text: renderedText, data: suggestion }; }); From d0d2b3c60a17fe1b5907e96e1de39705463f1601 Mon Sep 17 00:00:00 2001 From: Neil Date: Sun, 22 Feb 2015 12:31:46 +0100 Subject: [PATCH 7/8] Fix regexp to avoid matches inside html tags --- script/autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/autocomplete.js b/script/autocomplete.js index f61c82f..b7b02af 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -323,7 +323,7 @@ app.filter('highlight', ['$sce', function ($sce) { var words = searchParam.replace(/\ +/g, ' ').split(/\ /g); var escapedWords = words.map(escapeRegexp); //Make sure non alphanumeric characters are escaped properly before constructing the regexp escapedWords.forEach(function(escapedWord){ - var wordPattern = '(?!]*?>)('+escapedWord+')(?![^<]*?<\/span>)'; //Match the escapedWord only if it's not already wrapped within span tags + var wordPattern = '(?!]*?>)('+escapedWord+')(?![^<]*?<\/span>)(?=[^>]*(<|$))'; //Match the escapedWord only if it's not already wrapped within span tags, and it's not part of an html attribute or tag name (from previous insertions of span tags into the input) var wordRegexp = new RegExp(wordPattern, 'gi'); input = input.replace(wordRegexp, "$1"); }); From 0ef9d5921e678882fbe15444ff0f3ab917abb30e Mon Sep 17 00:00:00 2001 From: Neil Sanz Date: Thu, 19 Mar 2015 22:18:40 +0100 Subject: [PATCH 8/8] Fix selection by enter key in suggestion objects --- script/autocomplete.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/script/autocomplete.js b/script/autocomplete.js index b7b02af..a4cd713 100644 --- a/script/autocomplete.js +++ b/script/autocomplete.js @@ -99,7 +99,7 @@ app.directive('autocomplete', function() { $scope.wrappedSuggestions = []; $scope.$watchCollection('suggestions', function(newSuggestions){ if(newSuggestions instanceof Array){ - $scope.wrappedSuggestions = newSuggestions.map(function(suggestion){ + $scope.wrappedSuggestions = newSuggestions.map(function(suggestion, counterIndex){ var renderedText; if(typeof $scope.render === 'function'){ renderedText = $scope.render(suggestion); @@ -113,7 +113,8 @@ app.directive('autocomplete', function() { } return { text: renderedText, - data: suggestion + data: suggestion, + _id: ''+(counterIndex+1) }; }); } @@ -238,7 +239,12 @@ app.directive('autocomplete', function() { index = scope.getIndex(); // scope.preSelectOff(); if(index !== -1) { - scope.select(angular.element(angular.element(this).find('li')[index]).text()); + var jLiElement = angular.element(angular.element(this).find('li')[index]); + var suggestionId = jLiElement.attr('data-suggestion-id'); + var suggestion = scope.wrappedSuggestions.filter(function(wrappedSuggestion){ + return suggestionId == wrappedSuggestion._id; + })[0]; + scope.select(suggestion); if(keycode == key.enter) { e.preventDefault(); } @@ -279,6 +285,7 @@ app.directive('autocomplete', function() { ng-repeat="wrappedSuggestion in wrappedSuggestions | myFilter:searchFilter | orderBy:\'text\' track by $index"\ index="{{ $index }}"\ val="{{ wrappedSuggestion.text }}"\ + data-suggestion-id="{{ wrappedSuggestion._id }}"\ ng-class="{ active: ($index === selectedIndex) }"\ ng-click="select(wrappedSuggestion)"\ ng-bind-html="wrappedSuggestion.text | highlight:searchParam">
  • \