rec-rails 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +19 -0
- data/README.md +23 -0
- data/Rakefile +1 -0
- data/lib/rec-rails.rb +2 -0
- data/lib/rec/rails/engine.rb +6 -0
- data/lib/rec/rails/version.rb +6 -0
- data/olay-rails.gemspec +24 -0
- data/vendor/assets/javascripts/rec.js +352 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/lib/rec-rails.rb
ADDED
data/olay-rails.gemspec
ADDED
@@ -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
|
+
|