1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150 |
- /*!
- * Angular Material Design
- * https://github.com/angular/material
- * @license MIT
- * v0.11.4
- */
- (function( window, angular, undefined ){
- "use strict";
- /**
- * @ngdoc module
- * @name material.components.autocomplete
- */
- /*
- * @see js folder for autocomplete implementation
- */
- angular.module('material.components.autocomplete', [
- 'material.core',
- 'material.components.icon',
- 'material.components.virtualRepeat'
- ]);
- angular
- .module('material.components.autocomplete')
- .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
- var ITEM_HEIGHT = 41,
- MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
- MENU_PADDING = 8;
- function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
- $animate, $rootElement, $attrs, $q) {
- //-- private variables
- var ctrl = this,
- itemParts = $scope.itemsExpr.split(/ in /i),
- itemExpr = itemParts[ 1 ],
- elements = null,
- cache = {},
- noBlur = false,
- selectedItemWatchers = [],
- hasFocus = false,
- lastCount = 0;
- //-- public variables with handlers
- defineProperty('hidden', handleHiddenChange, true);
- //-- public variables
- ctrl.scope = $scope;
- ctrl.parent = $scope.$parent;
- ctrl.itemName = itemParts[ 0 ];
- ctrl.matches = [];
- ctrl.loading = false;
- ctrl.hidden = true;
- ctrl.index = null;
- ctrl.messages = [];
- ctrl.id = $mdUtil.nextUid();
- ctrl.isDisabled = null;
- ctrl.isRequired = null;
- ctrl.hasNotFound = false;
- //-- public methods
- ctrl.keydown = keydown;
- ctrl.blur = blur;
- ctrl.focus = focus;
- ctrl.clear = clearValue;
- ctrl.select = select;
- ctrl.listEnter = onListEnter;
- ctrl.listLeave = onListLeave;
- ctrl.mouseUp = onMouseup;
- ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
- ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
- ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
- ctrl.notFoundVisible = notFoundVisible;
- ctrl.loadingIsVisible = loadingIsVisible;
- return init();
- //-- initialization methods
- /**
- * Initialize the controller, setup watchers, gather elements
- */
- function init () {
- $mdUtil.initOptionalProperties($scope, $attrs, { searchText: null, selectedItem: null });
- $mdTheming($element);
- configureWatchers();
- $mdUtil.nextTick(function () {
- gatherElements();
- moveDropdown();
- focusElement();
- $element.on('focus', focusElement);
- });
- }
- /**
- * Calculates the dropdown's position and applies the new styles to the menu element
- * @returns {*}
- */
- function positionDropdown () {
- if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
- var hrect = elements.wrap.getBoundingClientRect(),
- vrect = elements.snap.getBoundingClientRect(),
- root = elements.root.getBoundingClientRect(),
- top = vrect.bottom - root.top,
- bot = root.bottom - vrect.top,
- left = hrect.left - root.left,
- width = hrect.width,
- styles = {
- left: left + 'px',
- minWidth: width + 'px',
- maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
- };
- if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
- styles.top = 'auto';
- styles.bottom = bot + 'px';
- styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
- } else {
- styles.top = top + 'px';
- styles.bottom = 'auto';
- styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom - hrect.bottom - MENU_PADDING) + 'px';
- }
- elements.$.scrollContainer.css(styles);
- $mdUtil.nextTick(correctHorizontalAlignment, false);
- /**
- * Makes sure that the menu doesn't go off of the screen on either side.
- */
- function correctHorizontalAlignment () {
- var dropdown = elements.scrollContainer.getBoundingClientRect(),
- styles = {};
- if (dropdown.right > root.right - MENU_PADDING) {
- styles.left = (hrect.right - dropdown.width) + 'px';
- }
- elements.$.scrollContainer.css(styles);
- }
- }
- /**
- * Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
- */
- function moveDropdown () {
- if (!elements.$.root.length) return;
- $mdTheming(elements.$.scrollContainer);
- elements.$.scrollContainer.detach();
- elements.$.root.append(elements.$.scrollContainer);
- if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
- }
- /**
- * Sends focus to the input element.
- */
- function focusElement () {
- if ($scope.autofocus) elements.input.focus();
- }
- /**
- * Sets up any watchers used by autocomplete
- */
- function configureWatchers () {
- var wait = parseInt($scope.delay, 10) || 0;
- $attrs.$observe('disabled', function (value) { ctrl.isDisabled = value; });
- $attrs.$observe('required', function (value) { ctrl.isRequired = value !== null; });
- $scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
- $scope.$watch('selectedItem', selectedItemChange);
- angular.element($window).on('resize', positionDropdown);
- $scope.$on('$destroy', cleanup);
- }
- /**
- * Removes any events or leftover elements created by this controller
- */
- function cleanup () {
- angular.element($window).off('resize', positionDropdown);
- if ( elements ){
- var items = 'ul scroller scrollContainer input'.split(' ');
- angular.forEach(items, function(key){
- elements.$[key].remove();
- });
- }
- }
- /**
- * Gathers all of the elements needed for this controller
- */
- function gatherElements () {
- elements = {
- main: $element[0],
- scrollContainer: $element[0].getElementsByClassName('md-virtual-repeat-container')[0],
- scroller: $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0],
- ul: $element.find('ul')[0],
- input: $element.find('input')[0],
- wrap: $element.find('md-autocomplete-wrap')[0],
- root: document.body
- };
- elements.li = elements.ul.getElementsByTagName('li');
- elements.snap = getSnapTarget();
- elements.$ = getAngularElements(elements);
- }
- /**
- * Finds the element that the menu will base its position on
- * @returns {*}
- */
- function getSnapTarget () {
- for (var element = $element; element.length; element = element.parent()) {
- if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
- }
- return elements.wrap;
- }
- /**
- * Gathers angular-wrapped versions of each element
- * @param elements
- * @returns {{}}
- */
- function getAngularElements (elements) {
- var obj = {};
- for (var key in elements) {
- if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
- }
- return obj;
- }
- //-- event/change handlers
- /**
- * Handles changes to the `hidden` property.
- * @param hidden
- * @param oldHidden
- */
- function handleHiddenChange (hidden, oldHidden) {
- if (!hidden && oldHidden) {
- positionDropdown();
- if (elements) {
- $mdUtil.nextTick(function () {
- $mdUtil.disableScrollAround(elements.ul);
- }, false, $scope);
- }
- } else if (hidden && !oldHidden) {
- $mdUtil.nextTick(function () {
- $mdUtil.enableScrolling();
- }, false, $scope);
- }
- }
- /**
- * When the user mouses over the dropdown menu, ignore blur events.
- */
- function onListEnter () {
- noBlur = true;
- }
- /**
- * When the user's mouse leaves the menu, blur events may hide the menu again.
- */
- function onListLeave () {
- noBlur = false;
- ctrl.hidden = shouldHide();
- }
- /**
- * When the mouse button is released, send focus back to the input field.
- */
- function onMouseup () {
- elements.input.focus();
- }
- /**
- * Handles changes to the selected item.
- * @param selectedItem
- * @param previousSelectedItem
- */
- function selectedItemChange (selectedItem, previousSelectedItem) {
- if (selectedItem) {
- getDisplayValue(selectedItem).then(function (val) {
- $scope.searchText = val;
- handleSelectedItemChange(selectedItem, previousSelectedItem);
- });
- }
- if (selectedItem !== previousSelectedItem) announceItemChange();
- }
- /**
- * Use the user-defined expression to announce changes each time a new item is selected
- */
- function announceItemChange () {
- angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
- }
- /**
- * Use the user-defined expression to announce changes each time the search text is changed
- */
- function announceTextChange () {
- angular.isFunction($scope.textChange) && $scope.textChange();
- }
- /**
- * Calls any external watchers listening for the selected item. Used in conjunction with
- * `registerSelectedItemWatcher`.
- * @param selectedItem
- * @param previousSelectedItem
- */
- function handleSelectedItemChange (selectedItem, previousSelectedItem) {
- selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
- }
- /**
- * Register a function to be called when the selected item changes.
- * @param cb
- */
- function registerSelectedItemWatcher (cb) {
- if (selectedItemWatchers.indexOf(cb) == -1) {
- selectedItemWatchers.push(cb);
- }
- }
- /**
- * Unregister a function previously registered for selected item changes.
- * @param cb
- */
- function unregisterSelectedItemWatcher (cb) {
- var i = selectedItemWatchers.indexOf(cb);
- if (i != -1) {
- selectedItemWatchers.splice(i, 1);
- }
- }
- /**
- * Handles changes to the searchText property.
- * @param searchText
- * @param previousSearchText
- */
- function handleSearchText (searchText, previousSearchText) {
- ctrl.index = getDefaultIndex();
- // do nothing on init
- if (searchText === previousSearchText) return;
- getDisplayValue($scope.selectedItem).then(function (val) {
- // clear selected item if search text no longer matches it
- if (searchText !== val) {
- $scope.selectedItem = null;
- // trigger change event if available
- if (searchText !== previousSearchText) announceTextChange();
- // cancel results if search text is not long enough
- if (!isMinLengthMet()) {
- ctrl.matches = [];
- setLoading(false);
- updateMessages();
- } else {
- handleQuery();
- }
- }
- });
- }
- /**
- * Handles input blur event, determines if the dropdown should hide.
- */
- function blur () {
- if (!noBlur) {
- hasFocus = false;
- ctrl.hidden = shouldHide();
- }
- }
- function doBlur(forceBlur) {
- if (forceBlur) {
- noBlur = false;
- }
- elements.input.blur();
- }
- /**
- * Handles input focus event, determines if the dropdown should show.
- */
- function focus () {
- hasFocus = true;
- //-- if searchText is null, let's force it to be a string
- if (!angular.isString($scope.searchText)) $scope.searchText = '';
- ctrl.hidden = shouldHide();
- if (!ctrl.hidden) handleQuery();
- }
- /**
- * Handles keyboard input.
- * @param event
- */
- function keydown (event) {
- switch (event.keyCode) {
- case $mdConstant.KEY_CODE.DOWN_ARROW:
- if (ctrl.loading) return;
- event.stopPropagation();
- event.preventDefault();
- ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
- updateScroll();
- updateMessages();
- break;
- case $mdConstant.KEY_CODE.UP_ARROW:
- if (ctrl.loading) return;
- event.stopPropagation();
- event.preventDefault();
- ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
- updateScroll();
- updateMessages();
- break;
- case $mdConstant.KEY_CODE.TAB:
- case $mdConstant.KEY_CODE.ENTER:
- if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
- event.stopPropagation();
- event.preventDefault();
- select(ctrl.index);
- break;
- case $mdConstant.KEY_CODE.ESCAPE:
- event.stopPropagation();
- event.preventDefault();
- clearValue();
- // Force the component to blur if they hit escape
- doBlur(true);
- break;
- default:
- }
- }
- //-- getters
- /**
- * Returns the minimum length needed to display the dropdown.
- * @returns {*}
- */
- function getMinLength () {
- return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
- }
- /**
- * Returns the display value for an item.
- * @param item
- * @returns {*}
- */
- function getDisplayValue (item) {
- return $q.when(getItemText(item) || item);
- /**
- * Getter function to invoke user-defined expression (in the directive)
- * to convert your object to a single string.
- */
- function getItemText (item) {
- return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
- }
- }
- /**
- * Returns the locals object for compiling item templates.
- * @param item
- * @returns {{}}
- */
- function getItemAsNameVal (item) {
- if (!item) return undefined;
- var locals = {};
- if (ctrl.itemName) locals[ ctrl.itemName ] = item;
- return locals;
- }
- /**
- * Returns the default index based on whether or not autoselect is enabled.
- * @returns {number}
- */
- function getDefaultIndex () {
- return $scope.autoselect ? 0 : -1;
- }
- /**
- * Sets the loading parameter and updates the hidden state.
- * @param value {boolean} Whether or not the component is currently loading.
- */
- function setLoading(value) {
- if (ctrl.loading != value) {
- ctrl.loading = value;
- }
- // Always refresh the hidden variable as something else might have changed
- ctrl.hidden = shouldHide();
- }
- /**
- * Determines if the menu should be hidden.
- * @returns {boolean}
- */
- function shouldHide () {
- if ((ctrl.loading && !hasMatches()) || hasSelection() || !hasFocus) {
- return true;
- }
- return !shouldShow();
- }
- /**
- * Determines if the menu should be shown.
- * @returns {boolean}
- */
- function shouldShow() {
- return (isMinLengthMet() && hasMatches()) || notFoundVisible();
- }
- /**
- * Returns true if the search text has matches.
- * @returns {boolean}
- */
- function hasMatches() {
- return ctrl.matches.length ? true : false;
- }
- /**
- * Returns true if the autocomplete has a valid selection.
- * @returns {boolean}
- */
- function hasSelection() {
- return ctrl.scope.selectedItem ? true : false;
- }
- /**
- * Returns true if the loading indicator is, or should be, visible.
- * @returns {boolean}
- */
- function loadingIsVisible() {
- return ctrl.loading && !hasSelection();
- }
- /**
- * Returns the display value of the current item.
- * @returns {*}
- */
- function getCurrentDisplayValue () {
- return getDisplayValue(ctrl.matches[ ctrl.index ]);
- }
- /**
- * Determines if the minimum length is met by the search text.
- * @returns {*}
- */
- function isMinLengthMet () {
- return ($scope.searchText || '').length >= getMinLength();
- }
- //-- actions
- /**
- * Defines a public property with a handler and a default value.
- * @param key
- * @param handler
- * @param value
- */
- function defineProperty (key, handler, value) {
- Object.defineProperty(ctrl, key, {
- get: function () { return value; },
- set: function (newValue) {
- var oldValue = value;
- value = newValue;
- handler(newValue, oldValue);
- }
- });
- }
- /**
- * Selects the item at the given index.
- * @param index
- */
- function select (index) {
- //-- force form to update state for validation
- $mdUtil.nextTick(function () {
- getDisplayValue(ctrl.matches[ index ]).then(function (val) {
- var ngModel = elements.$.input.controller('ngModel');
- ngModel.$setViewValue(val);
- ngModel.$render();
- }).finally(function () {
- $scope.selectedItem = ctrl.matches[ index ];
- setLoading(false);
- });
- }, false);
- }
- /**
- * Clears the searchText value and selected item.
- */
- function clearValue () {
- // Set the loading to true so we don't see flashes of content
- setLoading(true);
- // Reset our variables
- ctrl.index = 0;
- ctrl.matches = [];
- $scope.searchText = '';
- // Tell the select to fire and select nothing
- select(-1);
- // Per http://www.w3schools.com/jsref/event_oninput.asp
- var eventObj = document.createEvent('CustomEvent');
- eventObj.initCustomEvent('input', true, true, { value: $scope.searchText });
- elements.input.dispatchEvent(eventObj);
- elements.input.focus();
- }
- /**
- * Fetches the results for the provided search text.
- * @param searchText
- */
- function fetchResults (searchText) {
- var items = $scope.$parent.$eval(itemExpr),
- term = searchText.toLowerCase();
- if (angular.isArray(items)) {
- handleResults(items);
- } else if (items) {
- setLoading(true);
- $mdUtil.nextTick(function () {
- if (items.success) items.success(handleResults);
- if (items.then) items.then(handleResults);
- if (items.finally) items.finally(function () {
- setLoading(false);
- });
- },true, $scope);
- }
- function handleResults (matches) {
- cache[ term ] = matches;
- if ((searchText || '') !== ($scope.searchText || '')) return; //-- just cache the results if old request
- ctrl.matches = matches;
- ctrl.hidden = shouldHide();
- if ($scope.selectOnMatch) selectItemOnMatch();
- updateMessages();
- positionDropdown();
- }
- }
- /**
- * Updates the ARIA messages
- */
- function updateMessages () {
- getCurrentDisplayValue().then(function (msg) {
- ctrl.messages = [ getCountMessage(), msg ];
- });
- }
- /**
- * Returns the ARIA message for how many results match the current query.
- * @returns {*}
- */
- function getCountMessage () {
- if (lastCount === ctrl.matches.length) return '';
- lastCount = ctrl.matches.length;
- switch (ctrl.matches.length) {
- case 0:
- return 'There are no matches available.';
- case 1:
- return 'There is 1 match available.';
- default:
- return 'There are ' + ctrl.matches.length + ' matches available.';
- }
- }
- /**
- * Makes sure that the focused element is within view.
- */
- function updateScroll () {
- if (!elements.li[0]) return;
- var height = elements.li[0].offsetHeight,
- top = height * ctrl.index,
- bot = top + height,
- hgt = elements.scroller.clientHeight,
- scrollTop = elements.scroller.scrollTop;
- if (top < scrollTop) {
- scrollTo(top);
- } else if (bot > scrollTop + hgt) {
- scrollTo(bot - hgt);
- }
- }
- function scrollTo (offset) {
- elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
- }
- function notFoundVisible () {
- var textLength = (ctrl.scope.searchText || '').length;
- return ctrl.hasNotFound && !hasMatches() && !ctrl.loading && textLength >= getMinLength() && hasFocus && !hasSelection();
- }
- /**
- * Starts the query to gather the results for the current searchText. Attempts to return cached
- * results first, then forwards the process to `fetchResults` if necessary.
- */
- function handleQuery () {
- var searchText = $scope.searchText,
- term = searchText.toLowerCase();
- //-- if results are cached, pull in cached results
- if (!$scope.noCache && cache[ term ]) {
- ctrl.matches = cache[ term ];
- updateMessages();
- } else {
- fetchResults(searchText);
- }
- ctrl.hidden = shouldHide();
- }
- /**
- * If there is only one matching item and the search text matches its display value exactly,
- * automatically select that item. Note: This function is only called if the user uses the
- * `md-select-on-match` flag.
- */
- function selectItemOnMatch () {
- var searchText = $scope.searchText,
- matches = ctrl.matches,
- item = matches[ 0 ];
- if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
- if (searchText == displayValue) select(0);
- });
- }
- }
- MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q"];
- angular
- .module('material.components.autocomplete')
- .directive('mdAutocomplete', MdAutocomplete);
- /**
- * @ngdoc directive
- * @name mdAutocomplete
- * @module material.components.autocomplete
- *
- * @description
- * `<md-autocomplete>` is a special input component with a drop-down of all possible matches to a
- * custom query. This component allows you to provide real-time suggestions as the user types
- * in the input area.
- *
- * To start, you will need to specify the required parameters and provide a template for your
- * results. The content inside `md-autocomplete` will be treated as a template.
- *
- * In more complex cases, you may want to include other content such as a message to display when
- * no matches were found. You can do this by wrapping your template in `md-item-template` and
- * adding a tag for `md-not-found`. An example of this is shown below.
- *
- * ### Validation
- *
- * You can use `ng-messages` to include validation the same way that you would normally validate;
- * however, if you want to replicate a standard input with a floating label, you will have to
- * do the following:
- *
- * - Make sure that your template is wrapped in `md-item-template`
- * - Add your `ng-messages` code inside of `md-autocomplete`
- * - Add your validation properties to `md-autocomplete` (ie. `required`)
- * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
- *
- * There is an example below of how this should look.
- *
- *
- * @param {expression} md-items An expression in the format of `item in items` to iterate over
- * matches for your search.
- * @param {expression=} md-selected-item-change An expression to be run each time a new item is
- * selected
- * @param {expression=} md-search-text-change An expression to be run each time the search text
- * updates
- * @param {expression=} md-search-text A model to bind the search query text to
- * @param {object=} md-selected-item A model to bind the selected item to
- * @param {expression=} md-item-text An expression that will convert your object to a single string.
- * @param {string=} placeholder Placeholder text that will be forwarded to the input.
- * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
- * @param {boolean=} ng-disabled Determines whether or not to disable the input field
- * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
- * make suggestions
- * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
- * for results
- * @param {boolean=} md-autofocus If true, will immediately focus the input element
- * @param {boolean=} md-autoselect If true, the first item will be selected by default
- * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
- * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
- * `md-input-container`
- * @param {string=} md-input-name The name attribute given to the input element to be used with
- * FormController
- * @param {string=} md-input-id An ID to be added to the input element
- * @param {number=} md-input-minlength The minimum length for the input's value for validation
- * @param {number=} md-input-maxlength The maximum length for the input's value for validation
- * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
- * the item if the search text is an exact match
- *
- * @usage
- * ### Basic Example
- * <hljs lang="html">
- * <md-autocomplete
- * md-selected-item="selectedItem"
- * md-search-text="searchText"
- * md-items="item in getMatches(searchText)"
- * md-item-text="item.display">
- * <span md-highlight-text="searchText">{{item.display}}</span>
- * </md-autocomplete>
- * </hljs>
- *
- * ### Example with "not found" message
- * <hljs lang="html">
- * <md-autocomplete
- * md-selected-item="selectedItem"
- * md-search-text="searchText"
- * md-items="item in getMatches(searchText)"
- * md-item-text="item.display">
- * <md-item-template>
- * <span md-highlight-text="searchText">{{item.display}}</span>
- * </md-item-template>
- * <md-not-found>
- * No matches found.
- * </md-not-found>
- * </md-autocomplete>
- * </hljs>
- *
- * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
- * different parts that make up our component.
- *
- * ### Example with validation
- * <hljs lang="html">
- * <form name="autocompleteForm">
- * <md-autocomplete
- * required
- * md-input-name="autocomplete"
- * md-selected-item="selectedItem"
- * md-search-text="searchText"
- * md-items="item in getMatches(searchText)"
- * md-item-text="item.display">
- * <md-item-template>
- * <span md-highlight-text="searchText">{{item.display}}</span>
- * </md-item-template>
- * <div ng-messages="autocompleteForm.autocomplete.$error">
- * <div ng-message="required">This field is required</div>
- * </div>
- * </md-autocomplete>
- * </form>
- * </hljs>
- *
- * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
- * different parts that make up our component.
- */
- function MdAutocomplete () {
- var hasNotFoundTemplate = false;
- return {
- controller: 'MdAutocompleteCtrl',
- controllerAs: '$mdAutocompleteCtrl',
- scope: {
- inputName: '@mdInputName',
- inputMinlength: '@mdInputMinlength',
- inputMaxlength: '@mdInputMaxlength',
- searchText: '=?mdSearchText',
- selectedItem: '=?mdSelectedItem',
- itemsExpr: '@mdItems',
- itemText: '&mdItemText',
- placeholder: '@placeholder',
- noCache: '=?mdNoCache',
- selectOnMatch: '=?mdSelectOnMatch',
- itemChange: '&?mdSelectedItemChange',
- textChange: '&?mdSearchTextChange',
- minLength: '=?mdMinLength',
- delay: '=?mdDelay',
- autofocus: '=?mdAutofocus',
- floatingLabel: '@?mdFloatingLabel',
- autoselect: '=?mdAutoselect',
- menuClass: '@?mdMenuClass',
- inputId: '@?mdInputId'
- },
- link: function(scope, element, attrs, controller) {
- controller.hasNotFound = hasNotFoundTemplate;
- },
- template: function (element, attr) {
- var noItemsTemplate = getNoItemsTemplate(),
- itemTemplate = getItemTemplate(),
- leftover = element.html(),
- tabindex = attr.tabindex;
- if (noItemsTemplate) {
- hasNotFoundTemplate = true;
- }
- if (attr.hasOwnProperty('tabindex')) {
- element.attr('tabindex', '-1');
- }
- return '\
- <md-autocomplete-wrap\
- layout="row"\
- ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }"\
- role="listbox">\
- ' + getInputElement() + '\
- <md-progress-linear\
- ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
- md-mode="indeterminate"></md-progress-linear>\
- <md-virtual-repeat-container\
- md-auto-shrink\
- md-auto-shrink-min="1"\
- ng-hide="$mdAutocompleteCtrl.hidden"\
- class="md-autocomplete-suggestions-container md-whiteframe-z1"\
- role="presentation">\
- <ul class="md-autocomplete-suggestions"\
- ng-class="::menuClass"\
- id="ul-{{$mdAutocompleteCtrl.id}}"\
- ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
- ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
- ng-mouseup="$mdAutocompleteCtrl.mouseUp()">\
- <li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
- ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
- ng-click="$mdAutocompleteCtrl.select($index)"\
- md-extra-name="$mdAutocompleteCtrl.itemName">\
- ' + itemTemplate + '\
- </li>' + noItemsTemplate + '\
- </ul>\
- </md-virtual-repeat-container>\
- </md-autocomplete-wrap>\
- <aria-status\
- class="md-visually-hidden"\
- role="status"\
- aria-live="assertive">\
- <p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\
- </aria-status>';
- function getItemTemplate() {
- var templateTag = element.find('md-item-template').detach(),
- html = templateTag.length ? templateTag.html() : element.html();
- if (!templateTag.length) element.empty();
- return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
- }
- function getNoItemsTemplate() {
- var templateTag = element.find('md-not-found').detach(),
- template = templateTag.length ? templateTag.html() : '';
- return template
- ? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
- md-autocomplete-parent-scope>' + template + '</li>'
- : '';
- }
- function getInputElement () {
- if (attr.mdFloatingLabel) {
- return '\
- <md-input-container flex ng-if="floatingLabel">\
- <label>{{floatingLabel}}</label>\
- <input type="search"\
- ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
- id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
- name="{{inputName}}"\
- autocomplete="off"\
- ng-required="$mdAutocompleteCtrl.isRequired"\
- ng-minlength="inputMinlength"\
- ng-maxlength="inputMaxlength"\
- ng-disabled="$mdAutocompleteCtrl.isDisabled"\
- ng-model="$mdAutocompleteCtrl.scope.searchText"\
- ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
- ng-blur="$mdAutocompleteCtrl.blur()"\
- ng-focus="$mdAutocompleteCtrl.focus()"\
- aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
- aria-label="{{floatingLabel}}"\
- aria-autocomplete="list"\
- aria-haspopup="true"\
- aria-activedescendant=""\
- aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
- <div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
- </md-input-container>';
- } else {
- return '\
- <input flex type="search"\
- ' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
- id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
- name="{{inputName}}"\
- ng-if="!floatingLabel"\
- autocomplete="off"\
- ng-required="$mdAutocompleteCtrl.isRequired"\
- ng-disabled="$mdAutocompleteCtrl.isDisabled"\
- ng-model="$mdAutocompleteCtrl.scope.searchText"\
- ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
- ng-blur="$mdAutocompleteCtrl.blur()"\
- ng-focus="$mdAutocompleteCtrl.focus()"\
- placeholder="{{placeholder}}"\
- aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
- aria-label="{{placeholder}}"\
- aria-autocomplete="list"\
- aria-haspopup="true"\
- aria-activedescendant=""\
- aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
- <button\
- type="button"\
- tabindex="-1"\
- ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
- ng-click="$mdAutocompleteCtrl.clear()">\
- <md-icon md-svg-icon="md-close"></md-icon>\
- <span class="md-visually-hidden">Clear</span>\
- </button>\
- ';
- }
- }
- }
- };
- }
- angular
- .module('material.components.autocomplete')
- .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
- function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
- return {
- restrict: 'AE',
- link: postLink,
- terminal: true
- };
- function postLink(scope, element, attr) {
- var ctrl = scope.$mdAutocompleteCtrl;
- var newScope = ctrl.parent.$new();
- var itemName = ctrl.itemName;
- // Watch for changes to our scope's variables and copy them to the new scope
- watchVariable('$index', '$index');
- watchVariable('item', itemName);
- // Recompile the contents with the new/modified scope
- $compile(element.contents())(newScope);
- // Replace it if required
- if (attr.hasOwnProperty('mdAutocompleteReplace')) {
- element.after(element.contents());
- element.remove();
- }
- /**
- * Creates a watcher for variables that are copied from the parent scope
- * @param variable
- * @param alias
- */
- function watchVariable(variable, alias) {
- newScope[alias] = scope[variable];
- scope.$watch(variable, function(value) {
- $mdUtil.nextTick(function() {
- newScope[alias] = value;
- });
- });
- }
- }
- }
- MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];
- angular
- .module('material.components.autocomplete')
- .controller('MdHighlightCtrl', MdHighlightCtrl);
- function MdHighlightCtrl ($scope, $element, $attrs) {
- this.init = init;
- function init (termExpr, unsafeTextExpr) {
- var text = null,
- regex = null,
- flags = $attrs.mdHighlightFlags || '',
- watcher = $scope.$watch(function($scope) {
- return {
- term: termExpr($scope),
- unsafeText: unsafeTextExpr($scope)
- };
- }, function (state, prevState) {
- if (text === null || state.unsafeText !== prevState.unsafeText) {
- text = angular.element('<div>').text(state.unsafeText).html()
- }
- if (regex === null || state.term !== prevState.term) {
- regex = getRegExp(state.term, flags);
- }
- $element.html(text.replace(regex, '<span class="highlight">$&</span>'));
- }, true);
- $element.on('$destroy', function () { watcher(); });
- }
- function sanitize (term) {
- return term && term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
- }
- function getRegExp (text, flags) {
- var str = '';
- if (flags.indexOf('^') >= 1) str += '^';
- str += text;
- if (flags.indexOf('$') >= 1) str += '$';
- return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, ''));
- }
- }
- MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];
- angular
- .module('material.components.autocomplete')
- .directive('mdHighlightText', MdHighlight);
- /**
- * @ngdoc directive
- * @name mdHighlightText
- * @module material.components.autocomplete
- *
- * @description
- * The `md-highlight-text` directive allows you to specify text that should be highlighted within
- * an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
- * be styled through CSS. Please note that child elements may not be used with this directive.
- *
- * @param {string} md-highlight-text A model to be searched for
- * @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
- * #### **Supported flags**:
- * - `g`: Find all matches within the provided text
- * - `i`: Ignore case when searching for matches
- * - `$`: Only match if the text ends with the search term
- * - `^`: Only match if the text begins with the search term
- *
- * @usage
- * <hljs lang="html">
- * <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
- * <ul>
- * <li ng-repeat="result in results" md-highlight-text="searchTerm">
- * {{result.text}}
- * </li>
- * </ul>
- * </hljs>
- */
- function MdHighlight ($interpolate, $parse) {
- return {
- terminal: true,
- controller: 'MdHighlightCtrl',
- compile: function mdHighlightCompile(tElement, tAttr) {
- var termExpr = $parse(tAttr.mdHighlightText);
- var unsafeTextExpr = $interpolate(tElement.html());
- return function mdHighlightLink(scope, element, attr, ctrl) {
- ctrl.init(termExpr, unsafeTextExpr);
- };
- }
- };
- }
- MdHighlight.$inject = ["$interpolate", "$parse"];
- })(window, window.angular);
|