input {
36 | width: 100%;
37 | }
38 |
39 | a {
40 | text-decoration: none;
41 | color: inherit;
42 | -webkit-touch-callout: none;
43 | -webkit-tap-highlight-color: rgb(0, 0, 0);
44 | }
45 |
46 | .details {
47 | margin: auto;
48 | }
49 |
50 | .details>img {
51 | float:left;
52 | margin:10px;
53 | width: 80px;
54 | height: 80px;
55 | }
56 |
57 | .details h1 {
58 | padding: 12px 0px 4px 0px;
59 | margin: 0px 0px 0px 0px;
60 | font-size: 1.2rem;
61 | }
62 |
63 | .details h2 {
64 | padding: 0px 0px 0px 0px;
65 | margin: 0px 0px 0px 0px;
66 | font-size: 1.1rem;
67 | font-weight: normal;
68 | color: #888;
69 | }
70 |
71 | .list {
72 | list-style-type: none;
73 | }
74 |
75 | .list > li {
76 | position: relative;
77 | clear: both;
78 | padding: 0px;
79 | margin: 0px;
80 | }
81 |
82 | .list > li:nth-of-type(1) {
83 | border-top: none;
84 | }
85 |
86 | .list > li > a {
87 | margin: 0px;
88 | display: block;
89 | height: 57px;
90 | padding: 4px;
91 | }
92 |
93 |
94 | .list > li > a > p:nth-of-type(1) {
95 | margin: 8px 0px 0px 0px;
96 | font-weight: bold;
97 | }
98 |
99 | .list > li p:nth-of-type(2) {
100 | margin: 0px;
101 | color: #777;
102 | }
103 |
104 | .list > li img {
105 | width: 57px;
106 | height: 57px;
107 | float: left;
108 | margin-right: 8px;
109 | }
110 |
111 | .list li:active {
112 | background-color: #d6d6d6;
113 | }
114 |
115 | .actions > li > a {
116 | padding-left: 12px;
117 | }
118 |
119 | .action-icon {
120 | position: absolute !important;
121 | top: 18px;
122 | right: 20px !important;
123 | width: 28px !important;
124 | height: 28px;
125 | }
126 |
127 | .actions li p:nth-of-type(1) {
128 | color: #5DC1FF;
129 | font-size: 0.9em;
130 | font-weight: lighter;
131 | }
132 |
133 | .actions li p:nth-of-type(2) {
134 | color: inherit;
135 | }
136 |
137 | ul {
138 | clear:both;
139 | border-top: none !important;
140 | }
141 |
142 | .icon-call {
143 | background: transparent url(images/call.svg);
144 | background-repeat: no-repeat;
145 | -webkit-background-size: cover;
146 | -moz-background-size: cover;
147 | background-size: cover;
148 | }
149 |
150 | .icon-sms {
151 | background: transparent url(images/chat.svg);
152 | background-repeat: no-repeat;
153 | -webkit-background-size: cover;
154 | -moz-background-size: cover;
155 | background-size: cover;
156 | }
157 |
158 | .icon-mail {
159 | background: transparent url(images/email.svg);
160 | background-repeat: no-repeat;
161 | -webkit-background-size: cover;
162 | -moz-background-size: cover;
163 | background-size: cover;
164 | }
165 |
166 | .icon-manager {
167 | background: transparent url(images/next.svg);
168 | background-repeat: no-repeat;
169 | -webkit-background-size: cover;
170 | -moz-background-size: cover;
171 | background-size: cover;
172 | }
173 |
174 | .icon-reports {
175 | background: transparent url(images/next.svg);
176 | background-repeat: no-repeat;
177 | -webkit-background-size: cover;
178 | -moz-background-size: cover;
179 | background-size: cover;
180 | }
181 |
182 | .chevron {
183 | background: transparent url(images/next_blue.svg);
184 | background-repeat: no-repeat;
185 | background-size: contain;
186 | width: 20px;
187 | height: 20px;
188 | position: absolute;
189 | right: 12px;
190 | top: 22px;
191 | height: 50px;
192 | width: 28px;
193 | }
194 |
195 | .slide-left.ng-enter,
196 | .slide-left.ng-leave,
197 | .slide-right.ng-enter,
198 | .slide-right.ng-leave {
199 | position: absolute;
200 | top: 0; right: 0; bottom: 0; left: 0;
201 | background: inherit;
202 | -ms-transition: .25s ease-in-out;
203 | -webkit-transition: .25s ease-in-out;
204 | transition: .25s ease-in-out;
205 | }
206 |
207 |
208 | .slide-left.ng-enter {
209 | z-index: 101;
210 | -webkit-transform: translateX(100%);
211 | transform: translateX(100%);
212 | }
213 |
214 | .slide-left.ng-enter.ng-enter-active {
215 | -webkit-transform: translateX(0);
216 | transform: translateX(0);
217 | }
218 |
219 | .slide-left.ng-leave {
220 | z-index: 100;
221 | -webkit-transform: translateX(0);
222 | transform: translateX(0);
223 | }
224 |
225 | .slide-left.ng-leave.ng-leave-active {
226 | -webkit-transform: translateX(-100%);
227 | transform: translateX(-100%);
228 | }
229 |
230 | .slide-right.ng-enter {
231 | z-index: 100;
232 | -webkit-transform: translateX(-100%);
233 | transform: translateX(-100%);
234 | }
235 |
236 | .slide-right.ng-enter.ng-enter-active {
237 | -webkit-transform: translateX(0);
238 | transform: translateX(0);
239 | }
240 |
241 | .slide-right.ng-leave {
242 | z-index: 101;
243 | -webkit-transform: translateX(0);
244 | transform: translateX(0);
245 | }
246 |
247 | .slide-right.ng-leave.ng-leave-active {
248 | -webkit-transform: translateX(100%);
249 | transform: translateX(100%);
250 | }
--------------------------------------------------------------------------------
/client/js/memory-services.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | (function () {
4 |
5 | var employees = [
6 | {"id": 1, "firstName": "James", "lastName": "King", "managerId": 0, "managerName": "", "reports": 4, "title": "President and CEO", "department": "Corporate", "cellPhone": "617-000-0001", "officePhone": "781-000-0001", "email": "jking@fakemail.com", "city": "Boston, MA", "pic": "james_king.jpg", "twitterId": "@fakejking", "blog": "http://coenraets.org"},
7 | {"id": 2, "firstName": "Julie", "lastName": "Taylor", "managerId": 1, "managerName": "James King", "reports": 2, "title": "VP of Marketing", "department": "Marketing", "cellPhone": "617-000-0002", "officePhone": "781-000-0002", "email": "jtaylor@fakemail.com", "city": "Boston, MA", "pic": "julie_taylor.jpg", "twitterId": "@fakejtaylor", "blog": "http://coenraets.org"},
8 | {"id": 3, "firstName": "Eugene", "lastName": "Lee", "managerId": 1, "managerName": "James King", "reports": 0, "title": "CFO", "department": "Accounting", "cellPhone": "617-000-0003", "officePhone": "781-000-0003", "email": "elee@fakemail.com", "city": "Boston, MA", "pic": "eugene_lee.jpg", "twitterId": "@fakeelee", "blog": "http://coenraets.org"},
9 | {"id": 4, "firstName": "John", "lastName": "Williams", "managerId": 1, "managerName": "James King", "reports": 3, "title": "VP of Engineering", "department": "Engineering", "cellPhone": "617-000-0004", "officePhone": "781-000-0004", "email": "jwilliams@fakemail.com", "city": "Boston, MA", "pic": "john_williams.jpg", "twitterId": "@fakejwilliams", "blog": "http://coenraets.org"},
10 | {"id": 5, "firstName": "Ray", "lastName": "Moore", "managerId": 1, "managerName": "James King", "reports": 2, "title": "VP of Sales", "department": "Sales", "cellPhone": "617-000-0005", "officePhone": "781-000-0005", "email": "rmoore@fakemail.com", "city": "Boston, MA", "pic": "ray_moore.jpg", "twitterId": "@fakermoore", "blog": "http://coenraets.org"},
11 | {"id": 6, "firstName": "Paul", "lastName": "Jones", "managerId": 4, "managerName": "John Williams", "reports": 0, "title": "QA Manager", "department": "Engineering", "cellPhone": "617-000-0006", "officePhone": "781-000-0006", "email": "pjones@fakemail.com", "city": "Boston, MA", "pic": "paul_jones.jpg", "twitterId": "@fakepjones", "blog": "http://coenraets.org"},
12 | {"id": 7, "firstName": "Paula", "lastName": "Gates", "managerId": 4, "managerName": "John Williams", "reports": 0, "title": "Software Architect", "department": "Engineering", "cellPhone": "617-000-0007", "officePhone": "781-000-0007", "email": "pgates@fakemail.com", "city": "Boston, MA", "pic": "paula_gates.jpg", "twitterId": "@fakepgates", "blog": "http://coenraets.org"},
13 | {"id": 8, "firstName": "Lisa", "lastName": "Wong", "managerId": 2, "managerName": "Julie Taylor", "reports": 0, "title": "Marketing Manager", "department": "Marketing", "cellPhone": "617-000-0008", "officePhone": "781-000-0008", "email": "lwong@fakemail.com", "city": "Boston, MA", "pic": "lisa_wong.jpg", "twitterId": "@fakelwong", "blog": "http://coenraets.org"},
14 | {"id": 9, "firstName": "Gary", "lastName": "Donovan", "managerId": 2, "managerName": "Julie Taylor", "reports": 0, "title": "Marketing Manager", "department": "Marketing", "cellPhone": "617-000-0009", "officePhone": "781-000-0009", "email": "gdonovan@fakemail.com", "city": "Boston, MA", "pic": "gary_donovan.jpg", "twitterId": "@fakegdonovan", "blog": "http://coenraets.org"},
15 | {"id": 10, "firstName": "Kathleen", "lastName": "Byrne", "managerId": 5, "managerName": "Ray Moore", "reports": 0, "title": "Sales Representative", "department": "Sales", "cellPhone": "617-000-0010", "officePhone": "781-000-0010", "email": "kbyrne@fakemail.com", "city": "Boston, MA", "pic": "kathleen_byrne.jpg", "twitterId": "@fakekbyrne", "blog": "http://coenraets.org"},
16 | {"id": 11, "firstName": "Amy", "lastName": "Jones", "managerId": 5, "managerName": "Ray Moore", "reports": 0, "title": "Sales Representative", "department": "Sales", "cellPhone": "617-000-0011", "officePhone": "781-000-0011", "email": "ajones@fakemail.com", "city": "Boston, MA", "pic": "amy_jones.jpg", "twitterId": "@fakeajones", "blog": "http://coenraets.org"},
17 | {"id": 12, "firstName": "Steven", "lastName": "Wells", "managerId": 4, "managerName": "John Williams", "reports": 0, "title": "Software Architect", "department": "Engineering", "cellPhone": "617-000-0012", "officePhone": "781-000-0012", "email": "swells@fakemail.com", "city": "Boston, MA", "pic": "steven_wells.jpg", "twitterId": "@fakeswells", "blog": "http://coenraets.org"}
18 | ],
19 |
20 | findById = function (id) {
21 | var employee = null,
22 | l = employees.length,
23 | i;
24 | for (i = 0; i < l; i = i + 1) {
25 | if (employees[i].id === id) {
26 | employee = employees[i];
27 | break;
28 | }
29 | }
30 | return employee;
31 | },
32 |
33 | findByManager = function (managerId) {
34 | var results = employees.filter(function (element) {
35 | return managerId === element.managerId;
36 | });
37 | return results;
38 | };
39 |
40 |
41 | angular.module('myApp.memoryServices', [])
42 | .factory('Employee', [
43 | function () {
44 | return {
45 | query: function () {
46 | return employees;
47 | },
48 | get: function (employee) {
49 | return findById(parseInt(employee.employeeId));
50 | }
51 | }
52 |
53 | }])
54 | .factory('Report', [
55 | function () {
56 | return {
57 | query: function (employee) {
58 | return findByManager(parseInt(employee.employeeId));
59 | }
60 | }
61 |
62 | }]);
63 |
64 | }());
--------------------------------------------------------------------------------
/server/routes/employee.js:
--------------------------------------------------------------------------------
1 | var MongoClient = require('mongodb').MongoClient,
2 | Server = require('mongodb').Server,
3 | db;
4 |
5 | var mongoClient = new MongoClient(new Server('localhost', 27017));
6 | mongoClient.open(function(err, mongoClient) {
7 | db = mongoClient.db("angular-directory-db");
8 | db.collection('employees', {strict:true}, function(err, collection) {
9 | if (err) {
10 | console.log("The 'employees' collection doesn't exist. Creating it with sample data...");
11 | populateDB();
12 | }
13 | });
14 | });
15 |
16 | exports.findById = function(req, res) {
17 | console.log(req.params);
18 | var id = parseInt(req.params.id);
19 | console.log('findById: ' + id);
20 | db.collection('employees', function(err, collection) {
21 | collection.findOne({'id': id}, function(err, item) {
22 | console.log(item);
23 | res.jsonp(item);
24 | });
25 | });
26 | };
27 |
28 | exports.findByManager = function(req, res) {
29 | var id = parseInt(req.params.id);
30 | console.log('findByManager: ' + id);
31 | db.collection('employees', function(err, collection) {
32 | collection.find({'managerId': id}).toArray(function(err, items) {
33 | console.log(items);
34 | res.jsonp(items);
35 | });
36 | });
37 | };
38 |
39 | exports.findAll = function(req, res) {
40 | var name = req.query["name"];
41 | db.collection('employees', function(err, collection) {
42 | if (name) {
43 | collection.find({"fullName": new RegExp(name, "i")}).toArray(function(err, items) {
44 | res.jsonp(items);
45 | });
46 | } else {
47 | collection.find().toArray(function(err, items) {
48 | res.jsonp(items);
49 | });
50 | }
51 | });
52 | };
53 |
54 | /*--------------------------------------------------------------------------------------------------------------------*/
55 | // Populate database with sample data -- Only used once: the first time the application is started.
56 | // You'd typically not find this code in a real-life app, since the database would already exist.
57 | var populateDB = function() {
58 |
59 | console.log("Populating employee database...");
60 | var employees = [
61 | {"id": 1, "firstName": "James", "lastName": "King", "fullName": "James King", "managerId": 0, "reports": 4, managerName: "", "title": "President and CEO", "department": "Corporate", "cellPhone": "617-000-0001", "officePhone": "781-000-0001", "email": "jking@fakemail.com", "city": "Boston, MA", "pic": "james_king.jpg", "twitterId": "@fakejking", "blog": "http://coenraets.org"},
62 | {"id": 2, "firstName": "Julie", "lastName": "Taylor", "fullName": "Julie Taylor", "managerId": 1, "reports": 2, managerName: "James King", "title": "VP of Marketing", "department": "Marketing", "cellPhone": "617-000-0002", "officePhone": "781-000-0002", "email": "jtaylor@fakemail.com", "city": "Boston, MA", "pic": "julie_taylor.jpg", "twitterId": "@fakejtaylor", "blog": "http://coenraets.org"},
63 | {"id": 3, "firstName": "Eugene", "lastName": "Lee", "fullName": "Eugene Lee", "managerId": 1, "reports": 0, managerName: "James King", "title": "CFO", "department": "Accounting", "cellPhone": "617-000-0003", "officePhone": "781-000-0003", "email": "elee@fakemail.com", "city": "Boston, MA", "pic": "eugene_lee.jpg", "twitterId": "@fakeelee", "blog": "http://coenraets.org"},
64 | {"id": 4, "firstName": "John", "lastName": "Williams", "fullName": "John Williams", "managerId": 1, "reports": 3, managerName: "James King", "title": "VP of Engineering", "department": "Engineering", "cellPhone": "617-000-0004", "officePhone": "781-000-0004", "email": "jwilliams@fakemail.com", "city": "Boston, MA", "pic": "john_williams.jpg", "twitterId": "@fakejwilliams", "blog": "http://coenraets.org"},
65 | {"id": 5, "firstName": "Ray", "lastName": "Moore", "fullName": "Ray Moore", "managerId": 1, "reports": 2, managerName: "James King", "title": "VP of Sales", "department": "Sales", "cellPhone": "617-000-0005", "officePhone": "781-000-0005", "email": "rmoore@fakemail.com", "city": "Boston, MA", "pic": "ray_moore.jpg", "twitterId": "@fakermoore", "blog": "http://coenraets.org"},
66 | {"id": 6, "firstName": "Paul", "lastName": "Jones", "fullName": "Paul Jones", "managerId": 4, "reports": 0, managerName: "John Williams", "title": "QA Manager", "department": "Engineering", "cellPhone": "617-000-0006", "officePhone": "781-000-0006", "email": "pjones@fakemail.com", "city": "Boston, MA", "pic": "paul_jones.jpg", "twitterId": "@fakepjones", "blog": "http://coenraets.org"},
67 | {"id": 7, "firstName": "Paula", "lastName": "Gates", "fullName": "Paula Gates", "managerId": 4, "reports": 0, managerName: "John Williams", "title": "Software Architect", "department": "Engineering", "cellPhone": "617-000-0007", "officePhone": "781-000-0007", "email": "pgates@fakemail.com", "city": "Boston, MA", "pic": "paula_gates.jpg", "twitterId": "@fakepgates", "blog": "http://coenraets.org"},
68 | {"id": 8, "firstName": "Lisa", "lastName": "Wong", "fullName": "Lisa Wong", "managerId": 2, "reports": 0, managerName: "Julie Taylor", "title": "Marketing Manager", "department": "Marketing", "cellPhone": "617-000-0008", "officePhone": "781-000-0008", "email": "lwong@fakemail.com", "city": "Boston, MA", "pic": "lisa_wong.jpg", "twitterId": "@fakelwong", "blog": "http://coenraets.org"},
69 | {"id": 9, "firstName": "Gary", "lastName": "Donovan", "fullName": "Gary Donovan", "managerId": 2, "reports": 0, managerName: "Julie Taylor", "title": "Marketing Manager", "department": "Marketing", "cellPhone": "617-000-0009", "officePhone": "781-000-0009", "email": "gdonovan@fakemail.com", "city": "Boston, MA", "pic": "gary_donovan.jpg", "twitterId": "@fakegdonovan", "blog": "http://coenraets.org"},
70 | {"id": 10, "firstName": "Kathleen", "lastName": "Byrne", "fullName": "Kathleen Byrne", "managerId": 5, "reports": 0, managerName: "Ray Moore", "title": "Sales Representative", "department": "Sales", "cellPhone": "617-000-0010", "officePhone": "781-000-0010", "email": "kbyrne@fakemail.com", "city": "Boston, MA", "pic": "kathleen_byrne.jpg", "twitterId": "@fakekbyrne", "blog": "http://coenraets.org"},
71 | {"id": 11, "firstName": "Amy", "lastName": "Jones", "fullName": "Amy Jones", "managerId": 5, "reports": 0, managerName: "Ray Moore", "title": "Sales Representative", "department": "Sales", "cellPhone": "617-000-0011", "officePhone": "781-000-0011", "email": "ajones@fakemail.com", "city": "Boston, MA", "pic": "amy_jones.jpg", "twitterId": "@fakeajones", "blog": "http://coenraets.org"},
72 | {"id": 12, "firstName": "Steven", "lastName": "Wells", "fullName": "Steven Wells", "managerId": 4, "reports": 0, managerName: "John Williams", "title": "Software Architect", "department": "Engineering", "cellPhone": "617-000-0012", "officePhone": "781-000-0012", "email": "swells@fakemail.com", "city": "Boston, MA", "pic": "steven_wells.jpg", "twitterId": "@fakeswells", "blog": "http://coenraets.org"}
73 | ];
74 |
75 | db.collection('employees', function(err, collection) {
76 | collection.insert(employees, {safe:true}, function(err, result) {});
77 | });
78 |
79 | };
--------------------------------------------------------------------------------
/client/lib/angular-touch.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license AngularJS v1.2.0-rc.3
3 | * (c) 2010-2012 Google, Inc. http://angularjs.org
4 | * License: MIT
5 | */
6 | (function(window, angular, undefined) {'use strict';
7 |
8 | /**
9 | * @ngdoc overview
10 | * @name ngTouch
11 | * @description
12 | *
13 | * # ngTouch
14 | *
15 | * `ngTouch` is the name of the optional Angular module that provides touch events and other
16 | * helpers for touch-enabled devices.
17 | * The implementation is based on jQuery Mobile touch event handling
18 | * ([jquerymobile.com](http://jquerymobile.com/))
19 | *
20 | * {@installModule touch}
21 | *
22 | * See {@link ngTouch.$swipe `$swipe`} for usage.
23 | */
24 |
25 | // define ngTouch module
26 | var ngTouch = angular.module('ngTouch', []);
27 |
28 | /**
29 | * @ngdoc object
30 | * @name ngTouch.$swipe
31 | *
32 | * @description
33 | * The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
34 | * behavior, to make implementing swipe-related directives more convenient.
35 | *
36 | * Requires the {@link ngTouch `ngTouch`} module to be installed.
37 | *
38 | * `$swipe` is used by the `ngSwipeLeft` and `ngSwipeRight` directives in `ngTouch`, and by
39 | * `ngCarousel` in a separate component.
40 | *
41 | * # Usage
42 | * The `$swipe` service is an object with a single method: `bind`. `bind` takes an element
43 | * which is to be watched for swipes, and an object with four handler functions. See the
44 | * documentation for `bind` below.
45 | */
46 |
47 | ngTouch.factory('$swipe', [function() {
48 | // The total distance in any direction before we make the call on swipe vs. scroll.
49 | var MOVE_BUFFER_RADIUS = 10;
50 |
51 | function getCoordinates(event) {
52 | var touches = event.touches && event.touches.length ? event.touches : [event];
53 | var e = (event.changedTouches && event.changedTouches[0]) ||
54 | (event.originalEvent && event.originalEvent.changedTouches &&
55 | event.originalEvent.changedTouches[0]) ||
56 | touches[0].originalEvent || touches[0];
57 |
58 | return {
59 | x: e.clientX,
60 | y: e.clientY
61 | };
62 | }
63 |
64 | return {
65 | /**
66 | * @ngdoc method
67 | * @name ngTouch.$swipe#bind
68 | * @methodOf ngTouch.$swipe
69 | *
70 | * @description
71 | * The main method of `$swipe`. It takes an element to be watched for swipe motions, and an
72 | * object containing event handlers.
73 | *
74 | * The four events are `start`, `move`, `end`, and `cancel`. `start`, `move`, and `end`
75 | * receive as a parameter a coordinates object of the form `{ x: 150, y: 310 }`.
76 | *
77 | * `start` is called on either `mousedown` or `touchstart`. After this event, `$swipe` is
78 | * watching for `touchmove` or `mousemove` events. These events are ignored until the total
79 | * distance moved in either dimension exceeds a small threshold.
80 | *
81 | * Once this threshold is exceeded, either the horizontal or vertical delta is greater.
82 | * - If the horizontal distance is greater, this is a swipe and `move` and `end` events follow.
83 | * - If the vertical distance is greater, this is a scroll, and we let the browser take over.
84 | * A `cancel` event is sent.
85 | *
86 | * `move` is called on `mousemove` and `touchmove` after the above logic has determined that
87 | * a swipe is in progress.
88 | *
89 | * `end` is called when a swipe is successfully completed with a `touchend` or `mouseup`.
90 | *
91 | * `cancel` is called either on a `touchcancel` from the browser, or when we begin scrolling
92 | * as described above.
93 | *
94 | */
95 | bind: function(element, eventHandlers) {
96 | // Absolute total movement, used to control swipe vs. scroll.
97 | var totalX, totalY;
98 | // Coordinates of the start position.
99 | var startCoords;
100 | // Last event's position.
101 | var lastPos;
102 | // Whether a swipe is active.
103 | var active = false;
104 |
105 | element.on('touchstart mousedown', function(event) {
106 | startCoords = getCoordinates(event);
107 | active = true;
108 | totalX = 0;
109 | totalY = 0;
110 | lastPos = startCoords;
111 | eventHandlers['start'] && eventHandlers['start'](startCoords, event);
112 | });
113 |
114 | element.on('touchcancel', function(event) {
115 | active = false;
116 | eventHandlers['cancel'] && eventHandlers['cancel'](event);
117 | });
118 |
119 | element.on('touchmove mousemove', function(event) {
120 | if (!active) return;
121 |
122 | // Android will send a touchcancel if it thinks we're starting to scroll.
123 | // So when the total distance (+ or - or both) exceeds 10px in either direction,
124 | // we either:
125 | // - On totalX > totalY, we send preventDefault() and treat this as a swipe.
126 | // - On totalY > totalX, we let the browser handle it as a scroll.
127 |
128 | if (!startCoords) return;
129 | var coords = getCoordinates(event);
130 |
131 | totalX += Math.abs(coords.x - lastPos.x);
132 | totalY += Math.abs(coords.y - lastPos.y);
133 |
134 | lastPos = coords;
135 |
136 | if (totalX < MOVE_BUFFER_RADIUS && totalY < MOVE_BUFFER_RADIUS) {
137 | return;
138 | }
139 |
140 | // One of totalX or totalY has exceeded the buffer, so decide on swipe vs. scroll.
141 | if (totalY > totalX) {
142 | // Allow native scrolling to take over.
143 | active = false;
144 | eventHandlers['cancel'] && eventHandlers['cancel'](event);
145 | return;
146 | } else {
147 | // Prevent the browser from scrolling.
148 | event.preventDefault();
149 | eventHandlers['move'] && eventHandlers['move'](coords, event);
150 | }
151 | });
152 |
153 | element.on('touchend mouseup', function(event) {
154 | if (!active) return;
155 | active = false;
156 | eventHandlers['end'] && eventHandlers['end'](getCoordinates(event), event);
157 | });
158 | }
159 | };
160 | }]);
161 |
162 | /**
163 | * @ngdoc directive
164 | * @name ngTouch.directive:ngClick
165 | *
166 | * @description
167 | * A more powerful replacement for the default ngClick designed to be used on touchscreen
168 | * devices. Most mobile browsers wait about 300ms after a tap-and-release before sending
169 | * the click event. This version handles them immediately, and then prevents the
170 | * following click event from propagating.
171 | *
172 | * Requires the {@link ngTouch `ngTouch`} module to be installed.
173 | *
174 | * This directive can fall back to using an ordinary click event, and so works on desktop
175 | * browsers as well as mobile.
176 | *
177 | * This directive also sets the CSS class `ng-click-active` while the element is being held
178 | * down (by a mouse click or touch) so you can restyle the depressed element if you wish.
179 | *
180 | * @element ANY
181 | * @param {expression} ngClick {@link guide/expression Expression} to evaluate
182 | * upon tap. (Event object is available as `$event`)
183 | *
184 | * @example
185 |
186 |
187 |
190 | count: {{ count }}
191 |
192 |
193 | */
194 |
195 | ngTouch.config(['$provide', function($provide) {
196 | $provide.decorator('ngClickDirective', ['$delegate', function($delegate) {
197 | // drop the default ngClick directive
198 | $delegate.shift();
199 | return $delegate;
200 | }]);
201 | }]);
202 |
203 | ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
204 | function($parse, $timeout, $rootElement) {
205 | var TAP_DURATION = 750; // Shorter than 750ms is a tap, longer is a taphold or drag.
206 | var MOVE_TOLERANCE = 12; // 12px seems to work in most mobile browsers.
207 | var PREVENT_DURATION = 2500; // 2.5 seconds maximum from preventGhostClick call to click
208 | var CLICKBUSTER_THRESHOLD = 25; // 25 pixels in any dimension is the limit for busting clicks.
209 |
210 | var ACTIVE_CLASS_NAME = 'ng-click-active';
211 | var lastPreventedTime;
212 | var touchCoordinates;
213 |
214 |
215 | // TAP EVENTS AND GHOST CLICKS
216 | //
217 | // Why tap events?
218 | // Mobile browsers detect a tap, then wait a moment (usually ~300ms) to see if you're
219 | // double-tapping, and then fire a click event.
220 | //
221 | // This delay sucks and makes mobile apps feel unresponsive.
222 | // So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when
223 | // the user has tapped on something.
224 | //
225 | // What happens when the browser then generates a click event?
226 | // The browser, of course, also detects the tap and fires a click after a delay. This results in
227 | // tapping/clicking twice. So we do "clickbusting" to prevent it.
228 | //
229 | // How does it work?
230 | // We attach global touchstart and click handlers, that run during the capture (early) phase.
231 | // So the sequence for a tap is:
232 | // - global touchstart: Sets an "allowable region" at the point touched.
233 | // - element's touchstart: Starts a touch
234 | // (- touchmove or touchcancel ends the touch, no click follows)
235 | // - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
236 | // too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
237 | // - preventGhostClick() removes the allowable region the global touchstart created.
238 | // - The browser generates a click event.
239 | // - The global click handler catches the click, and checks whether it was in an allowable region.
240 | // - If preventGhostClick was called, the region will have been removed, the click is busted.
241 | // - If the region is still there, the click proceeds normally. Therefore clicks on links and
242 | // other elements without ngTap on them work normally.
243 | //
244 | // This is an ugly, terrible hack!
245 | // Yeah, tell me about it. The alternatives are using the slow click events, or making our users
246 | // deal with the ghost clicks, so I consider this the least of evils. Fortunately Angular
247 | // encapsulates this ugly logic away from the user.
248 | //
249 | // Why not just put click handlers on the element?
250 | // We do that too, just to be sure. The problem is that the tap event might have caused the DOM
251 | // to change, so that the click fires in the same position but something else is there now. So
252 | // the handlers are global and care only about coordinates and not elements.
253 |
254 | // Checks if the coordinates are close enough to be within the region.
255 | function hit(x1, y1, x2, y2) {
256 | return Math.abs(x1 - x2) < CLICKBUSTER_THRESHOLD && Math.abs(y1 - y2) < CLICKBUSTER_THRESHOLD;
257 | }
258 |
259 | // Checks a list of allowable regions against a click location.
260 | // Returns true if the click should be allowed.
261 | // Splices out the allowable region from the list after it has been used.
262 | function checkAllowableRegions(touchCoordinates, x, y) {
263 | for (var i = 0; i < touchCoordinates.length; i += 2) {
264 | if (hit(touchCoordinates[i], touchCoordinates[i+1], x, y)) {
265 | touchCoordinates.splice(i, i + 2);
266 | return true; // allowable region
267 | }
268 | }
269 | return false; // No allowable region; bust it.
270 | }
271 |
272 | // Global click handler that prevents the click if it's in a bustable zone and preventGhostClick
273 | // was called recently.
274 | function onClick(event) {
275 | if (Date.now() - lastPreventedTime > PREVENT_DURATION) {
276 | return; // Too old.
277 | }
278 |
279 | var touches = event.touches && event.touches.length ? event.touches : [event];
280 | var x = touches[0].clientX;
281 | var y = touches[0].clientY;
282 | // Work around desktop Webkit quirk where clicking a label will fire two clicks (on the label
283 | // and on the input element). Depending on the exact browser, this second click we don't want
284 | // to bust has either (0,0) or negative coordinates.
285 | if (x < 1 && y < 1) {
286 | return; // offscreen
287 | }
288 |
289 | // Look for an allowable region containing this click.
290 | // If we find one, that means it was created by touchstart and not removed by
291 | // preventGhostClick, so we don't bust it.
292 | if (checkAllowableRegions(touchCoordinates, x, y)) {
293 | return;
294 | }
295 |
296 | // If we didn't find an allowable region, bust the click.
297 | event.stopPropagation();
298 | event.preventDefault();
299 |
300 | // Blur focused form elements
301 | event.target && event.target.blur();
302 | }
303 |
304 |
305 | // Global touchstart handler that creates an allowable region for a click event.
306 | // This allowable region can be removed by preventGhostClick if we want to bust it.
307 | function onTouchStart(event) {
308 | var touches = event.touches && event.touches.length ? event.touches : [event];
309 | var x = touches[0].clientX;
310 | var y = touches[0].clientY;
311 | touchCoordinates.push(x, y);
312 |
313 | $timeout(function() {
314 | // Remove the allowable region.
315 | for (var i = 0; i < touchCoordinates.length; i += 2) {
316 | if (touchCoordinates[i] == x && touchCoordinates[i+1] == y) {
317 | touchCoordinates.splice(i, i + 2);
318 | return;
319 | }
320 | }
321 | }, PREVENT_DURATION, false);
322 | }
323 |
324 | // On the first call, attaches some event handlers. Then whenever it gets called, it creates a
325 | // zone around the touchstart where clicks will get busted.
326 | function preventGhostClick(x, y) {
327 | if (!touchCoordinates) {
328 | $rootElement[0].addEventListener('click', onClick, true);
329 | $rootElement[0].addEventListener('touchstart', onTouchStart, true);
330 | touchCoordinates = [];
331 | }
332 |
333 | lastPreventedTime = Date.now();
334 |
335 | checkAllowableRegions(touchCoordinates, x, y);
336 | }
337 |
338 | // Actual linking function.
339 | return function(scope, element, attr) {
340 | var clickHandler = $parse(attr.ngClick),
341 | tapping = false,
342 | tapElement, // Used to blur the element after a tap.
343 | startTime, // Used to check if the tap was held too long.
344 | touchStartX,
345 | touchStartY;
346 |
347 | function resetState() {
348 | tapping = false;
349 | element.removeClass(ACTIVE_CLASS_NAME);
350 | }
351 |
352 | element.on('touchstart', function(event) {
353 | tapping = true;
354 | tapElement = event.target ? event.target : event.srcElement; // IE uses srcElement.
355 | // Hack for Safari, which can target text nodes instead of containers.
356 | if(tapElement.nodeType == 3) {
357 | tapElement = tapElement.parentNode;
358 | }
359 |
360 | element.addClass(ACTIVE_CLASS_NAME);
361 |
362 | startTime = Date.now();
363 |
364 | var touches = event.touches && event.touches.length ? event.touches : [event];
365 | var e = touches[0].originalEvent || touches[0];
366 | touchStartX = e.clientX;
367 | touchStartY = e.clientY;
368 | });
369 |
370 | element.on('touchmove', function(event) {
371 | resetState();
372 | });
373 |
374 | element.on('touchcancel', function(event) {
375 | resetState();
376 | });
377 |
378 | element.on('touchend', function(event) {
379 | var diff = Date.now() - startTime;
380 |
381 | var touches = (event.changedTouches && event.changedTouches.length) ? event.changedTouches :
382 | ((event.touches && event.touches.length) ? event.touches : [event]);
383 | var e = touches[0].originalEvent || touches[0];
384 | var x = e.clientX;
385 | var y = e.clientY;
386 | var dist = Math.sqrt( Math.pow(x - touchStartX, 2) + Math.pow(y - touchStartY, 2) );
387 |
388 | if (tapping && diff < TAP_DURATION && dist < MOVE_TOLERANCE) {
389 | // Call preventGhostClick so the clickbuster will catch the corresponding click.
390 | preventGhostClick(x, y);
391 |
392 | // Blur the focused element (the button, probably) before firing the callback.
393 | // This doesn't work perfectly on Android Chrome, but seems to work elsewhere.
394 | // I couldn't get anything to work reliably on Android Chrome.
395 | if (tapElement) {
396 | tapElement.blur();
397 | }
398 |
399 | if (!angular.isDefined(attr.disabled) || attr.disabled === false) {
400 | element.triggerHandler('click', [event]);
401 | }
402 | }
403 |
404 | resetState();
405 | });
406 |
407 | // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
408 | // something else nearby.
409 | element.onclick = function(event) { };
410 |
411 | // Actual click handler.
412 | // There are three different kinds of clicks, only two of which reach this point.
413 | // - On desktop browsers without touch events, their clicks will always come here.
414 | // - On mobile browsers, the simulated "fast" click will call this.
415 | // - But the browser's follow-up slow click will be "busted" before it reaches this handler.
416 | // Therefore it's safe to use this directive on both mobile and desktop.
417 | element.on('click', function(event, touchend) {
418 | scope.$apply(function() {
419 | clickHandler(scope, {$event: (touchend || event)});
420 | });
421 | });
422 |
423 | element.on('mousedown', function(event) {
424 | element.addClass(ACTIVE_CLASS_NAME);
425 | });
426 |
427 | element.on('mousemove mouseup', function(event) {
428 | element.removeClass(ACTIVE_CLASS_NAME);
429 | });
430 |
431 | };
432 | }]);
433 |
434 | /**
435 | * @ngdoc directive
436 | * @name ngTouch.directive:ngSwipeLeft
437 | *
438 | * @description
439 | * Specify custom behavior when an element is swiped to the left on a touchscreen device.
440 | * A leftward swipe is a quick, right-to-left slide of the finger.
441 | * Though ngSwipeLeft is designed for touch-based devices, it will work with a mouse click and drag too.
442 | *
443 | * Requires the {@link ngTouch `ngTouch`} module to be installed.
444 | *
445 | * @element ANY
446 | * @param {expression} ngSwipeLeft {@link guide/expression Expression} to evaluate
447 | * upon left swipe. (Event object is available as `$event`)
448 | *
449 | * @example
450 |
451 |
452 |
453 | Some list content, like an email in the inbox
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 | */
462 |
463 | /**
464 | * @ngdoc directive
465 | * @name ngTouch.directive:ngSwipeRight
466 | *
467 | * @description
468 | * Specify custom behavior when an element is swiped to the right on a touchscreen device.
469 | * A rightward swipe is a quick, left-to-right slide of the finger.
470 | * Though ngSwipeRight is designed for touch-based devices, it will work with a mouse click and drag too.
471 | *
472 | * Requires the {@link ngTouch `ngTouch`} module to be installed.
473 | *
474 | * @element ANY
475 | * @param {expression} ngSwipeRight {@link guide/expression Expression} to evaluate
476 | * upon right swipe. (Event object is available as `$event`)
477 | *
478 | * @example
479 |
480 |
481 |
482 | Some list content, like an email in the inbox
483 |