PageRenderTime 26ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/public/lib/angular-strap/src/typeahead/typeahead.js

https://gitlab.com/Ronconi/MEAN-Skeleton
JavaScript | 331 lines | 233 code | 55 blank | 43 comment | 57 complexity | 4d02cd38cf576a22b2f5068397ccbb2f MD5 | raw file
  1. 'use strict';
  2. angular.module('mgcrea.ngStrap.typeahead', ['mgcrea.ngStrap.tooltip', 'mgcrea.ngStrap.helpers.parseOptions'])
  3. .provider('$typeahead', function () {
  4. var defaults = this.defaults = {
  5. animation: 'am-fade',
  6. prefixClass: 'typeahead',
  7. prefixEvent: '$typeahead',
  8. placement: 'bottom-left',
  9. templateUrl: 'typeahead/typeahead.tpl.html',
  10. trigger: 'focus',
  11. container: false,
  12. keyboard: true,
  13. html: false,
  14. delay: 0,
  15. minLength: 1,
  16. filter: 'bsAsyncFilter',
  17. limit: 6,
  18. autoSelect: false,
  19. comparator: '',
  20. trimValue: true
  21. };
  22. this.$get = function ($window, $rootScope, $tooltip, $$rAF, $timeout) {
  23. function TypeaheadFactory (element, controller, config) {
  24. var $typeahead = {};
  25. // Common vars
  26. var options = angular.extend({}, defaults, config);
  27. $typeahead = $tooltip(element, options);
  28. var parentScope = config.scope;
  29. var scope = $typeahead.$scope;
  30. scope.$resetMatches = function () {
  31. scope.$matches = [];
  32. scope.$activeIndex = options.autoSelect ? 0 : -1; // If set to 0, the first match will be highlighted
  33. };
  34. scope.$resetMatches();
  35. scope.$activate = function (index) {
  36. scope.$$postDigest(function () {
  37. $typeahead.activate(index);
  38. });
  39. };
  40. scope.$select = function (index, evt) {
  41. scope.$$postDigest(function () {
  42. $typeahead.select(index);
  43. });
  44. };
  45. scope.$isVisible = function () {
  46. return $typeahead.$isVisible();
  47. };
  48. // Public methods
  49. $typeahead.update = function (matches) {
  50. scope.$matches = matches;
  51. if (scope.$activeIndex >= matches.length) {
  52. scope.$activeIndex = options.autoSelect ? 0 : -1;
  53. }
  54. // wrap in a $timeout so the results are updated
  55. // before repositioning
  56. safeDigest(scope);
  57. $$rAF($typeahead.$applyPlacement);
  58. };
  59. $typeahead.activate = function (index) {
  60. scope.$activeIndex = index;
  61. };
  62. $typeahead.select = function (index) {
  63. if (index === -1) return;
  64. var value = scope.$matches[index].value;
  65. // console.log('$setViewValue', value);
  66. controller.$setViewValue(value);
  67. controller.$render();
  68. scope.$resetMatches();
  69. if (parentScope) parentScope.$digest();
  70. // Emit event
  71. scope.$emit(options.prefixEvent + '.select', value, index, $typeahead);
  72. if (angular.isDefined(options.onSelect) && angular.isFunction(options.onSelect)) {
  73. options.onSelect(value, index, $typeahead);
  74. }
  75. };
  76. // Protected methods
  77. $typeahead.$isVisible = function () {
  78. if (!options.minLength || !controller) {
  79. return !!scope.$matches.length;
  80. }
  81. // minLength support
  82. return scope.$matches.length && angular.isString(controller.$viewValue) && controller.$viewValue.length >= options.minLength;
  83. };
  84. $typeahead.$getIndex = function (value) {
  85. var index;
  86. for (index = scope.$matches.length; index--;) {
  87. if (angular.equals(scope.$matches[index].value, value)) break;
  88. }
  89. return index;
  90. };
  91. $typeahead.$onMouseDown = function (evt) {
  92. // Prevent blur on mousedown
  93. evt.preventDefault();
  94. evt.stopPropagation();
  95. };
  96. $typeahead.$onKeyDown = function (evt) {
  97. if (!/(38|40|13)/.test(evt.keyCode)) return;
  98. // Let ngSubmit pass if the typeahead tip is hidden or no option is selected
  99. if ($typeahead.$isVisible() && !(evt.keyCode === 13 && scope.$activeIndex === -1)) {
  100. evt.preventDefault();
  101. evt.stopPropagation();
  102. }
  103. // Select with enter
  104. if (evt.keyCode === 13 && scope.$matches.length) {
  105. $typeahead.select(scope.$activeIndex);
  106. // Navigate with keyboard
  107. } else if (evt.keyCode === 38 && scope.$activeIndex > 0) {
  108. scope.$activeIndex--;
  109. } else if (evt.keyCode === 40 && scope.$activeIndex < scope.$matches.length - 1) {
  110. scope.$activeIndex++;
  111. } else if (angular.isUndefined(scope.$activeIndex)) {
  112. scope.$activeIndex = 0;
  113. }
  114. scope.$digest();
  115. };
  116. // Overrides
  117. var show = $typeahead.show;
  118. $typeahead.show = function () {
  119. show();
  120. // use timeout to hookup the events to prevent
  121. // event bubbling from being processed immediately.
  122. $timeout(function () {
  123. if ($typeahead.$element) {
  124. $typeahead.$element.on('mousedown', $typeahead.$onMouseDown);
  125. if (options.keyboard) {
  126. if (element) element.on('keydown', $typeahead.$onKeyDown);
  127. }
  128. }
  129. }, 0, false);
  130. };
  131. var hide = $typeahead.hide;
  132. $typeahead.hide = function () {
  133. if ($typeahead.$element) $typeahead.$element.off('mousedown', $typeahead.$onMouseDown);
  134. if (options.keyboard) {
  135. if (element) element.off('keydown', $typeahead.$onKeyDown);
  136. }
  137. if (!options.autoSelect) {
  138. $typeahead.activate(-1);
  139. }
  140. hide();
  141. };
  142. return $typeahead;
  143. }
  144. // Helper functions
  145. function safeDigest (scope) {
  146. /* eslint-disable no-unused-expressions */
  147. scope.$$phase || (scope.$root && scope.$root.$$phase) || scope.$digest();
  148. /* eslint-enable no-unused-expressions */
  149. }
  150. TypeaheadFactory.defaults = defaults;
  151. return TypeaheadFactory;
  152. };
  153. })
  154. .filter('bsAsyncFilter', function ($filter) {
  155. return function (array, expression, comparator) {
  156. if (array && angular.isFunction(array.then)) {
  157. return array.then(function (results) {
  158. return $filter('filter')(results, expression, comparator);
  159. });
  160. }
  161. return $filter('filter')(array, expression, comparator);
  162. };
  163. })
  164. .directive('bsTypeahead', function ($window, $parse, $q, $typeahead, $parseOptions) {
  165. var defaults = $typeahead.defaults;
  166. return {
  167. restrict: 'EAC',
  168. require: 'ngModel',
  169. link: function postLink (scope, element, attr, controller) {
  170. // Fixes firefox bug when using objects in model with typeahead
  171. // Yes this breaks any other directive using a 'change' event on this input,
  172. // but if it is using the 'change' event why is it used with typeahead?
  173. element.off('change');
  174. // Directive options
  175. var options = {
  176. scope: scope
  177. };
  178. angular.forEach(['template', 'templateUrl', 'controller', 'controllerAs', 'placement', 'container', 'delay', 'trigger', 'keyboard', 'html', 'animation', 'filter', 'limit', 'minLength', 'watchOptions', 'selectMode', 'autoSelect', 'comparator', 'id', 'prefixEvent', 'prefixClass'], function (key) {
  179. if (angular.isDefined(attr[key])) options[key] = attr[key];
  180. });
  181. // use string regex match boolean attr falsy values, leave truthy values be
  182. var falseValueRegExp = /^(false|0|)$/i;
  183. angular.forEach(['html', 'container', 'trimValue', 'filter'], function (key) {
  184. if (angular.isDefined(attr[key]) && falseValueRegExp.test(attr[key])) options[key] = false;
  185. });
  186. // bind functions from the attrs to the show, hide and select events
  187. angular.forEach(['onBeforeShow', 'onShow', 'onBeforeHide', 'onHide', 'onSelect'], function (key) {
  188. var bsKey = 'bs' + key.charAt(0).toUpperCase() + key.slice(1);
  189. if (angular.isDefined(attr[bsKey])) {
  190. options[key] = scope.$eval(attr[bsKey]);
  191. }
  192. });
  193. // Disable browser autocompletion
  194. if (!element.attr('autocomplete')) element.attr('autocomplete', 'off');
  195. // Build proper bsOptions
  196. var filter = angular.isDefined(options.filter) ? options.filter : defaults.filter;
  197. var limit = options.limit || defaults.limit;
  198. var comparator = options.comparator || defaults.comparator;
  199. var bsOptions = attr.bsOptions;
  200. if (filter) {
  201. bsOptions += ' | ' + filter + ':$viewValue';
  202. if (comparator) bsOptions += ':' + comparator;
  203. }
  204. if (limit) bsOptions += ' | limitTo:' + limit;
  205. var parsedOptions = $parseOptions(bsOptions);
  206. // Initialize typeahead
  207. var typeahead = $typeahead(element, controller, options);
  208. // Watch options on demand
  209. if (options.watchOptions) {
  210. // Watch bsOptions values before filtering for changes, drop function calls
  211. var watchedOptions = parsedOptions.$match[7].replace(/\|.+/, '').replace(/\(.*\)/g, '').trim();
  212. scope.$watchCollection(watchedOptions, function (newValue, oldValue) {
  213. // console.warn('scope.$watch(%s)', watchedOptions, newValue, oldValue);
  214. parsedOptions.valuesFn(scope, controller).then(function (values) {
  215. typeahead.update(values);
  216. controller.$render();
  217. });
  218. });
  219. }
  220. // Watch model for changes
  221. scope.$watch(attr.ngModel, function (newValue, oldValue) {
  222. // console.warn('$watch', element.attr('ng-model'), newValue);
  223. scope.$modelValue = newValue; // Publish modelValue on scope for custom templates
  224. parsedOptions.valuesFn(scope, controller)
  225. .then(function (values) {
  226. // Prevent input with no future prospect if selectMode is truthy
  227. // @TODO test selectMode
  228. if (options.selectMode && !values.length && newValue.length > 0) {
  229. controller.$setViewValue(controller.$viewValue.substring(0, controller.$viewValue.length - 1));
  230. return;
  231. }
  232. if (values.length > limit) values = values.slice(0, limit);
  233. typeahead.update(values);
  234. // Queue a new rendering that will leverage collection loading
  235. controller.$render();
  236. });
  237. });
  238. // modelValue -> $formatters -> viewValue
  239. controller.$formatters.push(function (modelValue) {
  240. // console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
  241. var displayValue = parsedOptions.displayValue(modelValue);
  242. // If we can determine the displayValue, use that
  243. if (displayValue) {
  244. return displayValue;
  245. }
  246. // If there's no display value, attempt to use the modelValue.
  247. // If the model is an object not much we can do
  248. if (angular.isDefined(modelValue) && typeof modelValue !== 'object') {
  249. return modelValue;
  250. }
  251. return '';
  252. });
  253. // Model rendering in view
  254. controller.$render = function () {
  255. // console.warn('$render', element.attr('ng-model'), 'controller.$modelValue', typeof controller.$modelValue, controller.$modelValue, 'controller.$viewValue', typeof controller.$viewValue, controller.$viewValue);
  256. if (controller.$isEmpty(controller.$viewValue)) {
  257. return element.val('');
  258. }
  259. var index = typeahead.$getIndex(controller.$modelValue);
  260. var selected = index !== -1 ? typeahead.$scope.$matches[index].label : controller.$viewValue;
  261. selected = angular.isObject(selected) ? parsedOptions.displayValue(selected) : selected;
  262. var value = selected ? selected.toString().replace(/<(?:.|\n)*?>/gm, '') : '';
  263. var ss = element[0].selectionStart;
  264. var sd = element[0].selectionEnd;
  265. element.val(options.trimValue === false ? value : value.trim());
  266. element[0].setSelectionRange(ss, sd);
  267. };
  268. // Garbage collection
  269. scope.$on('$destroy', function () {
  270. if (typeahead) typeahead.destroy();
  271. options = null;
  272. typeahead = null;
  273. });
  274. }
  275. };
  276. });