Created
September 27, 2015 14:46
-
-
Save rupe120/9d095f5d43535e9b2786 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
'use strict'; | |
/** | |
* Based on the ui.bootstrap.typeahead control from ui.bootstrap 0.12.0 | |
* | |
* Has dependencies on the typeaheadParser directive and ui.bootstrap.position module from the ui.bootstrap module | |
*/ | |
window.appIndependent | |
// Based on the bindHtmlUnsafe2 directive in the ui.bootstrap.bindHtml module | |
.directive('bindHtmlUnsafe2', function () { | |
return function (scope, element, attr) { | |
element.addClass('ng-binding').data('$binding', attr.bindHtmlUnsafe2); | |
scope.$watch(attr.bindHtmlUnsafe2, function bindHtmlUnsafeWatchAction(value) { | |
if (value === undefined || value === null) | |
value = ''; | |
element.html(value); | |
}); | |
}; | |
}) | |
///** | |
// * A helper service that can parse typeahead's syntax (string provided by users) | |
// * Extracted to a separate service for ease of unit testing | |
// */ | |
// .factory('typeaheadParser', ['$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]) | |
// }; | |
// } | |
// }; | |
// }]) | |
.directive('typeaheadAdvanced', ['$compile', '$parse', '$q', '$timeout', '$document', '$position', 'typeaheadParser', '$exceptionHandler', '$rootScope', | |
function ($compile, $parse, $q, $timeout, $document, $position, typeaheadParser, $exceptionHandler, $rootScope) { | |
var HOT_KEYS = [9, 13, 27, 38, 40]; | |
return { | |
require: 'ngModel', | |
link: function (originalScope, element, attrs, modelCtrl) { | |
//if (console && console.debug) { | |
// console.debug("typeahead.link"); | |
//} | |
// *JRR* | |
element.attr('autocomplete', 'off'); | |
// Add the clear (x) link, for clearing the field | |
element.addClass('clearable'); | |
var wrapperDiv = $document[0].createElement('div'); | |
wrapperDiv.setAttribute('class', 'clearable-wrapper'); | |
element[0].parentNode.insertBefore(wrapperDiv, element[0]); | |
wrapperDiv.appendChild(element[0]); | |
var clearButton = $document[0].createElement('button'); | |
clearButton.setAttribute('class', 'close-icon glyphicon glyphicon-remove'); | |
clearButton.setAttribute('tabIndex', '-1'); | |
angular.element(clearButton).bind("click", function (event) { | |
// Ensure the clean was a click | |
if (event.clientX > 0) { | |
scope.select(-1); | |
modelCtrl.$render(); | |
} | |
}); | |
//element[0].appendChild(clearButton); | |
element[0].parentNode.insertBefore(clearButton, element[0].nextSibling); | |
//SUPPORTED ATTRIBUTES (OPTIONS) | |
//minimal no of characters that needs to be entered before typeahead kicks-in | |
var minSearch = originalScope.$eval(attrs.typeaheadMinLength) || 1; | |
//minimal wait time after last character typed before typehead 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; | |
// *JRR* | |
//Should this directive ensure that either a selection is made from the popup or the model value is undefined. Defaults to true | |
var requireSelection = !attrs.hasOwnProperty('typeaheadRequireSelection') ? true : originalScope.$eval(attrs.typeaheadRequireSelection) === true; | |
// *JRR* | |
//Should the suggestions popup automatically show? Defaults to true | |
var displayPopupOnClick = !attrs.hasOwnProperty('typeaheadDisplayPopupOnClick') ? true : originalScope.$eval(attrs.typeaheadDisplayPopupOnClick) === true; | |
//If true then force the minSearch to zero | |
if (displayPopupOnClick) { | |
minSearch = 0; | |
} | |
//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); | |
var onKeydownCallback = $parse(attrs.typeaheadOnKeydown); | |
var inputFormatter = attrs.typeaheadInputFormatter ? $parse(attrs.typeaheadInputFormatter) : undefined; | |
var appendToBody = attrs.typeaheadAppendToBody ? originalScope.$eval(attrs.typeaheadAppendToBody) : false; | |
var focusFirst = attrs.typeaheadFocusFirst ? originalScope.$eval(attrs.typeaheadFocusFirst) : true; | |
var autoSelectFirst = attrs.typeaheadAutoSelectFirst ? originalScope.$eval(attrs.typeaheadAutoSelectFirst) : false; | |
if(autoSelectFirst){ | |
focusFirst = true; | |
} | |
var managePopupHeight = originalScope.$eval(attrs.typeaheadManagePopupHeight) === true; | |
var defaultValue = attrs.typeaheadDefaultValue ? $parse(attrs.typeaheadDefaultValue) : undefined; | |
//INTERNAL VARIABLES | |
// *JRR* | |
var $getModelValue = $parse(attrs.ngModel); | |
//model setter executed upon match selection | |
var $setModelValue = $getModelValue.assign; | |
//expressions used by typeahead | |
var parserResult = typeaheadParser.parse(attrs.typeaheadAdvanced); | |
// *JRR* | |
var originalModelValue; | |
var originalInputValue; | |
// *JRR* | |
var isValidSelection; | |
//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(); | |
originalScope.$on('$destroy', function () { | |
scope.$destroy(); | |
}); | |
scope.hasFocus = false; | |
scope.noResults = false; | |
scope.managePopupHeight = managePopupHeight; | |
//if (console && console.debug) { | |
// console.debug("typeahead.scope.managePopupHeight = " + scope.managePopupHeight); | |
//} | |
// WAI-ARIA | |
var popupId = ''; | |
// JSR - Setting popup id based on input id. If no id exists, generate random id. | |
if (attrs.id) { | |
popupId = attrs.id + '-typeahead'; | |
} else { | |
popupId = scope.$id + '-' + Math.floor(Math.random() * 10000) + '-typeahead'; | |
} | |
element.attr({ | |
'aria-autocomplete': 'list', | |
'aria-expanded': false, | |
'aria-owns': popupId | |
}); | |
//pop-up element used to display matches | |
var popUpEl = angular.element('<div typeahead-advanced-popup></div>'); | |
popUpEl.attr({ | |
id: popupId, | |
matches: 'matches', | |
'has-focus': 'hasFocus', | |
'no-results': 'noResults', | |
active: 'activeIdx', | |
select: 'select(activeIdx, true)', | |
query: 'query', | |
'manage-popup-height': 'managePopupHeight', | |
position: 'position', | |
'position-popup': 'positionPopup' | |
}); | |
//custom item template | |
if (angular.isDefined(attrs.typeaheadTemplateUrl)) { | |
popUpEl.attr('template-url', attrs.typeaheadTemplateUrl); | |
} | |
var resetMatches = function () { | |
scope.matches = []; | |
scope.activeIdx = -1; | |
element.attr('aria-expanded', false); | |
}; | |
var getMatchId = function (index) { | |
return popupId + '-option-' + index; | |
}; | |
var getModelDisplay = function (modelValue) { | |
var formatters = modelCtrl.$formatters, | |
idx = formatters.length; | |
var viewValue = modelValue; | |
while (idx--) { | |
viewValue = formatters[idx](viewValue); | |
} | |
return viewValue; | |
} | |
// 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)); | |
} | |
}); | |
// The positioning of the popup in a scope method to allow for it to be called from different page events | |
scope.positionPopup = function () { | |
//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 | |
scope.position = appendToBody ? $position.offset(element) : $position.position(element); | |
scope.position.top = scope.position.top + element.prop('offsetHeight'); | |
//if (console && console.debug) { | |
// console.debug("typeahead.positionPopup - " + JSON.stringify(scope.position)); | |
//} | |
}; | |
var positionPopupListener; | |
var registerPopupPositioning = function () { | |
//if (console && console.debug) { | |
// console.debug("typeahead add positionPopup"); | |
//} | |
// Add a listener to reposition on $digest in case a change just displayed a message above the typeahead | |
positionPopupListener = scope.$watch(function () { | |
var position = appendToBody ? $position.offset(element) : $position.position(element); | |
return position.top; | |
}, function (newValue, oldValue) { | |
if (newValue != oldValue) { | |
scope.positionPopup(); | |
} | |
}); | |
} | |
scope.$watch(function () { return scope.matches.length > 0; }, function (newValue) { | |
// Remove the repositioning listener when the popup is no longer displayed | |
if (!newValue && positionPopupListener) { | |
//if (console && console.debug) { | |
// console.debug("typeahead remove positionPopup"); | |
//} | |
positionPopupListener(); | |
} | |
}); | |
var getMatchesAsync = function (inputValue) { | |
var locals = { $viewValue: typeof inputValue === "undefined" ? '' : inputValue }; | |
isLoadingSetter(originalScope, true); | |
$q.when(parserResult.source(originalScope, locals)).then(function (matches) { | |
scope.noResults = false; | |
//if (console && console.debug) { | |
// console.debug("typeahead.getMatchesAsync inputValue = " + inputValue + ", modelCtrl.$viewValue = " + modelCtrl.$viewValue + ", onCurrentRequest = " + (inputValue === modelCtrl.$viewValue) + ", scope.hasFocus = " + scope.hasFocus + ", matches.length = " + matches.length + ", modelCtrl.$viewValue === defaultValue() = " + (defaultValue && inputValue === '' && modelCtrl.$viewValue === defaultValue())); | |
//} | |
//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 && scope.hasFocus) { | |
scope.noResults = matches.length === 0; | |
if (matches.length > 0) { | |
scope.activeIdx = focusFirst && (inputValue || autoSelectFirst) ? 0 : -1; | |
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] | |
}); | |
} | |
//if (console && console.debug) { | |
// console.debug("typeahead.unshift.getMatchesAsync scope.matches.length = " + scope.matches.length); | |
//} | |
scope.query = inputValue; | |
// Register the popup repositioning listener | |
registerPopupPositioning(); | |
// Position the popup | |
scope.positionPopup(); | |
//if (console && console.debug) { | |
// console.debug(JSON.stringify(scope.position)); | |
//} | |
element.attr('aria-expanded', true); | |
} else { | |
resetMatches(); | |
} | |
} | |
if (onCurrentRequest) { | |
isLoadingSetter(originalScope, false); | |
} | |
}, function () { | |
resetMatches(); | |
isLoadingSetter(originalScope, false); | |
}); | |
}; | |
resetMatches(); | |
//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); | |
} | |
}; | |
//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) { | |
//if (console && console.debug) { | |
// console.debug((new Date()) + " $parsers"); | |
// console.debug(modelCtrl.$error); | |
//} | |
if (!scope.hasFocus) | |
return $getModelValue(originalScope); | |
isValidSelection = false; | |
//if (console && console.debug) { | |
// console.debug(inputValue); | |
//} | |
if ((inputValue && inputValue.length >= minSearch) || minSearch == 0) { | |
if (waitTime > 0) { | |
cancelPreviousTimeout(); | |
scheduleSearchWithTimeout(inputValue); | |
} else { | |
getMatchesAsync(inputValue); | |
} | |
} else { | |
isLoadingSetter(originalScope, false); | |
cancelPreviousTimeout(); | |
resetMatches(); | |
} | |
if (isEditable) { | |
return originalModelValue; | |
} else { | |
if (!inputValue) { | |
// Reset in case user had typed something previously. | |
modelCtrl.$setValidity('editable', true); | |
return originalModelValue; | |
} else { | |
modelCtrl.$setValidity('editable', false); | |
return undefined; | |
} | |
} | |
}); | |
modelCtrl.$formatters.push(function (modelValue) { | |
//if (console && console.debug) { | |
// console.debug((new Date()) + " $formatters"); | |
// console.debug(modelCtrl.$error); | |
//} | |
var candidateViewValue, emptyViewValue; | |
var locals = {}; | |
if (inputFormatter) { | |
locals.$model = modelValue; | |
return inputFormatter(originalScope, locals); | |
} else { | |
//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; | |
} | |
}); | |
scope.select = function (activeIdx, refocus) { | |
//called from within the $digest() cycle | |
var locals = {}; | |
var model, item; | |
// *JRR* if (activeIdx >= 0) { | |
if (activeIdx >= 0) { | |
locals[parserResult.itemName] = item = scope.matches[activeIdx].model; | |
model = parserResult.modelMapper(originalScope, locals); | |
} | |
var previousValue = originalModelValue; | |
originalModelValue = model; | |
$setModelValue(originalScope, model); | |
modelCtrl.$setDirty(); | |
// *JRR* trigger change listeners | |
angular.forEach(modelCtrl.$viewChangeListeners, function (listener) { | |
try { | |
listener(); | |
} catch (e) { | |
$exceptionHandler(e); | |
} | |
}); | |
onSelectCallback(originalScope, { | |
$item: item, | |
$model: model, | |
$previousModel: previousValue, | |
$label: parserResult.viewMapper(originalScope, locals) | |
}); | |
if (!$rootScope.$$phase) $rootScope.$digest(); | |
resetMatches(); | |
isValidSelection = true; | |
//if (console && console.debug) { | |
// console.debug("scope.select - activeIdx = " + activeIdx + ", isValidSelection = " + isValidSelection); | |
//} | |
if (activeIdx >= 0 && refocus) { | |
//return focus to the input element if a match was selected via a mouse click event | |
// use timeout to avoid $rootScope:inprog error | |
if (displayPopupOnClick) { | |
scope.preventPopup = true; | |
} | |
$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) { | |
//if (console && console.debug) { | |
// console.debug((new Date()) + " keydown - 1"); | |
// console.debug(modelCtrl.$error); | |
//} | |
onKeydownCallback(originalScope, { | |
$event: 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 selectable (i.e. focusFirst) and enter is hit, don't do anything | |
if ((scope.activeIdx == -1) && (evt.which === 13 || evt.which === 9)) | |
return; | |
// Only allow tab (9) to behave as normal | |
if (evt.which !== 9) | |
evt.preventDefault(); | |
//if (console && console.debug) { | |
// console.debug((new Date()) + " keydown - 2"); | |
// console.debug(modelCtrl.$error); | |
//} | |
if (evt.which === 40) { | |
// down arrow | |
scope.activeIdx = (scope.activeIdx + 1) % scope.matches.length; | |
scope.$digest(); | |
} else if (evt.which === 38) { | |
// up arrow | |
scope.activeIdx = (scope.activeIdx > 0 ? scope.activeIdx : scope.matches.length) - 1; | |
scope.$digest(); | |
} else if (evt.which === 13 || evt.which === 9) { | |
// enter (13) or tab (9) | |
scope.$apply(function () { | |
scope.select(scope.activeIdx); | |
}); | |
} else if (evt.which === 27) { | |
// esc | |
evt.stopPropagation(); | |
resetMatches(); | |
scope.$digest(); | |
} | |
//if (console && console.debug) { | |
// console.debug((new Date()) + " keydown - 3"); | |
// console.debug(modelCtrl.$error); | |
//} | |
}); | |
// *JRR* | |
element.bind('focus', function () { | |
scope.hasFocus = true; | |
isValidSelection = false; | |
originalModelValue = $getModelValue(originalScope); | |
if (typeof originalModelValue !== "undefined" && originalModelValue !== null) { | |
// If there's a value in the model, assume that it's valid | |
isValidSelection = true; | |
} else if (defaultValue) { | |
// If there's no value in the model and a default was supplied set the original values as the default | |
originalModelValue = defaultValue(originalScope); | |
} | |
originalInputValue = getModelDisplay(originalModelValue); | |
// If the computed Input value is null or undefined, set it to an empty string | |
if (typeof originalInputValue === "undefined" || originalInputValue === null) { | |
originalInputValue = ''; | |
} | |
//if (console && console.debug) { | |
// console.debug("element.focus - isValidSelection = " + isValidSelection + ", originalInputValue = " + originalInputValue + ", displayPopupOnClick = " + displayPopupOnClick + ", scope.preventPopup = " + scope.preventPopup); | |
//} | |
if (displayPopupOnClick) { | |
if (scope.preventPopup) { | |
scope.preventPopup = false; | |
} else { | |
//modelCtrl.$viewValue = originalInputValue; | |
getMatchesAsync(modelCtrl.$viewValue); | |
if (!$rootScope.$$phase) scope.$digest(); | |
} | |
} | |
}); | |
element.bind('blur', function (evt) { | |
scope.hasFocus = false; | |
//if (console && console.debug) { | |
// console.debug("element.blur - isValidSelection = " + isValidSelection); | |
//} | |
modelCtrl.$setValidity('editable', true); | |
modelCtrl.$setValidity('parse', true); | |
// *JRR* | |
if (requireSelection) { | |
scope.revertValuePromise = $timeout(function () { | |
if (isValidSelection) { | |
var expectedViewValue = getModelDisplay($getModelValue(originalScope)); | |
if (modelCtrl.$viewValue != expectedViewValue) { | |
modelCtrl.$viewValue = expectedViewValue; | |
modelCtrl.$render(); | |
} | |
} else { | |
if (element[0].value === "" && (typeof defaultValue === "undefined" || defaultValue === null)) { | |
//if (console && console.debug) { | |
// console.debug("element.blur - typeahead.select(-1) from element.blur"); | |
//} | |
if (typeof $getModelValue(originalScope) !== "undefined") { | |
scope.select(-1); | |
} | |
} else { | |
//if (console && console.debug) { | |
// console.debug("element.blur - typeahead reset element value from element.blur"); | |
//} | |
modelCtrl.$setViewValue(originalInputValue); | |
modelCtrl.$$runValidators(undefined, originalModelValue, originalInputValue, function () { }); | |
modelCtrl.$render(); | |
} | |
} | |
resetMatches(); | |
}, 200, true); | |
} | |
}); | |
// Keep reference to click handler to unbind it. | |
var dismissClickHandler = function (evt) { | |
if (element[0] !== evt.target) { | |
resetMatches(); | |
if (!$rootScope.$$phase) scope.$digest(); | |
} | |
}; | |
$document.bind('click', dismissClickHandler); | |
originalScope.$on('$destroy', function () { | |
$document.unbind('click', dismissClickHandler); | |
if (appendToBody) { | |
$popup.remove(); | |
} | |
}); | |
var $popup = $compile(popUpEl)(scope); | |
if (appendToBody) { | |
$document.find('body').append($popup); | |
} else { | |
element.after($popup); | |
} | |
} | |
}; | |
}]) | |
// *JRR* | |
.directive('typeaheadAdvancedPopup', ['$timeout', '$window', '$document', function ($timeout, $window, $document) { | |
return { | |
restrict: 'EA', | |
scope: { | |
matches: '=', | |
hasFocus: '=', | |
noResults: '=', | |
query: '=', | |
active: '=', | |
position: '=', | |
positionPopup: '&', | |
managePopupHeight: '=', | |
select: '&' | |
}, | |
replace: true, | |
templateUrl: 'template/typeahead/typeahead-advanced-popup.html', | |
link: function (scope, element, attrs) { | |
scope.templateUrl = attrs.templateUrl; | |
scope.isOpen = function () { | |
return scope.matches.length > 0; | |
}; | |
scope.showNoResults = function() { | |
return scope.noResults && scope.hasFocus; | |
}; | |
scope.isActive = function (matchIdx) { | |
return scope.active == matchIdx; | |
}; | |
scope.selectActive = function (matchIdx) { | |
scope.active = matchIdx; | |
}; | |
scope.selectMatch = function (activeIdx) { | |
// *JRR* | |
if (scope.revertValuePromise) { | |
$timeout.cancel(scope.revertValuePromise); | |
} | |
//if (console && console.debug) { | |
// console.debug("typeaheadPopup.selectMatch(" + activeIdx + ")"); | |
//} | |
scope.select({ activeIdx: activeIdx, refocus: true }); | |
}; | |
angular.element($window).on("resize", function () { | |
if (scope.isOpen()) { | |
scope.positionPopup(); | |
} | |
}); | |
if (scope.managePopupHeight) { | |
var overflow = element.css('overflow-x'); | |
angular.element($window).on("resize", function () { | |
if (scope.isOpen()) { | |
resizeDropdown(); | |
} | |
}); | |
scope.$watch("matches.length", function () { | |
if (scope.isOpen()) { | |
resizeDropdown(); | |
} | |
}); | |
} | |
function resizeDropdown() { | |
element.css('height', ''); | |
element.css('overflow-x', overflow); | |
// Putting the resizing into a timeout places it at the end of execution que allowing Angular to display the popup before the following code is executed. | |
// This works because in JavaScript (unless you are making a call to an outside system) there is only one execution path running at a time. | |
$timeout(function () { | |
var os = element[0].getBoundingClientRect(); | |
var pageHeight = $document[0].documentElement.clientHeight; | |
if (os.top > pageHeight) | |
return; // off the screen | |
var diff = pageHeight - os.top - 10; | |
if (diff < 200) { | |
diff = 200; | |
} | |
if (diff < os.height) { | |
element.css('height', diff + 'px'); | |
element.css('overflow-x', 'scroll'); | |
} | |
}); | |
} | |
} | |
}; | |
}]) | |
.directive('typeaheadAdvancedMatch', ['$http', '$templateCache', '$compile', '$parse', function ($http, $templateCache, $compile, $parse) { | |
return { | |
restrict: 'EA', | |
scope: { | |
index: '=', | |
match: '=', | |
query: '=' | |
}, | |
link: function (scope, element, attrs) { | |
var tplUrl = $parse(attrs.templateUrl)(scope.$parent) || 'template/typeahead/typeahead-advanced-match.html'; | |
$http.get(tplUrl, { cache: $templateCache }).success(function (tplContent) { | |
element.replaceWith($compile(tplContent.trim())(scope)); | |
}); | |
} | |
}; | |
}]) | |
.filter('typeaheadAdvancedHighlight', function () { | |
function escapeRegexp(queryToEscape) { | |
return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); | |
} | |
return function (matchItem, query) { | |
var retVal = query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '<strong>$&</strong>') : matchItem; | |
return retVal; | |
}; | |
}) | |
.run(["$templateCache", function ($templateCache) { | |
$templateCache.put("template/typeahead/typeahead-advanced-match.html", | |
"<a tabindex=\"-1\" bind-html-unsafe2=\"match.label | typeaheadAdvancedHighlight:query\"></a>"); | |
}]) | |
.run(["$templateCache", function ($templateCache) { | |
$templateCache.put("template/typeahead/typeahead-advanced-popup.html", | |
"<div>\n" + | |
"<ul class=\"dropdown-menu\" ng-show=\"showNoResults()\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!showNoResults()}}\">\n" + | |
" <li role=\"option\" id=\"{{match.id}}\">\n" + | |
" <div class=\"noResults\">No results returned</div>\n" + | |
" </li>\n" + | |
"</ul>\n" + | |
"<ul class=\"dropdown-menu\" ng-show=\"isOpen()\" ng-style=\"{top: position.top+'px', left: position.left+'px'}\" style=\"display: block;\" role=\"listbox\" aria-hidden=\"{{!isOpen()}}\">\n" + | |
" <li ng-repeat=\"match in matches track by $index\" ng-class=\"{active: isActive($index) }\" ng-mouseenter=\"selectActive($index)\" ng-click=\"selectMatch($index)\" role=\"option\" id=\"{{match.id}}\">\n" + | |
" <div typeahead-advanced-match index=\"$index\" match=\"match\" query=\"query\" template-url=\"templateUrl\" manage-popup-height=\"managePopupHeight\"></div>\n" + | |
" </li>\n" + | |
"</ul>\n" + | |
"</div>"+ | |
""); | |
}]); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment