123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- /*
- * ion-autocomplete 0.3.2
- * Copyright 2016 Danny Povolotski
- * Copyright modifications 2016 Guy Brand
- * https://github.com/guylabs/ion-autocomplete
- */
- (function() {
- 'use strict';
- angular.module('ion-autocomplete', []).directive('ionAutocomplete', [
- '$ionicBackdrop', '$ionicScrollDelegate', '$document', '$q', '$parse', '$interpolate', '$ionicPlatform', '$compile', '$templateRequest',
- function ($ionicBackdrop, $ionicScrollDelegate, $document, $q, $parse, $interpolate, $ionicPlatform, $compile, $templateRequest) {
- return {
- require: ['ngModel', 'ionAutocomplete'],
- restrict: 'A',
- scope: {},
- bindToController: {
- ngModel: '=',
- externalModel: '=',
- templateData: '=',
- itemsMethod: '&',
- itemsClickedMethod: '&',
- itemsRemovedMethod: '&',
- modelToItemMethod: '&',
- cancelButtonClickedMethod: '&',
- placeholder: '@',
- cancelLabel: '@',
- selectItemsLabel: '@',
- selectedItemsLabel: '@'
- },
- controllerAs: 'viewModel',
- controller: ['$attrs', '$timeout', '$scope', function ($attrs, $timeout, $scope) {
- var valueOrDefault = function (value, defaultValue) {
- return !value ? defaultValue : value;
- };
- var controller = this;
- // set the default values of the one way binded attributes
- $timeout(function () {
- controller.placeholder = valueOrDefault(controller.placeholder, 'Click to enter a value...');
- controller.cancelLabel = valueOrDefault(controller.cancelLabel, 'Done');
- controller.selectItemsLabel = valueOrDefault(controller.selectItemsLabel, "Select an item...");
- controller.selectedItemsLabel = valueOrDefault(controller.selectedItemsLabel, $interpolate("Selected items{{maxSelectedItems ? ' (max. ' + maxSelectedItems + ')' : ''}}:")(controller));
- });
- // set the default values of the passed in attributes
- this.maxSelectedItems = valueOrDefault($attrs.maxSelectedItems, undefined);
- this.templateUrl = valueOrDefault($attrs.templateUrl, undefined);
- this.itemsMethodValueKey = valueOrDefault($attrs.itemsMethodValueKey, undefined);
- this.itemValueKey = valueOrDefault($attrs.itemValueKey, undefined);
- this.itemViewValueKey = valueOrDefault($attrs.itemViewValueKey, undefined);
- this.componentId = valueOrDefault($attrs.componentId, undefined);
- this.loadingIcon = valueOrDefault($attrs.loadingIcon, undefined);
- this.manageExternally = valueOrDefault($attrs.manageExternally, "false");
- this.ngModelOptions = valueOrDefault($scope.$eval($attrs.ngModelOptions), {});
- // loading flag if the items-method is a function
- this.showLoadingIcon = false;
- // the items, selected items and the query for the list
- this.searchItems = [];
- this.selectedItems = [];
- this.searchQuery = undefined;
- this.isArray = function (array) {
- return angular.isArray(array);
- };
- }],
- link: function (scope, element, attrs, controllers) {
- // get the two needed controllers
- var ngModelController = controllers[0];
- var ionAutocompleteController = controllers[1];
- // use a random css class to bind the modal to the component
- ionAutocompleteController.randomCssClass = "ion-autocomplete-random-" + Math.floor((Math.random() * 1000) + 1);
- var template = [
- '<div class="ion-autocomplete-container ' + ionAutocompleteController.randomCssClass + ' modal" style="display: none;">',
- '<div class="bar bar-header item-input-inset">',
- '<label class="item-input-wrapper">',
- '<i class="icon ion-search placeholder-icon"></i>',
- '<input type="search" class="ion-autocomplete-search" ng-model="viewModel.searchQuery" ng-model-options="viewModel.ngModelOptions" placeholder="{{viewModel.placeholder}}"/>',
- '</label>',
- '<div class="ion-autocomplete-loading-icon" ng-if="viewModel.showLoadingIcon && viewModel.loadingIcon"><ion-spinner icon="{{viewModel.loadingIcon}}"></ion-spinner></div>',
- '<button class="ion-autocomplete-cancel button button-clear" ng-click="viewModel.cancelClick()">{{viewModel.cancelLabel}}</button>',
- '</div>',
- '<ion-content class="has-header">',
- '<ion-item class="item-divider">{{viewModel.selectedItemsLabel}}</ion-item>',
- '<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">',
- '<i class="icon ion-checkmark"></i>',
- '{{viewModel.getItemValue(selectedItem, viewModel.itemViewValueKey)}}',
- '<i class="icon ion-trash-a" style="cursor:pointer" ng-click="viewModel.removeItem($index)"></i>',
- '</ion-item>',
- '<ion-item ng-if="!viewModel.isArray(viewModel.selectedItems)" class="item-icon-left item-icon-right item-text-wrap">',
- '<i class="icon ion-checkmark"></i>',
- '{{viewModel.getItemValue(viewModel.selectedItems, viewModel.itemViewValueKey)}}',
- '<i class="icon ion-trash-a" style="cursor:pointer" ng-click="viewModel.removeItem(0)"></i>',
- '</ion-item>',
- '<ion-item class="item-divider" ng-if="viewModel.searchItems.length > 0">{{viewModel.selectItemsLabel}}</ion-item>',
- '<ion-item ng-repeat="item in viewModel.searchItems" item-height="55px" item-width="100%" ng-click="viewModel.selectItem(item)" class="item-text-wrap">',
- '{{viewModel.getItemValue(item, viewModel.itemViewValueKey)}}',
- '</ion-item>',
- '</ion-content>',
- '</div>'
- ].join('');
- // load the template synchronously or asynchronously
- $q.when().then(function () {
- // first check if a template url is set and use this as template
- if (ionAutocompleteController.templateUrl) {
- return $templateRequest(ionAutocompleteController.templateUrl);
- } else {
- return template;
- }
- }).then(function (template) {
- // compile the template
- var searchInputElement = $compile(angular.element(template))(scope);
- // append the template to body
- $document.find('body').append(searchInputElement);
- // returns the value of an item
- ionAutocompleteController.getItemValue = function (item, key) {
- // if it's an array, go through all items and add the values to a new array and return it
- if (angular.isArray(item)) {
- var items = [];
- angular.forEach(item, function (itemValue) {
- if (key && angular.isObject(item)) {
- items.push($parse(key)(itemValue));
- } else {
- items.push(itemValue);
- }
- });
- return items;
- } else {
- if (key && angular.isObject(item)) {
- return $parse(key)(item);
- }
- }
- return item;
- };
- // function which selects the item, hides the search container and the ionic backdrop if it has not maximum selected items attribute set
- ionAutocompleteController.selectItem = function (item) {
- // clear the search query when an item is selected
- ionAutocompleteController.searchQuery = undefined;
- // return if the max selected items is not equal to 1 and the maximum amount of selected items is reached
- if (ionAutocompleteController.maxSelectedItems != "1" &&
- angular.isArray(ionAutocompleteController.selectedItems) &&
- ionAutocompleteController.maxSelectedItems == ionAutocompleteController.selectedItems.length) {
- return;
- }
- // store the selected items
- if (!isKeyValueInObjectArray(ionAutocompleteController.selectedItems,
- ionAutocompleteController.itemValueKey, ionAutocompleteController.getItemValue(item, ionAutocompleteController.itemValueKey))) {
- // if it is a single select set the item directly
- if (ionAutocompleteController.maxSelectedItems == "1") {
- ionAutocompleteController.selectedItems = item;
- } else {
- // create a new array to update the model. See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
- ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.concat([item]);
- }
- }
- // set the view value and render it
- ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
- ngModelController.$render();
- // hide the container and the ionic backdrop if it is a single select to enhance usability
- if (ionAutocompleteController.maxSelectedItems == 1) {
- ionAutocompleteController.hideModal();
- }
- // call items clicked callback
- if (angular.isDefined(attrs.itemsClickedMethod)) {
- ionAutocompleteController.itemsClickedMethod({
- callback: {
- item: item,
- selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
- componentId: ionAutocompleteController.componentId
- }
- });
- }
- };
- // function which removes the item from the selected items.
- ionAutocompleteController.removeItem = function (index) {
- // clear the selected items if just one item is selected
- if (!angular.isArray(ionAutocompleteController.selectedItems)) {
- ionAutocompleteController.selectedItems = [];
- } else {
- // remove the item from the selected items and create a copy of the array to update the model.
- // See https://github.com/angular-ui/ui-select/issues/191#issuecomment-55471732
- var removed = ionAutocompleteController.selectedItems.splice(index, 1)[0];
- ionAutocompleteController.selectedItems = ionAutocompleteController.selectedItems.slice();
- }
- // set the view value and render it
- ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
- ngModelController.$render();
- // call items clicked callback
- if (angular.isDefined(attrs.itemsRemovedMethod)) {
- ionAutocompleteController.itemsRemovedMethod({
- callback: {
- item: removed,
- selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
- componentId: ionAutocompleteController.componentId
- }
- });
- }
- };
- // watcher on the search field model to update the list according to the input
- scope.$watch('viewModel.searchQuery', function (query) {
- ionAutocompleteController.fetchSearchQuery(query, false);
- });
- // update the search items based on the returned value of the items-method
- ionAutocompleteController.fetchSearchQuery = function (query, isInitializing) {
- // right away return if the query is undefined to not call the items method for nothing
- if (query === undefined) {
- return;
- }
- if (angular.isDefined(attrs.itemsMethod)) {
- // show the loading icon
- ionAutocompleteController.showLoadingIcon = true;
- var queryObject = {query: query, isInitializing: isInitializing};
- // if the component id is set, then add it to the query object
- if (ionAutocompleteController.componentId) {
- queryObject = {
- query: query,
- isInitializing: isInitializing,
- componentId: ionAutocompleteController.componentId
- }
- }
- // convert the given function to a $q promise to support promises too
- var promise = $q.when(ionAutocompleteController.itemsMethod(queryObject));
- promise.then(function (promiseData) {
- // if the promise data is not set do nothing
- if (!promiseData) {
- return;
- }
- // if the given promise data object has a data property use this for the further processing as the
- // standard httpPromises from the $http functions store the response data in a data property
- if (promiseData && promiseData.data) {
- promiseData = promiseData.data;
- }
- // set the items which are returned by the items method
- ionAutocompleteController.searchItems = ionAutocompleteController.getItemValue(promiseData,
- ionAutocompleteController.itemsMethodValueKey);
- // force the collection repeat to redraw itself as there were issues when the first items were added
- $ionicScrollDelegate.resize();
- // hide the loading icon
- ionAutocompleteController.showLoadingIcon = false;
- }, function (error) {
- // reject the error because we do not handle the error here
- return $q.reject(error);
- });
- }
- };
- var searchContainerDisplayed = false;
- ionAutocompleteController.showModal = function () {
- if (searchContainerDisplayed) {
- return;
- }
- // show the backdrop and the search container
- $ionicBackdrop.retain();
- angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass)).css('display', 'block');
- // hide the container if the back button is pressed
- scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(function () {
- ionAutocompleteController.hideModal();
- }, 300);
- // get the compiled search field
- var searchInputElement = angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass + ' input'));
- // focus on the search input field
- if (searchInputElement.length > 0) {
- searchInputElement[0].focus();
- setTimeout(function () {
- searchInputElement[0].focus();
- }, 0);
- }
- // force the collection repeat to redraw itself as there were issues when the first items were added
- $ionicScrollDelegate.resize();
- searchContainerDisplayed = true;
- };
- ionAutocompleteController.hideModal = function () {
- angular.element($document[0].querySelector('div.ion-autocomplete-container.' + ionAutocompleteController.randomCssClass)).css('display', 'none');
- ionAutocompleteController.searchQuery = undefined;
- $ionicBackdrop.release();
- scope.$deregisterBackButton && scope.$deregisterBackButton();
- searchContainerDisplayed = false;
- };
- // object to store if the user moved the finger to prevent opening the modal
- var scrolling = {
- moved: false,
- startX: 0,
- startY: 0
- };
- // store the start coordinates of the touch start event
- var onTouchStart = function (e) {
- scrolling.moved = false;
- // Use originalEvent when available, fix compatibility with jQuery
- if (typeof(e.originalEvent) !== 'undefined') {
- e = e.originalEvent;
- }
- scrolling.startX = e.touches[0].clientX;
- scrolling.startY = e.touches[0].clientY;
- };
- // check if the finger moves more than 10px and set the moved flag to true
- var onTouchMove = function (e) {
- // Use originalEvent when available, fix compatibility with jQuery
- if (typeof(e.originalEvent) !== 'undefined') {
- e = e.originalEvent;
- }
- if (Math.abs(e.touches[0].clientX - scrolling.startX) > 10 ||
- Math.abs(e.touches[0].clientY - scrolling.startY) > 10) {
- scrolling.moved = true;
- }
- };
- // click handler on the input field to show the search container
- var onClick = function (event) {
- // only open the dialog if was not touched at the beginning of a legitimate scroll event
- if (scrolling.moved) {
- return;
- }
- // prevent the default event and the propagation
- event.preventDefault();
- event.stopPropagation();
- // call the fetch search query method once to be able to initialize it when the modal is shown
- // use an empty string to signal that there is no change in the search query
- ionAutocompleteController.fetchSearchQuery("", true);
- // show the ionic backdrop and the search container
- ionAutocompleteController.showModal();
- };
- var isKeyValueInObjectArray = function (objectArray, key, value) {
- if (angular.isArray(objectArray)) {
- for (var i = 0; i < objectArray.length; i++) {
- if (ionAutocompleteController.getItemValue(objectArray[i], key) === value) {
- return true;
- }
- }
- }
- return false;
- };
- // function to call the model to item method and select the item
- var resolveAndSelectModelItem = function (modelValue) {
- // convert the given function to a $q promise to support promises too
- var promise = $q.when(ionAutocompleteController.modelToItemMethod({modelValue: modelValue}));
- promise.then(function (promiseData) {
- // select the item which are returned by the model to item method
- ionAutocompleteController.selectItem(promiseData);
- }, function (error) {
- // reject the error because we do not handle the error here
- return $q.reject(error);
- });
- };
- // if the click is not handled externally, bind the handlers to the click and touch events of the input field
- if (ionAutocompleteController.manageExternally == "false") {
- element.bind('touchstart', onTouchStart);
- element.bind('touchmove', onTouchMove);
- element.bind('touchend click focus', onClick);
- }
- // cancel handler for the cancel button which clears the search input field model and hides the
- // search container and the ionic backdrop and calls the cancel button clicked callback
- ionAutocompleteController.cancelClick = function () {
- ionAutocompleteController.hideModal();
- // call cancel button clicked callback
- if (angular.isDefined(attrs.cancelButtonClickedMethod)) {
- ionAutocompleteController.cancelButtonClickedMethod({
- callback: {
- selectedItems: angular.isArray(ionAutocompleteController.selectedItems) ? ionAutocompleteController.selectedItems.slice() : ionAutocompleteController.selectedItems,
- componentId: ionAutocompleteController.componentId
- }
- });
- }
- };
- // watch the external model for changes and select the items inside the model
- scope.$watch("viewModel.externalModel", function (newModel) {
- if (angular.isArray(newModel) && newModel.length == 0) {
- // clear the selected items and set the view value and render it
- ionAutocompleteController.selectedItems = [];
- ngModelController.$setViewValue(ionAutocompleteController.selectedItems);
- ngModelController.$render();
- return;
- }
- // prepopulate view and selected items if external model is already set
- if (newModel && angular.isDefined(attrs.modelToItemMethod)) {
- if (angular.isArray(newModel)) {
- ionAutocompleteController.selectedItems = [];
- angular.forEach(newModel, function (modelValue) {
- resolveAndSelectModelItem(modelValue);
- })
- } else {
- resolveAndSelectModelItem(newModel);
- }
- }
- });
- // remove the component from the dom when scope is getting destroyed
- scope.$on('$destroy', function () {
- // angular takes care of cleaning all $watch's and listeners, but we still need to remove the modal
- searchInputElement.remove();
- });
- // render the view value of the model
- ngModelController.$render = function () {
- element.val(ionAutocompleteController.getItemValue(ngModelController.$viewValue, ionAutocompleteController.itemViewValueKey));
- };
- // set the view value of the model
- ngModelController.$formatters.push(function (modelValue) {
- var viewValue = ionAutocompleteController.getItemValue(modelValue, ionAutocompleteController.itemViewValueKey);
- return viewValue == undefined ? "" : viewValue;
- });
- // set the model value of the model
- ngModelController.$parsers.push(function (viewValue) {
- return ionAutocompleteController.getItemValue(viewValue, ionAutocompleteController.itemValueKey);
- });
- });
- }
- };
- }
- ]);
- })();
|