jquery-textcomplete-rails 0.1.2
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 +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);
|