commonroutesApp/www/lib/angular-material/modules/closure/autocomplete/autocomplete.js
www/lib/angular-material/modules/closure/autocomplete/autocomplete.jsRaw
1/*!
2 * Angular Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.1.1
6 */
7goog.provide('ngmaterial.components.autocomplete');
8goog.require('ngmaterial.components.icon');
9goog.require('ngmaterial.components.virtualRepeat');
10goog.require('ngmaterial.core');
11/**
12 * @ngdoc module
13 * @name material.components.autocomplete
14 */
15/*
16 * @see js folder for autocomplete implementation
17 */
18angular.module('material.components.autocomplete', [
19 'material.core',
20 'material.components.icon',
21 'material.components.virtualRepeat'
22]);
23
24
25MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log"];angular
26 .module('material.components.autocomplete')
27 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
28
29var ITEM_HEIGHT = 41,
30 MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
31 MENU_PADDING = 8,
32 INPUT_PADDING = 2; // Padding provided by `md-input-container`
33
34function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
35 $animate, $rootElement, $attrs, $q, $log) {
36
37 // Internal Variables.
38 var ctrl = this,
39 itemParts = $scope.itemsExpr.split(/ in /i),
40 itemExpr = itemParts[ 1 ],
41 elements = null,
42 cache = {},
43 noBlur = false,
44 selectedItemWatchers = [],
45 hasFocus = false,
46 lastCount = 0,
47 fetchesInProgress = 0,
48 enableWrapScroll = null,
49 inputModelCtrl = null;
50
51 // Public Exported Variables with handlers
52 defineProperty('hidden', handleHiddenChange, true);
53
54 // Public Exported Variables
55 ctrl.scope = $scope;
56 ctrl.parent = $scope.$parent;
57 ctrl.itemName = itemParts[ 0 ];
58 ctrl.matches = [];
59 ctrl.loading = false;
60 ctrl.hidden = true;
61 ctrl.index = null;
62 ctrl.messages = [];
63 ctrl.id = $mdUtil.nextUid();
64 ctrl.isDisabled = null;
65 ctrl.isRequired = null;
66 ctrl.isReadonly = null;
67 ctrl.hasNotFound = false;
68
69 // Public Exported Methods
70 ctrl.keydown = keydown;
71 ctrl.blur = blur;
72 ctrl.focus = focus;
73 ctrl.clear = clearValue;
74 ctrl.select = select;
75 ctrl.listEnter = onListEnter;
76 ctrl.listLeave = onListLeave;
77 ctrl.mouseUp = onMouseup;
78 ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
79 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
80 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
81 ctrl.notFoundVisible = notFoundVisible;
82 ctrl.loadingIsVisible = loadingIsVisible;
83 ctrl.positionDropdown = positionDropdown;
84
85 return init();
86
87 //-- initialization methods
88
89 /**
90 * Initialize the controller, setup watchers, gather elements
91 */
92 function init () {
93 $mdUtil.initOptionalProperties($scope, $attrs, { searchText: '', selectedItem: null });
94 $mdTheming($element);
95 configureWatchers();
96 $mdUtil.nextTick(function () {
97
98 gatherElements();
99 moveDropdown();
100
101 // Forward all focus events to the input element when autofocus is enabled
102 if ($scope.autofocus) {
103 $element.on('focus', focusInputElement);
104 }
105 });
106 }
107
108 function updateModelValidators() {
109 if (!$scope.requireMatch || !inputModelCtrl) return;
110
111 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem);
112 }
113
114 /**
115 * Calculates the dropdown's position and applies the new styles to the menu element
116 * @returns {*}
117 */
118 function positionDropdown () {
119 if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
120 var hrect = elements.wrap.getBoundingClientRect(),
121 vrect = elements.snap.getBoundingClientRect(),
122 root = elements.root.getBoundingClientRect(),
123 top = vrect.bottom - root.top,
124 bot = root.bottom - vrect.top,
125 left = hrect.left - root.left,
126 width = hrect.width,
127 offset = getVerticalOffset(),
128 styles;
129 // Adjust the width to account for the padding provided by `md-input-container`
130 if ($attrs.mdFloatingLabel) {
131 left += INPUT_PADDING;
132 width -= INPUT_PADDING * 2;
133 }
134 styles = {
135 left: left + 'px',
136 minWidth: width + 'px',
137 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
138 };
139 if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
140 styles.top = 'auto';
141 styles.bottom = bot + 'px';
142 styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
143 } else {
144 styles.top = (top - offset) + 'px';
145 styles.bottom = 'auto';
146 styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px';
147 }
148
149 elements.$.scrollContainer.css(styles);
150 $mdUtil.nextTick(correctHorizontalAlignment, false);
151
152 /**
153 * Calculates the vertical offset for floating label examples to account for ngMessages
154 * @returns {number}
155 */
156 function getVerticalOffset () {
157 var offset = 0;
158 var inputContainer = $element.find('md-input-container');
159 if (inputContainer.length) {
160 var input = inputContainer.find('input');
161 offset = inputContainer.prop('offsetHeight');
162 offset -= input.prop('offsetTop');
163 offset -= input.prop('offsetHeight');
164 // add in the height left up top for the floating label text
165 offset += inputContainer.prop('offsetTop');
166 }
167 return offset;
168 }
169
170 /**
171 * Makes sure that the menu doesn't go off of the screen on either side.
172 */
173 function correctHorizontalAlignment () {
174 var dropdown = elements.scrollContainer.getBoundingClientRect(),
175 styles = {};
176 if (dropdown.right > root.right - MENU_PADDING) {
177 styles.left = (hrect.right - dropdown.width) + 'px';
178 }
179 elements.$.scrollContainer.css(styles);
180 }
181 }
182
183 /**
184 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
185 */
186 function moveDropdown () {
187 if (!elements.$.root.length) return;
188 $mdTheming(elements.$.scrollContainer);
189 elements.$.scrollContainer.detach();
190 elements.$.root.append(elements.$.scrollContainer);
191 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
192 }
193
194 /**
195 * Sends focus to the input element.
196 */
197 function focusInputElement () {
198 elements.input.focus();
199 }
200
201 /**
202 * Sets up any watchers used by autocomplete
203 */
204 function configureWatchers () {
205 var wait = parseInt($scope.delay, 10) || 0;
206 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
207 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
208 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
209 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
210 $scope.$watch('selectedItem', selectedItemChange);
211 angular.element($window).on('resize', positionDropdown);
212 $scope.$on('$destroy', cleanup);
213 }
214
215 /**
216 * Removes any events or leftover elements created by this controller
217 */
218 function cleanup () {
219 if (!ctrl.hidden) {
220 $mdUtil.enableScrolling();
221 }
222
223 angular.element($window).off('resize', positionDropdown);
224 if ( elements ){
225 var items = ['ul', 'scroller', 'scrollContainer', 'input'];
226 angular.forEach(items, function(key){
227 elements.$[key].remove();
228 });
229 }
230 }
231
232 /**
233 * Gathers all of the elements needed for this controller
234 */
235 function gatherElements () {
236 elements = {
237 main: $element[0],
238 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'),
239 scroller: $element[0].querySelector('.md-virtual-repeat-scroller'),
240 ul: $element.find('ul')[0],
241 input: $element.find('input')[0],
242 wrap: $element.find('md-autocomplete-wrap')[0],
243 root: document.body
244 };
245
246 elements.li = elements.ul.getElementsByTagName('li');
247 elements.snap = getSnapTarget();
248 elements.$ = getAngularElements(elements);
249
250 inputModelCtrl = elements.$.input.controller('ngModel');
251 }
252
253 /**
254 * Finds the element that the menu will base its position on
255 * @returns {*}
256 */
257 function getSnapTarget () {
258 for (var element = $element; element.length; element = element.parent()) {
259 if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
260 }
261 return elements.wrap;
262 }
263
264 /**
265 * Gathers angular-wrapped versions of each element
266 * @param elements
267 * @returns {{}}
268 */
269 function getAngularElements (elements) {
270 var obj = {};
271 for (var key in elements) {
272 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
273 }
274 return obj;
275 }
276
277 //-- event/change handlers
278
279 /**
280 * Handles changes to the `hidden` property.
281 * @param hidden
282 * @param oldHidden
283 */
284 function handleHiddenChange (hidden, oldHidden) {
285 if (!hidden && oldHidden) {
286 positionDropdown();
287
288 if (elements) {
289 $mdUtil.disableScrollAround(elements.ul);
290 enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
291 }
292 } else if (hidden && !oldHidden) {
293 $mdUtil.enableScrolling();
294
295 if (enableWrapScroll) {
296 enableWrapScroll();
297 enableWrapScroll = null;
298 }
299 }
300 }
301
302 /**
303 * Disables scrolling for a specific element
304 */
305 function disableElementScrollEvents(element) {
306
307 function preventDefault(e) {
308 e.preventDefault();
309 }
310
311 element.on('wheel', preventDefault);
312 element.on('touchmove', preventDefault);
313
314 return function() {
315 element.off('wheel', preventDefault);
316 element.off('touchmove', preventDefault);
317 };
318 }
319
320 /**
321 * When the user mouses over the dropdown menu, ignore blur events.
322 */
323 function onListEnter () {
324 noBlur = true;
325 }
326
327 /**
328 * When the user's mouse leaves the menu, blur events may hide the menu again.
329 */
330 function onListLeave () {
331 if (!hasFocus && !ctrl.hidden) elements.input.focus();
332 noBlur = false;
333 ctrl.hidden = shouldHide();
334 }
335
336 /**
337 * When the mouse button is released, send focus back to the input field.
338 */
339 function onMouseup () {
340 elements.input.focus();
341 }
342
343 /**
344 * Handles changes to the selected item.
345 * @param selectedItem
346 * @param previousSelectedItem
347 */
348 function selectedItemChange (selectedItem, previousSelectedItem) {
349
350 updateModelValidators();
351
352 if (selectedItem) {
353 getDisplayValue(selectedItem).then(function (val) {
354 $scope.searchText = val;
355 handleSelectedItemChange(selectedItem, previousSelectedItem);
356 });
357 } else if (previousSelectedItem && $scope.searchText) {
358 getDisplayValue(previousSelectedItem).then(function(displayValue) {
359 // Clear the searchText, when the selectedItem is set to null.
360 // Do not clear the searchText, when the searchText isn't matching with the previous
361 // selected item.
362 if (displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
363 $scope.searchText = '';
364 }
365 });
366 }
367
368 if (selectedItem !== previousSelectedItem) announceItemChange();
369 }
370
371 /**
372 * Use the user-defined expression to announce changes each time a new item is selected
373 */
374 function announceItemChange () {
375 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
376 }
377
378 /**
379 * Use the user-defined expression to announce changes each time the search text is changed
380 */
381 function announceTextChange () {
382 angular.isFunction($scope.textChange) && $scope.textChange();
383 }
384
385 /**
386 * Calls any external watchers listening for the selected item. Used in conjunction with
387 * `registerSelectedItemWatcher`.
388 * @param selectedItem
389 * @param previousSelectedItem
390 */
391 function handleSelectedItemChange (selectedItem, previousSelectedItem) {
392 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
393 }
394
395 /**
396 * Register a function to be called when the selected item changes.
397 * @param cb
398 */
399 function registerSelectedItemWatcher (cb) {
400 if (selectedItemWatchers.indexOf(cb) == -1) {
401 selectedItemWatchers.push(cb);
402 }
403 }
404
405 /**
406 * Unregister a function previously registered for selected item changes.
407 * @param cb
408 */
409 function unregisterSelectedItemWatcher (cb) {
410 var i = selectedItemWatchers.indexOf(cb);
411 if (i != -1) {
412 selectedItemWatchers.splice(i, 1);
413 }
414 }
415
416 /**
417 * Handles changes to the searchText property.
418 * @param searchText
419 * @param previousSearchText
420 */
421 function handleSearchText (searchText, previousSearchText) {
422 ctrl.index = getDefaultIndex();
423
424 // do nothing on init
425 if (searchText === previousSearchText) return;
426
427 updateModelValidators();
428
429 getDisplayValue($scope.selectedItem).then(function (val) {
430 // clear selected item if search text no longer matches it
431 if (searchText !== val) {
432 $scope.selectedItem = null;
433
434 // trigger change event if available
435 if (searchText !== previousSearchText) announceTextChange();
436
437 // cancel results if search text is not long enough
438 if (!isMinLengthMet()) {
439 ctrl.matches = [];
440 setLoading(false);
441 updateMessages();
442 } else {
443 handleQuery();
444 }
445 }
446 });
447
448 }
449
450 /**
451 * Handles input blur event, determines if the dropdown should hide.
452 */
453 function blur($event) {
454 hasFocus = false;
455
456 if (!noBlur) {
457 ctrl.hidden = shouldHide();
458 evalAttr('ngBlur', { $event: $event });
459 }
460 }
461
462 /**
463 * Force blur on input element
464 * @param forceBlur
465 */
466 function doBlur(forceBlur) {
467 if (forceBlur) {
468 noBlur = false;
469 hasFocus = false;
470 }
471 elements.input.blur();
472 }
473
474 /**
475 * Handles input focus event, determines if the dropdown should show.
476 */
477 function focus($event) {
478 hasFocus = true;
479
480 if (isSearchable() && isMinLengthMet()) {
481 handleQuery();
482 }
483
484 ctrl.hidden = shouldHide();
485
486 evalAttr('ngFocus', { $event: $event });
487 }
488
489 /**
490 * Handles keyboard input.
491 * @param event
492 */
493 function keydown (event) {
494 switch (event.keyCode) {
495 case $mdConstant.KEY_CODE.DOWN_ARROW:
496 if (ctrl.loading) return;
497 event.stopPropagation();
498 event.preventDefault();
499 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
500 updateScroll();
501 updateMessages();
502 break;
503 case $mdConstant.KEY_CODE.UP_ARROW:
504 if (ctrl.loading) return;
505 event.stopPropagation();
506 event.preventDefault();
507 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
508 updateScroll();
509 updateMessages();
510 break;
511 case $mdConstant.KEY_CODE.TAB:
512 // If we hit tab, assume that we've left the list so it will close
513 onListLeave();
514
515 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
516 select(ctrl.index);
517 break;
518 case $mdConstant.KEY_CODE.ENTER:
519 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
520 if (hasSelection()) return;
521 event.stopPropagation();
522 event.preventDefault();
523 select(ctrl.index);
524 break;
525 case $mdConstant.KEY_CODE.ESCAPE:
526 event.preventDefault(); // Prevent browser from always clearing input
527 if (!shouldProcessEscape()) return;
528 event.stopPropagation();
529
530 clearSelectedItem();
531 if ($scope.searchText && hasEscapeOption('clear')) {
532 clearSearchText();
533 }
534
535 // Manually hide (needed for mdNotFound support)
536 ctrl.hidden = true;
537
538 if (hasEscapeOption('blur')) {
539 // Force the component to blur if they hit escape
540 doBlur(true);
541 }
542
543 break;
544 default:
545 }
546 }
547
548 //-- getters
549
550 /**
551 * Returns the minimum length needed to display the dropdown.
552 * @returns {*}
553 */
554 function getMinLength () {
555 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
556 }
557
558 /**
559 * Returns the display value for an item.
560 * @param item
561 * @returns {*}
562 */
563 function getDisplayValue (item) {
564 return $q.when(getItemText(item) || item).then(function(itemText) {
565 if (itemText && !angular.isString(itemText)) {
566 $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
567 'Please check the `md-item-text` attribute.');
568 }
569
570 return itemText;
571 });
572
573 /**
574 * Getter function to invoke user-defined expression (in the directive)
575 * to convert your object to a single string.
576 */
577 function getItemText (item) {
578 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
579 }
580 }
581
582 /**
583 * Returns the locals object for compiling item templates.
584 * @param item
585 * @returns {{}}
586 */
587 function getItemAsNameVal (item) {
588 if (!item) return undefined;
589
590 var locals = {};
591 if (ctrl.itemName) locals[ ctrl.itemName ] = item;
592
593 return locals;
594 }
595
596 /**
597 * Returns the default index based on whether or not autoselect is enabled.
598 * @returns {number}
599 */
600 function getDefaultIndex () {
601 return $scope.autoselect ? 0 : -1;
602 }
603
604 /**
605 * Sets the loading parameter and updates the hidden state.
606 * @param value {boolean} Whether or not the component is currently loading.
607 */
608 function setLoading(value) {
609 if (ctrl.loading != value) {
610 ctrl.loading = value;
611 }
612
613 // Always refresh the hidden variable as something else might have changed
614 ctrl.hidden = shouldHide();
615 }
616
617 /**
618 * Determines if the menu should be hidden.
619 * @returns {boolean}
620 */
621 function shouldHide () {
622 if (!isSearchable()) return true; // Hide when not able to query
623 else return !shouldShow(); // Hide when the dropdown is not able to show.
624 }
625
626 /**
627 * Determines whether the autocomplete is able to query within the current state.
628 * @returns {boolean}
629 */
630 function isSearchable() {
631 if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
632 else if (hasSelection()) return false; // No query if there is already a selection
633 else if (!hasFocus) return false; // No query if the input does not have focus
634 return true;
635 }
636
637 /**
638 * Determines if the escape keydown should be processed
639 * @returns {boolean}
640 */
641 function shouldProcessEscape() {
642 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
643 }
644
645 /**
646 * Determines if an escape option is set
647 * @returns {boolean}
648 */
649 function hasEscapeOption(option) {
650 return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
651 }
652
653 /**
654 * Determines if the menu should be shown.
655 * @returns {boolean}
656 */
657 function shouldShow() {
658 return (isMinLengthMet() && hasMatches()) || notFoundVisible();
659 }
660
661 /**
662 * Returns true if the search text has matches.
663 * @returns {boolean}
664 */
665 function hasMatches() {
666 return ctrl.matches.length ? true : false;
667 }
668
669 /**
670 * Returns true if the autocomplete has a valid selection.
671 * @returns {boolean}
672 */
673 function hasSelection() {
674 return ctrl.scope.selectedItem ? true : false;
675 }
676
677 /**
678 * Returns true if the loading indicator is, or should be, visible.
679 * @returns {boolean}
680 */
681 function loadingIsVisible() {
682 return ctrl.loading && !hasSelection();
683 }
684
685 /**
686 * Returns the display value of the current item.
687 * @returns {*}
688 */
689 function getCurrentDisplayValue () {
690 return getDisplayValue(ctrl.matches[ ctrl.index ]);
691 }
692
693 /**
694 * Determines if the minimum length is met by the search text.
695 * @returns {*}
696 */
697 function isMinLengthMet () {
698 return ($scope.searchText || '').length >= getMinLength();
699 }
700
701 //-- actions
702
703 /**
704 * Defines a public property with a handler and a default value.
705 * @param key
706 * @param handler
707 * @param value
708 */
709 function defineProperty (key, handler, value) {
710 Object.defineProperty(ctrl, key, {
711 get: function () { return value; },
712 set: function (newValue) {
713 var oldValue = value;
714 value = newValue;
715 handler(newValue, oldValue);
716 }
717 });
718 }
719
720 /**
721 * Selects the item at the given index.
722 * @param index
723 */
724 function select (index) {
725 //-- force form to update state for validation
726 $mdUtil.nextTick(function () {
727 getDisplayValue(ctrl.matches[ index ]).then(function (val) {
728 var ngModel = elements.$.input.controller('ngModel');
729 ngModel.$setViewValue(val);
730 ngModel.$render();
731 }).finally(function () {
732 $scope.selectedItem = ctrl.matches[ index ];
733 setLoading(false);
734 });
735 }, false);
736 }
737
738 /**
739 * Clears the searchText value and selected item.
740 */
741 function clearValue () {
742 clearSelectedItem();
743 clearSearchText();
744 }
745
746 /**
747 * Clears the selected item
748 */
749 function clearSelectedItem () {
750 // Reset our variables
751 ctrl.index = 0;
752 ctrl.matches = [];
753 }
754
755 /**
756 * Clears the searchText value
757 */
758 function clearSearchText () {
759 // Set the loading to true so we don't see flashes of content.
760 // The flashing will only occur when an async request is running.
761 // So the loading process will stop when the results had been retrieved.
762 setLoading(true);
763
764 $scope.searchText = '';
765
766 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
767 // But some browsers are not detecting it properly, which means that we have to trigger the event.
768 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
769 // The `change` event is a good alternative and is supported by all supported browsers.
770 var eventObj = document.createEvent('CustomEvent');
771 eventObj.initCustomEvent('change', true, true, { value: '' });
772 elements.input.dispatchEvent(eventObj);
773
774 // For some reason, firing the above event resets the value of $scope.searchText if
775 // $scope.searchText has a space character at the end, so we blank it one more time and then
776 // focus.
777 elements.input.blur();
778 $scope.searchText = '';
779 elements.input.focus();
780 }
781
782 /**
783 * Fetches the results for the provided search text.
784 * @param searchText
785 */
786 function fetchResults (searchText) {
787 var items = $scope.$parent.$eval(itemExpr),
788 term = searchText.toLowerCase(),
789 isList = angular.isArray(items),
790 isPromise = !!items.then; // Every promise should contain a `then` property
791
792 if (isList) onResultsRetrieved(items);
793 else if (isPromise) handleAsyncResults(items);
794
795 function handleAsyncResults(items) {
796 if ( !items ) return;
797
798 items = $q.when(items);
799 fetchesInProgress++;
800 setLoading(true);
801
802 $mdUtil.nextTick(function () {
803 items
804 .then(onResultsRetrieved)
805 .finally(function(){
806 if (--fetchesInProgress === 0) {
807 setLoading(false);
808 }
809 });
810 },true, $scope);
811 }
812
813 function onResultsRetrieved(matches) {
814 cache[term] = matches;
815
816 // Just cache the results if the request is now outdated.
817 // The request becomes outdated, when the new searchText has changed during the result fetching.
818 if ((searchText || '') !== ($scope.searchText || '')) {
819 return;
820 }
821
822 handleResults(matches);
823 }
824 }
825
826 /**
827 * Updates the ARIA messages
828 */
829 function updateMessages () {
830 getCurrentDisplayValue().then(function (msg) {
831 ctrl.messages = [ getCountMessage(), msg ];
832 });
833 }
834
835 /**
836 * Returns the ARIA message for how many results match the current query.
837 * @returns {*}
838 */
839 function getCountMessage () {
840 if (lastCount === ctrl.matches.length) return '';
841 lastCount = ctrl.matches.length;
842 switch (ctrl.matches.length) {
843 case 0:
844 return 'There are no matches available.';
845 case 1:
846 return 'There is 1 match available.';
847 default:
848 return 'There are ' + ctrl.matches.length + ' matches available.';
849 }
850 }
851
852 /**
853 * Makes sure that the focused element is within view.
854 */
855 function updateScroll () {
856 if (!elements.li[0]) return;
857 var height = elements.li[0].offsetHeight,
858 top = height * ctrl.index,
859 bot = top + height,
860 hgt = elements.scroller.clientHeight,
861 scrollTop = elements.scroller.scrollTop;
862 if (top < scrollTop) {
863 scrollTo(top);
864 } else if (bot > scrollTop + hgt) {
865 scrollTo(bot - hgt);
866 }
867 }
868
869 function isPromiseFetching() {
870 return fetchesInProgress !== 0;
871 }
872
873 function scrollTo (offset) {
874 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
875 }
876
877 function notFoundVisible () {
878 var textLength = (ctrl.scope.searchText || '').length;
879
880 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
881 }
882
883 /**
884 * Starts the query to gather the results for the current searchText. Attempts to return cached
885 * results first, then forwards the process to `fetchResults` if necessary.
886 */
887 function handleQuery () {
888 var searchText = $scope.searchText || '';
889 var term = searchText.toLowerCase();
890
891 // If caching is enabled and the current searchText is stored in the cache
892 if (!$scope.noCache && cache[term]) {
893 // The results should be handled as same as a normal un-cached request does.
894 handleResults(cache[term]);
895 } else {
896 fetchResults(searchText);
897 }
898
899 ctrl.hidden = shouldHide();
900 }
901
902 /**
903 * Handles the retrieved results by showing them in the autocompletes dropdown.
904 * @param results Retrieved results
905 */
906 function handleResults(results) {
907 ctrl.matches = results;
908 ctrl.hidden = shouldHide();
909
910 // If loading is in progress, then we'll end the progress. This is needed for example,
911 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
912 if (ctrl.loading) setLoading(false);
913
914 if ($scope.selectOnMatch) selectItemOnMatch();
915
916 updateMessages();
917 positionDropdown();
918 }
919
920 /**
921 * If there is only one matching item and the search text matches its display value exactly,
922 * automatically select that item. Note: This function is only called if the user uses the
923 * `md-select-on-match` flag.
924 */
925 function selectItemOnMatch () {
926 var searchText = $scope.searchText,
927 matches = ctrl.matches,
928 item = matches[ 0 ];
929 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
930 var isMatching = searchText == displayValue;
931 if ($scope.matchInsensitive && !isMatching) {
932 isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
933 }
934
935 if (isMatching) select(0);
936 });
937 }
938
939 /**
940 * Evaluates an attribute expression against the parent scope.
941 * @param {String} attr Name of the attribute to be evaluated.
942 * @param {Object?} locals Properties to be injected into the evaluation context.
943 */
944 function evalAttr(attr, locals) {
945 if ($attrs[attr]) {
946 $scope.$parent.$eval($attrs[attr], locals || {});
947 }
948 }
949
950}
951
952
953MdAutocomplete.$inject = ["$$mdSvgRegistry"];angular
954 .module('material.components.autocomplete')
955 .directive('mdAutocomplete', MdAutocomplete);
956
957/**
958 * @ngdoc directive
959 * @name mdAutocomplete
960 * @module material.components.autocomplete
961 *
962 * @description
963 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
964 * custom query. This component allows you to provide real-time suggestions as the user types
965 * in the input area.
966 *
967 * To start, you will need to specify the required parameters and provide a template for your
968 * results. The content inside `md-autocomplete` will be treated as a template.
969 *
970 * In more complex cases, you may want to include other content such as a message to display when
971 * no matches were found. You can do this by wrapping your template in `md-item-template` and
972 * adding a tag for `md-not-found`. An example of this is shown below.
973 *
974 * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
975 *
976 * ### Validation
977 *
978 * You can use `ng-messages` to include validation the same way that you would normally validate;
979 * however, if you want to replicate a standard input with a floating label, you will have to
980 * do the following:
981 *
982 * - Make sure that your template is wrapped in `md-item-template`
983 * - Add your `ng-messages` code inside of `md-autocomplete`
984 * - Add your validation properties to `md-autocomplete` (ie. `required`)
985 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
986 *
987 * There is an example below of how this should look.
988 *
989 * ### Notes
990 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
991 * directive for displaying the results inside of the dropdown.<br/>
992 * > When encountering issues regarding the item template please take a look at the
993 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
994 *
995 *
996 * @param {expression} md-items An expression in the format of `item in items` to iterate over
997 * matches for your search.
998 * @param {expression=} md-selected-item-change An expression to be run each time a new item is
999 * selected
1000 * @param {expression=} md-search-text-change An expression to be run each time the search text
1001 * updates
1002 * @param {expression=} md-search-text A model to bind the search query text to
1003 * @param {object=} md-selected-item A model to bind the selected item to
1004 * @param {expression=} md-item-text An expression that will convert your object to a single string.
1005 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1006 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
1007 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
1008 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
1009 * which will evaluate to false, when no item is currently selected.
1010 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
1011 * make suggestions
1012 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
1013 * for results
1014 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
1015 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
1016 * Also the autocomplete will immediately focus the input element.
1017 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
1018 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
1019 * in the dropdown upon open.
1020 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
1021 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
1022 * `md-input-container`
1023 * @param {string=} md-input-name The name attribute given to the input element to be used with
1024 * FormController
1025 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
1026 * on focus.
1027 * @param {string=} md-input-id An ID to be added to the input element
1028 * @param {number=} md-input-minlength The minimum length for the input's value for validation
1029 * @param {number=} md-input-maxlength The maximum length for the input's value for validation
1030 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
1031 * the item if the search text is an exact match. <br/><br/>
1032 * Exact match means that there is only one match showing up.
1033 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
1034 * will select on case-insensitive match
1035 * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
1036 * Options: `blur | clear`, `none`
1037 *
1038 * @usage
1039 * ### Basic Example
1040 * <hljs lang="html">
1041 * <md-autocomplete
1042 * md-selected-item="selectedItem"
1043 * md-search-text="searchText"
1044 * md-items="item in getMatches(searchText)"
1045 * md-item-text="item.display">
1046 * <span md-highlight-text="searchText">{{item.display}}</span>
1047 * </md-autocomplete>
1048 * </hljs>
1049 *
1050 * ### Example with "not found" message
1051 * <hljs lang="html">
1052 * <md-autocomplete
1053 * md-selected-item="selectedItem"
1054 * md-search-text="searchText"
1055 * md-items="item in getMatches(searchText)"
1056 * md-item-text="item.display">
1057 * <md-item-template>
1058 * <span md-highlight-text="searchText">{{item.display}}</span>
1059 * </md-item-template>
1060 * <md-not-found>
1061 * No matches found.
1062 * </md-not-found>
1063 * </md-autocomplete>
1064 * </hljs>
1065 *
1066 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
1067 * different parts that make up our component.
1068 *
1069 * ### Example with validation
1070 * <hljs lang="html">
1071 * <form name="autocompleteForm">
1072 * <md-autocomplete
1073 * required
1074 * md-input-name="autocomplete"
1075 * md-selected-item="selectedItem"
1076 * md-search-text="searchText"
1077 * md-items="item in getMatches(searchText)"
1078 * md-item-text="item.display">
1079 * <md-item-template>
1080 * <span md-highlight-text="searchText">{{item.display}}</span>
1081 * </md-item-template>
1082 * <div ng-messages="autocompleteForm.autocomplete.$error">
1083 * <div ng-message="required">This field is required</div>
1084 * </div>
1085 * </md-autocomplete>
1086 * </form>
1087 * </hljs>
1088 *
1089 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
1090 * input validation for the field.
1091 */
1092
1093function MdAutocomplete ($$mdSvgRegistry) {
1094
1095 return {
1096 controller: 'MdAutocompleteCtrl',
1097 controllerAs: '$mdAutocompleteCtrl',
1098 scope: {
1099 inputName: '@mdInputName',
1100 inputMinlength: '@mdInputMinlength',
1101 inputMaxlength: '@mdInputMaxlength',
1102 searchText: '=?mdSearchText',
1103 selectedItem: '=?mdSelectedItem',
1104 itemsExpr: '@mdItems',
1105 itemText: '&mdItemText',
1106 placeholder: '@placeholder',
1107 noCache: '=?mdNoCache',
1108 requireMatch: '=?mdRequireMatch',
1109 selectOnMatch: '=?mdSelectOnMatch',
1110 matchInsensitive: '=?mdMatchCaseInsensitive',
1111 itemChange: '&?mdSelectedItemChange',
1112 textChange: '&?mdSearchTextChange',
1113 minLength: '=?mdMinLength',
1114 delay: '=?mdDelay',
1115 autofocus: '=?mdAutofocus',
1116 floatingLabel: '@?mdFloatingLabel',
1117 autoselect: '=?mdAutoselect',
1118 menuClass: '@?mdMenuClass',
1119 inputId: '@?mdInputId',
1120 escapeOptions: '@?mdEscapeOptions'
1121 },
1122 link: function(scope, element, attrs, controller) {
1123 // Retrieve the state of using a md-not-found template by using our attribute, which will
1124 // be added to the element in the template function.
1125 controller.hasNotFound = !!element.attr('md-has-not-found');
1126 },
1127 template: function (element, attr) {
1128 var noItemsTemplate = getNoItemsTemplate(),
1129 itemTemplate = getItemTemplate(),
1130 leftover = element.html(),
1131 tabindex = attr.tabindex;
1132
1133 // Set our attribute for the link function above which runs later.
1134 // We will set an attribute, because otherwise the stored variables will be trashed when
1135 // removing the element is hidden while retrieving the template. For example when using ngIf.
1136 if (noItemsTemplate) element.attr('md-has-not-found', true);
1137
1138 // Always set our tabindex of the autocomplete directive to -1, because our input
1139 // will hold the actual tabindex.
1140 element.attr('tabindex', '-1');
1141
1142 return '\
1143 <md-autocomplete-wrap\
1144 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }">\
1145 ' + getInputElement() + '\
1146 <md-progress-linear\
1147 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
1148 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
1149 md-mode="indeterminate"></md-progress-linear>\
1150 <md-virtual-repeat-container\
1151 md-auto-shrink\
1152 md-auto-shrink-min="1"\
1153 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
1154 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
1155 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
1156 ng-hide="$mdAutocompleteCtrl.hidden"\
1157 class="md-autocomplete-suggestions-container md-whiteframe-z1"\
1158 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
1159 role="presentation">\
1160 <ul class="md-autocomplete-suggestions"\
1161 ng-class="::menuClass"\
1162 id="ul-{{$mdAutocompleteCtrl.id}}">\
1163 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
1164 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
1165 ng-click="$mdAutocompleteCtrl.select($index)"\
1166 md-extra-name="$mdAutocompleteCtrl.itemName">\
1167 ' + itemTemplate + '\
1168 </li>' + noItemsTemplate + '\
1169 </ul>\
1170 </md-virtual-repeat-container>\
1171 </md-autocomplete-wrap>\
1172 <aria-status\
1173 class="md-visually-hidden"\
1174 role="status"\
1175 aria-live="assertive">\
1176 <p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\
1177 </aria-status>';
1178
1179 function getItemTemplate() {
1180 var templateTag = element.find('md-item-template').detach(),
1181 html = templateTag.length ? templateTag.html() : element.html();
1182 if (!templateTag.length) element.empty();
1183 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
1184 }
1185
1186 function getNoItemsTemplate() {
1187 var templateTag = element.find('md-not-found').detach(),
1188 template = templateTag.length ? templateTag.html() : '';
1189 return template
1190 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
1191 md-autocomplete-parent-scope>' + template + '</li>'
1192 : '';
1193
1194 }
1195
1196 function getInputElement () {
1197 if (attr.mdFloatingLabel) {
1198 return '\
1199 <md-input-container ng-if="floatingLabel">\
1200 <label>{{floatingLabel}}</label>\
1201 <input type="search"\
1202 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1203 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
1204 name="{{inputName}}"\
1205 autocomplete="off"\
1206 ng-required="$mdAutocompleteCtrl.isRequired"\
1207 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1208 ng-minlength="inputMinlength"\
1209 ng-maxlength="inputMaxlength"\
1210 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1211 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1212 ng-model-options="{ allowInvalid: true }"\
1213 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1214 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1215 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1216 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
1217 ' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\
1218 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
1219 aria-label="{{floatingLabel}}"\
1220 aria-autocomplete="list"\
1221 role="combobox"\
1222 aria-haspopup="true"\
1223 aria-activedescendant=""\
1224 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
1225 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
1226 </md-input-container>';
1227 } else {
1228 return '\
1229 <input type="search"\
1230 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1231 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
1232 name="{{inputName}}"\
1233 ng-if="!floatingLabel"\
1234 autocomplete="off"\
1235 ng-required="$mdAutocompleteCtrl.isRequired"\
1236 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1237 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1238 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1239 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1240 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1241 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1242 placeholder="{{placeholder}}"\
1243 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
1244 ' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
1245 aria-label="{{placeholder}}"\
1246 aria-autocomplete="list"\
1247 role="combobox"\
1248 aria-haspopup="true"\
1249 aria-activedescendant=""\
1250 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
1251 <button\
1252 type="button"\
1253 tabindex="-1"\
1254 ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
1255 ng-click="$mdAutocompleteCtrl.clear($event)">\
1256 <md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>\
1257 <span class="md-visually-hidden">Clear</span>\
1258 </button>\
1259 ';
1260 }
1261 }
1262 }
1263 };
1264}
1265
1266
1267MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];angular
1268 .module('material.components.autocomplete')
1269 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
1270
1271function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
1272 return {
1273 restrict: 'AE',
1274 compile: compile,
1275 terminal: true,
1276 transclude: 'element'
1277 };
1278
1279 function compile(tElement, tAttr, transclude) {
1280 return function postLink(scope, element, attr) {
1281 var ctrl = scope.$mdAutocompleteCtrl;
1282 var newScope = ctrl.parent.$new();
1283 var itemName = ctrl.itemName;
1284
1285 // Watch for changes to our scope's variables and copy them to the new scope
1286 watchVariable('$index', '$index');
1287 watchVariable('item', itemName);
1288
1289 // Ensure that $digest calls on our scope trigger $digest on newScope.
1290 connectScopes();
1291
1292 // Link the element against newScope.
1293 transclude(newScope, function(clone) {
1294 element.after(clone);
1295 });
1296
1297 /**
1298 * Creates a watcher for variables that are copied from the parent scope
1299 * @param variable
1300 * @param alias
1301 */
1302 function watchVariable(variable, alias) {
1303 newScope[alias] = scope[variable];
1304
1305 scope.$watch(variable, function(value) {
1306 $mdUtil.nextTick(function() {
1307 newScope[alias] = value;
1308 });
1309 });
1310 }
1311
1312 /**
1313 * Creates watchers on scope and newScope that ensure that for any
1314 * $digest of scope, newScope is also $digested.
1315 */
1316 function connectScopes() {
1317 var scopeDigesting = false;
1318 var newScopeDigesting = false;
1319
1320 scope.$watch(function() {
1321 if (newScopeDigesting || scopeDigesting) {
1322 return;
1323 }
1324
1325 scopeDigesting = true;
1326 scope.$$postDigest(function() {
1327 if (!newScopeDigesting) {
1328 newScope.$digest();
1329 }
1330
1331 scopeDigesting = newScopeDigesting = false;
1332 });
1333 });
1334
1335 newScope.$watch(function() {
1336 newScopeDigesting = true;
1337 });
1338 }
1339 };
1340 }
1341}
1342
1343MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];angular
1344 .module('material.components.autocomplete')
1345 .controller('MdHighlightCtrl', MdHighlightCtrl);
1346
1347function MdHighlightCtrl ($scope, $element, $attrs) {
1348 this.$scope = $scope;
1349 this.$element = $element;
1350 this.$attrs = $attrs;
1351
1352 // Cache the Regex to avoid rebuilding each time.
1353 this.regex = null;
1354}
1355
1356MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
1357
1358 this.flags = this.$attrs.mdHighlightFlags || '';
1359
1360 this.unregisterFn = this.$scope.$watch(function($scope) {
1361 return {
1362 term: unsafeTermFn($scope),
1363 contentText: unsafeContentFn($scope)
1364 };
1365 }.bind(this), this.onRender.bind(this), true);
1366
1367 this.$element.on('$destroy', this.unregisterFn);
1368};
1369
1370/**
1371 * Triggered once a new change has been recognized and the highlighted
1372 * text needs to be updated.
1373 */
1374MdHighlightCtrl.prototype.onRender = function(state, prevState) {
1375
1376 var contentText = state.contentText;
1377
1378 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
1379 if (this.regex === null || state.term !== prevState.term) {
1380 this.regex = this.createRegex(state.term, this.flags);
1381 }
1382
1383 /* If a term is available apply the regex to the content */
1384 if (state.term) {
1385 this.applyRegex(contentText);
1386 } else {
1387 this.$element.text(contentText);
1388 }
1389
1390};
1391
1392/**
1393 * Decomposes the specified text into different tokens (whether match or not).
1394 * Breaking down the string guarantees proper XSS protection due to the native browser
1395 * escaping of unsafe text.
1396 */
1397MdHighlightCtrl.prototype.applyRegex = function(text) {
1398 var tokens = this.resolveTokens(text);
1399
1400 this.$element.empty();
1401
1402 tokens.forEach(function (token) {
1403
1404 if (token.isMatch) {
1405 var tokenEl = angular.element('<span class="highlight">').text(token.text);
1406
1407 this.$element.append(tokenEl);
1408 } else {
1409 this.$element.append(document.createTextNode(token));
1410 }
1411
1412 }.bind(this));
1413
1414};
1415
1416 /**
1417 * Decomposes the specified text into different tokens by running the regex against the text.
1418 */
1419MdHighlightCtrl.prototype.resolveTokens = function(string) {
1420 var tokens = [];
1421 var lastIndex = 0;
1422
1423 // Use replace here, because it supports global and single regular expressions at same time.
1424 string.replace(this.regex, function(match, index) {
1425 appendToken(lastIndex, index);
1426
1427 tokens.push({
1428 text: match,
1429 isMatch: true
1430 });
1431
1432 lastIndex = index + match.length;
1433 });
1434
1435 // Append the missing text as a token.
1436 appendToken(lastIndex);
1437
1438 return tokens;
1439
1440 function appendToken(from, to) {
1441 var targetText = string.slice(from, to);
1442 targetText && tokens.push(targetText);
1443 }
1444};
1445
1446/** Creates a regex for the specified text with the given flags. */
1447MdHighlightCtrl.prototype.createRegex = function(term, flags) {
1448 var startFlag = '', endFlag = '';
1449 var regexTerm = this.sanitizeRegex(term);
1450
1451 if (flags.indexOf('^') >= 0) startFlag = '^';
1452 if (flags.indexOf('$') >= 0) endFlag = '$';
1453
1454 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$\^]/g, ''));
1455};
1456
1457/** Sanitizes a regex by removing all common RegExp identifiers */
1458MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
1459 return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
1460};
1461
1462
1463MdHighlight.$inject = ["$interpolate", "$parse"];angular
1464 .module('material.components.autocomplete')
1465 .directive('mdHighlightText', MdHighlight);
1466
1467/**
1468 * @ngdoc directive
1469 * @name mdHighlightText
1470 * @module material.components.autocomplete
1471 *
1472 * @description
1473 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
1474 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
1475 * be styled through CSS. Please note that child elements may not be used with this directive.
1476 *
1477 * @param {string} md-highlight-text A model to be searched for
1478 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
1479 * #### **Supported flags**:
1480 * - `g`: Find all matches within the provided text
1481 * - `i`: Ignore case when searching for matches
1482 * - `$`: Only match if the text ends with the search term
1483 * - `^`: Only match if the text begins with the search term
1484 *
1485 * @usage
1486 * <hljs lang="html">
1487 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
1488 * <ul>
1489 * <li ng-repeat="result in results" md-highlight-text="searchTerm">
1490 * {{result.text}}
1491 * </li>
1492 * </ul>
1493 * </hljs>
1494 */
1495
1496function MdHighlight ($interpolate, $parse) {
1497 return {
1498 terminal: true,
1499 controller: 'MdHighlightCtrl',
1500 compile: function mdHighlightCompile(tElement, tAttr) {
1501 var termExpr = $parse(tAttr.mdHighlightText);
1502 var unsafeContentExpr = $interpolate(tElement.html());
1503
1504 return function mdHighlightLink(scope, element, attr, ctrl) {
1505 ctrl.init(termExpr, unsafeContentExpr);
1506 };
1507 }
1508 };
1509}
1510
1511ngmaterial.components.autocomplete = angular.module("material.components.autocomplete");