conversations 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/.gitignore +8 -0
  2. data/CHANGELOG.md +7 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +146 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +55 -0
  7. data/Rakefile +32 -0
  8. data/TODO.md +4 -0
  9. data/app/assets/javascripts/conversations.js +5 -0
  10. data/app/assets/javascripts/jquery.tokeninput.js +860 -0
  11. data/app/assets/stylesheets/conversations.css +4 -0
  12. data/app/assets/stylesheets/token-input-facebook.css +122 -0
  13. data/app/assets/stylesheets/token-input-mac.css +204 -0
  14. data/app/assets/stylesheets/token-input.css +113 -0
  15. data/app/controllers/conversations/conversations_controller.rb +26 -0
  16. data/app/controllers/conversations/messages_controller.rb +17 -0
  17. data/app/controllers/conversations/user_conversations_controller.rb +28 -0
  18. data/app/models/conversations/conversation.rb +19 -0
  19. data/app/models/conversations/message.rb +11 -0
  20. data/app/models/conversations/user_conversation.rb +22 -0
  21. data/app/views/conversations/conversations/new.html.erb +19 -0
  22. data/app/views/conversations/user_conversations/index.html.erb +17 -0
  23. data/app/views/conversations/user_conversations/show.html.erb +35 -0
  24. data/config/routes.rb +12 -0
  25. data/conversations.gemspec +33 -0
  26. data/lib/conversations.rb +19 -0
  27. data/lib/conversations/engine.rb +29 -0
  28. data/lib/conversations/models/conversationalist.rb +22 -0
  29. data/lib/conversations/version.rb +3 -0
  30. data/lib/generators/conversations/conversations_generator.rb +20 -0
  31. data/lib/generators/conversations/templates/20120105153739_create_conversations.rb +9 -0
  32. data/lib/generators/conversations/templates/20120105153800_create_user_conversations.rb +12 -0
  33. data/lib/generators/conversations/templates/20120105153812_create_messages.rb +11 -0
  34. data/lib/tasks/conversations_tasks.rake +4 -0
  35. data/spec/dummy/Rakefile +7 -0
  36. data/spec/dummy/app/assets/javascripts/application.js +10 -0
  37. data/spec/dummy/app/assets/stylesheets/application.css +24 -0
  38. data/spec/dummy/app/controllers/application_controller.rb +13 -0
  39. data/spec/dummy/app/controllers/users_controller.rb +44 -0
  40. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  41. data/spec/dummy/app/mailers/.gitkeep +0 -0
  42. data/spec/dummy/app/models/.gitkeep +0 -0
  43. data/spec/dummy/app/models/user.rb +7 -0
  44. data/spec/dummy/app/views/layouts/application.html.erb +15 -0
  45. data/spec/dummy/app/views/users/_form.html.erb +5 -0
  46. data/spec/dummy/app/views/users/edit.html.erb +3 -0
  47. data/spec/dummy/app/views/users/index.html.erb +7 -0
  48. data/spec/dummy/app/views/users/new.html.erb +3 -0
  49. data/spec/dummy/app/views/users/show.html.erb +13 -0
  50. data/spec/dummy/config.ru +4 -0
  51. data/spec/dummy/config/application.rb +45 -0
  52. data/spec/dummy/config/boot.rb +10 -0
  53. data/spec/dummy/config/database.yml +25 -0
  54. data/spec/dummy/config/environment.rb +5 -0
  55. data/spec/dummy/config/environments/development.rb +37 -0
  56. data/spec/dummy/config/environments/production.rb +60 -0
  57. data/spec/dummy/config/environments/test.rb +39 -0
  58. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  59. data/spec/dummy/config/initializers/inflections.rb +10 -0
  60. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  61. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  62. data/spec/dummy/config/initializers/session_store.rb +8 -0
  63. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  64. data/spec/dummy/config/locales/en.yml +5 -0
  65. data/spec/dummy/config/routes.rb +69 -0
  66. data/spec/dummy/db/migrate/20120105152859_create_users.rb +9 -0
  67. data/spec/dummy/db/migrate/20120105153739_create_conversations.rb +9 -0
  68. data/spec/dummy/db/migrate/20120105153800_create_user_conversations.rb +12 -0
  69. data/spec/dummy/db/migrate/20120105153812_create_messages.rb +11 -0
  70. data/spec/dummy/db/schema.rb +45 -0
  71. data/spec/dummy/db/seeds.rb +12 -0
  72. data/spec/dummy/lib/assets/.gitkeep +0 -0
  73. data/spec/dummy/log/.gitkeep +0 -0
  74. data/spec/dummy/public/404.html +26 -0
  75. data/spec/dummy/public/422.html +26 -0
  76. data/spec/dummy/public/500.html +26 -0
  77. data/spec/dummy/public/favicon.ico +0 -0
  78. data/spec/dummy/script/rails +6 -0
  79. data/spec/factories.rb +29 -0
  80. data/spec/integration/conversations_controller_spec.rb +54 -0
  81. data/spec/integration/messages_controller_spec.rb +37 -0
  82. data/spec/integration/user_conversations_controller_spec.rb +51 -0
  83. data/spec/models/conversation_spec.rb +28 -0
  84. data/spec/models/message_spec.rb +23 -0
  85. data/spec/models/user_conversation_spec.rb +18 -0
  86. data/spec/spec_helper.rb +42 -0
  87. metadata +300 -0
@@ -0,0 +1,8 @@
1
+ .rvmrc
2
+ .bundle/
3
+ log/*.log
4
+ pkg/
5
+ spec/dummy/db/*.sqlite3
6
+ spec/dummy/log/*.log
7
+ spec/dummy/tmp/
8
+ coverage/
@@ -0,0 +1,7 @@
1
+ 0.0.1 (2012-01-14)
2
+ -
3
+ * Created Conversations modules
4
+ * Added Conversation model
5
+ * Added Message model
6
+ * Added UserConversation model and controller
7
+ * Added some sample data to dummy
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in conversations.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ # jquery-rails is used by the dummy application
9
+ gem "jquery-rails"
10
+
11
+ # Declare any dependencies that are still in development here instead of in
12
+ # your gemspec. These might include edge Rails or gems from your path or
13
+ # Git. Remember to move these dependencies to your gemspec before releasing
14
+ # your gem to rubygems.org.
15
+
16
+ # To use debugger
17
+ # gem 'ruby-debug19', :require => 'ruby-debug'
@@ -0,0 +1,146 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ conversations (0.0.1)
5
+ jquery-rails (~> 1.0.19)
6
+ rails (~> 3.1.3)
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ actionmailer (3.1.3)
12
+ actionpack (= 3.1.3)
13
+ mail (~> 2.3.0)
14
+ actionpack (3.1.3)
15
+ activemodel (= 3.1.3)
16
+ activesupport (= 3.1.3)
17
+ builder (~> 3.0.0)
18
+ erubis (~> 2.7.0)
19
+ i18n (~> 0.6)
20
+ rack (~> 1.3.5)
21
+ rack-cache (~> 1.1)
22
+ rack-mount (~> 0.8.2)
23
+ rack-test (~> 0.6.1)
24
+ sprockets (~> 2.0.3)
25
+ activemodel (3.1.3)
26
+ activesupport (= 3.1.3)
27
+ builder (~> 3.0.0)
28
+ i18n (~> 0.6)
29
+ activerecord (3.1.3)
30
+ activemodel (= 3.1.3)
31
+ activesupport (= 3.1.3)
32
+ arel (~> 2.2.1)
33
+ tzinfo (~> 0.3.29)
34
+ activeresource (3.1.3)
35
+ activemodel (= 3.1.3)
36
+ activesupport (= 3.1.3)
37
+ activesupport (3.1.3)
38
+ multi_json (~> 1.0)
39
+ arel (2.2.1)
40
+ builder (3.0.0)
41
+ capybara (1.1.2)
42
+ mime-types (>= 1.16)
43
+ nokogiri (>= 1.3.3)
44
+ rack (>= 1.0.0)
45
+ rack-test (>= 0.5.4)
46
+ selenium-webdriver (~> 2.0)
47
+ xpath (~> 0.1.4)
48
+ childprocess (0.2.8)
49
+ ffi (~> 1.0.6)
50
+ diff-lcs (1.1.3)
51
+ erubis (2.7.0)
52
+ factory_girl (2.3.2)
53
+ activesupport
54
+ factory_girl_rails (1.4.0)
55
+ factory_girl (~> 2.3.0)
56
+ railties (>= 3.0.0)
57
+ ffi (1.0.11)
58
+ hike (1.2.1)
59
+ i18n (0.6.0)
60
+ jquery-rails (1.0.19)
61
+ railties (~> 3.0)
62
+ thor (~> 0.14)
63
+ json (1.6.4)
64
+ mail (2.3.0)
65
+ i18n (>= 0.4.0)
66
+ mime-types (~> 1.16)
67
+ treetop (~> 1.4.8)
68
+ mime-types (1.17.2)
69
+ multi_json (1.0.4)
70
+ nokogiri (1.5.0)
71
+ polyglot (0.3.3)
72
+ rack (1.3.6)
73
+ rack-cache (1.1)
74
+ rack (>= 0.4)
75
+ rack-mount (0.8.3)
76
+ rack (>= 1.0.0)
77
+ rack-ssl (1.3.2)
78
+ rack
79
+ rack-test (0.6.1)
80
+ rack (>= 1.0)
81
+ rails (3.1.3)
82
+ actionmailer (= 3.1.3)
83
+ actionpack (= 3.1.3)
84
+ activerecord (= 3.1.3)
85
+ activeresource (= 3.1.3)
86
+ activesupport (= 3.1.3)
87
+ bundler (~> 1.0)
88
+ railties (= 3.1.3)
89
+ railties (3.1.3)
90
+ actionpack (= 3.1.3)
91
+ activesupport (= 3.1.3)
92
+ rack-ssl (~> 1.3.2)
93
+ rake (>= 0.8.7)
94
+ rdoc (~> 3.4)
95
+ thor (~> 0.14.6)
96
+ rake (0.9.2.2)
97
+ rdoc (3.12)
98
+ json (~> 1.4)
99
+ rspec (2.8.0)
100
+ rspec-core (~> 2.8.0)
101
+ rspec-expectations (~> 2.8.0)
102
+ rspec-mocks (~> 2.8.0)
103
+ rspec-core (2.8.0)
104
+ rspec-expectations (2.8.0)
105
+ diff-lcs (~> 1.1.2)
106
+ rspec-mocks (2.8.0)
107
+ rspec-rails (2.8.1)
108
+ actionpack (>= 3.0)
109
+ activesupport (>= 3.0)
110
+ railties (>= 3.0)
111
+ rspec (~> 2.8.0)
112
+ rubyzip (0.9.5)
113
+ selenium-webdriver (2.16.0)
114
+ childprocess (>= 0.2.5)
115
+ ffi (~> 1.0.9)
116
+ multi_json (~> 1.0.4)
117
+ rubyzip
118
+ simplecov (0.5.4)
119
+ multi_json (~> 1.0.3)
120
+ simplecov-html (~> 0.5.3)
121
+ simplecov-html (0.5.3)
122
+ sprockets (2.0.3)
123
+ hike (~> 1.2)
124
+ rack (~> 1.0)
125
+ tilt (~> 1.1, != 1.3.0)
126
+ sqlite3 (1.3.5)
127
+ thor (0.14.6)
128
+ tilt (1.3.3)
129
+ treetop (1.4.10)
130
+ polyglot
131
+ polyglot (>= 0.3.1)
132
+ tzinfo (0.3.31)
133
+ xpath (0.1.4)
134
+ nokogiri (~> 1.3)
135
+
136
+ PLATFORMS
137
+ ruby
138
+
139
+ DEPENDENCIES
140
+ capybara (~> 1.1)
141
+ conversations!
142
+ factory_girl_rails (~> 1.4)
143
+ jquery-rails
144
+ rspec-rails (~> 2.8)
145
+ simplecov (~> 0.5)
146
+ sqlite3 (~> 1.3)
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ Conversations
2
+ =
3
+
4
+ This project rocks big time and uses MIT-LICENSE.
5
+
6
+ Installation
7
+ -
8
+
9
+ 1. Add
10
+ `gem "conversations"`
11
+ to your Gemfile.
12
+
13
+ * Run
14
+ `bundle install`.
15
+
16
+ * Add
17
+ `//= require conversations`
18
+ to
19
+ `app/assets/javascripts/application.js`
20
+ file.
21
+
22
+ * Add
23
+ `*= require conversations`
24
+ to
25
+ `app/assets/stylesheets/application.css`
26
+ file.
27
+
28
+ * Run
29
+ `rails g conversations`.
30
+
31
+ * Run
32
+ `rake db:migrate`.
33
+
34
+ * Add
35
+ `has_conversations`
36
+ to User model
37
+
38
+ * Add links to the conversations anywhere you want
39
+ `<%= link_to 'Messages', user_conversations_path(current_user) %>`
40
+
41
+ * Restart server if running
42
+
43
+ Notice
44
+ -
45
+
46
+ In order to have it work, you need to provide current user like this:
47
+ `app/controllers/application_controller.rb`
48
+
49
+ def current_user
50
+ User.find(session[:user_id])
51
+ end
52
+
53
+ helper_method :current_user
54
+
55
+ which is provided in Authentication Gems like devise already.
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'Conversations'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rspec/core'
28
+ require 'rspec/core/rake_task'
29
+
30
+ RSpec::Core::RakeTask.new(:spec)
31
+
32
+ task :default => :spec
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ Todo
2
+ =
3
+
4
+ * Fix rake install migrations
@@ -0,0 +1,5 @@
1
+ //= require_tree .
2
+
3
+ $(function () {
4
+ $('#conversations_conversation_to_tokens').tokenInput('/users.json', { crossDomain: false });
5
+ });
@@ -0,0 +1,860 @@
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
+ contentType: "json",
17
+ queryParam: "q",
18
+ searchDelay: 300,
19
+ minChars: 1,
20
+ propertyToSearch: "name",
21
+ jsonContainer: null,
22
+
23
+ // Display settings
24
+ hintText: "Type in a search term",
25
+ noResultsText: "No results",
26
+ searchingText: "Searching...",
27
+ deleteText: "&times;",
28
+ animateDropdown: true,
29
+
30
+ // Tokenization settings
31
+ tokenLimit: null,
32
+ tokenDelimiter: ",",
33
+ preventDuplicates: false,
34
+
35
+ // Output settings
36
+ tokenValue: "id",
37
+
38
+ // Prepopulation settings
39
+ prePopulate: null,
40
+ processPrePopulate: false,
41
+
42
+ // Manipulation settings
43
+ idPrefix: "token-input-",
44
+
45
+ // Formatters
46
+ resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
47
+ tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },
48
+
49
+ // Callbacks
50
+ onResult: null,
51
+ onAdd: null,
52
+ onDelete: null,
53
+ onReady: null
54
+ };
55
+
56
+ // Default classes to use when theming
57
+ var DEFAULT_CLASSES = {
58
+ tokenList: "token-input-list",
59
+ token: "token-input-token",
60
+ tokenDelete: "token-input-delete-token",
61
+ selectedToken: "token-input-selected-token",
62
+ highlightedToken: "token-input-highlighted-token",
63
+ dropdown: "token-input-dropdown",
64
+ dropdownItem: "token-input-dropdown-item",
65
+ dropdownItem2: "token-input-dropdown-item2",
66
+ selectedDropdownItem: "token-input-selected-dropdown-item",
67
+ inputToken: "token-input-input-token"
68
+ };
69
+
70
+ // Input box position "enum"
71
+ var POSITION = {
72
+ BEFORE: 0,
73
+ AFTER: 1,
74
+ END: 2
75
+ };
76
+
77
+ // Keys "enum"
78
+ var KEY = {
79
+ BACKSPACE: 8,
80
+ TAB: 9,
81
+ ENTER: 13,
82
+ ESCAPE: 27,
83
+ SPACE: 32,
84
+ PAGE_UP: 33,
85
+ PAGE_DOWN: 34,
86
+ END: 35,
87
+ HOME: 36,
88
+ LEFT: 37,
89
+ UP: 38,
90
+ RIGHT: 39,
91
+ DOWN: 40,
92
+ NUMPAD_ENTER: 108,
93
+ COMMA: 188
94
+ };
95
+
96
+ // Additional public (exposed) methods
97
+ var methods = {
98
+ init: function(url_or_data_or_function, options) {
99
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
100
+
101
+ return this.each(function () {
102
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
103
+ });
104
+ },
105
+ clear: function() {
106
+ this.data("tokenInputObject").clear();
107
+ return this;
108
+ },
109
+ add: function(item) {
110
+ this.data("tokenInputObject").add(item);
111
+ return this;
112
+ },
113
+ remove: function(item) {
114
+ this.data("tokenInputObject").remove(item);
115
+ return this;
116
+ },
117
+ get: function() {
118
+ return this.data("tokenInputObject").getTokens();
119
+ }
120
+ }
121
+
122
+ // Expose the .tokenInput function to jQuery as a plugin
123
+ $.fn.tokenInput = function (method) {
124
+ // Method calling and initialization logic
125
+ if(methods[method]) {
126
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
127
+ } else {
128
+ return methods.init.apply(this, arguments);
129
+ }
130
+ };
131
+
132
+ // TokenList class for each input
133
+ $.TokenList = function (input, url_or_data, settings) {
134
+ //
135
+ // Initialization
136
+ //
137
+
138
+ // Configure the data source
139
+ if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
140
+ // Set the url to query against
141
+ settings.url = url_or_data;
142
+
143
+ // If the URL is a function, evaluate it here to do our initalization work
144
+ var url = computeURL();
145
+
146
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
147
+ if(settings.crossDomain === undefined) {
148
+ if(url.indexOf("://") === -1) {
149
+ settings.crossDomain = false;
150
+ } else {
151
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
152
+ }
153
+ }
154
+ } else if(typeof(url_or_data) === "object") {
155
+ // Set the local data to search through
156
+ settings.local_data = url_or_data;
157
+ }
158
+
159
+ // Build class names
160
+ if(settings.classes) {
161
+ // Use custom class names
162
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
163
+ } else if(settings.theme) {
164
+ // Use theme-suffixed default class names
165
+ settings.classes = {};
166
+ $.each(DEFAULT_CLASSES, function(key, value) {
167
+ settings.classes[key] = value + "-" + settings.theme;
168
+ });
169
+ } else {
170
+ settings.classes = DEFAULT_CLASSES;
171
+ }
172
+
173
+
174
+ // Save the tokens
175
+ var saved_tokens = [];
176
+
177
+ // Keep track of the number of tokens in the list
178
+ var token_count = 0;
179
+
180
+ // Basic cache to save on db hits
181
+ var cache = new $.TokenList.Cache();
182
+
183
+ // Keep track of the timeout, old vals
184
+ var timeout;
185
+ var input_val;
186
+
187
+ // Create a new text input an attach keyup events
188
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
189
+ .css({
190
+ outline: "none"
191
+ })
192
+ .attr("id", settings.idPrefix + input.id)
193
+ .focus(function () {
194
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
195
+ show_dropdown_hint();
196
+ }
197
+ })
198
+ .blur(function () {
199
+ hide_dropdown();
200
+ $(this).val("");
201
+ })
202
+ .bind("keyup keydown blur update", resize_input)
203
+ .keydown(function (event) {
204
+ var previous_token;
205
+ var next_token;
206
+
207
+ switch(event.keyCode) {
208
+ case KEY.LEFT:
209
+ case KEY.RIGHT:
210
+ case KEY.UP:
211
+ case KEY.DOWN:
212
+ if(!$(this).val()) {
213
+ previous_token = input_token.prev();
214
+ next_token = input_token.next();
215
+
216
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
217
+ // Check if there is a previous/next token and it is selected
218
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
219
+ deselect_token($(selected_token), POSITION.BEFORE);
220
+ } else {
221
+ deselect_token($(selected_token), POSITION.AFTER);
222
+ }
223
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
224
+ // We are moving left, select the previous token if it exists
225
+ select_token($(previous_token.get(0)));
226
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
227
+ // We are moving right, select the next token if it exists
228
+ select_token($(next_token.get(0)));
229
+ }
230
+ } else {
231
+ var dropdown_item = null;
232
+
233
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
234
+ dropdown_item = $(selected_dropdown_item).next();
235
+ } else {
236
+ dropdown_item = $(selected_dropdown_item).prev();
237
+ }
238
+
239
+ if(dropdown_item.length) {
240
+ select_dropdown_item(dropdown_item);
241
+ }
242
+ return false;
243
+ }
244
+ break;
245
+
246
+ case KEY.BACKSPACE:
247
+ previous_token = input_token.prev();
248
+
249
+ if(!$(this).val().length) {
250
+ if(selected_token) {
251
+ delete_token($(selected_token));
252
+ hidden_input.change();
253
+ } else if(previous_token.length) {
254
+ select_token($(previous_token.get(0)));
255
+ }
256
+
257
+ return false;
258
+ } else if($(this).val().length === 1) {
259
+ hide_dropdown();
260
+ } else {
261
+ // set a timeout just long enough to let this function finish.
262
+ setTimeout(function(){do_search();}, 5);
263
+ }
264
+ break;
265
+
266
+ case KEY.TAB:
267
+ case KEY.ENTER:
268
+ case KEY.NUMPAD_ENTER:
269
+ case KEY.COMMA:
270
+ if(selected_dropdown_item) {
271
+ add_token($(selected_dropdown_item).data("tokeninput"));
272
+ hidden_input.change();
273
+ return false;
274
+ }
275
+ break;
276
+
277
+ case KEY.ESCAPE:
278
+ hide_dropdown();
279
+ return true;
280
+
281
+ default:
282
+ if(String.fromCharCode(event.which)) {
283
+ // set a timeout just long enough to let this function finish.
284
+ setTimeout(function(){do_search();}, 5);
285
+ }
286
+ break;
287
+ }
288
+ });
289
+
290
+ // Keep a reference to the original input box
291
+ var hidden_input = $(input)
292
+ .hide()
293
+ .val("")
294
+ .focus(function () {
295
+ input_box.focus();
296
+ })
297
+ .blur(function () {
298
+ input_box.blur();
299
+ });
300
+
301
+ // Keep a reference to the selected token and dropdown item
302
+ var selected_token = null;
303
+ var selected_token_index = 0;
304
+ var selected_dropdown_item = null;
305
+
306
+ // The list to store the token items in
307
+ var token_list = $("<ul />")
308
+ .addClass(settings.classes.tokenList)
309
+ .click(function (event) {
310
+ var li = $(event.target).closest("li");
311
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
312
+ toggle_select_token(li);
313
+ } else {
314
+ // Deselect selected token
315
+ if(selected_token) {
316
+ deselect_token($(selected_token), POSITION.END);
317
+ }
318
+
319
+ // Focus input box
320
+ input_box.focus();
321
+ }
322
+ })
323
+ .mouseover(function (event) {
324
+ var li = $(event.target).closest("li");
325
+ if(li && selected_token !== this) {
326
+ li.addClass(settings.classes.highlightedToken);
327
+ }
328
+ })
329
+ .mouseout(function (event) {
330
+ var li = $(event.target).closest("li");
331
+ if(li && selected_token !== this) {
332
+ li.removeClass(settings.classes.highlightedToken);
333
+ }
334
+ })
335
+ .insertBefore(hidden_input);
336
+
337
+ // The token holding the input box
338
+ var input_token = $("<li />")
339
+ .addClass(settings.classes.inputToken)
340
+ .appendTo(token_list)
341
+ .append(input_box);
342
+
343
+ // The list to store the dropdown items in
344
+ var dropdown = $("<div>")
345
+ .addClass(settings.classes.dropdown)
346
+ .appendTo("body")
347
+ .hide();
348
+
349
+ // Magic element to help us resize the text input
350
+ var input_resizer = $("<tester/>")
351
+ .insertAfter(input_box)
352
+ .css({
353
+ position: "absolute",
354
+ top: -9999,
355
+ left: -9999,
356
+ width: "auto",
357
+ fontSize: input_box.css("fontSize"),
358
+ fontFamily: input_box.css("fontFamily"),
359
+ fontWeight: input_box.css("fontWeight"),
360
+ letterSpacing: input_box.css("letterSpacing"),
361
+ whiteSpace: "nowrap"
362
+ });
363
+
364
+ // Pre-populate list if items exist
365
+ hidden_input.val("");
366
+ var li_data = settings.prePopulate || hidden_input.data("pre");
367
+ if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
368
+ li_data = settings.onResult.call(hidden_input, li_data);
369
+ }
370
+ if(li_data && li_data.length) {
371
+ $.each(li_data, function (index, value) {
372
+ insert_token(value);
373
+ checkTokenLimit();
374
+ });
375
+ }
376
+
377
+ // Initialization is done
378
+ if($.isFunction(settings.onReady)) {
379
+ settings.onReady.call();
380
+ }
381
+
382
+ //
383
+ // Public functions
384
+ //
385
+
386
+ this.clear = function() {
387
+ token_list.children("li").each(function() {
388
+ if ($(this).children("input").length === 0) {
389
+ delete_token($(this));
390
+ }
391
+ });
392
+ }
393
+
394
+ this.add = function(item) {
395
+ add_token(item);
396
+ }
397
+
398
+ this.remove = function(item) {
399
+ token_list.children("li").each(function() {
400
+ if ($(this).children("input").length === 0) {
401
+ var currToken = $(this).data("tokeninput");
402
+ var match = true;
403
+ for (var prop in item) {
404
+ if (item[prop] !== currToken[prop]) {
405
+ match = false;
406
+ break;
407
+ }
408
+ }
409
+ if (match) {
410
+ delete_token($(this));
411
+ }
412
+ }
413
+ });
414
+ }
415
+
416
+ this.getTokens = function() {
417
+ return saved_tokens;
418
+ }
419
+
420
+ //
421
+ // Private functions
422
+ //
423
+
424
+ function checkTokenLimit() {
425
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
426
+ input_box.hide();
427
+ hide_dropdown();
428
+ return;
429
+ }
430
+ }
431
+
432
+ function resize_input() {
433
+ if(input_val === (input_val = input_box.val())) {return;}
434
+
435
+ // Enter new content into resizer and resize input accordingly
436
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
437
+ input_resizer.html(escaped);
438
+ input_box.width(input_resizer.width() + 30);
439
+ }
440
+
441
+ function is_printable_character(keycode) {
442
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
443
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
444
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
445
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
446
+ }
447
+
448
+ // Inner function to a token to the list
449
+ function insert_token(item) {
450
+ var this_token = settings.tokenFormatter(item);
451
+ this_token = $(this_token)
452
+ .addClass(settings.classes.token)
453
+ .insertBefore(input_token);
454
+
455
+ // The 'delete token' button
456
+ $("<span>" + settings.deleteText + "</span>")
457
+ .addClass(settings.classes.tokenDelete)
458
+ .appendTo(this_token)
459
+ .click(function () {
460
+ delete_token($(this).parent());
461
+ hidden_input.change();
462
+ return false;
463
+ });
464
+
465
+ // Store data on the token
466
+ var token_data = {"id": item.id};
467
+ token_data[settings.propertyToSearch] = item[settings.propertyToSearch];
468
+ $.data(this_token.get(0), "tokeninput", item);
469
+
470
+ // Save this token for duplicate checking
471
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
472
+ selected_token_index++;
473
+
474
+ // Update the hidden input
475
+ update_hidden_input(saved_tokens, hidden_input);
476
+
477
+ token_count += 1;
478
+
479
+ // Check the token limit
480
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
481
+ input_box.hide();
482
+ hide_dropdown();
483
+ }
484
+
485
+ return this_token;
486
+ }
487
+
488
+ // Add a token to the token list based on user input
489
+ function add_token (item) {
490
+ var callback = settings.onAdd;
491
+
492
+ // See if the token already exists and select it if we don't want duplicates
493
+ if(token_count > 0 && settings.preventDuplicates) {
494
+ var found_existing_token = null;
495
+ token_list.children().each(function () {
496
+ var existing_token = $(this);
497
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
498
+ if(existing_data && existing_data.id === item.id) {
499
+ found_existing_token = existing_token;
500
+ return false;
501
+ }
502
+ });
503
+
504
+ if(found_existing_token) {
505
+ select_token(found_existing_token);
506
+ input_token.insertAfter(found_existing_token);
507
+ input_box.focus();
508
+ return;
509
+ }
510
+ }
511
+
512
+ // Insert the new tokens
513
+ if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
514
+ insert_token(item);
515
+ checkTokenLimit();
516
+ }
517
+
518
+ // Clear input box
519
+ input_box.val("");
520
+
521
+ // Don't show the help dropdown, they've got the idea
522
+ hide_dropdown();
523
+
524
+ // Execute the onAdd callback if defined
525
+ if($.isFunction(callback)) {
526
+ callback.call(hidden_input,item);
527
+ }
528
+ }
529
+
530
+ // Select a token in the token list
531
+ function select_token (token) {
532
+ token.addClass(settings.classes.selectedToken);
533
+ selected_token = token.get(0);
534
+
535
+ // Hide input box
536
+ input_box.val("");
537
+
538
+ // Hide dropdown if it is visible (eg if we clicked to select token)
539
+ hide_dropdown();
540
+ }
541
+
542
+ // Deselect a token in the token list
543
+ function deselect_token (token, position) {
544
+ token.removeClass(settings.classes.selectedToken);
545
+ selected_token = null;
546
+
547
+ if(position === POSITION.BEFORE) {
548
+ input_token.insertBefore(token);
549
+ selected_token_index--;
550
+ } else if(position === POSITION.AFTER) {
551
+ input_token.insertAfter(token);
552
+ selected_token_index++;
553
+ } else {
554
+ input_token.appendTo(token_list);
555
+ selected_token_index = token_count;
556
+ }
557
+
558
+ // Show the input box and give it focus again
559
+ input_box.focus();
560
+ }
561
+
562
+ // Toggle selection of a token in the token list
563
+ function toggle_select_token(token) {
564
+ var previous_selected_token = selected_token;
565
+
566
+ if(selected_token) {
567
+ deselect_token($(selected_token), POSITION.END);
568
+ }
569
+
570
+ if(previous_selected_token === token.get(0)) {
571
+ deselect_token(token, POSITION.END);
572
+ } else {
573
+ select_token(token);
574
+ }
575
+ }
576
+
577
+ // Delete a token from the token list
578
+ function delete_token (token) {
579
+ // Remove the id from the saved list
580
+ var token_data = $.data(token.get(0), "tokeninput");
581
+ var callback = settings.onDelete;
582
+
583
+ var index = token.prevAll().length;
584
+ if(index > selected_token_index) index--;
585
+
586
+ // Delete the token
587
+ token.remove();
588
+ selected_token = null;
589
+
590
+ // Show the input box and give it focus again
591
+ input_box.focus();
592
+
593
+ // Remove this token from the saved list
594
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
595
+ if(index < selected_token_index) selected_token_index--;
596
+
597
+ // Update the hidden input
598
+ update_hidden_input(saved_tokens, hidden_input);
599
+
600
+ token_count -= 1;
601
+
602
+ if(settings.tokenLimit !== null) {
603
+ input_box
604
+ .show()
605
+ .val("")
606
+ .focus();
607
+ }
608
+
609
+ // Execute the onDelete callback if defined
610
+ if($.isFunction(callback)) {
611
+ callback.call(hidden_input,token_data);
612
+ }
613
+ }
614
+
615
+ // Update the hidden input box value
616
+ function update_hidden_input(saved_tokens, hidden_input) {
617
+ var token_values = $.map(saved_tokens, function (el) {
618
+ return el[settings.tokenValue];
619
+ });
620
+ hidden_input.val(token_values.join(settings.tokenDelimiter));
621
+
622
+ }
623
+
624
+ // Hide and clear the results dropdown
625
+ function hide_dropdown () {
626
+ dropdown.hide().empty();
627
+ selected_dropdown_item = null;
628
+ }
629
+
630
+ function show_dropdown() {
631
+ dropdown
632
+ .css({
633
+ position: "absolute",
634
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
635
+ left: $(token_list).offset().left,
636
+ zindex: 999
637
+ })
638
+ .show();
639
+ }
640
+
641
+ function show_dropdown_searching () {
642
+ if(settings.searchingText) {
643
+ dropdown.html("<p>"+settings.searchingText+"</p>");
644
+ show_dropdown();
645
+ }
646
+ }
647
+
648
+ function show_dropdown_hint () {
649
+ if(settings.hintText) {
650
+ dropdown.html("<p>"+settings.hintText+"</p>");
651
+ show_dropdown();
652
+ }
653
+ }
654
+
655
+ // Highlight the query part of the search term
656
+ function highlight_term(value, term) {
657
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
658
+ }
659
+
660
+ function find_value_and_highlight_term(template, value, term) {
661
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
662
+ }
663
+
664
+ // Populate the results dropdown with some results
665
+ function populate_dropdown (query, results) {
666
+ if(results && results.length) {
667
+ dropdown.empty();
668
+ var dropdown_ul = $("<ul>")
669
+ .appendTo(dropdown)
670
+ .mouseover(function (event) {
671
+ select_dropdown_item($(event.target).closest("li"));
672
+ })
673
+ .mousedown(function (event) {
674
+ add_token($(event.target).closest("li").data("tokeninput"));
675
+ hidden_input.change();
676
+ return false;
677
+ })
678
+ .hide();
679
+
680
+ $.each(results, function(index, value) {
681
+ var this_li = settings.resultsFormatter(value);
682
+
683
+ this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);
684
+
685
+ this_li = $(this_li).appendTo(dropdown_ul);
686
+
687
+ if(index % 2) {
688
+ this_li.addClass(settings.classes.dropdownItem);
689
+ } else {
690
+ this_li.addClass(settings.classes.dropdownItem2);
691
+ }
692
+
693
+ if(index === 0) {
694
+ select_dropdown_item(this_li);
695
+ }
696
+
697
+ $.data(this_li.get(0), "tokeninput", value);
698
+ });
699
+
700
+ show_dropdown();
701
+
702
+ if(settings.animateDropdown) {
703
+ dropdown_ul.slideDown("fast");
704
+ } else {
705
+ dropdown_ul.show();
706
+ }
707
+ } else {
708
+ if(settings.noResultsText) {
709
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
710
+ show_dropdown();
711
+ }
712
+ }
713
+ }
714
+
715
+ // Highlight an item in the results dropdown
716
+ function select_dropdown_item (item) {
717
+ if(item) {
718
+ if(selected_dropdown_item) {
719
+ deselect_dropdown_item($(selected_dropdown_item));
720
+ }
721
+
722
+ item.addClass(settings.classes.selectedDropdownItem);
723
+ selected_dropdown_item = item.get(0);
724
+ }
725
+ }
726
+
727
+ // Remove highlighting from an item in the results dropdown
728
+ function deselect_dropdown_item (item) {
729
+ item.removeClass(settings.classes.selectedDropdownItem);
730
+ selected_dropdown_item = null;
731
+ }
732
+
733
+ // Do a search and show the "searching" dropdown if the input is longer
734
+ // than settings.minChars
735
+ function do_search() {
736
+ var query = input_box.val().toLowerCase();
737
+
738
+ if(query && query.length) {
739
+ if(selected_token) {
740
+ deselect_token($(selected_token), POSITION.AFTER);
741
+ }
742
+
743
+ if(query.length >= settings.minChars) {
744
+ show_dropdown_searching();
745
+ clearTimeout(timeout);
746
+
747
+ timeout = setTimeout(function(){
748
+ run_search(query);
749
+ }, settings.searchDelay);
750
+ } else {
751
+ hide_dropdown();
752
+ }
753
+ }
754
+ }
755
+
756
+ // Do the actual search
757
+ function run_search(query) {
758
+ var cache_key = query + computeURL();
759
+ var cached_results = cache.get(cache_key);
760
+ if(cached_results) {
761
+ populate_dropdown(query, cached_results);
762
+ } else {
763
+ // Are we doing an ajax search or local data search?
764
+ if(settings.url) {
765
+ var url = computeURL();
766
+ // Extract exisiting get params
767
+ var ajax_params = {};
768
+ ajax_params.data = {};
769
+ if(url.indexOf("?") > -1) {
770
+ var parts = url.split("?");
771
+ ajax_params.url = parts[0];
772
+
773
+ var param_array = parts[1].split("&");
774
+ $.each(param_array, function (index, value) {
775
+ var kv = value.split("=");
776
+ ajax_params.data[kv[0]] = kv[1];
777
+ });
778
+ } else {
779
+ ajax_params.url = url;
780
+ }
781
+
782
+ // Prepare the request
783
+ ajax_params.data[settings.queryParam] = query;
784
+ ajax_params.type = settings.method;
785
+ ajax_params.dataType = settings.contentType;
786
+ if(settings.crossDomain) {
787
+ ajax_params.dataType = "jsonp";
788
+ }
789
+
790
+ // Attach the success callback
791
+ ajax_params.success = function(results) {
792
+ if($.isFunction(settings.onResult)) {
793
+ results = settings.onResult.call(hidden_input, results);
794
+ }
795
+ cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);
796
+
797
+ // only populate the dropdown if the results are associated with the active search query
798
+ if(input_box.val().toLowerCase() === query) {
799
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
800
+ }
801
+ };
802
+
803
+ // Make the request
804
+ $.ajax(ajax_params);
805
+ } else if(settings.local_data) {
806
+ // Do the search through local data
807
+ var results = $.grep(settings.local_data, function (row) {
808
+ return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
809
+ });
810
+
811
+ if($.isFunction(settings.onResult)) {
812
+ results = settings.onResult.call(hidden_input, results);
813
+ }
814
+ cache.add(cache_key, results);
815
+ populate_dropdown(query, results);
816
+ }
817
+ }
818
+ }
819
+
820
+ // compute the dynamic URL
821
+ function computeURL() {
822
+ var url = settings.url;
823
+ if(typeof settings.url == 'function') {
824
+ url = settings.url.call();
825
+ }
826
+ return url;
827
+ }
828
+ };
829
+
830
+ // Really basic cache for the results
831
+ $.TokenList.Cache = function (options) {
832
+ var settings = $.extend({
833
+ max_size: 500
834
+ }, options);
835
+
836
+ var data = {};
837
+ var size = 0;
838
+
839
+ var flush = function () {
840
+ data = {};
841
+ size = 0;
842
+ };
843
+
844
+ this.add = function (query, results) {
845
+ if(size > settings.max_size) {
846
+ flush();
847
+ }
848
+
849
+ if(!data[query]) {
850
+ size += 1;
851
+ }
852
+
853
+ data[query] = results;
854
+ };
855
+
856
+ this.get = function (query) {
857
+ return data[query];
858
+ };
859
+ };
860
+ }(jQuery));