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