jquery-textcomplete-rails 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +72 -0
- data/Rakefile +1 -0
- data/jquery-textcomplete-rails.gemspec +29 -0
- data/lib/jquery/textcomplete/rails.rb +10 -0
- data/lib/jquery/textcomplete/rails/version.rb +7 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/Gemfile +51 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js.coffee +17 -0
- data/spec/dummy/app/assets/stylesheets/application.css.scss +14 -0
- data/spec/dummy/app/controllers/application_controller.rb +8 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/.keep +0 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/views/application/index.html.erb +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +28 -0
- data/spec/dummy/config/boot.rb +4 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +80 -0
- data/spec/dummy/config/environments/test.rb +36 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +12 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/db/seeds.rb +7 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +58 -0
- data/spec/dummy/public/422.html +58 -0
- data/spec/dummy/public/500.html +57 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/spec/controllers/application_controller_spec.rb +13 -0
- data/spec/dummy/spec/spec_helper.rb +41 -0
- data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
- data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
- data/vendor/assets/javascripts/jquery-textcomplete-rails.js.coffee +1 -0
- data/vendor/assets/javascripts/jquery-textcomplete-rails/base.js +553 -0
- data/vendor/assets/stylesheets/jquery-textcomplete-rails.css.scss +1 -0
- data/vendor/assets/stylesheets/jquery-textcomplete-rails/base.css.scss +19 -0
- metadata +238 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>We're sorry, but something went wrong (500)</title>
|
5
|
+
<style>
|
6
|
+
body {
|
7
|
+
background-color: #EFEFEF;
|
8
|
+
color: #2E2F30;
|
9
|
+
text-align: center;
|
10
|
+
font-family: arial, sans-serif;
|
11
|
+
}
|
12
|
+
|
13
|
+
div.dialog {
|
14
|
+
width: 25em;
|
15
|
+
margin: 4em auto 0 auto;
|
16
|
+
border: 1px solid #CCC;
|
17
|
+
border-right-color: #999;
|
18
|
+
border-left-color: #999;
|
19
|
+
border-bottom-color: #BBB;
|
20
|
+
border-top: #B00100 solid 4px;
|
21
|
+
border-top-left-radius: 9px;
|
22
|
+
border-top-right-radius: 9px;
|
23
|
+
background-color: white;
|
24
|
+
padding: 7px 4em 0 4em;
|
25
|
+
}
|
26
|
+
|
27
|
+
h1 {
|
28
|
+
font-size: 100%;
|
29
|
+
color: #730E15;
|
30
|
+
line-height: 1.5em;
|
31
|
+
}
|
32
|
+
|
33
|
+
body > p {
|
34
|
+
width: 33em;
|
35
|
+
margin: 0 auto 1em;
|
36
|
+
padding: 1em 0;
|
37
|
+
background-color: #F7F7F7;
|
38
|
+
border: 1px solid #CCC;
|
39
|
+
border-right-color: #999;
|
40
|
+
border-bottom-color: #999;
|
41
|
+
border-bottom-left-radius: 4px;
|
42
|
+
border-bottom-right-radius: 4px;
|
43
|
+
border-top-color: #DADADA;
|
44
|
+
color: #666;
|
45
|
+
box-shadow:0 3px 8px rgba(50, 50, 50, 0.17);
|
46
|
+
}
|
47
|
+
</style>
|
48
|
+
</head>
|
49
|
+
|
50
|
+
<body>
|
51
|
+
<!-- This file lives in public/500.html -->
|
52
|
+
<div class="dialog">
|
53
|
+
<h1>We're sorry, but something went wrong.</h1>
|
54
|
+
</div>
|
55
|
+
<p>If you are the application owner check the logs for more information.</p>
|
56
|
+
</body>
|
57
|
+
</html>
|
File without changes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# This file is copied to spec/ when you run 'rails generate rspec:install'
|
2
|
+
ENV["RAILS_ENV"] ||= 'test'
|
3
|
+
require File.expand_path("../../config/environment", __FILE__)
|
4
|
+
require 'rspec/rails'
|
5
|
+
|
6
|
+
# Requires supporting ruby files with custom matchers and macros, etc, in
|
7
|
+
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
|
8
|
+
# run as spec files by default. This means that files in spec/support that end
|
9
|
+
# in _spec.rb will both be required and run as specs, causing the specs to be
|
10
|
+
# run twice. It is recommended that you do not name files matching this glob to
|
11
|
+
# end with _spec.rb. You can configure this pattern with with the --pattern
|
12
|
+
# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
|
13
|
+
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
|
14
|
+
|
15
|
+
# Checks for pending migrations before tests are run.
|
16
|
+
# If you are not using ActiveRecord, you can remove this line.
|
17
|
+
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
# ## Mock Framework
|
21
|
+
#
|
22
|
+
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
|
23
|
+
#
|
24
|
+
# config.mock_with :mocha
|
25
|
+
# config.mock_with :flexmock
|
26
|
+
# config.mock_with :rr
|
27
|
+
|
28
|
+
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
29
|
+
config.fixture_path = "#{::Rails.root}/spec/fixtures"
|
30
|
+
|
31
|
+
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
32
|
+
# examples within a transaction, remove the following line or assign false
|
33
|
+
# instead of true.
|
34
|
+
config.use_transactional_fixtures = true
|
35
|
+
|
36
|
+
# Run specs in random order to surface order dependencies. If you find an
|
37
|
+
# order dependency and want to debug it, you can fix the order by providing
|
38
|
+
# the seed, which is printed after each run.
|
39
|
+
# --seed 1234
|
40
|
+
config.order = "random"
|
41
|
+
end
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
#= require_tree ./jquery-textcomplete-rails
|
@@ -0,0 +1,553 @@
|
|
1
|
+
/*!
|
2
|
+
* jQuery.textcomplete.js
|
3
|
+
*
|
4
|
+
* Repositiory: https://github.com/yuku-t/jquery-textcomplete
|
5
|
+
* License: MIT
|
6
|
+
* Author: Yuku Takahashi
|
7
|
+
*/
|
8
|
+
|
9
|
+
;(function ($) {
|
10
|
+
|
11
|
+
'use strict';
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Exclusive execution control utility.
|
15
|
+
*/
|
16
|
+
var lock = function (func) {
|
17
|
+
var free, locked;
|
18
|
+
free = function () { locked = false; };
|
19
|
+
return function () {
|
20
|
+
var args;
|
21
|
+
if (locked) return;
|
22
|
+
locked = true;
|
23
|
+
args = toArray(arguments);
|
24
|
+
args.unshift(free);
|
25
|
+
func.apply(this, args);
|
26
|
+
};
|
27
|
+
};
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Convert arguments into a real array.
|
31
|
+
*/
|
32
|
+
var toArray = function (args) {
|
33
|
+
var result;
|
34
|
+
result = Array.prototype.slice.call(args);
|
35
|
+
return result;
|
36
|
+
};
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Get the styles of any element from property names.
|
40
|
+
*/
|
41
|
+
var getStyles = (function () {
|
42
|
+
var color;
|
43
|
+
color = $('<div></div>').css(['color']).color;
|
44
|
+
if (typeof color !== 'undefined') {
|
45
|
+
return function ($el, properties) {
|
46
|
+
return $el.css(properties);
|
47
|
+
};
|
48
|
+
} else { // for jQuery 1.8 or below
|
49
|
+
return function ($el, properties) {
|
50
|
+
var styles;
|
51
|
+
styles = {};
|
52
|
+
$.each(properties, function (i, property) {
|
53
|
+
styles[property] = $el.css(property);
|
54
|
+
});
|
55
|
+
return styles;
|
56
|
+
};
|
57
|
+
}
|
58
|
+
})();
|
59
|
+
|
60
|
+
/**
|
61
|
+
* Default template function.
|
62
|
+
*/
|
63
|
+
var identity = function (obj) { return obj; };
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Memoize a search function.
|
67
|
+
*/
|
68
|
+
var memoize = function (func) {
|
69
|
+
var memo = {};
|
70
|
+
return function (term, callback) {
|
71
|
+
if (memo[term]) {
|
72
|
+
callback(memo[term]);
|
73
|
+
} else {
|
74
|
+
func.call(this, term, function (data) {
|
75
|
+
memo[term] = (memo[term] || []).concat(data);
|
76
|
+
callback.apply(null, arguments);
|
77
|
+
});
|
78
|
+
}
|
79
|
+
};
|
80
|
+
};
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Determine if the array contains a given value.
|
84
|
+
*/
|
85
|
+
var include = function (array, value) {
|
86
|
+
var i, l;
|
87
|
+
if (array.indexOf) return array.indexOf(value) != -1;
|
88
|
+
for (i = 0, l = array.length; i < l; i++) {
|
89
|
+
if (array[i] === value) return true;
|
90
|
+
}
|
91
|
+
return false;
|
92
|
+
};
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Textarea manager class.
|
96
|
+
*/
|
97
|
+
var Completer = (function () {
|
98
|
+
var html, css, $baseWrapper, $baseList, _id;
|
99
|
+
|
100
|
+
html = {
|
101
|
+
wrapper: '<div class="textcomplete-wrapper"></div>',
|
102
|
+
list: '<ul class="dropdown-menu"></ul>'
|
103
|
+
};
|
104
|
+
css = {
|
105
|
+
wrapper: {
|
106
|
+
position: 'relative'
|
107
|
+
},
|
108
|
+
list: {
|
109
|
+
position: 'absolute',
|
110
|
+
top: 0,
|
111
|
+
left: 0,
|
112
|
+
zIndex: '100',
|
113
|
+
display: 'none'
|
114
|
+
}
|
115
|
+
};
|
116
|
+
$baseWrapper = $(html.wrapper).css(css.wrapper);
|
117
|
+
$baseList = $(html.list).css(css.list);
|
118
|
+
_id = 0;
|
119
|
+
|
120
|
+
function Completer($el) {
|
121
|
+
var focus;
|
122
|
+
this.el = $el.get(0); // textarea element
|
123
|
+
focus = this.el === document.activeElement;
|
124
|
+
// Cannot wrap $el at initialize method lazily due to Firefox's behavior.
|
125
|
+
this.$el = wrapElement($el); // Focus is lost
|
126
|
+
this.id = 'textComplete' + _id++;
|
127
|
+
this.strategies = [];
|
128
|
+
if (focus) {
|
129
|
+
this.initialize();
|
130
|
+
this.$el.focus();
|
131
|
+
} else {
|
132
|
+
this.$el.one('focus.textComplete', $.proxy(this.initialize, this));
|
133
|
+
}
|
134
|
+
}
|
135
|
+
|
136
|
+
/**
|
137
|
+
* Completer's public methods
|
138
|
+
*/
|
139
|
+
$.extend(Completer.prototype, {
|
140
|
+
|
141
|
+
/**
|
142
|
+
* Prepare ListView and bind events.
|
143
|
+
*/
|
144
|
+
initialize: function () {
|
145
|
+
var $list, globalEvents;
|
146
|
+
$list = $baseList.clone();
|
147
|
+
this.listView = new ListView($list, this);
|
148
|
+
this.$el
|
149
|
+
.before($list)
|
150
|
+
.on({
|
151
|
+
'keyup.textComplete': $.proxy(this.onKeyup, this),
|
152
|
+
'keydown.textComplete': $.proxy(this.listView.onKeydown,
|
153
|
+
this.listView)
|
154
|
+
});
|
155
|
+
globalEvents = {};
|
156
|
+
globalEvents['click.' + this.id] = $.proxy(this.onClickDocument, this);
|
157
|
+
globalEvents['keyup.' + this.id] = $.proxy(this.onKeyupDocument, this);
|
158
|
+
$(document).on(globalEvents);
|
159
|
+
},
|
160
|
+
|
161
|
+
/**
|
162
|
+
* Register strategies to the completer.
|
163
|
+
*/
|
164
|
+
register: function (strategies) {
|
165
|
+
this.strategies = this.strategies.concat(strategies);
|
166
|
+
},
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Show autocomplete list next to the caret.
|
170
|
+
*/
|
171
|
+
renderList: function (data) {
|
172
|
+
if (this.clearAtNext) {
|
173
|
+
this.listView.clear();
|
174
|
+
this.clearAtNext = false;
|
175
|
+
}
|
176
|
+
if (data.length) {
|
177
|
+
if (!this.listView.shown) {
|
178
|
+
this.listView
|
179
|
+
.setPosition(this.getCaretPosition())
|
180
|
+
.clear()
|
181
|
+
.activate();
|
182
|
+
this.listView.strategy = this.strategy;
|
183
|
+
}
|
184
|
+
data = data.slice(0, this.strategy.maxCount);
|
185
|
+
this.listView.render(data);
|
186
|
+
}
|
187
|
+
|
188
|
+
if (!this.listView.data.length && this.listView.shown) {
|
189
|
+
this.listView.deactivate();
|
190
|
+
}
|
191
|
+
},
|
192
|
+
|
193
|
+
searchCallbackFactory: function (free) {
|
194
|
+
var self = this;
|
195
|
+
return function (data, keep) {
|
196
|
+
self.renderList(data);
|
197
|
+
if (!keep) {
|
198
|
+
// This is the last callback for this search.
|
199
|
+
free();
|
200
|
+
self.clearAtNext = true;
|
201
|
+
}
|
202
|
+
};
|
203
|
+
},
|
204
|
+
|
205
|
+
/**
|
206
|
+
* Keyup event handler.
|
207
|
+
*/
|
208
|
+
onKeyup: function (e) {
|
209
|
+
var searchQuery, term;
|
210
|
+
if (this.skipSearch(e)) { return; }
|
211
|
+
|
212
|
+
searchQuery = this.extractSearchQuery(this.getTextFromHeadToCaret());
|
213
|
+
if (searchQuery.length) {
|
214
|
+
term = searchQuery[1];
|
215
|
+
if (this.term === term) return; // Ignore shift-key or something.
|
216
|
+
this.term = term;
|
217
|
+
this.search(searchQuery);
|
218
|
+
} else {
|
219
|
+
this.term = null;
|
220
|
+
this.listView.deactivate();
|
221
|
+
}
|
222
|
+
},
|
223
|
+
|
224
|
+
/**
|
225
|
+
* Suppress searching if it returns true.
|
226
|
+
*/
|
227
|
+
skipSearch: function (e) {
|
228
|
+
if (this.skipNextKeyup) {
|
229
|
+
this.skipNextKeyup = false;
|
230
|
+
return true;
|
231
|
+
}
|
232
|
+
switch (e.keyCode) {
|
233
|
+
case 40:
|
234
|
+
case 38:
|
235
|
+
return true;
|
236
|
+
}
|
237
|
+
},
|
238
|
+
|
239
|
+
onSelect: function (value) {
|
240
|
+
var pre, post, newSubStr;
|
241
|
+
pre = this.getTextFromHeadToCaret();
|
242
|
+
post = this.el.value.substring(this.el.selectionEnd);
|
243
|
+
|
244
|
+
newSubStr = this.strategy.replace(value);
|
245
|
+
if ($.isArray(newSubStr)) {
|
246
|
+
post = newSubStr[1] + post;
|
247
|
+
newSubStr = newSubStr[0];
|
248
|
+
}
|
249
|
+
pre = pre.replace(this.strategy.match, newSubStr);
|
250
|
+
this.$el.val(pre + post)
|
251
|
+
.trigger('change')
|
252
|
+
.trigger('textComplete:select', value);
|
253
|
+
this.el.focus();
|
254
|
+
this.el.selectionStart = this.el.selectionEnd = pre.length;
|
255
|
+
this.skipNextKeyup = true;
|
256
|
+
},
|
257
|
+
|
258
|
+
/**
|
259
|
+
* Global click event handler.
|
260
|
+
*/
|
261
|
+
onClickDocument: function (e) {
|
262
|
+
if (e.originalEvent && !e.originalEvent.keepTextCompleteDropdown) {
|
263
|
+
this.listView.deactivate();
|
264
|
+
}
|
265
|
+
},
|
266
|
+
|
267
|
+
/**
|
268
|
+
* Global keyup event handler.
|
269
|
+
*/
|
270
|
+
onKeyupDocument: function (e) {
|
271
|
+
if (this.listView.shown && e.keyCode === 27) { // ESC
|
272
|
+
this.listView.deactivate();
|
273
|
+
this.$el.focus();
|
274
|
+
}
|
275
|
+
},
|
276
|
+
|
277
|
+
/**
|
278
|
+
* Remove all event handlers and the wrapper element.
|
279
|
+
*/
|
280
|
+
destroy: function () {
|
281
|
+
var $wrapper;
|
282
|
+
this.$el.off('.textComplete');
|
283
|
+
$(document).off('.' + this.id);
|
284
|
+
if (this.listView) { this.listView.destroy(); }
|
285
|
+
$wrapper = this.$el.parent();
|
286
|
+
$wrapper.after(this.$el).remove();
|
287
|
+
this.$el.data('textComplete', void 0);
|
288
|
+
this.$el = null;
|
289
|
+
},
|
290
|
+
|
291
|
+
// Helper methods
|
292
|
+
// ==============
|
293
|
+
|
294
|
+
/**
|
295
|
+
* Returns caret's relative coordinates from textarea's left top corner.
|
296
|
+
*/
|
297
|
+
getCaretPosition: function () {
|
298
|
+
// Browser native API does not provide the way to know the position of
|
299
|
+
// caret in pixels, so that here we use a kind of hack to accomplish
|
300
|
+
// the aim. First of all it puts a div element and completely copies
|
301
|
+
// the textarea's style to the element, then it inserts the text and a
|
302
|
+
// span element into the textarea.
|
303
|
+
// Consequently, the span element's position is the thing what we want.
|
304
|
+
|
305
|
+
if (this.el.selectionEnd === 0) return;
|
306
|
+
var properties, css, $div, $span, position, dir;
|
307
|
+
|
308
|
+
dir = this.$el.attr('dir') || this.$el.css('direction');
|
309
|
+
properties = ['border-width', 'font-family', 'font-size', 'font-style',
|
310
|
+
'font-variant', 'font-weight', 'height', 'letter-spacing',
|
311
|
+
'word-spacing', 'line-height', 'text-decoration', 'text-align',
|
312
|
+
'width', 'padding-top', 'padding-right', 'padding-bottom',
|
313
|
+
'padding-left', 'margin-top', 'margin-right', 'margin-bottom',
|
314
|
+
'margin-left'
|
315
|
+
];
|
316
|
+
css = $.extend({
|
317
|
+
position: 'absolute',
|
318
|
+
overflow: 'auto',
|
319
|
+
'white-space': 'pre-wrap',
|
320
|
+
top: 0,
|
321
|
+
left: -9999,
|
322
|
+
direction: dir
|
323
|
+
}, getStyles(this.$el, properties));
|
324
|
+
|
325
|
+
$div = $('<div></div>').css(css).text(this.getTextFromHeadToCaret());
|
326
|
+
$span = $('<span></span>').text('.').appendTo($div);
|
327
|
+
this.$el.before($div);
|
328
|
+
position = $span.position();
|
329
|
+
position.top += $span.height() - this.$el.scrollTop();
|
330
|
+
if (dir === 'rtl') { position.left -= this.listView.$el.width(); }
|
331
|
+
$div.remove();
|
332
|
+
return position;
|
333
|
+
},
|
334
|
+
|
335
|
+
getTextFromHeadToCaret: function () {
|
336
|
+
var text, selectionEnd, range;
|
337
|
+
selectionEnd = this.el.selectionEnd;
|
338
|
+
if (typeof selectionEnd === 'number') {
|
339
|
+
text = this.el.value.substring(0, selectionEnd);
|
340
|
+
} else if (document.selection) {
|
341
|
+
range = this.el.createTextRange();
|
342
|
+
range.moveStart('character', 0);
|
343
|
+
range.moveEnd('textedit');
|
344
|
+
text = range.text;
|
345
|
+
}
|
346
|
+
return text;
|
347
|
+
},
|
348
|
+
|
349
|
+
/**
|
350
|
+
* Parse the value of textarea and extract search query.
|
351
|
+
*/
|
352
|
+
extractSearchQuery: function (text) {
|
353
|
+
// If a search query found, it returns used strategy and the query
|
354
|
+
// term. If the caret is currently in a code block or search query does
|
355
|
+
// not found, it returns an empty array.
|
356
|
+
|
357
|
+
var i, l, strategy, match;
|
358
|
+
for (i = 0, l = this.strategies.length; i < l; i++) {
|
359
|
+
strategy = this.strategies[i];
|
360
|
+
match = text.match(strategy.match);
|
361
|
+
if (match) { return [strategy, match[strategy.index]]; }
|
362
|
+
}
|
363
|
+
return [];
|
364
|
+
},
|
365
|
+
|
366
|
+
search: lock(function (free, searchQuery) {
|
367
|
+
var term;
|
368
|
+
this.strategy = searchQuery[0];
|
369
|
+
term = searchQuery[1];
|
370
|
+
this.strategy.search(term, this.searchCallbackFactory(free));
|
371
|
+
})
|
372
|
+
});
|
373
|
+
|
374
|
+
/**
|
375
|
+
* Completer's private functions
|
376
|
+
*/
|
377
|
+
var wrapElement = function ($el) {
|
378
|
+
return $el.wrap($baseWrapper.clone().css('display', $el.css('display')));
|
379
|
+
};
|
380
|
+
|
381
|
+
return Completer;
|
382
|
+
})();
|
383
|
+
|
384
|
+
/**
|
385
|
+
* Dropdown menu manager class.
|
386
|
+
*/
|
387
|
+
var ListView = (function () {
|
388
|
+
|
389
|
+
function ListView($el, completer) {
|
390
|
+
this.data = [];
|
391
|
+
this.$el = $el;
|
392
|
+
this.index = 0;
|
393
|
+
this.completer = completer;
|
394
|
+
|
395
|
+
this.$el.on('click.textComplete', 'li.textcomplete-item',
|
396
|
+
$.proxy(this.onClick, this));
|
397
|
+
}
|
398
|
+
|
399
|
+
$.extend(ListView.prototype, {
|
400
|
+
shown: false,
|
401
|
+
|
402
|
+
render: function (data) {
|
403
|
+
var html, i, l, index, val;
|
404
|
+
|
405
|
+
html = '';
|
406
|
+
for (i = 0, l = data.length; i < l; i++) {
|
407
|
+
val = data[i];
|
408
|
+
if (include(this.data, val)) continue;
|
409
|
+
index = this.data.length;
|
410
|
+
this.data.push(val);
|
411
|
+
html += '<li class="textcomplete-item" data-index="' + index + '"><a>';
|
412
|
+
html += this.strategy.template(val);
|
413
|
+
html += '</a></li>';
|
414
|
+
if (this.data.length === this.strategy.maxCount) break;
|
415
|
+
}
|
416
|
+
this.$el.append(html);
|
417
|
+
if (!this.data.length) {
|
418
|
+
this.deactivate();
|
419
|
+
} else {
|
420
|
+
this.activateIndexedItem();
|
421
|
+
}
|
422
|
+
},
|
423
|
+
|
424
|
+
clear: function () {
|
425
|
+
this.data = [];
|
426
|
+
this.$el.html('');
|
427
|
+
this.index = 0;
|
428
|
+
return this;
|
429
|
+
},
|
430
|
+
|
431
|
+
activateIndexedItem: function () {
|
432
|
+
this.$el.find('.active').removeClass('active');
|
433
|
+
this.getActiveItem().addClass('active');
|
434
|
+
},
|
435
|
+
|
436
|
+
getActiveItem: function () {
|
437
|
+
return $(this.$el.children().get(this.index));
|
438
|
+
},
|
439
|
+
|
440
|
+
activate: function () {
|
441
|
+
if (!this.shown) {
|
442
|
+
this.$el.show();
|
443
|
+
this.completer.$el.trigger('textComplete:show');
|
444
|
+
this.shown = true;
|
445
|
+
}
|
446
|
+
return this;
|
447
|
+
},
|
448
|
+
|
449
|
+
deactivate: function () {
|
450
|
+
if (this.shown) {
|
451
|
+
this.$el.hide();
|
452
|
+
this.completer.$el.trigger('textComplete:hide');
|
453
|
+
this.shown = false;
|
454
|
+
this.data = this.index = null;
|
455
|
+
}
|
456
|
+
return this;
|
457
|
+
},
|
458
|
+
|
459
|
+
setPosition: function (position) {
|
460
|
+
this.$el.css(position);
|
461
|
+
return this;
|
462
|
+
},
|
463
|
+
|
464
|
+
select: function (index) {
|
465
|
+
var self = this;
|
466
|
+
this.completer.onSelect(this.data[index]);
|
467
|
+
// Deactive at next tick to allow other event handlers to know whether
|
468
|
+
// the dropdown has been shown or not.
|
469
|
+
setTimeout(function () { self.deactivate(); }, 0);
|
470
|
+
},
|
471
|
+
|
472
|
+
onKeydown: function (e) {
|
473
|
+
if (!this.shown) return;
|
474
|
+
if (e.keyCode === 38) { // UP
|
475
|
+
e.preventDefault();
|
476
|
+
if (this.index === 0) {
|
477
|
+
this.index = this.data.length-1;
|
478
|
+
} else {
|
479
|
+
this.index -= 1;
|
480
|
+
}
|
481
|
+
this.activateIndexedItem();
|
482
|
+
} else if (e.keyCode === 40) { // DOWN
|
483
|
+
e.preventDefault();
|
484
|
+
if (this.index === this.data.length - 1) {
|
485
|
+
this.index = 0;
|
486
|
+
} else {
|
487
|
+
this.index += 1;
|
488
|
+
}
|
489
|
+
this.activateIndexedItem();
|
490
|
+
} else if (e.keyCode === 13 || e.keyCode === 9) { // ENTER or TAB
|
491
|
+
e.preventDefault();
|
492
|
+
this.select(parseInt(this.getActiveItem().data('index'), 10));
|
493
|
+
}
|
494
|
+
},
|
495
|
+
|
496
|
+
onClick: function (e) {
|
497
|
+
var $e = $(e.target);
|
498
|
+
e.originalEvent.keepTextCompleteDropdown = true;
|
499
|
+
if (!$e.hasClass('textcomplete-item')) {
|
500
|
+
$e = $e.parents('li.textcomplete-item');
|
501
|
+
}
|
502
|
+
this.select(parseInt($e.data('index'), 10));
|
503
|
+
},
|
504
|
+
|
505
|
+
destroy: function () {
|
506
|
+
this.deactivate();
|
507
|
+
this.$el.off('click.textComplete').remove();
|
508
|
+
this.$el = null;
|
509
|
+
}
|
510
|
+
});
|
511
|
+
|
512
|
+
return ListView;
|
513
|
+
})();
|
514
|
+
|
515
|
+
$.fn.textcomplete = function (strategies) {
|
516
|
+
var i, l, strategy, dataKey;
|
517
|
+
|
518
|
+
dataKey = 'textComplete';
|
519
|
+
|
520
|
+
if (strategies === 'destroy') {
|
521
|
+
return this.each(function () {
|
522
|
+
var completer = $(this).data(dataKey);
|
523
|
+
if (completer) { completer.destroy(); }
|
524
|
+
});
|
525
|
+
}
|
526
|
+
|
527
|
+
for (i = 0, l = strategies.length; i < l; i++) {
|
528
|
+
strategy = strategies[i];
|
529
|
+
if (!strategy.template) {
|
530
|
+
strategy.template = identity;
|
531
|
+
}
|
532
|
+
if (strategy.index == null) {
|
533
|
+
strategy.index = 2;
|
534
|
+
}
|
535
|
+
if (strategy.cache) {
|
536
|
+
strategy.search = memoize(strategy.search);
|
537
|
+
}
|
538
|
+
strategy.maxCount || (strategy.maxCount = 10);
|
539
|
+
}
|
540
|
+
|
541
|
+
return this.each(function () {
|
542
|
+
var $this, completer;
|
543
|
+
$this = $(this);
|
544
|
+
completer = $this.data(dataKey);
|
545
|
+
if (!completer) {
|
546
|
+
completer = new Completer($this);
|
547
|
+
$this.data(dataKey, completer);
|
548
|
+
}
|
549
|
+
completer.register(strategies);
|
550
|
+
});
|
551
|
+
};
|
552
|
+
|
553
|
+
})(window.jQuery || window.Zepto);
|