has_messages_generators 0.0.1

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