ion-autocomplete.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /*
  2. * ion-autocomplete 0.3.2
  3. * Copyright 2016 Danny Povolotski
  4. * Copyright modifications 2016 Guy Brand
  5. * https://github.com/guylabs/ion-autocomplete
  6. */
  7. (function() {
  8. 'use strict';
  9. angular.module('ion-autocomplete', []).directive('ionAutocomplete', [
  10. '$ionicBackdrop', '$ionicScrollDelegate', '$document', '$q', '$parse', '$interpolate', '$ionicPlatform', '$compile', '$templateRequest',
  11. function ($ionicBackdrop, $ionicScrollDelegate, $document, $q, $parse, $interpolate, $ionicPlatform, $compile, $templateRequest) {
  12. return {
  13. require: ['ngModel', 'ionAutocomplete'],
  14. restrict: 'A',
  15. scope: {},
  16. bindToController: {
  17. ngModel: '=',
  18. externalModel: '=',
  19. templateData: '=',
  20. itemsMethod: '&',
  21. itemsClickedMethod: '&',
  22. itemsRemovedMethod: '&',
  23. modelToItemMethod: '&',
  24. cancelButtonClickedMethod: '&',
  25. placeholder: '@',
  26. cancelLabel: '@',
  27. selectItemsLabel: '@',
  28. selectedItemsLabel: '@'
  29. },
  30. controllerAs: 'viewModel',
  31. controller: ['$attrs', '$timeout', '$scope', function ($attrs, $timeout, $scope) {
  32. var valueOrDefault = function (value, defaultValue) {
  33. return !value ? defaultValue : value;
  34. };
  35. var controller = this;
  36. // set the default values of the one way binded attributes
  37. $timeout(function () {
  38. controller.placeholder = valueOrDefault(controller.placeholder, 'Click to enter a value...');
  39. controller.cancelLabel = valueOrDefault(controller.cancelLabel, 'Done');
  40. controller.selectItemsLabel = valueOrDefault(controller.selectItemsLabel, "Select an item...");
  41. controller.selectedItemsLabel = valueOrDefault(controller.selectedItemsLabel, $interpolate("Selected items{{maxSelectedItems ? ' (max. ' + maxSelectedItems + ')' : ''}}:")(controller));
  42. });
  43. // set the default values of the passed in attributes
  44. this.maxSelectedItems = valueOrDefault($attrs.maxSelectedItems, undefined);
  45. this.templateUrl = valueOrDefault($attrs.templateUrl, undefined);
  46. this.itemsMethodValueKey = valueOrDefault($attrs.itemsMethodValueKey, undefined);
  47. this.itemValueKey = valueOrDefault($attrs.itemValueKey, undefined);
  48. this.itemViewValueKey = valueOrDefault($attrs.itemViewValueKey, undefined);
  49. this.componentId = valueOrDefault($attrs.componentId, undefined);
  50. this.loadingIcon = valueOrDefault($attrs.loadingIcon, undefined);
  51. this.manageExternally = valueOrDefault($attrs.manageExternally, "false");
  52. this.ngModelOptions = valueOrDefault($scope.$eval($attrs.ngModelOptions), {});
  53. // loading flag if the items-method is a function
  54. this.showLoadingIcon = false;
  55. // the items, selected items and the query for the list
  56. this.searchItems = [];
  57. this.selectedItems = [];
  58. this.searchQuery = undefined;
  59. this.isArray = function (array) {
  60. return angular.isArray(array);
  61. };
  62. }],
  63. link: function (scope, element, attrs, controllers) {
  64. // get the two needed controllers
  65. var ngModelController = controllers[0];
  66. var ionAutocompleteController = controllers[1];
  67. // use a random css class to bind the modal to the component
  68. ionAutocompleteController.randomCssClass = "ion-autocomplete-random-" + Math.floor((Math.random() * 1000) + 1);
  69. var template = [
  70. '<div class="ion-autocomplete-container ' + ionAutocompleteController.randomCssClass + ' modal" style="display: none;">',
  71. '<div class="bar bar-header item-input-inset">',
  72. '<label class="item-input-wrapper">',
  73. '<i class="icon ion-search placeholder-icon"></i>',
  74. '<input type="search" class="ion-autocomplete-search" ng-model="viewModel.searchQuery" ng-model-options="viewModel.ngModelOptions" placeholder="{{viewModel.placeholder}}"/>',
  75. '</label>',
  76. '<div class="ion-autocomplete-loading-icon" ng-if="viewModel.showLoadingIcon && viewModel.loadingIcon"><ion-spinner icon="{{viewModel.loadingIcon}}"></ion-spinner></div>',
  77. '<button class="ion-autocomplete-cancel button button-clear" ng-click="viewModel.cancelClick()">{{viewModel.cancelLabel}}</button>',
  78. '</div>',
  79. '<ion-content class="has-header">',
  80. '<ion-item class="item-divider">{{viewModel.selectedItemsLabel}}</ion-item>',
  81. '<ion-item ng-if="viewModel.isArray(viewModel.selectedItems)" ng-repeat="selectedItem in viewModel.selectedItems track by $index" class="item-icon-left item-icon-right item-text-wrap">',
  82. '<i class="icon ion-checkmark"></i>',
  83. '{{viewModel.getItemValue(selectedItem, viewModel.itemViewValueKey)}}',
  84. '<i class="icon ion-trash-a" style="cursor:pointer" ng-click="viewModel.removeItem($index)"></i>',
  85. '</ion-item>',
  86. '<ion-item ng-if="!viewModel.isArray(viewModel.selectedItems)" class="item-icon-left item-icon-right item-text-wrap">',
  87. '<i class="icon ion-checkmark"></i>',
  88. '{{viewModel.getItemValue(viewModel.selectedItems, viewModel.itemViewValueKey)}}',
  89. '<i class="icon ion-trash-a" style="cursor:pointer" ng-click="viewModel.removeItem(0)"></i>',
  90. '</ion-item>',
  91. '<ion-item class="item-divider" ng-if="viewModel.searchItems.length > 0">{{viewModel.selectItemsLabel}}</ion-item>',
  92. '<ion-item ng-repeat="item in viewModel.searchItems" item-height="55px" item-width="100%" ng-click="viewModel.selectItem(item)" class="item-text-wrap">',
  93. '{{viewModel.getItemValue(item, viewModel.itemViewValueKey)}}',
  94. '</ion-item>',
  95. '</ion-content>',
  96. '</div>'
  97. ].join('');
  98. // load the template synchronously or asynchronously
  99. $q.when().then(function () {
  100. // first check if a template url is set and use this as template
  101. if (ionAutocompleteController.templateUrl) {
  102. return $templateRequest(ionAutocompleteController.templateUrl);
  103. } else {
  104. return template;
  105. }
  106. }).then(function (template) {
  107. // compile the template
  108. var searchInputElement = $compile(angular.element(template))(scope);
  109. // append the template to body
  110. $document.find('body').append(searchInputElement);
  111. // returns the value of an item
  112. ionAutocompleteController.getItemValue = function (item, key) {
  113. // if it's an array, go through all items and add the values to a new array and return it
  114. if (angular.isArray(item)) {
  115. var items = [];
  116. angular.forEach(item, function (itemValue) {
  117. if (key && angular.isObject(item)) {
  118. items.push($parse(key)(itemValue));
  119. } else {
  120. items.push(itemValue);
  121. }
  122. });
  123. return items;
  124. } else {
  125. if (key && angular.isObject(item)) {
  126. return $parse(key)(item);
  127. }
  128. }
  129. return item;
  130. };
  131. // function which selects the item, hides the search container and the ionic backdrop if it has not maximum selected items attribute set
  132. ionAutocompleteController.selectItem = function (item) {
  133. // clear the search query when an item is selected
  134. ionAutocompleteController.searchQuery = undefined;
  135. // return if the max selected items is not equal to 1 and the maximum amount of selected items is reached
  136. if (ionAutocompleteController.maxSelectedItems != "1" &&
  137. angular.isArray(ionAutocompleteController.selectedItems) &&
  138. ionAutocompleteController.maxSelectedItems == ionAutocompleteController.selectedItems.length) {
  139. return;
  140. }
  141. // store the selected items
  142. if (!isKeyValueInObjectArray(ionAutocompleteController.selectedItems,
  143. ionAutocompleteController.itemValueKey, ionAutocompleteController.getItemValue(item, ionAutocompleteController.itemValueKey))) {
  144. // if it is a single select set the item directly
  145. if (ionAutocompleteController.maxSelectedItems == "1") {
  146. ionAutocompleteController.selectedItems = item;
  147. } else {
  148. // create a new array to update the model. See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
  149. ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.concat([item]);
  150. }
  151. }
  152. // set the view value and render it
  153. ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
  154. ngModelController.$render();
  155. // hide the container and the ionic backdrop if it is a single select to enhance usability
  156. if (ionAutocompleteController.maxSelectedItems == 1) {
  157. ionAutocompleteController.hideModal();
  158. }
  159. // call items clicked callback
  160. if (angular.isDefined(attrs.itemsClickedMethod)) {
  161. ionAutocompleteController.itemsClickedMethod({
  162. callback: {
  163. item: item,
  164. selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
  165. componentId: ionAutocompleteController.componentId
  166. }
  167. });
  168. }
  169. };
  170. // function which removes the item from the selected items.
  171. ionAutocompleteController.removeItem = function (index) {
  172. // clear the selected items if just one item is selected
  173. if (!angular.isArray(ionAutocompleteController.selectedItems)) {
  174. ionAutocompleteController.selectedItems = [];
  175. } else {
  176. // remove the item from the selected items and create a copy of the array to update the model.
  177. // See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
  178. var removed = ionAutocompleteController.selectedItems.splice(index, 1)[0];
  179. ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.slice();
  180. }
  181. // set the view value and render it
  182. ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
  183. ngModelController.$render();
  184. // call items clicked callback
  185. if (angular.isDefined(attrs.itemsRemovedMethod)) {
  186. ionAutocompleteController.itemsRemovedMethod({
  187. callback: {
  188. item: removed,
  189. selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
  190. componentId: ionAutocompleteController.componentId
  191. }
  192. });
  193. }
  194. };
  195. // watcher on the search field model to update the list according to the input
  196. scope.$watch('viewModel.searchQuery', function (query) {
  197. ionAutocompleteController.fetchSearchQuery(query, false);
  198. });
  199. // update the search items based on the returned value of the items-method
  200. ionAutocompleteController.fetchSearchQuery = function (query, isInitializing) {
  201. // right away return if the query is undefined to not call the items method for nothing
  202. if (query === undefined) {
  203. return;
  204. }
  205. if (angular.isDefined(attrs.itemsMethod)) {
  206. // show the loading icon
  207. ionAutocompleteController.showLoadingIcon = true;
  208. var queryObject = {query: query, isInitializing: isInitializing};
  209. // if the component id is set, then add it to the query object
  210. if (ionAutocompleteController.componentId) {
  211. queryObject = {
  212. query: query,
  213. isInitializing: isInitializing,
  214. componentId: ionAutocompleteController.componentId
  215. }
  216. }
  217. // convert the given function to a $q promise to support promises too
  218. var promise = $q.when(ionAutocompleteController.itemsMethod(queryObject));
  219. promise.then(function (promiseData) {
  220. // if the promise data is not set do nothing
  221. if (!promiseData) {
  222. return;
  223. }
  224. // if the given promise data object has a data property use this for the further processing as the
  225. // standard httpPromises from the $http functions store the response data in a data property
  226. if (promiseData && promiseData.data) {
  227. promiseData = promiseData.data;
  228. }
  229. // set the items which are returned by the items method
  230. ionAutocompleteController.searchItems = ionAutocompleteController.getItemValue(promiseData,
  231. ionAutocompleteController.itemsMethodValueKey);
  232. // force the collection repeat to redraw itself as there were issues when the first items were added
  233. $ionicScrollDelegate.resize();
  234. // hide the loading icon
  235. ionAutocompleteController.showLoadingIcon = false;
  236. }, function (error) {
  237. // reject the error because we do not handle the error here
  238. return $q.reject(error);
  239. });
  240. }
  241. };
  242. var searchContainerDisplayed = false;
  243. ionAutocompleteController.showModal = function () {
  244. if (searchContainerDisplayed) {
  245. return;
  246. }
  247. // show the backdrop and the search container
  248. $ionicBackdrop.retain();
  249. angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass)).css('display', 'block');
  250. // hide the container if the back button is pressed
  251. scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function () {
  252. ionAutocompleteController.hideModal();
  253. }, 300);
  254. // get the compiled search field
  255. var searchInputElement = angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass + ' input'));
  256. // focus on the search input field
  257. if (searchInputElement.length > 0) {
  258. searchInputElement[0].focus();
  259. setTimeout(function () {
  260. searchInputElement[0].focus();
  261. }, 0);
  262. }
  263. // force the collection repeat to redraw itself as there were issues when the first items were added
  264. $ionicScrollDelegate.resize();
  265. searchContainerDisplayed = true;
  266. };
  267. ionAutocompleteController.hideModal = function () {
  268. angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass)).css('display', 'none');
  269. ionAutocompleteController.searchQuery = undefined;
  270. $ionicBackdrop.release();
  271. scope.$deregisterBackButton && scope.$deregisterBackButton();
  272. searchContainerDisplayed = false;
  273. };
  274. // object to store if the user moved the finger to prevent opening the modal
  275. var scrolling = {
  276. moved: false,
  277. startX: 0,
  278. startY: 0
  279. };
  280. // store the start coordinates of the touch start event
  281. var onTouchStart = function (e) {
  282. scrolling.moved = false;
  283. // Use originalEvent when available, fix compatibility with jQuery
  284. if (typeof(e.originalEvent) !== 'undefined') {
  285. e = e.originalEvent;
  286. }
  287. scrolling.startX = e.touches[0].clientX;
  288. scrolling.startY = e.touches[0].clientY;
  289. };
  290. // check if the finger moves more than 10px and set the moved flag to true
  291. var onTouchMove = function (e) {
  292. // Use originalEvent when available, fix compatibility with jQuery
  293. if (typeof(e.originalEvent) !== 'undefined') {
  294. e = e.originalEvent;
  295. }
  296. if (Math.abs(e.touches[0].clientX - scrolling.startX) > 10 ||
  297. Math.abs(e.touches[0].clientY - scrolling.startY) > 10) {
  298. scrolling.moved = true;
  299. }
  300. };
  301. // click handler on the input field to show the search container
  302. var onClick = function (event) {
  303. // only open the dialog if was not touched at the beginning of a legitimate scroll event
  304. if (scrolling.moved) {
  305. return;
  306. }
  307. // prevent the default event and the propagation
  308. event.preventDefault();
  309. event.stopPropagation();
  310. // call the fetch search query method once to be able to initialize it when the modal is shown
  311. // use an empty string to signal that there is no change in the search query
  312. ionAutocompleteController.fetchSearchQuery("", true);
  313. // show the ionic backdrop and the search container
  314. ionAutocompleteController.showModal();
  315. };
  316. var isKeyValueInObjectArray = function (objectArray, key, value) {
  317. if (angular.isArray(objectArray)) {
  318. for (var i = 0; i < objectArray.length; i++) {
  319. if (ionAutocompleteController.getItemValue(objectArray[i], key) === value) {
  320. return true;
  321. }
  322. }
  323. }
  324. return false;
  325. };
  326. // function to call the model to item method and select the item
  327. var resolveAndSelectModelItem = function (modelValue) {
  328. // convert the given function to a $q promise to support promises too
  329. var promise = $q.when(ionAutocompleteController.modelToItemMethod({modelValue: modelValue}));
  330. promise.then(function (promiseData) {
  331. // select the item which are returned by the model to item method
  332. ionAutocompleteController.selectItem(promiseData);
  333. }, function (error) {
  334. // reject the error because we do not handle the error here
  335. return $q.reject(error);
  336. });
  337. };
  338. // if the click is not handled externally, bind the handlers to the click and touch events of the input field
  339. if (ionAutocompleteController.manageExternally == "false") {
  340. element.bind('touchstart', onTouchStart);
  341. element.bind('touchmove', onTouchMove);
  342. element.bind('touchend click focus', onClick);
  343. }
  344. // cancel handler for the cancel button which clears the search input field model and hides the
  345. // search container and the ionic backdrop and calls the cancel button clicked callback
  346. ionAutocompleteController.cancelClick = function () {
  347. ionAutocompleteController.hideModal();
  348. // call cancel button clicked callback
  349. if (angular.isDefined(attrs.cancelButtonClickedMethod)) {
  350. ionAutocompleteController.cancelButtonClickedMethod({
  351. callback: {
  352. selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
  353. componentId: ionAutocompleteController.componentId
  354. }
  355. });
  356. }
  357. };
  358. // watch the external model for changes and select the items inside the model
  359. scope.$watch("viewModel.externalModel", function (newModel) {
  360. if (angular.isArray(newModel) && newModel.length == 0) {
  361. // clear the selected items and set the view value and render it
  362. ionAutocompleteController.selectedItems = [];
  363. ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
  364. ngModelController.$render();
  365. return;
  366. }
  367. // prepopulate view and selected items if external model is already set
  368. if (newModel && angular.isDefined(attrs.modelToItemMethod)) {
  369. if (angular.isArray(newModel)) {
  370. ionAutocompleteController.selectedItems = [];
  371. angular.forEach(newModel, function (modelValue) {
  372. resolveAndSelectModelItem(modelValue);
  373. })
  374. } else {
  375. resolveAndSelectModelItem(newModel);
  376. }
  377. }
  378. });
  379. // remove the component from the dom when scope is getting destroyed
  380. scope.$on('$destroy', function () {
  381. // angular takes care of cleaning all $watch's and listeners, but we still need to remove the modal
  382. searchInputElement.remove();
  383. });
  384. // render the view value of the model
  385. ngModelController.$render = function () {
  386. element.val(ionAutocompleteController.getItemValue(ngModelController.$viewValue, ionAutocompleteController.itemViewValueKey));
  387. };
  388. // set the view value of the model
  389. ngModelController.$formatters.push(function (modelValue) {
  390. var viewValue = ionAutocompleteController.getItemValue(modelValue, ionAutocompleteController.itemViewValueKey);
  391. return viewValue == undefined ? "" : viewValue;
  392. });
  393. // set the model value of the model
  394. ngModelController.$parsers.push(function (viewValue) {
  395. return ionAutocompleteController.getItemValue(viewValue, ionAutocompleteController.itemValueKey);
  396. });
  397. });
  398. }
  399. };
  400. }
  401. ]);
  402. })();