cortex-reaver 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,6 +36,17 @@ module CortexReaver
36
36
  end
37
37
  end
38
38
 
39
+ # Returns a few autocomplete candidates for a given tag by title, separated
40
+ # by newlines.
41
+ def autocomplete
42
+ q = request[:q].gsub(/[^A-Za-z0-9 -_]/, '')
43
+ if q.empty?
44
+ respond ''
45
+ else
46
+ respond Tag.filter(:title.like(/^#{q}/i)).limit(8).map(:title).join("\n")
47
+ end
48
+ end
49
+
39
50
  def show(*ids)
40
51
  # Find tags
41
52
  tags = []
@@ -48,7 +48,7 @@ module Ramaze
48
48
 
49
49
  # Shortcut for current user or an anonymous proxy
50
50
  def user
51
- session[:user] || CortexReaver::User.new(:name => 'Anonymous')
51
+ session[:user] || CortexReaver::User.anonymous
52
52
  end
53
53
 
54
54
  def error_403
@@ -2,6 +2,43 @@ module Ramaze
2
2
  module Helper
3
3
  # Provides navigation rendering shortcuts
4
4
  module Navigation
5
+
6
+ # An HTML author/creation line for a model
7
+ def author_info(model)
8
+ s = '<span class="author-info">'
9
+ if model.respond_to? :creator and creator = model.creator
10
+ c = creator
11
+ end
12
+ if model.respond_to? :updater and updater = model.updater
13
+ u = updater
14
+ end
15
+
16
+ s << '<span class="creator">'
17
+ s << user_link(model, :creator)
18
+ s << '</span>'
19
+ s << ' on <span class="date">'
20
+ s << model.created_on.strftime('%e %B %Y')
21
+ s << '</span>'
22
+
23
+ ct = model.created_on
24
+ ut = model.updated_on
25
+ unless ut.year == ct.year and ut.month == ct.month and ut.day == ct.day
26
+ # Show the update time as well
27
+ if u.nil? or c == u
28
+ s << ' (updated'
29
+ else
30
+ s << ' (updated by '
31
+ s << '<span class="updater">'
32
+ s << user_link(model, :updater)
33
+ s << '</span>'
34
+ end
35
+ s << ' on <span class="date">'
36
+ s << ut.strftime('%e %B %Y')
37
+ s << '</span>)'
38
+ end
39
+ s << '</span>'
40
+ end
41
+
5
42
  # Returns a div with next/up/previous links for the record.
6
43
  def model_nav(model)
7
44
  n = '<div class="navigation actions">'
@@ -6,16 +6,22 @@ module Ramaze
6
6
  name = opts[:name]
7
7
  title = opts[:title] || name.to_s.titleize
8
8
 
9
- s = "<p><label for=\"#{name}\">#{title}</label>"
9
+ s = "<p><label for=\"#{name}\">#{title}</label>\n"
10
+ s << "<ul id=\"#{name}-holder\" class=\"acfb-holder\">\n"
10
11
  s << "<input name=\"#{name}\" id=\"#{name}\" type=\"text\" class=\"acfb-input\" value=\"#{attr_h(tags_on(model, false))}\" />"
11
- s << "</p>"
12
+ s << "</ul></p>"
12
13
 
13
14
  s << <<EOF
14
-
15
15
  <script type="text/javascript">
16
- /* <![CDATA[ */
17
- /* ]]> */
18
- </script>
16
+ /* <![CDATA[ */
17
+ $(document).ready(function() {
18
+ $("##{name}-holder").autoCompletefb({
19
+ urlLookup:'/tags/autocomplete',
20
+ acOptions:{extraParams:{id:'title'}}
21
+ });
22
+ });
23
+ /* ]]> */
24
+ </script>
19
25
  EOF
20
26
  end
21
27
 
@@ -52,7 +52,27 @@ module CortexReaver
52
52
  nil
53
53
  end
54
54
  end
55
-
55
+
56
+ # An anonymous proxy user, with no permissions.
57
+ def self.anonymous
58
+ # Return singleton if stored
59
+ return @anonymous_user if @anonymous_user
60
+
61
+ # Create anonymous user
62
+ @anonymous_user = self.new(:name => "Anonymous")
63
+ def @anonymous_user.can_create? other
64
+ false
65
+ end
66
+ def @anonymous_user.can_edit? other
67
+ false
68
+ end
69
+ def @anonymous_user.can_delete? other
70
+ false
71
+ end
72
+
73
+ @anonymous_user
74
+ end
75
+
56
76
  # CRUD uses this to construct URLs. Even though we don't need the full
57
77
  # power of Canonical, CRUD is pretty useful. :)
58
78
  def self.canonical_name_attr
@@ -49,3 +49,108 @@ textarea {
49
49
  }
50
50
 
51
51
  /* Autocomplete */
52
+
53
+ .ac_results {
54
+ padding: 0px;
55
+ border: 1px solid black;
56
+ background-color: Window;
57
+ overflow: hidden;
58
+ z-index: 99999;
59
+ }
60
+ .ac_results ul {
61
+ width: 100%;
62
+ list-style-position: outside;
63
+ list-style: none;
64
+ padding: 0;
65
+ margin: 0;
66
+ }
67
+ .ac_results li {
68
+ margin: 0px;
69
+ padding: 2px 5px;
70
+ cursor: default;
71
+ display: block;
72
+ font: menu;
73
+ font-size: 12px;
74
+ line-height: 16px;
75
+ overflow: hidden;
76
+ }
77
+ .ac_loading {
78
+ background : Window url('/images/indicator.gif') right center no-repeat;
79
+ }
80
+ .ac_odd {
81
+ background-color: #eee;
82
+ }
83
+ .ac_over {
84
+ background-color: Highlight;
85
+ color: HighlightText;
86
+ }
87
+ .ac_moreItems {
88
+ text-align: center;
89
+ background-color: #fff;
90
+ color: InactiveCaptionText;
91
+ margin: 0px;
92
+ padding: 0px 5px;
93
+ cursor: default;
94
+ display: block;
95
+ width: 100%;
96
+ font: bold menu 12px;
97
+ overflow: hidden;
98
+ -moz-user-select: none;
99
+ -khtml-user-select: none;
100
+ }
101
+
102
+ .p {cursor: pointer;}
103
+
104
+ .acfb-input{
105
+ width: 10em;
106
+ border-radius: 6px;
107
+ -moz-border-radius: 6px;
108
+ -webkit-border-radius: 6px;
109
+ border: 1px solid #CAD8F3;
110
+ padding: 1px 5px 2px;
111
+ background: transparent;
112
+ margin: 0 5px 4px 0;
113
+ font: 11px "Lucida Grande", "Verdana";
114
+ }
115
+
116
+ * html ul.acfb-holder,*:first-child+html ul.acfb-holder {
117
+ padding-bottom: 2px;
118
+ }
119
+
120
+ ul.acfb-holder {
121
+ margin : 0;
122
+ padding : 4px 5px 0;
123
+ border : 1px solid #999;
124
+ height : auto !important;
125
+ height : 1%;
126
+ overflow: hidden;
127
+ font : 11px "Lucida Grande", "Verdana";
128
+ }
129
+
130
+ ul.acfb-holder li {
131
+ float : left;
132
+ margin : 0 5px 4px 0;
133
+ list-style-type: none;
134
+ }
135
+
136
+ ul.acfb-holder li.acfb-data {
137
+ border-radius : 6px;
138
+ -moz-border-radius : 6px;
139
+ -webkit-border-radius : 6px;
140
+ border : 1px solid #CAD8F3;
141
+ padding : 1px 5px 2px;
142
+ background : #DEE7F8;
143
+ }
144
+
145
+ .acfb-add {
146
+ border-radius : 6px;
147
+ -moz-border-radius : 6px;
148
+ -webkit-border-radius : 6px;
149
+ border : 1px solid #CAD8F3;
150
+ padding : 1px 5px 2px;
151
+ background : #DEE7F8;
152
+ margin: 0 5px 4px 0;
153
+ display: inline-block;
154
+ text-align: center;
155
+ cursor: pointer;
156
+ }
@@ -15,7 +15,7 @@
15
15
  right: 0px;
16
16
  }
17
17
 
18
- .text-entry .written {
18
+ .text-entry .author-info {
19
19
  position: absolute;
20
20
  left: 0px;
21
21
  }
@@ -0,0 +1,849 @@
1
+ /*
2
+ * Autocomplete - jQuery plugin 1.0 Beta
3
+ *
4
+ * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
5
+ *
6
+ * Dual licensed under the MIT and GPL licenses:
7
+ * http://www.opensource.org/licenses/mit-license.php
8
+ * http://www.gnu.org/licenses/gpl.html
9
+ *
10
+ * Revision: $Id: jquery.autocomplete.js 4485 2008-01-20 13:52:47Z joern.zaefferer $
11
+ *
12
+ */
13
+
14
+ /**
15
+ * Provide autocomplete for text-inputs or textareas.
16
+ *
17
+ * Depends on dimensions plugin's offset method for correct positioning of the select box and bgiframe plugin
18
+ * to fix IE's problem with selects.
19
+ *
20
+ * @example $("#input_box").autocomplete("my_autocomplete_backend.php");
21
+ * @before <input id="input_box" />
22
+ * @desc Autocomplete a text-input with remote data. For small to giant datasets.
23
+ *
24
+ * When the user starts typing, a request is send to the specified backend ("my_autocomplete_backend.php"),
25
+ * with a GET parameter named q that contains the current value of the input box and a paremeter "limit" with
26
+ * the value specified for the max option.
27
+ *
28
+ * A value of "foo" would result in this request url: my_autocomplete_backend.php?q=foo&limit=10
29
+ *
30
+ * The result must return with one value on each line. The result is presented in the order
31
+ * the backend sends it.
32
+ *
33
+ * @example $("#input_box").autocomplete(["Cologne", "Berlin", "Munich"]);
34
+ * @before <input id="input_box" />
35
+ * @desc Autcomplete a text-input with local data. For small datasets.
36
+ *
37
+ * @example $.getJSON("my_backend.php", function(data) {
38
+ * $("#input_box").autocomplete(data);
39
+ * });
40
+ * @before <input id="input_box" />
41
+ * @desc Autcomplete a text-input with data received via AJAX. For small to medium sized datasets.
42
+ *
43
+ * @example $("#mytextarea").autocomplete(["Cologne", "Berlin", "Munich"], {
44
+ * multiple: true
45
+ * });
46
+ * @before <textarea id="mytextarea" />
47
+ * @desc Autcomplete a textarea with local data (for small datasets). Once the user chooses one
48
+ * value, a separator is appended (by default a comma, see multipleSeparator option) and more values
49
+ * are autocompleted.
50
+ *
51
+ * @name autocomplete
52
+ * @cat Plugins/Autocomplete
53
+ * @type $
54
+ * @param String|Array urlOrData Pass either an URL for remote-autocompletion or an array of data for local auto-completion
55
+ * @param Map options Optional settings
56
+ * @option String inputClass This class will be added to the input box. Default: "ac_input"
57
+ * @option String resultsClass The class for the UL that will contain the result items (result items are LI elements). Default: "ac_results"
58
+ * @option String loadingClass The class for the input box while results are being fetched from the server. Default: "ac_loading"
59
+ * @option Number minChars The minimum number of characters a user has to type before the autocompleter activates. Default: 1
60
+ * @option Number delay The delay in milliseconds the autocompleter waits after a keystroke to activate itself. Default: 400 for remote, 10 for local
61
+ * @option Number cacheLength The number of backend query results to store in cache. If set to 1 (the current result), no caching will happen. Do not set below 1. Default: 10
62
+ * @option Boolean matchSubset Whether or not the autocompleter can use a cache for more specific queries. This means that all matches of "foot" are a subset of all matches for "foo". Usually this is true, and using this options decreases server load and increases performance. Only useful with cacheLength settings bigger than one, like 10. Default: true
63
+ * @option Boolean matchCase Whether or not the comparison is case sensitive. Important only if you use caching. Default: false
64
+ * @option Boolean matchContains Whether or not the comparison looks inside (i.e. does "ba" match "foo bar") the search results. Important only if you use caching. Don't mix with autofill. Default: false
65
+ * @option Booolean mustMatch If set to true, the autocompleter will only allow results that are presented by the backend. Note that illegal values result in an empty input box. Default: false
66
+ * @option Object extraParams Extra parameters for the backend. If you were to specify { bar:4 }, the autocompleter would call my_autocomplete_backend.php?q=foo&bar=4 (assuming the input box contains "foo"). The param can be a function that is called to calculate the param before each request. Default: none
67
+ * @option Boolean selectFirst If this is set to true, the first autocomplete value will be automatically selected on tab/return, even if it has not been handpicked by keyboard or mouse action. If there is a handpicked (highlighted) result, that result will take precedence. Default: true
68
+ * @option Function formatItem Provides advanced markup for an item. For each row of results, this function will be called. The returned value will be displayed inside an LI element in the results list. Autocompleter will provide 4 parameters: the results row, the position of the row in the list of results (starting at 1), the number of items in the list of results and the search term. Default: none, assumes that a single row contains a single value.
69
+ * @option Function formatResult Similar to formatItem, but provides the formatting for the value to be put into the input field. Again three arguments: Data, position (starting with one) and total number of data. Default: none, assumes either plain data to use as result or uses the same value as provided by formatItem.
70
+ * @option Boolean multiple Whether to allow more than one autocomplted-value to enter. Default: false
71
+ * @option String multipleSeparator Seperator to put between values when using multiple option. Default: ", "
72
+ * @option Number width Specify a custom width for the select box. Default: width of the input element
73
+ * @option Boolean autoFill Fill the textinput while still selecting a value, replacing the value if more is typed or something else is selected. Default: false
74
+ * @option Number max Limit the number of items in the select box. Is also sent as a "limit" parameter with a remote request. Default: 10
75
+ * @option Boolean|Function highlight Whether and how to highlight matches in the select box. Set to false to disable. Set to a function to customize. The function gets the value as the first argument and the search term as the second and must return the formatted value. Default: Wraps the search term in a <strong> element
76
+ * @option Boolean scroll Whether to scroll when more results than configured via scrollHeight are available. Default: true
77
+ * @option Number scrollHeight height of scrolled autocomplete control in pixels
78
+ * @option String attachTo The element to attach the autocomplete list to. Useful if used inside a modal window like Thickbox. Default: body -MM
79
+ */
80
+
81
+ /**
82
+ * Handle the result of a search event. Is executed when the user selects a value or a
83
+ * programmatic search event is triggered (see search()).
84
+ *
85
+ * You can add and remove (using unbind("result")) this event at any time.
86
+ *
87
+ * @example $('input#suggest').result(function(event, data, formatted) {
88
+ * $("#result").html( !data ? "No match!" : "Selected: " + formatted);
89
+ * });
90
+ * @desc Bind a handler to the result event to display the selected value in a #result element.
91
+ * The first argument is a generic event object, in this case with type "result".
92
+ * The second argument refers to the selected data, which can be a plain string value or an array or object.
93
+ * The third argument is the formatted value that is inserted into the input field.
94
+ *
95
+ * @param Function handler The event handler, gets a default event object as first and
96
+ * the selected list item as second argument.
97
+ * @name result
98
+ * @cat Plugins/Autocomplete
99
+ * @type $
100
+ */
101
+
102
+ /**
103
+ * Trigger a search event. See result(Function) for binding to that event.
104
+ *
105
+ * A search event mimics the same behaviour as when the user selects a value from
106
+ * the list of autocomplete items. You can use it to execute anything that does something
107
+ * with the selected value, beyond simply putting the value into the input and submitting it.
108
+ *
109
+ * @example $('input#suggest').search();
110
+ * @desc Triggers a search event.
111
+ *
112
+ * @name search
113
+ * @cat Plugins/Autocomplete
114
+ * @type $
115
+ */
116
+
117
+ /**
118
+ * Flush (empty) the cache of matched input's autocompleters.
119
+ *
120
+ * @example $('input#suggest').flushCache();
121
+ *
122
+ * @name flushCache
123
+ * @cat Plugins/Autocomplete
124
+ * @type $
125
+ */
126
+
127
+ /**
128
+ * Updates the options for the current autocomplete field. This allows
129
+ * you to change things like the URL, max items to display, etc. If you're
130
+ * changing the URL, be sure to remember to call the flushCache() method.
131
+ *
132
+ * @example $('input#suggest').setOptions({
133
+ * max: 15
134
+ * });
135
+ * @desc Changes the maximum number of items to display to 15.
136
+ *
137
+ * @name setOptions
138
+ * @cat Plugins/Autocomplete
139
+ * @type $
140
+ */
141
+
142
+ ;(function($) {
143
+
144
+ $.fn.extend({
145
+ autocomplete: function(urlOrData, options) {
146
+ var isUrl = typeof urlOrData == "string";
147
+ options = $.extend({}, $.Autocompleter.defaults, {
148
+ url: isUrl ? urlOrData : null,
149
+ data: isUrl ? null : urlOrData,
150
+ delay: isUrl ? $.Autocompleter.defaults.delay : 10,
151
+ max: options && !options.scroll ? 10 : 150
152
+ }, options);
153
+
154
+ // if highlight is set to false, replace it with a do-nothing function
155
+ options.highlight = options.highlight || function(value) { return value; };
156
+
157
+ return this.each(function() {
158
+ new $.Autocompleter(this, options);
159
+ });
160
+ },
161
+ result: function(handler) {
162
+ return this.bind("result", handler);
163
+ },
164
+ search: function(handler) {
165
+ return this.trigger("search", [handler]);
166
+ },
167
+ flushCache: function() {
168
+ return this.trigger("flushCache");
169
+ },
170
+ setOptions: function(options){
171
+ return this.trigger("setOptions", [options]);
172
+ },
173
+ unautocomplete: function() {
174
+ return this.trigger("unautocomplete");
175
+ }
176
+ });
177
+
178
+ $.Autocompleter = function(input, options) {
179
+
180
+ var KEY = {
181
+ UP: 38,
182
+ DOWN: 40,
183
+ DEL: 46,
184
+ TAB: 9,
185
+ RETURN: 13,
186
+ ESC: 27,
187
+ COMMA: 188,
188
+ PAGEUP: 33,
189
+ PAGEDOWN: 34
190
+ };
191
+
192
+ // Create $ object for input element
193
+ var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
194
+
195
+ var timeout;
196
+ var previousValue = "";
197
+ var cache = $.Autocompleter.Cache(options);
198
+ var hasFocus = 0;
199
+ var lastKeyPressCode;
200
+ var config = {
201
+ mouseDownOnSelect: false
202
+ };
203
+ var select = $.Autocompleter.Select(options, input, selectCurrent, config);
204
+
205
+ $input.keydown(function(event) {
206
+ // track last key pressed
207
+ lastKeyPressCode = event.keyCode;
208
+ switch(event.keyCode) {
209
+
210
+ case KEY.UP:
211
+ event.preventDefault();
212
+ if ( select.visible() ) {
213
+ select.prev();
214
+ } else {
215
+ onChange(0, true);
216
+ }
217
+ break;
218
+
219
+ case KEY.DOWN:
220
+ event.preventDefault();
221
+ if ( select.visible() ) {
222
+ select.next();
223
+ } else {
224
+ onChange(0, true);
225
+ }
226
+ break;
227
+
228
+ case KEY.PAGEUP:
229
+ event.preventDefault();
230
+ if ( select.visible() ) {
231
+ select.pageUp();
232
+ } else {
233
+ onChange(0, true);
234
+ }
235
+ break;
236
+
237
+ case KEY.PAGEDOWN:
238
+ event.preventDefault();
239
+ if ( select.visible() ) {
240
+ select.pageDown();
241
+ } else {
242
+ onChange(0, true);
243
+ }
244
+ break;
245
+
246
+ // matches also semicolon
247
+ case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
248
+ case KEY.TAB:
249
+ case KEY.RETURN:
250
+ if( selectCurrent() ){
251
+ // make sure to blur off the current field
252
+ if( !options.multiple )
253
+ $input.blur();
254
+ event.preventDefault();
255
+ }
256
+ break;
257
+
258
+ case KEY.ESC:
259
+ select.hide();
260
+ break;
261
+
262
+ default:
263
+ clearTimeout(timeout);
264
+ timeout = setTimeout(onChange, options.delay);
265
+ break;
266
+ }
267
+ }).keypress(function() {
268
+ // having fun with opera - remove this binding and Opera submits the form when we select an entry via return
269
+ }).focus(function(){
270
+ // track whether the field has focus, we shouldn't process any
271
+ // results if the field no longer has focus
272
+ hasFocus++;
273
+ }).blur(function() {
274
+ hasFocus = 0;
275
+ if (!config.mouseDownOnSelect) {
276
+ hideResults();
277
+ }
278
+ }).click(function() {
279
+ // show select when clicking in a focused field
280
+ if ( hasFocus++ > 1 && !select.visible() ) {
281
+ onChange(0, true);
282
+ }
283
+ }).bind("search", function() {
284
+ // TODO why not just specifying both arguments?
285
+ var fn = (arguments.length > 1) ? arguments[1] : null;
286
+ function findValueCallback(q, data) {
287
+ var result;
288
+ if( data && data.length ) {
289
+ for (var i=0; i < data.length; i++) {
290
+ if( data[i].result.toLowerCase() == q.toLowerCase() ) {
291
+ result = data[i];
292
+ break;
293
+ }
294
+ }
295
+ }
296
+ if( typeof fn == "function" ) fn(result);
297
+ else $input.trigger("result", result && [result.data, result.value]);
298
+ }
299
+ $.each(trimWords($input.val()), function(i, value) {
300
+ request(value, findValueCallback, findValueCallback);
301
+ });
302
+ }).bind("flushCache", function() {
303
+ cache.flush();
304
+ }).bind("setOptions", function() {
305
+ $.extend(options, arguments[1]);
306
+ // if we've updated the data, repopulate
307
+ if ( "data" in arguments[1] )
308
+ cache.populate();
309
+ }).bind("unautocomplete", function() {
310
+ select.unbind();
311
+ $input.unbind();
312
+ });
313
+
314
+
315
+ function selectCurrent() {
316
+ var selected = select.selected();
317
+ if( !selected )
318
+ return false;
319
+
320
+ var v = selected.result;
321
+ previousValue = v;
322
+
323
+ if ( options.multiple ) {
324
+ var words = trimWords($input.val());
325
+ if ( words.length > 1 ) {
326
+ v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
327
+ }
328
+ v += options.multipleSeparator;
329
+ }
330
+
331
+ $input.val(v);
332
+ hideResultsNow();
333
+ $input.trigger("result", [selected.data, selected.value]);
334
+ return true;
335
+ }
336
+
337
+ function onChange(crap, skipPrevCheck) {
338
+ if( lastKeyPressCode == KEY.DEL ) {
339
+ select.hide();
340
+ return;
341
+ }
342
+
343
+ var currentValue = $input.val();
344
+
345
+ if ( !skipPrevCheck && currentValue == previousValue )
346
+ return;
347
+
348
+ previousValue = currentValue;
349
+
350
+ currentValue = lastWord(currentValue);
351
+ if ( currentValue.length >= options.minChars) {
352
+ $input.addClass(options.loadingClass);
353
+ if (!options.matchCase)
354
+ currentValue = currentValue.toLowerCase();
355
+ request(currentValue, receiveData, hideResultsNow);
356
+ } else {
357
+ stopLoading();
358
+ select.hide();
359
+ }
360
+ };
361
+
362
+ function trimWords(value) {
363
+ if ( !value ) {
364
+ return [""];
365
+ }
366
+ var words = value.split( $.trim( options.multipleSeparator ) );
367
+ var result = [];
368
+ $.each(words, function(i, value) {
369
+ if ( $.trim(value) )
370
+ result[i] = $.trim(value);
371
+ });
372
+ return result;
373
+ }
374
+
375
+ function lastWord(value) {
376
+ if ( !options.multiple )
377
+ return value;
378
+ var words = trimWords(value);
379
+ return words[words.length - 1];
380
+ }
381
+
382
+ // fills in the input box w/the first match (assumed to be the best match)
383
+ function autoFill(q, sValue){
384
+ // autofill in the complete box w/the first match as long as the user hasn't entered in more data
385
+ // if the last user key pressed was backspace, don't autofill
386
+ if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != 8 ) {
387
+ // fill in the value (keep the case the user has typed)
388
+ $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
389
+ // select the portion of the value not typed by the user (so the next character will erase)
390
+ $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
391
+ }
392
+ };
393
+
394
+ function hideResults() {
395
+ clearTimeout(timeout);
396
+ timeout = setTimeout(hideResultsNow, 200);
397
+ };
398
+
399
+ function hideResultsNow() {
400
+ select.hide();
401
+ clearTimeout(timeout);
402
+ stopLoading();
403
+ if (options.mustMatch) {
404
+ // call search and run callback
405
+ $input.search(
406
+ function (result){
407
+ // if no value found, clear the input box
408
+ if( !result ) $input.val("");
409
+ }
410
+ );
411
+ }
412
+ };
413
+
414
+ function receiveData(q, data) {
415
+ if ( data && data.length && hasFocus ) {
416
+ stopLoading();
417
+ select.display(data, q);
418
+ autoFill(q, data[0].value);
419
+ select.show();
420
+ } else {
421
+ hideResultsNow();
422
+ }
423
+ };
424
+
425
+ function request(term, success, failure) {
426
+ if (!options.matchCase)
427
+ term = term.toLowerCase();
428
+ var data = cache.load(term);
429
+ // recieve the cached data
430
+ if (data && data.length) {
431
+ success(term, data);
432
+ // if an AJAX url has been supplied, try loading the data now
433
+ } else if( (typeof options.url == "string") && (options.url.length > 0) ){
434
+
435
+ var extraParams = {};
436
+ $.each(options.extraParams, function(key, param) {
437
+ extraParams[key] = typeof param == "function" ? param() : param;
438
+ });
439
+
440
+ $.ajax({
441
+ // try to leverage ajaxQueue plugin to abort previous requests
442
+ mode: "abort",
443
+ // limit abortion to this input
444
+ port: "autocomplete" + input.name,
445
+ dataType: options.dataType,
446
+ url: options.url,
447
+ data: $.extend({
448
+ q: lastWord(term),
449
+ limit: options.max
450
+ }, extraParams),
451
+ success: function(data) {
452
+ var parsed = options.parse && options.parse(data) || parse(data);
453
+ cache.add(term, parsed);
454
+ success(term, parsed);
455
+ }
456
+ });
457
+ } else {
458
+ failure(term);
459
+ }
460
+ };
461
+
462
+ function parse(data) {
463
+ var parsed = [];
464
+ var rows = data.split("\n");
465
+ for (var i=0; i < rows.length; i++) {
466
+ var row = $.trim(rows[i]);
467
+ if (row) {
468
+ row = row.split("|");
469
+ parsed[parsed.length] = {
470
+ data: row,
471
+ value: row[0],
472
+ result: options.formatResult && options.formatResult(row, row[0]) || row[0]
473
+ };
474
+ }
475
+ }
476
+ return parsed;
477
+ };
478
+
479
+ function stopLoading() {
480
+ $input.removeClass(options.loadingClass);
481
+ };
482
+
483
+ };
484
+
485
+ $.Autocompleter.defaults = {
486
+ inputClass: "ac_input",
487
+ resultsClass: "ac_results",
488
+ loadingClass: "ac_loading",
489
+ minChars: 1,
490
+ delay: 400,
491
+ matchCase: false,
492
+ matchSubset: true,
493
+ matchContains: false,
494
+ cacheLength: 10,
495
+ max: 100,
496
+ mustMatch: false,
497
+ extraParams: {},
498
+ selectFirst: true,
499
+ formatItem: function(row) { return row[0]; },
500
+ autoFill: false,
501
+ width: 0,
502
+ multiple: false,
503
+ multipleSeparator: ", ",
504
+ highlight: function(value, term) {
505
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
506
+ },
507
+ scroll: true,
508
+ scrollHeight: 180,
509
+ attachTo: 'body'
510
+ };
511
+
512
+ $.Autocompleter.Cache = function(options) {
513
+
514
+ var data = {};
515
+ var length = 0;
516
+
517
+ function matchSubset(s, sub) {
518
+ if (!options.matchCase)
519
+ s = s.toLowerCase();
520
+ var i = s.indexOf(sub);
521
+ if (i == -1) return false;
522
+ return i == 0 || options.matchContains;
523
+ };
524
+
525
+ function add(q, value) {
526
+ if (length > options.cacheLength){
527
+ flush();
528
+ }
529
+ if (!data[q]){
530
+ length++;
531
+ }
532
+ data[q] = value;
533
+ }
534
+
535
+ function populate(){
536
+ if( !options.data ) return false;
537
+ // track the matches
538
+ var stMatchSets = {},
539
+ nullData = 0;
540
+
541
+ // no url was specified, we need to adjust the cache length to make sure it fits the local data store
542
+ if( !options.url ) options.cacheLength = 1;
543
+
544
+ // track all options for minChars = 0
545
+ stMatchSets[""] = [];
546
+
547
+ // loop through the array and create a lookup structure
548
+ for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
549
+ var rawValue = options.data[i];
550
+ // if rawValue is a string, make an array otherwise just reference the array
551
+ rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
552
+
553
+ var value = options.formatItem(rawValue, i+1, options.data.length);
554
+ if ( value === false )
555
+ continue;
556
+
557
+ var firstChar = value.charAt(0).toLowerCase();
558
+ // if no lookup array for this character exists, look it up now
559
+ if( !stMatchSets[firstChar] )
560
+ stMatchSets[firstChar] = [];
561
+
562
+ // if the match is a string
563
+ var row = {
564
+ value: value,
565
+ data: rawValue,
566
+ result: options.formatResult && options.formatResult(rawValue) || value
567
+ };
568
+
569
+ // push the current match into the set list
570
+ stMatchSets[firstChar].push(row);
571
+
572
+ // keep track of minChars zero items
573
+ if ( nullData++ < options.max ) {
574
+ stMatchSets[""].push(row);
575
+ }
576
+ };
577
+
578
+ // add the data items to the cache
579
+ $.each(stMatchSets, function(i, value) {
580
+ // increase the cache size
581
+ options.cacheLength++;
582
+ // add to the cache
583
+ add(i, value);
584
+ });
585
+ }
586
+
587
+ // populate any existing data
588
+ setTimeout(populate, 25);
589
+
590
+ function flush(){
591
+ data = {};
592
+ length = 0;
593
+ }
594
+
595
+ return {
596
+ flush: flush,
597
+ add: add,
598
+ populate: populate,
599
+ load: function(q) {
600
+ if (!options.cacheLength || !length)
601
+ return null;
602
+ /*
603
+ * if dealing w/local data and matchContains than we must make sure
604
+ * to loop through all the data collections looking for matches
605
+ */
606
+ if( !options.url && options.matchContains ){
607
+ // track all matches
608
+ var csub = [];
609
+ // loop through all the data grids for matches
610
+ for( var k in data ){
611
+ // don't search through the stMatchSets[""] (minChars: 0) cache
612
+ // this prevents duplicates
613
+ if( k.length > 0 ){
614
+ var c = data[k];
615
+ $.each(c, function(i, x) {
616
+ // if we've got a match, add it to the array
617
+ if (matchSubset(x.value, q)) {
618
+ csub.push(x);
619
+ }
620
+ });
621
+ }
622
+ }
623
+ return csub;
624
+ } else
625
+ // if the exact item exists, use it
626
+ if (data[q]){
627
+ return data[q];
628
+ } else
629
+ if (options.matchSubset) {
630
+ for (var i = q.length - 1; i >= options.minChars; i--) {
631
+ var c = data[q.substr(0, i)];
632
+ if (c) {
633
+ var csub = [];
634
+ $.each(c, function(i, x) {
635
+ if (matchSubset(x.value, q)) {
636
+ csub[csub.length] = x;
637
+ }
638
+ });
639
+ return csub;
640
+ }
641
+ }
642
+ }
643
+ return null;
644
+ }
645
+ };
646
+ };
647
+
648
+ $.Autocompleter.Select = function (options, input, select, config) {
649
+ var CLASSES = {
650
+ ACTIVE: "ac_over"
651
+ };
652
+
653
+ var listItems,
654
+ active = -1,
655
+ data,
656
+ term = "",
657
+ needsInit = true,
658
+ element,
659
+ list;
660
+
661
+ // Create results
662
+ function init() {
663
+ if (!needsInit)
664
+ return;
665
+ element = $("<div/>")
666
+ .hide()
667
+ .addClass(options.resultsClass)
668
+ .css("position", "absolute")
669
+ .appendTo(options.attachTo);
670
+
671
+ list = $("<ul>").appendTo(element).mouseover( function(event) {
672
+ if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
673
+ active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
674
+ $(target(event)).addClass(CLASSES.ACTIVE);
675
+ }
676
+ }).click(function(event) {
677
+ $(target(event)).addClass(CLASSES.ACTIVE);
678
+ select();
679
+ input.focus();
680
+ return false;
681
+ }).mousedown(function() {
682
+ config.mouseDownOnSelect = true;
683
+ }).mouseup(function() {
684
+ config.mouseDownOnSelect = false;
685
+ });
686
+
687
+ if( options.width > 0 )
688
+ element.css("width", options.width);
689
+
690
+ needsInit = false;
691
+ }
692
+
693
+ function target(event) {
694
+ var element = event.target;
695
+ while(element && element.tagName != "LI")
696
+ element = element.parentNode;
697
+ // more fun with IE, sometimes event.target is empty, just ignore it then
698
+ if(!element)
699
+ return [];
700
+ return element;
701
+ }
702
+
703
+ function moveSelect(step) {
704
+ listItems.slice(active, active + 1).removeClass();
705
+ movePosition(step);
706
+ var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
707
+ if(options.scroll) {
708
+ var offset = 0;
709
+ listItems.slice(0, active).each(function() {
710
+ offset += this.offsetHeight;
711
+ });
712
+ if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
713
+ list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
714
+ } else if(offset < list.scrollTop()) {
715
+ list.scrollTop(offset);
716
+ }
717
+ }
718
+ };
719
+
720
+ function movePosition(step) {
721
+ active += step;
722
+ if (active < 0) {
723
+ active = listItems.size() - 1;
724
+ } else if (active >= listItems.size()) {
725
+ active = 0;
726
+ }
727
+ }
728
+
729
+ function limitNumberOfItems(available) {
730
+ return options.max && options.max < available
731
+ ? options.max
732
+ : available;
733
+ }
734
+
735
+ function fillList() {
736
+ list.empty();
737
+ var max = limitNumberOfItems(data.length);
738
+ for (var i=0; i < max; i++) {
739
+ if (!data[i])
740
+ continue;
741
+ var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
742
+ if ( formatted === false )
743
+ continue;
744
+ var li = $("<li>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_event" : "ac_odd").appendTo(list)[0];
745
+ $.data(li, "ac_data", data[i]);
746
+ }
747
+ listItems = list.find("li");
748
+ if ( options.selectFirst ) {
749
+ listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
750
+ active = 0;
751
+ }
752
+ list.bgiframe();
753
+ }
754
+
755
+ return {
756
+ display: function(d, q) {
757
+ init();
758
+ data = d;
759
+ term = q;
760
+ fillList();
761
+ },
762
+ next: function() {
763
+ moveSelect(1);
764
+ },
765
+ prev: function() {
766
+ moveSelect(-1);
767
+ },
768
+ pageUp: function() {
769
+ if (active != 0 && active - 8 < 0) {
770
+ moveSelect( -active );
771
+ } else {
772
+ moveSelect(-8);
773
+ }
774
+ },
775
+ pageDown: function() {
776
+ if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
777
+ moveSelect( listItems.size() - 1 - active );
778
+ } else {
779
+ moveSelect(8);
780
+ }
781
+ },
782
+ hide: function() {
783
+ element && element.hide();
784
+ active = -1;
785
+ },
786
+ visible : function() {
787
+ return element && element.is(":visible");
788
+ },
789
+ current: function() {
790
+ return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
791
+ },
792
+ show: function() {
793
+ var offset = $(input).offset();
794
+ element.css({
795
+ width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
796
+ top: offset.top + input.offsetHeight,
797
+ left: offset.left
798
+ }).show();
799
+ if(options.scroll) {
800
+ list.scrollTop(0);
801
+ list.css({
802
+ maxHeight: options.scrollHeight,
803
+ overflow: 'auto'
804
+ });
805
+
806
+ if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
807
+ var listHeight = 0;
808
+ listItems.each(function() {
809
+ listHeight += this.offsetHeight;
810
+ });
811
+ var scrollbarsVisible = listHeight > options.scrollHeight;
812
+ list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
813
+ if (!scrollbarsVisible) {
814
+ // IE doesn't recalculate width when scrollbar disappears
815
+ listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
816
+ }
817
+ }
818
+
819
+ }
820
+ },
821
+ selected: function() {
822
+ var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
823
+ return selected && selected.length && $.data(selected[0], "ac_data");
824
+ },
825
+ unbind: function() {
826
+ element && element.remove();
827
+ }
828
+ };
829
+ };
830
+
831
+ $.Autocompleter.Selection = function(field, start, end) {
832
+ if( field.createTextRange ){
833
+ var selRange = field.createTextRange();
834
+ selRange.collapse(true);
835
+ selRange.moveStart("character", start);
836
+ selRange.moveEnd("character", end);
837
+ selRange.select();
838
+ } else if( field.setSelectionRange ){
839
+ field.setSelectionRange(start, end);
840
+ } else {
841
+ if( field.selectionStart ){
842
+ field.selectionStart = start;
843
+ field.selectionEnd = end;
844
+ }
845
+ }
846
+ field.focus();
847
+ };
848
+
849
+ })(jQuery);