has_messages_generators 0.0.1

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 (23) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.mdown +43 -0
  3. data/lib/generators/base.rb +179 -0
  4. data/lib/generators/has_messages/install/USAGE +10 -0
  5. data/lib/generators/has_messages/install/install_generator.rb +126 -0
  6. data/lib/generators/has_messages/install/templates/README +26 -0
  7. data/lib/generators/has_messages/install/templates/assets/javascripts/jquery.tokeninput.js +718 -0
  8. data/lib/generators/has_messages/install/templates/assets/javascripts/messages.js +19 -0
  9. data/lib/generators/has_messages/install/templates/assets/stylesheets/messages.css +107 -0
  10. data/lib/generators/has_messages/install/templates/assets/stylesheets/token-input-facebook.css +122 -0
  11. data/lib/generators/has_messages/install/templates/controllers/messages_controller.rb +111 -0
  12. data/lib/generators/has_messages/install/templates/helpers/messages_helper.rb +2 -0
  13. data/lib/generators/has_messages/install/templates/lib/has_messages.rb +67 -0
  14. data/lib/generators/has_messages/install/templates/models/create_messages.rb +24 -0
  15. data/lib/generators/has_messages/install/templates/models/message.rb +67 -0
  16. data/lib/generators/has_messages/install/templates/views/_head.html.erb +20 -0
  17. data/lib/generators/has_messages/install/templates/views/_messages.html.erb +56 -0
  18. data/lib/generators/has_messages/install/templates/views/_tabs_panel.html.erb +11 -0
  19. data/lib/generators/has_messages/install/templates/views/index.html.erb +10 -0
  20. data/lib/generators/has_messages/install/templates/views/index.js.erb +1 -0
  21. data/lib/generators/has_messages/install/templates/views/new.html.erb +26 -0
  22. data/lib/generators/has_messages/install/templates/views/show.html.erb +35 -0
  23. metadata +78 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2011 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.
data/README.mdown ADDED
@@ -0,0 +1,43 @@
1
+ ## Gem Dependency
2
+ - `devise` (works with other authentication gems, but needs some modifications)
3
+ - `jquery-rails`
4
+ - `kaminari`
5
+ - `ancestry`
6
+
7
+ # Installation Instructions
8
+
9
+ This generator will required you to have Authentication gems such as Devise, to use this
10
+ you must include `devise` and `has_messages` in your Gemfile.
11
+
12
+ gem "devise"
13
+ gem "has_messages"
14
+
15
+ After that setup the `devise` properly.
16
+
17
+ rails g devise:install
18
+ rails g devise user # => user model name example.
19
+
20
+ After you configure `devise` properly now install `has_messages` with the given USER MODEL NAME
21
+
22
+ rails g has_messages:install user
23
+
24
+ this will generate all the necessary files and code into your Rails app.
25
+
26
+ At the end of the installation just follow the instructions to modify your layout file.
27
+
28
+ If you wish to add some links for `devise` and `has_messages` path, just add this code into your layout file.
29
+
30
+ <% if user_signed_in? %>
31
+ <%= link_to "inbox(#{current_user.inbox(:opened => false).count})", messages_path(:inbox), :id => "inbox-link" %> |
32
+ Signed in as <%= current_user.email %> Not You?
33
+ <%= link_to 'Sign out', destroy_user_session_path, :method => :delete %>
34
+ <% else %>
35
+ <%= link_to 'Sign up', new_user_registration_path %> or <%= link_to 'Sign in', new_user_session_path %>
36
+ <% end %>
37
+
38
+ Run the migration and start the app!
39
+
40
+ ## Found a bug?
41
+
42
+ This is stil under development mode, if you are having some problem with `has_messages`,
43
+ feel free to submit an issue here. http://github.com/fajrif/has_messages/issues
@@ -0,0 +1,179 @@
1
+ require 'rails/generators/base'
2
+ require 'bundler'
3
+ require 'bundler/dsl'
4
+
5
+ module HasMessages
6
+ module Generators
7
+ class Base < Rails::Generators::Base #:nodoc:
8
+
9
+ def self.source_root
10
+ @_has_messages_source_root = File.expand_path(File.join(File.dirname(__FILE__), 'has_messages', generator_name, 'templates'))
11
+ end
12
+
13
+ def self.banner
14
+ "rails generate has_messages:#{generator_name} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]"
15
+ end
16
+
17
+ protected
18
+
19
+ def root_path(path)
20
+ File.expand_path(File.join(File.dirname(__FILE__), 'has_messages', path))
21
+ end
22
+
23
+ def destination_path(path)
24
+ File.join(destination_root, path)
25
+ end
26
+
27
+ def file_exists?(path)
28
+ File.exist? destination_path(path)
29
+ end
30
+
31
+ def folder_exists?(path)
32
+ File.directory? path
33
+ end
34
+
35
+ def class_exists?(class_name)
36
+ klass = Rails.application.class.parent_name.constantize.const_get(class_name)
37
+ return klass.is_a?(Class)
38
+ rescue NameError
39
+ return false
40
+ end
41
+
42
+ def extract(filepath,destinationpath,foldername)
43
+ begin
44
+ print_notes("Extracting #{filepath}")
45
+ system("tar -C '#{destination_path(destinationpath)}' -xzf '#{root_path(filepath)}' #{foldername}/")
46
+ rescue Exception => e
47
+ raise e
48
+ end
49
+ end
50
+
51
+ def asking(messages,&block)
52
+ opt = ask("=> #{messages} [yes]")
53
+ if opt == "yes" || opt.blank?
54
+ yield
55
+ end
56
+ rescue Exception => e
57
+ raise e
58
+ end
59
+
60
+ def print_notes(message,notes = "notes",color = :yellow)
61
+ unless message.blank?
62
+ puts '', '='*80
63
+ say_status "#{notes}", "#{message}", color
64
+ puts '='*80, ''; sleep 0.5
65
+ else
66
+ puts "\n"
67
+ end
68
+ end
69
+
70
+ def print_usage
71
+ self.class.help(Thor::Base.shell.new)
72
+ exit
73
+ end
74
+
75
+ def install_local_gem(name,version = nil)
76
+ ::Bundler.with_clean_env do
77
+ if version
78
+ `gem install #{name} -v=#{version}`
79
+ else
80
+ `gem install #{name}`
81
+ end
82
+ end
83
+ $? == 0 ? true : false
84
+ rescue Exception => e
85
+ raise e
86
+ end
87
+
88
+ def check_local_gem?(name,version = nil)
89
+ ::Bundler.with_clean_env do
90
+ if version
91
+ `gem list #{name} -i -v=#{version}`
92
+ else
93
+ `gem list #{name} -i`
94
+ end
95
+ end
96
+ $? == 0 ? true : false
97
+ rescue Exception => e
98
+ raise e
99
+ end
100
+
101
+ def refresh_bundle
102
+ ::Bundler.with_clean_env do
103
+ `bundle`
104
+ end
105
+ rescue Exception => e
106
+ raise e
107
+ end
108
+
109
+ def set_application_config(&block)
110
+ inject_into_class "config/application.rb", "Application" do
111
+ yield
112
+ end
113
+ rescue Exception => e
114
+ raise e
115
+ end
116
+
117
+ def must_load_lib_directory
118
+ set_application_config do
119
+ ' config.autoload_paths += %W(#{config.root}/lib)' + "\n"
120
+ end
121
+ end
122
+
123
+ def gemfile_included?(name)
124
+ ::Bundler.with_clean_env do
125
+ `bundle show #{name}`
126
+ end
127
+ $?.exitstatus == 0 ? true : false
128
+ rescue Exception => e
129
+ raise e
130
+ end
131
+
132
+ def check_required_gems?(*names)
133
+ names.each do |name|
134
+ return false unless gemfile_included? name
135
+ end
136
+ true
137
+ rescue Exception => e
138
+ raise e
139
+ end
140
+
141
+ def rails_3_1?
142
+ Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR >= 1
143
+ end
144
+
145
+ def copy_asset(source, *args, &block)
146
+ if rails_3_1?
147
+ if args.first =~ /^public/
148
+ args.first.gsub!(/^public/,"app/assets")
149
+ end
150
+ if args.first.include?("javascripts/application.js") or args.first.include?("stylesheets/application.css")
151
+ last_line = IO.readlines(args.first).last
152
+ content = IO.read(File.expand_path(find_in_source_paths(source.to_s)))
153
+ content.gsub!(/images/,"assets")
154
+ inject_into_file args.first, :after => last_line do
155
+ content
156
+ end
157
+ return
158
+ end
159
+ end
160
+ copy_file(source, *args, &block)
161
+ end
162
+
163
+ def remove_asset(path, config={})
164
+ if rails_3_1?
165
+ if path =~ /^public/
166
+ path.gsub!(/^public/,"app/assets")
167
+ end
168
+ end
169
+ remove_asset(path, config)
170
+ end
171
+
172
+ end
173
+ end
174
+ end
175
+
176
+ # => set callback on when calling +gem+
177
+ set_trace_func proc { |event, file, line, id, binding, classname|
178
+ ::Bundler.with_clean_env { `bundle` } if classname == Rails::Generators::Actions && id == :gem && event == 'return'
179
+ }
@@ -0,0 +1,10 @@
1
+ Description:
2
+ The has_messages generator creates a basic messaging system between user in your app.
3
+
4
+ To install all the files into your app:
5
+
6
+ rails g has_messages:install user
7
+
8
+ or if you want to completely remove the files from your directory
9
+
10
+ rails g has_messages:install user --destroy
@@ -0,0 +1,126 @@
1
+ require 'generators/base'
2
+ require 'rails/generators/active_record'
3
+ require 'rails/generators/migration'
4
+ require 'rails/generators/generated_attribute'
5
+
6
+ module HasMessages
7
+ module Generators
8
+ class InstallGenerator < Base
9
+ include Rails::Generators::Migration
10
+
11
+ argument :arg, :type => :string, :required => true, :banner => 'USER MODEL NAME'
12
+ class_option :destroy, :desc => 'Destroy all `has_messages` files', :type => :boolean, :default => false
13
+
14
+ def generate_has_messages
15
+ @model_path = "app/models/#{arg}.rb"
16
+ @class_name = arg.classify
17
+ if file_exists?(@model_path)
18
+ if class_exists?(@class_name)
19
+ if options.destroy?
20
+ destroy_has_messages
21
+ else
22
+ install_required_gem
23
+ must_load_lib_directory unless rails_3_1?
24
+ copy_migrations
25
+ copy_models_and_inject_code_into_user_model
26
+ copy_controller_and_helper
27
+ copy_views
28
+ copy_assets
29
+ add_routes
30
+ readme "README"
31
+ end
32
+ else
33
+ raise "#{@class_name} class are not exists!"
34
+ end
35
+ else
36
+ raise "#{@model_path} are not exists in your current directory!"
37
+ end
38
+ rescue Exception => e
39
+ print_notes(e.message,"error",:red)
40
+ end
41
+
42
+ private
43
+
44
+ def install_required_gem
45
+ gem "jquery-rails"
46
+ generate("jquery:install") unless rails_3_1?
47
+ gem "kaminari"
48
+ gem "ancestry"
49
+ end
50
+
51
+ def copy_migrations
52
+ migration_template "models/create_messages.rb", "db/migrate/create_messages.rb"
53
+ end
54
+
55
+ def copy_models_and_inject_code_into_user_model
56
+ template "models/message.rb", "app/models/message.rb"
57
+ copy_file "lib/has_messages.rb", "lib/has_messages.rb"
58
+
59
+ inject_into_class @model_path, @class_name do
60
+ "\n has_many :messages" +
61
+ "\n include HasMessages\n"
62
+ end
63
+ end
64
+
65
+ def copy_controller_and_helper
66
+ template 'controllers/messages_controller.rb', 'app/controllers/messages_controller.rb'
67
+ copy_file 'helpers/messages_helper.rb', 'app/helpers/messages_helper.rb'
68
+ end
69
+
70
+ def copy_views
71
+ copy_file "views/_head.html.erb", "app/views/messages/_head.html.erb"
72
+ copy_file "views/_messages.html.erb", "app/views/messages/_messages.html.erb"
73
+ copy_file "views/_tabs_panel.html.erb", "app/views/messages/_tabs_panel.html.erb"
74
+ copy_file "views/index.html.erb", "app/views/messages/index.html.erb"
75
+ copy_file "views/index.js.erb", "app/views/messages/index.js.erb"
76
+ copy_file "views/new.html.erb", "app/views/messages/new.html.erb"
77
+ copy_file "views/show.html.erb", "app/views/messages/show.html.erb"
78
+ end
79
+
80
+ def copy_assets
81
+ copy_asset 'assets/stylesheets/messages.css', 'public/stylesheets/messages.css'
82
+ copy_asset 'assets/stylesheets/token-input-facebook.css', 'public/stylesheets/token-input-facebook.css'
83
+ copy_asset 'assets/javascripts/messages.js', 'public/javascripts/messages.js'
84
+ copy_asset 'assets/javascripts/jquery.tokeninput.js', 'public/javascripts/jquery.tokeninput.js'
85
+ end
86
+
87
+ def add_routes
88
+ inject_into_file "config/routes.rb", :after => "Application.routes.draw do" do
89
+ "\n\n\t resources :messages, :only => [:new, :create] do" +
90
+ "\n\t collection do" +
91
+ "\n\t get 'token' => 'messages#token', :as => 'token'" +
92
+ "\n\t post 'empty/:messagebox' => 'messages#empty', :as => 'empty'" +
93
+ "\n\t put 'update' => 'messages#update'" +
94
+ "\n\t get ':messagebox/show/:id' => 'messages#show', :as => 'show', :constraints => { :messagebox => /inbox|outbox|trash/ }" +
95
+ "\n\t get '(/:messagebox)' => 'messages#index', :as => 'box', :constraints => { :messagebox => /inbox|outbox|trash/ }" +
96
+ "\n\t end" +
97
+ "\n\t end\n"
98
+ end
99
+ end
100
+
101
+ def destroy_has_messages
102
+ asking "Are you sure want to destroy the `has_messages` files?" do
103
+ remove_file "app/models/message.rb"
104
+ run('rm db/migrate/*_create_messages.rb')
105
+ remove_file "app/controllers/messages_controller.rb"
106
+ remove_file "app/helpers/messages_helper.rb"
107
+ gsub_file @model_path, /has_many :messages/, ''
108
+ gsub_file @model_path, /include HasMessages/, ''
109
+ remove_file "lib/has_messages.rb"
110
+ remove_dir "app/views/messages"
111
+ remove_asset 'public/stylesheets/messages.css'
112
+ remove_asset 'public/stylesheets/token-input-facebook.css'
113
+ remove_asset 'public/javascripts/messages.js'
114
+ remove_asset 'public/javascripts/jquery.tokeninput.js'
115
+ gsub_file 'config/routes.rb', /resources :messages.*:constraints => { :messagebox => \/inbox|outbox|trash\/ }(\s*end){2}/m, ''
116
+ end
117
+ end
118
+
119
+ # FIXME: Should be proxied to ActiveRecord::Generators::Base
120
+ # Implement the required interface for Rails::Generators::Migration.
121
+ def self.next_migration_number(dirname) #:nodoc:
122
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,26 @@
1
+
2
+ ========================================================================================================================
3
+
4
+ Some setup you must do manually if you haven't yet:
5
+
6
+ 1. Ensure you have set authentication filter in app/controllers/messages_controller.rb.
7
+ For example if using Devise:
8
+
9
+ before_filter :authenticate_user!
10
+
11
+ 2. Ensure you have content placeholder :head in app/views/layouts/application.html.erb.
12
+ For example:
13
+
14
+ <%= yield(:head) %>
15
+
16
+ 3. Ensure you have flash messages in app/views/layouts/application.html.erb.
17
+ For example:
18
+
19
+ <p class="notice"><%= notice %></p>
20
+ <p class="alert"><%= alert %></p>
21
+
22
+ 4. If you like to put some link to your app/views/layouts/application.html.erb.
23
+
24
+ <%= link_to "inbox(#{current_user.inbox(:opened => false).count})", messages_path(:inbox), :id => "inbox-link" %>
25
+
26
+ ========================================================================================================================
@@ -0,0 +1,718 @@
1
+ /*
2
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
3
+ * Version 1.4.2
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
+ hintText: "Type in a search term",
15
+ noResultsText: "No results",
16
+ searchingText: "Searching...",
17
+ deleteText: "&times;",
18
+ searchDelay: 300,
19
+ minChars: 1,
20
+ tokenLimit: null,
21
+ jsonContainer: null,
22
+ method: "GET",
23
+ contentType: "json",
24
+ queryParam: "q",
25
+ tokenDelimiter: ",",
26
+ preventDuplicates: false,
27
+ prePopulate: null,
28
+ animateDropdown: true,
29
+ onResult: null,
30
+ onAdd: null,
31
+ onDelete: null
32
+ };
33
+
34
+ // Default classes to use when theming
35
+ var DEFAULT_CLASSES = {
36
+ tokenList: "token-input-list",
37
+ token: "token-input-token",
38
+ tokenDelete: "token-input-delete-token",
39
+ selectedToken: "token-input-selected-token",
40
+ highlightedToken: "token-input-highlighted-token",
41
+ dropdown: "token-input-dropdown",
42
+ dropdownItem: "token-input-dropdown-item",
43
+ dropdownItem2: "token-input-dropdown-item2",
44
+ selectedDropdownItem: "token-input-selected-dropdown-item",
45
+ inputToken: "token-input-input-token"
46
+ };
47
+
48
+ // Input box position "enum"
49
+ var POSITION = {
50
+ BEFORE: 0,
51
+ AFTER: 1,
52
+ END: 2
53
+ };
54
+
55
+ // Keys "enum"
56
+ var KEY = {
57
+ BACKSPACE: 8,
58
+ TAB: 9,
59
+ ENTER: 13,
60
+ ESCAPE: 27,
61
+ SPACE: 32,
62
+ PAGE_UP: 33,
63
+ PAGE_DOWN: 34,
64
+ END: 35,
65
+ HOME: 36,
66
+ LEFT: 37,
67
+ UP: 38,
68
+ RIGHT: 39,
69
+ DOWN: 40,
70
+ NUMPAD_ENTER: 108,
71
+ COMMA: 188
72
+ };
73
+
74
+
75
+ // Expose the .tokenInput function to jQuery as a plugin
76
+ $.fn.tokenInput = function (url_or_data, options) {
77
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
78
+
79
+ return this.each(function () {
80
+ new $.TokenList(this, url_or_data, settings);
81
+ });
82
+ };
83
+
84
+
85
+ // TokenList class for each input
86
+ $.TokenList = function (input, url_or_data, settings) {
87
+ //
88
+ // Initialization
89
+ //
90
+
91
+ // Configure the data source
92
+ if($.type(url_or_data) === "string") {
93
+ // Set the url to query against
94
+ settings.url = url_or_data;
95
+
96
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
97
+ if(settings.crossDomain === undefined) {
98
+ if(settings.url.indexOf("://") === -1) {
99
+ settings.crossDomain = false;
100
+ } else {
101
+ settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
102
+ }
103
+ }
104
+ } else if($.type(url_or_data) === "array") {
105
+ // Set the local data to search through
106
+ settings.local_data = url_or_data;
107
+ }
108
+
109
+ // Build class names
110
+ if(settings.classes) {
111
+ // Use custom class names
112
+ settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes);
113
+ } else if(settings.theme) {
114
+ // Use theme-suffixed default class names
115
+ settings.classes = {};
116
+ $.each(DEFAULT_CLASSES, function(key, value) {
117
+ settings.classes[key] = value + "-" + settings.theme;
118
+ });
119
+ } else {
120
+ settings.classes = DEFAULT_CLASSES;
121
+ }
122
+
123
+
124
+ // Save the tokens
125
+ var saved_tokens = [];
126
+
127
+ // Keep track of the number of tokens in the list
128
+ var token_count = 0;
129
+
130
+ // Basic cache to save on db hits
131
+ var cache = new $.TokenList.Cache();
132
+
133
+ // Keep track of the timeout, old vals
134
+ var timeout;
135
+ var input_val;
136
+
137
+ // Create a new text input an attach keyup events
138
+ var input_box = $("<input type=\"text\" autocomplete=\"off\">")
139
+ .css({
140
+ outline: "none"
141
+ })
142
+ .focus(function () {
143
+ if (settings.tokenLimit === null || settings.tokenLimit !== token_count) {
144
+ show_dropdown_hint();
145
+ }
146
+ })
147
+ .blur(function () {
148
+ hide_dropdown();
149
+ })
150
+ .bind("keyup keydown blur update", resize_input)
151
+ .keydown(function (event) {
152
+ var previous_token;
153
+ var next_token;
154
+
155
+ switch(event.keyCode) {
156
+ case KEY.LEFT:
157
+ case KEY.RIGHT:
158
+ case KEY.UP:
159
+ case KEY.DOWN:
160
+ if(!$(this).val()) {
161
+ previous_token = input_token.prev();
162
+ next_token = input_token.next();
163
+
164
+ if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) {
165
+ // Check if there is a previous/next token and it is selected
166
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
167
+ deselect_token($(selected_token), POSITION.BEFORE);
168
+ } else {
169
+ deselect_token($(selected_token), POSITION.AFTER);
170
+ }
171
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
172
+ // We are moving left, select the previous token if it exists
173
+ select_token($(previous_token.get(0)));
174
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
175
+ // We are moving right, select the next token if it exists
176
+ select_token($(next_token.get(0)));
177
+ }
178
+ } else {
179
+ var dropdown_item = null;
180
+
181
+ if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
182
+ dropdown_item = $(selected_dropdown_item).next();
183
+ } else {
184
+ dropdown_item = $(selected_dropdown_item).prev();
185
+ }
186
+
187
+ if(dropdown_item.length) {
188
+ select_dropdown_item(dropdown_item);
189
+ }
190
+ return false;
191
+ }
192
+ break;
193
+
194
+ case KEY.BACKSPACE:
195
+ previous_token = input_token.prev();
196
+
197
+ if(!$(this).val().length) {
198
+ if(selected_token) {
199
+ delete_token($(selected_token));
200
+ } else if(previous_token.length) {
201
+ select_token($(previous_token.get(0)));
202
+ }
203
+
204
+ return false;
205
+ } else if($(this).val().length === 1) {
206
+ hide_dropdown();
207
+ } else {
208
+ // set a timeout just long enough to let this function finish.
209
+ setTimeout(function(){do_search();}, 5);
210
+ }
211
+ break;
212
+
213
+ case KEY.TAB:
214
+ case KEY.ENTER:
215
+ case KEY.NUMPAD_ENTER:
216
+ case KEY.COMMA:
217
+ if(selected_dropdown_item) {
218
+ add_token($(selected_dropdown_item));
219
+ return false;
220
+ }
221
+ break;
222
+
223
+ case KEY.ESCAPE:
224
+ hide_dropdown();
225
+ return true;
226
+
227
+ default:
228
+ if(String.fromCharCode(event.which)) {
229
+ // set a timeout just long enough to let this function finish.
230
+ setTimeout(function(){do_search();}, 5);
231
+ }
232
+ break;
233
+ }
234
+ });
235
+
236
+ // Keep a reference to the original input box
237
+ var hidden_input = $(input)
238
+ .hide()
239
+ .val("")
240
+ .focus(function () {
241
+ input_box.focus();
242
+ })
243
+ .blur(function () {
244
+ input_box.blur();
245
+ });
246
+
247
+ // Keep a reference to the selected token and dropdown item
248
+ var selected_token = null;
249
+ var selected_dropdown_item = null;
250
+
251
+ // The list to store the token items in
252
+ var token_list = $("<ul />")
253
+ .addClass(settings.classes.tokenList)
254
+ .click(function (event) {
255
+ var li = $(event.target).closest("li");
256
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
257
+ toggle_select_token(li);
258
+ } else {
259
+ // Deselect selected token
260
+ if(selected_token) {
261
+ deselect_token($(selected_token), POSITION.END);
262
+ }
263
+
264
+ // Focus input box
265
+ input_box.focus();
266
+ }
267
+ })
268
+ .mouseover(function (event) {
269
+ var li = $(event.target).closest("li");
270
+ if(li && selected_token !== this) {
271
+ li.addClass(settings.classes.highlightedToken);
272
+ }
273
+ })
274
+ .mouseout(function (event) {
275
+ var li = $(event.target).closest("li");
276
+ if(li && selected_token !== this) {
277
+ li.removeClass(settings.classes.highlightedToken);
278
+ }
279
+ })
280
+ .insertBefore(hidden_input);
281
+
282
+ // The token holding the input box
283
+ var input_token = $("<li />")
284
+ .addClass(settings.classes.inputToken)
285
+ .appendTo(token_list)
286
+ .append(input_box);
287
+
288
+ // The list to store the dropdown items in
289
+ var dropdown = $("<div>")
290
+ .addClass(settings.classes.dropdown)
291
+ .appendTo("body")
292
+ .hide();
293
+
294
+ // Magic element to help us resize the text input
295
+ var input_resizer = $("<tester/>")
296
+ .insertAfter(input_box)
297
+ .css({
298
+ position: "absolute",
299
+ top: -9999,
300
+ left: -9999,
301
+ width: "auto",
302
+ fontSize: input_box.css("fontSize"),
303
+ fontFamily: input_box.css("fontFamily"),
304
+ fontWeight: input_box.css("fontWeight"),
305
+ letterSpacing: input_box.css("letterSpacing"),
306
+ whiteSpace: "nowrap"
307
+ });
308
+
309
+ // Pre-populate list if items exist
310
+ hidden_input.val("");
311
+ li_data = settings.prePopulate || hidden_input.data("pre");
312
+ if(li_data && li_data.length) {
313
+ $.each(li_data, function (index, value) {
314
+ insert_token(value.id, value.name);
315
+ });
316
+ }
317
+
318
+
319
+
320
+ //
321
+ // Private functions
322
+ //
323
+
324
+ function resize_input() {
325
+ if(input_val === (input_val = input_box.val())) {return;}
326
+
327
+ // Enter new content into resizer and resize input accordingly
328
+ var escaped = input_val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
329
+ input_resizer.html(escaped);
330
+ input_box.width(input_resizer.width() + 30);
331
+ }
332
+
333
+ function is_printable_character(keycode) {
334
+ return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
335
+ (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
336
+ (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
337
+ (keycode >= 219 && keycode <= 222)); // ( \ ) '
338
+ }
339
+
340
+ // Inner function to a token to the list
341
+ function insert_token(id, value) {
342
+ var this_token = $("<li><p>"+ value +"</p> </li>")
343
+ .addClass(settings.classes.token)
344
+ .insertBefore(input_token);
345
+
346
+ // The 'delete token' button
347
+ $("<span>" + settings.deleteText + "</span>")
348
+ .addClass(settings.classes.tokenDelete)
349
+ .appendTo(this_token)
350
+ .click(function () {
351
+ delete_token($(this).parent());
352
+ return false;
353
+ });
354
+
355
+ // Store data on the token
356
+ var token_data = {"id": id, "name": value};
357
+ $.data(this_token.get(0), "tokeninput", token_data);
358
+
359
+ // Save this token for duplicate checking
360
+ saved_tokens.push(token_data);
361
+
362
+ // Update the hidden input
363
+ var token_ids = $.map(saved_tokens, function (el) {
364
+ return el.id;
365
+ });
366
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
367
+
368
+ token_count += 1;
369
+
370
+ return this_token;
371
+ }
372
+
373
+ // Add a token to the token list based on user input
374
+ function add_token (item) {
375
+ var li_data = $.data(item.get(0), "tokeninput");
376
+ var callback = settings.onAdd;
377
+
378
+ // See if the token already exists and select it if we don't want duplicates
379
+ if(token_count > 0 && settings.preventDuplicates) {
380
+ var found_existing_token = null;
381
+ token_list.children().each(function () {
382
+ var existing_token = $(this);
383
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
384
+ if(existing_data && existing_data.id === li_data.id) {
385
+ found_existing_token = existing_token;
386
+ return false;
387
+ }
388
+ });
389
+
390
+ if(found_existing_token) {
391
+ select_token(found_existing_token);
392
+ input_token.insertAfter(found_existing_token);
393
+ input_box.focus();
394
+ return;
395
+ }
396
+ }
397
+
398
+ // Insert the new tokens
399
+ insert_token(li_data.id, li_data.name);
400
+
401
+ // Check the token limit
402
+ if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
403
+ input_box.hide();
404
+ hide_dropdown();
405
+ return;
406
+ } else {
407
+ input_box.focus();
408
+ }
409
+
410
+ // Clear input box
411
+ input_box.val("");
412
+
413
+ // Don't show the help dropdown, they've got the idea
414
+ hide_dropdown();
415
+
416
+ // Execute the onAdd callback if defined
417
+ if($.isFunction(callback)) {
418
+ callback(li_data);
419
+ }
420
+ }
421
+
422
+ // Select a token in the token list
423
+ function select_token (token) {
424
+ token.addClass(settings.classes.selectedToken);
425
+ selected_token = token.get(0);
426
+
427
+ // Hide input box
428
+ input_box.val("");
429
+
430
+ // Hide dropdown if it is visible (eg if we clicked to select token)
431
+ hide_dropdown();
432
+ }
433
+
434
+ // Deselect a token in the token list
435
+ function deselect_token (token, position) {
436
+ token.removeClass(settings.classes.selectedToken);
437
+ selected_token = null;
438
+
439
+ if(position === POSITION.BEFORE) {
440
+ input_token.insertBefore(token);
441
+ } else if(position === POSITION.AFTER) {
442
+ input_token.insertAfter(token);
443
+ } else {
444
+ input_token.appendTo(token_list);
445
+ }
446
+
447
+ // Show the input box and give it focus again
448
+ input_box.focus();
449
+ }
450
+
451
+ // Toggle selection of a token in the token list
452
+ function toggle_select_token(token) {
453
+ var previous_selected_token = selected_token;
454
+
455
+ if(selected_token) {
456
+ deselect_token($(selected_token), POSITION.END);
457
+ }
458
+
459
+ if(previous_selected_token === token.get(0)) {
460
+ deselect_token(token, POSITION.END);
461
+ } else {
462
+ select_token(token);
463
+ }
464
+ }
465
+
466
+ // Delete a token from the token list
467
+ function delete_token (token) {
468
+ // Remove the id from the saved list
469
+ var token_data = $.data(token.get(0), "tokeninput");
470
+ var callback = settings.onDelete;
471
+
472
+ // Delete the token
473
+ token.remove();
474
+ selected_token = null;
475
+
476
+ // Show the input box and give it focus again
477
+ input_box.focus();
478
+
479
+ // Remove this token from the saved list
480
+ saved_tokens = $.grep(saved_tokens, function (val) {
481
+ return (val.id !== token_data.id);
482
+ });
483
+
484
+ // Update the hidden input
485
+ var token_ids = $.map(saved_tokens, function (el) {
486
+ return el.id;
487
+ });
488
+ hidden_input.val(token_ids.join(settings.tokenDelimiter));
489
+
490
+ token_count -= 1;
491
+
492
+ if(settings.tokenLimit !== null) {
493
+ input_box
494
+ .show()
495
+ .val("")
496
+ .focus();
497
+ }
498
+
499
+ // Execute the onDelete callback if defined
500
+ if($.isFunction(callback)) {
501
+ callback(token_data);
502
+ }
503
+ }
504
+
505
+ // Hide and clear the results dropdown
506
+ function hide_dropdown () {
507
+ dropdown.hide().empty();
508
+ selected_dropdown_item = null;
509
+ }
510
+
511
+ function show_dropdown() {
512
+ dropdown
513
+ .css({
514
+ position: "absolute",
515
+ top: $(token_list).offset().top + $(token_list).outerHeight(),
516
+ left: $(token_list).offset().left,
517
+ zindex: 999
518
+ })
519
+ .show();
520
+ }
521
+
522
+ function show_dropdown_searching () {
523
+ if(settings.searchingText) {
524
+ dropdown.html("<p>"+settings.searchingText+"</p>");
525
+ show_dropdown();
526
+ }
527
+ }
528
+
529
+ function show_dropdown_hint () {
530
+ if(settings.hintText) {
531
+ dropdown.html("<p>"+settings.hintText+"</p>");
532
+ show_dropdown();
533
+ }
534
+ }
535
+
536
+ // Highlight the query part of the search term
537
+ function highlight_term(value, term) {
538
+ return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
539
+ }
540
+
541
+ // Populate the results dropdown with some results
542
+ function populate_dropdown (query, results) {
543
+ if(results && results.length) {
544
+ dropdown.empty();
545
+ var dropdown_ul = $("<ul>")
546
+ .appendTo(dropdown)
547
+ .mouseover(function (event) {
548
+ select_dropdown_item($(event.target).closest("li"));
549
+ })
550
+ .mousedown(function (event) {
551
+ add_token($(event.target).closest("li"));
552
+ return false;
553
+ })
554
+ .hide();
555
+
556
+ $.each(results, function(index, value) {
557
+ var this_li = $("<li>" + highlight_term(value.name, query) + "</li>")
558
+ .appendTo(dropdown_ul);
559
+
560
+ if(index % 2) {
561
+ this_li.addClass(settings.classes.dropdownItem);
562
+ } else {
563
+ this_li.addClass(settings.classes.dropdownItem2);
564
+ }
565
+
566
+ if(index === 0) {
567
+ select_dropdown_item(this_li);
568
+ }
569
+
570
+ $.data(this_li.get(0), "tokeninput", {"id": value.id, "name": value.name});
571
+ });
572
+
573
+ show_dropdown();
574
+
575
+ if(settings.animateDropdown) {
576
+ dropdown_ul.slideDown("fast");
577
+ } else {
578
+ dropdown_ul.show();
579
+ }
580
+ } else {
581
+ if(settings.noResultsText) {
582
+ dropdown.html("<p>"+settings.noResultsText+"</p>");
583
+ show_dropdown();
584
+ }
585
+ }
586
+ }
587
+
588
+ // Highlight an item in the results dropdown
589
+ function select_dropdown_item (item) {
590
+ if(item) {
591
+ if(selected_dropdown_item) {
592
+ deselect_dropdown_item($(selected_dropdown_item));
593
+ }
594
+
595
+ item.addClass(settings.classes.selectedDropdownItem);
596
+ selected_dropdown_item = item.get(0);
597
+ }
598
+ }
599
+
600
+ // Remove highlighting from an item in the results dropdown
601
+ function deselect_dropdown_item (item) {
602
+ item.removeClass(settings.classes.selectedDropdownItem);
603
+ selected_dropdown_item = null;
604
+ }
605
+
606
+ // Do a search and show the "searching" dropdown if the input is longer
607
+ // than settings.minChars
608
+ function do_search() {
609
+ var query = input_box.val().toLowerCase();
610
+
611
+ if(query && query.length) {
612
+ if(selected_token) {
613
+ deselect_token($(selected_token), POSITION.AFTER);
614
+ }
615
+
616
+ if(query.length >= settings.minChars) {
617
+ show_dropdown_searching();
618
+ clearTimeout(timeout);
619
+
620
+ timeout = setTimeout(function(){
621
+ run_search(query);
622
+ }, settings.searchDelay);
623
+ } else {
624
+ hide_dropdown();
625
+ }
626
+ }
627
+ }
628
+
629
+ // Do the actual search
630
+ function run_search(query) {
631
+ var cached_results = cache.get(query);
632
+ if(cached_results) {
633
+ populate_dropdown(query, cached_results);
634
+ } else {
635
+ // Are we doing an ajax search or local data search?
636
+ if(settings.url) {
637
+ // Extract exisiting get params
638
+ var ajax_params = {};
639
+ ajax_params.data = {};
640
+ if(settings.url.indexOf("?") > -1) {
641
+ var parts = settings.url.split("?");
642
+ ajax_params.url = parts[0];
643
+
644
+ var param_array = parts[1].split("&");
645
+ $.each(param_array, function (index, value) {
646
+ var kv = value.split("=");
647
+ ajax_params.data[kv[0]] = kv[1];
648
+ });
649
+ } else {
650
+ ajax_params.url = settings.url;
651
+ }
652
+
653
+ // Prepare the request
654
+ ajax_params.data[settings.queryParam] = query;
655
+ ajax_params.type = settings.method;
656
+ ajax_params.dataType = settings.contentType;
657
+ if(settings.crossDomain) {
658
+ ajax_params.dataType = "jsonp";
659
+ }
660
+
661
+ // Attach the success callback
662
+ ajax_params.success = function(results) {
663
+ if($.isFunction(settings.onResult)) {
664
+ results = settings.onResult.call(this, results);
665
+ }
666
+ cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
667
+
668
+ // only populate the dropdown if the results are associated with the active search query
669
+ if(input_box.val().toLowerCase() === query) {
670
+ populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
671
+ }
672
+ };
673
+
674
+ // Make the request
675
+ $.ajax(ajax_params);
676
+ } else if(settings.local_data) {
677
+ // Do the search through local data
678
+ var results = $.grep(settings.local_data, function (row) {
679
+ return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
680
+ });
681
+
682
+ populate_dropdown(query, results);
683
+ }
684
+ }
685
+ }
686
+ };
687
+
688
+ // Really basic cache for the results
689
+ $.TokenList.Cache = function (options) {
690
+ var settings = $.extend({
691
+ max_size: 500
692
+ }, options);
693
+
694
+ var data = {};
695
+ var size = 0;
696
+
697
+ var flush = function () {
698
+ data = {};
699
+ size = 0;
700
+ };
701
+
702
+ this.add = function (query, results) {
703
+ if(size > settings.max_size) {
704
+ flush();
705
+ }
706
+
707
+ if(!data[query]) {
708
+ size += 1;
709
+ }
710
+
711
+ data[query] = results;
712
+ };
713
+
714
+ this.get = function (query) {
715
+ return data[query];
716
+ };
717
+ };
718
+ }(jQuery));