Bladeren bron

customer autocomplete feature added

robert 8 jaren geleden
bovenliggende
commit
8b4e7c5d0d

+ 2 - 1
bower.json

@@ -11,6 +11,7 @@
     "angular-translate": "^2.11.1",
     "squel": "^5.3.3",
     "ngstorage": "^0.3.11",
-    "ti-segmented-control": "^0.0.2"
+    "ti-segmented-control": "^0.0.2",
+    "ion-autocomplete": "^0.3.2"
   }
 }

+ 2 - 0
www/index.html

@@ -49,9 +49,11 @@
   <!-- 3rd Party -->
   <!-- CSS -->
   <link href="lib/ionic-filter-bar/dist/ionic.filter.bar.css" rel="stylesheet">
+  <link href="lib/ion-autocomplete/dist/ion-autocomplete.css" rel="stylesheet">
 
   <!-- JS -->
   <script src="lib/ionic-filter-bar/dist/ionic.filter.bar.js"></script>
+  <script src="lib/ion-autocomplete/dist/ion-autocomplete.js"></script>
   <script src="lib/ti-segmented-control/dist/ti-segmented-control.js"></script>
   <script src="lib/ngCordova/dist/ng-cordova.min.js"></script>
   <script src="lib/angular-translate/angular-translate.js"></script>

+ 2 - 1
www/js/app.js

@@ -5,6 +5,7 @@ angular.module(
         'xml-rpc',
         'ngCordova',
         'ngStorage',
+        'ion-autocomplete',
         'ti-segmented-control',
         'jett.ionic.filter.bar',
         'pascalprecht.translate'
@@ -99,7 +100,7 @@ angular.module(
             })
 
             .state('app.opportunities', {
-                url: '/opportunities',
+                url: '/opportunities/:id',
                 views: {
                     'content': {
                         templateUrl: 'templates/sales/opportunities.html',

+ 3 - 3
www/js/controllers/customer.controller.js

@@ -143,7 +143,7 @@ angular.module('odoo')
         /**
          *
          */
-        $scope.openOptions = function (index) {
+        $scope.openOptions = function (index, e) {
             deviceFactory.vibrate();
 
             $scope.selectedIndex = index;
@@ -153,8 +153,8 @@ angular.module('odoo')
             } else {
                 $scope.customer = $scope.customers[index];
             }
-
-            console.log('Customer selected => ' + angular.toJson($scope.customer, true));
+            
+            console.log($scope.customer);
 
             $ionicActionSheet.show({
                 titleText: 'Acciones',

+ 33 - 0
www/js/controllers/lead.controller.js

@@ -4,6 +4,7 @@ angular.module('odoo')
      *
      */
     .controller('LeadsController', function (
+        $q,
         $scope,
         $ionicModal,
         $ionicActionSheet,
@@ -11,6 +12,7 @@ angular.module('odoo')
         leadsRemoteFactory,
         leadsStorageFactory,
         opportunitiesStorageFactory,
+        customersRemoteFactory,
         sqlFactory,
         deviceFactory
     ) {
@@ -126,6 +128,37 @@ angular.module('odoo')
             $scope.leadModal.show();
         }
 
+         /**
+         *
+         */
+        $scope.getCustomersSuggestions = function (query) {
+            var defer = $q.defer();
+
+            customersRemoteFactory.getAll(function (customers) { 
+                defer.resolve(customers.filter(function (item) {
+                    return item.name.toLowerCase().indexOf(query) != -1 ? item : null; 
+                }));
+            }, function (err) {
+                defer.reject([]);
+            });
+            
+            return defer.promise;
+        }
+
+        /**
+         *
+         */
+        $scope.selectCustomer = function (callback) {
+            $scope.lead.contact_name = callback.item.name;
+        }
+
+        /**
+         *
+         */
+        $scope.deselectCustomer = function (callback) {
+            $scope.opportunity.contact_name = null;
+        }
+
         /**
          *
          */

+ 44 - 4
www/js/controllers/opportunity.controller.js

@@ -4,14 +4,17 @@ angular.module('odoo')
      *
      */
     .controller('OpportunitiesController', function (
+        $q,
         $scope,
         $ionicPopup,
         $ionicModal,
+        $stateParams,
         $ionicFilterBar,
         $ionicActionSheet,
         deviceFactory,
         crmStagesDataFactory,
-        opportunitiesDataFactory
+        opportunitiesDataFactory,
+        customersRemoteFactory
     ) {
 
         // =======================================================================================================
@@ -65,6 +68,7 @@ angular.module('odoo')
          *
          */
         $scope.$on('$ionicView.enter', function () {
+            console.log($stateParams);
             $scope.initialize();
         });
 
@@ -131,7 +135,13 @@ angular.module('odoo')
                 success(opportunities);
             }, function (syncErr) {
                 opportunitiesDataFactory.getAll(function (opportunities) {
-                    $scope.opportunities = opportunities;
+                    $scope.opportunities = opportunities.filter(function (item) {
+                        if ($routeParams.id) {
+                            return item.partner_id == $routeParams.id;
+                        } else {
+                            return true;
+                        }
+                    });
 
                     success(opportunities);
                 }, function (getAllErr) {
@@ -163,12 +173,43 @@ angular.module('odoo')
          */
         $scope.toogleNew = function () {
             if ($scope.stages.length > 0) {
-                $scope.opportunity.stage_id = $scope.stages[0].remote_id;
+                $scope.opportunity.stage_id = $scope.opportunity.stage_id || $scope.stages[0].id;
             }
 
             $scope.modal.show();
         }
 
+        /**
+         *
+         */
+        $scope.getCustomersSuggestions = function (query) {
+            var defer = $q.defer();
+
+            customersRemoteFactory.getAll(function (customers) { 
+                defer.resolve(customers.filter(function (item) {
+                    return item.name.toLowerCase().indexOf(query) != -1 && item.remote_id != 0 ? item : null; 
+                }));
+            }, function (err) {
+                defer.reject([]);
+            });
+            
+            return defer.promise;
+        }
+
+        /**
+         *
+         */
+        $scope.selectCustomer = function (callback) {
+            $scope.opportunity.contact_name = callback.item.name;
+        }
+
+        /**
+         *
+         */
+        $scope.deselectCustomer = function (callback) {
+            $scope.opportunity.contact_name = null;
+        }
+
         /**
         *
         */
@@ -191,7 +232,6 @@ angular.module('odoo')
                 console.log($scope.opportunity);
 
                 if (!$scope.opportunity.id) {
-                    console.log('Entra aqui');
                     $scope.opportunity.id = opportunityId;
                     $scope.opportunities.push($scope.opportunity);
                 }

+ 17 - 0
www/js/factories/sales/customer.sync.factory.js

@@ -112,6 +112,22 @@ angular.module('odoo')
             });
         }
 
+        /**
+         *
+         */
+        var getAll = function (success, error) {
+            sqlFactory.selectByConstraint('partner', 'customer = 1 AND modified != 2', function (customers) {
+                var data = [];
+
+                for (var i = 0; i < customers.length; i++) {
+                    data.push(customers.item(i));
+                }
+
+                success(data);
+            }, function (err) {
+                error(err);
+            });
+        }
 
         /**
          *
@@ -249,6 +265,7 @@ angular.module('odoo')
             pull: pull,
             push: push,
             destroy: destroy,
+            getAll: getAll,
             sync: sync
         }
     });

+ 70 - 33
www/js/factories/sales/lead.sync.factory.js

@@ -5,6 +5,8 @@ angular.module('odoo')
      */
     .factory('leadsRemoteFactory', function (
         leadsStorageFactory,
+        customersRemoteFactory,
+        customersStorageFactory,
         odooFactory,
         sqlFactory,
         asyncLoopFactory
@@ -36,56 +38,92 @@ angular.module('odoo')
          *
          */
         var push = function (id, data, success, error) {
+            checkCustomer(data, function (customerRemoteId) {
+                data.partner_id = customerRemoteId;
+                
+                 // Avoid odoo server warning message
+                delete data.id;
+                delete data.remote_id;
+                delete data.modified;
+                delete data.priority;
 
-            // Avoid odoo server warning message
-            delete data.id;
-            delete data.remote_id;
-            delete data.modified;
-            delete data.priority;
+                if (id) {
 
-            if (id) {
+                    pull(id, function (response) {
+                        if (response.length == 1) {
+                            var remoteData = response[0];
 
-                pull(id, function (response) {
-                    if (response.length == 1) {
-                        var remoteData = response[0];
+                            var remoteDate = new Date(remoteData.__last_update);
+                            var localDate = new Date(data.modified_date);
 
-                        var remoteDate = new Date(remoteData.__last_update);
-                        var localDate = new Date(data.modified_date);
+                            delete data.modified_date;
 
-                        delete data.modified_date;
+                            if (localDate > remoteDate) {
 
-                        if (localDate > remoteDate) {
+                                odooFactory.write('crm.lead', id, data, function (response) {
+                                    console.log(response);
+                                }, function (odooErr) {
+                                    console.log(odooErr);
+                                    error(odooErr);
+                                });
 
-                            odooFactory.write('crm.lead', id, data, function (response) {
-                                console.log(response);
-                            }, function (odooErr) {
-                                console.log(odooErr);
-                                error(odooErr);
-                            });
+                            } else {
+                                success(response);
+                            }
 
                         } else {
                             success(response);
                         }
 
-                    } else {
-                        success(response);
-                    }
+                    }, function (pullErr) {
+                        error(pullErr);
+                    });
 
-                }, function (pullErr) {
-                    error(pullErr);
-                });
+                } else {
 
-            } else {
+                    delete data.modified_date;
 
-                delete data.modified_date;
+                    odooFactory.create('crm.lead', data, function (response) {
+                        success(response);
+                    }, function (odooErr) {
+                        error(odooErr);
+                    });
 
-                odooFactory.create('crm.lead', data, function (response) {
-                    success(response);
-                }, function (odooErr) {
-                    error(odooErr);
-                });
+                }
+            }, function (err) { 
+                error(err);
+            });
+        }
 
+        /**
+         *
+         */
+        var checkCustomer = function (data, success, error) {
+            if (data.partner_id == 0) {
+                return success(0);
             }
+
+            sqlFactory.selectById('partner', data.partner_id, function (row) { 
+                var customer = row.item(0);
+
+                if (customer.partner_id == 0) {
+                    customersRemoteFactory.push(null, customer, function (remoteId) {
+                        customer.remote_id = remoteId;
+
+                        customersStorageFactory.save(customer, function (customerId) { 
+                            success(remoteId);
+                        }, function (err) {
+                            error(err);
+                        });
+                    }, function (err) {
+                         error(err);
+                    });
+                } else {
+                    success(customer.partner_id);
+                }
+            }, function (err) {
+                error(err);
+            });
         }
 
         /**
@@ -123,7 +161,6 @@ angular.module('odoo')
                     push(null, data, function (response) {
                         loop.next();
                     }, function (pushErr) {
-                        console.log(pushErr);
                         loop.next();
                     });
 

+ 12 - 0
www/js/factories/utils.factory.js

@@ -42,6 +42,17 @@ angular.module('odoo')
             });
         };
 
+        // Execute native SQL SELECT instruction with a constraint
+        var selectById = function (tableName, id, success, error) {
+            var sql = 'SELECT * FROM ' + tableName + ' WHERE id = ?';
+
+            db.executeSql(sql, [id], function (result) {
+                success(result.rows);
+            }, function (err) {
+                error(err);
+            });
+        };
+
         // Execute native SQL SELECT instruction with count instruction
         var count = function (tableName, success, error) {
             var sql = 'SELECT COUNT(*) AS total FROM ' + tableName;
@@ -56,6 +67,7 @@ angular.module('odoo')
         return {
             select: select,
             selectByConstraint: selectByConstraint,
+            selectById: selectById,
             count: count
         }
     })

+ 53 - 0
www/lib/ion-autocomplete/.bower.json

@@ -0,0 +1,53 @@
+{
+  "name": "ion-autocomplete",
+  "version": "0.3.2",
+  "description": "A configurable Ionic directive for an autocomplete dropdown",
+  "main": [
+    "./dist/ion-autocomplete.js",
+    "./dist/ion-autocomplete.css"
+  ],
+  "license": "MIT",
+  "ignore": [
+    "**/.*",
+    ".gitignore",
+    "Gruntfile.js",
+    "karma.conf.js",
+    "lib",
+    "src",
+    "test",
+    "node_modules",
+    "bower_components",
+    "demo.gif",
+    "protractor-conf.js"
+  ],
+  "keywords": [
+    "AngularJS",
+    "angular",
+    "Ionic",
+    "ion",
+    "autocomplete"
+  ],
+  "authors": [
+    "Danny Povolotski <dannypovolotski@gmail.com>",
+    "Guy Brand <admin@guylabs.org>"
+  ],
+  "homepage": "https://github.com/guylabs/ion-autocomplete",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/guylabs/ion-autocomplete.git"
+  },
+  "devDependencies": {
+    "ionic": "driftyco/ionic-bower#1.1.1",
+    "angular-mocks": "1.4.3"
+  },
+  "_release": "0.3.2",
+  "_resolution": {
+    "type": "version",
+    "tag": "v0.3.2",
+    "commit": "e29cfa9a9b98b2d05c808eacb83ed36d0c207df2"
+  },
+  "_source": "https://github.com/guylabs/ion-autocomplete.git",
+  "_target": "^0.3.2",
+  "_originalSource": "ion-autocomplete",
+  "_direct": true
+}

+ 22 - 0
www/lib/ion-autocomplete/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Danny Povolotski
+Copyright (c) 2015 Modifications by Guy Brand
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 557 - 0
www/lib/ion-autocomplete/README.md

@@ -0,0 +1,557 @@
+ion-autocomplete
+================
+[![Build Status](https://travis-ci.org/guylabs/ion-autocomplete.svg?branch=master)](https://travis-ci.org/guylabs/ion-autocomplete)
+[![Coverage Status](https://img.shields.io/coveralls/guylabs/ion-autocomplete.svg)](https://coveralls.io/r/guylabs/ion-autocomplete)
+[![Bower version](https://badge.fury.io/bo/ion-autocomplete.svg)](http://badge.fury.io/bo/ion-autocomplete)
+[![npm version](https://badge.fury.io/js/ion-autocomplete.svg)](http://badge.fury.io/js/ion-autocomplete)
+
+> Configurable Ionic directive for an autocomplete dropdown.
+
+#Table of contents
+
+- [Demo](#demo)
+- [Introduction](#introduction)
+- [Features](#features)
+- [Installation](#installation)
+- [Ionic compatibility](#ionic-compatibility)
+- [Usage](#usage)
+    - [Configurable options](#configurable-options)
+        - [The `items-method`](#the-items-method)
+        - [The `items-method-value-key`](#the-items-method-value-key)
+        - [The `item-value-key`](#the-item-value-key)
+        - [The `item-view-value-key`](#the-item-view-value-key)
+        - [The `max-selected-items`](#the-max-selected-items)
+        - [The `items-clicked-method`](#the-items-clicked-method)
+        - [The `items-removed-method`](#the-items-removed-method)
+        - [External model](#external-model)
+        - [The `model-to-item-method`](#the-model-to-item-method)
+        - [The `cancel-button-clicked-method`](#the-cancel-button-clicked-method)
+        - [ComponentId](#component-id)
+        - [Placeholder](#placeholder)
+        - [Cancel button label](#cancel-button-label)
+        - [Select items label](#select-items-label)
+        - [Selected items label](#selected-items-label)
+        - [Template url](#template-url)
+        - [Template data](#template-data)
+        - [Loading icon](#loading-icon)
+        - [Manage externally](#manage-externally)
+    - [Using expressions in value keys](#using-expressions-in-value-keys)
+    - [Debouncing](#debouncing)
+- [Release notes](#release-notes)
+- [Acknowledgements](#acknowledgements)
+- [License](#license)
+
+# Demo
+
+You can find a live demo on [Codepen](http://codepen.io/guylabs/pen/GJmwMw) or see it in action in the following image:
+
+![Animated demo](https://github.com/guylabs/ion-autocomplete/raw/master/demo.gif)
+
+# Introduction
+
+For one of my private projects I needed an autocomplete component in Ionic. I searched a lot and found some plain Angular autocompletes, but these had too much other dependencies and mostly didn't look that good within Ionic. Then one day I stumbled upon the [ion-google-place](https://github.com/israelidanny/ion-google-place) project which was exactly what I was looking for, except that it was just working with the Google Places API. So I forked the project and made it configurable such that you can add the service you need. The differences between the ion-google-place project and the ion-autocomplete are listed in the features.
+
+# Features
+
+The ion-autocomplete component has the following features:
+- Multiple selection support
+- Configurable service which provides the items to list
+- Allow to define the maximum number of selected items
+- Configure what is stored in the model and what is seen in the list
+- Configure the template used to show the autocomplete component
+- Configure a callback when an item is clicked/removed
+- Configure a callback when the done button is clicked
+- Configure all labels used in the component
+
+# Installation
+
+1. Use bower to install the new module:
+```bash
+bower install ion-autocomplete --save
+```
+2. Import the `ion-autocomplete` javascript and css file into your HTML file:
+```html
+<script src="lib/ion-autocomplete/dist/ion-autocomplete.js"></script>
+<link href="lib/ion-autocomplete/dist/ion-autocomplete.css" rel="stylesheet">
+```
+3. Add `ion-autocomplete` as a dependency on your Ionic app:
+```javascript
+angular.module('myApp', [
+  'ionic',
+  'ion-autocomplete'
+]);
+```
+
+# Ionic compatibility
+
+The ion-autocomplete component is running with the following Ionic versions:
+
+ion-autocomplete version | Ionic version
+------------------------ | -------------
+0.0.2 - 0.1.2 | 1.0.0-beta.14
+0.2.0 - 0.2.1 | 1.0.0-rc.3
+0.2.2 - 0.2.3 | 1.0.0
+0.3.0 - 0.3.1 | 1.1.0
+0.3.2 - latest | 1.1.1
+
+# Usage
+
+To use the `ion-autocomplete` directive in single select mode you need set the `max-selected-items` attribute and add the following snippet to your template:
+```html
+//usage with the attribute restriction
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" max-selected-items="1" />
+```
+
+If you want to use it in multiple select mode you do not need to add anything special, just the following snippet to your template: 
+```html
+//usage with the attribute restriction
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" />
+```
+
+Check out the next chapter on how to configure the directive.
+
+## Configurable options
+
+### The `items-method`
+
+You are able to pass in a callback method which gets called when the user changes the value of the search input field. This is
+normally a call to the back end which retrieves the items for the specified query. Here is a small sample which will
+return a static item of the query:
+
+Define the callback in your scope:
+```javascript
+$scope.callbackMethod = function (query, isInitializing) {
+    return [query];
+}
+```
+
+And set the items method on the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" items-method="callbackMethod(query)" />
+```
+
+You are also able to return a promise from this callback method. For example:
+```javascript
+$scope.callbackMethod = function (query, isInitializing) {
+    return $http.get(endpoint);
+}
+```
+
+Note that the parameter for the `callbackMethod` needs to be named `query`. Otherwise the callback will not get called properly.
+If you want to also retrieve the [ComponentId](#component-id) then you need to add a second parameter called `componentId`:
+```javascript
+$scope.callbackMethod = function (query, isInitializing, componentId) {
+    if(componentId == "component1") {
+        return $http.get(endpoint1);
+    }
+    return [query];
+}
+```
+
+If you want to pre populate the items which are shown when the modal is visible before the user enters a query then you can check the `isInitializing` flag of
+the `items-method` as this is set to true if it is called for the initial items. Here is an example which shows the `test` item as an initial item:
+```javascript
+$scope.callbackMethod = function (query, isInitializing) {
+    if(isInitializing) {
+        // depends on the configuration of the `items-method-value-key` (items) and the `item-value-key` (name) and `item-view-value-key` (name)
+        return { items: [ { name: "test" } ] }
+    } else {
+        return $http.get(endpoint);
+    }
+}
+```
+
+If you want to clear the list each time the user opens the modal then just return an empty array like in the following example:
+```javascript
+$scope.callbackMethod = function (query, isInitializing) {
+    if(isInitializing) {
+        // depends on the configuration of the `items-method-value-key` (items) and the `item-value-key` (name) and `item-view-value-key` (name)
+        return { items: [] }
+    } else {
+        return $http.get(endpoint);
+    }
+}
+```
+
+And if you do not want that the searched items list gets modified then just return nothing as in this example:
+```javascript
+$scope.callbackMethod = function (query, isInitializing) {
+    if(!isInitializing) {
+        return $http.get(endpoint);
+    }
+}
+```
+
+### The `items-method-value-key`
+
+You are able to set the `items-method-value-key` attribute which maps to a value of the returned data of the `items-method`. If for
+example your callback method returns the following object:
+```json
+{
+    "items" : [ {
+        "name" : "item1"
+    },{
+        "name" : "item2"
+    },
+        ...
+    ]
+}
+```
+Then when you do not specify the `items-method-value-key` there will be no list displayed when you search for items in
+the search input field. You need to set the `items-method-value-key` to `items` such that the items are shown. If you right
+away return an array of items then you do not need to set the `items-method-value-key`.
+
+### The `item-value-key`
+
+You are able to set the `item-value-key` attribute which maps to a value of the returned object from the `items-method`. The value
+is then saved in the defined `ng-model`. Here an example:
+
+The items method returns the following object:
+```javascript
+[
+    {
+        "id": "1",
+        "name": "Item 1",
+        ...
+    }
+    ...
+]
+```
+
+And now you set the following `item-value-key`:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" item-value-key="id" />
+```
+
+Now when the user selects the `Item 1` from the list, then the value of the objects `id` is stored in the `ng-model`. If
+no `item-value-key` is passed into the directive, the whole item object will be stored in the `ng-model`.
+
+### The `item-view-value-key`
+
+You are able to set the `item-view-value-key` attribute which maps to a value of the returned object from the `items-method`. The
+value is then showed in both input fields. Here an example:
+
+The `items-method` returns the following object:
+```javascript
+[
+    {
+        "id": "1",
+        "name": "Item 1",
+        ...
+    }
+    ...
+]
+```
+
+And now you set the following `item-view-value-key`:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" item-view-value-key="name" />
+```
+
+Now when the user selects the `Item 1` from the list, then the value of the objects `name` is showed in both input fields. If
+no `item-view-value-key` is passed into the directive, the whole item object will be showed in both input fields.
+
+### The `max-selected-items`
+
+You are able to set the `max-selected-items` attribute to any number to set the maximum selectable items inside the component. Here an example:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" max-selected-items="3" />
+```
+
+Then the user is just able to select three items out of the returned items and also delete them again. The given `ng-model` is an 
+array if multiple items are selected.
+
+### The `items-clicked-method`
+
+You are able to pass a function to the `items-clicked-method` attribute to be notified when an item is clicked. The name of the 
+parameter of the function must be `callback`. Here is an example:
+
+Define the callback in your scope:
+```javascript
+$scope.clickedMethod = function (callback) {
+    // print out the selected item
+    console.log(callback.item); 
+    
+    // print out the component id
+    console.log(callback.componentId);
+    
+    // print out the selected items if the multiple select flag is set to true and multiple elements are selected
+    console.log(callback.selectedItems); 
+}
+```
+
+And pass in the callback method in the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" items-clicked-method="clickedMethod(callback)" />
+```
+
+Then you get a callback object with the clicked/selected item and the selected items if you have multiple selected items (see [The `multiple-select`](#the-multiple-select)).
+
+### The `items-removed-method`
+
+You are able to pass a function to the `items-removed-method` attribute to be notified when an item is removed from a multi-select list. The name of the 
+parameter of the function must be `callback`. It is similar to items-clicked-method.  This attribute has no defined behaviour for a single select list.
+
+Here is an example:
+
+Define the callback in your scope:
+```javascript
+$scope.removedMethod = function (callback) {
+    // print out the removed item
+    console.log(callback.item); 
+
+    // print out the component id
+    console.log(callback.componentId);
+    
+    // print out the selected items
+    console.log(callback.selectedItems); 
+}
+```
+
+And pass in the callback method in the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" items-removed-method="removedMethod(callback)" />
+```
+
+Then you get a callback object with the removed item and the selected items.
+
+### External model
+
+The two way binded external model (`external-model` attribute on the component) is used to prepopulate the selected items with the model value. The [`model-to-item-method`](#the-model-to-item-method) is used to get the view item to the model and then the item is selected in the 
+component. Be aware that the `external-model` is not updated by the component when an item is selected. It is just used to prepopulate or clear the selected items. If you need to get the current selected items you are able 
+to read the value of the `ng-model`. For an example have a look at the [`model-to-item-method`](#the-model-to-item-method) documentation.
+
+If you need to clear the selected items then you are able to set the `external-model` to an empty array (another value is not clearing the selected items).
+
+### The `model-to-item-method`
+
+This method is used if you want to prepopulate the model of the `ion-autocomplete` component. The [external model](#external-model) needs 
+to have the same data as it would have when you select the items by hand. The component then takes the model values 
+and calls the specified `model-to-item-method` to resolve the item from the back end and select it such that it is preselected.
+
+Here a small example:
+
+Define the `model-to-item-method` and `external-model` in your scope:
+```javascript
+$scope.modelToItemMethod = function (modelValue) {
+
+    // get the full model item from the model value and return it. You need to implement the `getModelItem` method by yourself 
+    // as this is just a sample. The method needs to retrieve the whole item (like the `items-method`) from just the model value.
+    var modelItem = getModelItem(modelValue);
+    return modelItem;
+}
+$scope.externalModel = ['test1', 'test2', 'test3'];
+```
+
+And set the `model-to-item-method` on the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" external-model="externalModel" model-to-item-method="modelToItemMethod(modelValue)" />
+```
+
+You are also able to return a promise from this callback method. For example:
+```javascript
+$scope.modelToItemMethod = function (modelValue) {
+    return $http.get(endpoint + '?q=' + modelValue);
+}
+```
+
+Note that the parameter for the `model-to-item-method` needs to be named `modelValue`. Otherwise the callback will not get called properly.
+
+### The `cancel-button-clicked-method`
+
+You are able to pass a function to the `cancel-button-clicked-method` attribute to be notified when the cancel button is clicked to close the modal. The name of the 
+parameter of the function must be `callback`. Here is an example:
+
+Define the callback in your scope:
+```javascript
+$scope.cancelButtonClickedMethod = function (callback) {    
+    // print out the component id
+    console.log(callback.componentId);
+    
+    // print out the selected items
+    console.log(callback.selectedItems); 
+}
+```
+
+And pass in the callback method in the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" cancel-button-clicked-method="cancelButtonClickedMethod(callback)" />
+```
+
+Then you get a callback object with the selected items and the component id.
+
+### Component Id
+
+The component id is an attribute on the `ion-autocomplete` component which sets a given id to the component. This id is then returned in 
+the callback object of the [`items-clicked-method`](#the-items-clicked-method) and as a second parameter of the [`items-method`](#the-items-method).
+Here an example:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" component-id="component1" />`
+```
+
+You are able to set this is on each component if you have multiple components built up in a ng-repeat where you do not want to have multiple `items-method` 
+for each component because you want to display other items in each component. You will also get it in the `items-clicked-method` callback object such that you just 
+need to define one callback method and you can distinguish the calls with the `componentId` attribute right inside the method.
+
+### Placeholder
+
+You are also able to set the placeholder on the input field and on the search input field if you add the `placeholder`
+attribute to the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" placeholder="Enter the query to search for ..." />`
+```
+
+### Cancel button label
+
+You are also able to set the cancel button label (defaults to `Cancel`) if you add the `cancel-label` attribute to the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" cancel-label="Go back" />`
+```
+
+### Select items label
+
+You are also able to set the select items label (defaults to `Select an item...`) if you add the `select-items-label` attribute to the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" select-items-label="Select your items..." />`
+```
+
+### Selected items label
+
+You are also able to set the selected items label (defaults to `Selected items:`) if you add the `selected-items-label` attribute to the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" selected-items-label="Selected:" />`
+```
+
+### Template url
+
+You are also able to set an own template for the autocomplete component (defaults to `''`) if you add the `template-url` attribute to the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" template-url="templates/template.html" />`
+```
+
+This way you are able to override the default template (the `template` variable [here](https://github.com/guylabs/ion-autocomplete/blob/master/src/ion-autocomplete.js#L68)) 
+and use your own template. The component will use the default template if the `template-url` is not defined.
+
+You are able to use all the configurable attributes as expressions in your template. I would advise to use the default template as base template
+and then add your custom additions to it.
+
+> Please also take care when you change how the items are shown or what method is called if an item is clicked, 
+> because changing this could make the component unusable.
+
+You will need to set the proper `randomCssClass` for the outer most div container in your template and you can get the value by using the `{{viewModel.randomCssClass}}` expression
+like in the following example:
+
+```html
+<div class="ion-autocomplete-container {{viewModel.randomCssClass}} modal" style="display: none;">
+```
+
+### Template data
+
+If you change the template with the `template-url` and want to pass in additional data then you are able to set 
+the `template-data` attribute on the directive. If you for example have a `templateData.testData` expression in your own 
+template like this:
+```html
+...
+<div>{{templateData.testData}}</div>
+...
+```
+Then you need to set the proper object on your Angular scope the following way:
+```javascript
+$scope.templateData = {
+    testData: "test-data"
+};
+```
+And now you just need to add the `templateData` attribute on the directive:
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" template-data="templateData" />`
+```
+
+Then the expression in your template gets resolved properly.
+
+### Loading icon
+
+If you want to display a loading icon when the `items-method` promise gets resolved then you need to set the `loading-icon` 
+attribute to a value given by the Ionic spinner: http://ionicframework.com/docs/api/directive/ionSpinner. Then the spinner should 
+be shown at the right side of the search input field. 
+
+### Manage externally
+
+To manage the `ion-autocomplete` component externally means that you need to handle when the search modal is shown. To enable this functionality 
+you need to set the `manage-externally` attribute to `true` and then you can call the `showModal()` method on the controller. Here an example:
+
+```javascript
+// create the externally managed component and a button which has a click handler to a scope method
+<input ion-autocomplete type="text" class="ion-autocomplete" autocomplete="off" ng-model="model" manage-externally="true" />
+<button class="button" ng-click="clickButton()">Open modal</button>
+
+// inside your controller you can define the 'clickButton()' method the following way
+this.clickButton = function () {
+    var ionAutocompleteElement = document.getElementsByClassName("ion-autocomplete");
+    angular.element(ionAutocompleteElement).controller('ionAutocomplete').fetchSearchQuery("", true);
+    angular.element(ionAutocompleteElement).controller('ionAutocomplete').showModal();
+}
+```
+
+Then you will need to click on the button to open the search modal. This functionality is useful if the user wants to edit the selected item inside the 
+input field after she/he selected the item/s.
+
+### Selected items
+
+If you want to clear the selected items programmatically, then you are able to set the `selected-items` attribute with a two way binded model value which then gets updated 
+when the items get selected. If you want to clear them just set the given model value to an empty array.
+
+Please *do not* use it for pre populating the selected items. For this use the standard `ng-model` value and [the `model-to-item-method`](#the-model-to-item-method).
+
+## Using expressions in value keys
+
+All value keys are parsed with the Angular `$parse` service such that you are able to use expressions like in the following
+example:
+
+```javascript
+[
+    {
+        "id": "1",
+        "name": "Item 1",
+        "child": {
+            "name": "Child Item 1",
+        }
+        ...
+    }
+    ...
+]
+```
+
+This would be the JSON model returned by the `items-method` and in the next snippet we define that we want to show the
+name attribute of the child object:
+
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" item-view-value-key="child.name" />
+```
+
+## Debouncing
+
+If you want to debounce the search input field request, then you are able to set the `ng-model-options` attribute on the input field where you define the `ion-autocomplete`
+directive. These options will then be added to the search input field. Be aware that when you add a debounce the update of the model value will also be debounced the 
+ same amount as the request to the `items-method`. Here a small example:
+ 
+```html
+<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ng-model="model" ng-model-options="{debounce:1000}" />
+```
+
+# Release notes
+
+Check them here: [Release notes](https://github.com/guylabs/ion-autocomplete/blob/master/RELEASENOTES.md)
+
+# Acknowledgements
+
+When I first searched for an Ionic autocomplete component I just found the project from Danny. So please have a look at
+his [ion-google-place](https://github.com/israelidanny/ion-google-place) project as this project here is a fork of it.
+At this point I want to thank him for his nice work.
+
+# License
+
+This Ionic autocomplete directive is available under the MIT license.
+
+(c) Danny Povolotski
+
+(c) Modifications by Guy Brand

+ 153 - 0
www/lib/ion-autocomplete/RELEASENOTES.md

@@ -0,0 +1,153 @@
+# Release notes of ion-autocomplete
+
+## Version 0.3.2
+
+* Tag: [0.3.2](https://github.com/guylabs/ion-autocomplete/tree/v0.3.2)
+* Release: [ion-autocomplete-0.3.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.2.zip)
+
+### Changes
+
+* A single selected item is not an array anymore with a single value - [#115](https://github.com/guylabs/ion-autocomplete/issues/115)
+* The item repeat has been switched from `collection-repeat` to `ng-repeat` as there were some issues - [#126](https://github.com/guylabs/ion-autocomplete/issues/126)
+
+### Migration notes
+
+* As part of the [#115](https://github.com/guylabs/ion-autocomplete/issues/115) issue when you now select a single value (`maxSelectedItems=1`) then the item
+is now returned as object and not as an array with one element. If you use a custom template, please also check the changes in the default template.
+
+## Version 0.3.1
+
+* Tag: [0.3.1](https://github.com/guylabs/ion-autocomplete/tree/v0.3.1)
+* Release: [ion-autocomplete-0.3.1.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.1.zip)
+
+### Changes
+
+* Added the `max-selected-items` attribute to restrict the selected search items - [#58](https://github.com/guylabs/ion-autocomplete/issues/58).
+* Added the `cancel-button-clicked-method` attribute to be able to get notified when the cancel button is clicked - [#63](https://github.com/guylabs/ion-autocomplete/issues/63).
+* Added the `external-model` attribute to be able to prepopulate the selected items and to clear them programmatically - [#66](https://github.com/guylabs/ion-autocomplete/issues/66), [#89](https://github.com/guylabs/ion-autocomplete/issues/89).
+* Added the ability to pass the `ng-model-options` to the inner search input field - [#91](https://github.com/guylabs/ion-autocomplete/issues/91)
+* Added the ability to initialize the search items within the `items-method` - [#57](https://github.com/guylabs/ion-autocomplete/issues/57)
+
+### Migration notes
+
+* As of version `0.3.1` the `multiple-select` attribute has been dropped in favor of the `max-selected-items` attribute.
+Please have a look at the documentation here https://github.com/guylabs/ion-autocomplete#the-max-selected-items on how to migrate this.
+* The `search-items` attribute has been removed as now the initialization of the `search-items` is done in the `items-method`. See the 
+new documentation here https://github.com/guylabs/ion-autocomplete#the-items-method.
+
+## Version 0.3.0
+
+* Tag: [0.3.0](https://github.com/guylabs/ion-autocomplete/tree/v0.3.0)
+* Release: [ion-autocomplete-0.3.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.3.0.zip)
+
+### Changes
+
+* Upgraded to Ionic 1.1.0 and Angular 1.4.3.
+* Fixed an issue with multiple `ion-autocomplete` directives on one page.
+* Prepared for Angular 2.0.
+* Upgraded project to use newest libraries.
+
+### Migration notes
+
+* As of version `0.3.0` the component does not support the element restriction anymore, such that you are just able to 
+use the attribute restriction on all your elements. This means that you need to convert all `<ion-autocomplete ... />` 
+ tags to the following tag: `<input ion-autocomplete type="text" readonly="readonly" class="ion-autocomplete" autocomplete="off" ... />`
+
+## Version 0.2.3
+
+* Tag: [0.2.3](https://github.com/guylabs/ion-autocomplete/tree/v0.2.3)
+* Release: [ion-autocomplete-0.2.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.3.zip)
+
+### Changes
+
+* Add new `model-to-item-method` to be able to prepopulate the model. (See issue [#25](https://github.com/guylabs/ion-autocomplete/issues/25))
+* Fixed issue 'Feature: Loading icon' - [#12](https://github.com/guylabs/ion-autocomplete/issues/12).
+* Fixed issue 'Bug: Cannot pass in pre-populated model' - [#25](https://github.com/guylabs/ion-autocomplete/issues/25).
+* Fixed issue 'Bug: $http promise not working' - [#27](https://github.com/guylabs/ion-autocomplete/issues/27).
+* Fixed issue 'Bug: Item list from variable won't show first time you start typing' - [#30](https://github.com/guylabs/ion-autocomplete/issues/30).
+* Fixed issue 'Feature: No callback when items removed' - [#32](https://github.com/guylabs/ion-autocomplete/issues/32).
+* Fixed issue 'Bug: JS error when using with jQuery' - [#34](https://github.com/guylabs/ion-autocomplete/issues/34).
+* Fixed issue 'Feature: Display all items if query is empty' - [#38](https://github.com/guylabs/ion-autocomplete/issues/38).
+* Fixed issue 'Feature: Ability to pass in arbitrary data to pass to the template' - [#39](https://github.com/guylabs/ion-autocomplete/issues/39).
+* Fixed issue 'Feature: Autocomplete box should open on <tab> as well as click' - [#43](https://github.com/guylabs/ion-autocomplete/issues/43).
+
+### Migration notes
+
+* The query can now also be empty in the `items-method` and this could change the logic in your `items-method`. Please check [#38](https://github.com/guylabs/ion-autocomplete/issues/38) for more information.
+
+## Version 0.2.2
+
+* Tag: [0.2.2](https://github.com/guylabs/ion-autocomplete/tree/v0.2.2)
+* Release: [ion-autocomplete-0.2.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.2.zip)
+
+### Changes
+
+* Fixed issue 'Model binding issue' - [#14](https://github.com/guylabs/ion-autocomplete/issues/14).
+* Fixed issue 'Upgrade to Ionic 1.0.0' - [#15](https://github.com/guylabs/ion-autocomplete/issues/15).
+* Fixed issue 'Cant display list in from query' - [#16](https://github.com/guylabs/ion-autocomplete/issues/16).
+* Fixed issue 'How can I call the auto complete form in the ng-click' - [#18](https://github.com/guylabs/ion-autocomplete/issues/18).
+* Fixed issue 'I cant get autocomplete to work in my project' - [#21](https://github.com/guylabs/ion-autocomplete/issues/21).
+* Fixed issue 'Add version table to documentation' - [#22](https://github.com/guylabs/ion-autocomplete/issues/22).
+* Fixed issue 'Ability to pass sort of id to the item-method' - [#23](https://github.com/guylabs/ion-autocomplete/issues/23).
+
+## Version 0.2.1
+
+* Tag: [0.2.1](https://github.com/guylabs/ion-autocomplete/tree/v0.2.1)
+* Release: [ion-autocomplete-0.2.1.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.1.zip)
+
+### Changes
+
+* Fixed issue [#5](https://github.com/guylabs/ion-autocomplete/issues/5).
+* Fixed issue [#6](https://github.com/guylabs/ion-autocomplete/issues/6).
+* Fixed issue [#8](https://github.com/guylabs/ion-autocomplete/issues/8).
+* Fixed issue [#10](https://github.com/guylabs/ion-autocomplete/issues/10).
+
+## Version 0.2.0
+
+* Tag: [0.2.0](https://github.com/guylabs/ion-autocomplete/tree/v0.2.0)
+* Release: [ion-autocomplete-0.2.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.2.0.zip)
+
+### Changes
+
+* Add the ability to select multiple items
+* Add ability to pass in a callback function when an item is clicked
+* Use `collection-repeat` instead of `ng-repeat` to improve the performance
+* Add the ability to use an own external template
+
+## Version 0.1.2
+
+* Tag: [0.1.2](https://github.com/guylabs/ion-autocomplete/tree/v0.1.2)
+* Release: [ion-autocomplete-0.1.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.2.zip)
+
+### Changes
+
+* Fixed issue that the model was not shown if it was already populated
+* Add ability to use expressions in the value keys
+
+## Version 0.1.1
+
+* Tag: [0.1.1](https://github.com/guylabs/ion-autocomplete/tree/v0.1.1)
+* Release: [ion-autocomplete-0.1.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.1.zip)
+
+### Changes
+
+* Fix release version
+
+## Version 0.1.0
+
+* Tag: [0.1.0](https://github.com/guylabs/ion-autocomplete/tree/v0.1.0)
+* Release: [ion-autocomplete-0.1.0.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.1.0.zip)
+
+### Changes
+
+* Add promise support for the items method
+* Add `items-method-value-key` property to specify the value key of the data returned by the items method
+
+## Version 0.0.2
+
+* Tag: [0.1.0](https://github.com/guylabs/ion-autocomplete/tree/v0.0.2)
+* Release: [ion-autocomplete-0.0.2.zip](https://github.com/guylabs/ion-autocomplete/archive/v0.0.2.zip)
+
+### Changes
+
+* Initial release of `ion-autocomplete`

+ 43 - 0
www/lib/ion-autocomplete/bower.json

@@ -0,0 +1,43 @@
+{
+  "name": "ion-autocomplete",
+  "version": "0.3.2",
+  "description": "A configurable Ionic directive for an autocomplete dropdown",
+  "main": [
+    "./dist/ion-autocomplete.js",
+    "./dist/ion-autocomplete.css"
+  ],
+  "license": "MIT",
+  "ignore": [
+    "**/.*",
+    ".gitignore",
+    "Gruntfile.js",
+    "karma.conf.js",
+    "lib",
+    "src",
+    "test",
+    "node_modules",
+    "bower_components",
+    "demo.gif",
+    "protractor-conf.js"
+  ],
+  "keywords": [
+    "AngularJS",
+    "angular",
+    "Ionic",
+    "ion",
+    "autocomplete"
+  ],
+  "authors": [
+    "Danny Povolotski <dannypovolotski@gmail.com>",
+    "Guy Brand <admin@guylabs.org>"
+  ],
+  "homepage": "https://github.com/guylabs/ion-autocomplete",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/guylabs/ion-autocomplete.git"
+  },
+  "devDependencies": {
+    "ionic": "driftyco/ionic-bower#1.1.1",
+    "angular-mocks": "1.4.3"
+  }
+}

+ 19 - 0
www/lib/ion-autocomplete/dist/ion-autocomplete.css

@@ -0,0 +1,19 @@
+.ion-autocomplete-container {
+    position: fixed;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+    z-index: 20;
+    display: none;
+    margin: auto;
+}
+
+input.ion-autocomplete[readonly] {
+    background-color: transparent;
+    cursor: text;
+}
+
+.ion-autocomplete-loading-icon {
+    padding-left: 10px;
+}

+ 480 - 0
www/lib/ion-autocomplete/dist/ion-autocomplete.js

@@ -0,0 +1,480 @@
+/*
+ * 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);
+                    });
+
+                });
+
+            }
+        };
+    }
+]);
+
+})();

+ 1 - 0
www/lib/ion-autocomplete/dist/ion-autocomplete.min.css

@@ -0,0 +1 @@
+.ion-autocomplete-container{position:fixed;top:0;right:0;bottom:0;left:0;z-index:20;display:none;margin:auto}input.ion-autocomplete[readonly]{background-color:transparent;cursor:text}.ion-autocomplete-loading-icon{padding-left:10px}

File diff suppressed because it is too large
+ 41 - 0
www/lib/ion-autocomplete/dist/ion-autocomplete.min.js


+ 63 - 0
www/lib/ion-autocomplete/package.json

@@ -0,0 +1,63 @@
+{
+  "name": "ion-autocomplete",
+  "version": "0.3.2",
+  "description": "A configurable Ionic directive for an autocomplete dropdown",
+  "keywords": [
+    "AngularJS",
+    "angular",
+    "Ionic",
+    "ion",
+    "autocomplete"
+  ],
+  "homepage": "https://github.com/guylabs/ion-autocomplete",
+  "bugs": "https://github.com/guylabs/ion-autocomplete/issues",
+  "author": {
+    "name": "Danny Povolotski",
+    "email": "dannypovolotski@gmail.com",
+    "url": "https://github.com/israelidanny"
+  },
+  "contributors": [
+    {
+      "name": "Guy Brand",
+      "email": "admin@guylabs.org",
+      "url": "http://guylabs.ch"
+    }
+  ],
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/guylabs/ion-autocomplete.git"
+  },
+  "devDependencies": {
+    "bower": "~1.7.7",
+    "coveralls": "~2.11.6",
+    "grunt": "~0.4.5",
+    "grunt-bower-install-simple": "~1.2.1",
+    "grunt-contrib-concat": "~0.5.1",
+    "grunt-contrib-cssmin": "~0.14.0",
+    "grunt-contrib-uglify": "~0.11.1",
+    "grunt-http-server": "~1.13.0",
+    "grunt-karma": "~0.12.1",
+    "grunt-protractor-runner": "~3.0.0",
+    "karma": "~0.13.19",
+    "karma-chrome-launcher": "~0.2.2",
+    "karma-coverage": "~0.5.3",
+    "karma-firefox-launcher": "~0.1.7",
+    "karma-jasmine": "~0.3.7",
+    "karma-ng-html2js-preprocessor": "~0.2.0",
+    "karma-phantomjs-launcher": "~1.0.0",
+    "load-grunt-tasks": "~3.4.0",
+    "time-grunt": "~1.3.0"
+  },
+  "dependencies": {},
+  "license": "MIT",
+  "licenses": [
+    {
+      "type": "MIT",
+      "url": "https://github.com/guylabs/ion-autocomplete/blob/master/LICENSE"
+    }
+  ],
+  "scripts": {
+    "start": "./node_modules/protractor/bin/webdriver-manager update && ./node_modules/protractor/bin/webdriver-manager start",
+    "test": "[ -d node_modules/protractor ] && ./node_modules/protractor/bin/webdriver-manager update && grunt build"
+  }
+}

+ 4 - 4
www/templates/menu.html

@@ -19,10 +19,10 @@
         </ion-header-bar>
         <ion-content>
             <ion-list>
-                <ion-item menu-close class="item-icon-left" ui-sref="app.main"><i class="icon ion-home positive" style="font-size:22px"></i> Inicio</ion-item>
-                <ion-item menu-close class="item-icon-left" ui-sref="app.sales"><i class="icon ion-pricetag positive" style="font-size:22px"></i> Ventas</ion-item>
-                <ion-item menu-close class="item-icon-left"><i class="icon ion-briefcase positive" style="font-size:22px"></i> Proyectos</ion-item>
-                <ion-item menu-close class="item-icon-left" ui-sref="app.preferences"><i class="icon ion-settings positive" style="font-size:22px"></i> Preferencias</ion-item>
+                <ion-item menu-close class="item-icon-left" ui-sref="app.main" ui-sref-active="active"><i class="icon ion-home positive" style="font-size:22px"></i> Inicio</ion-item>
+                <ion-item menu-close class="item-icon-left" ui-sref="app.sales" ui-sref-active="active"><i class="icon ion-pricetag positive" style="font-size:22px"></i> Ventas</ion-item>
+                <ion-item menu-close class="item-icon-left" ui-sref-active="active"><i class="icon ion-briefcase positive" style="font-size:22px"></i> Proyectos</ion-item>
+                <ion-item menu-close class="item-icon-left" ui-sref="app.preferences" ui-sref-active="active"><i class="icon ion-settings positive" style="font-size:22px"></i> Preferencias</ion-item>
             </ion-list>
         </ion-content>
     </ion-side-menu>

File diff suppressed because it is too large
+ 0 - 0
www/templates/sales/customers.html


+ 5 - 1
www/templates/sales/lead.html

@@ -8,9 +8,13 @@
     <ion-content>
         <form id="lead-form" ng-submit="save()">
             <div class="list">
+                <label class="item item-input item-stacked-label">
+                    <span class="input-label">Cliente</span>
+                    <input ion-autocomplete readonly type="text" name="name" class="ion-autocomplete" autocomplete="off" max-selected-items="1" items-method="getCustomersSuggestions(query)" placeholder="Buscar cliente" select-items-label="Seleccione un cliente" selected-items-label="Cliente:" ng-model="lead.partner_id" item-view-value-key="name" item-value-key="remote_id" items-clicked-method="selectCustomer(callback)" items-removed-method="deselectCustomer(callback)">
+                </label>
                 <label class="item item-input item-stacked-label">
                     <span class="input-label">Asunto</span>
-                    <input type="text" autofocus="autofocus" ng-model="lead.name">
+                    <input type="text" ng-model="lead.name">
                 </label>
                 <label class="item item-input item-stacked-label">
                     <span class="input-label">Contacto</span>

+ 6 - 4
www/templates/sales/opportunity.html

@@ -2,7 +2,7 @@
     <ion-header-bar class="bar bar-positive">
         <h1 class="title">Oportunidad</h1>
         <div class="buttons">
-            <button class="button button-clear ion-checkmark-round" style="font-size:22px !important; padding-left: 5px;" type="submit" form="opportunity-form" ng-disabled="!opportunity.name"></button>
+            <button class="button button-clear ion-checkmark-round" style="font-size:22px !important; padding-left: 5px;" type="submit" form="opportunity-form" ng-disabled="!opportunity.stage_id || !opportunity.name"></button>
         </div>
    </ion-header-bar>
     <ion-content>
@@ -12,9 +12,11 @@
                     <div class="input-label">
                         Estado
                     </div>
-                    <select ng-model="opportunity.stage_id">
-                        <option ng-repeat="s in stages" ng-selected="{{ opportunity.stage_id == s.remote_id}}" value="{{ s.remote_id }}">{{ s.name | translate }}</option>
-                    </select>
+                    <select ng-model="opportunity.stage_id" ng-options="s.id as s.name | translate for s in stages"></select>
+                </label>
+                <label class="item item-input item-stacked-label">
+                    <span class="input-label">Cliente</span>
+                    <input ion-autocomplete readonly type="text" name="name" class="ion-autocomplete" autocomplete="off" max-selected-items="1" items-method="getCustomersSuggestions(query)" placeholder="Buscar cliente" select-items-label="Seleccione un cliente" selected-items-label="Cliente:" ng-model="opportunity.partner_id" item-view-value-key="name" item-value-key="remote_id" items-clicked-method="selectCustomer(callback)" items-removed-method="deselectCustomer(callback)">
                 </label>
                 <label class="item item-input item-stacked-label">
                     <span class="input-label">Asunto</span>

+ 3 - 3
www/templates/sales/sales.html

@@ -1,9 +1,9 @@
 <ion-view title="Ventas">
     <ion-content>
         <ion-list>
-            <ion-item class="item-icon-left" ui-sref="app.customers"><i class="ion-person-stalker positive" style="font-size:22px"></i> Clientes</ion-item>
-            <ion-item class="item-icon-left" ui-sref="app.leads"><i class="ion-happy positive" style="font-size:22px"></i> Iniciativas</ion-item>
-            <ion-item class="item-icon-left" ui-sref="app.opportunities"><i class="ion-star positive" style="font-size:22px"></i> Oportunidades</ion-item>
+            <ion-item class="item-icon-left" ui-sref="app.customers" ui-sref-active="active"><i class="ion-person-stalker positive" style="font-size:22px"></i> Clientes</ion-item>
+            <ion-item class="item-icon-left" ui-sref="app.leads" ui-sref-active="active"><i class="ion-happy positive" style="font-size:22px"></i> Iniciativas</ion-item>
+            <ion-item class="item-icon-left" ui-sref="app.opportunities" ui-sref-active="active"><i class="ion-star positive" style="font-size:22px"></i> Oportunidades</ion-item>
         </ion-list>
     </ion-content>
 </ion-view>

Some files were not shown because too many files changed in this diff