source: trip-planner-front/node_modules/angular-material/modules/closure/autocomplete/autocomplete.js@ 6a3a178

Last change on this file since 6a3a178 was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 70.1 KB
Line 
1/*!
2 * AngularJS Material Design
3 * https://github.com/angular/material
4 * @license MIT
5 * v1.2.3
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", "$mdLiveAnnouncer"];angular
26 .module('material.components.autocomplete')
27 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
28
29var ITEM_HEIGHT = 48,
30 MAX_ITEMS = 5,
31 MENU_PADDING = 8,
32 INPUT_PADDING = 2, // Padding provided by `md-input-container`
33 MODE_STANDARD = 'standard',
34 MODE_VIRTUAL = 'virtual';
35
36function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
37 $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
38
39 // Internal Variables.
40 var ctrl = this,
41 itemParts = $scope.itemsExpr.split(/ in /i),
42 itemExpr = itemParts[ 1 ],
43 elements = null,
44 cache = {},
45 noBlur = false,
46 selectedItemWatchers = [],
47 hasFocus = false,
48 fetchesInProgress = 0,
49 enableWrapScroll = null,
50 inputModelCtrl = null,
51 debouncedOnResize = $mdUtil.debounce(onWindowResize),
52 mode = MODE_VIRTUAL; // default
53
54 /**
55 * The root document element. This is used for attaching a top-level click handler to
56 * close the options panel when a click outside said panel occurs. We use `documentElement`
57 * instead of body because, when scrolling is disabled, some browsers consider the body element
58 * to be completely off the screen and propagate events directly to the html element.
59 * @type {!Object} angular.JQLite
60 */
61 ctrl.documentElement = angular.element(document.documentElement);
62
63 // Public Exported Variables with handlers
64 defineProperty('hidden', handleHiddenChange, true);
65
66 // Public Exported Variables
67 ctrl.scope = $scope;
68 ctrl.parent = $scope.$parent;
69 ctrl.itemName = itemParts[0];
70 ctrl.matches = [];
71 ctrl.loading = false;
72 ctrl.hidden = true;
73 ctrl.index = -1;
74 ctrl.activeOption = null;
75 ctrl.id = $mdUtil.nextUid();
76 ctrl.isDisabled = null;
77 ctrl.isRequired = null;
78 ctrl.isReadonly = null;
79 ctrl.hasNotFound = false;
80 ctrl.selectedMessage = $scope.selectedMessage || 'selected';
81 ctrl.noMatchMessage = $scope.noMatchMessage || 'There are no matches available.';
82 ctrl.singleMatchMessage = $scope.singleMatchMessage || 'There is 1 match available.';
83 ctrl.multipleMatchStartMessage = $scope.multipleMatchStartMessage || 'There are ';
84 ctrl.multipleMatchEndMessage = $scope.multipleMatchEndMessage || ' matches available.';
85 ctrl.defaultEscapeOptions = 'clear';
86
87 // Public Exported Methods
88 ctrl.keydown = keydown;
89 ctrl.blur = blur;
90 ctrl.focus = focus;
91 ctrl.clear = clearValue;
92 ctrl.select = select;
93 ctrl.listEnter = onListEnter;
94 ctrl.listLeave = onListLeave;
95 ctrl.focusInput = focusInputElement;
96 ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
97 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
98 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
99 ctrl.notFoundVisible = notFoundVisible;
100 ctrl.loadingIsVisible = loadingIsVisible;
101 ctrl.positionDropdown = positionDropdown;
102
103 /**
104 * Report types to be used for the $mdLiveAnnouncer
105 * @enum {number} Unique flag id.
106 */
107 var ReportType = {
108 Count: 1,
109 Selected: 2
110 };
111
112 return init();
113
114 // initialization methods
115
116 /**
117 * Initialize the controller, setup watchers, gather elements
118 */
119 function init () {
120
121 $mdUtil.initOptionalProperties($scope, $attrs, {
122 searchText: '',
123 selectedItem: null,
124 clearButton: false,
125 disableVirtualRepeat: false,
126 });
127
128 $mdTheming($element);
129 configureWatchers();
130 $mdUtil.nextTick(function () {
131
132 gatherElements();
133 moveDropdown();
134
135 // Touch devices often do not send a click event on tap. We still want to focus the input
136 // and open the options pop-up in these cases.
137 $element.on('touchstart', focusInputElement);
138
139 // Forward all focus events to the input element when autofocus is enabled
140 if ($scope.autofocus) {
141 $element.on('focus', focusInputElement);
142 }
143 if ($scope.inputAriaDescribedBy) {
144 elements.input.setAttribute('aria-describedby', $scope.inputAriaDescribedBy);
145 }
146 if (!$scope.floatingLabel) {
147 if ($scope.inputAriaLabel) {
148 elements.input.setAttribute('aria-label', $scope.inputAriaLabel);
149 } else if ($scope.inputAriaLabelledBy) {
150 elements.input.setAttribute('aria-labelledby', $scope.inputAriaLabelledBy);
151 } else if ($scope.placeholder) {
152 // If no aria-label or aria-labelledby references are defined, then just label using the
153 // placeholder.
154 elements.input.setAttribute('aria-label', $scope.placeholder);
155 }
156 }
157 });
158 }
159
160 function updateModelValidators() {
161 if (!$scope.requireMatch || !inputModelCtrl) return;
162
163 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
164 }
165
166 /**
167 * Calculates the dropdown's position and applies the new styles to the menu element
168 * @returns {*}
169 */
170 function positionDropdown () {
171 if (!elements) {
172 return $mdUtil.nextTick(positionDropdown, false, $scope);
173 }
174
175 var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
176 var hrect = elements.wrap.getBoundingClientRect(),
177 vrect = elements.snap.getBoundingClientRect(),
178 root = elements.root.getBoundingClientRect(),
179 top = vrect.bottom - root.top,
180 bot = root.bottom - vrect.top,
181 left = hrect.left - root.left,
182 width = hrect.width,
183 offset = getVerticalOffset(),
184 position = $scope.dropdownPosition,
185 styles, enoughBottomSpace, enoughTopSpace;
186 var bottomSpace = root.bottom - vrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
187 var topSpace = vrect.top - MENU_PADDING;
188
189 // Automatically determine dropdown placement based on available space in viewport.
190 if (!position) {
191 enoughTopSpace = topSpace > dropdownHeight;
192 enoughBottomSpace = bottomSpace > dropdownHeight;
193 if (enoughBottomSpace) {
194 position = 'bottom';
195 } else if (enoughTopSpace) {
196 position = 'top';
197 } else {
198 position = topSpace > bottomSpace ? 'top' : 'bottom';
199 }
200 }
201 // Adjust the width to account for the padding provided by `md-input-container`
202 if ($attrs.mdFloatingLabel) {
203 left += INPUT_PADDING;
204 width -= INPUT_PADDING * 2;
205 }
206 styles = {
207 left: left + 'px',
208 minWidth: width + 'px',
209 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
210 };
211
212 if (position === 'top') {
213 styles.top = 'auto';
214 styles.bottom = bot + 'px';
215 styles.maxHeight = Math.min(dropdownHeight, topSpace) + 'px';
216 } else {
217 bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
218
219 styles.top = (top - offset) + 'px';
220 styles.bottom = 'auto';
221 styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
222 }
223
224 elements.$.scrollContainer.css(styles);
225 $mdUtil.nextTick(correctHorizontalAlignment, false, $scope);
226
227 /**
228 * Calculates the vertical offset for floating label examples to account for ngMessages
229 * @returns {number}
230 */
231 function getVerticalOffset () {
232 var offset = 0;
233 var inputContainer = $element.find('md-input-container');
234 if (inputContainer.length) {
235 var input = inputContainer.find('input');
236 offset = inputContainer.prop('offsetHeight');
237 offset -= input.prop('offsetTop');
238 offset -= input.prop('offsetHeight');
239 // add in the height left up top for the floating label text
240 offset += inputContainer.prop('offsetTop');
241 }
242 return offset;
243 }
244
245 /**
246 * Makes sure that the menu doesn't go off of the screen on either side.
247 */
248 function correctHorizontalAlignment () {
249 var dropdown = elements.scrollContainer.getBoundingClientRect(),
250 styles = {};
251 if (dropdown.right > root.right) {
252 styles.left = (hrect.right - dropdown.width) + 'px';
253 }
254 elements.$.scrollContainer.css(styles);
255 }
256 }
257
258 /**
259 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
260 */
261 function moveDropdown () {
262 if (!elements.$.root.length) return;
263 $mdTheming(elements.$.scrollContainer);
264 elements.$.scrollContainer.detach();
265 elements.$.root.append(elements.$.scrollContainer);
266 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
267 }
268
269 /**
270 * Sends focus to the input element.
271 */
272 function focusInputElement () {
273 elements.input.focus();
274 }
275
276 /**
277 * Update the activeOption based on the selected item in the listbox.
278 * The activeOption is used in the template to set the aria-activedescendant attribute, which
279 * enables screen readers to properly handle visual focus within the listbox and announce the
280 * item's place in the list. I.e. "List item 3 of 50". Anytime that `ctrl.index` changes, this
281 * function needs to be called to update the activeOption.
282 */
283 function updateActiveOption() {
284 var selectedOption = elements.scroller.querySelector('.selected');
285 if (selectedOption) {
286 ctrl.activeOption = selectedOption.id;
287 } else {
288 ctrl.activeOption = null;
289 }
290 }
291
292 /**
293 * Sets up any watchers used by autocomplete
294 */
295 function configureWatchers () {
296 var wait = parseInt($scope.delay, 10) || 0;
297
298 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
299 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
300 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
301
302 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
303 $scope.$watch('selectedItem', selectedItemChange);
304
305 angular.element($window).on('resize', debouncedOnResize);
306
307 $scope.$on('$destroy', cleanup);
308 }
309
310 /**
311 * Removes any events or leftover elements created by this controller
312 */
313 function cleanup () {
314 if (!ctrl.hidden) {
315 $mdUtil.enableScrolling();
316 }
317
318 angular.element($window).off('resize', debouncedOnResize);
319
320 if (elements){
321 var items = ['ul', 'scroller', 'scrollContainer', 'input'];
322 angular.forEach(items, function(key){
323 elements.$[key].remove();
324 });
325 }
326 }
327
328 /**
329 * Event handler to be called whenever the window resizes.
330 */
331 function onWindowResize() {
332 if (!ctrl.hidden) {
333 positionDropdown();
334 }
335 }
336
337 /**
338 * Gathers all of the elements needed for this controller
339 */
340 function gatherElements () {
341
342 var snapWrap = gatherSnapWrap();
343
344 elements = {
345 main: $element[0],
346 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container, .md-standard-list-container'),
347 scroller: $element[0].querySelector('.md-virtual-repeat-scroller, .md-standard-list-scroller'),
348 ul: $element.find('ul')[0],
349 input: $element.find('input')[0],
350 wrap: snapWrap.wrap,
351 snap: snapWrap.snap,
352 root: document.body,
353 };
354
355 elements.li = elements.ul.getElementsByTagName('li');
356 elements.$ = getAngularElements(elements);
357 mode = elements.scrollContainer.classList.contains('md-standard-list-container') ? MODE_STANDARD : MODE_VIRTUAL;
358 inputModelCtrl = elements.$.input.controller('ngModel');
359 }
360
361 /**
362 * Gathers the snap and wrap elements
363 *
364 */
365 function gatherSnapWrap() {
366 var element;
367 var value;
368 for (element = $element; element.length; element = element.parent()) {
369 value = element.attr('md-autocomplete-snap');
370 if (angular.isDefined(value)) break;
371 }
372
373 if (element.length) {
374 return {
375 snap: element[0],
376 wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
377 };
378 }
379
380 var wrap = $element.find('md-autocomplete-wrap')[0];
381 return {
382 snap: wrap,
383 wrap: wrap
384 };
385 }
386
387 /**
388 * Gathers angular-wrapped versions of each element
389 * @param elements
390 * @returns {{}}
391 */
392 function getAngularElements (elements) {
393 var obj = {};
394 for (var key in elements) {
395 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
396 }
397 return obj;
398 }
399
400 // event/change handlers
401
402 /**
403 * @param {Event} $event
404 */
405 function preventDefault($event) {
406 $event.preventDefault();
407 }
408
409 /**
410 * @param {Event} $event
411 */
412 function stopPropagation($event) {
413 $event.stopPropagation();
414 }
415
416 /**
417 * Handles changes to the `hidden` property.
418 * @param {boolean} hidden true to hide the options pop-up, false to show it.
419 * @param {boolean} oldHidden the previous value of hidden
420 */
421 function handleHiddenChange (hidden, oldHidden) {
422 var scrollContainerElement;
423
424 if (elements) {
425 scrollContainerElement = angular.element(elements.scrollContainer);
426 }
427 if (!hidden && oldHidden) {
428 positionDropdown();
429
430 // Report in polite mode, because the screen reader should finish the default description of
431 // the input element.
432 reportMessages(true, ReportType.Count | ReportType.Selected);
433
434 if (elements) {
435 $mdUtil.disableScrollAround(elements.scrollContainer);
436 enableWrapScroll = disableElementScrollEvents(elements.wrap);
437 if ($mdUtil.isIos) {
438 ctrl.documentElement.on('touchend', handleTouchOutsidePanel);
439 if (scrollContainerElement) {
440 scrollContainerElement.on('touchstart touchmove touchend', stopPropagation);
441 }
442 }
443 ctrl.index = getDefaultIndex();
444 $mdUtil.nextTick(function() {
445 updateActiveOption();
446 updateScroll();
447 });
448 }
449 } else if (hidden && !oldHidden) {
450 if ($mdUtil.isIos) {
451 ctrl.documentElement.off('touchend', handleTouchOutsidePanel);
452 if (scrollContainerElement) {
453 scrollContainerElement.off('touchstart touchmove touchend', stopPropagation);
454 }
455 }
456 $mdUtil.enableScrolling();
457
458 if (enableWrapScroll) {
459 enableWrapScroll();
460 enableWrapScroll = null;
461 }
462 }
463 }
464
465 /**
466 * Handling touch events that bubble up to the document is required for closing the dropdown
467 * panel on touch outside of the options pop-up panel on iOS.
468 * @param {Event} $event
469 */
470 function handleTouchOutsidePanel($event) {
471 ctrl.hidden = true;
472 // iOS does not blur the pop-up for touches on the scroll mask, so we have to do it.
473 doBlur(true);
474 }
475
476 /**
477 * Disables scrolling for a specific element.
478 * @param {!string|!DOMElement} element to disable scrolling
479 * @return {Function} function to call to re-enable scrolling for the element
480 */
481 function disableElementScrollEvents(element) {
482 var elementToDisable = angular.element(element);
483 elementToDisable.on('wheel touchmove', preventDefault);
484
485 return function() {
486 elementToDisable.off('wheel touchmove', preventDefault);
487 };
488 }
489
490 /**
491 * When the user mouses over the dropdown menu, ignore blur events.
492 */
493 function onListEnter () {
494 noBlur = true;
495 }
496
497 /**
498 * When the user's mouse leaves the menu, blur events may hide the menu again.
499 */
500 function onListLeave () {
501 if (!hasFocus && !ctrl.hidden) elements.input.focus();
502 noBlur = false;
503 ctrl.hidden = shouldHide();
504 }
505
506 /**
507 * Handles changes to the selected item.
508 * @param selectedItem
509 * @param previousSelectedItem
510 */
511 function selectedItemChange (selectedItem, previousSelectedItem) {
512
513 updateModelValidators();
514
515 if (selectedItem) {
516 getDisplayValue(selectedItem).then(function (val) {
517 $scope.searchText = val;
518 handleSelectedItemChange(selectedItem, previousSelectedItem);
519 });
520 } else if (previousSelectedItem && $scope.searchText) {
521 getDisplayValue(previousSelectedItem).then(function(displayValue) {
522 // Clear the searchText, when the selectedItem is set to null.
523 // Do not clear the searchText, when the searchText isn't matching with the previous
524 // selected item.
525 if (angular.isString($scope.searchText)
526 && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
527 $scope.searchText = '';
528 }
529 });
530 }
531
532 if (selectedItem !== previousSelectedItem) {
533 announceItemChange();
534 }
535 }
536
537 /**
538 * Use the user-defined expression to announce changes each time a new item is selected
539 */
540 function announceItemChange () {
541 angular.isFunction($scope.itemChange) &&
542 $scope.itemChange(getItemAsNameVal($scope.selectedItem));
543 }
544
545 /**
546 * Use the user-defined expression to announce changes each time the search text is changed
547 */
548 function announceTextChange () {
549 angular.isFunction($scope.textChange) && $scope.textChange();
550 }
551
552 /**
553 * Calls any external watchers listening for the selected item. Used in conjunction with
554 * `registerSelectedItemWatcher`.
555 * @param selectedItem
556 * @param previousSelectedItem
557 */
558 function handleSelectedItemChange (selectedItem, previousSelectedItem) {
559 selectedItemWatchers.forEach(function (watcher) {
560 watcher(selectedItem, previousSelectedItem);
561 });
562 }
563
564 /**
565 * Register a function to be called when the selected item changes.
566 * @param cb
567 */
568 function registerSelectedItemWatcher (cb) {
569 if (selectedItemWatchers.indexOf(cb) === -1) {
570 selectedItemWatchers.push(cb);
571 }
572 }
573
574 /**
575 * Unregister a function previously registered for selected item changes.
576 * @param cb
577 */
578 function unregisterSelectedItemWatcher (cb) {
579 var i = selectedItemWatchers.indexOf(cb);
580 if (i !== -1) {
581 selectedItemWatchers.splice(i, 1);
582 }
583 }
584
585 /**
586 * Handles changes to the searchText property.
587 * @param {string} searchText
588 * @param {string} previousSearchText
589 */
590 function handleSearchText (searchText, previousSearchText) {
591 ctrl.index = getDefaultIndex();
592
593 // do nothing on init
594 if (searchText === previousSearchText) return;
595
596 updateModelValidators();
597
598 getDisplayValue($scope.selectedItem).then(function (val) {
599 // clear selected item if search text no longer matches it
600 if (searchText !== val) {
601 $scope.selectedItem = null;
602
603 // trigger change event if available
604 if (searchText !== previousSearchText) {
605 announceTextChange();
606 }
607
608 // cancel results if search text is not long enough
609 if (!isMinLengthMet()) {
610 ctrl.matches = [];
611
612 setLoading(false);
613 reportMessages(true, ReportType.Count);
614
615 } else {
616 handleQuery();
617 }
618 }
619 });
620
621 }
622
623 /**
624 * Handles input blur event, determines if the dropdown should hide.
625 * @param {Event=} $event
626 */
627 function blur($event) {
628 hasFocus = false;
629
630 if (!noBlur) {
631 ctrl.hidden = shouldHide();
632 evalAttr('ngBlur', { $event: $event });
633 } else if (angular.isObject($event)) {
634 $event.stopImmediatePropagation();
635 }
636 }
637
638 /**
639 * Force blur on input element
640 * @param {boolean} forceBlur
641 */
642 function doBlur(forceBlur) {
643 if (forceBlur) {
644 noBlur = false;
645 hasFocus = false;
646 }
647 elements.input.blur();
648 }
649
650 /**
651 * Handles input focus event, determines if the dropdown should show.
652 */
653 function focus($event) {
654 hasFocus = true;
655
656 if (isSearchable() && isMinLengthMet()) {
657 handleQuery();
658 }
659
660 ctrl.hidden = shouldHide();
661
662 evalAttr('ngFocus', { $event: $event });
663 }
664
665 /**
666 * Handles keyboard input.
667 * @param event
668 */
669 function keydown (event) {
670 switch (event.keyCode) {
671 case $mdConstant.KEY_CODE.DOWN_ARROW:
672 if (ctrl.loading || hasSelection()) return;
673 event.stopPropagation();
674 event.preventDefault();
675 ctrl.index = ctrl.index + 1 > ctrl.matches.length - 1 ? 0 : Math.min(ctrl.index + 1, ctrl.matches.length - 1);
676 $mdUtil.nextTick(updateActiveOption);
677 updateScroll();
678 break;
679 case $mdConstant.KEY_CODE.UP_ARROW:
680 if (ctrl.loading || hasSelection()) return;
681 event.stopPropagation();
682 event.preventDefault();
683 ctrl.index = ctrl.index - 1 < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
684 $mdUtil.nextTick(updateActiveOption);
685 updateScroll();
686 break;
687 case $mdConstant.KEY_CODE.TAB:
688 // If we hit tab, assume that we've left the list so it will close
689 onListLeave();
690
691 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
692 select(ctrl.index);
693 break;
694 case $mdConstant.KEY_CODE.ENTER:
695 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
696 if (hasSelection()) return;
697 event.stopImmediatePropagation();
698 event.preventDefault();
699 select(ctrl.index);
700 break;
701 case $mdConstant.KEY_CODE.ESCAPE:
702 event.preventDefault(); // Prevent browser from always clearing input
703 if (!shouldProcessEscape()) return;
704 event.stopPropagation();
705
706 clearSelectedItem();
707 if ($scope.searchText && hasEscapeOption('clear')) {
708 clearSearchText();
709 }
710
711 // Manually hide (needed for mdNotFound support)
712 ctrl.hidden = true;
713
714 if (hasEscapeOption('blur')) {
715 // Force the component to blur if they hit escape
716 doBlur(true);
717 }
718
719 break;
720 default:
721 }
722 }
723
724 // getters
725
726 /**
727 * Returns the minimum length needed to display the dropdown.
728 * @returns {*}
729 */
730 function getMinLength () {
731 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
732 }
733
734 /**
735 * Returns the display value for an item.
736 * @param {*} item
737 * @returns {*}
738 */
739 function getDisplayValue (item) {
740 return $q.when(getItemText(item) || item).then(function(itemText) {
741 if (itemText && !angular.isString(itemText)) {
742 $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
743 'Please check the `md-item-text` attribute.');
744 }
745
746 return itemText;
747 });
748
749 /**
750 * Getter function to invoke user-defined expression (in the directive)
751 * to convert your object to a single string.
752 * @param {*} item
753 * @returns {string|null}
754 */
755 function getItemText (item) {
756 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
757 }
758 }
759
760 /**
761 * Returns the locals object for compiling item templates.
762 * @param {*} item
763 * @returns {Object|undefined}
764 */
765 function getItemAsNameVal (item) {
766 if (!item) {
767 return undefined;
768 }
769
770 var locals = {};
771 if (ctrl.itemName) {
772 locals[ ctrl.itemName ] = item;
773 }
774
775 return locals;
776 }
777
778 /**
779 * Returns the default index based on whether or not autoselect is enabled.
780 * @returns {number} 0 if autoselect is enabled, -1 if not.
781 */
782 function getDefaultIndex () {
783 return $scope.autoselect ? 0 : -1;
784 }
785
786 /**
787 * Sets the loading parameter and updates the hidden state.
788 * @param value {boolean} Whether or not the component is currently loading.
789 */
790 function setLoading(value) {
791 if (ctrl.loading !== value) {
792 ctrl.loading = value;
793 }
794
795 // Always refresh the hidden variable as something else might have changed
796 ctrl.hidden = shouldHide();
797 }
798
799 /**
800 * Determines if the menu should be hidden.
801 * @returns {boolean} true if the menu should be hidden
802 */
803 function shouldHide () {
804 return !shouldShow();
805 }
806
807 /**
808 * Determines whether the autocomplete is able to query within the current state.
809 * @returns {boolean} true if the query can be run
810 */
811 function isSearchable() {
812 if (ctrl.loading && !hasMatches()) {
813 // No query when query is in progress.
814 return false;
815 } else if (hasSelection()) {
816 // No query if there is already a selection
817 return false;
818 }
819 else if (!hasFocus) {
820 // No query if the input does not have focus
821 return false;
822 }
823 return true;
824 }
825
826 /**
827 * @returns {boolean} if the escape keydown should be processed, return true.
828 * Otherwise return false.
829 */
830 function shouldProcessEscape() {
831 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
832 }
833
834 /**
835 * @param {string} option check if this option is set
836 * @returns {boolean} if the specified escape option is set, return true. Return false otherwise.
837 */
838 function hasEscapeOption(option) {
839 if (!angular.isString($scope.escapeOptions)) {
840 return ctrl.defaultEscapeOptions.indexOf(option) !== -1;
841 } else {
842 return $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
843 }
844 }
845
846 /**
847 * Determines if the menu should be shown.
848 * @returns {boolean} true if the menu should be shown
849 */
850 function shouldShow() {
851 if (ctrl.isReadonly) {
852 // Don't show if read only is set
853 return false;
854 } else if (!isSearchable()) {
855 // Don't show if a query is in progress, there is already a selection,
856 // or the input is not focused.
857 return false;
858 }
859 return (isMinLengthMet() && hasMatches()) || notFoundVisible();
860 }
861
862 /**
863 * @returns {boolean} true if the search text has matches.
864 */
865 function hasMatches() {
866 return ctrl.matches.length ? true : false;
867 }
868
869 /**
870 * @returns {boolean} true if the autocomplete has a valid selection.
871 */
872 function hasSelection() {
873 return ctrl.scope.selectedItem ? true : false;
874 }
875
876 /**
877 * @returns {boolean} true if the loading indicator is, or should be, visible.
878 */
879 function loadingIsVisible() {
880 return ctrl.loading && !hasSelection();
881 }
882
883 /**
884 * @returns {*} the display value of the current item.
885 */
886 function getCurrentDisplayValue () {
887 return getDisplayValue(ctrl.matches[ ctrl.index ]);
888 }
889
890 /**
891 * Determines if the minimum length is met by the search text.
892 * @returns {*} true if the minimum length is met by the search text
893 */
894 function isMinLengthMet () {
895 return ($scope.searchText || '').length >= getMinLength();
896 }
897
898 // actions
899
900 /**
901 * Defines a public property with a handler and a default value.
902 * @param {string} key
903 * @param {Function} handler function
904 * @param {*} defaultValue default value
905 */
906 function defineProperty (key, handler, defaultValue) {
907 Object.defineProperty(ctrl, key, {
908 get: function () { return defaultValue; },
909 set: function (newValue) {
910 var oldValue = defaultValue;
911 defaultValue = newValue;
912 handler(newValue, oldValue);
913 }
914 });
915 }
916
917 /**
918 * Selects the item at the given index.
919 * @param {number} index to select
920 */
921 function select (index) {
922 // force form to update state for validation
923 $mdUtil.nextTick(function () {
924 getDisplayValue(ctrl.matches[ index ]).then(function (val) {
925 var ngModel = elements.$.input.controller('ngModel');
926 $mdLiveAnnouncer.announce(val + ' ' + ctrl.selectedMessage, 'assertive');
927 ngModel.$setViewValue(val);
928 ngModel.$render();
929 }).finally(function () {
930 $scope.selectedItem = ctrl.matches[ index ];
931 setLoading(false);
932 });
933 }, false);
934 }
935
936 /**
937 * Clears the searchText value and selected item.
938 * @param {Event} $event
939 */
940 function clearValue ($event) {
941 if ($event) {
942 $event.stopPropagation();
943 }
944 clearSelectedItem();
945 clearSearchText();
946 }
947
948 /**
949 * Clears the selected item
950 */
951 function clearSelectedItem () {
952 // Reset our variables
953 ctrl.index = -1;
954 $mdUtil.nextTick(updateActiveOption);
955 ctrl.matches = [];
956 }
957
958 /**
959 * Clears the searchText value
960 */
961 function clearSearchText () {
962 // Set the loading to true so we don't see flashes of content.
963 // The flashing will only occur when an async request is running.
964 // So the loading process will stop when the results had been retrieved.
965 setLoading(true);
966
967 $scope.searchText = '';
968
969 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
970 // But some browsers are not detecting it properly, which means that we have to trigger the event.
971 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
972 // The `change` event is a good alternative and is supported by all supported browsers.
973 var eventObj = document.createEvent('CustomEvent');
974 eventObj.initCustomEvent('change', true, true, { value: '' });
975 elements.input.dispatchEvent(eventObj);
976
977 // For some reason, firing the above event resets the value of $scope.searchText if
978 // $scope.searchText has a space character at the end, so we blank it one more time and then
979 // focus.
980 elements.input.blur();
981 $scope.searchText = '';
982 elements.input.focus();
983 }
984
985 /**
986 * Fetches the results for the provided search text.
987 * @param searchText
988 */
989 function fetchResults (searchText) {
990 var items = $scope.$parent.$eval(itemExpr),
991 term = searchText.toLowerCase(),
992 isList = angular.isArray(items),
993 isPromise = !!items.then; // Every promise should contain a `then` property
994
995 if (isList) onResultsRetrieved(items);
996 else if (isPromise) handleAsyncResults(items);
997
998 function handleAsyncResults(items) {
999 if (!items) return;
1000
1001 items = $q.when(items);
1002 fetchesInProgress++;
1003 setLoading(true);
1004
1005 $mdUtil.nextTick(function () {
1006 items
1007 .then(onResultsRetrieved)
1008 .finally(function(){
1009 if (--fetchesInProgress === 0) {
1010 setLoading(false);
1011 }
1012 });
1013 },true, $scope);
1014 }
1015
1016 function onResultsRetrieved(matches) {
1017 cache[term] = matches;
1018
1019 // Just cache the results if the request is now outdated.
1020 // The request becomes outdated, when the new searchText has changed during the result fetching.
1021 if ((searchText || '') !== ($scope.searchText || '')) {
1022 return;
1023 }
1024
1025 handleResults(matches);
1026 }
1027 }
1028
1029
1030 /**
1031 * Reports given message types to supported screen readers.
1032 * @param {boolean} isPolite Whether the announcement should be polite.
1033 * @param {!number} types Message flags to be reported to the screen reader.
1034 */
1035 function reportMessages(isPolite, types) {
1036 var politeness = isPolite ? 'polite' : 'assertive';
1037 var messages = [];
1038
1039 if (types & ReportType.Selected && ctrl.index !== -1) {
1040 messages.push(getCurrentDisplayValue());
1041 }
1042
1043 if (types & ReportType.Count) {
1044 messages.push($q.resolve(getCountMessage()));
1045 }
1046
1047 $q.all(messages).then(function(data) {
1048 $mdLiveAnnouncer.announce(data.join(' '), politeness);
1049 });
1050 }
1051
1052 /**
1053 * @returns {string} the ARIA message for how many results match the current query.
1054 */
1055 function getCountMessage () {
1056 switch (ctrl.matches.length) {
1057 case 0:
1058 return ctrl.noMatchMessage;
1059 case 1:
1060 return ctrl.singleMatchMessage;
1061 default:
1062 return ctrl.multipleMatchStartMessage + ctrl.matches.length + ctrl.multipleMatchEndMessage;
1063 }
1064 }
1065
1066 /**
1067 * Makes sure that the focused element is within view.
1068 */
1069 function updateScroll () {
1070 if (!elements.li[0]) return;
1071 if (mode === MODE_STANDARD) {
1072 updateStandardScroll();
1073 } else {
1074 updateVirtualScroll();
1075 }
1076 }
1077
1078 function updateVirtualScroll() {
1079 // elements in virtual scroll have consistent heights
1080 var optionHeight = elements.li[0].offsetHeight,
1081 top = optionHeight * Math.max(0, ctrl.index),
1082 bottom = top + optionHeight,
1083 containerHeight = elements.scroller.clientHeight,
1084 scrollTop = elements.scroller.scrollTop;
1085
1086 if (top < scrollTop) {
1087 scrollTo(top);
1088 } else if (bottom > scrollTop + containerHeight) {
1089 scrollTo(bottom - containerHeight);
1090 }
1091 }
1092
1093 function updateStandardScroll() {
1094 // elements in standard scroll have variable heights
1095 var selected = elements.li[Math.max(0, ctrl.index)];
1096 var containerHeight = elements.scrollContainer.offsetHeight,
1097 top = selected && selected.offsetTop || 0,
1098 bottom = top + selected.clientHeight,
1099 scrollTop = elements.scrollContainer.scrollTop;
1100
1101 if (top < scrollTop) {
1102 scrollTo(top);
1103 } else if (bottom > scrollTop + containerHeight) {
1104 scrollTo(bottom - containerHeight);
1105 }
1106 }
1107
1108 function isPromiseFetching() {
1109 return fetchesInProgress !== 0;
1110 }
1111
1112 function scrollTo (offset) {
1113 if (mode === MODE_STANDARD) {
1114 elements.scrollContainer.scrollTop = offset;
1115 } else {
1116 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
1117 }
1118 }
1119
1120 function notFoundVisible () {
1121 var textLength = (ctrl.scope.searchText || '').length;
1122
1123 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
1124 }
1125
1126 /**
1127 * Starts the query to gather the results for the current searchText. Attempts to return cached
1128 * results first, then forwards the process to `fetchResults` if necessary.
1129 */
1130 function handleQuery () {
1131 var searchText = $scope.searchText || '';
1132 var term = searchText.toLowerCase();
1133
1134 // If caching is enabled and the current searchText is stored in the cache
1135 if (!$scope.noCache && cache[term]) {
1136 // The results should be handled as same as a normal un-cached request does.
1137 handleResults(cache[term]);
1138 } else {
1139 fetchResults(searchText);
1140 }
1141
1142 ctrl.hidden = shouldHide();
1143 }
1144
1145 /**
1146 * Handles the retrieved results by showing them in the autocompletes dropdown.
1147 * @param results Retrieved results
1148 */
1149 function handleResults(results) {
1150 ctrl.matches = results;
1151 ctrl.hidden = shouldHide();
1152
1153 // If loading is in progress, then we'll end the progress. This is needed for example,
1154 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
1155 if (ctrl.loading) setLoading(false);
1156
1157 if ($scope.selectOnMatch) selectItemOnMatch();
1158
1159 positionDropdown();
1160 reportMessages(true, ReportType.Count);
1161 }
1162
1163 /**
1164 * If there is only one matching item and the search text matches its display value exactly,
1165 * automatically select that item. Note: This function is only called if the user uses the
1166 * `md-select-on-match` flag.
1167 */
1168 function selectItemOnMatch () {
1169 var searchText = $scope.searchText,
1170 matches = ctrl.matches,
1171 item = matches[ 0 ];
1172 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
1173 var isMatching = searchText === displayValue;
1174 if ($scope.matchInsensitive && !isMatching) {
1175 isMatching = searchText.toLowerCase() === displayValue.toLowerCase();
1176 }
1177
1178 if (isMatching) {
1179 select(0);
1180 }
1181 });
1182 }
1183
1184 /**
1185 * Evaluates an attribute expression against the parent scope.
1186 * @param {String} attr Name of the attribute to be evaluated.
1187 * @param {Object?} locals Properties to be injected into the evaluation context.
1188 */
1189 function evalAttr(attr, locals) {
1190 if ($attrs[attr]) {
1191 $scope.$parent.$eval($attrs[attr], locals || {});
1192 }
1193 }
1194
1195}
1196
1197
1198MdAutocomplete['$inject'] = ["$$mdSvgRegistry"];angular
1199 .module('material.components.autocomplete')
1200 .directive('mdAutocomplete', MdAutocomplete);
1201
1202/**
1203 * @ngdoc directive
1204 * @name mdAutocomplete
1205 * @module material.components.autocomplete
1206 *
1207 * @description
1208 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
1209 * custom query. This component allows you to provide real-time suggestions as the user types
1210 * in the input area.
1211 *
1212 * To start, you will need to specify the required parameters and provide a template for your
1213 * results. The content inside `md-autocomplete` will be treated as a template.
1214 *
1215 * In more complex cases, you may want to include other content such as a message to display when
1216 * no matches were found. You can do this by wrapping your template in `md-item-template` and
1217 * adding a tag for `md-not-found`. An example of this is shown below.
1218 *
1219 * To reset the displayed value you must clear both values for `md-search-text` and
1220 * `md-selected-item`.
1221 *
1222 * ### Validation
1223 *
1224 * You can use `ng-messages` to include validation the same way that you would normally validate;
1225 * however, if you want to replicate a standard input with a floating label, you will have to
1226 * do the following:
1227 *
1228 * - Make sure that your template is wrapped in `md-item-template`
1229 * - Add your `ng-messages` code inside of `md-autocomplete`
1230 * - Add your validation properties to `md-autocomplete` (ie. `required`)
1231 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
1232 *
1233 * There is an example below of how this should look.
1234 *
1235 * ### Snapping Drop-Down
1236 *
1237 * You can cause the autocomplete drop-down to snap to an ancestor element by applying the
1238 * `md-autocomplete-snap` attribute to that element. You can also snap to the width of
1239 * the `md-autocomplete-snap` element by setting the attribute's value to `width`
1240 * (ie. `md-autocomplete-snap="width"`).
1241 *
1242 * ### Notes
1243 *
1244 * **Autocomplete Dropdown Items Rendering**
1245 *
1246 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeat">
1247 * mdVirtualRepeat</a> directive for displaying the results inside of the dropdown.<br/>
1248 *
1249 * > When encountering issues regarding the item template please take a look at the
1250 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
1251 *
1252 * **Autocomplete inside of a Virtual Repeat**
1253 *
1254 * When using the `md-autocomplete` directive inside of a
1255 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items
1256 * might not update properly, because caching of the results is enabled by default.
1257 *
1258 * The autocomplete will then show invalid dropdown items, because the Virtual Repeat only updates
1259 * the scope bindings rather than re-creating the `md-autocomplete`. This means that the previous
1260 * cached results will be used.
1261 *
1262 * > To avoid such problems, ensure that the autocomplete does not cache any results via
1263 * `md-no-cache="true"`:
1264 *
1265 * <hljs lang="html">
1266 * <md-autocomplete
1267 * md-no-cache="true"
1268 * md-selected-item="selectedItem"
1269 * md-items="item in items"
1270 * md-search-text="searchText"
1271 * md-item-text="item.display">
1272 * <span>{{ item.display }}</span>
1273 * </md-autocomplete>
1274 * </hljs>
1275 *
1276 *
1277 * @param {expression} md-items An expression in the format of `item in results` to iterate over
1278 * matches for your search.<br/><br/>
1279 * The `results` expression can be also a function, which returns the results synchronously
1280 * or asynchronously (per Promise).
1281 * @param {expression=} md-selected-item-change An expression to be run each time a new item is
1282 * selected.
1283 * @param {expression=} md-search-text-change An expression to be run each time the search text
1284 * updates.
1285 * @param {expression=} md-search-text A model to bind the search query text to.
1286 * @param {object=} md-selected-item A model to bind the selected item to.
1287 * @param {expression=} md-item-text An expression that will convert your object to a single string.
1288 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1289 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete.
1290 * @param {boolean=} ng-disabled Determines whether or not to disable the input field.
1291 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
1292 * which will evaluate to false, when no item is currently selected.
1293 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
1294 * make suggestions.
1295 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
1296 * for results.
1297 * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show
1298 * up or not. When `md-floating-label` is set, defaults to false, defaults to true otherwise.
1299 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a
1300 * `$mdDialog`, `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening.
1301 * <br/><br/>
1302 * Also the autocomplete will immediately focus the input element.
1303 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating
1304 * label.
1305 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
1306 * in the dropdown upon open.
1307 * @param {string=} md-input-name The name attribute given to the input element to be used with
1308 * FormController.
1309 * @param {string=} md-menu-class This class will be applied to the dropdown menu for styling.
1310 * @param {string=} md-menu-container-class This class will be applied to the parent container
1311 * of the dropdown panel.
1312 * @param {string=} md-input-class This will be applied to the input for styling. This attribute
1313 * is only valid when a `md-floating-label` is defined.
1314 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
1315 * `md-input-container`.
1316 * @param {string=} md-select-on-focus When present the input's text will be automatically selected
1317 * on focus.
1318 * @param {string=} md-input-id An ID to be added to the input element.
1319 * @param {number=} md-input-minlength The minimum length for the input's value for validation.
1320 * @param {number=} md-input-maxlength The maximum length for the input's value for validation.
1321 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select
1322 * the item if the search text is an exact match. <br/><br/>
1323 * An exact match is when only one match is displayed.
1324 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
1325 * will select on case-insensitive match.
1326 * @param {string=} md-escape-options Override escape key logic. Default is `clear`.<br/>
1327 * Options: `blur`, `clear`, `none`.
1328 * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
1329 * the dropdown.<br/><br/>
1330 * When the dropdown doesn't fit into the viewport, the dropdown will shrink
1331 * as much as possible.
1332 * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`,
1333 * `bottom`.
1334 * @param {string=} input-aria-describedby A space-separated list of element IDs. This should
1335 * contain the IDs of any elements that describe this autocomplete. Screen readers will read the
1336 * content of these elements at the end of announcing that the autocomplete has been selected
1337 * and describing its current state. The descriptive elements do not need to be visible on the
1338 * page.
1339 * @param {string=} input-aria-labelledby A space-separated list of element IDs. The ideal use case
1340 * is that this would contain the ID of a `<label>` element that is associated with this
1341 * autocomplete. This will only have affect when `md-floating-label` is not defined.<br><br>
1342 * For `<label id="state">US State</label>`, you would set this to
1343 * `input-aria-labelledby="state"`.
1344 * @param {string=} input-aria-label A label that will be applied to the autocomplete's input.
1345 * This will be announced by screen readers before the placeholder.
1346 * This will only have affect when `md-floating-label` is not defined. If you define both
1347 * `input-aria-label` and `input-aria-labelledby`, then `input-aria-label` will take precedence.
1348 * @param {string=} md-selected-message Attribute to specify the text that the screen reader will
1349 * announce after a value is selected. Default is: "selected". If `Alaska` is selected in the
1350 * options panel, it will read "Alaska selected". You will want to override this when your app
1351 * runs in a non-English locale.
1352 * @param {string=} md-no-match-message Attribute to specify the text that the screen reader will
1353 * announce after a query returns no matching results.
1354 * Default is: "There are no matches available.". You will want to override this when your app
1355 * runs in a non-English locale.
1356 * @param {string=} md-single-match-message Attribute to specify the text that the screen reader
1357 * will announce after a query returns a single matching result.
1358 * Default is: "There is 1 match available.". You will want to override this when your app
1359 * runs in a non-English locale.
1360 * @param {string=} md-multiple-match-start-message Attribute to specify the text that the screen
1361 * reader will announce after a query returns multiple matching results. The number of matching
1362 * results will be read after this text. Default is: "There are ". You will want to override this
1363 * when your app runs in a non-English locale.
1364 * @param {string=} md-multiple-match-end-message Attribute to specify the text that the screen
1365 * reader will announce after a query returns multiple matching results. The number of matching
1366 * results will be read before this text. Default is: " matches available.". You will want to
1367 * override this when your app runs in a non-English locale.
1368 * @param {boolean=} ng-trim If set to false, the search text will be not trimmed automatically.
1369 * Defaults to true.
1370 * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
1371 * See the [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
1372 * for more details.
1373 * @param {string=} md-mode Specify the repeat mode for suggestion lists. Acceptable values include
1374 * `virtual` (md-virtual-repeat) and `standard` (ng-repeat). See the
1375 * `Specifying Repeat Mode` example for mode details. Default is `virtual`.
1376 *
1377 * @usage
1378 * ### Basic Example
1379 * <hljs lang="html">
1380 * <md-autocomplete
1381 * md-selected-item="selectedItem"
1382 * md-search-text="searchText"
1383 * md-items="item in getMatches(searchText)"
1384 * md-item-text="item.display">
1385 * <span md-highlight-text="searchText">{{item.display}}</span>
1386 * </md-autocomplete>
1387 * </hljs>
1388 *
1389 * ### Example with "not found" message
1390 * <hljs lang="html">
1391 * <md-autocomplete
1392 * md-selected-item="selectedItem"
1393 * md-search-text="searchText"
1394 * md-items="item in getMatches(searchText)"
1395 * md-item-text="item.display">
1396 * <md-item-template>
1397 * <span md-highlight-text="searchText">{{item.display}}</span>
1398 * </md-item-template>
1399 * <md-not-found>
1400 * No matches found.
1401 * </md-not-found>
1402 * </md-autocomplete>
1403 * </hljs>
1404 *
1405 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
1406 * different parts that make up our component.
1407 *
1408 * ### Clear button for the input
1409 * By default, the clear button is displayed when there is input. This aligns with the spec's
1410 * [Search Pattern](https://material.io/archive/guidelines/patterns/search.html#search-in-app-search).
1411 * In floating label mode, when `md-floating-label="My Label"` is applied, the clear button is not
1412 * displayed by default (see the spec's
1413 * [Autocomplete Text Field](https://material.io/archive/guidelines/components/text-fields.html#text-fields-layout)).
1414 *
1415 * Nevertheless, developers are able to explicitly toggle the clear button for all autocomplete
1416 * components with `md-clear-button`.
1417 *
1418 * <hljs lang="html">
1419 * <md-autocomplete ... md-clear-button="true"></md-autocomplete>
1420 * <md-autocomplete ... md-clear-button="false"></md-autocomplete>
1421 * </hljs>
1422 *
1423 * In previous versions, the clear button was always hidden when the component was disabled.
1424 * This changed in `1.1.5` to give the developer control of this behavior. This example
1425 * will hide the clear button only when the component is disabled.
1426 *
1427 * <hljs lang="html">
1428 * <md-autocomplete ... ng-disabled="disabled" md-clear-button="!disabled"></md-autocomplete>
1429 * </hljs>
1430 *
1431 * ### Example with validation
1432 * <hljs lang="html">
1433 * <form name="autocompleteForm">
1434 * <md-autocomplete
1435 * required
1436 * md-input-name="autocomplete"
1437 * md-selected-item="selectedItem"
1438 * md-search-text="searchText"
1439 * md-items="item in getMatches(searchText)"
1440 * md-item-text="item.display">
1441 * <md-item-template>
1442 * <span md-highlight-text="searchText">{{item.display}}</span>
1443 * </md-item-template>
1444 * <div ng-messages="autocompleteForm.autocomplete.$error">
1445 * <div ng-message="required">This field is required</div>
1446 * </div>
1447 * </md-autocomplete>
1448 * </form>
1449 * </hljs>
1450 *
1451 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
1452 * input validation for the field.
1453 *
1454 * ### Asynchronous Results
1455 * The autocomplete items expression also supports promises, which will resolve with the query
1456 * results.
1457 *
1458 * <hljs lang="js">
1459 * function AppController($scope, $http) {
1460 * $scope.query = function(searchText) {
1461 * return $http
1462 * .get(BACKEND_URL + '/items/' + searchText)
1463 * .then(function(data) {
1464 * // Map the response object to the data object.
1465 * return data;
1466 * });
1467 * };
1468 * }
1469 * </hljs>
1470 *
1471 * <hljs lang="html">
1472 * <md-autocomplete
1473 * md-selected-item="selectedItem"
1474 * md-search-text="searchText"
1475 * md-items="item in query(searchText)">
1476 * <md-item-template>
1477 * <span md-highlight-text="searchText">{{item}}</span>
1478 * </md-item-template>
1479 * </md-autocomplete>
1480 * </hljs>
1481 *
1482 * ### Specifying Repeat Mode
1483 * You can use `md-mode` to specify whether to use standard or virtual lists for
1484 * rendering autocomplete options.
1485 * The `md-mode` accepts two values:
1486 * - `virtual` (default) Uses `md-virtual-repeat` to render list items. Virtual
1487 * mode requires you to have consistent heights for all suggestions.
1488 * - `standard` uses `ng-repeat` to render list items. This allows you to have
1489 * options of varying heights.
1490 *
1491 * Note that using 'standard' mode will require you to address any list
1492 * performance issues (e.g. pagination) separately within your application.
1493 *
1494 * <hljs lang="html">
1495 * <md-autocomplete
1496 * md-selected-item="selectedItem"
1497 * md-search-text="searchText"
1498 * md-items="item in getMatches(searchText)"
1499 * md-item-text="item.display"
1500 * md-mode="standard">
1501 * <span md-highlight-text="searchText">{{item.display}}</span>
1502 * </md-autocomplete>
1503 * </hljs>
1504 */
1505function MdAutocomplete ($$mdSvgRegistry) {
1506 var REPEAT_STANDARD = 'standard';
1507 var REPEAT_VIRTUAL = 'virtual';
1508 var REPEAT_MODES = [REPEAT_STANDARD, REPEAT_VIRTUAL];
1509
1510 /** get a valid repeat mode from an md-mode attribute string. */
1511 function getRepeatMode(modeStr) {
1512 if (!modeStr) { return REPEAT_VIRTUAL; }
1513 modeStr = modeStr.toLowerCase();
1514 return REPEAT_MODES.indexOf(modeStr) > -1 ? modeStr : REPEAT_VIRTUAL;
1515 }
1516
1517 return {
1518 controller: 'MdAutocompleteCtrl',
1519 controllerAs: '$mdAutocompleteCtrl',
1520 scope: {
1521 inputName: '@mdInputName',
1522 inputMinlength: '@mdInputMinlength',
1523 inputMaxlength: '@mdInputMaxlength',
1524 searchText: '=?mdSearchText',
1525 selectedItem: '=?mdSelectedItem',
1526 itemsExpr: '@mdItems',
1527 itemText: '&mdItemText',
1528 placeholder: '@placeholder',
1529 inputAriaDescribedBy: '@?inputAriaDescribedby',
1530 inputAriaLabelledBy: '@?inputAriaLabelledby',
1531 inputAriaLabel: '@?inputAriaLabel',
1532 noCache: '=?mdNoCache',
1533 requireMatch: '=?mdRequireMatch',
1534 selectOnMatch: '=?mdSelectOnMatch',
1535 matchInsensitive: '=?mdMatchCaseInsensitive',
1536 itemChange: '&?mdSelectedItemChange',
1537 textChange: '&?mdSearchTextChange',
1538 minLength: '=?mdMinLength',
1539 delay: '=?mdDelay',
1540 autofocus: '=?mdAutofocus',
1541 floatingLabel: '@?mdFloatingLabel',
1542 autoselect: '=?mdAutoselect',
1543 menuClass: '@?mdMenuClass',
1544 menuContainerClass: '@?mdMenuContainerClass',
1545 inputClass: '@?mdInputClass',
1546 inputId: '@?mdInputId',
1547 escapeOptions: '@?mdEscapeOptions',
1548 dropdownItems: '=?mdDropdownItems',
1549 dropdownPosition: '@?mdDropdownPosition',
1550 clearButton: '=?mdClearButton',
1551 selectedMessage: '@?mdSelectedMessage',
1552 noMatchMessage: '@?mdNoMatchMessage',
1553 singleMatchMessage: '@?mdSingleMatchMessage',
1554 multipleMatchStartMessage: '@?mdMultipleMatchStartMessage',
1555 multipleMatchEndMessage: '@?mdMultipleMatchEndMessage',
1556 mdMode: '=?mdMode'
1557 },
1558 compile: function(tElement, tAttrs) {
1559 var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
1560 var input = tElement.find('input');
1561
1562 attributes.forEach(function(attribute) {
1563 var attrValue = tAttrs[tAttrs.$normalize(attribute)];
1564
1565 if (attrValue !== null) {
1566 input.attr(attribute, attrValue);
1567 }
1568 });
1569
1570 return function(scope, element, attrs, ctrl) {
1571 // Retrieve the state of using a md-not-found template by using our attribute, which will
1572 // be added to the element in the template function.
1573 ctrl.hasNotFound = !!element.attr('md-has-not-found');
1574
1575 // By default the inset autocomplete should show the clear button when not explicitly
1576 // overwritten or in floating label mode.
1577 if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
1578 scope.clearButton = true;
1579 }
1580
1581 scope.mdMode = getRepeatMode(attrs.mdMode);
1582
1583 // Stop click events from bubbling up to the document and triggering a flicker of the
1584 // options panel while still supporting ng-click to be placed on md-autocomplete.
1585 element.on('click touchstart touchend', function(event) {
1586 event.stopPropagation();
1587 });
1588 };
1589 },
1590 template: function (element, attr) {
1591 var noItemsTemplate = getNoItemsTemplate(),
1592 itemTemplate = getItemTemplate(),
1593 leftover = element.html(),
1594 tabindex = attr.tabindex;
1595
1596 // Set our attribute for the link function above which runs later.
1597 // We will set an attribute, because otherwise the stored variables will be trashed when
1598 // removing the element is hidden while retrieving the template. For example when using ngIf.
1599 if (noItemsTemplate) element.attr('md-has-not-found', true);
1600
1601 // Always set our tabindex of the autocomplete directive to -1, because our input
1602 // will hold the actual tabindex.
1603 element.attr('tabindex', '-1');
1604
1605 return '\
1606 <md-autocomplete-wrap\
1607 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
1608 \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
1609 \'md-show-clear-button\': !!clearButton }">\
1610 ' + getInputElement() + '\
1611 ' + getClearButton() + '\
1612 <md-progress-linear\
1613 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
1614 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
1615 md-mode="indeterminate"></md-progress-linear>\
1616 ' + getContainer(attr.mdMenuContainerClass, attr.mdMode) + '\
1617 <ul class="md-autocomplete-suggestions"\
1618 ng-class="::menuClass"\
1619 id="ul-{{$mdAutocompleteCtrl.id}}"\
1620 ng-mouseup="$mdAutocompleteCtrl.focusInput()"\
1621 role="listbox">\
1622 <li class="md-autocomplete-suggestion" ' + getRepeatType(attr.mdMode) + ' ="item in $mdAutocompleteCtrl.matches"\
1623 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
1624 ng-attr-id="{{\'md-option-\' + $mdAutocompleteCtrl.id + \'-\' + $index}}"\
1625 ng-click="$mdAutocompleteCtrl.select($index)"\
1626 role="option"\
1627 aria-setsize="{{$mdAutocompleteCtrl.matches.length}}"\
1628 aria-posinset="{{$index+1}}"\
1629 aria-selected="{{$index === $mdAutocompleteCtrl.index ? true : false}}" \
1630 md-extra-name="$mdAutocompleteCtrl.itemName">\
1631 ' + itemTemplate + '\
1632 </li>' + noItemsTemplate + '\
1633 </ul>\
1634 ' + getContainerClosingTags(attr.mdMode) + '\
1635 </md-autocomplete-wrap>';
1636
1637 function getItemTemplate() {
1638 var templateTag = element.find('md-item-template').detach(),
1639 html = templateTag.length ? templateTag.html() : element.html();
1640 if (!templateTag.length) element.empty();
1641 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html +
1642 '</md-autocomplete-parent-scope>';
1643 }
1644
1645 function getNoItemsTemplate() {
1646 var templateTag = element.find('md-not-found').detach(),
1647 template = templateTag.length ? templateTag.html() : '';
1648 return template
1649 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()" class="md-autocomplete-suggestion"\
1650 md-autocomplete-parent-scope>' + template + '</li>'
1651 : '';
1652 }
1653
1654 function getContainer(menuContainerClass, repeatMode) {
1655 // prepend a space if needed
1656 menuContainerClass = menuContainerClass ? ' ' + menuContainerClass : '';
1657
1658 if (isVirtualRepeatDisabled(repeatMode)) {
1659 return '\
1660 <div \
1661 ng-hide="$mdAutocompleteCtrl.hidden"\
1662 class="md-standard-list-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
1663 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
1664 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
1665 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
1666 role="presentation">\
1667 <div class="md-standard-list-scroller" role="presentation">';
1668 }
1669
1670 return '\
1671 <md-virtual-repeat-container\
1672 md-auto-shrink\
1673 md-auto-shrink-min="1"\
1674 ng-hide="$mdAutocompleteCtrl.hidden"\
1675 class="md-virtual-repeat-container md-autocomplete-suggestions-container md-whiteframe-z1' + menuContainerClass + '"\
1676 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
1677 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
1678 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
1679 role="presentation">';
1680 }
1681
1682 function getContainerClosingTags(repeatMode) {
1683 return isVirtualRepeatDisabled(repeatMode) ?
1684 ' </div>\
1685 </div>\
1686 </div>' : '</md-virtual-repeat-container>';
1687 }
1688
1689 function getRepeatType(repeatMode) {
1690 return isVirtualRepeatDisabled(repeatMode) ?
1691 'ng-repeat' : 'md-virtual-repeat';
1692 }
1693
1694 function isVirtualRepeatDisabled(repeatMode) {
1695 // ensure we have a valid repeat mode
1696 var correctedRepeatMode = getRepeatMode(repeatMode);
1697 return correctedRepeatMode !== REPEAT_VIRTUAL;
1698 }
1699
1700 function getInputElement () {
1701 if (attr.mdFloatingLabel) {
1702 return '\
1703 <md-input-container ng-if="floatingLabel">\
1704 <label>{{floatingLabel}}</label>\
1705 <input type="text"\
1706 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1707 id="{{inputId || \'fl-input-\' + $mdAutocompleteCtrl.id}}"\
1708 name="{{inputName || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
1709 ng-class="::inputClass"\
1710 autocomplete="off"\
1711 ng-required="$mdAutocompleteCtrl.isRequired"\
1712 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1713 ng-minlength="inputMinlength"\
1714 ng-maxlength="inputMaxlength"\
1715 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1716 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1717 ng-model-options="{ allowInvalid: true }"\
1718 ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
1719 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1720 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1721 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1722 aria-label="{{floatingLabel}}"\
1723 ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
1724 ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
1725 aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
1726 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
1727 ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
1728 ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">\
1729 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
1730 </md-input-container>';
1731 } else {
1732 return '\
1733 <input type="text"\
1734 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1735 id="{{inputId || \'input-\' + $mdAutocompleteCtrl.id}}"\
1736 name="{{inputName || \'input-\' + $mdAutocompleteCtrl.id }}"\
1737 ng-class="::inputClass"\
1738 ng-if="!floatingLabel"\
1739 autocomplete="off"\
1740 ng-required="$mdAutocompleteCtrl.isRequired"\
1741 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1742 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1743 ng-minlength="inputMinlength"\
1744 ng-maxlength="inputMaxlength"\
1745 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1746 ng-mousedown="$mdAutocompleteCtrl.focusInput()"\
1747 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1748 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1749 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1750 placeholder="{{placeholder}}"\
1751 aria-label="{{placeholder}}"\
1752 ng-attr-aria-autocomplete="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'list\'}}"\
1753 ng-attr-role="{{$mdAutocompleteCtrl.isDisabled ? undefined : \'combobox\'}}"\
1754 aria-haspopup="{{!$mdAutocompleteCtrl.isDisabled}}"\
1755 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"\
1756 ng-attr-aria-owns="{{$mdAutocompleteCtrl.hidden || $mdAutocompleteCtrl.isDisabled ? undefined : \'ul-\' + $mdAutocompleteCtrl.id}}"\
1757 ng-attr-aria-activedescendant="{{!$mdAutocompleteCtrl.hidden && $mdAutocompleteCtrl.activeOption ? $mdAutocompleteCtrl.activeOption : undefined}}">';
1758 }
1759 }
1760
1761 function getClearButton() {
1762 return '' +
1763 '<button ' +
1764 'type="button" ' +
1765 'aria-label="Clear Input" ' +
1766 'tabindex="0" ' +
1767 'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText" ' +
1768 'ng-click="$mdAutocompleteCtrl.clear($event)">' +
1769 '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
1770 '</button>';
1771 }
1772 }
1773 };
1774}
1775
1776
1777MdAutocompleteItemScopeDirective['$inject'] = ["$compile", "$mdUtil"];angular
1778 .module('material.components.autocomplete')
1779 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
1780
1781function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
1782 return {
1783 restrict: 'AE',
1784 compile: compile,
1785 terminal: true,
1786 transclude: 'element'
1787 };
1788
1789 function compile(tElement, tAttr, transclude) {
1790 return function postLink(scope, element, attr) {
1791 var ctrl = scope.$mdAutocompleteCtrl;
1792 var newScope = ctrl.parent.$new();
1793 var itemName = ctrl.itemName;
1794
1795 // Watch for changes to our scope's variables and copy them to the new scope
1796 watchVariable('$index', '$index');
1797 watchVariable('item', itemName);
1798
1799 // Ensure that $digest calls on our scope trigger $digest on newScope.
1800 connectScopes();
1801
1802 // Link the element against newScope.
1803 transclude(newScope, function(clone) {
1804 element.after(clone);
1805 });
1806
1807 /**
1808 * Creates a watcher for variables that are copied from the parent scope
1809 * @param variable
1810 * @param alias
1811 */
1812 function watchVariable(variable, alias) {
1813 newScope[alias] = scope[variable];
1814
1815 scope.$watch(variable, function(value) {
1816 $mdUtil.nextTick(function() {
1817 newScope[alias] = value;
1818 });
1819 });
1820 }
1821
1822 /**
1823 * Creates watchers on scope and newScope that ensure that for any
1824 * $digest of scope, newScope is also $digested.
1825 */
1826 function connectScopes() {
1827 var scopeDigesting = false;
1828 var newScopeDigesting = false;
1829
1830 scope.$watch(function() {
1831 if (newScopeDigesting || scopeDigesting) {
1832 return;
1833 }
1834
1835 scopeDigesting = true;
1836 scope.$$postDigest(function() {
1837 if (!newScopeDigesting) {
1838 newScope.$digest();
1839 }
1840
1841 scopeDigesting = newScopeDigesting = false;
1842 });
1843 });
1844
1845 newScope.$watch(function() {
1846 newScopeDigesting = true;
1847 });
1848 }
1849 };
1850 }
1851}
1852
1853MdHighlightCtrl['$inject'] = ["$scope", "$element", "$attrs", "$mdUtil"];angular
1854 .module('material.components.autocomplete')
1855 .controller('MdHighlightCtrl', MdHighlightCtrl);
1856
1857function MdHighlightCtrl ($scope, $element, $attrs, $mdUtil) {
1858 this.$scope = $scope;
1859 this.$element = $element;
1860 this.$attrs = $attrs;
1861 this.$mdUtil = $mdUtil;
1862
1863 // Cache the Regex to avoid rebuilding each time.
1864 this.regex = null;
1865}
1866
1867MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
1868
1869 this.flags = this.$attrs.mdHighlightFlags || '';
1870
1871 this.unregisterFn = this.$scope.$watch(function($scope) {
1872 return {
1873 term: unsafeTermFn($scope),
1874 contentText: unsafeContentFn($scope)
1875 };
1876 }.bind(this), this.onRender.bind(this), true);
1877
1878 this.$element.on('$destroy', this.unregisterFn);
1879};
1880
1881/**
1882 * Triggered once a new change has been recognized and the highlighted
1883 * text needs to be updated.
1884 */
1885MdHighlightCtrl.prototype.onRender = function(state, prevState) {
1886
1887 var contentText = state.contentText;
1888
1889 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
1890 if (this.regex === null || state.term !== prevState.term) {
1891 this.regex = this.createRegex(state.term, this.flags);
1892 }
1893
1894 /* If a term is available apply the regex to the content */
1895 if (state.term) {
1896 this.applyRegex(contentText);
1897 } else {
1898 this.$element.text(contentText);
1899 }
1900
1901};
1902
1903/**
1904 * Decomposes the specified text into different tokens (whether match or not).
1905 * Breaking down the string guarantees proper XSS protection due to the native browser
1906 * escaping of unsafe text.
1907 */
1908MdHighlightCtrl.prototype.applyRegex = function(text) {
1909 var tokens = this.resolveTokens(text);
1910
1911 this.$element.empty();
1912
1913 tokens.forEach(function (token) {
1914
1915 if (token.isMatch) {
1916 var tokenEl = angular.element('<span class="highlight">').text(token.text);
1917
1918 this.$element.append(tokenEl);
1919 } else {
1920 this.$element.append(document.createTextNode(token));
1921 }
1922
1923 }.bind(this));
1924
1925};
1926
1927 /**
1928 * Decomposes the specified text into different tokens by running the regex against the text.
1929 */
1930MdHighlightCtrl.prototype.resolveTokens = function(string) {
1931 var tokens = [];
1932 var lastIndex = 0;
1933
1934 // Use replace here, because it supports global and single regular expressions at same time.
1935 string.replace(this.regex, function(match, index) {
1936 appendToken(lastIndex, index);
1937
1938 tokens.push({
1939 text: match,
1940 isMatch: true
1941 });
1942
1943 lastIndex = index + match.length;
1944 });
1945
1946 // Append the missing text as a token.
1947 appendToken(lastIndex);
1948
1949 return tokens;
1950
1951 function appendToken(from, to) {
1952 var targetText = string.slice(from, to);
1953 targetText && tokens.push(targetText);
1954 }
1955};
1956
1957/** Creates a regex for the specified text with the given flags. */
1958MdHighlightCtrl.prototype.createRegex = function(term, flags) {
1959 var startFlag = '', endFlag = '';
1960 var regexTerm = this.$mdUtil.sanitize(term);
1961
1962 if (flags.indexOf('^') >= 0) startFlag = '^';
1963 if (flags.indexOf('$') >= 0) endFlag = '$';
1964
1965 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$^]/g, ''));
1966};
1967
1968
1969MdHighlight['$inject'] = ["$interpolate", "$parse"];angular
1970 .module('material.components.autocomplete')
1971 .directive('mdHighlightText', MdHighlight);
1972
1973/**
1974 * @ngdoc directive
1975 * @name mdHighlightText
1976 * @module material.components.autocomplete
1977 *
1978 * @description
1979 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
1980 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
1981 * be styled through CSS. Please note that child elements may not be used with this directive.
1982 *
1983 * @param {string} md-highlight-text A model to be searched for
1984 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
1985 * #### **Supported flags**:
1986 * - `g`: Find all matches within the provided text
1987 * - `i`: Ignore case when searching for matches
1988 * - `$`: Only match if the text ends with the search term
1989 * - `^`: Only match if the text begins with the search term
1990 *
1991 * @usage
1992 * <hljs lang="html">
1993 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
1994 * <ul>
1995 * <li ng-repeat="result in results" md-highlight-text="searchTerm" md-highlight-flags="i">
1996 * {{result.text}}
1997 * </li>
1998 * </ul>
1999 * </hljs>
2000 */
2001
2002function MdHighlight ($interpolate, $parse) {
2003 return {
2004 terminal: true,
2005 controller: 'MdHighlightCtrl',
2006 compile: function mdHighlightCompile(tElement, tAttr) {
2007 var termExpr = $parse(tAttr.mdHighlightText);
2008 var unsafeContentExpr = $interpolate(tElement.html());
2009
2010 return function mdHighlightLink(scope, element, attr, ctrl) {
2011 ctrl.init(termExpr, unsafeContentExpr);
2012 };
2013 }
2014 };
2015}
2016
2017ngmaterial.components.autocomplete = angular.module("material.components.autocomplete");
Note: See TracBrowser for help on using the repository browser.