voluntary 0.0.3 → 0.1.0.rc1

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.
Files changed (45) hide show
  1. data/README.rdoc +37 -2
  2. data/app/assets/images/voluntary/spinner.gif +0 -0
  3. data/app/assets/javascripts/voluntary/application.js +2 -2
  4. data/app/assets/javascripts/voluntary/base.js.coffee +8 -1
  5. data/app/assets/javascripts/voluntary/users.js.coffee +2 -3
  6. data/app/assets/stylesheets/voluntary/application.css +2 -0
  7. data/app/assets/stylesheets/voluntary/base.css.sass +62 -0
  8. data/app/assets/stylesheets/voluntary/bootstrap_and_overrides.css.sass +4 -0
  9. data/app/controllers/workflow/project_owner_controller.rb +1 -1
  10. data/app/helpers/collection_helper.rb +5 -1
  11. data/app/helpers/form_helper.rb +6 -2
  12. data/{lib/generators/voluntary/install/templates/app → app}/models/ability.rb +0 -0
  13. data/app/models/page.rb +2 -2
  14. data/app/models/product.rb +9 -9
  15. data/app/models/project.rb +8 -1
  16. data/app/models/task.rb +2 -14
  17. data/app/presenters/shared/collection/table_presenter.rb +2 -2
  18. data/app/views/general/wizard.html.erb +1 -1
  19. data/app/views/layouts/application.html.erb +6 -0
  20. data/app/views/layouts/shared/_navigation.html.erb +4 -1
  21. data/app/views/vacancies/show.html.erb +0 -2
  22. data/db/migrate/20121122185954_add_klass_name_to_products.rb +17 -0
  23. data/lib/applicat/mvc/controller.rb +0 -2
  24. data/lib/applicat/mvc/controller/resource.rb +1 -1
  25. data/lib/applicat/mvc/model/resource/base.rb +8 -3
  26. data/lib/db_seed.rb +1 -1
  27. data/lib/generators/voluntary/install/install_generator.rb +1 -2
  28. data/lib/generators/voluntary/install/templates/app/controllers/application_controller.rb +4 -0
  29. data/lib/generators/voluntary/install/templates/app/models/application/ability.rb +8 -0
  30. data/lib/generators/voluntary/install/templates/config/deploy.rb +2 -0
  31. data/lib/generators/voluntary/install/templates/features/step_definitions/user_steps.rb +4 -0
  32. data/lib/generators/voluntary/install/templates/features/support/user_cuke_helpers.rb +2 -2
  33. data/lib/generators/voluntary/product_dummy/product_dummy_generator.rb +1 -1
  34. data/lib/generators/voluntary/product_dummy/templates/vendor/assets/javascripts/jquery.tokeninput.js +915 -0
  35. data/lib/generators/voluntary/product_dummy/templates/vendor/assets/stylesheets/token-input-facebook.css +122 -0
  36. data/lib/generators/voluntary/product_dummy/templates/vendor/assets/stylesheets/token-input-mac.css +204 -0
  37. data/lib/generators/voluntary/product_dummy/templates/vendor/assets/stylesheets/token-input.css +127 -0
  38. data/lib/volontariat_seed.rb +27 -0
  39. data/lib/voluntary.rb +6 -5
  40. data/lib/voluntary/version.rb +1 -1
  41. metadata +560 -209
  42. data/app/assets/javascripts/voluntary/bootstrap.js.coffee +0 -4
  43. data/app/assets/stylesheets/voluntary/base.css.scss +0 -40
  44. data/app/assets/stylesheets/voluntary/bootstrap_and_overrides.css.less +0 -67
  45. data/config/initializers/recaptcha.rb +0 -7
@@ -4,8 +4,6 @@ module Applicat
4
4
  module Controller
5
5
  def self.included(base)
6
6
  base.class_eval do
7
- include RailsInfo::Controller::ExceptionDiagnostics
8
-
9
7
  include ErrorHandling
10
8
  include TransitionActions
11
9
 
@@ -10,7 +10,7 @@ module Applicat::Mvc::Controller::Resource
10
10
  module InstanceMethods
11
11
  def autocomplete
12
12
  render json: (
13
- controller_name.classify.constantize.
13
+ params[:controller].classify.constantize.
14
14
  select(:name).order(:name).where("name LIKE ?", "%#{params[:term]}%").
15
15
  map(&:name)
16
16
  )
@@ -11,9 +11,8 @@ module Applicat
11
11
  @@per_page = 20
12
12
 
13
13
  attr_accessor :current_user
14
-
14
+
15
15
  if self.table_exists?
16
- #reflections.values.select{|r| [:has_one, :has_many].include?(r.macro)}.map(&:name)
17
16
 
18
17
  columns.map(&:name).select{|c| c.match('_id')}.each do |column|
19
18
  association = column.split('_id').first.classify
@@ -33,7 +32,13 @@ module Applicat
33
32
  association_type = self.send("#{association.underscore}_type")
34
33
  end
35
34
 
36
- self.send("#{association.underscore}=", association_type.constantize.find_or_initialize_by_name(name))
35
+ begin
36
+ association_type = association_type.constantize
37
+ rescue
38
+ association_type = self.class.reflections[column.split('_id').first.to_sym].options[:class_name].constantize
39
+ end
40
+
41
+ self.send("#{association.underscore}=", association_type.find_or_initialize_by_name(name))
37
42
  end
38
43
  end
39
44
  end
@@ -73,7 +73,7 @@ class DbSeed
73
73
  password: "#{role}2012", language: 'en', country: 'Germany', interface_language: 'en'
74
74
  }
75
75
  attributes[:password_confirmation] = attributes[:password]
76
- create_record(User, attributes, events: ['skip_confirmation!'])
76
+ create_record(User, attributes) # events: ['skip_confirmation!']
77
77
  end
78
78
  end
79
79
 
@@ -18,8 +18,7 @@ module Voluntary
18
18
 
19
19
  # model
20
20
  gem 'settingslogic', git: 'https://github.com/binarylogic/settingslogic.git'
21
- gem 'acts-as-taggable-on', git: 'https://github.com/mbleigh/acts-as-taggable-on.git'
22
-
21
+
23
22
  # view
24
23
  gem 'acts_as_markup', git: 'git://github.com/vigetlabs/acts_as_markup.git'
25
24
  gem 'auto_html', git: 'git://github.com/Applicat/auto_html'
@@ -1,3 +1,7 @@
1
1
  class ApplicationController < Voluntary::ApplicationController
2
2
  layout Proc.new { |controller| controller.request.xhr? ? 'facebox' : 'application' }
3
+
4
+ def current_ability
5
+ @current_ability ||= super.merge(Application::Ability.new(current_user, controller_namespace: current_namespace))
6
+ end
3
7
  end
@@ -0,0 +1,8 @@
1
+ class Application::Ability
2
+ include CanCan::Ability
3
+
4
+ def initialize(user, options = {})
5
+ if user.present?
6
+ end
7
+ end
8
+ end
@@ -40,7 +40,9 @@ namespace :deploy do
40
40
  task :symlink_config, roles: :app do
41
41
  run "ln -nfs #{shared_path}/config/database.yml #{release_path}/config/database.yml"
42
42
  run "ln -nfs #{shared_path}/config/email.yml #{release_path}/config/email.yml"
43
+ run "ln -nfs #{shared_path}/config/initializers/airbrake.rb #{release_path}/config/initializers/airbrake.rb"
43
44
  run "ln -nfs #{shared_path}/config/initializers/recaptcha.rb #{release_path}/config/initializers/recaptcha.rb"
45
+ run "ln -nfs #{shared_path}/config/initializers/secret_token.rb #{release_path}/config/initializers/secret_token.rb"
44
46
  end
45
47
  after "deploy:finalize_update", "deploy:symlink_config"
46
48
 
@@ -0,0 +1,4 @@
1
+ Given /^a user named "([^\"]*)"$/ do |name|
2
+ @me = FactoryGirl.create(:user, name: name, email: "#{name}@volontari.at")
3
+ @me.reload
4
+ end
@@ -4,7 +4,7 @@ module UserCukeHelpers
4
4
  # and the given override attributes, adds the standard aspects to it
5
5
  # and returns it
6
6
  def create_user(overrides={})
7
- Factory(
7
+ FactoryGirl.create(
8
8
  :user, {
9
9
  password: 'password',
10
10
  password_confirmation: 'password'
@@ -22,7 +22,7 @@ module UserCukeHelpers
22
22
  # create a new @me user, if not present, and log in using the
23
23
  # integration_sessions controller (automatic)
24
24
  def automatic_login
25
- @me ||= Factory(:user)
25
+ @me ||= FactoryGirl.create(:user)
26
26
  page.driver.visit(new_integration_sessions_path(user_id: @me.slug))
27
27
  click_button "Login"
28
28
  end
@@ -4,7 +4,7 @@ module Voluntary
4
4
  source_root File.expand_path("../templates", __FILE__)
5
5
 
6
6
  def copy_templates
7
- ['app', 'config', 'features', 'spec'].each do |directory_name|
7
+ ['app', 'config', 'features', 'spec', 'vendor'].each do |directory_name|
8
8
  directory directory_name
9
9
  end
10
10
  end
@@ -0,0 +1,915 @@
1
+ /*
2
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
3
+ * Version 1.6.0
4
+ *
5
+ * Copyright (c) 2009 James Smith (http://loopj.com)
6
+ * Licensed jointly under the GPL and MIT licenses,
7
+ * choose which one suits your project best!
8
+ *
9
+ */
10
+
11
+ (function ($) {
12
+ // Default settings
13
+ var DEFAULT_SETTINGS = {
14
+ // Search settings
15
+ method: "GET",
16
+ queryParam: "q",
17
+ searchDelay: 300,
18
+ minChars: 1,
19
+ propertyToSearch: "name",
20
+ jsonContainer: null,
21
+ contentType: "json",
22
+
23
+ // Prepopulation settings
24
+ prePopulate: null,
25
+ processPrePopulate: false,
26
+
27
+ // Display settings
28
+ hintText: "Type in a search term",
29
+ noResultsText: "No results",
30
+ searchingText: "Searching...",
31
+ deleteText: "&times;",
32
+ animateDropdown: true,
33
+ theme: null,
34
+ zindex: 999,
35
+ resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
36
+ tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },
37
+
38
+ // Tokenization settings
39
+ tokenLimit: null,
40
+ tokenDelimiter: ",",
41
+ preventDuplicates: false,
42
+ tokenValue: "id",
43
+
44
+ // Callbacks
45
+ onResult: null,
46
+ onAdd: null,
47
+ onDelete: null,
48
+ onReady: null,
49
+
50
+ // Other settings
51
+ idPrefix: "token-input-",
52
+
53
+ // Keep track if the input is currently in disabled mode
54
+ disabled: false
55
+ };
56
+
57
+ // Default classes to use when theming
58
+ var DEFAULT_CLASSES = {
59
+ tokenList: "token-input-list",
60
+ token: "token-input-token",
61
+ tokenDelete: "token-input-delete-token",
62
+ selectedToken: "token-input-selected-token",
63
+ highlightedToken: "token-input-highlighted-token",
64
+ dropdown: "token-input-dropdown",
65
+ dropdownItem: "token-input-dropdown-item",
66
+ dropdownItem2: "token-input-dropdown-item2",
67
+ selectedDropdownItem: "token-input-selected-dropdown-item",
68
+ inputToken: "token-input-input-token",
69
+ focused: "token-input-focused",
70
+ disabled: "token-input-disabled"
71
+ };
72
+
73
+ // Input box position "enum"
74
+ var POSITION = {
75
+ BEFORE: 0,
76
+ AFTER: 1,
77
+ END: 2
78
+ };
79
+
80
+ // Keys "enum"
81
+ var KEY = {
82
+ BACKSPACE: 8,
83
+ TAB: 9,
84
+ ENTER: 13,
85
+ ESCAPE: 27,
86
+ SPACE: 32,
87
+ PAGE_UP: 33,
88
+ PAGE_DOWN: 34,
89
+ END: 35,
90
+ HOME: 36,
91
+ LEFT: 37,
92
+ UP: 38,
93
+ RIGHT: 39,
94
+ DOWN: 40,
95
+ NUMPAD_ENTER: 108,
96
+ COMMA: 188
97
+ };
98
+
99
+ // Additional public (exposed) methods
100
+ var methods = {
101
+ init: function(url_or_data_or_function, options) {
102
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
103
+
104
+ return this.each(function () {
105
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
106
+ });
107
+ },
108
+ clear: function() {
109
+ this.data("tokenInputObject").clear();
110
+ return this;
111
+ },
112
+ add: function(item) {
113
+ this.data("tokenInputObject").add(item);
114
+ return this;
115
+ },
116
+ remove: function(item) {
117
+ this.data("tokenInputObject").remove(item);
118
+ return this;
119
+ },
120
+ get: function() {
121
+ return this.data("tokenInputObject").getTokens();
122
+ },
123
+ toggleDisabled: function(disable) {
124
+ this.data("tokenInputObject").toggleDisabled(disable);
125
+ return this;
126
+ }
127
+ }
128
+
129
+ // Expose the .tokenInput function to jQuery as a plugin
130
+ $.fn.tokenInput = function (method) {
131
+ // Method calling and initialization logic
132
+ if(methods[method]) {
133
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
134
+ } else {
135
+ return methods.init.apply(this, arguments);
136
+ }
137
+ };
138
+
139
+ // TokenList class for each input
140
+ $.TokenList = function (input, url_or_data, settings) {
141
+ //
142
+ // Initialization
143
+ //
144
+
145
+ // Configure the data source
146
+ if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
147
+ // Set the url to query against
148
+ settings.url = url_or_data;
149
+
150
+ // If the URL is a function, evaluate it here to do our initalization work
151
+ var url = computeURL();
152
+
153
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
154
+ if(settings.crossDomain === undefined && typeof url === "string") {
155
+ if(url.indexOf("://") === -1) {
156
+ settings.crossDomain = false;
157
+ } else {
158
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
159
+ }
160
+ }
161
+ } else if(typeof(url_or_data) === "object") {
162
+ // Set the local data to search through
163
+ settings.local_data = url_or_data;
164
+ }
165
+
166
+ // Build class names
167
+ if(settings.classes) {
168
+ // Use custom class names
169
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
170
+ } else if(settings.theme) {
171
+ // Use theme-suffixed default class names
172
+ settings.classes = {};
173
+ $.each(DEFAULT_CLASSES, function(key, value) {
174
+ settings.classes[key] = value + "-" + settings.theme;
175
+ });
176
+ } else {
177
+ settings.classes = DEFAULT_CLASSES;
178
+ }
179
+
180
+
181
+ // Save the tokens
182
+ var saved_tokens = [];
183
+
184
+ // Keep track of the number of tokens in the list
185
+ var token_count = 0;
186
+
187
+ // Basic cache to save on db hits
188
+ var cache = new $.TokenList.Cache();
189
+
190
+ // Keep track of the timeout, old vals
191
+ var timeout;
192
+ var input_val;
193
+
194
+ // Create a new text input an attach keyup events
195
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
196
+ .css({
197
+ outline: "none"
198
+ })
199
+ .attr("id", settings.idPrefix + input.id)
200
+ .focus(function () {
201
+ if (settings.disabled) {
202
+ return false;
203
+ } else
204
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
205
+ show_dropdown_hint();
206
+ }
207
+ token_list.addClass(settings.classes.focused);
208
+ })
209
+ .blur(function () {
210
+ hide_dropdown();
211
+ $(this).val("");
212
+ token_list.removeClass(settings.classes.focused);
213
+ })
214
+ .bind("keyup keydown blur update", resize_input)
215
+ .keydown(function (event) {
216
+ var previous_token;
217
+ var next_token;
218
+
219
+ switch(event.keyCode) {
220
+ case KEY.LEFT:
221
+ case KEY.RIGHT:
222
+ case KEY.UP:
223
+ case KEY.DOWN:
224
+ if(!$(this).val()) {
225
+ previous_token = input_token.prev();
226
+ next_token = input_token.next();
227
+
228
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
229
+ // Check if there is a previous/next token and it is selected
230
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
231
+ deselect_token($(selected_token), POSITION.BEFORE);
232
+ } else {
233
+ deselect_token($(selected_token), POSITION.AFTER);
234
+ }
235
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
236
+ // We are moving left, select the previous token if it exists
237
+ select_token($(previous_token.get(0)));
238
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
239
+ // We are moving right, select the next token if it exists
240
+ select_token($(next_token.get(0)));
241
+ }
242
+ } else {
243
+ var dropdown_item = null;
244
+
245
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
246
+ dropdown_item = $(selected_dropdown_item).next();
247
+ } else {
248
+ dropdown_item = $(selected_dropdown_item).prev();
249
+ }
250
+
251
+ if(dropdown_item.length) {
252
+ select_dropdown_item(dropdown_item);
253
+ }
254
+ }
255
+ return false;
256
+ break;
257
+
258
+ case KEY.BACKSPACE:
259
+ previous_token = input_token.prev();
260
+
261
+ if(!$(this).val().length) {
262
+ if(selected_token) {
263
+ delete_token($(selected_token));
264
+ hidden_input.change();
265
+ } else if(previous_token.length) {
266
+ select_token($(previous_token.get(0)));
267
+ }
268
+
269
+ return false;
270
+ } else if($(this).val().length === 1) {
271
+ hide_dropdown();
272
+ } else {
273
+ // set a timeout just long enough to let this function finish.
274
+ setTimeout(function(){do_search();}, 5);
275
+ }
276
+ break;
277
+
278
+ case KEY.TAB:
279
+ case KEY.ENTER:
280
+ case KEY.NUMPAD_ENTER:
281
+ case KEY.COMMA:
282
+ if(selected_dropdown_item) {
283
+ add_token($(selected_dropdown_item).data("tokeninput"));
284
+ hidden_input.change();
285
+ return false;
286
+ }
287
+ break;
288
+
289
+ case KEY.ESCAPE:
290
+ hide_dropdown();
291
+ return true;
292
+
293
+ default:
294
+ if(String.fromCharCode(event.which)) {
295
+ // set a timeout just long enough to let this function finish.
296
+ setTimeout(function(){do_search();}, 5);
297
+ }
298
+ break;
299
+ }
300
+ });
301
+
302
+ // Keep a reference to the original input box
303
+ var hidden_input = $(input)
304
+ .hide()
305
+ .val("")
306
+ .focus(function () {
307
+ focus_with_timeout(input_box);
308
+ })
309
+ .blur(function () {
310
+ input_box.blur();
311
+ });
312
+
313
+ // Keep a reference to the selected token and dropdown item
314
+ var selected_token = null;
315
+ var selected_token_index = 0;
316
+ var selected_dropdown_item = null;
317
+
318
+ // The list to store the token items in
319
+ var token_list = $("<ul />")
320
+ .addClass(settings.classes.tokenList)
321
+ .click(function (event) {
322
+ var li = $(event.target).closest("li");
323
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
324
+ toggle_select_token(li);
325
+ } else {
326
+ // Deselect selected token
327
+ if(selected_token) {
328
+ deselect_token($(selected_token), POSITION.END);
329
+ }
330
+
331
+ // Focus input box
332
+ focus_with_timeout(input_box);
333
+ }
334
+ })
335
+ .mouseover(function (event) {
336
+ var li = $(event.target).closest("li");
337
+ if(li && selected_token !== this) {
338
+ li.addClass(settings.classes.highlightedToken);
339
+ }
340
+ })
341
+ .mouseout(function (event) {
342
+ var li = $(event.target).closest("li");
343
+ if(li && selected_token !== this) {
344
+ li.removeClass(settings.classes.highlightedToken);
345
+ }
346
+ })
347
+ .insertBefore(hidden_input);
348
+
349
+ // The token holding the input box
350
+ var input_token = $("<li />")
351
+ .addClass(settings.classes.inputToken)
352
+ .appendTo(token_list)
353
+ .append(input_box);
354
+
355
+ // The list to store the dropdown items in
356
+ var dropdown = $("<div>")
357
+ .addClass(settings.classes.dropdown)
358
+ .appendTo("body")
359
+ .hide();
360
+
361
+ // Magic element to help us resize the text input
362
+ var input_resizer = $("<tester/>")
363
+ .insertAfter(input_box)
364
+ .css({
365
+ position: "absolute",
366
+ top: -9999,
367
+ left: -9999,
368
+ width: "auto",
369
+ fontSize: input_box.css("fontSize"),
370
+ fontFamily: input_box.css("fontFamily"),
371
+ fontWeight: input_box.css("fontWeight"),
372
+ letterSpacing: input_box.css("letterSpacing"),
373
+ whiteSpace: "nowrap"
374
+ });
375
+
376
+ // Pre-populate list if items exist
377
+ hidden_input.val("");
378
+ var li_data = settings.prePopulate || hidden_input.data("pre");
379
+ if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
380
+ li_data = settings.onResult.call(hidden_input, li_data);
381
+ }
382
+ if(li_data && li_data.length) {
383
+ $.each(li_data, function (index, value) {
384
+ insert_token(value);
385
+ checkTokenLimit();
386
+ });
387
+ }
388
+
389
+ // Check if widget should initialize as disabled
390
+ if (settings.disabled) {
391
+ toggleDisabled(true);
392
+ }
393
+
394
+ // Initialization is done
395
+ if($.isFunction(settings.onReady)) {
396
+ settings.onReady.call();
397
+ }
398
+
399
+ //
400
+ // Public functions
401
+ //
402
+
403
+ this.clear = function() {
404
+ token_list.children("li").each(function() {
405
+ if ($(this).children("input").length === 0) {
406
+ delete_token($(this));
407
+ }
408
+ });
409
+ }
410
+
411
+ this.add = function(item) {
412
+ add_token(item);
413
+ }
414
+
415
+ this.remove = function(item) {
416
+ token_list.children("li").each(function() {
417
+ if ($(this).children("input").length === 0) {
418
+ var currToken = $(this).data("tokeninput");
419
+ var match = true;
420
+ for (var prop in item) {
421
+ if (item[prop] !== currToken[prop]) {
422
+ match = false;
423
+ break;
424
+ }
425
+ }
426
+ if (match) {
427
+ delete_token($(this));
428
+ }
429
+ }
430
+ });
431
+ }
432
+
433
+ this.getTokens = function() {
434
+ return saved_tokens;
435
+ }
436
+
437
+ this.toggleDisabled = function(disable) {
438
+ toggleDisabled(disable);
439
+ }
440
+
441
+ //
442
+ // Private functions
443
+ //
444
+
445
+ // Toggles the widget between enabled and disabled state, or according
446
+ // to the [disable] parameter.
447
+ function toggleDisabled(disable) {
448
+ if (typeof disable === 'boolean') {
449
+ settings.disabled = disable
450
+ } else {
451
+ settings.disabled = !settings.disabled;
452
+ }
453
+ input_box.prop('disabled', settings.disabled);
454
+ token_list.toggleClass(settings.classes.disabled, settings.disabled);
455
+ // if there is any token selected we deselect it
456
+ if(selected_token) {
457
+ deselect_token($(selected_token), POSITION.END);
458
+ }
459
+ hidden_input.prop('disabled', settings.disabled);
460
+ }
461
+
462
+ function checkTokenLimit() {
463
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
464
+ input_box.hide();
465
+ hide_dropdown();
466
+ return;
467
+ }
468
+ }
469
+
470
+ function resize_input() {
471
+ if(input_val === (input_val = input_box.val())) {return;}
472
+
473
+ // Enter new content into resizer and resize input accordingly
474
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
475
+ input_resizer.html(escaped);
476
+ input_box.width(input_resizer.width() + 30);
477
+ }
478
+
479
+ function is_printable_character(keycode) {
480
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
481
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
482
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
483
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
484
+ }
485
+
486
+ // Inner function to a token to the list
487
+ function insert_token(item) {
488
+ var this_token = settings.tokenFormatter(item);
489
+ this_token = $(this_token)
490
+ .addClass(settings.classes.token)
491
+ .insertBefore(input_token);
492
+
493
+ // The 'delete token' button
494
+ $("<span>" + settings.deleteText + "</span>")
495
+ .addClass(settings.classes.tokenDelete)
496
+ .appendTo(this_token)
497
+ .click(function () {
498
+ if (!settings.disabled) {
499
+ delete_token($(this).parent());
500
+ hidden_input.change();
501
+ return false;
502
+ }
503
+ });
504
+
505
+ // Store data on the token
506
+ var token_data = item;
507
+ $.data(this_token.get(0), "tokeninput", item);
508
+
509
+ // Save this token for duplicate checking
510
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
511
+ selected_token_index++;
512
+
513
+ // Update the hidden input
514
+ update_hidden_input(saved_tokens, hidden_input);
515
+
516
+ token_count += 1;
517
+
518
+ // Check the token limit
519
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
520
+ input_box.hide();
521
+ hide_dropdown();
522
+ }
523
+
524
+ return this_token;
525
+ }
526
+
527
+ // Add a token to the token list based on user input
528
+ function add_token (item) {
529
+ var callback = settings.onAdd;
530
+
531
+ // See if the token already exists and select it if we don't want duplicates
532
+ if(token_count > 0 && settings.preventDuplicates) {
533
+ var found_existing_token = null;
534
+ token_list.children().each(function () {
535
+ var existing_token = $(this);
536
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
537
+ if(existing_data && existing_data.id === item.id) {
538
+ found_existing_token = existing_token;
539
+ return false;
540
+ }
541
+ });
542
+
543
+ if(found_existing_token) {
544
+ select_token(found_existing_token);
545
+ input_token.insertAfter(found_existing_token);
546
+ focus_with_timeout(input_box);
547
+ return;
548
+ }
549
+ }
550
+
551
+ // Insert the new tokens
552
+ if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
553
+ insert_token(item);
554
+ checkTokenLimit();
555
+ }
556
+
557
+ // Clear input box
558
+ input_box.val("");
559
+
560
+ // Don't show the help dropdown, they've got the idea
561
+ hide_dropdown();
562
+
563
+ // Execute the onAdd callback if defined
564
+ if($.isFunction(callback)) {
565
+ callback.call(hidden_input,item);
566
+ }
567
+ }
568
+
569
+ // Select a token in the token list
570
+ function select_token (token) {
571
+ if (!settings.disabled) {
572
+ token.addClass(settings.classes.selectedToken);
573
+ selected_token = token.get(0);
574
+
575
+ // Hide input box
576
+ input_box.val("");
577
+
578
+ // Hide dropdown if it is visible (eg if we clicked to select token)
579
+ hide_dropdown();
580
+ }
581
+ }
582
+
583
+ // Deselect a token in the token list
584
+ function deselect_token (token, position) {
585
+ token.removeClass(settings.classes.selectedToken);
586
+ selected_token = null;
587
+
588
+ if(position === POSITION.BEFORE) {
589
+ input_token.insertBefore(token);
590
+ selected_token_index--;
591
+ } else if(position === POSITION.AFTER) {
592
+ input_token.insertAfter(token);
593
+ selected_token_index++;
594
+ } else {
595
+ input_token.appendTo(token_list);
596
+ selected_token_index = token_count;
597
+ }
598
+
599
+ // Show the input box and give it focus again
600
+ focus_with_timeout(input_box);
601
+ }
602
+
603
+ // Toggle selection of a token in the token list
604
+ function toggle_select_token(token) {
605
+ var previous_selected_token = selected_token;
606
+
607
+ if(selected_token) {
608
+ deselect_token($(selected_token), POSITION.END);
609
+ }
610
+
611
+ if(previous_selected_token === token.get(0)) {
612
+ deselect_token(token, POSITION.END);
613
+ } else {
614
+ select_token(token);
615
+ }
616
+ }
617
+
618
+ // Delete a token from the token list
619
+ function delete_token (token) {
620
+ // Remove the id from the saved list
621
+ var token_data = $.data(token.get(0), "tokeninput");
622
+ var callback = settings.onDelete;
623
+
624
+ var index = token.prevAll().length;
625
+ if(index > selected_token_index) index--;
626
+
627
+ // Delete the token
628
+ token.remove();
629
+ selected_token = null;
630
+
631
+ // Show the input box and give it focus again
632
+ focus_with_timeout(input_box);
633
+
634
+ // Remove this token from the saved list
635
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
636
+ if(index < selected_token_index) selected_token_index--;
637
+
638
+ // Update the hidden input
639
+ update_hidden_input(saved_tokens, hidden_input);
640
+
641
+ token_count -= 1;
642
+
643
+ if(settings.tokenLimit !== null) {
644
+ input_box
645
+ .show()
646
+ .val("");
647
+ focus_with_timeout(input_box);
648
+ }
649
+
650
+ // Execute the onDelete callback if defined
651
+ if($.isFunction(callback)) {
652
+ callback.call(hidden_input,token_data);
653
+ }
654
+ }
655
+
656
+ // Update the hidden input box value
657
+ function update_hidden_input(saved_tokens, hidden_input) {
658
+ var token_values = $.map(saved_tokens, function (el) {
659
+ if(typeof settings.tokenValue == 'function')
660
+ return settings.tokenValue.call(this, el);
661
+
662
+ return el[settings.tokenValue];
663
+ });
664
+ hidden_input.val(token_values.join(settings.tokenDelimiter));
665
+
666
+ }
667
+
668
+ // Hide and clear the results dropdown
669
+ function hide_dropdown () {
670
+ dropdown.hide().empty();
671
+ selected_dropdown_item = null;
672
+ }
673
+
674
+ function show_dropdown() {
675
+ dropdown
676
+ .css({
677
+ position: "absolute",
678
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
679
+ left: $(token_list).offset().left,
680
+ width: $(token_list).outerWidth(),
681
+ 'z-index': settings.zindex
682
+ })
683
+ .show();
684
+ }
685
+
686
+ function show_dropdown_searching () {
687
+ if(settings.searchingText) {
688
+ dropdown.html("<p>"+settings.searchingText+"</p>");
689
+ show_dropdown();
690
+ }
691
+ }
692
+
693
+ function show_dropdown_hint () {
694
+ if(settings.hintText) {
695
+ dropdown.html("<p>"+settings.hintText+"</p>");
696
+ show_dropdown();
697
+ }
698
+ }
699
+
700
+ // Highlight the query part of the search term
701
+ function highlight_term(value, term) {
702
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
703
+ }
704
+
705
+ function find_value_and_highlight_term(template, value, term) {
706
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
707
+ }
708
+
709
+ // Populate the results dropdown with some results
710
+ function populate_dropdown (query, results) {
711
+ if(results && results.length) {
712
+ dropdown.empty();
713
+ var dropdown_ul = $("<ul>")
714
+ .appendTo(dropdown)
715
+ .mouseover(function (event) {
716
+ select_dropdown_item($(event.target).closest("li"));
717
+ })
718
+ .mousedown(function (event) {
719
+ add_token($(event.target).closest("li").data("tokeninput"));
720
+ hidden_input.change();
721
+ return false;
722
+ })
723
+ .hide();
724
+
725
+ $.each(results, function(index, value) {
726
+ var this_li = settings.resultsFormatter(value);
727
+
728
+ this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);
729
+
730
+ this_li = $(this_li).appendTo(dropdown_ul);
731
+
732
+ if(index % 2) {
733
+ this_li.addClass(settings.classes.dropdownItem);
734
+ } else {
735
+ this_li.addClass(settings.classes.dropdownItem2);
736
+ }
737
+
738
+ if(index === 0) {
739
+ select_dropdown_item(this_li);
740
+ }
741
+
742
+ $.data(this_li.get(0), "tokeninput", value);
743
+ });
744
+
745
+ show_dropdown();
746
+
747
+ if(settings.animateDropdown) {
748
+ dropdown_ul.slideDown("fast");
749
+ } else {
750
+ dropdown_ul.show();
751
+ }
752
+ } else {
753
+ if(settings.noResultsText) {
754
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
755
+ show_dropdown();
756
+ }
757
+ }
758
+ }
759
+
760
+ // Highlight an item in the results dropdown
761
+ function select_dropdown_item (item) {
762
+ if(item) {
763
+ if(selected_dropdown_item) {
764
+ deselect_dropdown_item($(selected_dropdown_item));
765
+ }
766
+
767
+ item.addClass(settings.classes.selectedDropdownItem);
768
+ selected_dropdown_item = item.get(0);
769
+ }
770
+ }
771
+
772
+ // Remove highlighting from an item in the results dropdown
773
+ function deselect_dropdown_item (item) {
774
+ item.removeClass(settings.classes.selectedDropdownItem);
775
+ selected_dropdown_item = null;
776
+ }
777
+
778
+ // Do a search and show the "searching" dropdown if the input is longer
779
+ // than settings.minChars
780
+ function do_search() {
781
+ var query = input_box.val();
782
+
783
+ if(query && query.length) {
784
+ if(selected_token) {
785
+ deselect_token($(selected_token), POSITION.AFTER);
786
+ }
787
+
788
+ if(query.length >= settings.minChars) {
789
+ show_dropdown_searching();
790
+ clearTimeout(timeout);
791
+
792
+ timeout = setTimeout(function(){
793
+ run_search(query);
794
+ }, settings.searchDelay);
795
+ } else {
796
+ hide_dropdown();
797
+ }
798
+ }
799
+ }
800
+
801
+ // Do the actual search
802
+ function run_search(query) {
803
+ var cache_key = query + computeURL();
804
+ var cached_results = cache.get(cache_key);
805
+ if(cached_results) {
806
+ populate_dropdown(query, cached_results);
807
+ } else {
808
+ // Are we doing an ajax search or local data search?
809
+ if(settings.url) {
810
+ var url = computeURL();
811
+ // Extract exisiting get params
812
+ var ajax_params = {};
813
+ ajax_params.data = {};
814
+ if(url.indexOf("?") > -1) {
815
+ var parts = url.split("?");
816
+ ajax_params.url = parts[0];
817
+
818
+ var param_array = parts[1].split("&");
819
+ $.each(param_array, function (index, value) {
820
+ var kv = value.split("=");
821
+ ajax_params.data[kv[0]] = kv[1];
822
+ });
823
+ } else {
824
+ ajax_params.url = url;
825
+ }
826
+
827
+ // Prepare the request
828
+ ajax_params.data[settings.queryParam] = query;
829
+ ajax_params.type = settings.method;
830
+ ajax_params.dataType = settings.contentType;
831
+ if(settings.crossDomain) {
832
+ ajax_params.dataType = "jsonp";
833
+ }
834
+
835
+ // Attach the success callback
836
+ ajax_params.success = function(results) {
837
+ if($.isFunction(settings.onResult)) {
838
+ results = settings.onResult.call(hidden_input, results);
839
+ }
840
+ cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);
841
+
842
+ // only populate the dropdown if the results are associated with the active search query
843
+ if(input_box.val() === query) {
844
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
845
+ }
846
+ };
847
+
848
+ // Make the request
849
+ $.ajax(ajax_params);
850
+ } else if(settings.local_data) {
851
+ // Do the search through local data
852
+ var results = $.grep(settings.local_data, function (row) {
853
+ return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
854
+ });
855
+
856
+ if($.isFunction(settings.onResult)) {
857
+ results = settings.onResult.call(hidden_input, results);
858
+ }
859
+ cache.add(cache_key, results);
860
+ populate_dropdown(query, results);
861
+ }
862
+ }
863
+ }
864
+
865
+ // compute the dynamic URL
866
+ function computeURL() {
867
+ var url = settings.url;
868
+ if(typeof settings.url == 'function') {
869
+ url = settings.url.call(settings);
870
+ }
871
+ return url;
872
+ }
873
+
874
+ // Bring browser focus to the specified object.
875
+ // Use of setTimeout is to get around an IE bug.
876
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
877
+ //
878
+ // obj: a jQuery object to focus()
879
+ function focus_with_timeout(obj) {
880
+ setTimeout(function() { obj.focus(); }, 50);
881
+ }
882
+
883
+ };
884
+
885
+ // Really basic cache for the results
886
+ $.TokenList.Cache = function (options) {
887
+ var settings = $.extend({
888
+ max_size: 500
889
+ }, options);
890
+
891
+ var data = {};
892
+ var size = 0;
893
+
894
+ var flush = function () {
895
+ data = {};
896
+ size = 0;
897
+ };
898
+
899
+ this.add = function (query, results) {
900
+ if(size > settings.max_size) {
901
+ flush();
902
+ }
903
+
904
+ if(!data[query]) {
905
+ size += 1;
906
+ }
907
+
908
+ data[query] = results;
909
+ };
910
+
911
+ this.get = function (query) {
912
+ return data[query];
913
+ };
914
+ };
915
+ }(jQuery));