| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- angular.module('ui.bootstrap.debounce', [])
- /**
- * A helper, internal service that debounces a function
- */
- .factory('$$debounce', ['$timeout', function($timeout) {
- return function(callback, debounceTime) {
- var timeoutPromise;
- return function() {
- var self = this;
- var args = Array.prototype.slice(arguments);
- if (timeoutPromise) {
- $timeout.cancel(timeoutPromise);
- }
- timeoutPromise = $timeout(function() {
- callback.apply(self, args);
- }, debounceTime);
- };
- };
- }]);
- angular.module('ui.bootstrap.position', [])
- /**
- * A set of utility methods that can be use to retrieve position of DOM elements.
- * It is meant to be used where we need to absolute-position DOM elements in
- * relation to other, existing elements (this is the case for tooltips, popovers,
- * typeahead suggestions etc.).
- */
- .factory('$uibPosition', ['$document', '$window', function($document, $window) {
- function getStyle(el, cssprop) {
- if (el.currentStyle) { //IE
- return el.currentStyle[cssprop];
- } else if ($window.getComputedStyle) {
- return $window.getComputedStyle(el)[cssprop];
- }
- // finally try and get inline style
- return el.style[cssprop];
- }
- /**
- * Checks if a given element is statically positioned
- * @param element - raw DOM element
- */
- function isStaticPositioned(element) {
- return (getStyle(element, 'position') || 'static' ) === 'static';
- }
- /**
- * returns the closest, non-statically positioned parentOffset of a given element
- * @param element
- */
- var parentOffsetEl = function(element) {
- var docDomEl = $document[0];
- var offsetParent = element.offsetParent || docDomEl;
- while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent) ) {
- offsetParent = offsetParent.offsetParent;
- }
- return offsetParent || docDomEl;
- };
- return {
- /**
- * Provides read-only equivalent of jQuery's position function:
- * http://api.jquery.com/position/
- */
- position: function(element) {
- var elBCR = this.offset(element);
- var offsetParentBCR = { top: 0, left: 0 };
- var offsetParentEl = parentOffsetEl(element[0]);
- if (offsetParentEl !== $document[0]) {
- offsetParentBCR = this.offset(angular.element(offsetParentEl));
- offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
- offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
- }
- var boundingClientRect = element[0].getBoundingClientRect();
- return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: elBCR.top - offsetParentBCR.top,
- left: elBCR.left - offsetParentBCR.left
- };
- },
- /**
- * Provides read-only equivalent of jQuery's offset function:
- * http://api.jquery.com/offset/
- */
- offset: function(element) {
- var boundingClientRect = element[0].getBoundingClientRect();
- return {
- width: boundingClientRect.width || element.prop('offsetWidth'),
- height: boundingClientRect.height || element.prop('offsetHeight'),
- top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
- left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
- };
- },
- /**
- * Provides coordinates for the targetEl in relation to hostEl
- */
- positionElements: function(hostEl, targetEl, positionStr, appendToBody) {
- var positionStrParts = positionStr.split('-');
- var pos0 = positionStrParts[0], pos1 = positionStrParts[1] || 'center';
- var hostElPos,
- targetElWidth,
- targetElHeight,
- targetElPos;
- hostElPos = appendToBody ? this.offset(hostEl) : this.position(hostEl);
- targetElWidth = targetEl.prop('offsetWidth');
- targetElHeight = targetEl.prop('offsetHeight');
- var shiftWidth = {
- center: function() {
- return hostElPos.left + hostElPos.width / 2 - targetElWidth / 2;
- },
- left: function() {
- return hostElPos.left;
- },
- right: function() {
- return hostElPos.left + hostElPos.width;
- }
- };
- var shiftHeight = {
- center: function() {
- return hostElPos.top + hostElPos.height / 2 - targetElHeight / 2;
- },
- top: function() {
- return hostElPos.top;
- },
- bottom: function() {
- return hostElPos.top + hostElPos.height;
- }
- };
- switch (pos0) {
- case 'right':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: shiftWidth[pos0]()
- };
- break;
- case 'left':
- targetElPos = {
- top: shiftHeight[pos1](),
- left: hostElPos.left - targetElWidth
- };
- break;
- case 'bottom':
- targetElPos = {
- top: shiftHeight[pos0](),
- left: shiftWidth[pos1]()
- };
- break;
- default:
- targetElPos = {
- top: hostElPos.top - targetElHeight,
- left: shiftWidth[pos1]()
- };
- }
- return targetElPos;
- }
- };
- }]);
- angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.debounce', 'ui.bootstrap.position'])
- /**
- * A helper service that can parse typeahead's syntax (string provided by users)
- * Extracted to a separate service for ease of unit testing
- */
- .factory('uibTypeaheadParser', ['$parse', function($parse) {
- // 00000111000000000000022200000000000000003333333333333330000000000044000
- var TYPEAHEAD_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;
- return {
- parse: function(input) {
- var match = input.match(TYPEAHEAD_REGEXP);
- if (!match) {
- throw new Error(
- 'Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_"' +
- ' but got "' + input + '".');
- }
- return {
- itemName: match[3],
- source: $parse(match[4]),
- viewMapper: $parse(match[2] || match[1]),
- modelMapper: $parse(match[1])
- };
- }
- };
- }])
- .controller('UibTypeaheadController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$q', '$timeout', '$document', '$window', '$rootScope', '$$debounce', '$uibPosition', 'uibTypeaheadParser',
- function(originalScope, element, attrs, $compile, $parse, $q, $timeout, $document, $window, $rootScope, $$debounce, $position, typeaheadParser) {
- var HOT_KEYS = [9, 13, 27, 38, 40];
- var eventDebounceTime = 200;
- var modelCtrl, ngModelOptions;
- //SUPPORTED ATTRIBUTES (OPTIONS)
- //minimal no of characters that needs to be entered before typeahead kicks-in
- var minLength = originalScope.$eval(attrs.typeaheadMinLength);
- if (!minLength && minLength !== 0) {
- minLength = 1;
- }
- //minimal wait time after last character typed before typeahead kicks-in
- var waitTime = originalScope.$eval(attrs.typeaheadWaitMs) || 0;
- //should it restrict model values to the ones selected from the popup only?
- var isEditable = originalScope.$eval(attrs.typeaheadEditable) !== false;
- originalScope.$watch(attrs.typeaheadEditable, function (newVal) {
- isEditable = newVal !== false;
- });
- //binding to a variable that indicates if matches are being retrieved asynchronously
- var isLoadingSetter = $parse(attrs.typeaheadLoading).assign || angular.noop;
- //a callback executed when a match is selected
- var onSelectCallback = $parse(attrs.typeaheadOnSelect);
- //should it select highlighted popup value when losing focus?
- var isSelectOnBlur = angular.isDefined(attrs.typeaheadSelectOnBlur) ? originalScope.$eval(attrs.typeaheadSelectOnBlur) : false;
- //binding to a variable that indicates if there were no results after the query is completed
- var isNoResultsSetter = $parse(attrs.typeaheadNoResults).assign || angular.noop;
- var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined;
- var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false;
- var appendTo = attrs.typeaheadAppendTo ?
- originalScope.$eval(attrs.typeaheadAppendTo) : null;
- var focusFirst = originalScope.$eval(attrs.typeaheadFocusFirst) !== false;
- //If input matches an item of the list exactly, select it automatically
- var selectOnExact = attrs.typeaheadSelectOnExact ? originalScope.$eval(attrs.typeaheadSelectOnExact) : false;
- //binding to a variable that indicates if dropdown is open
- var isOpenSetter = $parse(attrs.typeaheadIsOpen).assign || angular.noop;
- var showHint = originalScope.$eval(attrs.typeaheadShowHint) || false;
- //INTERNAL VARIABLES
- //model setter executed upon match selection
- var parsedModel = $parse(attrs.ngModel);
- var invokeModelSetter = $parse(attrs.ngModel + '($$$p)');
- var $setModelValue = function(scope, newValue) {
- if (angular.isFunction(parsedModel(originalScope)) &&
- ngModelOptions && ngModelOptions.$options && ngModelOptions.$options.getterSetter) {
- return invokeModelSetter(scope, {$$$p: newValue});
- }
- return parsedModel.assign(scope, newValue);
- };
- //expressions used by typeahead
- var parserResult = typeaheadParser.parse(attrs.uibTypeahead);
- var hasFocus;
- //Used to avoid bug in iOS webview where iOS keyboard does not fire
- //mousedown & mouseup events
- //Issue #3699
- var selected;
- //create a child scope for the typeahead directive so we are not polluting original scope
- //with typeahead-specific data (matches, query etc.)
- var scope = originalScope.$new();
- var offDestroy = originalScope.$on('$destroy', function() {
- scope.$destroy();
- });
- scope.$on('$destroy', offDestroy);
- // WAI-ARIA
- var popupId = 'typeahead-' + scope.$id + '-' + Math.floor(Math.random() * 10000);
- element.attr({
- 'aria-autocomplete': 'list',
- 'aria-expanded': false,
- 'aria-owns': popupId
- });
- var inputsContainer, hintInputElem;
- //add read-only input to show hint
- if (showHint) {
- inputsContainer = angular.element('<div></div>');
- inputsContainer.css('position', 'relative');
- element.after(inputsContainer);
- hintInputElem = element.clone();
- hintInputElem.attr('placeholder', '');
- hintInputElem.val('');
- hintInputElem.css({
- 'position': 'absolute',
- 'top': '0px',
- 'left': '0px',
- 'border-color': 'transparent',
- 'box-shadow': 'none',
- 'opacity': 1,
- 'background': 'none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)',
- 'color': '#999'
- });
- element.css({
- 'position': 'relative',
- 'vertical-align': 'top',
- 'background-color': 'transparent'
- });
- inputsContainer.append(hintInputElem);
- hintInputElem.after(element);
- }
- //pop-up element used to display matches
- var popUpEl = angular.element('<div uib-typeahead-popup></div>');
- popUpEl.attr({
- id: popupId,
- matches: 'matches',
- active: 'activeIdx',
- select: 'select(activeIdx)',
- 'move-in-progress': 'moveInProgress',
- query: 'query',
- position: 'position',
- 'assign-is-open': 'assignIsOpen(isOpen)'
- });
- //custom item template
- if (angular.isDefined(attrs.typeaheadTemplateUrl)) {
- popUpEl.attr('template-url', attrs.typeaheadTemplateUrl);
- }
- if (angular.isDefined(attrs.typeaheadPopupTemplateUrl)) {
- popUpEl.attr('popup-template-url', attrs.typeaheadPopupTemplateUrl);
- }
- var resetHint = function() {
- if (showHint) {
- hintInputElem.val('');
- }
- };
- var resetMatches = function() {
- scope.matches = [];
- scope.activeIdx = -1;
- element.attr('aria-expanded', false);
- resetHint();
- };
- var getMatchId = function(index) {
- return popupId + '-option-' + index;
- };
- // Indicate that the specified match is the active (pre-selected) item in the list owned by this typeahead.
- // This attribute is added or removed automatically when the `activeIdx` changes.
- scope.$watch('activeIdx', function(index) {
- if (index < 0) {
- element.removeAttr('aria-activedescendant');
- } else {
- element.attr('aria-activedescendant', getMatchId(index));
- }
- });
- var inputIsExactMatch = function(inputValue, index) {
- if (scope.matches.length > index && inputValue) {
- return inputValue.toUpperCase() === scope.matches[index].label.toUpperCase();
- }
- return false;
- };
- var getMatchesAsync = function(inputValue) {
- var locals = {$viewValue: inputValue};
- isLoadingSetter(originalScope, true);
- isNoResultsSetter(originalScope, false);
- $q.when(parserResult.source(originalScope, locals)).then(function(matches) {
- //it might happen that several async queries were in progress if a user were typing fast
- //but we are interested only in responses that correspond to the current view value
- var onCurrentRequest = inputValue === modelCtrl.$viewValue;
- if (onCurrentRequest && hasFocus) {
- if (matches && matches.length > 0) {
- scope.activeIdx = focusFirst ? 0 : -1;
- isNoResultsSetter(originalScope, false);
- scope.matches.length = 0;
- //transform labels
- for (var i = 0; i < matches.length; i++) {
- locals[parserResult.itemName] = matches[i];
- scope.matches.push({
- id: getMatchId(i),
- label: parserResult.viewMapper(scope, locals),
- model: matches[i]
- });
- }
- scope.query = inputValue;
- //position pop-up with matches - we need to re-calculate its position each time we are opening a window
- //with matches as a pop-up might be absolute-positioned and position of an input might have changed on a page
- //due to other elements being rendered
- recalculatePosition();
- element.attr('aria-expanded', true);
- //Select the single remaining option if user input matches
- if (selectOnExact && scope.matches.length === 1 && inputIsExactMatch(inputValue, 0)) {
- scope.select(0);
- }
- if (showHint) {
- var firstLabel = scope.matches[0].label;
- if (inputValue.length > 0 && firstLabel.slice(0, inputValue.length).toUpperCase() === inputValue.toUpperCase()) {
- hintInputElem.val(inputValue + firstLabel.slice(inputValue.length));
- }
- else {
- hintInputElem.val('');
- }
- }
- } else {
- resetMatches();
- isNoResultsSetter(originalScope, true);
- }
- }
- if (onCurrentRequest) {
- isLoadingSetter(originalScope, false);
- }
- }, function() {
- resetMatches();
- isLoadingSetter(originalScope, false);
- isNoResultsSetter(originalScope, true);
- });
- };
- // bind events only if appendToBody params exist - performance feature
- if (appendToBody) {
- angular.element($window).bind('resize', fireRecalculating);
- $document.find('body').bind('scroll', fireRecalculating);
- }
- // Declare the debounced function outside recalculating for
- // proper debouncing
- var debouncedRecalculate = $$debounce(function() {
- // if popup is visible
- if (scope.matches.length) {
- recalculatePosition();
- }
- scope.moveInProgress = false;
- }, eventDebounceTime);
- // Default progress type
- scope.moveInProgress = false;
- function fireRecalculating() {
- if (!scope.moveInProgress) {
- scope.moveInProgress = true;
- scope.$digest();
- }
- debouncedRecalculate();
- }
- // recalculate actual position and set new values to scope
- // after digest loop is popup in right position
- function recalculatePosition() {
- scope.position = appendToBody ? $position.offset(element) : $position.position(element);
- scope.position.top += element.prop('offsetHeight');
- }
- //we need to propagate user's query so we can higlight matches
- scope.query = undefined;
- //Declare the timeout promise var outside the function scope so that stacked calls can be cancelled later
- var timeoutPromise;
- var scheduleSearchWithTimeout = function(inputValue) {
- timeoutPromise = $timeout(function() {
- getMatchesAsync(inputValue);
- }, waitTime);
- };
- var cancelPreviousTimeout = function() {
- if (timeoutPromise) {
- $timeout.cancel(timeoutPromise);
- }
- };
- resetMatches();
- scope.assignIsOpen = function (isOpen) {
- isOpenSetter(originalScope, isOpen);
- };
- scope.select = function(activeIdx) {
- //called from within the $digest() cycle
- var locals = {};
- var model, item;
- selected = true;
- locals[parserResult.itemName] = item = scope.matches[activeIdx].model;
- model = parserResult.modelMapper(originalScope, locals);
- $setModelValue(originalScope, model);
- modelCtrl.$setValidity('editable', true);
- modelCtrl.$setValidity('parse', true);
- onSelectCallback(originalScope, {
- $item: item,
- $model: model,
- $label: parserResult.viewMapper(originalScope, locals)
- });
- resetMatches();
- //return focus to the input element if a match was selected via a mouse click event
- // use timeout to avoid $rootScope:inprog error
- if (scope.$eval(attrs.typeaheadFocusOnSelect) !== false) {
- $timeout(function() { element[0].focus(); }, 0, false);
- }
- };
- //bind keyboard events: arrows up(38) / down(40), enter(13) and tab(9), esc(27)
- element.bind('keydown', function(evt) {
- //typeahead is open and an "interesting" key was pressed
- if (scope.matches.length === 0 || HOT_KEYS.indexOf(evt.which) === -1) {
- return;
- }
- // if there's nothing selected (i.e. focusFirst) and enter or tab is hit, clear the results
- if (scope.activeIdx === -1 && (evt.which === 9 || evt.which === 13)) {
- resetMatches();
- scope.$digest();
- return;
- }
- evt.preventDefault();
- if (evt.which === 40) {
- scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length;
- scope.$digest();
- popUpEl.children()[scope.activeIdx].scrollIntoView(false);
- } else if (evt.which === 38) {
- scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1;
- scope.$digest();
- popUpEl.children()[scope.activeIdx].scrollIntoView(false);
- } else if (evt.which === 13 || evt.which === 9) {
- scope.$apply(function () {
- scope.select(scope.activeIdx);
- });
- } else if (evt.which === 27) {
- evt.stopPropagation();
- resetMatches();
- scope.$digest();
- }
- });
- element.bind('focus', function () {
- hasFocus = true;
- if (minLength === 0 && !modelCtrl.$viewValue) {
- getMatchesAsync(modelCtrl.$viewValue);
- }
- });
- element.bind('blur', function() {
- if (isSelectOnBlur && scope.matches.length && scope.activeIdx !== -1 && !selected) {
- selected = true;
- scope.$apply(function() {
- scope.select(scope.activeIdx);
- });
- }
- if (!isEditable && modelCtrl.$error.editable) {
- modelCtrl.$viewValue = '';
- element.val('');
- }
- hasFocus = false;
- selected = false;
- });
- // Keep reference to click handler to unbind it.
- var dismissClickHandler = function(evt) {
- // Issue #3973
- // Firefox treats right click as a click on document
- if (element[0] !== evt.target && evt.which !== 3 && scope.matches.length !== 0) {
- resetMatches();
- if (!$rootScope.$$phase) {
- scope.$digest();
- }
- }
- };
- $document.bind('click', dismissClickHandler);
- originalScope.$on('$destroy', function() {
- $document.unbind('click', dismissClickHandler);
- if (appendToBody || appendTo) {
- $popup.remove();
- }
- if (appendToBody) {
- angular.element($window).unbind('resize', fireRecalculating);
- $document.find('body').unbind('scroll', fireRecalculating);
- }
- // Prevent jQuery cache memory leak
- popUpEl.remove();
- if (showHint) {
- inputsContainer.remove();
- }
- });
- var $popup = $compile(popUpEl)(scope);
- if (appendToBody) {
- $document.find('body').append($popup);
- } else if (appendTo) {
- angular.element(appendTo).eq(0).append($popup);
- } else {
- element.after($popup);
- }
- this.init = function(_modelCtrl, _ngModelOptions) {
- modelCtrl = _modelCtrl;
- ngModelOptions = _ngModelOptions;
- //plug into $parsers pipeline to open a typeahead on view changes initiated from DOM
- //$parsers kick-in on all the changes coming from the view as well as manually triggered by $setViewValue
- modelCtrl.$parsers.unshift(function(inputValue) {
- hasFocus = true;
- if (minLength === 0 || inputValue && inputValue.length >= minLength) {
- if (waitTime > 0) {
- cancelPreviousTimeout();
- scheduleSearchWithTimeout(inputValue);
- } else {
- getMatchesAsync(inputValue);
- }
- } else {
- isLoadingSetter(originalScope, false);
- cancelPreviousTimeout();
- resetMatches();
- }
- if (isEditable) {
- return inputValue;
- }
- if (!inputValue) {
- // Reset in case user had typed something previously.
- modelCtrl.$setValidity('editable', true);
- return null;
- }
- modelCtrl.$setValidity('editable', false);
- return undefined;
- });
- modelCtrl.$formatters.push(function(modelValue) {
- var candidateViewValue, emptyViewValue;
- var locals = {};
- // The validity may be set to false via $parsers (see above) if
- // the model is restricted to selected values. If the model
- // is set manually it is considered to be valid.
- if (!isEditable) {
- modelCtrl.$setValidity('editable', true);
- }
- if (inputFormatter) {
- locals.$model = modelValue;
- return inputFormatter(originalScope, locals);
- }
- //it might happen that we don't have enough info to properly render input value
- //we need to check for this situation and simply return model value if we can't apply custom formatting
- locals[parserResult.itemName] = modelValue;
- candidateViewValue = parserResult.viewMapper(originalScope, locals);
- locals[parserResult.itemName] = undefined;
- emptyViewValue = parserResult.viewMapper(originalScope, locals);
- return candidateViewValue !== emptyViewValue ? candidateViewValue : modelValue;
- });
- };
- }])
- .directive('uibTypeahead', function() {
- return {
- controller: 'UibTypeaheadController',
- require: ['ngModel', '^?ngModelOptions', 'uibTypeahead'],
- link: function(originalScope, element, attrs, ctrls) {
- ctrls[2].init(ctrls[0], ctrls[1]);
- }
- };
- })
- .directive('uibTypeaheadPopup', function() {
- return {
- scope: {
- matches: '=',
- query: '=',
- active: '=',
- position: '&',
- moveInProgress: '=',
- select: '&',
- assignIsOpen: '&'
- },
- replace: true,
- templateUrl: function(element, attrs) {
- return attrs.popupTemplateUrl || 'html/typeahead-popup.html';
- },
- link: function(scope, element, attrs) {
- scope.templateUrl = attrs.templateUrl;
- scope.isOpen = function() {
- var isDropdownOpen = scope.matches.length > 0;
- scope.assignIsOpen({ isOpen: isDropdownOpen });
- return isDropdownOpen;
- };
- scope.isActive = function(matchIdx) {
- return scope.active === matchIdx;
- };
- scope.selectActive = function(matchIdx) {
- scope.active = matchIdx;
- };
- scope.selectMatch = function(activeIdx) {
- scope.select({activeIdx: activeIdx});
- };
- }
- };
- })
- .directive('uibTypeaheadMatch', ['$templateRequest', '$compile', '$parse', function($templateRequest, $compile, $parse) {
- return {
- scope: {
- index: '=',
- match: '=',
- query: '='
- },
- link: function(scope, element, attrs) {
- var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'html/typeahead-match.html';
- $templateRequest(tplUrl).then(function(tplContent) {
- var tplEl = angular.element(tplContent.trim());
- element.replaceWith(tplEl);
- $compile(tplEl)(scope);
- });
- }
- };
- }])
- .filter('uibTypeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) {
- var isSanitizePresent;
- isSanitizePresent = $injector.has('$sanitize');
- function escapeRegexp(queryToEscape) {
- // Regex: capture the whole query string and replace it with the string that will be used to match
- // the results, for example if the capture is "a" the result will be \a
- return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1');
- }
- function containsHtml(matchItem) {
- return /<.*>/g.test(matchItem);
- }
- return function(matchItem, query) {
- if (!isSanitizePresent && containsHtml(matchItem)) {
- $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger
- }
- matchItem = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag
- if (!isSanitizePresent) {
- matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive
- }
- return matchItem;
- };
- }]);
|