zipcode-rack 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9a72a26ab78d0c7a3b36da3079d9f646729e9068
4
- data.tar.gz: 7233ff6e62eaebea24cca44ff851cac14d96e0c2
3
+ metadata.gz: 5897d5b4ced03a593f32fb730321c58153d0f018
4
+ data.tar.gz: 38b9ecbf5fbe8bd6fa9c4533b6affdbe34a83516
5
5
  SHA512:
6
- metadata.gz: bac9cf71c1409cb82c8816cafe5c1c276651632a9ed8f72be09ce81c21685e088e6305ffad95832cf3f8a7040e663cac76852f08eb048b21971c732b0521ce1b
7
- data.tar.gz: b2df86801a2f190eafd7bf81920fadb2df1888d623d02105aab85335711bcae89eaa61349972ccb68c866b0223671d92992fcc1465a4e28f9ac8c28d661330b2
6
+ metadata.gz: 717bda60af4d256a9d4f1f4511522b491f4e1b6d6a3464e4936ad725acea36edd710d02bd28d103593cd348e3af896b1302025c125b0cc7ca6bcde22dfd7b8f8
7
+ data.tar.gz: d5b564297165788bd246d6a17bb05d1a9ec85fac5f1188108e95f8b5bc3d37c0e9d122b5c0ba1d9185de0088f5b9bebb8e5d13972b853dc192331cba5a6fa71e
@@ -0,0 +1,357 @@
1
+ (function () {
2
+ function parse(text) {
3
+ var parsed = {};
4
+ var re = /^\s*(\d+)?\s*(\D.*)?$/;
5
+ var m = re.exec(text);
6
+ if (m !== null) {
7
+ parsed.zip = m[1];
8
+ parsed.name = m[2];
9
+ }
10
+ return parsed;
11
+ }
12
+
13
+ function queryString(obj) {
14
+ var kv = [];
15
+ for (var key in obj) {
16
+ if (obj.hasOwnProperty(key)) {
17
+ kv.push([key, obj[key]].join('='));
18
+ }
19
+ }
20
+ return kv.join('&');
21
+ }
22
+
23
+ function request(method, url, params, done) {
24
+ var http = new XMLHttpRequest();
25
+ http.open(method, url + '?' + queryString(params), true);
26
+ http.onreadystatechange = function() {
27
+ if (http.readyState === 4) {
28
+ if (http.status === 200) {
29
+ var data = JSON.parse(http.responseText);
30
+ done(data);
31
+ }
32
+ }
33
+ };
34
+ http.send(null);
35
+ }
36
+
37
+ var settings = {
38
+ endpoint: null,
39
+ dropdownClass: 'zip-city-dropdown',
40
+ };
41
+
42
+ function endpoint(url) {
43
+ if (url) { settings.endpoint = url; }
44
+ return settings.endpoint;
45
+ }
46
+
47
+ function dropdownClass(name) {
48
+ if (name) { settings.dropdownClass = name; }
49
+ return settings.dropdownClass;
50
+ }
51
+
52
+ function search(text, limit, done) {
53
+ var parsed = parse(text);
54
+ if (parsed.zip) {
55
+ request('GET', endpoint(), { zip: parsed.zip, limit: limit }, done);
56
+ } else if (parsed.name) {
57
+ request('GET', endpoint(), { name: parsed.name, limit: limit }, done);
58
+ }
59
+ }
60
+
61
+ function handleSearch(e) {
62
+ var input = e.target.value;
63
+ if (input === '' && e.target.dropdown) {
64
+ e.target.dropdown.remove();
65
+ e.target.dropdown = null;
66
+ }
67
+
68
+ search(input, 10, function (data) {
69
+ updatePicker(e.target, data);
70
+ });
71
+ }
72
+
73
+ function dropdownFor(input) {
74
+ if (input.dropdown) {
75
+ return input.dropdown;
76
+ }
77
+
78
+ var dropdown = createDropdown(input);
79
+ assignDropdown(input, dropdown);
80
+ return dropdown;
81
+ }
82
+
83
+ function createDropdown(input) {
84
+ var dropdown = document.createElement('div');
85
+
86
+ // TODO: auto-align right if needed
87
+ // TODO: max-width + hide overflow
88
+ var style = '';
89
+ style += 'position: absolute;';
90
+ style += 'top: ' + (input.offsetTop + input.offsetHeight) + 'px;';
91
+ style += 'left: ' + input.offsetLeft + 'px;';
92
+ style += 'min-width: ' + input.offsetWidth + 'px;';
93
+ dropdown.setAttribute('style', style);
94
+ dropdown.classList.add(dropdownClass());
95
+
96
+ return dropdown;
97
+ }
98
+
99
+ function isHovered(dropdown) {
100
+ var items = document.querySelectorAll(':hover');
101
+ for (var i = 0; i < items.length; i++) {
102
+ if (items[i] === dropdown) {
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+
109
+ function isFocused(input) {
110
+ var items = document.querySelectorAll('input:focus');
111
+ for (var i = 0; i < items.length; i++) {
112
+ if (items[i] === input) {
113
+ return true;
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+
119
+ function assignDropdown(input, dropdown) {
120
+ document.querySelector('body').appendChild(dropdown);
121
+ input.dropdown = dropdown;
122
+ input.addEventListener('blur', function() {
123
+ if (!isHovered(input.dropdown)) { removeDropdown(input); }
124
+ });
125
+ dropdown.addEventListener('mouseleave', function() {
126
+ if (!isFocused(input)) { removeDropdown(input); }
127
+ });
128
+ }
129
+
130
+ function removeDropdown(input) {
131
+ if (input.dropdown) {
132
+ var dropdown = input.dropdown;
133
+ input.dropdown = null;
134
+ dropdown.remove();
135
+ }
136
+ }
137
+
138
+ function renderResults(data) {
139
+ var list = '';
140
+ list += '<ul>';
141
+ for (var i = 0; i < data.length; i++) {
142
+ list += '<li>' + data[i].zip + ' ' + data[i].name + '</li>';
143
+ }
144
+ list += '</ul>';
145
+
146
+ return list;
147
+ }
148
+
149
+ function renderNoResult() {
150
+ return '<span>No results</span>';
151
+ }
152
+
153
+ function renderResultValue(item) {
154
+ return '' + item.zip + ' ' + item.name;
155
+ }
156
+
157
+ function setSelection(dropdown, item) {
158
+ var items = dropdown.querySelectorAll('li');
159
+ for (var i = 0; i < items.length; i++) {
160
+ if (items[i] === item) { dropdown.selection = i; }
161
+ }
162
+ }
163
+
164
+ function redrawSelection(dropdown) {
165
+ var items = dropdown.querySelectorAll('li');
166
+ for (var i = 0; i < items.length; i++) {
167
+ items[i].classList.remove('selected');
168
+ }
169
+ items[dropdown.selection].classList.add('selected');
170
+ }
171
+
172
+ function selectedValue(dropdown) {
173
+ var items = dropdown.querySelectorAll('li');
174
+ return items[dropdown.selection].data;
175
+ }
176
+
177
+ function applySelection(input) {
178
+ input.value = selectedValue(input.dropdown);
179
+ removeDropdown(input);
180
+ }
181
+
182
+ function handleResultMouseEnter(item, input) {
183
+ setSelection(input.dropdown, item);
184
+ redrawSelection(input.dropdown);
185
+ }
186
+
187
+ function handleResultClick(item, input) {
188
+ setSelection(input.dropdown, item);
189
+ redrawSelection(input.dropdown);
190
+ applySelection(input);
191
+ }
192
+
193
+ function resultMouseEnterHandler(input) {
194
+ return (function (e) { handleResultMouseEnter(e.target, input); });
195
+ }
196
+
197
+ function resultClickHandler(input) {
198
+ return (function (e) { handleResultClick(e.target, input); });
199
+ }
200
+
201
+ function setResultValues(dropdown, data) {
202
+ var items = dropdown.querySelectorAll('li');
203
+ for (var i = 0; i < items.length; i++) {
204
+ items[i].data = renderResultValue(data[i]);
205
+ }
206
+ }
207
+
208
+ function setResultEvents(input) {
209
+ var dropdown = input.dropdown;
210
+ var items = dropdown.querySelectorAll('li');
211
+ for (var i = 0; i < items.length; i++) {
212
+ items[i].addEventListener('click', resultClickHandler(input));
213
+ items[i].addEventListener('mouseenter', resultMouseEnterHandler(input));
214
+ }
215
+ }
216
+
217
+ function initializeSelection(dropdown) {
218
+ dropdown.selection = -1;
219
+ }
220
+
221
+ function updatePicker(input, data) {
222
+ var dropdown = dropdownFor(input);
223
+ initializeSelection(dropdown);
224
+ if (data.length > 0) {
225
+ dropdown.innerHTML = renderResults(data);
226
+ setResultValues(dropdown, data);
227
+ setResultEvents(input);
228
+ } else {
229
+ dropdown.innerHTML = renderNoResult();
230
+ }
231
+ }
232
+
233
+ function hasDropdown(input) {
234
+ return !!input.dropdown;
235
+ }
236
+
237
+ function wrapSelection(dropdown, length) {
238
+ if (dropdown.selection == length) {
239
+ dropdown.selection = 0;
240
+ }
241
+ if (dropdown.selection < 0) {
242
+ dropdown.selection = length - 1;
243
+ }
244
+ }
245
+
246
+ function moveSelection(dropdown, direction) {
247
+ var items = dropdown.querySelectorAll('li');
248
+ if (items.length > 0) {
249
+ dropdown.selection += direction;
250
+ wrapSelection(dropdown, items.length);
251
+ redrawSelection(dropdown);
252
+ }
253
+ }
254
+
255
+ var UP = -1;
256
+ var DOWN = 1;
257
+
258
+ function handleDown(e) {
259
+ e.preventDefault();
260
+ if (hasDropdown(e.target)) {
261
+ moveSelection(e.target.dropdown, DOWN);
262
+ } else {
263
+ handleSearch(e);
264
+ }
265
+ }
266
+
267
+ function handleUp(e) {
268
+ e.preventDefault();
269
+ if (hasDropdown(e.target)) {
270
+ moveSelection(e.target.dropdown, UP);
271
+ } else {
272
+ handleSearch(e);
273
+ }
274
+ }
275
+
276
+ function handleEnter(e) {
277
+ e.preventDefault();
278
+ applySelection(e.target);
279
+ removeDropdown(e.target);
280
+ }
281
+
282
+ function handleEscape(e) {
283
+ e.preventDefault();
284
+ removeDropdown(e.target);
285
+ }
286
+
287
+ function handleKeydown(e) {
288
+ switch (e.keyCode) {
289
+ case 40: // ArrowDown
290
+ handleDown(e);
291
+ break;
292
+ case 38: // ArrowUp
293
+ handleUp(e);
294
+ break;
295
+ case 13: // Enter
296
+ handleEnter(e);
297
+ break;
298
+ case 27: // Escape
299
+ handleEscape(e);
300
+ break;
301
+ default:
302
+ return;
303
+ }
304
+ }
305
+
306
+ function polyfill(input) {
307
+ if (!input.dropdown_listener) {
308
+ input.dropdown_listener = true;
309
+ input.addEventListener('keydown', handleKeydown);
310
+ input.addEventListener('input', handleSearch);
311
+ }
312
+ }
313
+
314
+ function polyfillDocument() {
315
+ var elements = document.querySelectorAll('input[type=zip-city]');
316
+ for (var i = 0; i < elements.length; i++) {
317
+ polyfill(elements[i]);
318
+ }
319
+ }
320
+
321
+ function polyfillInsertedNodes() {
322
+ var observer = new MutationObserver(function (mutations) {
323
+ mutations.forEach(function (mutation) {
324
+ for (var i = 0; i < mutation.addedNodes.length; i++) {
325
+ var node = mutation.addedNodes[i];
326
+ if (node.nodeName === 'INPUT' && node.getAttribute('type') === 'zip-city') {
327
+ polyfill(node);
328
+ }
329
+ }
330
+ });
331
+ });
332
+ observer.observe(document.body, { childList: true });
333
+ }
334
+
335
+ var loaded = false;
336
+ function handleLoaded() {
337
+ if (loaded) { return; }
338
+ loaded = true;
339
+
340
+ polyfillDocument();
341
+ polyfillInsertedNodes();
342
+
343
+ }
344
+
345
+ // export
346
+ this.ZipCode = {
347
+ endpoint: endpoint,
348
+ dropdownClass: dropdownClass,
349
+ };
350
+
351
+ // handle async
352
+ if (document.readyState == 'complete' || document.readyState == 'interactive') {
353
+ setTimeout(handleLoaded, 1);
354
+ } else {
355
+ document.addEventListener('DOMContentLoaded', handleLoaded);
356
+ }
357
+ }).call(this);
data/lib/json_enum.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+
3
+ class Enumerator
4
+ def to_json
5
+ '[' << each.with_object('') do |e, s|
6
+ s << "#{s.empty? ? '' : ','}" << e.to_json
7
+ end << ']'
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ require 'zipcode-rack'
2
+
3
+ class ZipCode::Engine < ::Rails::Engine
4
+ end
data/lib/zipcode-rack.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'grape'
2
+ require 'json_enum'
2
3
 
3
4
  module ZipCode
4
5
  class API < Grape::API
@@ -29,10 +30,11 @@ module ZipCode
29
30
  optional :name, type: String
30
31
  optional :zip, type: String
31
32
  optional :country, type: Symbol, regexp: /^[a-z]{2}$/
33
+ optional :limit, type: Integer, default: 20
32
34
  exactly_one_of :name, :zip
33
35
  end
34
36
  get '/search' do
35
- db.for(country).search(*criteria)
37
+ db.for(country).search(*criteria).take(params[:limit])
36
38
  end
37
39
  end
38
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zipcode-rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Loic Nageleisen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-05 00:00:00.000000000 Z
11
+ date: 2015-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grape
@@ -115,7 +115,10 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - app/assets/javascript/zipcode-input.js
119
+ - lib/json_enum.rb
118
120
  - lib/zipcode-rack.rb
121
+ - lib/zipcode/rails.rb
119
122
  homepage: https://github.com/lloeki/zipcode-rack
120
123
  licenses:
121
124
  - MIT