1angular.module( 'MassAutoComplete', [] ) 2.directive( 'massAutocomplete', ["$timeout", "$window", "$document", "$q", function( $timeout, $window, $document, $q ) { 3 'use strict'; 4 5 return { 6 restrict: "A", 7 scope: { options: '&massAutocomplete' }, 8 transclude: true, 9 template: 10 '<span ng-transclude></span>' + 11 '<div class="ac-container" ng-show="show_autocomplete && results.length > 0" style="position:absolute;">' + 12 '<ul class="ac-menu">' + 13 '<li ng-repeat="result in results" ng-if="$index > 0" ' + 14 'class="ac-menu-item" ng-class="$index == selected_index ? \'ac-state-focus\': \'\'">' + 15 '<a href ng-click="apply_selection($index)" ng-bind-html="result.label"></a>' + 16 '</li>' + 17 '</ul>' + 18 '</div>', 19 link: function( scope, element ) { 20 scope.container = angular.element( element[0].getElementsByClassName( 'ac-container' )[0] ); 21 }, 22 controller: ["$scope", function( $scope ) { 23 var that = this; 24 25 var KEYS = { 26 TAB: 9, 27 ESC: 27, 28 ENTER: 13, 29 UP: 38, 30 DOWN: 40 31 }; 32 33 var EVENTS = { 34 KEYDOWN: 'keydown', 35 RESIZE: 'resize', 36 BLUR: 'blur' 37 }; 38 39 var _user_options = $scope.options() || {}; 40 var user_options = { 41 debounce_position: _user_options.debounce_position || 150, 42 debounce_attach: _user_options.debounce_attach || 300, 43 debounce_suggest: _user_options.debounce_suggest || 200, 44 debounce_blur: _user_options.debounce_blur || 150 45 }; 46 47 var current_element, 48 current_model, 49 current_options, 50 previous_value, 51 value_watch, 52 last_selected_value; 53 54 $scope.show_autocomplete = false; 55 56 // Debounce - taken from underscore 57 function debounce( func, wait, immediate ) { 58 var timeout; 59 return function() { 60 var context = this, args = arguments; 61 var later = function() { 62 timeout = null; 63 if( !immediate ) func.apply( context, args ); 64 }; 65 var callNow = immediate && !timeout; 66 clearTimeout( timeout ); 67 timeout = setTimeout( later, wait ); 68 if( callNow ) func.apply( context, args ); 69 }; 70 } 71 72 function _position_autocomplete() { 73 if (!current_element) 74 return; 75 var rect = current_element[0].getBoundingClientRect(), 76 scrollTop = $document[0].body.scrollTop || $document[0].documentElement.scrollTop || $window.pageYOffset, 77 scrollLeft = $document[0].body.scrollLeft || $document[0].documentElement.scrollLeft || $window.pageXOffset, 78 container = $scope.container[0]; 79 80 container.style.top = rect.top + rect.height + scrollTop + 'px'; 81 container.style.left = rect.left + scrollLeft + 'px'; 82 container.style.width = rect.width + 'px'; 83 } 84 var position_autocomplete = debounce( _position_autocomplete, user_options.debounce_position ); 85 86 // Attach autocomplete behaviour to an input element. 87 function _attach( ngmodel, target_element, options ) { 88 // Element is already attached. 89 if( current_element === target_element ) return; 90 // Safe: clear previously attached elements. 91 if( current_element ) that.detach(); 92 // The element is still the active element. 93 if( target_element[0] !== $document[0].activeElement ) return; 94 95 options.on_attach && options.on_attach(); 96 97 current_element = target_element; 98 current_model = ngmodel; 99 current_options = options; 100 previous_value = ngmodel.$viewValue; 101 102 $scope.results = []; 103 $scope.selected_index = -1; 104 bind_element(); 105 106 value_watch = $scope.$watch( 107 function() { 108 return ngmodel.$modelValue; 109 }, 110 function( nv, ov ) { 111 // Prevent suggestion cycle when the value is the last value selected. 112 // When selecting from the menu the ng-model is updated and this watch 113 // is triggered. This causes another suggestion cycle that will provide as 114 // suggestion the value that is currently selected - this is unnecessary. 115 if( nv === last_selected_value ) 116 return; 117 118 _position_autocomplete(); 119 suggest( nv, current_element ); 120 } 121 ); 122 } 123 that.attach = debounce( _attach, user_options.debounce_attach ); 124 125 function _suggest( term, target_element ) { 126 $scope.selected_index = 0; 127 $scope.waiting_for_suggestion = true; 128 129 if( typeof ( term ) === 'string' && term.length > 0 ) { 130 $q.when( current_options.suggest( term ), 131 function suggest_succeeded( suggestions ) { 132 // Make sure the suggestion we are processing is of the current element. 133 // When using remote sources for example, a suggestion cycnle might be 134 // triggered at a later time (When a different field is in focus). 135 if( !current_element || current_element !== target_element ) 136 return; 137 138 if( suggestions && suggestions.length > 0 ) { 139 // Add the original term as the first value to enable the user 140 // to return to his original expression after suggestions were made. 141 $scope.results = [{ value: term, label: '' }].concat( suggestions ); 142 $scope.show_autocomplete = true; 143 if( current_options.auto_select_first ) 144 set_selection( 1 ); 145 } else { 146 $scope.results = []; 147 } 148 }, 149 function suggest_failed( error ) { 150 $scope.show_autocomplete = false; 151 current_options.on_error && current_options.on_error( error ); 152 } 153 ).finally( function suggest_finally() { 154 $scope.waiting_for_suggestion = false; 155 } ); 156 } else { 157 $scope.waiting_for_suggestion = false; 158 $scope.show_autocomplete = false; 159 $scope.$apply(); 160 } 161 } 162 var suggest = debounce( _suggest, user_options.debounce_suggest ); 163 164 // Trigger end of editing and remove all attachments made by 165 // this directive to the input element. 166 that.detach = function() { 167 if( current_element ) { 168 var value = current_element.val(); 169 update_model_value( value ); 170 current_options.on_detach && current_options.on_detach( value ); 171 current_element.unbind( EVENTS.KEYDOWN ); 172 current_element.unbind( EVENTS.BLUR ); 173 } 174 175 // Clear references and events. 176 $scope.show_autocomplete = false; 177 angular.element( $window ).unbind( EVENTS.RESIZE ); 178 value_watch && value_watch(); 179 $scope.selected_index = $scope.results = undefined; 180 current_model = current_element = previous_value = undefined; 181 }; 182 183 // Update angular's model view value. 184 // It is important that before triggering hooks the model's view 185 // value will be synced with the visible value to the user. This will 186 // allow the consumer controller to rely on its local ng-model. 187 function update_model_value( value ) { 188 if( current_model.$modelValue !== value ) { 189 current_model.$setViewValue( value ); 190 current_model.$render(); 191 } 192 } 193 194 // Set the current selection while navigating through the menu. 195 function set_selection( i ) { 196 // We use value instead of setting the model's view value 197 // because we watch the model value and setting it will trigger 198 // a new suggestion cycle. 199 var selected = $scope.results[i];
200 current_element.val( selected.value ); 201 $scope.selected_index = i; 202 return selected; 203 } 204 205 // Apply and accept the current selection made from the menu. 206 // When selecting from the menu directly (using click or touch) the 207 // selection is directly applied. 208 $scope.apply_selection = function( i ) { 209 current_element[0].focus(); 210 if( !$scope.show_autocomplete || i > $scope.results.length || i < 0 ) 211 return; 212 213 var selected = set_selection( i ); 214 last_selected_value = selected.value; 215 update_model_value( selected.value ); 216 $scope.show_autocomplete = false; 217 218 current_options.on_select && current_options.on_select( selected ); 219 }; 220 221 function bind_element() { 222 angular.element( $window ).bind( EVENTS.RESIZE, position_autocomplete ); 223 224 current_element.bind( EVENTS.BLUR, function() { 225 // Detach the element from the auto complete when input loses focus. 226 // Focus is lost when a selection is made from the auto complete menu 227 // using the mouse (or touch). In that case we don't want to detach so 228 // we wait several ms for the input to regain focus. 229 $timeout( function() { 230 if( !current_element || current_element[0] !== $document[0].activeElement ) 231 that.detach(); 232 }, user_options.debounce_blur ); 233 } ); 234 235 current_element.bind( EVENTS.KEYDOWN, function( e ) { 236 // Reserve key combinations with shift for different purposes. 237 if( e.shiftKey ) return; 238 239 switch( e.keyCode ) { 240 // Close the menu if it's open. Or, undo changes made to the value 241 // if the menu is closed. 242 case KEYS.ESC: 243 if( $scope.show_autocomplete ) { 244 $scope.show_autocomplete = false; 245 $scope.$apply(); 246 } else { 247 current_element.val( previous_value ); 248 } 249 break; 250 251 // Select an element and close the menu. Or, if a selection is 252 // unavailable let the event propagate. 253 case KEYS.ENTER: 254 // Accept a selection only if results exist, the menu is 255 // displayed and the results are valid (no current request 256 // for new suggestions is active). 257 if( $scope.show_autocomplete && 258 $scope.selected_index > 0 && 259 !$scope.waiting_for_suggestion ) { 260 $scope.apply_selection( $scope.selected_index ); 261 // When selecting an item from the AC list the focus is set on 262 // the input element. So the enter will cause a keypress event 263 // on the input itself. Since this enter is not intended for the 264 // input but for the AC result we prevent propagation to parent 265 // elements because this event is not of their concern. We cannot 266 // prevent events from firing when the event was registered on 267 // the input itself. 268 e.stopPropagation(); 269 e.preventDefault(); 270 } 271 272 $scope.show_autocomplete = false; 273 $scope.$apply(); 274 break; 275 276 // Navigate the menu when it's open. When it's not open fall back 277 // to default behavior. 278 case KEYS.TAB: 279 if( !$scope.show_autocomplete ) 280 break; 281 282 e.preventDefault(); 283 /* falls through */ 284 285 // Open the menu when results exists but are not displayed. Or, 286 // select the next element when the menu is open. When reaching 287 // bottom wrap to top. 288 case KEYS.DOWN: 289 if( $scope.results.length > 0 ) { 290 if( $scope.show_autocomplete ) { 291 set_selection( $scope.selected_index + 1 > $scope.results.length - 1 ? 0 : $scope.selected_index + 1 ); 292 } else { 293 $scope.show_autocomplete = true; 294 $scope.selected_index = 0; 295 } 296 $scope.$apply(); 297 } 298 break; 299 300 // Navigate up in the menu. When reaching the top wrap to bottom. 301 case KEYS.UP: 302 if( $scope.show_autocomplete ) { 303 e.preventDefault(); 304 set_selection( $scope.selected_index - 1 >= 0 ? $scope.selected_index - 1 : $scope.results.length - 1 ); 305 $scope.$apply(); 306 } 307 break; 308 } 309 } ); 310 } 311 312 $scope.$on( '$destroy', function() { 313 that.detach(); 314 $scope.container.remove(); 315 } ); 316 }] 317 }; 318}] ) 319 320.directive( 'massAutocompleteItem', function() { 321 'use strict'; 322 323 return { 324 restrict: "A",
325 require: ["^massAutocomplete", "ngModel"], 326 scope: { 'massAutocompleteItem': "&" }, 327 link: function( scope, element, attrs, required ) { 328 // Prevent html5/browser auto completion. 329 attrs.$set( 'autocomplete', 'off' ); 330 331 element.bind( 'focus', function() { 332 var options = scope.massAutocompleteItem(); 333 if( !options ) 334 throw "Invalid options"; 335 required[0].attach( required[1], element, options ); 336 } ); 337 } 338 }; 339} );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.