rec-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA512:
3
+ metadata.gz: 609036ff9cbd04e3bedf450eefb6ed1b60d9b75e838e36c67e8e7dcdaf749f1dc197a4542affe79a56bdddf2aeb4949fe090a895bd01e444f8ec04522e4b79b5
4
+ data.tar.gz: 433ecae74bd13bf3a77f1c5feb7b26a2d2d839cfd33634802e1185bed63582b0cc6842fcd4fd172e740c1ed0dfabd417aaa6b58a03bbaa6fa1ff942a2d4441e4
5
+ SHA1:
6
+ metadata.gz: d545d4c2fa2552209c2619b209861566c6ca5ff7
7
+ data.tar.gz: acb204b73ef1f77a33e54174f29f91a9bff62618
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in olay-rails.gemspec
4
+ gemspec
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2013 Casey Foster <c@sey.me>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ # Rec for Rails
2
+
3
+ Places [Rec](http://orgsync.github.com/rec) into the Rails asset pipeline.
4
+
5
+ ## Versioning
6
+
7
+ This project uses [Semantic Versioning](http://semver.org/) and will account for the underlying Rec JavaScript library versioning.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'rec-rails', '~> 0.1'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ ## Usage
20
+
21
+ In your application.js:
22
+
23
+ //= require rec
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,2 @@
1
+ require 'rec/rails/version'
2
+ require 'rec/rails/engine'
@@ -0,0 +1,6 @@
1
+ module Rec
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Rec
2
+ VERSION = '0.0.1'
3
+ module Rails
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rec/rails/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'rec-rails'
8
+ gem.version = Rec::Rails::VERSION
9
+ gem.summary = "Recommendations as you type."
10
+ gem.description = "Places Rec #{Rec::VERSION} in the Rails asset pipeline."
11
+ gem.homepage = 'https://github.com/orgsync/rec-rails'
12
+
13
+ gem.authors = ['Casey Foster']
14
+ gem.email = ['c@sey.me']
15
+
16
+ gem.license = 'MIT'
17
+
18
+ gem.files = `git ls-files`.split($/)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ['lib']
22
+
23
+ gem.add_dependency 'rails', '>= 3.1'
24
+ end
@@ -0,0 +1,352 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ // Store a local reference to jQuery and Underscorea.
5
+ var $ = window.jQuery;
6
+ var _ = window._;
7
+
8
+ var keyDirMap = {'38': 'prev', '40': 'next'};
9
+ var ctrlKeyDirMap = {'80': 'prev', '78': 'next'};
10
+
11
+ // Delay a callback `n` ms or invoke immediately if 0.
12
+ var delay = function (cb, n) { return !n ? cb() : _.delay(cb, n); };
13
+
14
+ // Direct event bindings to `$el`.
15
+ var directEvents = {
16
+ mouseover: function () { this._hover = true; },
17
+ mouseout: function () {
18
+ this._hover = false;
19
+ _.defer(_.bind(function () {
20
+ if (!this._focus && !this._hover) this.hide();
21
+ }, this));
22
+ }
23
+ };
24
+
25
+ // Delegated event bindings to children of `$el`.
26
+ var delegatedEvents = {
27
+
28
+ // Delegated events for the input area.
29
+ '.js-rec-input': {
30
+
31
+ // Update the `_focus` flag and show the latest query results.
32
+ focus: function (ev) {
33
+ this._focus = true;
34
+ this.query($(ev.currentTarget).val()).show();
35
+ },
36
+
37
+ // Check for the up/down keys and enter key, and finally submit a new
38
+ // query.
39
+ keydown: function (ev) {
40
+ var key = ev.which;
41
+ var dir = keyDirMap[key] || (ev.ctrlKey && ctrlKeyDirMap[key]);
42
+ if (dir) {
43
+ this[dir]();
44
+ return ev.preventDefault();
45
+ }
46
+ if (key === 13) {
47
+ var selected = this.$el.find('.js-rec-selected')[0];
48
+ if (selected && selected !== ev.currentTarget) {
49
+ selected.click();
50
+ return ev.preventDefault();
51
+ }
52
+ }
53
+ var $input = $(ev.currentTarget);
54
+ if (key === 27) {
55
+ $input.blur();
56
+ this.hide();
57
+ return ev.preventDefault();
58
+ }
59
+ _.defer(_.bind(function () { this.query($input.val()); }, this));
60
+ },
61
+
62
+ // Hide the results when focus is lost.
63
+ blur: function () {
64
+ this._focus = false;
65
+ if (!this._hover) this.hide();
66
+ }
67
+ },
68
+
69
+ // Delegated events for invidiual results.
70
+ '.js-rec-selectable': {
71
+
72
+ // Select the result that is being hovered over.
73
+ mouseover: function (ev) { this.select($(ev.currentTarget)); },
74
+
75
+ // Optionally clear and hide the input and results on result click.
76
+ click: function (ev) {
77
+ var $input = this.$el.find('.js-rec-input');
78
+ if (ev.currentTarget === $input[0]) return;
79
+ if (this.clearOnClick) $input.val('').focus();
80
+ if (this.hideOnClick) {
81
+ $input.blur();
82
+ this.hide();
83
+ }
84
+ }
85
+ }
86
+ };
87
+
88
+
89
+ // Create the `Rec` constructor.
90
+ //
91
+ // ```js
92
+ // var rec = new Rec('#my-rec-container', {
93
+ // url: '/search',
94
+ // template: jst['search-result']
95
+ // });
96
+ // ```
97
+ var Rec = window.Rec = function (el, options) {
98
+
99
+ // Define direct and delegated event hashes unique to this instance. This
100
+ // will allow a clean unbinding of these events without worry about any
101
+ // event collisions.
102
+ this._directEvents = {};
103
+ _.each(directEvents, function (cb, key) {
104
+ this._directEvents[key] = _.bind(cb, this);
105
+ }, this);
106
+ this._delegatedEvents = {};
107
+ _.each(delegatedEvents, function (evs, selector) {
108
+ var bound = this._delegatedEvents[selector] = {};
109
+ _.each(evs, function (cb, name) {
110
+ bound[name] = _.bind(cb, this);
111
+ }, this);
112
+ }, this);
113
+
114
+ // Create a cache object to store results.
115
+ this._cached = {};
116
+
117
+ // Track concurrent fetches for showing/hiding the loading spinner.
118
+ this._fetchQueue = 0;
119
+
120
+ // Set the container element.
121
+ this.setElement(el);
122
+
123
+ // Extend the instance with its options.
124
+ _.extend(this, options);
125
+ };
126
+
127
+ // Define `prototype` properties and methods for `Rec`.
128
+ _.extend(Rec.prototype, {
129
+
130
+ // How long since the last keystroke should rec wait to call `fetch`?
131
+ delay: 250,
132
+
133
+ // How many results should be displayed? `0` means as many as are returned
134
+ // by `fetch`.
135
+ limit: 0,
136
+
137
+ // Should results be cached? Any cross-session caching will need to be
138
+ // handled by your `fetch` method, but in-memory caching is free with rec.
139
+ cache: true,
140
+
141
+ // Should the second selectable item be automatically selected? In other
142
+ // words, will hitting return fire the first recommended result or the broad
143
+ // result?
144
+ selectSecond: false,
145
+
146
+ // Should the results be hidden and focus blurred when a result is clicked?
147
+ hideOnClick: false,
148
+
149
+ // Should the input be cleared when a result is clicked?
150
+ clearOnClick: true,
151
+
152
+ // The query string key that will be used with the default `fetch` method.
153
+ queryKey: 'q',
154
+
155
+ // Should rec attempt to fetch query results for an empty string?
156
+ fetchNothing: false,
157
+
158
+ // Override this template functions to return your labels and results the
159
+ // way you want them. If you are not using the `groupBy` option,
160
+ // `labelTemplate` will be ignored.
161
+ labelTemplate: function (label) { return '<div>' + label + '</div>'; },
162
+ resultTemplate: function (result) { return '<div>' + result + '</div>'; },
163
+
164
+ // Parse the query before sending it to `fetch`. This allows you to clean up
165
+ // bad characters, extra spaces, etc...
166
+ parse: function (q) {
167
+ return q.replace(/\s+/g, ' ').replace(/^\s+|\s+$/g, '').toLowerCase();
168
+ },
169
+
170
+ // `filter` works alongside `cache` to intelligently predict possible
171
+ // results while waiting for a server request to come back. Override this
172
+ // function with logic that best matches your server's filter logic.
173
+ filter: function (q, result) {
174
+ var str = _.values(result).join(' ').toLowerCase();
175
+ return _.every(q.split(' '), function (w) { return ~str.indexOf(w); });
176
+ },
177
+
178
+ // The default `fetch` is designed for simple, JSON AJAX requests. Override
179
+ // thid function to suite your application's needs.
180
+ fetch: function (q, cb) {
181
+ var options = this.fetchOptions || {};
182
+ (options.data || (options.data = {}))[this.queryKey] = q;
183
+ return $.ajax(_.extend({
184
+ type: 'get',
185
+ dataType: 'json',
186
+ success: function (results) { cb(null, results); },
187
+ error: function (xhr) { cb(xhr.responseText); }
188
+ }, options));
189
+ },
190
+
191
+ query: function (q) {
192
+
193
+ // Parse the raw query string.
194
+ q = this.parse(q);
195
+
196
+ // Compare to the last query and store so we don't waste any time.
197
+ if (q === this._lastQ) return this;
198
+ this._lastQ = q;
199
+
200
+ // Clear the old `fetch` request if one was about to fire.
201
+ if (this._timeout) {
202
+ clearTimeout(this._timeout);
203
+ this._timeout = null;
204
+ if (!--this._fetchQueue) this.$el.removeClass('js-rec-loading');
205
+ }
206
+
207
+ // Looked for a cache result before trying to load.
208
+ if (!this._getCached(q) && (q || this.fetchEmptyQuery)) {
209
+
210
+ // Add the loading class.
211
+ ++this._fetchQueue;
212
+ this.$el.addClass('js-rec-loading');
213
+
214
+ // Start a timeout to `fetch`
215
+ var self = this;
216
+ this._timeout = delay(function () {
217
+ self._timeout = null;
218
+ self.fetch(q, function (er, results, temp) {
219
+ if (!temp && !--self._fetchQueue) {
220
+ self.$el.removeClass('js-rec-loading');
221
+ }
222
+ self._setCached(q, results);
223
+ self._render();
224
+ });
225
+ }, this.delay);
226
+ }
227
+ return this._render();
228
+ },
229
+
230
+ // Set the input element the rec will watch.
231
+ setElement: function (el) {
232
+ if (this.$el) this._unbind();
233
+ this.$el = el instanceof $ ? el : $(el);
234
+ return this._bind();
235
+ },
236
+
237
+ show: function () {
238
+ this.$el.addClass('js-rec-active');
239
+ this.select();
240
+ return this;
241
+ },
242
+
243
+ hide: function () {
244
+ this.$el.removeClass('js-rec-active');
245
+ return this;
246
+ },
247
+
248
+ select: function ($el) {
249
+ this.$el.find('.js-rec-selected').removeClass('js-rec-selected');
250
+ if (!$el) {
251
+ var $selectable = this.$el.find('.js-rec-selectable:visible');
252
+ var i = Math.min(this.selectSecond ? 1 : 0, $selectable.length - 1);
253
+ $el = $selectable.eq(i);
254
+ }
255
+ $el.addClass('js-rec-selected');
256
+ return this;
257
+ },
258
+
259
+ _dir: function (step) {
260
+ var $selectable = this.$el.find('.js-rec-selectable:visible');
261
+ var $selected = $selectable.filter('.js-rec-selected');
262
+ var i = _.indexOf($selectable, $selected[0]);
263
+ if (i === -1) return this.select($selectable.first());
264
+ i += step;
265
+ if (i === -1 || i === $selectable.length) return this;
266
+ return this.select($selectable.eq(i));
267
+ },
268
+
269
+ prev: function () { this._dir(-1); },
270
+
271
+ next: function () { this._dir(1); },
272
+
273
+ _getCached: function (q) {
274
+ return this.cache && this._cached[q];
275
+ },
276
+
277
+ _setCached: function (q, results) {
278
+ this._cached[q] = results;
279
+ return this;
280
+ },
281
+
282
+ _getFiltered: function (q) {
283
+ if (!this.filter) return null;
284
+ var cached;
285
+ for (var i = q.length - 1; i > 0; --i) {
286
+ if (cached = this._getCached(q.slice(0, i))) break;
287
+ }
288
+ if (!cached) return null;
289
+ var filter = _.bind(this.filter, this, q);
290
+ var matches = _.filter(cached, filter);
291
+ return _.size(matches) ? matches : null;
292
+ },
293
+
294
+
295
+ // Bind (or unbind) delegated events from a multi-level object.
296
+ _bind: function (unbind) {
297
+ var $el = this.$el;
298
+ var method = unbind ? 'off': 'on';
299
+ $el[method](this._directEvents);
300
+ _.each(this._delegatedEvents, function (evs, selector) {
301
+ _.each(evs, function (cb, name) { $el[method](name, selector, cb); });
302
+ });
303
+ return this;
304
+ },
305
+
306
+ // Inverse `_bind`.
307
+ _unbind: function () { return this._bind(true); },
308
+
309
+ // Build the elements for the most recent query.
310
+ _render: function () {
311
+ var q = this._lastQ;
312
+ var results = this._getCached(q) || this._getFiltered(q);
313
+ this.$el[(q === '' ? 'add' : 'remove') + 'Class']('js-rec-nothing')
314
+ .removeClass('js-rec-no-results')
315
+ .find('.js-rec-result, .js-rec-label').remove();
316
+ if (results) {
317
+ var $results = this.$el.find('.js-rec-results');
318
+ if (results.length) {
319
+ results = this.limit ? _.first(results, this.limit) : results;
320
+ var limit = results.length;
321
+ var count = 0;
322
+ results = _.groupBy(results, this.groupBy || 'undefined');
323
+ _.each(results, function (results, label) {
324
+ if (count === limit) return;
325
+ if (label !== 'undefined') {
326
+ $results.append(this._renderLabel(label));
327
+ }
328
+ _.each(results, function (result) {
329
+ if (count === limit) return;
330
+ $results.append(this._renderResult(result));
331
+ ++count;
332
+ }, this);
333
+ }, this);
334
+ } else {
335
+ this.$el.addClass('js-rec-no-results');
336
+ }
337
+ }
338
+ return this.select();
339
+ },
340
+
341
+ _renderLabel: function (label) {
342
+ var el = this.labelTemplate(label);
343
+ return (el instanceof $ ? el : $(el)).addClass('js-rec-label');
344
+ },
345
+
346
+ _renderResult: function (result) {
347
+ var el = this.resultTemplate(result);
348
+ return (el instanceof $ ? el : $(el))
349
+ .addClass('js-rec-result js-rec-selectable');
350
+ }
351
+ });
352
+ })();
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rec-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Casey Foster
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2013-04-01 00:00:00 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ version_requirements: &id001 !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: "3.1"
20
+ prerelease: false
21
+ name: rails
22
+ requirement: *id001
23
+ type: :runtime
24
+ description: Places Rec 0.0.1 in the Rails asset pipeline.
25
+ email:
26
+ - c@sey.me
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - .gitignore
35
+ - Gemfile
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/rec-rails.rb
40
+ - lib/rec/rails/engine.rb
41
+ - lib/rec/rails/version.rb
42
+ - olay-rails.gemspec
43
+ - vendor/assets/javascripts/rec.js
44
+ homepage: https://github.com/orgsync/rec-rails
45
+ licenses:
46
+ - MIT
47
+ metadata: {}
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - &id002
57
+ - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - *id002
63
+ requirements: []
64
+
65
+ rubyforge_project:
66
+ rubygems_version: 2.0.0
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Recommendations as you type.
70
+ test_files: []
71
+