/** * angular-strap * @version v2.0.3 - 2014-05-30 * @link http://mgcrea.github.io/angular-strap * @author Olivier Louvignes (olivier@mg-crea.com) * @license MIT License, http://www.opensource.org/licenses/MIT */ 'use strict'; angular.module('mgcrea.ngStrap.typeahead', [ 'mgcrea.ngStrap.tooltip', 'mgcrea.ngStrap.helpers.parseOptions' ]).provider('$typeahead', function () { var defaults = this.defaults = { animation: 'am-fade', prefixClass: 'typeahead', placement: 'bottom-left', template: 'typeahead/typeahead.tpl.html', trigger: 'focus', container: false, keyboard: true, html: false, delay: 0, minLength: 1, filter: 'filter', limit: 6 }; this.$get = [ '$window', '$rootScope', '$tooltip', function ($window, $rootScope, $tooltip) { var bodyEl = angular.element($window.document.body); function TypeaheadFactory(element, controller, config) { var $typeahead = {}; // Common vars var options = angular.extend({}, defaults, config); $typeahead = $tooltip(element, options); var parentScope = config.scope; var scope = $typeahead.$scope; scope.$resetMatches = function () { scope.$matches = []; scope.$activeIndex = 0; }; scope.$resetMatches(); scope.$activate = function (index) { scope.$$postDigest(function () { $typeahead.activate(index); }); }; scope.$select = function (index, evt) { scope.$$postDigest(function () { $typeahead.select(index); }); }; scope.$isVisible = function () { return $typeahead.$isVisible(); }; // Public methods $typeahead.update = function (matches) { scope.$matches = matches; if (scope.$activeIndex >= matches.length) { scope.$activeIndex = 0; } }; $typeahead.activate = function (index) { scope.$activeIndex = index; }; $typeahead.select = function (index) { var value = scope.$matches[index].value; controller.$setViewValue(value); controller.$render(); scope.$resetMatches(); if (parentScope) parentScope.$digest(); // Emit event scope.$emit('$typeahead.select', value, index); }; // Protected methods $typeahead.$isVisible = function () { if (!options.minLength || !controller) { return !!scope.$matches.length; } // minLength support return scope.$matches.length && angular.isString(controller.$viewValue) && controller.$viewValue.length >= options.minLength; }; $typeahead.$getIndex = function (value) { var l = scope.$matches.length, i = l; if (!l) return; for (i = l; i--;) { if (scope.$matches[i].value === value) break; } if (i < 0) return; return i; }; $typeahead.$onMouseDown = function (evt) { // Prevent blur on mousedown evt.preventDefault(); evt.stopPropagation(); }; $typeahead.$onKeyDown = function (evt) { if (!/(38|40|13)/.test(evt.keyCode)) return; evt.preventDefault(); evt.stopPropagation(); // Select with enter if (evt.keyCode === 13 && scope.$matches.length) { $typeahead.select(scope.$activeIndex); } // Navigate with keyboard else if (evt.keyCode === 38 && scope.$activeIndex > 0) scope.$activeIndex--; else if (evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) scope.$activeIndex++; else if (angular.isUndefined(scope.$activeIndex)) scope.$activeIndex = 0; scope.$digest(); }; // Overrides var show = $typeahead.show; $typeahead.show = function () { show(); setTimeout(function () { $typeahead.$element.on('mousedown', $typeahead.$onMouseDown); if (options.keyboard) { element.on('keydown', $typeahead.$onKeyDown); } }); }; var hide = $typeahead.hide; $typeahead.hide = function () { $typeahead.$element.off('mousedown', $typeahead.$onMouseDown); if (options.keyboard) { element.off('keydown', $typeahead.$onKeyDown); } hide(); }; return $typeahead; } TypeaheadFactory.defaults = defaults; return TypeaheadFactory; } ]; }).directive('bsTypeahead', [ '$window', '$parse', '$q', '$typeahead', '$parseOptions', function ($window, $parse, $q, $typeahead, $parseOptions) { var defaults = $typeahead.defaults; return { restrict: 'EAC', require: 'ngModel', link: function postLink(scope, element, attr, controller) { // Directive options var options = { scope: scope }; angular.forEach([ 'placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'template', 'filter', 'limit', 'minLength' ], function (key) { if (angular.isDefined(attr[key])) options[key] = attr[key]; }); // Build proper ngOptions var filter = options.filter || defaults.filter; var limit = options.limit || defaults.limit; var ngOptions = attr.ngOptions; if (filter) ngOptions += ' | ' + filter + ':$viewValue'; if (limit) ngOptions += ' | limitTo:' + limit; var parsedOptions = $parseOptions(ngOptions); // Initialize typeahead var typeahead = $typeahead(element, controller, options); // Watch model for changes scope.$watch(attr.ngModel, function (newValue, oldValue) { // console.warn('$watch', element.attr('ng-model'), newValue); scope.$modelValue = newValue; // Publish modelValue on scope for custom templates parsedOptions.valuesFn(scope, controller).then(function (values) { if (values.length > limit) values = values.slice(0, limit); // Do not re-queue an update if a correct value has been selected if (values.length === 1 && values[0].value === newValue) return; typeahead.update(values); // Queue a new rendering that will leverage collection loading controller.$render(); }); }); // Model rendering in view controller.$render = function () { // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue); if (controller.$isEmpty(controller.$viewValue)) return element.val(''); var index = typeahead.$getIndex(controller.$modelValue); var selected = angular.isDefined(index) ? typeahead.$scope.$matches[index].label : controller.$viewValue; selected = angular.isObject(selected) ? selected.label : selected; element.val(selected.replace(/<(?:.|\n)*?>/gm, '').trim()); }; // Garbage collection scope.$on('$destroy', function () { typeahead.destroy(); options = null; typeahead = null; }); } }; } ]);