voluntary 0.0.3 → 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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));