ledgerrb/public/app/bower_components/angular-material/modules/js/autocomplete/autocomplete.js
Gwenhael Le Moine d5d3e3c31c bower update
2015-08-30 19:52:32 +02:00

1047 lines
No EOL
34 KiB
JavaScript

/*!
* Angular Material Design
* https://github.com/angular/material
* @license MIT
* v0.10.1
*/
(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'
]);
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;
//-- 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;
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();
focusElement();
moveDropdown();
});
}
/**
* Calculates the dropdown's position and applies the new styles to the menu element
* @returns {*}
*/
function positionDropdown () {
if (!elements) return $mdUtil.nextTick(positionDropdown, false);
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.$.ul.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.ul.getBoundingClientRect(),
styles = {};
if (dropdown.right > root.right - MENU_PADDING) {
styles.left = (hrect.right - dropdown.width) + 'px';
}
elements.$.ul.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.$.ul);
elements.$.ul.detach();
elements.$.root.append(elements.$.ul);
if ($animate.pin) $animate.pin(elements.$.ul, $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);
elements.$.ul.remove();
}
/**
* Gathers all of the elements needed for this controller
*/
function gatherElements () {
elements = {
main: $element[ 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);
} else if (hidden && !oldHidden) {
$mdUtil.nextTick(function () { $mdUtil.enableScrolling(); }, false);
}
}
/**
* 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;
if (!hasFocus) ctrl.hidden = true;
}
/**
* 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.loading = false;
ctrl.matches = [];
ctrl.hidden = shouldHide();
updateMessages();
} else {
handleQuery();
}
}
});
}
/**
* Handles input blur event, determines if the dropdown should hide.
*/
function blur () {
hasFocus = false;
if (!noBlur) ctrl.hidden = true;
}
/**
* 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 = '';
if ($scope.minLength > 0) return;
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();
ctrl.matches = [];
ctrl.hidden = true;
ctrl.index = getDefaultIndex();
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;
}
/**
* Determines if the menu should be hidden.
* @returns {boolean}
*/
function shouldHide () {
if (!isMinLengthMet() || !ctrl.matches.length) return true;
}
/**
* 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 angular.isDefined($scope.searchText) && $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 ];
ctrl.loading = false;
ctrl.hidden = true;
ctrl.index = 0;
ctrl.matches = [];
});
}, false);
}
/**
* Clears the searchText value and selected item.
*/
function clearValue () {
$scope.searchText = '';
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) {
$mdUtil.nextTick(function () {
ctrl.loading = true;
if (items.success) items.success(handleResults);
if (items.then) items.then(handleResults);
if (items.finally) items.finally(function () { ctrl.loading = false; });
});
}
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[ ctrl.index ]) return;
var li = elements.li[ ctrl.index ],
top = li.offsetTop,
bot = top + li.offsetHeight,
hgt = elements.ul.clientHeight;
if (top < elements.ul.scrollTop) {
elements.ul.scrollTop = top;
} else if (bot > elements.ul.scrollTop + hgt) {
elements.ul.scrollTop = bot - hgt;
}
}
/**
* 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);
}
if (hasFocus) 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 {string=} 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 {string=} 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 () {
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'
},
template: function (element, attr) {
var noItemsTemplate = getNoItemsTemplate(),
itemTemplate = getItemTemplate(),
leftover = element.html();
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.loading && !$mdAutocompleteCtrl.hidden"\
md-mode="indeterminate"></md-progress-linear>\
<ul role="presentation"\
class="md-autocomplete-suggestions md-whiteframe-z1 {{menuClass || \'\'}}"\
id="ul-{{$mdAutocompleteCtrl.id}}"\
ng-hide="$mdAutocompleteCtrl.hidden"\
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
ng-mouseup="$mdAutocompleteCtrl.mouseUp()">\
<li ng-repeat="(index, item) in $mdAutocompleteCtrl.matches"\
ng-class="{ selected: index === $mdAutocompleteCtrl.index }"\
ng-click="$mdAutocompleteCtrl.select(index)"\
md-autocomplete-list-item="$mdAutocompleteCtrl.itemName">\
' + itemTemplate + '\
</li>\
' + noItemsTemplate + '\
</ul>\
</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').remove(),
html = templateTag.length ? templateTag.html() : element.html();
if (!templateTag.length) element.empty();
return html;
}
function getNoItemsTemplate () {
var templateTag = element.find('md-not-found').remove(),
template = templateTag.length ? templateTag.html() : '';
return template
? '<li ng-if="!$mdAutocompleteCtrl.matches.length && !$mdAutocompleteCtrl.loading\
&& !$mdAutocompleteCtrl.hidden"\
ng-hide="$mdAutocompleteCtrl.hidden"\
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"\
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"\
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')
.controller('MdHighlightCtrl', MdHighlightCtrl);
function MdHighlightCtrl ($scope, $element, $interpolate) {
var ctrl = this;
ctrl.term = null;
ctrl.template = null;
ctrl.watchers = [];
ctrl.init = init;
function init (term, template) {
createWatchers(term, template);
$element.on('$destroy', cleanup);
}
function createWatchers (term, template) {
ctrl.watchers.push($scope.$watch(term, function (term) {
ctrl.term = term;
updateHTML(term, ctrl.template);
}));
ctrl.watchers.push($scope.$watch(compileTemplate, function (template) {
ctrl.template = template;
updateHTML(ctrl.term, template);
}));
function compileTemplate () { return $interpolate(template)($scope); }
}
function cleanup () {
ctrl.watchers.forEach(function (watcher) { watcher(); });
}
function updateHTML () {
if (ctrl.term === null || ctrl.template === null) return;
var unsafeText = $interpolate(ctrl.template)($scope),
text = angular.element('<div>').text(unsafeText).html(),
flags = $element.attr('md-highlight-flags') || '',
regex = getRegExp(ctrl.term, flags),
html = text.replace(regex, '<span class="highlight">$&</span>');
$element.html(html);
}
function sanitize (term) {
if (!term) return term;
return 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", "$interpolate"];
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 () {
return {
terminal: true,
scope: false,
controller: 'MdHighlightCtrl',
compile: function (element, attr) {
var template = element.html();
return function (scope, element, attr, ctrl) {
ctrl.init(attr.mdHighlightText, template);
};
}
};
}
angular
.module('material.components.autocomplete')
.directive('mdAutocompleteListItem', MdAutocompleteListItem);
function MdAutocompleteListItem ($compile, $mdUtil) {
return {
terminal: true,
link: postLink,
scope: false
};
function postLink (scope, element, attr) {
var ctrl = scope.$parent.$mdAutocompleteCtrl,
newScope = ctrl.parent.$new(false, ctrl.parent),
itemName = ctrl.scope.$eval(attr.mdAutocompleteListItem);
newScope[ itemName ] = scope.item;
$compile(element.contents())(newScope);
element.attr({
role: 'option',
id: 'item_' + $mdUtil.nextUid()
});
}
}
MdAutocompleteListItem.$inject = ["$compile", "$mdUtil"];
angular
.module('material.components.autocomplete')
.directive('mdAutocompleteParentScope', MdAutocompleteParentScope);
function MdAutocompleteParentScope ($compile) {
return {
restrict: 'A',
terminal: true,
link: postLink,
scope: false
};
function postLink (scope, element, attr) {
var ctrl = scope.$parent.$mdAutocompleteCtrl;
$compile(element.contents())(ctrl.parent);
if (attr.hasOwnProperty('mdAutocompleteReplace')) {
element.after(element.contents());
element.remove();
}
}
}
MdAutocompleteParentScope.$inject = ["$compile"];
})(window, window.angular);