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