2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ngmaterial.components.autocomplete');
8 goog.require('ngmaterial.components.icon');
9 goog.require('ngmaterial.components.virtualRepeat');
10 goog.require('ngmaterial.core');
13 * @name material.components.autocomplete
16 * @see js folder for autocomplete implementation
18 angular.module('material.components.autocomplete', [
20 'material.components.icon',
21 'material.components.virtualRepeat'
25 MdAutocompleteCtrl['$inject'] = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q", "$log", "$mdLiveAnnouncer"];angular
26 .module('material.components.autocomplete')
27 .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
32 INPUT_PADDING = 2; // Padding provided by `md-input-container`
34 function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
35 $animate, $rootElement, $attrs, $q, $log, $mdLiveAnnouncer) {
37 // Internal Variables.
39 itemParts = $scope.itemsExpr.split(/ in /i),
40 itemExpr = itemParts[ 1 ],
44 selectedItemWatchers = [],
46 fetchesInProgress = 0,
47 enableWrapScroll = null,
48 inputModelCtrl = null,
49 debouncedOnResize = $mdUtil.debounce(onWindowResize);
51 // Public Exported Variables with handlers
52 defineProperty('hidden', handleHiddenChange, true);
54 // Public Exported Variables
56 ctrl.parent = $scope.$parent;
57 ctrl.itemName = itemParts[ 0 ];
62 ctrl.id = $mdUtil.nextUid();
63 ctrl.isDisabled = null;
64 ctrl.isRequired = null;
65 ctrl.isReadonly = null;
66 ctrl.hasNotFound = false;
68 // Public Exported Methods
69 ctrl.keydown = keydown;
72 ctrl.clear = clearValue;
74 ctrl.listEnter = onListEnter;
75 ctrl.listLeave = onListLeave;
76 ctrl.mouseUp = onMouseup;
77 ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
78 ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
79 ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
80 ctrl.notFoundVisible = notFoundVisible;
81 ctrl.loadingIsVisible = loadingIsVisible;
82 ctrl.positionDropdown = positionDropdown;
85 * Report types to be used for the $mdLiveAnnouncer
86 * @enum {number} Unique flag id.
95 //-- initialization methods
98 * Initialize the controller, setup watchers, gather elements
102 $mdUtil.initOptionalProperties($scope, $attrs, {
108 $mdTheming($element);
110 $mdUtil.nextTick(function () {
115 // Forward all focus events to the input element when autofocus is enabled
116 if ($scope.autofocus) {
117 $element.on('focus', focusInputElement);
122 function updateModelValidators() {
123 if (!$scope.requireMatch || !inputModelCtrl) return;
125 inputModelCtrl.$setValidity('md-require-match', !!$scope.selectedItem || !$scope.searchText);
129 * Calculates the dropdown's position and applies the new styles to the menu element
132 function positionDropdown () {
134 return $mdUtil.nextTick(positionDropdown, false, $scope);
137 var dropdownHeight = ($scope.dropdownItems || MAX_ITEMS) * ITEM_HEIGHT;
139 var hrect = elements.wrap.getBoundingClientRect(),
140 vrect = elements.snap.getBoundingClientRect(),
141 root = elements.root.getBoundingClientRect(),
142 top = vrect.bottom - root.top,
143 bot = root.bottom - vrect.top,
144 left = hrect.left - root.left,
146 offset = getVerticalOffset(),
147 position = $scope.dropdownPosition,
150 // Automatically determine dropdown placement based on available space in viewport.
152 position = (top > bot && root.height - hrect.bottom - MENU_PADDING < dropdownHeight) ? 'top' : 'bottom';
154 // Adjust the width to account for the padding provided by `md-input-container`
155 if ($attrs.mdFloatingLabel) {
156 left += INPUT_PADDING;
157 width -= INPUT_PADDING * 2;
161 minWidth: width + 'px',
162 maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
165 if (position === 'top') {
167 styles.bottom = bot + 'px';
168 styles.maxHeight = Math.min(dropdownHeight, hrect.top - root.top - MENU_PADDING) + 'px';
170 var bottomSpace = root.bottom - hrect.bottom - MENU_PADDING + $mdUtil.getViewportTop();
172 styles.top = (top - offset) + 'px';
173 styles.bottom = 'auto';
174 styles.maxHeight = Math.min(dropdownHeight, bottomSpace) + 'px';
177 elements.$.scrollContainer.css(styles);
178 $mdUtil.nextTick(correctHorizontalAlignment, false);
181 * Calculates the vertical offset for floating label examples to account for ngMessages
184 function getVerticalOffset () {
186 var inputContainer = $element.find('md-input-container');
187 if (inputContainer.length) {
188 var input = inputContainer.find('input');
189 offset = inputContainer.prop('offsetHeight');
190 offset -= input.prop('offsetTop');
191 offset -= input.prop('offsetHeight');
192 // add in the height left up top for the floating label text
193 offset += inputContainer.prop('offsetTop');
199 * Makes sure that the menu doesn't go off of the screen on either side.
201 function correctHorizontalAlignment () {
202 var dropdown = elements.scrollContainer.getBoundingClientRect(),
204 if (dropdown.right > root.right - MENU_PADDING) {
205 styles.left = (hrect.right - dropdown.width) + 'px';
207 elements.$.scrollContainer.css(styles);
212 * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
214 function moveDropdown () {
215 if (!elements.$.root.length) return;
216 $mdTheming(elements.$.scrollContainer);
217 elements.$.scrollContainer.detach();
218 elements.$.root.append(elements.$.scrollContainer);
219 if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
223 * Sends focus to the input element.
225 function focusInputElement () {
226 elements.input.focus();
230 * Sets up any watchers used by autocomplete
232 function configureWatchers () {
233 var wait = parseInt($scope.delay, 10) || 0;
235 $attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
236 $attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
237 $attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
239 $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
240 $scope.$watch('selectedItem', selectedItemChange);
242 angular.element($window).on('resize', debouncedOnResize);
244 $scope.$on('$destroy', cleanup);
248 * Removes any events or leftover elements created by this controller
250 function cleanup () {
252 $mdUtil.enableScrolling();
255 angular.element($window).off('resize', debouncedOnResize);
258 var items = ['ul', 'scroller', 'scrollContainer', 'input'];
259 angular.forEach(items, function(key){
260 elements.$[key].remove();
266 * Event handler to be called whenever the window resizes.
268 function onWindowResize() {
275 * Gathers all of the elements needed for this controller
277 function gatherElements () {
279 var snapWrap = gatherSnapWrap();
283 scrollContainer: $element[0].querySelector('.md-virtual-repeat-container'),
284 scroller: $element[0].querySelector('.md-virtual-repeat-scroller'),
285 ul: $element.find('ul')[0],
286 input: $element.find('input')[0],
292 elements.li = elements.ul.getElementsByTagName('li');
293 elements.$ = getAngularElements(elements);
295 inputModelCtrl = elements.$.input.controller('ngModel');
299 * Gathers the snap and wrap elements
302 function gatherSnapWrap() {
305 for (element = $element; element.length; element = element.parent()) {
306 value = element.attr('md-autocomplete-snap');
307 if (angular.isDefined(value)) break;
310 if (element.length) {
313 wrap: (value.toLowerCase() === 'width') ? element[0] : $element.find('md-autocomplete-wrap')[0]
317 var wrap = $element.find('md-autocomplete-wrap')[0];
325 * Gathers angular-wrapped versions of each element
329 function getAngularElements (elements) {
331 for (var key in elements) {
332 if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
337 //-- event/change handlers
340 * Handles changes to the `hidden` property.
344 function handleHiddenChange (hidden, oldHidden) {
345 if (!hidden && oldHidden) {
348 // Report in polite mode, because the screenreader should finish the default description of
349 // the input. element.
350 reportMessages(true, ReportType.Count | ReportType.Selected);
353 $mdUtil.disableScrollAround(elements.ul);
354 enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
356 } else if (hidden && !oldHidden) {
357 $mdUtil.enableScrolling();
359 if (enableWrapScroll) {
361 enableWrapScroll = null;
367 * Disables scrolling for a specific element
369 function disableElementScrollEvents(element) {
371 function preventDefault(e) {
375 element.on('wheel', preventDefault);
376 element.on('touchmove', preventDefault);
379 element.off('wheel', preventDefault);
380 element.off('touchmove', preventDefault);
385 * When the user mouses over the dropdown menu, ignore blur events.
387 function onListEnter () {
392 * When the user's mouse leaves the menu, blur events may hide the menu again.
394 function onListLeave () {
395 if (!hasFocus && !ctrl.hidden) elements.input.focus();
397 ctrl.hidden = shouldHide();
401 * When the mouse button is released, send focus back to the input field.
403 function onMouseup () {
404 elements.input.focus();
408 * Handles changes to the selected item.
409 * @param selectedItem
410 * @param previousSelectedItem
412 function selectedItemChange (selectedItem, previousSelectedItem) {
414 updateModelValidators();
417 getDisplayValue(selectedItem).then(function (val) {
418 $scope.searchText = val;
419 handleSelectedItemChange(selectedItem, previousSelectedItem);
421 } else if (previousSelectedItem && $scope.searchText) {
422 getDisplayValue(previousSelectedItem).then(function(displayValue) {
423 // Clear the searchText, when the selectedItem is set to null.
424 // Do not clear the searchText, when the searchText isn't matching with the previous
426 if (angular.isString($scope.searchText)
427 && displayValue.toString().toLowerCase() === $scope.searchText.toLowerCase()) {
428 $scope.searchText = '';
433 if (selectedItem !== previousSelectedItem) announceItemChange();
437 * Use the user-defined expression to announce changes each time a new item is selected
439 function announceItemChange () {
440 angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
444 * Use the user-defined expression to announce changes each time the search text is changed
446 function announceTextChange () {
447 angular.isFunction($scope.textChange) && $scope.textChange();
451 * Calls any external watchers listening for the selected item. Used in conjunction with
452 * `registerSelectedItemWatcher`.
453 * @param selectedItem
454 * @param previousSelectedItem
456 function handleSelectedItemChange (selectedItem, previousSelectedItem) {
457 selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
461 * Register a function to be called when the selected item changes.
464 function registerSelectedItemWatcher (cb) {
465 if (selectedItemWatchers.indexOf(cb) == -1) {
466 selectedItemWatchers.push(cb);
471 * Unregister a function previously registered for selected item changes.
474 function unregisterSelectedItemWatcher (cb) {
475 var i = selectedItemWatchers.indexOf(cb);
477 selectedItemWatchers.splice(i, 1);
482 * Handles changes to the searchText property.
484 * @param previousSearchText
486 function handleSearchText (searchText, previousSearchText) {
487 ctrl.index = getDefaultIndex();
489 // do nothing on init
490 if (searchText === previousSearchText) return;
492 updateModelValidators();
494 getDisplayValue($scope.selectedItem).then(function (val) {
495 // clear selected item if search text no longer matches it
496 if (searchText !== val) {
497 $scope.selectedItem = null;
500 // trigger change event if available
501 if (searchText !== previousSearchText) announceTextChange();
503 // cancel results if search text is not long enough
504 if (!isMinLengthMet()) {
508 reportMessages(false, ReportType.Count);
519 * Handles input blur event, determines if the dropdown should hide.
521 function blur($event) {
525 ctrl.hidden = shouldHide();
526 evalAttr('ngBlur', { $event: $event });
531 * Force blur on input element
534 function doBlur(forceBlur) {
539 elements.input.blur();
543 * Handles input focus event, determines if the dropdown should show.
545 function focus($event) {
548 if (isSearchable() && isMinLengthMet()) {
552 ctrl.hidden = shouldHide();
554 evalAttr('ngFocus', { $event: $event });
558 * Handles keyboard input.
561 function keydown (event) {
562 switch (event.keyCode) {
563 case $mdConstant.KEY_CODE.DOWN_ARROW:
564 if (ctrl.loading) return;
565 event.stopPropagation();
566 event.preventDefault();
567 ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
569 reportMessages(false, ReportType.Selected);
571 case $mdConstant.KEY_CODE.UP_ARROW:
572 if (ctrl.loading) return;
573 event.stopPropagation();
574 event.preventDefault();
575 ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
577 reportMessages(false, ReportType.Selected);
579 case $mdConstant.KEY_CODE.TAB:
580 // If we hit tab, assume that we've left the list so it will close
583 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
586 case $mdConstant.KEY_CODE.ENTER:
587 if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
588 if (hasSelection()) return;
589 event.stopPropagation();
590 event.preventDefault();
593 case $mdConstant.KEY_CODE.ESCAPE:
594 event.preventDefault(); // Prevent browser from always clearing input
595 if (!shouldProcessEscape()) return;
596 event.stopPropagation();
599 if ($scope.searchText && hasEscapeOption('clear')) {
603 // Manually hide (needed for mdNotFound support)
606 if (hasEscapeOption('blur')) {
607 // Force the component to blur if they hit escape
619 * Returns the minimum length needed to display the dropdown.
622 function getMinLength () {
623 return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
627 * Returns the display value for an item.
631 function getDisplayValue (item) {
632 return $q.when(getItemText(item) || item).then(function(itemText) {
633 if (itemText && !angular.isString(itemText)) {
634 $log.warn('md-autocomplete: Could not resolve display value to a string. ' +
635 'Please check the `md-item-text` attribute.');
642 * Getter function to invoke user-defined expression (in the directive)
643 * to convert your object to a single string.
645 function getItemText (item) {
646 return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
651 * Returns the locals object for compiling item templates.
655 function getItemAsNameVal (item) {
656 if (!item) return undefined;
659 if (ctrl.itemName) locals[ ctrl.itemName ] = item;
665 * Returns the default index based on whether or not autoselect is enabled.
668 function getDefaultIndex () {
669 return $scope.autoselect ? 0 : -1;
673 * Sets the loading parameter and updates the hidden state.
674 * @param value {boolean} Whether or not the component is currently loading.
676 function setLoading(value) {
677 if (ctrl.loading != value) {
678 ctrl.loading = value;
681 // Always refresh the hidden variable as something else might have changed
682 ctrl.hidden = shouldHide();
686 * Determines if the menu should be hidden.
689 function shouldHide () {
690 if (!isSearchable()) return true; // Hide when not able to query
691 else return !shouldShow(); // Hide when the dropdown is not able to show.
695 * Determines whether the autocomplete is able to query within the current state.
698 function isSearchable() {
699 if (ctrl.loading && !hasMatches()) return false; // No query when query is in progress.
700 else if (hasSelection()) return false; // No query if there is already a selection
701 else if (!hasFocus) return false; // No query if the input does not have focus
706 * Determines if the escape keydown should be processed
709 function shouldProcessEscape() {
710 return hasEscapeOption('blur') || !ctrl.hidden || ctrl.loading || hasEscapeOption('clear') && $scope.searchText;
714 * Determines if an escape option is set
717 function hasEscapeOption(option) {
718 return !$scope.escapeOptions || $scope.escapeOptions.toLowerCase().indexOf(option) !== -1;
722 * Determines if the menu should be shown.
725 function shouldShow() {
726 return (isMinLengthMet() && hasMatches()) || notFoundVisible();
730 * Returns true if the search text has matches.
733 function hasMatches() {
734 return ctrl.matches.length ? true : false;
738 * Returns true if the autocomplete has a valid selection.
741 function hasSelection() {
742 return ctrl.scope.selectedItem ? true : false;
746 * Returns true if the loading indicator is, or should be, visible.
749 function loadingIsVisible() {
750 return ctrl.loading && !hasSelection();
754 * Returns the display value of the current item.
757 function getCurrentDisplayValue () {
758 return getDisplayValue(ctrl.matches[ ctrl.index ]);
762 * Determines if the minimum length is met by the search text.
765 function isMinLengthMet () {
766 return ($scope.searchText || '').length >= getMinLength();
772 * Defines a public property with a handler and a default value.
777 function defineProperty (key, handler, value) {
778 Object.defineProperty(ctrl, key, {
779 get: function () { return value; },
780 set: function (newValue) {
781 var oldValue = value;
783 handler(newValue, oldValue);
789 * Selects the item at the given index.
792 function select (index) {
793 //-- force form to update state for validation
794 $mdUtil.nextTick(function () {
795 getDisplayValue(ctrl.matches[ index ]).then(function (val) {
796 var ngModel = elements.$.input.controller('ngModel');
797 ngModel.$setViewValue(val);
799 }).finally(function () {
800 $scope.selectedItem = ctrl.matches[ index ];
807 * Clears the searchText value and selected item.
809 function clearValue () {
815 * Clears the selected item
817 function clearSelectedItem () {
818 // Reset our variables
824 * Clears the searchText value
826 function clearSearchText () {
827 // Set the loading to true so we don't see flashes of content.
828 // The flashing will only occur when an async request is running.
829 // So the loading process will stop when the results had been retrieved.
832 $scope.searchText = '';
834 // Normally, triggering the change / input event is unnecessary, because the browser detects it properly.
835 // But some browsers are not detecting it properly, which means that we have to trigger the event.
836 // Using the `input` is not working properly, because for example IE11 is not supporting the `input` event.
837 // The `change` event is a good alternative and is supported by all supported browsers.
838 var eventObj = document.createEvent('CustomEvent');
839 eventObj.initCustomEvent('change', true, true, { value: '' });
840 elements.input.dispatchEvent(eventObj);
842 // For some reason, firing the above event resets the value of $scope.searchText if
843 // $scope.searchText has a space character at the end, so we blank it one more time and then
845 elements.input.blur();
846 $scope.searchText = '';
847 elements.input.focus();
851 * Fetches the results for the provided search text.
854 function fetchResults (searchText) {
855 var items = $scope.$parent.$eval(itemExpr),
856 term = searchText.toLowerCase(),
857 isList = angular.isArray(items),
858 isPromise = !!items.then; // Every promise should contain a `then` property
860 if (isList) onResultsRetrieved(items);
861 else if (isPromise) handleAsyncResults(items);
863 function handleAsyncResults(items) {
864 if ( !items ) return;
866 items = $q.when(items);
870 $mdUtil.nextTick(function () {
872 .then(onResultsRetrieved)
874 if (--fetchesInProgress === 0) {
881 function onResultsRetrieved(matches) {
882 cache[term] = matches;
884 // Just cache the results if the request is now outdated.
885 // The request becomes outdated, when the new searchText has changed during the result fetching.
886 if ((searchText || '') !== ($scope.searchText || '')) {
890 handleResults(matches);
896 * Reports given message types to supported screenreaders.
897 * @param {boolean} isPolite Whether the announcement should be polite.
898 * @param {!number} types Message flags to be reported to the screenreader.
900 function reportMessages(isPolite, types) {
902 var politeness = isPolite ? 'polite' : 'assertive';
905 if (types & ReportType.Selected && ctrl.index !== -1) {
906 messages.push(getCurrentDisplayValue());
909 if (types & ReportType.Count) {
910 messages.push($q.resolve(getCountMessage()));
913 $q.all(messages).then(function(data) {
914 $mdLiveAnnouncer.announce(data.join(' '), politeness);
920 * Returns the ARIA message for how many results match the current query.
923 function getCountMessage () {
924 switch (ctrl.matches.length) {
926 return 'There are no matches available.';
928 return 'There is 1 match available.';
930 return 'There are ' + ctrl.matches.length + ' matches available.';
935 * Makes sure that the focused element is within view.
937 function updateScroll () {
938 if (!elements.li[0]) return;
939 var height = elements.li[0].offsetHeight,
940 top = height * ctrl.index,
942 hgt = elements.scroller.clientHeight,
943 scrollTop = elements.scroller.scrollTop;
944 if (top < scrollTop) {
946 } else if (bot > scrollTop + hgt) {
951 function isPromiseFetching() {
952 return fetchesInProgress !== 0;
955 function scrollTo (offset) {
956 elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
959 function notFoundVisible () {
960 var textLength = (ctrl.scope.searchText || '').length;
962 return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
966 * Starts the query to gather the results for the current searchText. Attempts to return cached
967 * results first, then forwards the process to `fetchResults` if necessary.
969 function handleQuery () {
970 var searchText = $scope.searchText || '';
971 var term = searchText.toLowerCase();
973 // If caching is enabled and the current searchText is stored in the cache
974 if (!$scope.noCache && cache[term]) {
975 // The results should be handled as same as a normal un-cached request does.
976 handleResults(cache[term]);
978 fetchResults(searchText);
981 ctrl.hidden = shouldHide();
985 * Handles the retrieved results by showing them in the autocompletes dropdown.
986 * @param results Retrieved results
988 function handleResults(results) {
989 ctrl.matches = results;
990 ctrl.hidden = shouldHide();
992 // If loading is in progress, then we'll end the progress. This is needed for example,
993 // when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
994 if (ctrl.loading) setLoading(false);
996 if ($scope.selectOnMatch) selectItemOnMatch();
999 reportMessages(true, ReportType.Count);
1003 * If there is only one matching item and the search text matches its display value exactly,
1004 * automatically select that item. Note: This function is only called if the user uses the
1005 * `md-select-on-match` flag.
1007 function selectItemOnMatch () {
1008 var searchText = $scope.searchText,
1009 matches = ctrl.matches,
1010 item = matches[ 0 ];
1011 if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
1012 var isMatching = searchText == displayValue;
1013 if ($scope.matchInsensitive && !isMatching) {
1014 isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
1017 if (isMatching) select(0);
1022 * Evaluates an attribute expression against the parent scope.
1023 * @param {String} attr Name of the attribute to be evaluated.
1024 * @param {Object?} locals Properties to be injected into the evaluation context.
1026 function evalAttr(attr, locals) {
1028 $scope.$parent.$eval($attrs[attr], locals || {});
1035 MdAutocomplete['$inject'] = ["$$mdSvgRegistry"];angular
1036 .module('material.components.autocomplete')
1037 .directive('mdAutocomplete', MdAutocomplete);
1041 * @name mdAutocomplete
1042 * @module material.components.autocomplete
1045 * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
1046 * custom query. This component allows you to provide real-time suggestions as the user types
1047 * in the input area.
1049 * To start, you will need to specify the required parameters and provide a template for your
1050 * results. The content inside `md-autocomplete` will be treated as a template.
1052 * In more complex cases, you may want to include other content such as a message to display when
1053 * no matches were found. You can do this by wrapping your template in `md-item-template` and
1054 * adding a tag for `md-not-found`. An example of this is shown below.
1056 * To reset the displayed value you must clear both values for `md-search-text` and `md-selected-item`.
1060 * You can use `ng-messages` to include validation the same way that you would normally validate;
1061 * however, if you want to replicate a standard input with a floating label, you will have to
1064 * - Make sure that your template is wrapped in `md-item-template`
1065 * - Add your `ng-messages` code inside of `md-autocomplete`
1066 * - Add your validation properties to `md-autocomplete` (ie. `required`)
1067 * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
1069 * There is an example below of how this should look.
1071 * ### Snapping Drop-Down
1073 * You can cause the autocomplete drop-down to snap to an ancestor element by applying the
1074 * `md-autocomplete-snap` attribute to that element. You can also snap to the width of
1075 * the `md-autocomplete-snap` element by setting the attribute's value to `width`
1076 * (ie. `md-autocomplete-snap="width"`).
1080 * **Autocomplete Dropdown Items Rendering**
1082 * The `md-autocomplete` uses the the <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeat</a>
1083 * directive for displaying the results inside of the dropdown.<br/>
1085 * > When encountering issues regarding the item template please take a look at the
1086 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> documentation.
1088 * **Autocomplete inside of a Virtual Repeat**
1090 * When using the `md-autocomplete` directive inside of a
1091 * <a ng-href="api/directive/mdVirtualRepeatContainer">VirtualRepeatContainer</a> the dropdown items might
1092 * not update properly, because caching of the results is enabled by default.
1094 * The autocomplete will then show invalid dropdown items, because the VirtualRepeat only updates the
1095 * scope bindings, rather than re-creating the `md-autocomplete` and the previous cached results will be used.
1097 * > To avoid such problems ensure that the autocomplete does not cache any results.
1099 * <hljs lang="html">
1101 * md-no-cache="true"
1102 * md-selected-item="selectedItem"
1103 * md-items="item in items"
1104 * md-search-text="searchText"
1105 * md-item-text="item.display">
1106 * <span>{{ item.display }}</span>
1107 * </md-autocomplete>
1112 * @param {expression} md-items An expression in the format of `item in results` to iterate over
1113 * matches for your search.<br/><br/>
1114 * The `results` expression can be also a function, which returns the results synchronously
1115 * or asynchronously (per Promise)
1116 * @param {expression=} md-selected-item-change An expression to be run each time a new item is
1118 * @param {expression=} md-search-text-change An expression to be run each time the search text
1120 * @param {expression=} md-search-text A model to bind the search query text to
1121 * @param {object=} md-selected-item A model to bind the selected item to
1122 * @param {expression=} md-item-text An expression that will convert your object to a single string.
1123 * @param {string=} placeholder Placeholder text that will be forwarded to the input.
1124 * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
1125 * @param {boolean=} ng-disabled Determines whether or not to disable the input field
1126 * @param {boolean=} md-require-match When set to true, the autocomplete will add a validator,
1127 * which will evaluate to false, when no item is currently selected.
1128 * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
1130 * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
1132 * @param {boolean=} md-clear-button Whether the clear button for the autocomplete input should show up or not.
1133 * @param {boolean=} md-autofocus If true, the autocomplete will be automatically focused when a `$mdDialog`,
1134 * `$mdBottomsheet` or `$mdSidenav`, which contains the autocomplete, is opening. <br/><br/>
1135 * Also the autocomplete will immediately focus the input element.
1136 * @param {boolean=} md-no-asterisk When present, asterisk will not be appended to the floating label
1137 * @param {boolean=} md-autoselect If set to true, the first item will be automatically selected
1138 * in the dropdown upon open.
1139 * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
1140 * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
1141 * `md-input-container`
1142 * @param {string=} md-input-name The name attribute given to the input element to be used with
1144 * @param {string=} md-select-on-focus When present the inputs text will be automatically selected
1146 * @param {string=} md-input-id An ID to be added to the input element
1147 * @param {number=} md-input-minlength The minimum length for the input's value for validation
1148 * @param {number=} md-input-maxlength The maximum length for the input's value for validation
1149 * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
1150 * the item if the search text is an exact match. <br/><br/>
1151 * Exact match means that there is only one match showing up.
1152 * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
1153 * will select on case-insensitive match
1154 * @param {string=} md-escape-options Override escape key logic. Default is `blur clear`.<br/>
1155 * Options: `blur | clear`, `none`
1156 * @param {string=} md-dropdown-items Specifies the maximum amount of items to be shown in
1157 * the dropdown.<br/><br/>
1158 * When the dropdown doesn't fit into the viewport, the dropdown will shrink
1159 * as less as possible.
1160 * @param {string=} md-dropdown-position Overrides the default dropdown position. Options: `top`, `bottom`.
1161 * @param {string=} ng-trim If set to false, the search text will be not trimmed automatically.
1163 * @param {string=} ng-pattern Adds the pattern validator to the ngModel of the search text.
1164 * [ngPattern Directive](https://docs.angularjs.org/api/ng/directive/ngPattern)
1168 * <hljs lang="html">
1170 * md-selected-item="selectedItem"
1171 * md-search-text="searchText"
1172 * md-items="item in getMatches(searchText)"
1173 * md-item-text="item.display">
1174 * <span md-highlight-text="searchText">{{item.display}}</span>
1175 * </md-autocomplete>
1178 * ### Example with "not found" message
1179 * <hljs lang="html">
1181 * md-selected-item="selectedItem"
1182 * md-search-text="searchText"
1183 * md-items="item in getMatches(searchText)"
1184 * md-item-text="item.display">
1185 * <md-item-template>
1186 * <span md-highlight-text="searchText">{{item.display}}</span>
1187 * </md-item-template>
1191 * </md-autocomplete>
1194 * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
1195 * different parts that make up our component.
1197 * ### Clear button for the input
1198 * By default, for floating label autocomplete's the clear button is not showing up
1199 * ([See specs](https://material.google.com/components/text-fields.html#text-fields-auto-complete-text-field))
1201 * Nevertheless, developers are able to explicitly toggle the clear button for all types of autocomplete's.
1203 * <hljs lang="html">
1204 * <md-autocomplete ... md-clear-button="true"></md-autocomplete>
1205 * <md-autocomplete ... md-clear-button="false"></md-autocomplete>
1208 * ### Example with validation
1209 * <hljs lang="html">
1210 * <form name="autocompleteForm">
1213 * md-input-name="autocomplete"
1214 * md-selected-item="selectedItem"
1215 * md-search-text="searchText"
1216 * md-items="item in getMatches(searchText)"
1217 * md-item-text="item.display">
1218 * <md-item-template>
1219 * <span md-highlight-text="searchText">{{item.display}}</span>
1220 * </md-item-template>
1221 * <div ng-messages="autocompleteForm.autocomplete.$error">
1222 * <div ng-message="required">This field is required</div>
1224 * </md-autocomplete>
1228 * In this example, our code utilizes `md-item-template` and `ng-messages` to specify
1229 * input validation for the field.
1231 * ### Asynchronous Results
1232 * The autocomplete items expression also supports promises, which will resolve with the query results.
1235 * function AppController($scope, $http) {
1236 * $scope.query = function(searchText) {
1238 * .get(BACKEND_URL + '/items/' + searchText)
1239 * .then(function(data) {
1240 * // Map the response object to the data object.
1247 * <hljs lang="html">
1249 * md-selected-item="selectedItem"
1250 * md-search-text="searchText"
1251 * md-items="item in query(searchText)">
1252 * <md-item-template>
1253 * <span md-highlight-text="searchText">{{item}}</span>
1254 * </md-item-template>
1255 * </md-autocomplete>
1260 function MdAutocomplete ($$mdSvgRegistry) {
1263 controller: 'MdAutocompleteCtrl',
1264 controllerAs: '$mdAutocompleteCtrl',
1266 inputName: '@mdInputName',
1267 inputMinlength: '@mdInputMinlength',
1268 inputMaxlength: '@mdInputMaxlength',
1269 searchText: '=?mdSearchText',
1270 selectedItem: '=?mdSelectedItem',
1271 itemsExpr: '@mdItems',
1272 itemText: '&mdItemText',
1273 placeholder: '@placeholder',
1274 noCache: '=?mdNoCache',
1275 requireMatch: '=?mdRequireMatch',
1276 selectOnMatch: '=?mdSelectOnMatch',
1277 matchInsensitive: '=?mdMatchCaseInsensitive',
1278 itemChange: '&?mdSelectedItemChange',
1279 textChange: '&?mdSearchTextChange',
1280 minLength: '=?mdMinLength',
1282 autofocus: '=?mdAutofocus',
1283 floatingLabel: '@?mdFloatingLabel',
1284 autoselect: '=?mdAutoselect',
1285 menuClass: '@?mdMenuClass',
1286 inputId: '@?mdInputId',
1287 escapeOptions: '@?mdEscapeOptions',
1288 dropdownItems: '=?mdDropdownItems',
1289 dropdownPosition: '@?mdDropdownPosition',
1290 clearButton: '=?mdClearButton'
1292 compile: function(tElement, tAttrs) {
1293 var attributes = ['md-select-on-focus', 'md-no-asterisk', 'ng-trim', 'ng-pattern'];
1294 var input = tElement.find('input');
1296 attributes.forEach(function(attribute) {
1297 var attrValue = tAttrs[tAttrs.$normalize(attribute)];
1299 if (attrValue !== null) {
1300 input.attr(attribute, attrValue);
1304 return function(scope, element, attrs, ctrl) {
1305 // Retrieve the state of using a md-not-found template by using our attribute, which will
1306 // be added to the element in the template function.
1307 ctrl.hasNotFound = !!element.attr('md-has-not-found');
1309 // By default the inset autocomplete should show the clear button when not explicitly overwritten.
1310 if (!angular.isDefined(attrs.mdClearButton) && !scope.floatingLabel) {
1311 scope.clearButton = true;
1315 template: function (element, attr) {
1316 var noItemsTemplate = getNoItemsTemplate(),
1317 itemTemplate = getItemTemplate(),
1318 leftover = element.html(),
1319 tabindex = attr.tabindex;
1321 // Set our attribute for the link function above which runs later.
1322 // We will set an attribute, because otherwise the stored variables will be trashed when
1323 // removing the element is hidden while retrieving the template. For example when using ngIf.
1324 if (noItemsTemplate) element.attr('md-has-not-found', true);
1326 // Always set our tabindex of the autocomplete directive to -1, because our input
1327 // will hold the actual tabindex.
1328 element.attr('tabindex', '-1');
1331 <md-autocomplete-wrap\
1332 ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \
1333 \'md-menu-showing\': !$mdAutocompleteCtrl.hidden, \
1334 \'md-show-clear-button\': !!clearButton }">\
1335 ' + getInputElement() + '\
1336 ' + getClearButton() + '\
1337 <md-progress-linear\
1338 class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
1339 ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
1340 md-mode="indeterminate"></md-progress-linear>\
1341 <md-virtual-repeat-container\
1343 md-auto-shrink-min="1"\
1344 ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
1345 ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
1346 ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
1347 ng-hide="$mdAutocompleteCtrl.hidden"\
1348 class="md-autocomplete-suggestions-container md-whiteframe-z1"\
1349 ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
1350 role="presentation">\
1351 <ul class="md-autocomplete-suggestions"\
1352 ng-class="::menuClass"\
1353 id="ul-{{$mdAutocompleteCtrl.id}}">\
1354 <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
1355 ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
1356 ng-click="$mdAutocompleteCtrl.select($index)"\
1357 md-extra-name="$mdAutocompleteCtrl.itemName">\
1358 ' + itemTemplate + '\
1359 </li>' + noItemsTemplate + '\
1361 </md-virtual-repeat-container>\
1362 </md-autocomplete-wrap>';
1364 function getItemTemplate() {
1365 var templateTag = element.find('md-item-template').detach(),
1366 html = templateTag.length ? templateTag.html() : element.html();
1367 if (!templateTag.length) element.empty();
1368 return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
1371 function getNoItemsTemplate() {
1372 var templateTag = element.find('md-not-found').detach(),
1373 template = templateTag.length ? templateTag.html() : '';
1375 ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
1376 md-autocomplete-parent-scope>' + template + '</li>'
1381 function getInputElement () {
1382 if (attr.mdFloatingLabel) {
1384 <md-input-container ng-if="floatingLabel">\
1385 <label>{{floatingLabel}}</label>\
1386 <input type="search"\
1387 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1388 id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
1389 name="{{inputName}}"\
1391 ng-required="$mdAutocompleteCtrl.isRequired"\
1392 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1393 ng-minlength="inputMinlength"\
1394 ng-maxlength="inputMaxlength"\
1395 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1396 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1397 ng-model-options="{ allowInvalid: true }"\
1398 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1399 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1400 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1401 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
1402 aria-label="{{floatingLabel}}"\
1403 aria-autocomplete="list"\
1405 aria-haspopup="true"\
1406 aria-activedescendant=""\
1407 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
1408 <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
1409 </md-input-container>';
1412 <input type="search"\
1413 ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
1414 id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
1415 name="{{inputName}}"\
1416 ng-if="!floatingLabel"\
1418 ng-required="$mdAutocompleteCtrl.isRequired"\
1419 ng-disabled="$mdAutocompleteCtrl.isDisabled"\
1420 ng-readonly="$mdAutocompleteCtrl.isReadonly"\
1421 ng-minlength="inputMinlength"\
1422 ng-maxlength="inputMaxlength"\
1423 ng-model="$mdAutocompleteCtrl.scope.searchText"\
1424 ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
1425 ng-blur="$mdAutocompleteCtrl.blur($event)"\
1426 ng-focus="$mdAutocompleteCtrl.focus($event)"\
1427 placeholder="{{placeholder}}"\
1428 aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
1429 aria-label="{{placeholder}}"\
1430 aria-autocomplete="list"\
1432 aria-haspopup="true"\
1433 aria-activedescendant=""\
1434 aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>';
1438 function getClearButton() {
1442 'aria-label="Clear Input" ' +
1444 'ng-if="clearButton && $mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled" ' +
1445 'ng-click="$mdAutocompleteCtrl.clear($event)">' +
1446 '<md-icon md-svg-src="' + $$mdSvgRegistry.mdClose + '"></md-icon>' +
1454 MdAutocompleteItemScopeDirective['$inject'] = ["$compile", "$mdUtil"];angular
1455 .module('material.components.autocomplete')
1456 .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
1458 function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
1463 transclude: 'element'
1466 function compile(tElement, tAttr, transclude) {
1467 return function postLink(scope, element, attr) {
1468 var ctrl = scope.$mdAutocompleteCtrl;
1469 var newScope = ctrl.parent.$new();
1470 var itemName = ctrl.itemName;
1472 // Watch for changes to our scope's variables and copy them to the new scope
1473 watchVariable('$index', '$index');
1474 watchVariable('item', itemName);
1476 // Ensure that $digest calls on our scope trigger $digest on newScope.
1479 // Link the element against newScope.
1480 transclude(newScope, function(clone) {
1481 element.after(clone);
1485 * Creates a watcher for variables that are copied from the parent scope
1489 function watchVariable(variable, alias) {
1490 newScope[alias] = scope[variable];
1492 scope.$watch(variable, function(value) {
1493 $mdUtil.nextTick(function() {
1494 newScope[alias] = value;
1500 * Creates watchers on scope and newScope that ensure that for any
1501 * $digest of scope, newScope is also $digested.
1503 function connectScopes() {
1504 var scopeDigesting = false;
1505 var newScopeDigesting = false;
1507 scope.$watch(function() {
1508 if (newScopeDigesting || scopeDigesting) {
1512 scopeDigesting = true;
1513 scope.$$postDigest(function() {
1514 if (!newScopeDigesting) {
1518 scopeDigesting = newScopeDigesting = false;
1522 newScope.$watch(function() {
1523 newScopeDigesting = true;
1530 MdHighlightCtrl['$inject'] = ["$scope", "$element", "$attrs"];angular
1531 .module('material.components.autocomplete')
1532 .controller('MdHighlightCtrl', MdHighlightCtrl);
1534 function MdHighlightCtrl ($scope, $element, $attrs) {
1535 this.$scope = $scope;
1536 this.$element = $element;
1537 this.$attrs = $attrs;
1539 // Cache the Regex to avoid rebuilding each time.
1543 MdHighlightCtrl.prototype.init = function(unsafeTermFn, unsafeContentFn) {
1545 this.flags = this.$attrs.mdHighlightFlags || '';
1547 this.unregisterFn = this.$scope.$watch(function($scope) {
1549 term: unsafeTermFn($scope),
1550 contentText: unsafeContentFn($scope)
1552 }.bind(this), this.onRender.bind(this), true);
1554 this.$element.on('$destroy', this.unregisterFn);
1558 * Triggered once a new change has been recognized and the highlighted
1559 * text needs to be updated.
1561 MdHighlightCtrl.prototype.onRender = function(state, prevState) {
1563 var contentText = state.contentText;
1565 /* Update the regex if it's outdated, because we don't want to rebuilt it constantly. */
1566 if (this.regex === null || state.term !== prevState.term) {
1567 this.regex = this.createRegex(state.term, this.flags);
1570 /* If a term is available apply the regex to the content */
1572 this.applyRegex(contentText);
1574 this.$element.text(contentText);
1580 * Decomposes the specified text into different tokens (whether match or not).
1581 * Breaking down the string guarantees proper XSS protection due to the native browser
1582 * escaping of unsafe text.
1584 MdHighlightCtrl.prototype.applyRegex = function(text) {
1585 var tokens = this.resolveTokens(text);
1587 this.$element.empty();
1589 tokens.forEach(function (token) {
1591 if (token.isMatch) {
1592 var tokenEl = angular.element('<span class="highlight">').text(token.text);
1594 this.$element.append(tokenEl);
1596 this.$element.append(document.createTextNode(token));
1604 * Decomposes the specified text into different tokens by running the regex against the text.
1606 MdHighlightCtrl.prototype.resolveTokens = function(string) {
1610 // Use replace here, because it supports global and single regular expressions at same time.
1611 string.replace(this.regex, function(match, index) {
1612 appendToken(lastIndex, index);
1619 lastIndex = index + match.length;
1622 // Append the missing text as a token.
1623 appendToken(lastIndex);
1627 function appendToken(from, to) {
1628 var targetText = string.slice(from, to);
1629 targetText && tokens.push(targetText);
1633 /** Creates a regex for the specified text with the given flags. */
1634 MdHighlightCtrl.prototype.createRegex = function(term, flags) {
1635 var startFlag = '', endFlag = '';
1636 var regexTerm = this.sanitizeRegex(term);
1638 if (flags.indexOf('^') >= 0) startFlag = '^';
1639 if (flags.indexOf('$') >= 0) endFlag = '$';
1641 return new RegExp(startFlag + regexTerm + endFlag, flags.replace(/[$\^]/g, ''));
1644 /** Sanitizes a regex by removing all common RegExp identifiers */
1645 MdHighlightCtrl.prototype.sanitizeRegex = function(term) {
1646 return term && term.toString().replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
1650 MdHighlight['$inject'] = ["$interpolate", "$parse"];angular
1651 .module('material.components.autocomplete')
1652 .directive('mdHighlightText', MdHighlight);
1656 * @name mdHighlightText
1657 * @module material.components.autocomplete
1660 * The `md-highlight-text` directive allows you to specify text that should be highlighted within
1661 * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
1662 * be styled through CSS. Please note that child elements may not be used with this directive.
1664 * @param {string} md-highlight-text A model to be searched for
1665 * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
1666 * #### **Supported flags**:
1667 * - `g`: Find all matches within the provided text
1668 * - `i`: Ignore case when searching for matches
1669 * - `$`: Only match if the text ends with the search term
1670 * - `^`: Only match if the text begins with the search term
1673 * <hljs lang="html">
1674 * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
1676 * <li ng-repeat="result in results" md-highlight-text="searchTerm">
1683 function MdHighlight ($interpolate, $parse) {
1686 controller: 'MdHighlightCtrl',
1687 compile: function mdHighlightCompile(tElement, tAttr) {
1688 var termExpr = $parse(tAttr.mdHighlightText);
1689 var unsafeContentExpr = $interpolate(tElement.html());
1691 return function mdHighlightLink(scope, element, attr, ctrl) {
1692 ctrl.init(termExpr, unsafeContentExpr);
1698 ngmaterial.components.autocomplete = angular.module("material.components.autocomplete");