diff --git a/Gemfile b/Gemfile
index 1c8ec24..8f525bf 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,8 +12,16 @@ gem 'railties'
gem 'coffee-rails'
gem 'bootstrap-sass', "2.3.2.0"
gem 'uglifier'
-
gem 'thin'
+gem 'devise', '3.0.0.rc'
+gem 'jquery-rails'
+gem 'acts-as-taggable-on'
+gem 'rails-jquery-tokeninput'
+gem 'protected_attributes'
+
+source 'https://rails-assets.org' do
+ gem 'rails-assets-chosen'
+end
# Language detection
gem "github-linguist", "~> 2.3.4" , require: "linguist"
@@ -21,5 +29,4 @@ gem "github-linguist", "~> 2.3.4" , require: "linguist"
# Syntax highlighter
gem "pygments.rb", git: "https://github.com/tmm1/pygments.rb", branch: "master"
gem 'diffy'
-gem 'jquery-rails'
-gem 'devise', '3.0.0.rc'
+
diff --git a/Gemfile.lock b/Gemfile.lock
index 4981f2a..aaa3b8d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -9,6 +9,7 @@ GIT
GEM
remote: https://rubygems.org/
+ remote: https://rails-assets.org/
specs:
actionmailer (4.0.1)
actionpack (= 4.0.1)
@@ -34,6 +35,8 @@ GEM
multi_json (~> 1.3)
thread_safe (~> 0.1)
tzinfo (~> 0.3.37)
+ acts-as-taggable-on (3.5.0)
+ activerecord (>= 3.2, < 5)
arel (4.0.1)
atomic (1.1.14)
bcrypt-ruby (3.1.2)
@@ -79,6 +82,8 @@ GEM
orm_adapter (0.4.0)
polyglot (0.3.3)
posix-spawn (0.3.6)
+ protected_attributes (1.1.3)
+ activemodel (>= 4.0.1, < 5.0)
rack (1.5.2)
rack-test (0.6.2)
rack (>= 1.0)
@@ -90,6 +95,11 @@ GEM
bundler (>= 1.3.0, < 2.0)
railties (= 4.0.1)
sprockets-rails (~> 2.0.0)
+ rails-assets-chosen (1.6.1)
+ rails-assets-jquery (>= 1.4.4)
+ rails-assets-jquery (3.1.0)
+ rails-jquery-tokeninput (0.2.6)
+ jquery-rails (>= 2)
railties (4.0.1)
actionpack (= 4.0.1)
activesupport (= 4.0.1)
@@ -135,14 +145,18 @@ PLATFORMS
ruby
DEPENDENCIES
+ acts-as-taggable-on
bootstrap-sass (= 2.3.2.0)
coffee-rails
devise (= 3.0.0.rc)
diffy
github-linguist (~> 2.3.4)
jquery-rails
+ protected_attributes
pygments.rb!
rails (~> 4.0.1)
+ rails-assets-chosen!
+ rails-jquery-tokeninput
railties
redcarpet
sass-rails
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 5dcb43d..f2ae6c6 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -12,8 +12,13 @@
//
//= require jquery
//= require jquery_ujs
+//= require jquery.tokeninput
//= require_tree .
// Loads all Bootstrap javascripts
//= require bootstrap
//
+
+//Needed for tag auto completition
+//= require chosen
+//
diff --git a/app/assets/javascripts/posts.js b/app/assets/javascripts/posts.js
index f890d83..8fe6e9c 100644
--- a/app/assets/javascripts/posts.js
+++ b/app/assets/javascripts/posts.js
@@ -82,4 +82,11 @@ $(document).ready(function() {
}, 100);
});
+ $('#post_tags').tokenInput("/posts/tags.json", {
+ tokenValue: "name",
+ allowFreeTagging: true,
+ preventDuplicates: true,
+ prePopulate: $("#post_tags").data("pre")
+ });
});
+
diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss
index fb3e57f..f49e327 100644
--- a/app/assets/stylesheets/application.css.scss
+++ b/app/assets/stylesheets/application.css.scss
@@ -9,7 +9,9 @@
* compiled file, but it's generally better to create a new file per style scope.
*
*= require_self
+ *= require token-input
*= require_tree .
+ *= require chosen
*/
/* compensate the overlap caused by bootstrap top navigation, min. 40px at the top! */
diff --git a/app/assets/stylesheets/posts.css.scss b/app/assets/stylesheets/posts.css.scss
index 1e1f0d0..9750d9a 100644
--- a/app/assets/stylesheets/posts.css.scss
+++ b/app/assets/stylesheets/posts.css.scss
@@ -86,6 +86,10 @@
padding-bottom: 20px;
}
+.set-lower-25 {
+ padding-top: 25px;
+}
+
input.input-sm {
height: 12px;
width: 32px;
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index deb7973..6ed4ab9 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -30,9 +30,9 @@ def show
@post = Post.find(params[:id])
respond_to do |format|
- format.html # show.html.erb
- format.text { render :text => @post.content }
- end
+ format.html # show.html.erb
+ format.text { render :text => @post.content }
+ end
end
# GET /posts/new
@@ -252,6 +252,17 @@ def parentlist
end
end
+ def tags
+ @tags = ActsAsTaggableOn::Tag.where("tags.name LIKE ?", "%#{params[:q]}%")
+ respond_to do |format|
+ format.json {render :json => @tags.map{|t| {:id => t.name, :name => t.name }}}
+ end
+ end
+
+ def search_by_tag
+ @posts = Post.tagged_with(params[:tag_name])
+ end
+
private
def post_params
diff --git a/app/models/post.rb b/app/models/post.rb
index bcccf31..10da91e 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1,5 +1,6 @@
class Post < ActiveRecord::Base
require 'tempfile'
+ #require 'acts-as-taggable-on'
def self.MaxUploadSize
102400 # 100kb
@@ -22,6 +23,10 @@ def self.MaxUploadSize
:foreign_key => :parent_id, :dependent => :destroy
belongs_to :parent, :class_name => "Post"
+ acts_as_taggable
+
+ attr_accessible :tag_list, :title, :author, :content_type, :content
+
attr_accessor :uploaded_file
def self.file_extensions
@@ -63,11 +68,28 @@ def all_comments
end
def create_version(params)
- child = self.class.new(params)
- child.parent_id = self.id
- self.newest = false
- self.save
- return child
+ if self.newest != true
+ children_ids = self.children.map{|x| x.id}
+ newest_post = Post.where(:id => children_ids, "newest" => true).first
+ former_newest_params = newest_post.serializable_hash
+ former_newest_params.delete("id")
+ former_newest_params["newest"] = false
+ former_newest = self.class.new(former_newest_params)
+ former_newest.save
+ newest_post.update_attributes(params)
+ newest_post.parent_id = former_newest.id
+ return newest_post
+ else
+ old_params = self.serializable_hash
+ old_params.delete("id")
+ parent = self.class.new(old_params)
+ parent.newest = false
+ self.newest = true
+ parent.save
+ self.parent_id = parent.id
+ self.update_attributes(params)
+ return self
+ end
end
def self.new_from_file(params, data)
@@ -136,8 +158,18 @@ def diff_to_parent(post_id = nil)
end
end
+ def children
+ child = Post.where(:parent_id => self.id).first
+ return [] if child.nil?
+ return child.children + [child]
+ end
+
def parents
return [] if parent_id.nil?
- return parent.parents + [parent]
+ return [parent] + parent.parents
+ end
+
+ def tag_list_tokens=(tokens)
+ self.tag_list = tokens.gsub("'", "")
end
end
diff --git a/app/views/posts/_form.html.erb b/app/views/posts/_form.html.erb
index 84ac167..a73b514 100644
--- a/app/views/posts/_form.html.erb
+++ b/app/views/posts/_form.html.erb
@@ -45,7 +45,8 @@
:id => "inputContentType")%>
-
+
+
@@ -60,7 +61,7 @@
-
+
-
+
+
- <%= f.submit :class => "btn pull-right" %>
+
+
+ <%= f.text_field :tag_list, :id => "post_tags",
+ "data-pre" => @post.tags.map(&:attributes).to_json %>
+
+
+
+
+ <%= f.submit :class => "btn pull-right" %>
diff --git a/app/views/posts/_options.html.erb b/app/views/posts/_options.html.erb
index 8be4cd4..bd39d0a 100644
--- a/app/views/posts/_options.html.erb
+++ b/app/views/posts/_options.html.erb
@@ -16,10 +16,11 @@
<% end %>
<% end %>
- <% if @post.parent.present? &&
- !current_page?(root_url) && !current_page?(posts_path) %>
+ <% if !current_page?(root_url) && !current_page?(posts_path) %>
+ <%= !@post.parent.nil? ? parent_placeholder =
+ @post.parent.id.to_s : parent_placeholder = "" %>
<%= text_field_tag(:diff_id, nil, :class => "input-sm",
- :placeholder => @post.parent.id.to_s) %>
+ :placeholder => parent_placeholder) %>
<%= button_tag(:type => "submit", :class => "btn btn-mini",
:title => "Diff") do %>
@@ -30,13 +31,20 @@
<% end %>
@@ -58,8 +66,8 @@
<% end %>
<% end %>
- <%= link_to post_path(@post, :format => :json),
- :method => :delete, :remote => true,
+ <%= link_to post_path(@post),
+ :method => :delete,
data: { confirm: 'Are you sure?' }, :class => "btn btn-mini",
:id => "delete_trigger", :title => "Delete" do %>
diff --git a/app/views/posts/_post.html.erb b/app/views/posts/_post.html.erb
index 993e27c..8834aed 100644
--- a/app/views/posts/_post.html.erb
+++ b/app/views/posts/_post.html.erb
@@ -19,7 +19,7 @@
- <%= @post.updated_at.strftime('%a, %d %b %Y %H:%M:%S') %>
-<%= render 'options' %>
+<%= render 'posts/options' %>
diff --git a/app/views/posts/parentlist.html.erb b/app/views/posts/parentlist.html.erb
index 869f60a..8b94fb1 100644
--- a/app/views/posts/parentlist.html.erb
+++ b/app/views/posts/parentlist.html.erb
@@ -1,6 +1,15 @@
- <% @post.parents.each.with_index(1) do |parent, id| %>
-
<%= link_to(diff_post_path(@post, :diff_id => parent.id)){"Version: #{id}"} %>
+ <% children = @post.children %>
+ <% parents = @post.parents %>
+ <% children.each.with_index(0) do |child, id| %>
+ <%= link_to(diff_post_path(@post,
+ :diff_id => child.id)){"Version: #{parents.size + 1 + children.size - id}"} %>
+ <% end %>
+ <%= link_to(diff_post_path(@post,
+ :diff_id => @post.id)) { "Version: #{parents.size + 1} (self)"} %>
+ <% parents.each.with_index(0) do |parent, id| %>
+ <%= link_to(diff_post_path(@post,
+ :diff_id => parent.id)){"Version: #{parents.size - id}"} %>
<% end %>
diff --git a/app/views/posts/search_by_tag.html.erb b/app/views/posts/search_by_tag.html.erb
new file mode 100644
index 0000000..aebbd64
--- /dev/null
+++ b/app/views/posts/search_by_tag.html.erb
@@ -0,0 +1,5 @@
+<% unless @posts.blank? %>
+
+ <%= render :partial => "post", :collection => @posts %>
+
+<% end %>
diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb
index 642b7db..311a28a 100644
--- a/app/views/posts/show.html.erb
+++ b/app/views/posts/show.html.erb
@@ -14,6 +14,12 @@
+
+ <% @post.tag_counts_on(:tags).map(&:name).each do |tag| %>
+ <%= link_to tag, {:action => 'search_by_tag', :tag_name => tag},
+ :class => "btn btn-mini" %>
+ <% end %>
+
<%= render 'like_dislike' %>
<%= render 'comments/comment' %>
diff --git a/config/application.rb b/config/application.rb
index e13407b..7fa32c3 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -18,7 +18,7 @@ class Application < Rails::Application
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
- # Only load the plugins named here, in the order given (default is alphabetical).
+ # Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
diff --git a/config/routes.rb b/config/routes.rb
index af2f4eb..2cfb6ff 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,8 @@
# note that the helper methods are still called with 'posts' instead of 'p'
# for legibility purposes
+ get "posts/tags" => "posts#tags", :as => :tags
+
resources :posts, :path => :p do
resources :comments
resources :linecomments
@@ -12,8 +14,10 @@
get :dislike
get :markdown
get :parentlist
- end
+ get :search_by_tag
+ end
end
+
resources :posts, :as => :p
get '/help' => 'posts#help', :as => :help
diff --git a/db/migrate/20160707134101_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb b/db/migrate/20160707134101_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb
new file mode 100644
index 0000000..6bbd559
--- /dev/null
+++ b/db/migrate/20160707134101_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb
@@ -0,0 +1,31 @@
+# This migration comes from acts_as_taggable_on_engine (originally 1)
+class ActsAsTaggableOnMigration < ActiveRecord::Migration
+ def self.up
+ create_table :tags do |t|
+ t.string :name
+ end
+
+ create_table :taggings do |t|
+ t.references :tag
+
+ # You should make sure that the column created is
+ # long enough to store the required class names.
+ t.references :taggable, polymorphic: true
+ t.references :tagger, polymorphic: true
+
+ # Limit is created to prevent MySQL error on index
+ # length for MyISAM table type: http://bit.ly/vgW2Ql
+ t.string :context, limit: 128
+
+ t.datetime :created_at
+ end
+
+ add_index :taggings, :tag_id
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+
+ def self.down
+ drop_table :taggings
+ drop_table :tags
+ end
+end
diff --git a/db/migrate/20160707134102_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20160707134102_add_missing_unique_indices.acts_as_taggable_on_engine.rb
new file mode 100644
index 0000000..4ca676f
--- /dev/null
+++ b/db/migrate/20160707134102_add_missing_unique_indices.acts_as_taggable_on_engine.rb
@@ -0,0 +1,20 @@
+# This migration comes from acts_as_taggable_on_engine (originally 2)
+class AddMissingUniqueIndices < ActiveRecord::Migration
+ def self.up
+ add_index :tags, :name, unique: true
+
+ remove_index :taggings, :tag_id
+ remove_index :taggings, [:taggable_id, :taggable_type, :context]
+ add_index :taggings,
+ [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
+ unique: true, name: 'taggings_idx'
+ end
+
+ def self.down
+ remove_index :tags, :name
+
+ remove_index :taggings, name: 'taggings_idx'
+ add_index :taggings, :tag_id
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+end
diff --git a/db/migrate/20160707134103_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20160707134103_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
new file mode 100644
index 0000000..8edb508
--- /dev/null
+++ b/db/migrate/20160707134103_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
@@ -0,0 +1,15 @@
+# This migration comes from acts_as_taggable_on_engine (originally 3)
+class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
+ def self.up
+ add_column :tags, :taggings_count, :integer, default: 0
+
+ ActsAsTaggableOn::Tag.reset_column_information
+ ActsAsTaggableOn::Tag.find_each do |tag|
+ ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
+ end
+ end
+
+ def self.down
+ remove_column :tags, :taggings_count
+ end
+end
diff --git a/db/migrate/20160707134104_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20160707134104_add_missing_taggable_index.acts_as_taggable_on_engine.rb
new file mode 100644
index 0000000..71f2d7f
--- /dev/null
+++ b/db/migrate/20160707134104_add_missing_taggable_index.acts_as_taggable_on_engine.rb
@@ -0,0 +1,10 @@
+# This migration comes from acts_as_taggable_on_engine (originally 4)
+class AddMissingTaggableIndex < ActiveRecord::Migration
+ def self.up
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+
+ def self.down
+ remove_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+end
diff --git a/db/migrate/20160707134105_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20160707134105_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
new file mode 100644
index 0000000..bfb06bc
--- /dev/null
+++ b/db/migrate/20160707134105_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
@@ -0,0 +1,10 @@
+# This migration comes from acts_as_taggable_on_engine (originally 5)
+# This migration is added to circumvent issue #623 and have special characters
+# work properly
+class ChangeCollationForTagNames < ActiveRecord::Migration
+ def up
+ if ActsAsTaggableOn::Utils.using_mysql?
+ execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6287fcd..33810df 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20140202010251) do
+ActiveRecord::Schema.define(version: 20160707134105) do
create_table "apikeys", force: true do |t|
t.string "key"
@@ -60,6 +60,26 @@
t.string "content_type", default: "None"
end
+ create_table "taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
+ add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
+
+ create_table "tags", force: true do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ add_index "tags", ["name"], name: "index_tags_on_name", unique: true
+
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb
index ee2452d..1c6b950 100644
--- a/test/functional/posts_controller_test.rb
+++ b/test/functional/posts_controller_test.rb
@@ -80,18 +80,18 @@ class PostsControllerTest < ActionController::TestCase
assert_nil assigns(:post).parent_id
end
- test "should update post with a new post and old post as parent" do
+ test "should update post with an old post and a new post as parent" do
put :update, id: @post, post: { content: @post.content + "new",
title: @post.title }
- assert_redirected_to post_path(assigns(:post))
- assert_not_equal assigns(:post).id, @post.id
- assert_equal assigns(:post).parent_id, @post.id
- @post.reload
- assert_equal false, @post.newest
+ assert_redirected_to post_path(@post)
+ assert_equal assigns(:post).id, @post.id
+ assert_not_equal assigns(:post).parent_id, @post.parent_id
+ assigns(:post).reload
+ assert_equal true, @post.newest
assert_equal true, assigns(:post).newest
end
- test "should update post with a new post from file and old post as parent" do
+ test "should update post with an old post from file and a new post as parent" do
test_image = "test/fixtures/test.txt"
file = Rack::Test::UploadedFile.new(test_image, "text/plain")
@@ -101,11 +101,11 @@ class PostsControllerTest < ActionController::TestCase
author: @post.author,
upload_file: file
}
- assert_redirected_to post_path(assigns(:post))
- assert_not_equal assigns(:post).id, @post.id
- assert_equal assigns(:post).parent_id, @post.id
- @post.reload
- assert_equal false, @post.newest
+ assert_redirected_to post_path(@post)
+ assert_equal assigns(:post).id, @post.id
+ assert_not_equal assigns(:post).parent_id, @post.parent_id
+ assigns(:post).reload
+ assert_equal true, @post.newest
assert_equal true, assigns(:post).newest
end
diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb
index e2d800a..6d6b6ad 100644
--- a/test/unit/post_test.rb
+++ b/test/unit/post_test.rb
@@ -64,15 +64,16 @@ class PostTest < ActiveSupport::TestCase
assert_equal parent, child.parent
end
- test "should make new version of post" do
- assert_not_nil parent = Post.find(1)
- assert_not_nil child = parent.create_version({
+ test "should stay the same post after creating new version" do
+ assert_not_nil old_post = Post.find(1)
+ content_before = old_post.content
+ old_post.content = old_post.content + "new"
+ assert_not_nil new_post = old_post.create_version({
:content => "far out"})
- assert_not_equal child, parent
- assert child.save
- assert_equal parent.id, child.parent_id
- assert_not_equal true, parent.newest
- assert_equal true, child.newest
+ assert_equal new_post, old_post
+ assert_not_equal old_post.content, content_before
+ assert_not_equal new_post.content, content_before
+ assert_not_equal new_post.content, new_post.parent.content
end
test "collect all parent ids" do
diff --git a/vendor/assets/javascripts/jquery.tokeninput.js b/vendor/assets/javascripts/jquery.tokeninput.js
new file mode 100755
index 0000000..4b69d82
--- /dev/null
+++ b/vendor/assets/javascripts/jquery.tokeninput.js
@@ -0,0 +1,1106 @@
+/*
+ * jQuery Plugin: Tokenizing Autocomplete Text Entry
+ * Version 1.6.2
+ *
+ * Copyright (c) 2009 James Smith (http://loopj.com)
+ * Licensed jointly under the GPL and MIT licenses,
+ * choose which one suits your project best!
+ *
+ */
+;(function ($) {
+ var DEFAULT_SETTINGS = {
+ // Search settings
+ method: "GET",
+ queryParam: "q",
+ searchDelay: 300,
+ minChars: 1,
+ propertyToSearch: "name",
+ jsonContainer: null,
+ contentType: "json",
+ excludeCurrent: false,
+ excludeCurrentParameter: "x",
+
+ // Prepopulation settings
+ prePopulate: null,
+ processPrePopulate: false,
+
+ // Display settings
+ hintText: "Type in a search term",
+ noResultsText: "No results",
+ searchingText: "Searching...",
+ deleteText: "×",
+ animateDropdown: true,
+ placeholder: null,
+ theme: null,
+ zindex: 999,
+ resultsLimit: null,
+
+ enableHTML: false,
+
+ resultsFormatter: function(item) {
+ var string = item[this.propertyToSearch];
+ return "" + (this.enableHTML ? string : _escapeHTML(string)) + "";
+ },
+
+ tokenFormatter: function(item) {
+ var string = item[this.propertyToSearch];
+ return "" + (this.enableHTML ? string : _escapeHTML(string)) + "
";
+ },
+
+ // Tokenization settings
+ tokenLimit: null,
+ tokenDelimiter: ",",
+ preventDuplicates: false,
+ tokenValue: "id",
+
+ // Behavioral settings
+ allowFreeTagging: false,
+ allowTabOut: false,
+ autoSelectFirstResult: false,
+
+ // Callbacks
+ onResult: null,
+ onCachedResult: null,
+ onAdd: null,
+ onFreeTaggingAdd: null,
+ onDelete: null,
+ onReady: null,
+
+ // Other settings
+ idPrefix: "token-input-",
+
+ // Keep track if the input is currently in disabled mode
+ disabled: false
+ };
+
+ // Default classes to use when theming
+ var DEFAULT_CLASSES = {
+ tokenList : "token-input-list",
+ token : "token-input-token",
+ tokenReadOnly : "token-input-token-readonly",
+ tokenDelete : "token-input-delete-token",
+ selectedToken : "token-input-selected-token",
+ highlightedToken : "token-input-highlighted-token",
+ dropdown : "token-input-dropdown",
+ dropdownItem : "token-input-dropdown-item",
+ dropdownItem2 : "token-input-dropdown-item2",
+ selectedDropdownItem : "token-input-selected-dropdown-item",
+ inputToken : "token-input-input-token",
+ focused : "token-input-focused",
+ disabled : "token-input-disabled"
+ };
+
+ // Input box position "enum"
+ var POSITION = {
+ BEFORE : 0,
+ AFTER : 1,
+ END : 2
+ };
+
+ // Keys "enum"
+ var KEY = {
+ BACKSPACE : 8,
+ TAB : 9,
+ ENTER : 13,
+ ESCAPE : 27,
+ SPACE : 32,
+ PAGE_UP : 33,
+ PAGE_DOWN : 34,
+ END : 35,
+ HOME : 36,
+ LEFT : 37,
+ UP : 38,
+ RIGHT : 39,
+ DOWN : 40,
+ NUMPAD_ENTER : 108,
+ COMMA : 188
+ };
+
+ var HTML_ESCAPES = {
+ '&' : '&',
+ '<' : '<',
+ '>' : '>',
+ '"' : '"',
+ "'" : ''',
+ '/' : '/'
+ };
+
+ var HTML_ESCAPE_CHARS = /[&<>"'\/]/g;
+
+ function coerceToString(val) {
+ return String((val === null || val === undefined) ? '' : val);
+ }
+
+ function _escapeHTML(text) {
+ return coerceToString(text).replace(HTML_ESCAPE_CHARS, function(match) {
+ return HTML_ESCAPES[match];
+ });
+ }
+
+ // Additional public (exposed) methods
+ var methods = {
+ init: function(url_or_data_or_function, options) {
+ var settings = $.extend({}, DEFAULT_SETTINGS, options || {});
+
+ return this.each(function () {
+ $(this).data("settings", settings);
+ $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings));
+ });
+ },
+ clear: function() {
+ this.data("tokenInputObject").clear();
+ return this;
+ },
+ add: function(item) {
+ this.data("tokenInputObject").add(item);
+ return this;
+ },
+ remove: function(item) {
+ this.data("tokenInputObject").remove(item);
+ return this;
+ },
+ get: function() {
+ return this.data("tokenInputObject").getTokens();
+ },
+ toggleDisabled: function(disable) {
+ this.data("tokenInputObject").toggleDisabled(disable);
+ return this;
+ },
+ setOptions: function(options){
+ $(this).data("settings", $.extend({}, $(this).data("settings"), options || {}));
+ return this;
+ },
+ destroy: function () {
+ if (this.data("tokenInputObject")) {
+ this.data("tokenInputObject").clear();
+ var tmpInput = this;
+ var closest = this.parent();
+ closest.empty();
+ tmpInput.show();
+ closest.append(tmpInput);
+ return tmpInput;
+ }
+ }
+ };
+
+ // Expose the .tokenInput function to jQuery as a plugin
+ $.fn.tokenInput = function (method) {
+ // Method calling and initialization logic
+ if (methods[method]) {
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ } else {
+ return methods.init.apply(this, arguments);
+ }
+ };
+
+ // TokenList class for each input
+ $.TokenList = function (input, url_or_data, settings) {
+ //
+ // Initialization
+ //
+
+ // Configure the data source
+ if (typeof(url_or_data) === "string" || typeof(url_or_data) === "function") {
+ // Set the url to query against
+ $(input).data("settings").url = url_or_data;
+
+ // If the URL is a function, evaluate it here to do our initalization work
+ var url = computeURL();
+
+ // Make a smart guess about cross-domain if it wasn't explicitly specified
+ if ($(input).data("settings").crossDomain === undefined && typeof url === "string") {
+ if(url.indexOf("://") === -1) {
+ $(input).data("settings").crossDomain = false;
+ } else {
+ $(input).data("settings").crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
+ }
+ }
+ } else if (typeof(url_or_data) === "object") {
+ // Set the local data to search through
+ $(input).data("settings").local_data = url_or_data;
+ }
+
+ // Build class names
+ if($(input).data("settings").classes) {
+ // Use custom class names
+ $(input).data("settings").classes = $.extend({}, DEFAULT_CLASSES, $(input).data("settings").classes);
+ } else if($(input).data("settings").theme) {
+ // Use theme-suffixed default class names
+ $(input).data("settings").classes = {};
+ $.each(DEFAULT_CLASSES, function(key, value) {
+ $(input).data("settings").classes[key] = value + "-" + $(input).data("settings").theme;
+ });
+ } else {
+ $(input).data("settings").classes = DEFAULT_CLASSES;
+ }
+
+ // Save the tokens
+ var saved_tokens = [];
+
+ // Keep track of the number of tokens in the list
+ var token_count = 0;
+
+ // Basic cache to save on db hits
+ var cache = new $.TokenList.Cache();
+
+ // Keep track of the timeout, old vals
+ var timeout;
+ var input_val;
+
+ // Create a new text input an attach keyup events
+ var input_box = $("")
+ .css({
+ outline: "none"
+ })
+ .attr("id", $(input).data("settings").idPrefix + input.id)
+ .focus(function () {
+ if ($(input).data("settings").disabled) {
+ return false;
+ } else
+ if ($(input).data("settings").tokenLimit === null || $(input).data("settings").tokenLimit !== token_count) {
+ show_dropdown_hint();
+ }
+ token_list.addClass($(input).data("settings").classes.focused);
+ })
+ .blur(function () {
+ hide_dropdown();
+
+ if ($(input).data("settings").allowFreeTagging) {
+ add_freetagging_tokens();
+ }
+
+ $(this).val("");
+ token_list.removeClass($(input).data("settings").classes.focused);
+ })
+ .bind("keyup keydown blur update", resize_input)
+ .keydown(function (event) {
+ var previous_token;
+ var next_token;
+
+ switch(event.keyCode) {
+ case KEY.LEFT:
+ case KEY.RIGHT:
+ case KEY.UP:
+ case KEY.DOWN:
+ if(this.value.length === 0) {
+ previous_token = input_token.prev();
+ next_token = input_token.next();
+
+ if((previous_token.length && previous_token.get(0) === selected_token) ||
+ (next_token.length && next_token.get(0) === selected_token)) {
+ // Check if there is a previous/next token and it is selected
+ if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) {
+ deselect_token($(selected_token), POSITION.BEFORE);
+ } else {
+ deselect_token($(selected_token), POSITION.AFTER);
+ }
+ } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) {
+ // We are moving left, select the previous token if it exists
+ select_token($(previous_token.get(0)));
+ } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) {
+ // We are moving right, select the next token if it exists
+ select_token($(next_token.get(0)));
+ }
+ } else {
+ var dropdown_item = null;
+
+ if (event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) {
+ dropdown_item = $(dropdown).find('li').first();
+
+ if (selected_dropdown_item) {
+ dropdown_item = $(selected_dropdown_item).next();
+ }
+ } else {
+ dropdown_item = $(dropdown).find('li').last();
+
+ if (selected_dropdown_item) {
+ dropdown_item = $(selected_dropdown_item).prev();
+ }
+ }
+
+ select_dropdown_item(dropdown_item);
+ }
+
+ break;
+
+ case KEY.BACKSPACE:
+ previous_token = input_token.prev();
+
+ if (this.value.length === 0) {
+ if (selected_token) {
+ delete_token($(selected_token));
+ hiddenInput.change();
+ } else if(previous_token.length) {
+ select_token($(previous_token.get(0)));
+ }
+
+ return false;
+ } else if($(this).val().length === 1) {
+ hide_dropdown();
+ } else {
+ // set a timeout just long enough to let this function finish.
+ setTimeout(function(){ do_search(); }, 5);
+ }
+ break;
+
+ case KEY.TAB:
+ case KEY.ENTER:
+ case KEY.NUMPAD_ENTER:
+ case KEY.COMMA:
+ if(selected_dropdown_item) {
+ add_token($(selected_dropdown_item).data("tokeninput"));
+ hiddenInput.change();
+ } else {
+ if ($(input).data("settings").allowFreeTagging) {
+ if($(input).data("settings").allowTabOut && $(this).val() === "") {
+ return true;
+ } else {
+ add_freetagging_tokens();
+ }
+ } else {
+ $(this).val("");
+ if($(input).data("settings").allowTabOut) {
+ return true;
+ }
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ return false;
+
+ case KEY.ESCAPE:
+ hide_dropdown();
+ return true;
+
+ default:
+ if (String.fromCharCode(event.which)) {
+ // set a timeout just long enough to let this function finish.
+ setTimeout(function(){ do_search(); }, 5);
+ }
+ break;
+ }
+ });
+
+ // Keep reference for placeholder
+ if (settings.placeholder) {
+ input_box.attr("placeholder", settings.placeholder);
+ }
+
+ // Keep a reference to the original input box
+ var hiddenInput = $(input)
+ .hide()
+ .val("")
+ .focus(function () {
+ focusWithTimeout(input_box);
+ })
+ .blur(function () {
+ input_box.blur();
+
+ //return the object to this can be referenced in the callback functions.
+ return hiddenInput;
+ })
+ ;
+
+ // Keep a reference to the selected token and dropdown item
+ var selected_token = null;
+ var selected_token_index = 0;
+ var selected_dropdown_item = null;
+
+ // The list to store the token items in
+ var token_list = $("")
+ .addClass($(input).data("settings").classes.tokenList)
+ .click(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
+ toggle_select_token(li);
+ } else {
+ // Deselect selected token
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+
+ // Focus input box
+ focusWithTimeout(input_box);
+ }
+ })
+ .mouseover(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && selected_token !== this) {
+ li.addClass($(input).data("settings").classes.highlightedToken);
+ }
+ })
+ .mouseout(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && selected_token !== this) {
+ li.removeClass($(input).data("settings").classes.highlightedToken);
+ }
+ })
+ .insertBefore(hiddenInput);
+
+ // The token holding the input box
+ var input_token = $("")
+ .addClass($(input).data("settings").classes.inputToken)
+ .appendTo(token_list)
+ .append(input_box);
+
+ // The list to store the dropdown items in
+ var dropdown = $("")
+ .addClass($(input).data("settings").classes.dropdown)
+ .appendTo("body")
+ .hide();
+
+ // Magic element to help us resize the text input
+ var input_resizer = $("")
+ .insertAfter(input_box)
+ .css({
+ position: "absolute",
+ top: -9999,
+ left: -9999,
+ width: "auto",
+ fontSize: input_box.css("fontSize"),
+ fontFamily: input_box.css("fontFamily"),
+ fontWeight: input_box.css("fontWeight"),
+ letterSpacing: input_box.css("letterSpacing"),
+ whiteSpace: "nowrap"
+ });
+
+ // Pre-populate list if items exist
+ hiddenInput.val("");
+ var li_data = $(input).data("settings").prePopulate || hiddenInput.data("pre");
+
+ if ($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) {
+ li_data = $(input).data("settings").onResult.call(hiddenInput, li_data);
+ }
+
+ if (li_data && li_data.length) {
+ $.each(li_data, function (index, value) {
+ insert_token(value);
+ checkTokenLimit();
+ input_box.attr("placeholder", null)
+ });
+ }
+
+ // Check if widget should initialize as disabled
+ if ($(input).data("settings").disabled) {
+ toggleDisabled(true);
+ }
+
+ // Initialization is done
+ if (typeof($(input).data("settings").onReady) === "function") {
+ $(input).data("settings").onReady.call();
+ }
+
+ //
+ // Public functions
+ //
+
+ this.clear = function() {
+ token_list.children("li").each(function() {
+ if ($(this).children("input").length === 0) {
+ delete_token($(this));
+ }
+ });
+ };
+
+ this.add = function(item) {
+ add_token(item);
+ };
+
+ this.remove = function(item) {
+ token_list.children("li").each(function() {
+ if ($(this).children("input").length === 0) {
+ var currToken = $(this).data("tokeninput");
+ var match = true;
+ for (var prop in item) {
+ if (item[prop] !== currToken[prop]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ delete_token($(this));
+ }
+ }
+ });
+ };
+
+ this.getTokens = function() {
+ return saved_tokens;
+ };
+
+ this.toggleDisabled = function(disable) {
+ toggleDisabled(disable);
+ };
+
+ // Resize input to maximum width so the placeholder can be seen
+ resize_input();
+
+ //
+ // Private functions
+ //
+
+ function escapeHTML(text) {
+ return $(input).data("settings").enableHTML ? text : _escapeHTML(text);
+ }
+
+ // Toggles the widget between enabled and disabled state, or according
+ // to the [disable] parameter.
+ function toggleDisabled(disable) {
+ if (typeof disable === 'boolean') {
+ $(input).data("settings").disabled = disable
+ } else {
+ $(input).data("settings").disabled = !$(input).data("settings").disabled;
+ }
+ input_box.attr('disabled', $(input).data("settings").disabled);
+ token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled);
+ // if there is any token selected we deselect it
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+ hiddenInput.attr('disabled', $(input).data("settings").disabled);
+ }
+
+ function checkTokenLimit() {
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+ input_box.hide();
+ hide_dropdown();
+ return;
+ }
+ }
+
+ function resize_input() {
+ if(input_val === (input_val = input_box.val())) {return;}
+
+ // Get width left on the current line
+ var width_left = token_list.width() - input_box.offset().left - token_list.offset().left;
+ // Enter new content into resizer and resize input accordingly
+ input_resizer.html(_escapeHTML(input_val) || _escapeHTML(settings.placeholder));
+ // Get maximum width, minimum the size of input and maximum the widget's width
+ input_box.width(Math.min(token_list.width(),
+ Math.max(width_left, input_resizer.width() + 30)));
+ }
+
+ function add_freetagging_tokens() {
+ var value = $.trim(input_box.val());
+ var tokens = value.split($(input).data("settings").tokenDelimiter);
+ $.each(tokens, function(i, token) {
+ if (!token) {
+ return;
+ }
+
+ if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) {
+ token = $(input).data("settings").onFreeTaggingAdd.call(hiddenInput, token);
+ }
+ var object = {};
+ object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token;
+ add_token(object);
+ });
+ }
+
+ // Inner function to a token to the list
+ function insert_token(item) {
+ var $this_token = $($(input).data("settings").tokenFormatter(item));
+ var readonly = item.readonly === true;
+
+ if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly);
+
+ $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token);
+
+ // The 'delete token' button
+ if(!readonly) {
+ $("" + $(input).data("settings").deleteText + "")
+ .addClass($(input).data("settings").classes.tokenDelete)
+ .appendTo($this_token)
+ .click(function () {
+ if (!$(input).data("settings").disabled) {
+ delete_token($(this).parent());
+ hiddenInput.change();
+ return false;
+ }
+ });
+ }
+
+ // Store data on the token
+ var token_data = item;
+ $.data($this_token.get(0), "tokeninput", item);
+
+ // Save this token for duplicate checking
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
+ selected_token_index++;
+
+ // Update the hidden input
+ update_hiddenInput(saved_tokens, hiddenInput);
+
+ token_count += 1;
+
+ // Check the token limit
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+ input_box.hide();
+ hide_dropdown();
+ }
+
+ return $this_token;
+ }
+
+ // Add a token to the token list based on user input
+ function add_token (item) {
+ var callback = $(input).data("settings").onAdd;
+
+ // See if the token already exists and select it if we don't want duplicates
+ if(token_count > 0 && $(input).data("settings").preventDuplicates) {
+ var found_existing_token = null;
+ token_list.children().each(function () {
+ var existing_token = $(this);
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
+ if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) {
+ found_existing_token = existing_token;
+ return false;
+ }
+ });
+
+ if(found_existing_token) {
+ select_token(found_existing_token);
+ input_token.insertAfter(found_existing_token);
+ focusWithTimeout(input_box);
+ return;
+ }
+ }
+
+ // Squeeze input_box so we force no unnecessary line break
+ input_box.width(1);
+
+ // Insert the new tokens
+ if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) {
+ insert_token(item);
+ // Remove the placeholder so it's not seen after you've added a token
+ input_box.attr("placeholder", null);
+ checkTokenLimit();
+ }
+
+ // Clear input box
+ input_box.val("");
+
+ // Don't show the help dropdown, they've got the idea
+ hide_dropdown();
+
+ // Execute the onAdd callback if defined
+ if($.isFunction(callback)) {
+ callback.call(hiddenInput,item);
+ }
+ }
+
+ // Select a token in the token list
+ function select_token (token) {
+ if (!$(input).data("settings").disabled) {
+ token.addClass($(input).data("settings").classes.selectedToken);
+ selected_token = token.get(0);
+
+ // Hide input box
+ input_box.val("");
+
+ // Hide dropdown if it is visible (eg if we clicked to select token)
+ hide_dropdown();
+ }
+ }
+
+ // Deselect a token in the token list
+ function deselect_token (token, position) {
+ token.removeClass($(input).data("settings").classes.selectedToken);
+ selected_token = null;
+
+ if(position === POSITION.BEFORE) {
+ input_token.insertBefore(token);
+ selected_token_index--;
+ } else if(position === POSITION.AFTER) {
+ input_token.insertAfter(token);
+ selected_token_index++;
+ } else {
+ input_token.appendTo(token_list);
+ selected_token_index = token_count;
+ }
+
+ // Show the input box and give it focus again
+ focusWithTimeout(input_box);
+ }
+
+ // Toggle selection of a token in the token list
+ function toggle_select_token(token) {
+ var previous_selected_token = selected_token;
+
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+
+ if(previous_selected_token === token.get(0)) {
+ deselect_token(token, POSITION.END);
+ } else {
+ select_token(token);
+ }
+ }
+
+ // Delete a token from the token list
+ function delete_token (token) {
+ // Remove the id from the saved list
+ var token_data = $.data(token.get(0), "tokeninput");
+ var callback = $(input).data("settings").onDelete;
+
+ var index = token.prevAll().length;
+ if(index > selected_token_index) index--;
+
+ // Delete the token
+ token.remove();
+ selected_token = null;
+
+ // Show the input box and give it focus again
+ focusWithTimeout(input_box);
+
+ // Remove this token from the saved list
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
+ if (saved_tokens.length == 0) {
+ input_box.attr("placeholder", settings.placeholder)
+ }
+ if(index < selected_token_index) selected_token_index--;
+
+ // Update the hidden input
+ update_hiddenInput(saved_tokens, hiddenInput);
+
+ token_count -= 1;
+
+ if($(input).data("settings").tokenLimit !== null) {
+ input_box
+ .show()
+ .val("");
+ focusWithTimeout(input_box);
+ }
+
+ // Execute the onDelete callback if defined
+ if($.isFunction(callback)) {
+ callback.call(hiddenInput,token_data);
+ }
+ }
+
+ // Update the hidden input box value
+ function update_hiddenInput(saved_tokens, hiddenInput) {
+ var token_values = $.map(saved_tokens, function (el) {
+ if(typeof $(input).data("settings").tokenValue == 'function')
+ return $(input).data("settings").tokenValue.call(this, el);
+
+ return el[$(input).data("settings").tokenValue];
+ });
+ hiddenInput.val(token_values.join($(input).data("settings").tokenDelimiter));
+
+ }
+
+ // Hide and clear the results dropdown
+ function hide_dropdown () {
+ dropdown.hide().empty();
+ selected_dropdown_item = null;
+ }
+
+ function show_dropdown() {
+ dropdown
+ .css({
+ position: "absolute",
+ top: token_list.offset().top + token_list.outerHeight(true),
+ left: token_list.offset().left,
+ width: token_list.width(),
+ 'z-index': $(input).data("settings").zindex
+ })
+ .show();
+ }
+
+ function show_dropdown_searching () {
+ if($(input).data("settings").searchingText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").searchingText) + "
");
+ show_dropdown();
+ }
+ }
+
+ function show_dropdown_hint () {
+ if($(input).data("settings").hintText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").hintText) + "
");
+ show_dropdown();
+ }
+ }
+
+ var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
+ function regexp_escape(term) {
+ return term.replace(regexp_special_chars, '\\$&');
+ }
+
+ // Highlight the query part of the search term
+ function highlight_term(value, term) {
+ return value.replace(
+ new RegExp(
+ "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)",
+ "gi"
+ ), function(match, p1) {
+ return "" + escapeHTML(p1) + "";
+ }
+ );
+ }
+
+ function find_value_and_highlight_term(template, value, term) {
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
+ }
+
+ // exclude existing tokens from dropdown, so the list is clearer
+ function excludeCurrent(results) {
+ if ($(input).data("settings").excludeCurrent) {
+ var currentTokens = $(input).data("tokenInputObject").getTokens(),
+ trimmedList = [];
+ if (currentTokens.length) {
+ $.each(results, function(index, value) {
+ var notFound = true;
+ $.each(currentTokens, function(cIndex, cValue) {
+ if (value[$(input).data("settings").propertyToSearch] == cValue[$(input).data("settings").propertyToSearch]) {
+ notFound = false;
+ return false;
+ }
+ });
+
+ if (notFound) {
+ trimmedList.push(value);
+ }
+ });
+ results = trimmedList;
+ }
+ }
+
+ return results;
+ }
+
+ // Populate the results dropdown with some results
+ function populateDropdown (query, results) {
+ // exclude current tokens if configured
+ results = excludeCurrent(results);
+
+ if(results && results.length) {
+ dropdown.empty();
+ var dropdown_ul = $("")
+ .appendTo(dropdown)
+ .mouseover(function (event) {
+ select_dropdown_item($(event.target).closest("li"));
+ })
+ .mousedown(function (event) {
+ add_token($(event.target).closest("li").data("tokeninput"));
+ hiddenInput.change();
+ return false;
+ })
+ .hide();
+
+ if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) {
+ results = results.slice(0, $(input).data("settings").resultsLimit);
+ }
+
+ $.each(results, function(index, value) {
+ var this_li = $(input).data("settings").resultsFormatter(value);
+
+ this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query);
+ this_li = $(this_li).appendTo(dropdown_ul);
+
+ if(index % 2) {
+ this_li.addClass($(input).data("settings").classes.dropdownItem);
+ } else {
+ this_li.addClass($(input).data("settings").classes.dropdownItem2);
+ }
+
+ if(index === 0 && $(input).data("settings").autoSelectFirstResult) {
+ select_dropdown_item(this_li);
+ }
+
+ $.data(this_li.get(0), "tokeninput", value);
+ });
+
+ show_dropdown();
+
+ if($(input).data("settings").animateDropdown) {
+ dropdown_ul.slideDown("fast");
+ } else {
+ dropdown_ul.show();
+ }
+ } else {
+ if($(input).data("settings").noResultsText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").noResultsText) + "
");
+ show_dropdown();
+ }
+ }
+ }
+
+ // Highlight an item in the results dropdown
+ function select_dropdown_item (item) {
+ if(item) {
+ if(selected_dropdown_item) {
+ deselect_dropdown_item($(selected_dropdown_item));
+ }
+
+ item.addClass($(input).data("settings").classes.selectedDropdownItem);
+ selected_dropdown_item = item.get(0);
+ }
+ }
+
+ // Remove highlighting from an item in the results dropdown
+ function deselect_dropdown_item (item) {
+ item.removeClass($(input).data("settings").classes.selectedDropdownItem);
+ selected_dropdown_item = null;
+ }
+
+ // Do a search and show the "searching" dropdown if the input is longer
+ // than $(input).data("settings").minChars
+ function do_search() {
+ var query = input_box.val();
+
+ if(query && query.length) {
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.AFTER);
+ }
+
+ if(query.length >= $(input).data("settings").minChars) {
+ show_dropdown_searching();
+ clearTimeout(timeout);
+
+ timeout = setTimeout(function(){
+ run_search(query);
+ }, $(input).data("settings").searchDelay);
+ } else {
+ hide_dropdown();
+ }
+ }
+ }
+
+ // Do the actual search
+ function run_search(query) {
+ var cache_key = query + computeURL();
+ var cached_results = cache.get(cache_key);
+ if (cached_results) {
+ if ($.isFunction($(input).data("settings").onCachedResult)) {
+ cached_results = $(input).data("settings").onCachedResult.call(hiddenInput, cached_results);
+ }
+ populateDropdown(query, cached_results);
+ } else {
+ // Are we doing an ajax search or local data search?
+ if($(input).data("settings").url) {
+ var url = computeURL();
+ // Extract existing get params
+ var ajax_params = {};
+ ajax_params.data = {};
+ if(url.indexOf("?") > -1) {
+ var parts = url.split("?");
+ ajax_params.url = parts[0];
+
+ var param_array = parts[1].split("&");
+ $.each(param_array, function (index, value) {
+ var kv = value.split("=");
+ ajax_params.data[kv[0]] = kv[1];
+ });
+ } else {
+ ajax_params.url = url;
+ }
+
+ // Prepare the request
+ ajax_params.data[$(input).data("settings").queryParam] = query;
+ ajax_params.type = $(input).data("settings").method;
+ ajax_params.dataType = $(input).data("settings").contentType;
+ if ($(input).data("settings").crossDomain) {
+ ajax_params.dataType = "jsonp";
+ }
+
+ // exclude current tokens?
+ // send exclude list to the server, so it can also exclude existing tokens
+ if ($(input).data("settings").excludeCurrent) {
+ var currentTokens = $(input).data("tokenInputObject").getTokens();
+ var tokenList = $.map(currentTokens, function (el) {
+ if(typeof $(input).data("settings").tokenValue == 'function')
+ return $(input).data("settings").tokenValue.call(this, el);
+
+ return el[$(input).data("settings").tokenValue];
+ });
+
+ ajax_params.data[$(input).data("settings").excludeCurrentParameter] = tokenList.join($(input).data("settings").tokenDelimiter);
+ }
+
+ // Attach the success callback
+ ajax_params.success = function(results) {
+ cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
+ if($.isFunction($(input).data("settings").onResult)) {
+ results = $(input).data("settings").onResult.call(hiddenInput, results);
+ }
+
+ // only populate the dropdown if the results are associated with the active search query
+ if(input_box.val() === query) {
+ populateDropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
+ }
+ };
+
+ // Provide a beforeSend callback
+ if (settings.onSend) {
+ settings.onSend(ajax_params);
+ }
+
+ // Make the request
+ $.ajax(ajax_params);
+ } else if($(input).data("settings").local_data) {
+ // Do the search through local data
+ var results = $.grep($(input).data("settings").local_data, function (row) {
+ return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
+ });
+
+ cache.add(cache_key, results);
+ if($.isFunction($(input).data("settings").onResult)) {
+ results = $(input).data("settings").onResult.call(hiddenInput, results);
+ }
+ populateDropdown(query, results);
+ }
+ }
+ }
+
+ // compute the dynamic URL
+ function computeURL() {
+ var settings = $(input).data("settings");
+ return typeof settings.url == 'function' ? settings.url.call(settings) : settings.url;
+ }
+
+ // Bring browser focus to the specified object.
+ // Use of setTimeout is to get around an IE bug.
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
+ //
+ // obj: a jQuery object to focus()
+ function focusWithTimeout(object) {
+ setTimeout(
+ function() {
+ object.focus();
+ },
+ 50
+ );
+ }
+ };
+
+ // Really basic cache for the results
+ $.TokenList.Cache = function (options) {
+ var settings, data = {}, size = 0, flush;
+
+ settings = $.extend({ max_size: 500 }, options);
+
+ flush = function () {
+ data = {};
+ size = 0;
+ };
+
+ this.add = function (query, results) {
+ if (size > settings.max_size) {
+ flush();
+ }
+
+ if (!data[query]) {
+ size += 1;
+ }
+
+ data[query] = results;
+ };
+
+ this.get = function (query) {
+ return data[query];
+ };
+ };
+
+}(jQuery));
diff --git a/vendor/assets/stylesheets/token-input.css b/vendor/assets/stylesheets/token-input.css
new file mode 100644
index 0000000..e2b09cb
--- /dev/null
+++ b/vendor/assets/stylesheets/token-input.css
@@ -0,0 +1,143 @@
+/* Example tokeninput style #1: Token vertical list*/
+ul.token-input-list {
+ overflow: hidden;
+ height: auto !important;
+ height: 1%;
+ width: 220px;
+ border: 1px solid #ccc;
+ cursor: text;
+ font-size: 14px;
+ font-family: Verdana, sans-serif;
+ line-height: 10px;
+ vertical-align: middle;
+ z-index: 999;
+ margin-bottom: 0;
+ margin-left: 0 !important;
+ padding: 0;
+ background-color: #fff;
+ list-style-type: none;
+ clear: left;
+ border-radius: 4px;
+}
+
+
+ul.token-input-list.token-input-focused {
+ border-color: rgba(82, 168, 236, 0.8);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
+ outline: 0;
+ outline: thin dotted 9;
+}
+
+ul.token-input-list li {
+ list-style-type: none;
+}
+
+ul.token-input-list li input {
+ border: 0;
+ box-shadow: none !important;
+ outline: none !important;
+ width: 220px !important;
+ padding: 4px 6px;
+ margin-bottom: 0 !important;
+ background-color: white;
+ -webkit-appearance: caret;
+}
+
+ul.token-input-disabled,
+ul.token-input-disabled li input {
+ background-color: #E8E8E8;
+}
+
+ul.token-input-disabled li.token-input-token {
+ background-color: #b0b0b0;
+ color: #7D7D7D
+}
+
+ul.token-input-disabled li.token-input-token span {
+ color: #CFCFCF;
+ cursor: default;
+}
+
+li.token-input-token {
+ overflow: hidden;
+ height: auto !important;
+ height: 1%;;
+ margin: 3px;
+ padding: 3px 5px;
+ background-color: #b0b0b0;
+ color: #000;
+ font-weight: bold;
+ cursor: default;
+ display: block;
+}
+
+li.token-input-token p {
+ float: left;
+ padding: 0;
+ margin: 0;
+}
+
+li.token-input-token span {
+ float: right;
+ color: #777;
+ cursor: pointer;
+}
+
+li.token-input-selected-token {
+ background-color: #333333;
+ color: #fff;
+}
+
+li.token-input-selected-token span {
+ color: #bbb;
+}
+
+div.token-input-dropdown {
+ position: absolute;
+ width: 220px;
+ background-color: #fff;
+ overflow: hidden;
+ border-left: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-bottom: 1px solid #ccc;
+ cursor: default;
+ font-size: 12px;
+ font-family: Verdana, sans-serif;
+ z-index: 1;
+ border-radius: 3px;
+}
+
+div.token-input-dropdown p {
+ margin: 0;
+ padding: 5px;
+ font-weight: bold;
+ color: #777;
+}
+
+div.token-input-dropdown ul {
+ margin: 0;
+ padding: 0;
+}
+
+div.token-input-dropdown ul li {
+ background-color: #fff;
+ padding: 3px;
+ list-style-type: none;
+}
+
+div.token-input-dropdown ul li.token-input-dropdown-item {
+ background-color: #fafafa;
+}
+
+div.token-input-dropdown ul li.token-input-dropdown-item2 {
+ background-color: #fff;
+}
+
+div.token-input-dropdown ul li em {
+ font-weight: bold;
+ font-style: normal;
+}
+
+div.token-input-dropdown ul li.token-input-selected-dropdown-item {
+ background-color: #b0b0b0;
+}
diff --git a/vendor/cache/actionmailer-4.0.1.gem b/vendor/cache/actionmailer-4.0.1.gem
new file mode 100644
index 0000000..1beac4d
Binary files /dev/null and b/vendor/cache/actionmailer-4.0.1.gem differ
diff --git a/vendor/cache/actionpack-4.0.1.gem b/vendor/cache/actionpack-4.0.1.gem
new file mode 100644
index 0000000..438f856
Binary files /dev/null and b/vendor/cache/actionpack-4.0.1.gem differ
diff --git a/vendor/cache/activemodel-4.0.1.gem b/vendor/cache/activemodel-4.0.1.gem
new file mode 100644
index 0000000..697256d
Binary files /dev/null and b/vendor/cache/activemodel-4.0.1.gem differ
diff --git a/vendor/cache/activerecord-4.0.1.gem b/vendor/cache/activerecord-4.0.1.gem
new file mode 100644
index 0000000..834251a
Binary files /dev/null and b/vendor/cache/activerecord-4.0.1.gem differ
diff --git a/vendor/cache/activerecord-deprecated_finders-1.0.3.gem b/vendor/cache/activerecord-deprecated_finders-1.0.3.gem
new file mode 100644
index 0000000..ac30ab9
Binary files /dev/null and b/vendor/cache/activerecord-deprecated_finders-1.0.3.gem differ
diff --git a/vendor/cache/activesupport-4.0.1.gem b/vendor/cache/activesupport-4.0.1.gem
new file mode 100644
index 0000000..bfca142
Binary files /dev/null and b/vendor/cache/activesupport-4.0.1.gem differ
diff --git a/vendor/cache/acts-as-taggable-on-3.5.0.gem b/vendor/cache/acts-as-taggable-on-3.5.0.gem
new file mode 100644
index 0000000..2723ac2
Binary files /dev/null and b/vendor/cache/acts-as-taggable-on-3.5.0.gem differ
diff --git a/vendor/cache/arel-4.0.1.gem b/vendor/cache/arel-4.0.1.gem
new file mode 100644
index 0000000..71329ff
Binary files /dev/null and b/vendor/cache/arel-4.0.1.gem differ
diff --git a/vendor/cache/atomic-1.1.14.gem b/vendor/cache/atomic-1.1.14.gem
new file mode 100644
index 0000000..7c87df2
Binary files /dev/null and b/vendor/cache/atomic-1.1.14.gem differ
diff --git a/vendor/cache/bcrypt-ruby-3.1.2.gem b/vendor/cache/bcrypt-ruby-3.1.2.gem
new file mode 100644
index 0000000..b2cda30
Binary files /dev/null and b/vendor/cache/bcrypt-ruby-3.1.2.gem differ
diff --git a/vendor/cache/bootstrap-sass-2.3.2.0.gem b/vendor/cache/bootstrap-sass-2.3.2.0.gem
new file mode 100644
index 0000000..0c84b60
Binary files /dev/null and b/vendor/cache/bootstrap-sass-2.3.2.0.gem differ
diff --git a/vendor/cache/builder-3.1.4.gem b/vendor/cache/builder-3.1.4.gem
new file mode 100644
index 0000000..b6090be
Binary files /dev/null and b/vendor/cache/builder-3.1.4.gem differ
diff --git a/vendor/cache/charlock_holmes-0.6.9.4.gem b/vendor/cache/charlock_holmes-0.6.9.4.gem
new file mode 100644
index 0000000..f8a4f6e
Binary files /dev/null and b/vendor/cache/charlock_holmes-0.6.9.4.gem differ
diff --git a/vendor/cache/coffee-rails-4.0.0.gem b/vendor/cache/coffee-rails-4.0.0.gem
new file mode 100644
index 0000000..b210a32
Binary files /dev/null and b/vendor/cache/coffee-rails-4.0.0.gem differ
diff --git a/vendor/cache/coffee-script-2.2.0.gem b/vendor/cache/coffee-script-2.2.0.gem
new file mode 100644
index 0000000..c33f4d9
Binary files /dev/null and b/vendor/cache/coffee-script-2.2.0.gem differ
diff --git a/vendor/cache/coffee-script-source-1.6.2.gem b/vendor/cache/coffee-script-source-1.6.2.gem
new file mode 100644
index 0000000..76b8b7c
Binary files /dev/null and b/vendor/cache/coffee-script-source-1.6.2.gem differ
diff --git a/vendor/cache/daemons-1.1.9.gem b/vendor/cache/daemons-1.1.9.gem
new file mode 100644
index 0000000..8a64804
Binary files /dev/null and b/vendor/cache/daemons-1.1.9.gem differ
diff --git a/vendor/cache/devise-3.0.0.rc.gem b/vendor/cache/devise-3.0.0.rc.gem
new file mode 100644
index 0000000..cc326fd
Binary files /dev/null and b/vendor/cache/devise-3.0.0.rc.gem differ
diff --git a/vendor/cache/diffy-2.1.4.gem b/vendor/cache/diffy-2.1.4.gem
new file mode 100644
index 0000000..33f9358
Binary files /dev/null and b/vendor/cache/diffy-2.1.4.gem differ
diff --git a/vendor/cache/erubis-2.7.0.gem b/vendor/cache/erubis-2.7.0.gem
new file mode 100644
index 0000000..4acd2e7
Binary files /dev/null and b/vendor/cache/erubis-2.7.0.gem differ
diff --git a/vendor/cache/escape_utils-0.2.4.gem b/vendor/cache/escape_utils-0.2.4.gem
new file mode 100644
index 0000000..2d05a6e
Binary files /dev/null and b/vendor/cache/escape_utils-0.2.4.gem differ
diff --git a/vendor/cache/eventmachine-1.0.3.gem b/vendor/cache/eventmachine-1.0.3.gem
new file mode 100644
index 0000000..ca7ddbb
Binary files /dev/null and b/vendor/cache/eventmachine-1.0.3.gem differ
diff --git a/vendor/cache/execjs-1.4.0.gem b/vendor/cache/execjs-1.4.0.gem
new file mode 100644
index 0000000..815ab62
Binary files /dev/null and b/vendor/cache/execjs-1.4.0.gem differ
diff --git a/vendor/cache/github-linguist-2.3.4.gem b/vendor/cache/github-linguist-2.3.4.gem
new file mode 100644
index 0000000..f1c0dcc
Binary files /dev/null and b/vendor/cache/github-linguist-2.3.4.gem differ
diff --git a/vendor/cache/hike-1.2.3.gem b/vendor/cache/hike-1.2.3.gem
new file mode 100644
index 0000000..3dd8fe7
Binary files /dev/null and b/vendor/cache/hike-1.2.3.gem differ
diff --git a/vendor/cache/i18n-0.6.5.gem b/vendor/cache/i18n-0.6.5.gem
new file mode 100644
index 0000000..186c34b
Binary files /dev/null and b/vendor/cache/i18n-0.6.5.gem differ
diff --git a/vendor/cache/jquery-rails-3.0.1.gem b/vendor/cache/jquery-rails-3.0.1.gem
new file mode 100644
index 0000000..2c9ab9a
Binary files /dev/null and b/vendor/cache/jquery-rails-3.0.1.gem differ
diff --git a/vendor/cache/mail-2.5.4.gem b/vendor/cache/mail-2.5.4.gem
new file mode 100644
index 0000000..bc7eea2
Binary files /dev/null and b/vendor/cache/mail-2.5.4.gem differ
diff --git a/vendor/cache/mime-types-1.25.gem b/vendor/cache/mime-types-1.25.gem
new file mode 100644
index 0000000..deaca38
Binary files /dev/null and b/vendor/cache/mime-types-1.25.gem differ
diff --git a/vendor/cache/minitest-4.7.5.gem b/vendor/cache/minitest-4.7.5.gem
new file mode 100644
index 0000000..24410e2
Binary files /dev/null and b/vendor/cache/minitest-4.7.5.gem differ
diff --git a/vendor/cache/multi_json-1.8.2.gem b/vendor/cache/multi_json-1.8.2.gem
new file mode 100644
index 0000000..8d64dcc
Binary files /dev/null and b/vendor/cache/multi_json-1.8.2.gem differ
diff --git a/vendor/cache/orm_adapter-0.4.0.gem b/vendor/cache/orm_adapter-0.4.0.gem
new file mode 100644
index 0000000..95cb6b2
Binary files /dev/null and b/vendor/cache/orm_adapter-0.4.0.gem differ
diff --git a/vendor/cache/polyglot-0.3.3.gem b/vendor/cache/polyglot-0.3.3.gem
new file mode 100644
index 0000000..0b87664
Binary files /dev/null and b/vendor/cache/polyglot-0.3.3.gem differ
diff --git a/vendor/cache/posix-spawn-0.3.6.gem b/vendor/cache/posix-spawn-0.3.6.gem
new file mode 100644
index 0000000..1156f3a
Binary files /dev/null and b/vendor/cache/posix-spawn-0.3.6.gem differ
diff --git a/vendor/cache/protected_attributes-1.1.3.gem b/vendor/cache/protected_attributes-1.1.3.gem
new file mode 100644
index 0000000..0e010e9
Binary files /dev/null and b/vendor/cache/protected_attributes-1.1.3.gem differ
diff --git a/vendor/cache/rack-1.5.2.gem b/vendor/cache/rack-1.5.2.gem
new file mode 100644
index 0000000..e1f7bfd
Binary files /dev/null and b/vendor/cache/rack-1.5.2.gem differ
diff --git a/vendor/cache/rack-test-0.6.2.gem b/vendor/cache/rack-test-0.6.2.gem
new file mode 100644
index 0000000..4934321
Binary files /dev/null and b/vendor/cache/rack-test-0.6.2.gem differ
diff --git a/vendor/cache/rails-4.0.1.gem b/vendor/cache/rails-4.0.1.gem
new file mode 100644
index 0000000..e2097c6
Binary files /dev/null and b/vendor/cache/rails-4.0.1.gem differ
diff --git a/vendor/cache/rails-assets-chosen-1.6.1.gem b/vendor/cache/rails-assets-chosen-1.6.1.gem
new file mode 100644
index 0000000..ce3ac18
Binary files /dev/null and b/vendor/cache/rails-assets-chosen-1.6.1.gem differ
diff --git a/vendor/cache/rails-assets-jquery-3.1.0.gem b/vendor/cache/rails-assets-jquery-3.1.0.gem
new file mode 100644
index 0000000..e6c8d73
Binary files /dev/null and b/vendor/cache/rails-assets-jquery-3.1.0.gem differ
diff --git a/vendor/cache/rails-jquery-tokeninput-0.2.6.gem b/vendor/cache/rails-jquery-tokeninput-0.2.6.gem
new file mode 100644
index 0000000..129fb8b
Binary files /dev/null and b/vendor/cache/rails-jquery-tokeninput-0.2.6.gem differ
diff --git a/vendor/cache/railties-4.0.1.gem b/vendor/cache/railties-4.0.1.gem
new file mode 100644
index 0000000..397a4a3
Binary files /dev/null and b/vendor/cache/railties-4.0.1.gem differ
diff --git a/vendor/cache/rake-10.1.0.gem b/vendor/cache/rake-10.1.0.gem
new file mode 100644
index 0000000..5402cd6
Binary files /dev/null and b/vendor/cache/rake-10.1.0.gem differ
diff --git a/vendor/cache/redcarpet-3.3.4.gem b/vendor/cache/redcarpet-3.3.4.gem
new file mode 100644
index 0000000..f6a202a
Binary files /dev/null and b/vendor/cache/redcarpet-3.3.4.gem differ
diff --git a/vendor/cache/sass-3.2.9.gem b/vendor/cache/sass-3.2.9.gem
new file mode 100644
index 0000000..07812e7
Binary files /dev/null and b/vendor/cache/sass-3.2.9.gem differ
diff --git a/vendor/cache/sass-rails-4.0.0.gem b/vendor/cache/sass-rails-4.0.0.gem
new file mode 100644
index 0000000..86048c9
Binary files /dev/null and b/vendor/cache/sass-rails-4.0.0.gem differ
diff --git a/vendor/cache/sprockets-2.10.0.gem b/vendor/cache/sprockets-2.10.0.gem
new file mode 100644
index 0000000..c15b00c
Binary files /dev/null and b/vendor/cache/sprockets-2.10.0.gem differ
diff --git a/vendor/cache/sprockets-rails-2.0.1.gem b/vendor/cache/sprockets-rails-2.0.1.gem
new file mode 100644
index 0000000..b115d28
Binary files /dev/null and b/vendor/cache/sprockets-rails-2.0.1.gem differ
diff --git a/vendor/cache/sqlite3-1.3.7.gem b/vendor/cache/sqlite3-1.3.7.gem
new file mode 100644
index 0000000..b7895b2
Binary files /dev/null and b/vendor/cache/sqlite3-1.3.7.gem differ
diff --git a/vendor/cache/thin-1.6.1.gem b/vendor/cache/thin-1.6.1.gem
new file mode 100644
index 0000000..63d9b7e
Binary files /dev/null and b/vendor/cache/thin-1.6.1.gem differ
diff --git a/vendor/cache/thor-0.18.1.gem b/vendor/cache/thor-0.18.1.gem
new file mode 100644
index 0000000..9fd0308
Binary files /dev/null and b/vendor/cache/thor-0.18.1.gem differ
diff --git a/vendor/cache/thread_safe-0.1.3.gem b/vendor/cache/thread_safe-0.1.3.gem
new file mode 100644
index 0000000..d605b1b
Binary files /dev/null and b/vendor/cache/thread_safe-0.1.3.gem differ
diff --git a/vendor/cache/tilt-1.4.1.gem b/vendor/cache/tilt-1.4.1.gem
new file mode 100644
index 0000000..3ad79a9
Binary files /dev/null and b/vendor/cache/tilt-1.4.1.gem differ
diff --git a/vendor/cache/treetop-1.4.15.gem b/vendor/cache/treetop-1.4.15.gem
new file mode 100644
index 0000000..0a621ef
Binary files /dev/null and b/vendor/cache/treetop-1.4.15.gem differ
diff --git a/vendor/cache/tzinfo-0.3.38.gem b/vendor/cache/tzinfo-0.3.38.gem
new file mode 100644
index 0000000..9b7672b
Binary files /dev/null and b/vendor/cache/tzinfo-0.3.38.gem differ
diff --git a/vendor/cache/uglifier-2.1.1.gem b/vendor/cache/uglifier-2.1.1.gem
new file mode 100644
index 0000000..a29f88b
Binary files /dev/null and b/vendor/cache/uglifier-2.1.1.gem differ
diff --git a/vendor/cache/warden-1.2.3.gem b/vendor/cache/warden-1.2.3.gem
new file mode 100644
index 0000000..3b0e4c2
Binary files /dev/null and b/vendor/cache/warden-1.2.3.gem differ
diff --git a/vendor/cache/yajl-ruby-1.1.0.gem b/vendor/cache/yajl-ruby-1.1.0.gem
new file mode 100644
index 0000000..3fcb580
Binary files /dev/null and b/vendor/cache/yajl-ruby-1.1.0.gem differ