PageSourceSearch

https://it-conf.ru/lib/jQuery/mass-autocomplete.js

js it-conf.ru collected 2026-09-26 11:48:53 UTC 12,042 bytes, 339 lines download raw bytes

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.