Commit 79e1ad7bc95de7c8b5c312b57d904181b45f7b6f

Authored by Rodrigo Souto
2 parents b1d5a85f 181fc268

Merge branch 'master' into stoa

Conflicts:
	app/controllers/application_controller.rb
	app/controllers/my_profile/memberships_controller.rb
	app/controllers/public/account_controller.rb
	app/helpers/application_helper.rb
	app/helpers/layout_helper.rb
	app/helpers/token_helper.rb
	app/views/account/signup.html.erb
	app/views/layouts/_javascript.html.erb
	db/schema.rb
	test/functional/application_controller_test.rb
Showing 61 changed files with 3943 additions and 303 deletions   Show diff stats
app/controllers/admin/categories_controller.rb
... ... @@ -45,9 +45,11 @@ class CategoriesController < AdminController
45 45 if request.post?
46 46 @category.update_attributes!(params[:category])
47 47 @saved = true
  48 + session[:notice] = _("Category %s saved." % @category.name)
48 49 redirect_to :action => 'index'
49 50 end
50 51 rescue Exception => e
  52 + session[:notice] = _('Could not save category.')
51 53 render :action => 'edit'
52 54 end
53 55 end
... ...
app/controllers/admin/environment_design_controller.rb
... ... @@ -3,9 +3,7 @@ class EnvironmentDesignController < BoxOrganizerController
3 3 protect 'edit_environment_design', :environment
4 4  
5 5 def available_blocks
6   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
7   - # the Noosfero core soon, see ActionItem3045
8   - @available_blocks ||= [ ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
  6 + @available_blocks ||= [ ArticleBlock, LoginBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
9 7 @available_blocks += plugins.dispatch(:extra_blocks, :type => Environment)
10 8 end
11 9  
... ...
app/controllers/admin/features_controller.rb
... ... @@ -51,4 +51,10 @@ class FeaturesController < AdminController
51 51 redirect_to :action => 'manage_fields'
52 52 end
53 53  
  54 + def search_members
  55 + arg = params[:q].downcase
  56 + result = environment.people.find(:all, :conditions => ['LOWER(name) LIKE ? OR identifier LIKE ?', "%#{arg}%", "%#{arg}%"])
  57 + render :text => prepare_to_token_input(result).to_json
  58 + end
  59 +
54 60 end
... ...
app/controllers/application_controller.rb
... ... @@ -7,6 +7,12 @@ class ApplicationController < ActionController::Base
7 7 before_filter :detect_stuff_by_domain
8 8 before_filter :init_noosfero_plugins
9 9 before_filter :allow_cross_domain_access
  10 + before_filter :login_required, :if => :private_environment?
  11 + before_filter :verify_members_whitelist, :if => [:private_environment?, :user]
  12 +
  13 + def verify_members_whitelist
  14 + render_access_denied unless user.is_admin? || environment.in_whitelist?(user)
  15 + end
10 16  
11 17 after_filter :set_csrf_cookie
12 18  
... ... @@ -185,4 +191,8 @@ class ApplicationController < ActionController::Base
185 191 def find_suggestions(query, context, asset, options={})
186 192 plugins.dispatch_first(:find_suggestions, query, context, asset, options)
187 193 end
  194 +
  195 + def private_environment?
  196 + @environment.enabled?(:restrict_to_members)
  197 + end
188 198 end
... ...
app/controllers/my_profile/cms_controller.rb
... ... @@ -4,6 +4,12 @@ class CmsController < MyProfileController
4 4  
5 5 include ArticleHelper
6 6  
  7 + def search_tags
  8 + arg = params[:term].downcase
  9 + result = ActsAsTaggableOn::Tag.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"])
  10 + render :text => prepare_to_token_input_by_label(result).to_json, :content_type => 'application/json'
  11 + end
  12 +
7 13 def self.protect_if(*args)
8 14 before_filter(*args) do |c|
9 15 user, profile = c.send(:user), c.send(:profile)
... ...
app/controllers/my_profile/memberships_controller.rb
... ... @@ -27,7 +27,7 @@ class MembershipsController < MyProfileController
27 27 redirect_to :action => 'welcome', :community_id => @community.id, :back_to => @back_to
28 28 rescue ActiveRecord::RecordNotFound
29 29 # Community pending approval
30   - session[:notice] = _('Your community creation request is waiting approval of the administrator.')
  30 + session[:notice] = _('Your new community creation request will be evaluated by an administrator. You will be notified.')
31 31 redirect_to @back_to
32 32 end
33 33 return
... ...
app/controllers/my_profile/tasks_controller.rb
... ... @@ -4,6 +4,7 @@ class TasksController < MyProfileController
4 4  
5 5 def index
6 6 @filter = params[:filter_type].blank? ? nil : params[:filter_type]
  7 + @task_types = Task.pending_types_for(profile)
7 8 @tasks = Task.to(profile).without_spam.pending.of(@filter).order_by('created_at', 'asc').paginate(:per_page => Task.per_page, :page => params[:page])
8 9 @failed = params ? params[:failed] : {}
9 10 end
... ...
app/controllers/public/account_controller.rb
... ... @@ -15,11 +15,23 @@ class AccountController < ApplicationController
15 15  
16 16 def activate
17 17 @user = User.find_by_activation_code(params[:activation_code]) if params[:activation_code]
18   - if @user and @user.activate
19   - @message = _("Your account has been activated, now you can log in!")
20   - check_redirection
21   - session[:join] = params[:join] unless params[:join].blank?
22   - render :action => 'login', :userlogin => @user.login
  18 + if @user
  19 + unless @user.environment.enabled?('admin_must_approve_new_users')
  20 + if @user.activate
  21 + @message = _("Your account has been activated, now you can log in!")
  22 + check_redirection
  23 + session[:join] = params[:join] unless params[:join].blank?
  24 + render :action => 'login', :userlogin => @user.login
  25 + end
  26 + else
  27 + if @user.create_moderate_task
  28 + session[:notice] = _('Thanks for registering. The administrators were notified.')
  29 + @register_pending = true
  30 + @user.activation_code = nil
  31 + @user.save!
  32 + redirect_to :controller => :home
  33 + end
  34 + end
23 35 else
24 36 session[:notice] = _("It looks like you're trying to activate an account. Perhaps have already activated this account?")
25 37 redirect_to :controller => :home
... ... @@ -111,6 +123,7 @@ class AccountController < ApplicationController
111 123 go_to_signup_initial_page
112 124 else
113 125 redirect_to :controller => :home, :action => :welcome, :template_id => @user.person.template.id
  126 + session[:notice] = _('Thanks for registering!')
114 127 end
115 128 end
116 129 end
... ...
app/helpers/application_helper.rb
... ... @@ -1438,4 +1438,15 @@ module ApplicationHelper
1438 1438 ''
1439 1439 end
1440 1440 end
  1441 +
  1442 + def labelled_colorpicker_field(human_name, object_name, method, options = {})
  1443 + options[:id] ||= 'text-field-' + FormsHelper.next_id_number
  1444 + content_tag('label', human_name, :for => options[:id], :class => 'formlabel') +
  1445 + colorpicker_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1446 + end
  1447 +
  1448 + def colorpicker_field(object_name, method, options = {})
  1449 + text_field(object_name, method, options.merge(:class => 'colorpicker_field'))
  1450 + end
  1451 +
1441 1452 end
... ...
app/helpers/article_helper.rb
... ... @@ -83,6 +83,10 @@ module ArticleHelper
83 83 array.map { |object| {:id => object.id, :name => object.name} }
84 84 end
85 85  
  86 + def prepare_to_token_input_by_label(array)
  87 + array.map { |object| {:label => object.name, :value => object.name} }
  88 + end
  89 +
86 90 def cms_label_for_new_children
87 91 _('New article')
88 92 end
... ...
app/helpers/categories_helper.rb
1 1 module CategoriesHelper
2 2  
3   -
4   - COLORS = [
5   - [ N_('Do not display at the menu'), nil ],
6   - [ N_('Orange'), 1],
7   - [ N_('Green'), 2],
8   - [ N_('Purple'), 3],
9   - [ N_('Red'), 4],
10   - [ N_('Dark Green'), 5],
11   - [ N_('Blue Oil'), 6],
12   - [ N_('Blue'), 7],
13   - [ N_('Brown'), 8],
14   - [ N_('Light Green'), 9],
15   - [ N_('Light Blue'), 10],
16   - [ N_('Dark Blue'), 11],
17   - [ N_('Blue Pool'), 12],
18   - [ N_('Beige'), 13],
19   - [ N_('Yellow'), 14],
20   - [ N_('Light Brown'), 15]
21   - ]
22   -
23 3 TYPES = [
24 4 [ _('General Category'), Category.to_s ],
25 5 [ _('Product Category'), ProductCategory.to_s ],
26 6 [ _('Region'), Region.to_s ],
27 7 ]
28 8  
29   - def select_color_for_category
30   - if @category.top_level?
31   - labelled_form_field(_('Display at the menu?'), select('category', 'display_color', CategoriesHelper::COLORS.map {|item| [gettext(item[0]), item[1]] }))
32   - else
33   - ""
34   - end
35   - end
36   -
37   - def display_color_for_category(category)
38   - color = category.display_color
39   - if color.nil?
40   - ""
41   - else
42   - "[" + gettext(CategoriesHelper::COLORS.find {|item| item[1] == color}.first) + "]"
43   - end
44   - end
45   -
46 9 def select_category_type(field)
47 10 value = params[field]
48 11 labelled_form_field(_('Type of category'), select_tag('type', options_for_select(TYPES, value)))
49 12 end
50 13  
  14 + def category_color_style(category)
  15 + return '' if category.nil? or category.display_color.blank?
  16 + 'background-color: #'+category.display_color+';'
  17 + end
  18 +
51 19 #FIXME make this test
52 20 def selected_category_link(cat)
53 21 js_remove = "jQuery('#selected-category-#{cat.id}').remove();"
... ...
app/helpers/layout_helper.rb
... ... @@ -28,6 +28,7 @@ module LayoutHelper
28 28 'lightbox',
29 29 'colorbox',
30 30 'selectordie',
  31 + 'inputosaurus',
31 32 pngfix_stylesheet_path,
32 33 ] + tokeninput_stylesheets
33 34 plugins_stylesheets = @plugins.select(&:stylesheet?).map { |plugin| plugin.class.public_path('style.css') }
... ...
app/helpers/token_helper.rb
... ... @@ -19,6 +19,7 @@ module TokenHelper
19 19 options[:on_add] ||= 'null'
20 20 options[:on_delete] ||= 'null'
21 21 options[:on_ready] ||= 'null'
  22 + options[:query_param] ||= 'q'
22 23  
23 24 result = text_field_tag(name, nil, text_field_options.merge(html_options.merge({:id => element_id})))
24 25 result += javascript_tag("jQuery('##{element_id}')
... ... @@ -32,7 +33,7 @@ module TokenHelper
32 33 preventDuplicates: #{options[:prevent_duplicates].to_json},
33 34 backspaceDeleteItem: #{options[:backspace_delete_item].to_json},
34 35 zindex: #{options[:zindex].to_json},
35   - queryParam: #{name.to_json},
  36 + queryParam: #{options[:query_param].to_json},
36 37 tokenLimit: #{options[:token_limit].to_json},
37 38 onResult: #{options[:on_result]},
38 39 onAdd: #{options[:on_add]},
... ...
app/models/box.rb
... ... @@ -28,9 +28,6 @@ class Box < ActiveRecord::Base
28 28 CategoriesBlock,
29 29 CommunitiesBlock,
30 30 EnterprisesBlock,
31   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
32   - # the Noosfero core soon, see ActionItem3045
33   - EnvironmentStatisticsBlock,
34 31 FansBlock,
35 32 FavoriteEnterprisesBlock,
36 33 FeedReaderBlock,
... ... @@ -53,9 +50,6 @@ class Box < ActiveRecord::Base
53 50 CommunitiesBlock,
54 51 DisabledEnterpriseMessageBlock,
55 52 EnterprisesBlock,
56   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
57   - # the Noosfero core soon, see ActionItem3045
58   - EnvironmentStatisticsBlock,
59 53 FansBlock,
60 54 FavoriteEnterprisesBlock,
61 55 FeaturedProductsBlock,
... ...
app/models/category.rb
... ... @@ -14,9 +14,6 @@ class Category < ActiveRecord::Base
14 14 validates_uniqueness_of :slug,:scope => [ :environment_id, :parent_id ], :message => N_('{fn} is already being used by another category.').fix_i18n
15 15 belongs_to :environment
16 16  
17   - validates_inclusion_of :display_color, :in => 1..15, :allow_nil => true
18   - validates_uniqueness_of :display_color, :scope => :environment_id, :if => (lambda { |cat| ! cat.display_color.nil? }), :message => N_('{fn} was already assigned to another category.').fix_i18n
19   -
20 17 # Finds all top level categories for a given environment.
21 18 scope :top_level_for, lambda { |environment|
22 19 {:conditions => ['parent_id is null and environment_id = ?', environment.id ]}
... ... @@ -42,6 +39,13 @@ class Category < ActiveRecord::Base
42 39  
43 40 acts_as_having_image
44 41  
  42 + before_save :normalize_display_color
  43 +
  44 + def normalize_display_color
  45 + display_color.gsub!('#', '') if display_color
  46 + display_color = nil if display_color.blank?
  47 + end
  48 +
45 49 scope :from_types, lambda { |types|
46 50 types.select{ |t| t.blank? }.empty? ?
47 51 { :conditions => { :type => types } } :
... ... @@ -101,4 +105,12 @@ class Category < ActiveRecord::Base
101 105 self.children.find(:all, :conditions => {:display_in_menu => true}).empty?
102 106 end
103 107  
  108 + def with_color
  109 + if display_color.blank?
  110 + parent.nil? ? nil : parent.with_color
  111 + else
  112 + self
  113 + end
  114 + end
  115 +
104 116 end
... ...
app/models/environment.rb
... ... @@ -3,7 +3,7 @@
3 3 # domains.
4 4 class Environment < ActiveRecord::Base
5 5  
6   - attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body
  6 + attr_accessible :name, :is_default, :signup_welcome_text_subject, :signup_welcome_text_body, :terms_of_use, :message_for_disabled_enterprise, :news_amount_by_folder, :default_language, :languages, :description, :organization_approval_method, :enabled_plugins, :enabled_features, :redirection_after_login, :redirection_after_signup, :contact_email, :theme, :reports_lower_bound, :noreply_email, :signup_welcome_screen_body, :members_whitelist_enabled, :members_whitelist
7 7  
8 8 has_many :users
9 9  
... ... @@ -125,6 +125,7 @@ class Environment &lt; ActiveRecord::Base
125 125 'organizations_are_moderated_by_default' => _("Organizations have moderated publication by default"),
126 126 'enable_organization_url_change' => _("Allow organizations to change their URL"),
127 127 'admin_must_approve_new_communities' => _("Admin must approve creation of communities"),
  128 + 'admin_must_approve_new_users' => _("Admin must approve registration of new users"),
128 129 'show_balloon_with_profile_links_when_clicked' => _('Show a balloon with profile links when a profile image is clicked'),
129 130 'xmpp_chat' => _('XMPP/Jabber based chat'),
130 131 'show_zoom_button_on_article_images' => _('Show a zoom link on all article images'),
... ... @@ -133,7 +134,8 @@ class Environment &lt; ActiveRecord::Base
133 134 'send_welcome_email_to_new_users' => _('Send welcome e-mail to new users'),
134 135 'allow_change_of_redirection_after_login' => _('Allow users to set the page to redirect after login'),
135 136 'display_my_communities_on_user_menu' => _('Display on menu the list of communities the user can manage'),
136   - 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage')
  137 + 'display_my_enterprises_on_user_menu' => _('Display on menu the list of enterprises the user can manage'),
  138 + 'restrict_to_members' => _('Show content only to members')
137 139 }
138 140 end
139 141  
... ... @@ -177,9 +179,6 @@ class Environment &lt; ActiveRecord::Base
177 179  
178 180 # "left" area
179 181 env.boxes[1].blocks << LoginBlock.new
180   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
181   - # the Noosfero core soon, see ActionItem3045
182   - env.boxes[1].blocks << EnvironmentStatisticsBlock.new
183 182 env.boxes[1].blocks << RecentDocumentsBlock.new
184 183  
185 184 # "right" area
... ... @@ -305,6 +304,17 @@ class Environment &lt; ActiveRecord::Base
305 304 settings[:signup_welcome_screen_body].present?
306 305 end
307 306  
  307 + settings_items :members_whitelist_enabled, :type => :boolean, :default => false
  308 + settings_items :members_whitelist, :type => Array, :default => []
  309 +
  310 + def in_whitelist?(person)
  311 + !members_whitelist_enabled || members_whitelist.include?(person.id)
  312 + end
  313 +
  314 + def members_whitelist=(members)
  315 + settings[:members_whitelist] = members.split(',').map(&:to_i)
  316 + end
  317 +
308 318 def news_amount_by_folder=(amount)
309 319 settings[:news_amount_by_folder] = amount.to_i
310 320 end
... ...
app/models/environment_statistics_block.rb
... ... @@ -1,33 +0,0 @@
1   -# TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
2   -# the Noosfero core soon, see ActionItem3045
3   -
4   -class EnvironmentStatisticsBlock < Block
5   -
6   - def self.description
7   - _('Environment stastistics (DEPRECATED)')
8   - end
9   -
10   - def default_title
11   - _('Statistics for %s') % owner.name
12   - end
13   -
14   - def help
15   - _('This block presents some statistics about your environment.')
16   - end
17   -
18   - def content(args={})
19   - users = owner.people.visible.count
20   - enterprises = owner.enterprises.visible.count
21   - communities = owner.communities.visible.count
22   -
23   - info = []
24   - info << (n_('One user', '%{num} users', users) % { :num => users })
25   - unless owner.enabled?('disable_asset_enterprises')
26   - info << (n_('One enterprise', '%{num} enterprises', enterprises) % { :num => enterprises })
27   - end
28   - info << (n_('One community', '%{num} communities', communities) % { :num => communities })
29   -
30   - block_title(title) + content_tag('ul', info.map {|item| content_tag('li', item) }.join("\n"))
31   - end
32   -
33   -end
app/models/moderate_user_registration.rb 0 → 100644
... ... @@ -0,0 +1,59 @@
  1 +class ModerateUserRegistration < Task
  2 +
  3 + settings_items :user_id, :type => String
  4 + settings_items :name, :type => String
  5 + settings_items :author_name, :type => String
  6 + settings_items :email, :type => String
  7 +
  8 + after_create :schedule_spam_checking
  9 +
  10 + alias :environment :target
  11 + alias :environment= :target=
  12 +
  13 + def schedule_spam_checking
  14 + self.delay.check_for_spam
  15 + end
  16 +
  17 + include Noosfero::Plugin::HotSpot
  18 +
  19 + def sender
  20 + "#{name} (#{email})"
  21 + end
  22 +
  23 + def perform
  24 + user=environment.users.find_by_id(user_id)
  25 + user.activate
  26 + end
  27 +
  28 + def title
  29 + _("New user")
  30 + end
  31 +
  32 + def subject
  33 + name
  34 + end
  35 +
  36 + def information
  37 + { :message => _('%{sender} wants to register.'),
  38 + :variables => {:sender => sender} }
  39 + end
  40 +
  41 + def icon
  42 + result = {:type => :defined_image, :src => '/images/icons-app/person-minor.png', :name => name}
  43 + end
  44 +
  45 + def target_notification_description
  46 + _('%{sender} tried to register.') %
  47 + {:sender => sender}
  48 + end
  49 +
  50 + def target_notification_message
  51 + target_notification_description + "\n\n" +
  52 + _('You need to login on %{system} in order to approve or reject this user.') % { :environment => self.environment }
  53 + end
  54 +
  55 + def target_notification_message
  56 + _("User \"%{user}\" just requested to register. You have to approve or reject it through the \"Pending Validations\" section in your control panel.\n") % { :user => self.name }
  57 + end
  58 +
  59 +end
0 60 \ No newline at end of file
... ...
app/models/task.rb
... ... @@ -73,10 +73,6 @@ class Task &lt; ActiveRecord::Base
73 73 end
74 74 end
75 75  
76   - def self.all_types
77   - %w[Invitation EnterpriseActivation AddMember Ticket SuggestArticle AddFriend CreateCommunity AbuseComplaint ApproveComment ApproveArticle CreateEnterprise ChangePassword EmailActivation InviteFriend InviteMember]
78   - end
79   -
80 76 # this method finished the task. It calls #perform, which must be overriden
81 77 # by subclasses. At the end a message (as returned by #finish_message) is
82 78 # sent to the requestor with #notify_requestor.
... ... @@ -254,6 +250,10 @@ class Task &lt; ActiveRecord::Base
254 250 { :conditions => [environment_condition, profile_condition].compact.join(' OR ') }
255 251 }
256 252  
  253 + def self.pending_types_for(profile)
  254 + Task.to(profile).pending.select('distinct type').map { |t| [t.class.name, t.title] }
  255 + end
  256 +
257 257 def opened?
258 258 status == Task::Status::ACTIVE || status == Task::Status::HIDDEN
259 259 end
... ...
app/models/user.rb
... ... @@ -51,8 +51,12 @@ class User &lt; ActiveRecord::Base
51 51  
52 52 user.person = p
53 53 end
54   - if user.environment.enabled?('skip_new_user_email_confirmation')
55   - user.activate
  54 + if user.environment.enabled?('skip_new_user_email_confirmation')
  55 + if user.environment.enabled?('admin_must_approve_new_users')
  56 + create_moderate_task
  57 + else
  58 + user.activate
  59 + end
56 60 end
57 61 end
58 62 after_create :deliver_activation_code
... ... @@ -141,6 +145,15 @@ class User &lt; ActiveRecord::Base
141 145 end
142 146 end
143 147  
  148 + def create_moderate_task
  149 + @task = ModerateUserRegistration.new
  150 + @task.user_id = self.id
  151 + @task.name = self.name
  152 + @task.email = self.email
  153 + @task.target = self.environment
  154 + @task.save
  155 + end
  156 +
144 157 def activated?
145 158 self.activation_code.nil? && !self.activated_at.nil?
146 159 end
... ...
app/sweepers/profile_sweeper.rb
... ... @@ -8,9 +8,6 @@ class ProfileSweeper # &lt; ActiveRecord::Observer
8 8 end
9 9  
10 10 def after_create(profile)
11   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
12   - # the Noosfero core soon, see ActionItem3045
13   - expire_statistics_block_cache(profile)
14 11 end
15 12  
16 13 protected
... ... @@ -31,13 +28,6 @@ protected
31 28 expire_blogs(profile) if profile.organization?
32 29 end
33 30  
34   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
35   - # the Noosfero core soon, see ActionItem3045
36   - def expire_statistics_block_cache(profile)
37   - blocks = profile.environment.blocks.select { |b| b.kind_of?(EnvironmentStatisticsBlock) }
38   - BlockSweeper.expire_blocks(blocks)
39   - end
40   -
41 31 def expire_blogs(profile)
42 32 profile.blogs.select{|b| !b.empty?}.each do |blog|
43 33 pages = blog.posts.count / blog.posts_per_page + 1
... ...
app/views/categories/_category.html.erb
1 1 <li>
2 2 <div class='treeitem'>
3   - <%= display_color_for_category(category) %>
4   - <%= category.name %>
  3 + <% unless category_color_style(category).empty? %>
  4 + <span class="color_marker" style="<%= category_color_style(category) %>" ></span>
  5 + <% end %>
  6 + <span><%= category.name %></span>
  7 +
5 8 <% if category.children.count > 0 %>
6 9 <div class='button' id="category-loading-<%= category.id %>" style="position: relative;">
7 10 <a href="#" id="show-button-<%= category.id %>" class="show-button" onclick="return false;" data-category="<%= category.id %>"><%= _('Show') %></a>
... ...
app/views/categories/_form.html.erb
  1 +<%= stylesheet_link_tag 'spectrum.css' %>
  2 +<%= javascript_include_tag "spectrum.js" %>
  3 +<%= javascript_include_tag "colorpicker-noosfero.js" %>
  4 +
1 5 <%= error_messages_for 'category' %>
2 6  
3 7 <%= labelled_form_for 'category', :html => { :multipart => true} do |f| %>
... ... @@ -13,12 +17,13 @@
13 17 <% end %>
14 18 <% end %>
15 19  
16   - <%= select_color_for_category if !environment.enabled?('disable_categories_menu') %>
17   -
18 20 <%= required f.text_field('name') %>
19 21  
20 22 <%= labelled_check_box(_('Display in the menu'), 'category[display_in_menu]', '1', @category.display_in_menu) %>
21 23  
  24 + <%= labelled_colorpicker_field(_('Pick a color'), :category, 'display_color' ) unless environment.enabled?('disable_categories_menu')%>
  25 + <span id="color_preview" class = "color_marker" style="<%= category_color_style(@category) %>" ></span>
  26 +
22 27 <%= f.fields_for :image_builder, @category.image do |i| %>
23 28 <%= file_field_or_thumbnail(_('Image:'), @category.image, i) %>
24 29 <% end %>
... ...
app/views/cms/edit.html.erb
... ... @@ -31,9 +31,18 @@
31 31  
32 32 <%= select_categories(:article, _('Categorize your article')) %>
33 33  
  34 + <br />
  35 +
34 36 <%= f.text_field('tag_list', :size => 64) %>
35 37 <%= content_tag( 'small', _('Separate tags with commas') ) %>
36 38  
  39 + <script>
  40 + jQuery('#article_tag_list').inputosaurus({
  41 + autoCompleteSource: <%= "'/myprofile/#{profile.identifier}/cms/search_tags'," %>
  42 + activateFinalResult : true
  43 + })
  44 + </script>
  45 +
37 46 <div id='edit-article-options'>
38 47 <%= options_for_article(@article, @tokenized_children) %>
39 48 </div>
... ...
app/views/features/index.html.erb
... ... @@ -37,6 +37,18 @@ Check all the features you want to enable for your environment, uncheck all the
37 37 <%= select_organization_approval_method('environment', 'organization_approval_method') %>
38 38 <hr/>
39 39  
  40 +<h3><%= _('Members Whitelist') %></h3>
  41 + <div class="option">
  42 + <%= check_box :environment, :members_whitelist_enabled %>
  43 + <label><%= _('Enable whitelist') %></label>
  44 + </div>
  45 + <div class="input">
  46 + <div class="info"><%= _('Allow these people to access this environment:') %></div>
  47 + <% tokenized_members = prepare_to_token_input(environment.people.find(:all, :conditions => {:id => environment.members_whitelist})) %>
  48 + <%= token_input_field_tag('environment[members_whitelist]', 'search-members', {:action => 'search_members'}, {:focus => false, :hint_text => _('Type in a search term for a user'), :pre_populate => tokenized_members}) %>
  49 + </div>
  50 +<hr/>
  51 +
40 52 <div>
41 53 <% button_bar do %>
42 54 <%= submit_button('save', _('Save changes')) %>
... ...
app/views/home/welcome.html.erb
... ... @@ -6,11 +6,15 @@
6 6 <%= environment.settings[:signup_welcome_screen_body].html_safe %>
7 7 <% else %>
8 8 <h3><%= _("Thanks for signing up, we're thrilled to have you on our social network!") %></h3>
9   - <p><%= _("Firstly, some tips for getting started:") %></p>
10 9 <% if @display_confirmation_tips %>
  10 + <p><%= _("Firstly, some tips for getting started:") %></p>
11 11 <h4><%= _("Confirm your account!") %></h4>
12 12 <p><%= _("You should receive a welcome email from us shortly. Please take a second to follow the link within to confirm your account.") %></p>
13 13 <p><%= _("You won't appear as %s until your account is confirmed.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
  14 + <% else %>
  15 + <h4><%= _("Wait for admin approvement!") %></h4>
  16 + <p><%= _("The administrators will evaluate your signup request for approvement.") %></p>
  17 + <p><%= _("You won't appear as %s until your account is approved.") % link_to(_('user'), {:controller => :search, :action => :people, :filter => 'more_recent'}, :target => '_blank') %></p>
14 18 <% end %>
15 19 <h4><%= _("What to do next?") %></h4>
16 20 <p><%= _("Access your %s and see your face on the network!") % link_to(_('Profile'), {:controller => 'profile', :profile => user.identifier}, :target => '_blank') %></p>
... ...
app/views/layouts/_javascript.html.erb
... ... @@ -4,7 +4,7 @@
4 4 'jquery-ui-1.10.4/js/jquery-ui-1.10.4.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate',
5 5 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'jquery.typewatch', 'jquery.textchange',
6 6 'add-and-join', 'report-abuse', 'catalog', 'manage-products', 'autogrow', 'select-or-die/_src/selectordie',
7   -'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', :cache => 'cache/application' %>
  7 +'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', 'inputosaurus.js', :cache => 'cache/application' %>
8 8  
9 9 <% language = FastGettext.locale %>
10 10 <% %w{messages methods}.each do |type| %>
... ...
app/views/tasks/index.html.erb
... ... @@ -3,10 +3,9 @@
3 3 <h1><%= _("%s's pending tasks") % profile.name %></h1>
4 4 <p>
5 5  
6   -<% type_collection = [[nil, _('All')]] %>
7   -<% type_collection += Task.all_types.sort_by {|klass| klass.constantize.new.title}.map{|s| [s, s.constantize.new.title] } %>
8   -
9   -
  6 +<%
  7 + type_collection = [[nil, _('All')]] + @task_types
  8 +%>
10 9  
11 10 <% if !@failed.blank? %>
12 11 <div id="errorExplanation">
... ... @@ -39,7 +38,7 @@
39 38  
40 39 <ul class='task-list'>
41 40 <p>
42   - <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :onchange => 'document.location.href = "?filter_type="+this.value')%>
  41 + <%= labelled_select(_('Filter')+': ', :filter_type, :first, :last, @filter, type_collection, :onchange => "document.location.href = '?filter_type='+this.value") %>
43 42 </p>
44 43 <p>
45 44 <%= labelled_select(_("Set all to: "), 'set-decisions', 'first', 'last', nil, [['',""],['accept',_("Accept")],['reject',_("Reject")],['skip',_("Skip")]], :id => "up-set-all-tasks-to") %>
... ...
baseplugins/statistics 0 → 120000
... ... @@ -0,0 +1 @@
  1 +../plugins/statistics
0 2 \ No newline at end of file
... ...
config/initializers/passenger.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +if defined? PhusionPassenger
  2 +
  3 + # from http://russbrooks.com/2010/10/20/rails-cache-memcache-on-passenger-with-smart-spawning
  4 + PhusionPassenger.on_event :starting_worker_process do |forked|
  5 + if forked
  6 + Rails.cache.instance_variable_get(:@data).reset if Rails.cache.class == ActiveSupport::Cache::MemCacheStore
  7 + end
  8 + end
  9 +end
... ...
db/migrate/20140724134601_fix_yaml_encoding.rb
... ... @@ -6,6 +6,7 @@ class FixYamlEncoding &lt; ActiveRecord::Migration
6 6 fix_encoding(Profile, 'data')
7 7 fix_encoding(ActionTracker::Record, 'params')
8 8 fix_encoding(Article, 'setting')
  9 + fix_encoding(Task, 'data')
9 10 end
10 11  
11 12 def self.down
... ...
db/migrate/20140807134625_change_category_display_color_to_string.rb 0 → 100644
... ... @@ -0,0 +1,36 @@
  1 +class ChangeCategoryDisplayColorToString < ActiveRecord::Migration
  2 +
  3 + COLORS = ['ffa500', '00FF00', 'a020f0', 'ff0000', '006400', '191970', '0000ff', 'a52a2a', '32cd32', 'add8e6', '483d8b', 'b8e9ee', 'f5f5dc', 'ffff00', 'f4a460']
  4 +
  5 + def self.up
  6 + change_table :categories do |t|
  7 + t.string :display_color_tmp, :limit => 6
  8 + end
  9 +
  10 + COLORS.each_with_index do |color, i|
  11 + Category.update_all({:display_color_tmp => color}, {:display_color => i+1})
  12 + end
  13 +
  14 + change_table :categories do |t|
  15 + t.remove :display_color
  16 + t.rename :display_color_tmp, :display_color
  17 + end
  18 + end
  19 +
  20 + def self.down
  21 + puts "WARNING: only old defined colors will be reverted"
  22 +
  23 + change_table :categories do |t|
  24 + t.integer :display_color_tmp
  25 + end
  26 +
  27 + COLORS.each_with_index do |color, i|
  28 + Category.update_all({:display_color_tmp => i+1}, {:display_color => color})
  29 + end
  30 +
  31 + change_table :categories do |t|
  32 + t.remove :display_color
  33 + t.rename :display_color_tmp, :display_color
  34 + end
  35 + end
  36 +end
... ...
db/migrate/20140827191326_remove_environment_statistics_block.rb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +class RemoveEnvironmentStatisticsBlock < ActiveRecord::Migration
  2 + def self.up
  3 + update("UPDATE blocks SET type = 'StatisticsBlock' WHERE type = 'EnvironmentStatisticsBlock'")
  4 + end
  5 +
  6 + def self.down
  7 + say("Nothing to undo (cannot recover the data)")
  8 + end
  9 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20140811141211) do
  14 +ActiveRecord::Schema.define(:version => 20140827191326) do
15 15  
16 16 create_table "abuse_reports", :force => true do |t|
17 17 t.integer "reporter_id"
... ... @@ -194,19 +194,19 @@ ActiveRecord::Schema.define(:version =&gt; 20140811141211) do
194 194 create_table "categories", :force => true do |t|
195 195 t.string "name"
196 196 t.string "slug"
197   - t.text "path", :default => ""
198   - t.integer "display_color"
  197 + t.text "path", :default => ""
199 198 t.integer "environment_id"
200 199 t.integer "parent_id"
201 200 t.string "type"
202 201 t.float "lat"
203 202 t.float "lng"
204   - t.boolean "display_in_menu", :default => false
205   - t.integer "children_count", :default => 0
206   - t.boolean "accept_products", :default => true
  203 + t.boolean "display_in_menu", :default => false
  204 + t.integer "children_count", :default => 0
  205 + t.boolean "accept_products", :default => true
207 206 t.integer "image_id"
208 207 t.string "acronym"
209 208 t.string "abbreviation"
  209 + t.string "display_color", :limit => 6
210 210 end
211 211  
212 212 create_table "categories_profiles", :id => false, :force => true do |t|
... ...
features/signup.feature
... ... @@ -313,3 +313,55 @@ Feature: signup
313 313 And wait for the captcha signup time
314 314 And I press "Create my account"
315 315 Then "José da Silva" should be a member of "Free Software"
  316 +
  317 + @selenium
  318 + Scenario: user registration is moderated by admin
  319 + Given feature "admin_must_approve_new_users" is enabled on environment
  320 + And feature "skip_new_user_email_confirmation" is disabled on environment
  321 + And I go to /account/signup
  322 + And I fill in "Username" with "teste"
  323 + And I fill in "Password" with "123456"
  324 + And I fill in "Password confirmation" with "123456"
  325 + And I fill in "e-Mail" with "teste@teste.com"
  326 + And I fill in "Full name" with "Teste da Silva"
  327 + And wait for the captcha signup time
  328 + And I press "Create my account"
  329 + And I go to teste's confirmation URL
  330 + And I am logged in as admin
  331 + And I follow "Control panel"
  332 + And I follow "Tasks"
  333 + And I choose "Accept"
  334 + And I press "Apply!"
  335 + And I follow "Logout"
  336 + And Teste da Silva's account is activated
  337 + And I follow "Login"
  338 + And I fill in "Username / Email" with "teste"
  339 + And I fill in "Password" with "123456"
  340 + And I press "Log in"
  341 + Then I should see "teste"
  342 +
  343 +
  344 + @selenium
  345 + Scenario: user registration is not accepted by the admin
  346 + Given feature "admin_must_approve_new_users" is enabled on environment
  347 + And feature "skip_new_user_email_confirmation" is disabled on environment
  348 + And I go to /account/signup
  349 + And I fill in "Username" with "teste"
  350 + And I fill in "Password" with "123456"
  351 + And I fill in "Password confirmation" with "123456"
  352 + And I fill in "e-Mail" with "teste@teste.com"
  353 + And I fill in "Full name" with "Teste da Silva"
  354 + And wait for the captcha signup time
  355 + And I press "Create my account"
  356 + And I go to teste's confirmation URL
  357 + And I am logged in as admin
  358 + And I follow "Control panel"
  359 + And I follow "Tasks"
  360 + And I choose "Reject"
  361 + And I press "Apply!"
  362 + And I follow "Logout"
  363 + And I follow "Login"
  364 + And I fill in "Username / Email" with "teste"
  365 + And I fill in "Password" with "123456"
  366 + And I press "Log in"
  367 + Then I should not see "teste"
316 368 \ No newline at end of file
... ...
features/step_definitions/web_steps.rb
... ... @@ -11,8 +11,14 @@ require File.expand_path(File.join(File.dirname(__FILE__), &quot;..&quot;, &quot;support&quot;, &quot;pat
11 11  
12 12 module WithinHelpers
13 13 def with_scope(locator)
14   - locator = locator ? first(locator) : locator
15   - locator ? within(locator) { yield } : yield
  14 + if locator
  15 + locator = first(locator) || locator
  16 + within(locator) do
  17 + yield
  18 + end
  19 + else
  20 + yield
  21 + end
16 22 end
17 23 end
18 24 World(WithinHelpers)
... ...
lib/tasks/plugins.rake
... ... @@ -7,7 +7,11 @@ namespace :noosfero do
7 7 plugin_migration_dirs = Dir.glob(Rails.root.join('{baseplugins,config/plugins}', '*', 'db', 'migrate'))
8 8  
9 9 task :load_config do
10   - dirs = Dir.glob("{baseplugins,config/plugins}/*/db/migrate")
  10 + dirs = Dir.glob("{baseplugins,config/plugins}/*").uniq do |dir|
  11 + File.basename(dir)
  12 + end.map do |dir|
  13 + File.join(dir, 'db/migrate')
  14 + end
11 15 dirs.each do |dir|
12 16 ActiveRecord::Migrator.migrations_paths << dir
13 17 end
... ...
plugins/community_track/lib/community_track_plugin/track_helper.rb
1 1 module CommunityTrackPlugin::TrackHelper
2 2  
  3 + include CategoriesHelper
  4 +
3 5 def category_class(track)
4 6 'category_' + (track.categories.empty? ? 'not_defined' : track.categories.first.name.to_slug)
5 7 end
... ... @@ -9,4 +11,13 @@ module CommunityTrackPlugin::TrackHelper
9 11 excerpt(lead_stripped, lead_stripped.first(3), track.image ? 180 : 300)
10 12 end
11 13  
  14 + def track_color_style(track)
  15 + category_color_style(track.categories.first.with_color) if !track.categories.empty?
  16 + end
  17 +
  18 + def track_name_color_style(track)
  19 + category = track.categories.empty? ? nil : track.categories.first.with_color
  20 + category ? "color: ##{category.display_color};" : ''
  21 + end
  22 +
12 23 end
... ...
plugins/community_track/test/unit/community_track_plugin/track_helper_test.rb
... ... @@ -52,4 +52,16 @@ class TrackHelperTest &lt; ActiveSupport::TestCase
52 52 assert_equal 186, track_card_lead(@track).length
53 53 end
54 54  
  55 + should 'return category color if its defined' do
  56 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  57 + @track.categories << category1
  58 + assert_equal 'background-color: #fbfbfb;', track_color_style(@track)
  59 + end
  60 +
  61 + should 'return category color for track name' do
  62 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  63 + @track.categories << category1
  64 + assert_equal 'color: #fbfbfb;', track_name_color_style(@track)
  65 + end
  66 +
55 67 end
... ...
plugins/community_track/views/blocks/_track_card.html.erb
... ... @@ -2,13 +2,13 @@
2 2 <div class="item_card <%= category_class(track_card) %>">
3 3 <a href="<%= url_for track_card.url %>">
4 4 <div class="track_content">
5   - <div class="title">
  5 + <div class="title" style="<%= track_color_style(track_card) %>">
6 6 <%= track_card.category_name %>
7 7 </div>
8 8 <div class="image">
9 9 <%= image_tag track_card.image.public_filename if track_card.image %>
10 10 </div>
11   - <div class="name">
  11 + <div class="name" style="<%= track_name_color_style(track_card) %>">
12 12 <%= track_card.name %>
13 13 </div>
14 14 <div class="lead">
... ...
po/pt/noosfero.po
... ... @@ -13,7 +13,7 @@ msgid &quot;&quot;
13 13 msgstr ""
14 14 "Project-Id-Version: noosfero 0.47.1\n"
15 15 "POT-Creation-Date: 2014-06-05 20:27-0000\n"
16   -"PO-Revision-Date: 2013-07-30 12:53-0300\n"
  16 +"PO-Revision-Date: 2014-08-22 11:19-0300\n"
17 17 "Last-Translator: Rodrigo Souto <rodrigo@colivre.coop.br>\n"
18 18 "Language-Team: Noosfero Develeopers <noosfero-dev@listas.softwarelivre.org>\n"
19 19 "Language: pt\n"
... ... @@ -469,7 +469,7 @@ msgstr &quot;Local: &quot;
469 469 #: app/helpers/boxes_helper.rb:37 app/helpers/boxes_helper.rb:63
470 470 #: app/models/main_block.rb:4
471 471 msgid "Main content"
472   -msgstr "Gerenciar conteúdo"
  472 +msgstr "Conteúdo principal"
473 473  
474 474 #: app/helpers/boxes_helper.rb:100
475 475 msgid "This block is invisible. Your visitors will not see it."
... ... @@ -8925,7 +8925,7 @@ msgstr &quot;Boas vindas ao ambiente %s&quot;
8925 8925 msgid ""
8926 8926 "Thanks for signing up, we're thrilled to have you on our social network!"
8927 8927 msgstr ""
8928   -"Obrigado por se registrar! Agora verifique seu e-mail para ativar sua conta!"
  8928 +"Obrigado por se registrar, estamos empolgados de ter você na nossa rede social!"
8929 8929  
8930 8930 #: app/views/account/signup.rhtml:5
8931 8931 msgid "Firstly, some tips for getting started:"
... ...
public/javascripts/colorpicker-noosfero.js 0 → 100644
... ... @@ -0,0 +1,25 @@
  1 +jQuery(document).ready(function($) {
  2 + $(".colorpicker_field").spectrum({
  3 + showInput: true,
  4 + showInitial: false,
  5 + preferredFormat: "hex",
  6 + allowEmpty: true,
  7 + showPalette: true,
  8 + palette: [
  9 + ["rgb(0, 0, 0)", "rgb(67, 67, 67)", "rgb(102, 102, 102)",
  10 + "rgb(204, 204, 204)", "rgb(217, 217, 217)","rgb(255, 255, 255)"],
  11 + ["rgb(152, 0, 0)", "rgb(255, 0, 0)", "rgb(255, 153, 0)", "rgb(255, 255, 0)", "rgb(0, 255, 0)",
  12 + "rgb(0, 255, 255)", "rgb(74, 134, 232)", "rgb(0, 0, 255)", "rgb(153, 0, 255)", "rgb(255, 0, 255)"],
  13 + ["rgb(230, 184, 175)", "rgb(244, 204, 204)", "rgb(252, 229, 205)", "rgb(255, 242, 204)", "rgb(217, 234, 211)",
  14 + "rgb(208, 224, 227)", "rgb(201, 218, 248)", "rgb(207, 226, 243)", "rgb(217, 210, 233)", "rgb(234, 209, 220)",
  15 + "rgb(221, 126, 107)", "rgb(234, 153, 153)", "rgb(249, 203, 156)", "rgb(255, 229, 153)", "rgb(182, 215, 168)",
  16 + "rgb(162, 196, 201)", "rgb(164, 194, 244)", "rgb(159, 197, 232)", "rgb(180, 167, 214)", "rgb(213, 166, 189)",
  17 + "rgb(204, 65, 37)", "rgb(224, 102, 102)", "rgb(246, 178, 107)", "rgb(255, 217, 102)", "rgb(147, 196, 125)",
  18 + "rgb(118, 165, 175)", "rgb(109, 158, 235)", "rgb(111, 168, 220)", "rgb(142, 124, 195)", "rgb(194, 123, 160)",
  19 + "rgb(166, 28, 0)", "rgb(204, 0, 0)", "rgb(230, 145, 56)", "rgb(241, 194, 50)", "rgb(106, 168, 79)",
  20 + "rgb(69, 129, 142)", "rgb(60, 120, 216)", "rgb(61, 133, 198)", "rgb(103, 78, 167)", "rgb(166, 77, 121)",
  21 + "rgb(91, 15, 0)", "rgb(102, 0, 0)", "rgb(120, 63, 4)", "rgb(127, 96, 0)", "rgb(39, 78, 19)",
  22 + "rgb(12, 52, 61)", "rgb(28, 69, 135)", "rgb(7, 55, 99)", "rgb(32, 18, 77)", "rgb(76, 17, 48)"]
  23 + ]
  24 + });
  25 +});
... ...
public/javascripts/inputosaurus.js 0 → 100644
... ... @@ -0,0 +1,523 @@
  1 +/**
  2 + * Inputosaurus Text
  3 + *
  4 + * Must be instantiated on an <input> element
  5 + * Allows multiple input items. Each item is represented with a removable tag that appears to be inside the input area.
  6 + *
  7 + * @requires:
  8 + *
  9 + * jQuery 1.7+
  10 + * jQueryUI 1.8+ Core
  11 + *
  12 + * @version 0.1.6
  13 + * @author Dan Kielp <dan@sproutsocial.com>
  14 + * @created October 3,2012
  15 + *
  16 + */
  17 +
  18 +
  19 +(function($) {
  20 +
  21 + var inputosaurustext = {
  22 +
  23 + version: "0.1.6",
  24 +
  25 + eventprefix: "inputosaurus",
  26 +
  27 + options: {
  28 +
  29 + // bindable events
  30 + //
  31 + // 'change' - triggered whenever a tag is added or removed (should be similar to binding the the change event of the instantiated input
  32 + // 'keyup' - keyup event on the newly created input
  33 +
  34 + // while typing, the user can separate values using these delimiters
  35 + // the value tags are created on the fly when an inputDelimiter is detected
  36 + inputDelimiters : [',', ';'],
  37 +
  38 + // this separator is used to rejoin all input items back to the value of the original <input>
  39 + outputDelimiter : ',',
  40 +
  41 + allowDuplicates : false,
  42 +
  43 + parseOnBlur : false,
  44 +
  45 + // optional wrapper for widget
  46 + wrapperElement : null,
  47 +
  48 + width : null,
  49 +
  50 + // simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality
  51 + autoCompleteSource : '',
  52 +
  53 + // When forcing users to select from the autocomplete list, allow them to press 'Enter' to select an item if it's the only option left.
  54 + activateFinalResult : false,
  55 +
  56 + // manipulate and return the input value after parseInput() parsing
  57 + // the array of tag names is passed and expected to be returned as an array after manipulation
  58 + parseHook : null,
  59 +
  60 + // define a placeholder to display when the input is empty
  61 + placeholder: null,
  62 +
  63 + // when you check for duplicates it check for the case
  64 + caseSensitiveDuplicates: false
  65 + },
  66 +
  67 + _create: function() {
  68 + var widget = this,
  69 + els = {},
  70 + o = widget.options,
  71 + placeholder = o.placeholder || this.element.attr('placeholder') || null;
  72 +
  73 + this._chosenValues = [];
  74 +
  75 + // Create the elements
  76 + els.ul = $('<ul class="inputosaurus-container">');
  77 + els.input = $('<input type="text" />');
  78 + els.inputCont = $('<li class="inputosaurus-input inputosaurus-required"></li>');
  79 + els.origInputCont = $('<li class="inputosaurus-input-hidden inputosaurus-required">');
  80 +
  81 + // define starting placeholder
  82 + if (placeholder) {
  83 + o.placeholder = placeholder;
  84 + els.input.attr('placeholder', o.placeholder);
  85 + if (o.width) {
  86 + els.input.css('min-width', o.width - 50);
  87 + }
  88 + }
  89 +
  90 + o.wrapperElement && o.wrapperElement.append(els.ul);
  91 + this.element.replaceWith(o.wrapperElement || els.ul);
  92 + els.origInputCont.append(this.element).hide();
  93 +
  94 + els.inputCont.append(els.input);
  95 + els.ul.append(els.inputCont);
  96 + els.ul.append(els.origInputCont);
  97 +
  98 + o.width && els.ul.css('width', o.width);
  99 +
  100 + this.elements = els;
  101 +
  102 + widget._attachEvents();
  103 +
  104 + // if instantiated input already contains a value, parse that junk
  105 + if($.trim(this.element.val())){
  106 + els.input.val( this.element.val() );
  107 + this.parseInput();
  108 + }
  109 +
  110 + this._instAutocomplete();
  111 + },
  112 +
  113 + _instAutocomplete : function() {
  114 + if(this.options.autoCompleteSource){
  115 + var widget = this;
  116 +
  117 + this.elements.input.autocomplete({
  118 + position : {
  119 + of : this.elements.ul
  120 + },
  121 + source : this.options.autoCompleteSource,
  122 + minLength : 1,
  123 + select : function(ev, ui){
  124 + ev.preventDefault();
  125 + widget.elements.input.val(ui.item.value);
  126 + widget.parseInput();
  127 + },
  128 + open : function() {
  129 + // Older versions of jQueryUI have a different namespace
  130 + var auto = $(this).data('ui-autocomplete') || $(this).data('autocomplete');
  131 + var menu = auto.menu,
  132 + $menuItems;
  133 +
  134 +
  135 + // zIndex will force the element on top of anything (like a dialog it's in)
  136 + menu.element.zIndex && menu.element.zIndex($(this).zIndex() + 1);
  137 + menu.element.width(widget.elements.ul.outerWidth());
  138 +
  139 + // auto-activate the result if it's the only one
  140 + if(widget.options.activateFinalResult){
  141 + $menuItems = menu.element.find('li');
  142 +
  143 + // activate single item to allow selection upon pressing 'Enter'
  144 + if($menuItems.size() === 1){
  145 + menu[menu.activate ? 'activate' : 'focus']($.Event('click'), $menuItems);
  146 + }
  147 + }
  148 + }
  149 + });
  150 + }
  151 + },
  152 +
  153 + _autoCompleteMenuPosition : function() {
  154 + var widget;
  155 + if(this.options.autoCompleteSource){
  156 + widget = this.elements.input.data('ui-autocomplete') || this.elements.input.data('autocomplete');
  157 + widget && widget.menu.element.position({
  158 + of: this.elements.ul,
  159 + my: 'left top',
  160 + at: 'left bottom',
  161 + collision: 'none'
  162 + });
  163 + }
  164 + },
  165 +
  166 + /*_closeAutoCompleteMenu : function() {
  167 + if(this.options.autoCompleteSource){
  168 + this.elements.input.autocomplete('close');
  169 + }
  170 + },*/
  171 +
  172 + parseInput : function(ev) {
  173 + var widget = (ev && ev.data.widget) || this,
  174 + val,
  175 + delimiterFound = false,
  176 + values = [];
  177 +
  178 + val = widget.elements.input.val();
  179 +
  180 + val && (delimiterFound = widget._containsDelimiter(val));
  181 +
  182 + if(delimiterFound !== false){
  183 + values = val.split(delimiterFound);
  184 + } else if(!ev || ev.which === $.ui.keyCode.ENTER && !$('.ui-menu-item.ui-state-focus').size() && !$('.ui-menu-item .ui-state-focus').size() && !$('#ui-active-menuitem').size()){
  185 + values.push(val);
  186 + ev && ev.preventDefault();
  187 +
  188 + // prevent autoComplete menu click from causing a false 'blur'
  189 + } else if(ev.type === 'blur' && !$('#ui-active-menuitem').size()){
  190 + values.push(val);
  191 + }
  192 +
  193 + $.isFunction(widget.options.parseHook) && (values = widget.options.parseHook(values));
  194 +
  195 + if(values.length){
  196 + widget._setChosen(values);
  197 + widget.elements.input.val('');
  198 + widget._resizeInput();
  199 + }
  200 +
  201 + widget._resetPlaceholder();
  202 + },
  203 +
  204 + _inputFocus : function(ev) {
  205 + var widget = ev.data.widget || this;
  206 +
  207 + widget.elements.input.value || (widget.options.autoCompleteSource.length && widget.elements.input.autocomplete('search', ''));
  208 + },
  209 +
  210 + _inputKeypress : function(ev) {
  211 + var widget = ev.data.widget || this;
  212 +
  213 + ev.type === 'keyup' && widget._trigger('keyup', ev, widget);
  214 +
  215 + switch(ev.which){
  216 + case $.ui.keyCode.BACKSPACE:
  217 + ev.type === 'keydown' && widget._inputBackspace(ev);
  218 + break;
  219 +
  220 + case $.ui.keyCode.LEFT:
  221 + ev.type === 'keydown' && widget._inputBackspace(ev);
  222 + break;
  223 +
  224 + default :
  225 + widget.parseInput(ev);
  226 + widget._resizeInput(ev);
  227 + }
  228 +
  229 + // reposition autoComplete menu as <ul> grows and shrinks vertically
  230 + if(widget.options.autoCompleteSource){
  231 + setTimeout(function(){widget._autoCompleteMenuPosition.call(widget);}, 200);
  232 + }
  233 + },
  234 +
  235 + // the input dynamically resizes based on the length of its value
  236 + _resizeInput : function(ev) {
  237 + var widget = (ev && ev.data.widget) || this,
  238 + maxWidth = widget.elements.ul.width(),
  239 + val = widget.elements.input.val(),
  240 + txtWidth = 25 + val.length * 8;
  241 +
  242 + widget.elements.input.width(txtWidth < maxWidth ? txtWidth : maxWidth);
  243 + },
  244 +
  245 + // resets placeholder on representative input
  246 + _resetPlaceholder: function () {
  247 + var placeholder = this.options.placeholder,
  248 + input = this.elements.input,
  249 + width = this.options.width || 'inherit';
  250 + if (placeholder && this.element.val().length === 0) {
  251 + input.attr('placeholder', placeholder).css('min-width', width - 50)
  252 + }else {
  253 + input.attr('placeholder', '').css('min-width', 'inherit')
  254 + }
  255 + },
  256 +
  257 + // if our input contains no value and backspace has been pressed, select the last tag
  258 + _inputBackspace : function(ev) {
  259 + var widget = (ev && ev.data.widget) || this;
  260 + lastTag = widget.elements.ul.find('li:not(.inputosaurus-required):last');
  261 +
  262 + // IE goes back in history if the event isn't stopped
  263 + ev.stopPropagation();
  264 +
  265 + if((!$(ev.currentTarget).val() || (('selectionStart' in ev.currentTarget) && ev.currentTarget.selectionStart === 0 && ev.currentTarget.selectionEnd === 0)) && lastTag.size()){
  266 + ev.preventDefault();
  267 + lastTag.find('a').focus();
  268 + }
  269 +
  270 + },
  271 +
  272 + _editTag : function(ev) {
  273 + var widget = (ev && ev.data.widget) || this,
  274 + tagName = '',
  275 + $closest = $(ev.currentTarget).closest('li'),
  276 + tagKey = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus');
  277 +
  278 + if(!tagKey){
  279 + return true;
  280 + }
  281 +
  282 + ev.preventDefault();
  283 +
  284 + $.each(widget._chosenValues, function(i,v) {
  285 + v.key === tagKey && (tagName = v.value);
  286 + });
  287 +
  288 + widget.elements.input.val(tagName);
  289 +
  290 + widget._removeTag(ev);
  291 + widget._resizeInput(ev);
  292 + },
  293 +
  294 + _tagKeypress : function(ev) {
  295 + var widget = ev.data.widget;
  296 + switch(ev.which){
  297 +
  298 + case $.ui.keyCode.BACKSPACE:
  299 + ev && ev.preventDefault();
  300 + ev && ev.stopPropagation();
  301 + $(ev.currentTarget).trigger('click');
  302 + break;
  303 +
  304 + // 'e' - edit tag (removes tag and places value into visible input
  305 + case 69:
  306 + widget._editTag(ev);
  307 + break;
  308 +
  309 + case $.ui.keyCode.LEFT:
  310 + ev.type === 'keydown' && widget._prevTag(ev);
  311 + break;
  312 +
  313 + case $.ui.keyCode.RIGHT:
  314 + ev.type === 'keydown' && widget._nextTag(ev);
  315 + break;
  316 +
  317 + case $.ui.keyCode.DOWN:
  318 + ev.type === 'keydown' && widget._focus(ev);
  319 + break;
  320 + }
  321 + },
  322 +
  323 + // select the previous tag or input if no more tags exist
  324 + _prevTag : function(ev) {
  325 + var widget = (ev && ev.data.widget) || this,
  326 + tag = $(ev.currentTarget).closest('li'),
  327 + previous = tag.prev();
  328 +
  329 + if(previous.is('li')){
  330 + previous.find('a').focus();
  331 + } else {
  332 + widget._focus();
  333 + }
  334 + },
  335 +
  336 + // select the next tag or input if no more tags exist
  337 + _nextTag : function(ev) {
  338 + var widget = (ev && ev.data.widget) || this,
  339 + tag = $(ev.currentTarget).closest('li'),
  340 + next = tag.next();
  341 +
  342 + if(next.is('li:not(.inputosaurus-input)')){
  343 + next.find('a').focus();
  344 + } else {
  345 + widget._focus();
  346 + }
  347 + },
  348 +
  349 + // return the inputDelimiter that was detected or false if none were found
  350 + _containsDelimiter : function(tagStr) {
  351 +
  352 + var found = false;
  353 +
  354 + $.each(this.options.inputDelimiters, function(k,v) {
  355 + if(tagStr.indexOf(v) !== -1){
  356 + found = v;
  357 + }
  358 + });
  359 +
  360 + return found;
  361 + },
  362 +
  363 + _setChosen : function(valArr) {
  364 + var self = this;
  365 +
  366 + if(!$.isArray(valArr)){
  367 + return false;
  368 + }
  369 +
  370 + $.each(valArr, function(k,v) {
  371 + var exists = false,
  372 + obj = {
  373 + key : '',
  374 + value : ''
  375 + };
  376 +
  377 + v = $.trim(v);
  378 +
  379 + $.each(self._chosenValues, function(kk,vv) {
  380 + if(!self.options.caseSensitiveDuplicates){
  381 + vv.value.toLowerCase() === v.toLowerCase() && (exists = true);
  382 + }
  383 + else{
  384 + vv.value === v && (exists = true);
  385 + }
  386 + });
  387 +
  388 + if(v !== '' && (!exists || self.options.allowDuplicates)){
  389 +
  390 + obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
  391 + obj.value = v;
  392 + self._chosenValues.push(obj);
  393 +
  394 + self._renderTags();
  395 + }
  396 + });
  397 + self._setValue(self._buildValue());
  398 + },
  399 +
  400 + _buildValue : function() {
  401 + var widget = this,
  402 + value = '';
  403 +
  404 + $.each(this._chosenValues, function(k,v) {
  405 + value += value.length ? widget.options.outputDelimiter + v.value : v.value;
  406 + });
  407 +
  408 + return value;
  409 + },
  410 +
  411 + _setValue : function(value) {
  412 + var val = this.element.val();
  413 +
  414 + if(val !== value){
  415 + this.element.val(value);
  416 + this._trigger('change');
  417 + }
  418 + },
  419 +
  420 + // @name text for tag
  421 + // @className optional className for <li>
  422 + _createTag : function(name, key, className) {
  423 + className = className ? ' class="' + className + '"' : '';
  424 +
  425 + if(name !== undefined){
  426 + return $('<li' + className + ' data-inputosaurus="' + key + '"><span>' + name + '</span> <a href="javascript:void(0);" class="ficon">&#x2716;</a></li>');
  427 + }
  428 + },
  429 +
  430 + _renderTags : function() {
  431 + var self = this;
  432 +
  433 + this.elements.ul.find('li:not(.inputosaurus-required)').remove();
  434 +
  435 + $.each(this._chosenValues, function(k,v) {
  436 + var el = self._createTag(v.value, v.key);
  437 + self.elements.ul.find('li.inputosaurus-input').before(el);
  438 + });
  439 + },
  440 +
  441 + _removeTag : function(ev) {
  442 + var $closest = $(ev.currentTarget).closest('li'),
  443 + key = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus'),
  444 + indexFound = false,
  445 + widget = (ev && ev.data.widget) || this;
  446 +
  447 +
  448 + $.each(widget._chosenValues, function(k,v) {
  449 + if(key === v.key){
  450 + indexFound = k;
  451 + }
  452 + });
  453 +
  454 + indexFound !== false && widget._chosenValues.splice(indexFound, 1);
  455 +
  456 + widget._setValue(widget._buildValue());
  457 +
  458 + $(ev.currentTarget).closest('li').remove();
  459 + widget.elements.input.focus();
  460 + },
  461 +
  462 + _focus : function(ev) {
  463 + var widget = (ev && ev.data.widget) || this,
  464 + $closest = $(ev.target).closest('li'),
  465 + $data = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus');
  466 +
  467 + if(!ev || !$data){
  468 + widget.elements.input.focus();
  469 + }
  470 + },
  471 +
  472 + _tagFocus : function(ev) {
  473 + $(ev.currentTarget).parent()[ev.type === 'focusout' ? 'removeClass' : 'addClass']('inputosaurus-selected');
  474 + },
  475 +
  476 + refresh : function() {
  477 + var delim = this.options.outputDelimiter,
  478 + val = this.element.val(),
  479 + values = [];
  480 +
  481 + values.push(val);
  482 + delim && (values = val.split(delim));
  483 +
  484 + if(values.length){
  485 + this._chosenValues = [];
  486 +
  487 + $.isFunction(this.options.parseHook) && (values = this.options.parseHook(values));
  488 +
  489 + this._setChosen(values);
  490 + this._renderTags();
  491 + this.elements.input.val('');
  492 + this._resizeInput();
  493 + }
  494 + },
  495 +
  496 + _attachEvents : function() {
  497 + var widget = this;
  498 +
  499 + this.elements.input.on('keyup.inputosaurus', {widget : widget}, this._inputKeypress);
  500 + this.elements.input.on('keydown.inputosaurus', {widget : widget}, this._inputKeypress);
  501 + this.elements.input.on('change.inputosaurus', {widget : widget}, this._inputKeypress);
  502 + this.elements.input.on('focus.inputosaurus', {widget : widget}, this._inputFocus);
  503 + this.options.parseOnBlur && this.elements.input.on('blur.inputosaurus', {widget : widget}, this.parseInput);
  504 +
  505 + this.elements.ul.on('click.inputosaurus', {widget : widget}, this._focus);
  506 + this.elements.ul.on('click.inputosaurus', 'a', {widget : widget}, this._removeTag);
  507 + this.elements.ul.on('dblclick.inputosaurus', 'li', {widget : widget}, this._editTag);
  508 + this.elements.ul.on('focus.inputosaurus', 'a', {widget : widget}, this._tagFocus);
  509 + this.elements.ul.on('blur.inputosaurus', 'a', {widget : widget}, this._tagFocus);
  510 + this.elements.ul.on('keydown.inputosaurus', 'a', {widget : widget}, this._tagKeypress);
  511 + },
  512 +
  513 + _destroy: function() {
  514 + this.elements.input.unbind('.inputosaurus');
  515 +
  516 + this.elements.ul.replaceWith(this.element);
  517 +
  518 + }
  519 + };
  520 +
  521 + $.widget("ui.inputosaurus", inputosaurustext);
  522 +})(jQuery);
  523 +
... ...
public/javascripts/spectrum.js 0 → 100644
... ... @@ -0,0 +1,2259 @@
  1 +// Spectrum Colorpicker v1.4.1
  2 +// https://github.com/bgrins/spectrum
  3 +// Author: Brian Grinstead
  4 +// License: MIT
  5 +
  6 +(function (window, $, undefined) {
  7 + "use strict";
  8 +
  9 + var defaultOpts = {
  10 +
  11 + // Callbacks
  12 + beforeShow: noop,
  13 + move: noop,
  14 + change: noop,
  15 + show: noop,
  16 + hide: noop,
  17 +
  18 + // Options
  19 + color: false,
  20 + flat: false,
  21 + showInput: false,
  22 + allowEmpty: false,
  23 + showButtons: true,
  24 + clickoutFiresChange: false,
  25 + showInitial: false,
  26 + showPalette: false,
  27 + showPaletteOnly: false,
  28 + hideAfterPaletteSelect: false,
  29 + togglePaletteOnly: false,
  30 + showSelectionPalette: true,
  31 + localStorageKey: false,
  32 + appendTo: "body",
  33 + maxSelectionSize: 7,
  34 + cancelText: "cancel",
  35 + chooseText: "choose",
  36 + togglePaletteMoreText: "more",
  37 + togglePaletteLessText: "less",
  38 + clearText: "Clear Color Selection",
  39 + noColorSelectedText: "No Color Selected",
  40 + preferredFormat: false,
  41 + className: "", // Deprecated - use containerClassName and replacerClassName instead.
  42 + containerClassName: "",
  43 + replacerClassName: "",
  44 + showAlpha: false,
  45 + theme: "sp-light",
  46 + palette: [["#ffffff", "#000000", "#ff0000", "#ff8000", "#ffff00", "#008000", "#0000ff", "#4b0082", "#9400d3"]],
  47 + selectionPalette: [],
  48 + disabled: false
  49 + },
  50 + spectrums = [],
  51 + IE = !!/msie/i.exec( window.navigator.userAgent ),
  52 + rgbaSupport = (function() {
  53 + function contains( str, substr ) {
  54 + return !!~('' + str).indexOf(substr);
  55 + }
  56 +
  57 + var elem = document.createElement('div');
  58 + var style = elem.style;
  59 + style.cssText = 'background-color:rgba(0,0,0,.5)';
  60 + return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla');
  61 + })(),
  62 + inputTypeColorSupport = (function() {
  63 + var colorInput = $("<input type='color' value='!' />")[0];
  64 + return colorInput.type === "color" && colorInput.value !== "!";
  65 + })(),
  66 + replaceInput = [
  67 + "<div class='sp-replacer'>",
  68 + "<div class='sp-preview'><div class='sp-preview-inner'></div></div>",
  69 + "<div class='sp-dd'>&#9660;</div>",
  70 + "</div>"
  71 + ].join(''),
  72 + markup = (function () {
  73 +
  74 + // IE does not support gradients with multiple stops, so we need to simulate
  75 + // that for the rainbow slider with 8 divs that each have a single gradient
  76 + var gradientFix = "";
  77 + if (IE) {
  78 + for (var i = 1; i <= 6; i++) {
  79 + gradientFix += "<div class='sp-" + i + "'></div>";
  80 + }
  81 + }
  82 +
  83 + return [
  84 + "<div class='sp-container sp-hidden'>",
  85 + "<div class='sp-palette-container'>",
  86 + "<div class='sp-palette sp-thumb sp-cf'></div>",
  87 + "<div class='sp-palette-button-container sp-cf'>",
  88 + "<button type='button' class='sp-palette-toggle'></button>",
  89 + "</div>",
  90 + "</div>",
  91 + "<div class='sp-picker-container'>",
  92 + "<div class='sp-top sp-cf'>",
  93 + "<div class='sp-fill'></div>",
  94 + "<div class='sp-top-inner'>",
  95 + "<div class='sp-color'>",
  96 + "<div class='sp-sat'>",
  97 + "<div class='sp-val'>",
  98 + "<div class='sp-dragger'></div>",
  99 + "</div>",
  100 + "</div>",
  101 + "</div>",
  102 + "<div class='sp-clear sp-clear-display'>",
  103 + "</div>",
  104 + "<div class='sp-hue'>",
  105 + "<div class='sp-slider'></div>",
  106 + gradientFix,
  107 + "</div>",
  108 + "</div>",
  109 + "<div class='sp-alpha'><div class='sp-alpha-inner'><div class='sp-alpha-handle'></div></div></div>",
  110 + "</div>",
  111 + "<div class='sp-input-container sp-cf'>",
  112 + "<input class='sp-input' type='text' spellcheck='false' />",
  113 + "</div>",
  114 + "<div class='sp-initial sp-thumb sp-cf'></div>",
  115 + "<div class='sp-button-container sp-cf'>",
  116 + "<a class='sp-cancel' href='#'></a>",
  117 + "<button type='button' class='sp-choose'></button>",
  118 + "</div>",
  119 + "</div>",
  120 + "</div>"
  121 + ].join("");
  122 + })();
  123 +
  124 + function paletteTemplate (p, color, className, opts) {
  125 + var html = [];
  126 + for (var i = 0; i < p.length; i++) {
  127 + var current = p[i];
  128 + if(current) {
  129 + var tiny = tinycolor(current);
  130 + var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
  131 + c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : "";
  132 + var formattedString = tiny.toString(opts.preferredFormat || "rgb");
  133 + var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
  134 + html.push('<span title="' + formattedString + '" data-color="' + tiny.toRgbString() + '" class="' + c + '"><span class="sp-thumb-inner" style="' + swatchStyle + ';" /></span>');
  135 + } else {
  136 + var cls = 'sp-clear-display';
  137 + html.push($('<div />')
  138 + .append($('<span data-color="" style="background-color:transparent;" class="' + cls + '"></span>')
  139 + .attr('title', opts.noColorSelectedText)
  140 + )
  141 + .html()
  142 + );
  143 + }
  144 + }
  145 + return "<div class='sp-cf " + className + "'>" + html.join('') + "</div>";
  146 + }
  147 +
  148 + function hideAll() {
  149 + for (var i = 0; i < spectrums.length; i++) {
  150 + if (spectrums[i]) {
  151 + spectrums[i].hide();
  152 + }
  153 + }
  154 + }
  155 +
  156 + function instanceOptions(o, callbackContext) {
  157 + var opts = $.extend({}, defaultOpts, o);
  158 + opts.callbacks = {
  159 + 'move': bind(opts.move, callbackContext),
  160 + 'change': bind(opts.change, callbackContext),
  161 + 'show': bind(opts.show, callbackContext),
  162 + 'hide': bind(opts.hide, callbackContext),
  163 + 'beforeShow': bind(opts.beforeShow, callbackContext)
  164 + };
  165 +
  166 + return opts;
  167 + }
  168 +
  169 + function spectrum(element, o) {
  170 +
  171 + var opts = instanceOptions(o, element),
  172 + flat = opts.flat,
  173 + showSelectionPalette = opts.showSelectionPalette,
  174 + localStorageKey = opts.localStorageKey,
  175 + theme = opts.theme,
  176 + callbacks = opts.callbacks,
  177 + resize = throttle(reflow, 10),
  178 + visible = false,
  179 + dragWidth = 0,
  180 + dragHeight = 0,
  181 + dragHelperHeight = 0,
  182 + slideHeight = 0,
  183 + slideWidth = 0,
  184 + alphaWidth = 0,
  185 + alphaSlideHelperWidth = 0,
  186 + slideHelperHeight = 0,
  187 + currentHue = 0,
  188 + currentSaturation = 0,
  189 + currentValue = 0,
  190 + currentAlpha = 1,
  191 + palette = [],
  192 + paletteArray = [],
  193 + paletteLookup = {},
  194 + selectionPalette = opts.selectionPalette.slice(0),
  195 + maxSelectionSize = opts.maxSelectionSize,
  196 + draggingClass = "sp-dragging",
  197 + shiftMovementDirection = null;
  198 +
  199 + var doc = element.ownerDocument,
  200 + body = doc.body,
  201 + boundElement = $(element),
  202 + disabled = false,
  203 + container = $(markup, doc).addClass(theme),
  204 + pickerContainer = container.find(".sp-picker-container"),
  205 + dragger = container.find(".sp-color"),
  206 + dragHelper = container.find(".sp-dragger"),
  207 + slider = container.find(".sp-hue"),
  208 + slideHelper = container.find(".sp-slider"),
  209 + alphaSliderInner = container.find(".sp-alpha-inner"),
  210 + alphaSlider = container.find(".sp-alpha"),
  211 + alphaSlideHelper = container.find(".sp-alpha-handle"),
  212 + textInput = container.find(".sp-input"),
  213 + paletteContainer = container.find(".sp-palette"),
  214 + initialColorContainer = container.find(".sp-initial"),
  215 + cancelButton = container.find(".sp-cancel"),
  216 + clearButton = container.find(".sp-clear"),
  217 + chooseButton = container.find(".sp-choose"),
  218 + toggleButton = container.find(".sp-palette-toggle"),
  219 + isInput = boundElement.is("input"),
  220 + isInputTypeColor = isInput && inputTypeColorSupport && boundElement.attr("type") === "color",
  221 + shouldReplace = isInput && !flat,
  222 + replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className).addClass(opts.replacerClassName) : $([]),
  223 + offsetElement = (shouldReplace) ? replacer : boundElement,
  224 + previewElement = replacer.find(".sp-preview-inner"),
  225 + initialColor = opts.color || (isInput && boundElement.val()),
  226 + colorOnShow = false,
  227 + preferredFormat = opts.preferredFormat,
  228 + currentPreferredFormat = preferredFormat,
  229 + clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange,
  230 + isEmpty = !initialColor,
  231 + allowEmpty = opts.allowEmpty && !isInputTypeColor;
  232 +
  233 + function applyOptions() {
  234 +
  235 + if (opts.showPaletteOnly) {
  236 + opts.showPalette = true;
  237 + }
  238 +
  239 + toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
  240 +
  241 + if (opts.palette) {
  242 + palette = opts.palette.slice(0);
  243 + paletteArray = $.isArray(palette[0]) ? palette : [palette];
  244 + paletteLookup = {};
  245 + for (var i = 0; i < paletteArray.length; i++) {
  246 + for (var j = 0; j < paletteArray[i].length; j++) {
  247 + var rgb = tinycolor(paletteArray[i][j]).toRgbString();
  248 + paletteLookup[rgb] = true;
  249 + }
  250 + }
  251 + }
  252 +
  253 + container.toggleClass("sp-flat", flat);
  254 + container.toggleClass("sp-input-disabled", !opts.showInput);
  255 + container.toggleClass("sp-alpha-enabled", opts.showAlpha);
  256 + container.toggleClass("sp-clear-enabled", allowEmpty);
  257 + container.toggleClass("sp-buttons-disabled", !opts.showButtons);
  258 + container.toggleClass("sp-palette-buttons-disabled", !opts.togglePaletteOnly);
  259 + container.toggleClass("sp-palette-disabled", !opts.showPalette);
  260 + container.toggleClass("sp-palette-only", opts.showPaletteOnly);
  261 + container.toggleClass("sp-initial-disabled", !opts.showInitial);
  262 + container.addClass(opts.className).addClass(opts.containerClassName);
  263 +
  264 + reflow();
  265 + }
  266 +
  267 + function initialize() {
  268 +
  269 + if (IE) {
  270 + container.find("*:not(input)").attr("unselectable", "on");
  271 + }
  272 +
  273 + applyOptions();
  274 +
  275 + if (shouldReplace) {
  276 + boundElement.after(replacer).hide();
  277 + }
  278 +
  279 + if (!allowEmpty) {
  280 + clearButton.hide();
  281 + }
  282 +
  283 + if (flat) {
  284 + boundElement.after(container).hide();
  285 + }
  286 + else {
  287 +
  288 + var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
  289 + if (appendTo.length !== 1) {
  290 + appendTo = $("body");
  291 + }
  292 +
  293 + appendTo.append(container);
  294 + }
  295 +
  296 + updateSelectionPaletteFromStorage();
  297 +
  298 + offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
  299 + if (!disabled) {
  300 + toggle();
  301 + }
  302 +
  303 + e.stopPropagation();
  304 +
  305 + if (!$(e.target).is("input")) {
  306 + e.preventDefault();
  307 + }
  308 + });
  309 +
  310 + if(boundElement.is(":disabled") || (opts.disabled === true)) {
  311 + disable();
  312 + }
  313 +
  314 + // Prevent clicks from bubbling up to document. This would cause it to be hidden.
  315 + container.click(stopPropagation);
  316 +
  317 + // Handle user typed input
  318 + textInput.change(setFromTextInput);
  319 + textInput.bind("paste", function () {
  320 + setTimeout(setFromTextInput, 1);
  321 + });
  322 + textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
  323 +
  324 + cancelButton.text(opts.cancelText);
  325 + cancelButton.bind("click.spectrum", function (e) {
  326 + e.stopPropagation();
  327 + e.preventDefault();
  328 + hide("cancel");
  329 + });
  330 +
  331 + clearButton.attr("title", opts.clearText);
  332 + clearButton.bind("click.spectrum", function (e) {
  333 + e.stopPropagation();
  334 + e.preventDefault();
  335 + isEmpty = true;
  336 + move();
  337 +
  338 + if(flat) {
  339 + //for the flat style, this is a change event
  340 + updateOriginalInput(true);
  341 + }
  342 + });
  343 +
  344 + chooseButton.text(opts.chooseText);
  345 + chooseButton.bind("click.spectrum", function (e) {
  346 + e.stopPropagation();
  347 + e.preventDefault();
  348 +
  349 + if (isValid()) {
  350 + updateOriginalInput(true);
  351 + hide();
  352 + }
  353 + });
  354 +
  355 + toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
  356 + toggleButton.bind("click.spectrum", function (e) {
  357 + e.stopPropagation();
  358 + e.preventDefault();
  359 +
  360 + opts.showPaletteOnly = !opts.showPaletteOnly;
  361 +
  362 + // To make sure the Picker area is drawn on the right, next to the
  363 + // Palette area (and not below the palette), first move the Palette
  364 + // to the left to make space for the picker, plus 5px extra.
  365 + // The 'applyOptions' function puts the whole container back into place
  366 + // and takes care of the button-text and the sp-palette-only CSS class.
  367 + if (!opts.showPaletteOnly && !flat) {
  368 + container.css('left', '-=' + (pickerContainer.outerWidth(true) + 5));
  369 + }
  370 + applyOptions();
  371 + });
  372 +
  373 + draggable(alphaSlider, function (dragX, dragY, e) {
  374 + currentAlpha = (dragX / alphaWidth);
  375 + isEmpty = false;
  376 + if (e.shiftKey) {
  377 + currentAlpha = Math.round(currentAlpha * 10) / 10;
  378 + }
  379 +
  380 + move();
  381 + }, dragStart, dragStop);
  382 +
  383 + draggable(slider, function (dragX, dragY) {
  384 + currentHue = parseFloat(dragY / slideHeight);
  385 + isEmpty = false;
  386 + if (!opts.showAlpha) {
  387 + currentAlpha = 1;
  388 + }
  389 + move();
  390 + }, dragStart, dragStop);
  391 +
  392 + draggable(dragger, function (dragX, dragY, e) {
  393 +
  394 + // shift+drag should snap the movement to either the x or y axis.
  395 + if (!e.shiftKey) {
  396 + shiftMovementDirection = null;
  397 + }
  398 + else if (!shiftMovementDirection) {
  399 + var oldDragX = currentSaturation * dragWidth;
  400 + var oldDragY = dragHeight - (currentValue * dragHeight);
  401 + var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
  402 +
  403 + shiftMovementDirection = furtherFromX ? "x" : "y";
  404 + }
  405 +
  406 + var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
  407 + var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
  408 +
  409 + if (setSaturation) {
  410 + currentSaturation = parseFloat(dragX / dragWidth);
  411 + }
  412 + if (setValue) {
  413 + currentValue = parseFloat((dragHeight - dragY) / dragHeight);
  414 + }
  415 +
  416 + isEmpty = false;
  417 + if (!opts.showAlpha) {
  418 + currentAlpha = 1;
  419 + }
  420 +
  421 + move();
  422 +
  423 + }, dragStart, dragStop);
  424 +
  425 + if (!!initialColor) {
  426 + set(initialColor);
  427 +
  428 + // In case color was black - update the preview UI and set the format
  429 + // since the set function will not run (default color is black).
  430 + updateUI();
  431 + currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
  432 +
  433 + addColorToSelectionPalette(initialColor);
  434 + }
  435 + else {
  436 + updateUI();
  437 + }
  438 +
  439 + if (flat) {
  440 + show();
  441 + }
  442 +
  443 + function paletteElementClick(e) {
  444 + if (e.data && e.data.ignore) {
  445 + set($(e.target).closest(".sp-thumb-el").data("color"));
  446 + move();
  447 + }
  448 + else {
  449 + set($(e.target).closest(".sp-thumb-el").data("color"));
  450 + move();
  451 + updateOriginalInput(true);
  452 + if (opts.hideAfterPaletteSelect) {
  453 + hide();
  454 + }
  455 + }
  456 +
  457 + return false;
  458 + }
  459 +
  460 + var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
  461 + paletteContainer.delegate(".sp-thumb-el", paletteEvent, paletteElementClick);
  462 + initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, paletteElementClick);
  463 + }
  464 +
  465 + function updateSelectionPaletteFromStorage() {
  466 +
  467 + if (localStorageKey && window.localStorage) {
  468 +
  469 + // Migrate old palettes over to new format. May want to remove this eventually.
  470 + try {
  471 + var oldPalette = window.localStorage[localStorageKey].split(",#");
  472 + if (oldPalette.length > 1) {
  473 + delete window.localStorage[localStorageKey];
  474 + $.each(oldPalette, function(i, c) {
  475 + addColorToSelectionPalette(c);
  476 + });
  477 + }
  478 + }
  479 + catch(e) { }
  480 +
  481 + try {
  482 + selectionPalette = window.localStorage[localStorageKey].split(";");
  483 + }
  484 + catch (e) { }
  485 + }
  486 + }
  487 +
  488 + function addColorToSelectionPalette(color) {
  489 + if (showSelectionPalette) {
  490 + var rgb = tinycolor(color).toRgbString();
  491 + if (!paletteLookup[rgb] && $.inArray(rgb, selectionPalette) === -1) {
  492 + selectionPalette.push(rgb);
  493 + while(selectionPalette.length > maxSelectionSize) {
  494 + selectionPalette.shift();
  495 + }
  496 + }
  497 +
  498 + if (localStorageKey && window.localStorage) {
  499 + try {
  500 + window.localStorage[localStorageKey] = selectionPalette.join(";");
  501 + }
  502 + catch(e) { }
  503 + }
  504 + }
  505 + }
  506 +
  507 + function getUniqueSelectionPalette() {
  508 + var unique = [];
  509 + if (opts.showPalette) {
  510 + for (var i = 0; i < selectionPalette.length; i++) {
  511 + var rgb = tinycolor(selectionPalette[i]).toRgbString();
  512 +
  513 + if (!paletteLookup[rgb]) {
  514 + unique.push(selectionPalette[i]);
  515 + }
  516 + }
  517 + }
  518 +
  519 + return unique.reverse().slice(0, opts.maxSelectionSize);
  520 + }
  521 +
  522 + function drawPalette() {
  523 +
  524 + var currentColor = get();
  525 +
  526 + var html = $.map(paletteArray, function (palette, i) {
  527 + return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i, opts);
  528 + });
  529 +
  530 + updateSelectionPaletteFromStorage();
  531 +
  532 + if (selectionPalette) {
  533 + html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection", opts));
  534 + }
  535 +
  536 + paletteContainer.html(html.join(""));
  537 + }
  538 +
  539 + function drawInitial() {
  540 + if (opts.showInitial) {
  541 + var initial = colorOnShow;
  542 + var current = get();
  543 + initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial", opts));
  544 + }
  545 + }
  546 +
  547 + function dragStart() {
  548 + if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
  549 + reflow();
  550 + }
  551 + container.addClass(draggingClass);
  552 + shiftMovementDirection = null;
  553 + boundElement.trigger('dragstart.spectrum', [ get() ]);
  554 + }
  555 +
  556 + function dragStop() {
  557 + container.removeClass(draggingClass);
  558 + boundElement.trigger('dragstop.spectrum', [ get() ]);
  559 + }
  560 +
  561 + function setFromTextInput() {
  562 +
  563 + var value = textInput.val();
  564 +
  565 + if ((value === null || value === "") && allowEmpty) {
  566 + set(null);
  567 + updateOriginalInput(true);
  568 + }
  569 + else {
  570 + var tiny = tinycolor(value);
  571 + if (tiny.isValid()) {
  572 + set(tiny);
  573 + updateOriginalInput(true);
  574 + }
  575 + else {
  576 + textInput.addClass("sp-validation-error");
  577 + }
  578 + }
  579 + }
  580 +
  581 + function toggle() {
  582 + if (visible) {
  583 + hide();
  584 + }
  585 + else {
  586 + show();
  587 + }
  588 + }
  589 +
  590 + function show() {
  591 + var event = $.Event('beforeShow.spectrum');
  592 +
  593 + if (visible) {
  594 + reflow();
  595 + return;
  596 + }
  597 +
  598 + boundElement.trigger(event, [ get() ]);
  599 +
  600 + if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
  601 + return;
  602 + }
  603 +
  604 + hideAll();
  605 + visible = true;
  606 +
  607 + $(doc).bind("click.spectrum", hide);
  608 + $(window).bind("resize.spectrum", resize);
  609 + replacer.addClass("sp-active");
  610 + container.removeClass("sp-hidden");
  611 +
  612 + reflow();
  613 + updateUI();
  614 +
  615 + colorOnShow = get();
  616 +
  617 + drawInitial();
  618 + callbacks.show(colorOnShow);
  619 + boundElement.trigger('show.spectrum', [ colorOnShow ]);
  620 + }
  621 +
  622 + function hide(e) {
  623 +
  624 + // Return on right click
  625 + if (e && e.type == "click" && e.button == 2) { return; }
  626 +
  627 + // Return if hiding is unnecessary
  628 + if (!visible || flat) { return; }
  629 + visible = false;
  630 +
  631 + $(doc).unbind("click.spectrum", hide);
  632 + $(window).unbind("resize.spectrum", resize);
  633 +
  634 + replacer.removeClass("sp-active");
  635 + container.addClass("sp-hidden");
  636 +
  637 + var colorHasChanged = !tinycolor.equals(get(), colorOnShow);
  638 +
  639 + if (colorHasChanged) {
  640 + if (clickoutFiresChange && e !== "cancel") {
  641 + updateOriginalInput(true);
  642 + }
  643 + else {
  644 + revert();
  645 + }
  646 + }
  647 +
  648 + callbacks.hide(get());
  649 + boundElement.trigger('hide.spectrum', [ get() ]);
  650 + }
  651 +
  652 + function revert() {
  653 + set(colorOnShow, true);
  654 + }
  655 +
  656 + function set(color, ignoreFormatChange) {
  657 + if (tinycolor.equals(color, get())) {
  658 + // Update UI just in case a validation error needs
  659 + // to be cleared.
  660 + updateUI();
  661 + return;
  662 + }
  663 +
  664 + var newColor, newHsv;
  665 + if (!color && allowEmpty) {
  666 + isEmpty = true;
  667 + } else {
  668 + isEmpty = false;
  669 + newColor = tinycolor(color);
  670 + newHsv = newColor.toHsv();
  671 +
  672 + currentHue = (newHsv.h % 360) / 360;
  673 + currentSaturation = newHsv.s;
  674 + currentValue = newHsv.v;
  675 + currentAlpha = newHsv.a;
  676 + }
  677 + updateUI();
  678 +
  679 + if (newColor && newColor.isValid() && !ignoreFormatChange) {
  680 + currentPreferredFormat = preferredFormat || newColor.getFormat();
  681 + }
  682 + }
  683 +
  684 + function get(opts) {
  685 + opts = opts || { };
  686 +
  687 + if (allowEmpty && isEmpty) {
  688 + return null;
  689 + }
  690 +
  691 + return tinycolor.fromRatio({
  692 + h: currentHue,
  693 + s: currentSaturation,
  694 + v: currentValue,
  695 + a: Math.round(currentAlpha * 100) / 100
  696 + }, { format: opts.format || currentPreferredFormat });
  697 + }
  698 +
  699 + function isValid() {
  700 + return !textInput.hasClass("sp-validation-error");
  701 + }
  702 +
  703 + function move() {
  704 + updateUI();
  705 +
  706 + callbacks.move(get());
  707 + boundElement.trigger('move.spectrum', [ get() ]);
  708 + }
  709 +
  710 + function updateUI() {
  711 +
  712 + textInput.removeClass("sp-validation-error");
  713 +
  714 + updateHelperLocations();
  715 +
  716 + // Update dragger background color (gradients take care of saturation and value).
  717 + var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
  718 + dragger.css("background-color", flatColor.toHexString());
  719 +
  720 + // Get a format that alpha will be included in (hex and names ignore alpha)
  721 + var format = currentPreferredFormat;
  722 + if (currentAlpha < 1 && !(currentAlpha === 0 && format === "name")) {
  723 + if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
  724 + format = "rgb";
  725 + }
  726 + }
  727 +
  728 + var realColor = get({ format: format }),
  729 + displayColor = '';
  730 +
  731 + //reset background info for preview element
  732 + previewElement.removeClass("sp-clear-display");
  733 + previewElement.css('background-color', 'transparent');
  734 +
  735 + if (!realColor && allowEmpty) {
  736 + // Update the replaced elements background with icon indicating no color selection
  737 + previewElement.addClass("sp-clear-display");
  738 + }
  739 + else {
  740 + var realHex = realColor.toHexString(),
  741 + realRgb = realColor.toRgbString();
  742 +
  743 + // Update the replaced elements background color (with actual selected color)
  744 + if (rgbaSupport || realColor.alpha === 1) {
  745 + previewElement.css("background-color", realRgb);
  746 + }
  747 + else {
  748 + previewElement.css("background-color", "transparent");
  749 + previewElement.css("filter", realColor.toFilter());
  750 + }
  751 +
  752 + if (opts.showAlpha) {
  753 + var rgb = realColor.toRgb();
  754 + rgb.a = 0;
  755 + var realAlpha = tinycolor(rgb).toRgbString();
  756 + var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
  757 +
  758 + if (IE) {
  759 + alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
  760 + }
  761 + else {
  762 + alphaSliderInner.css("background", "-webkit-" + gradient);
  763 + alphaSliderInner.css("background", "-moz-" + gradient);
  764 + alphaSliderInner.css("background", "-ms-" + gradient);
  765 + // Use current syntax gradient on unprefixed property.
  766 + alphaSliderInner.css("background",
  767 + "linear-gradient(to right, " + realAlpha + ", " + realHex + ")");
  768 + }
  769 + }
  770 +
  771 + displayColor = realColor.toString(format);
  772 + }
  773 +
  774 + // Update the text entry input as it changes happen
  775 + if (opts.showInput) {
  776 + textInput.val(displayColor);
  777 + }
  778 +
  779 + if (opts.showPalette) {
  780 + drawPalette();
  781 + }
  782 +
  783 + drawInitial();
  784 + }
  785 +
  786 + function updateHelperLocations() {
  787 + var s = currentSaturation;
  788 + var v = currentValue;
  789 +
  790 + if(allowEmpty && isEmpty) {
  791 + //if selected color is empty, hide the helpers
  792 + alphaSlideHelper.hide();
  793 + slideHelper.hide();
  794 + dragHelper.hide();
  795 + }
  796 + else {
  797 + //make sure helpers are visible
  798 + alphaSlideHelper.show();
  799 + slideHelper.show();
  800 + dragHelper.show();
  801 +
  802 + // Where to show the little circle in that displays your current selected color
  803 + var dragX = s * dragWidth;
  804 + var dragY = dragHeight - (v * dragHeight);
  805 + dragX = Math.max(
  806 + -dragHelperHeight,
  807 + Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
  808 + );
  809 + dragY = Math.max(
  810 + -dragHelperHeight,
  811 + Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
  812 + );
  813 + dragHelper.css({
  814 + "top": dragY + "px",
  815 + "left": dragX + "px"
  816 + });
  817 +
  818 + var alphaX = currentAlpha * alphaWidth;
  819 + alphaSlideHelper.css({
  820 + "left": (alphaX - (alphaSlideHelperWidth / 2)) + "px"
  821 + });
  822 +
  823 + // Where to show the bar that displays your current selected hue
  824 + var slideY = (currentHue) * slideHeight;
  825 + slideHelper.css({
  826 + "top": (slideY - slideHelperHeight) + "px"
  827 + });
  828 + }
  829 + }
  830 +
  831 + function updateOriginalInput(fireCallback) {
  832 + var color = get(),
  833 + displayColor = '',
  834 + hasChanged = !tinycolor.equals(color, colorOnShow);
  835 +
  836 + if (color) {
  837 + displayColor = color.toString(currentPreferredFormat);
  838 + // Update the selection palette with the current color
  839 + addColorToSelectionPalette(color);
  840 + }
  841 +
  842 + if (isInput) {
  843 + boundElement.val(displayColor);
  844 + }
  845 +
  846 + colorOnShow = color;
  847 +
  848 + if (fireCallback && hasChanged) {
  849 + callbacks.change(color);
  850 + boundElement.trigger('change', [ color ]);
  851 + }
  852 + }
  853 +
  854 + function reflow() {
  855 + dragWidth = dragger.width();
  856 + dragHeight = dragger.height();
  857 + dragHelperHeight = dragHelper.height();
  858 + slideWidth = slider.width();
  859 + slideHeight = slider.height();
  860 + slideHelperHeight = slideHelper.height();
  861 + alphaWidth = alphaSlider.width();
  862 + alphaSlideHelperWidth = alphaSlideHelper.width();
  863 +
  864 + if (!flat) {
  865 + container.css("position", "absolute");
  866 + container.offset(getOffset(container, offsetElement));
  867 + }
  868 +
  869 + updateHelperLocations();
  870 +
  871 + if (opts.showPalette) {
  872 + drawPalette();
  873 + }
  874 +
  875 + boundElement.trigger('reflow.spectrum');
  876 + }
  877 +
  878 + function destroy() {
  879 + boundElement.show();
  880 + offsetElement.unbind("click.spectrum touchstart.spectrum");
  881 + container.remove();
  882 + replacer.remove();
  883 + spectrums[spect.id] = null;
  884 + }
  885 +
  886 + function option(optionName, optionValue) {
  887 + if (optionName === undefined) {
  888 + return $.extend({}, opts);
  889 + }
  890 + if (optionValue === undefined) {
  891 + return opts[optionName];
  892 + }
  893 +
  894 + opts[optionName] = optionValue;
  895 + applyOptions();
  896 + }
  897 +
  898 + function enable() {
  899 + disabled = false;
  900 + boundElement.attr("disabled", false);
  901 + offsetElement.removeClass("sp-disabled");
  902 + }
  903 +
  904 + function disable() {
  905 + hide();
  906 + disabled = true;
  907 + boundElement.attr("disabled", true);
  908 + offsetElement.addClass("sp-disabled");
  909 + }
  910 +
  911 + initialize();
  912 +
  913 + var spect = {
  914 + show: show,
  915 + hide: hide,
  916 + toggle: toggle,
  917 + reflow: reflow,
  918 + option: option,
  919 + enable: enable,
  920 + disable: disable,
  921 + set: function (c) {
  922 + set(c);
  923 + updateOriginalInput();
  924 + },
  925 + get: get,
  926 + destroy: destroy,
  927 + container: container
  928 + };
  929 +
  930 + spect.id = spectrums.push(spect) - 1;
  931 +
  932 + return spect;
  933 + }
  934 +
  935 + /**
  936 + * checkOffset - get the offset below/above and left/right element depending on screen position
  937 + * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
  938 + */
  939 + function getOffset(picker, input) {
  940 + var extraY = 0;
  941 + var dpWidth = picker.outerWidth();
  942 + var dpHeight = picker.outerHeight();
  943 + var inputHeight = input.outerHeight();
  944 + var doc = picker[0].ownerDocument;
  945 + var docElem = doc.documentElement;
  946 + var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
  947 + var viewHeight = docElem.clientHeight + $(doc).scrollTop();
  948 + var offset = input.offset();
  949 + offset.top += inputHeight;
  950 +
  951 + offset.left -=
  952 + Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
  953 + Math.abs(offset.left + dpWidth - viewWidth) : 0);
  954 +
  955 + offset.top -=
  956 + Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
  957 + Math.abs(dpHeight + inputHeight - extraY) : extraY));
  958 +
  959 + return offset;
  960 + }
  961 +
  962 + /**
  963 + * noop - do nothing
  964 + */
  965 + function noop() {
  966 +
  967 + }
  968 +
  969 + /**
  970 + * stopPropagation - makes the code only doing this a little easier to read in line
  971 + */
  972 + function stopPropagation(e) {
  973 + e.stopPropagation();
  974 + }
  975 +
  976 + /**
  977 + * Create a function bound to a given object
  978 + * Thanks to underscore.js
  979 + */
  980 + function bind(func, obj) {
  981 + var slice = Array.prototype.slice;
  982 + var args = slice.call(arguments, 2);
  983 + return function () {
  984 + return func.apply(obj, args.concat(slice.call(arguments)));
  985 + };
  986 + }
  987 +
  988 + /**
  989 + * Lightweight drag helper. Handles containment within the element, so that
  990 + * when dragging, the x is within [0,element.width] and y is within [0,element.height]
  991 + */
  992 + function draggable(element, onmove, onstart, onstop) {
  993 + onmove = onmove || function () { };
  994 + onstart = onstart || function () { };
  995 + onstop = onstop || function () { };
  996 + var doc = element.ownerDocument || document;
  997 + var dragging = false;
  998 + var offset = {};
  999 + var maxHeight = 0;
  1000 + var maxWidth = 0;
  1001 + var hasTouch = ('ontouchstart' in window);
  1002 +
  1003 + var duringDragEvents = {};
  1004 + duringDragEvents["selectstart"] = prevent;
  1005 + duringDragEvents["dragstart"] = prevent;
  1006 + duringDragEvents["touchmove mousemove"] = move;
  1007 + duringDragEvents["touchend mouseup"] = stop;
  1008 +
  1009 + function prevent(e) {
  1010 + if (e.stopPropagation) {
  1011 + e.stopPropagation();
  1012 + }
  1013 + if (e.preventDefault) {
  1014 + e.preventDefault();
  1015 + }
  1016 + e.returnValue = false;
  1017 + }
  1018 +
  1019 + function move(e) {
  1020 + if (dragging) {
  1021 + // Mouseup happened outside of window
  1022 + if (IE && document.documentMode < 9 && !e.button) {
  1023 + return stop();
  1024 + }
  1025 +
  1026 + var touches = e.originalEvent.touches;
  1027 + var pageX = touches ? touches[0].pageX : e.pageX;
  1028 + var pageY = touches ? touches[0].pageY : e.pageY;
  1029 +
  1030 + var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
  1031 + var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
  1032 +
  1033 + if (hasTouch) {
  1034 + // Stop scrolling in iOS
  1035 + prevent(e);
  1036 + }
  1037 +
  1038 + onmove.apply(element, [dragX, dragY, e]);
  1039 + }
  1040 + }
  1041 +
  1042 + function start(e) {
  1043 + var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
  1044 + var touches = e.originalEvent.touches;
  1045 +
  1046 + if (!rightclick && !dragging) {
  1047 + if (onstart.apply(element, arguments) !== false) {
  1048 + dragging = true;
  1049 + maxHeight = $(element).height();
  1050 + maxWidth = $(element).width();
  1051 + offset = $(element).offset();
  1052 +
  1053 + $(doc).bind(duringDragEvents);
  1054 + $(doc.body).addClass("sp-dragging");
  1055 +
  1056 + if (!hasTouch) {
  1057 + move(e);
  1058 + }
  1059 +
  1060 + prevent(e);
  1061 + }
  1062 + }
  1063 + }
  1064 +
  1065 + function stop() {
  1066 + if (dragging) {
  1067 + $(doc).unbind(duringDragEvents);
  1068 + $(doc.body).removeClass("sp-dragging");
  1069 + onstop.apply(element, arguments);
  1070 + }
  1071 + dragging = false;
  1072 + }
  1073 +
  1074 + $(element).bind("touchstart mousedown", start);
  1075 + }
  1076 +
  1077 + function throttle(func, wait, debounce) {
  1078 + var timeout;
  1079 + return function () {
  1080 + var context = this, args = arguments;
  1081 + var throttler = function () {
  1082 + timeout = null;
  1083 + func.apply(context, args);
  1084 + };
  1085 + if (debounce) clearTimeout(timeout);
  1086 + if (debounce || !timeout) timeout = setTimeout(throttler, wait);
  1087 + };
  1088 + }
  1089 +
  1090 + /**
  1091 + * Define a jQuery plugin
  1092 + */
  1093 + var dataID = "spectrum.id";
  1094 + $.fn.spectrum = function (opts, extra) {
  1095 +
  1096 + if (typeof opts == "string") {
  1097 +
  1098 + var returnValue = this;
  1099 + var args = Array.prototype.slice.call( arguments, 1 );
  1100 +
  1101 + this.each(function () {
  1102 + var spect = spectrums[$(this).data(dataID)];
  1103 + if (spect) {
  1104 + var method = spect[opts];
  1105 + if (!method) {
  1106 + throw new Error( "Spectrum: no such method: '" + opts + "'" );
  1107 + }
  1108 +
  1109 + if (opts == "get") {
  1110 + returnValue = spect.get();
  1111 + }
  1112 + else if (opts == "container") {
  1113 + returnValue = spect.container;
  1114 + }
  1115 + else if (opts == "option") {
  1116 + returnValue = spect.option.apply(spect, args);
  1117 + }
  1118 + else if (opts == "destroy") {
  1119 + spect.destroy();
  1120 + $(this).removeData(dataID);
  1121 + }
  1122 + else {
  1123 + method.apply(spect, args);
  1124 + }
  1125 + }
  1126 + });
  1127 +
  1128 + return returnValue;
  1129 + }
  1130 +
  1131 + // Initializing a new instance of spectrum
  1132 + return this.spectrum("destroy").each(function () {
  1133 + var options = $.extend({}, opts, $(this).data());
  1134 + var spect = spectrum(this, options);
  1135 + $(this).data(dataID, spect.id);
  1136 + });
  1137 + };
  1138 +
  1139 + $.fn.spectrum.load = true;
  1140 + $.fn.spectrum.loadOpts = {};
  1141 + $.fn.spectrum.draggable = draggable;
  1142 + $.fn.spectrum.defaults = defaultOpts;
  1143 +
  1144 + $.spectrum = { };
  1145 + $.spectrum.localization = { };
  1146 + $.spectrum.palettes = { };
  1147 +
  1148 + $.fn.spectrum.processNativeColorInputs = function () {
  1149 + if (!inputTypeColorSupport) {
  1150 + $("input[type=color]").spectrum({
  1151 + preferredFormat: "hex6"
  1152 + });
  1153 + }
  1154 + };
  1155 +
  1156 + // TinyColor v1.0.0
  1157 + // https://github.com/bgrins/TinyColor
  1158 + // Brian Grinstead, MIT License
  1159 +
  1160 + (function() {
  1161 +
  1162 + var trimLeft = /^[\s,#]+/,
  1163 + trimRight = /\s+$/,
  1164 + tinyCounter = 0,
  1165 + math = Math,
  1166 + mathRound = math.round,
  1167 + mathMin = math.min,
  1168 + mathMax = math.max,
  1169 + mathRandom = math.random;
  1170 +
  1171 + var tinycolor = function tinycolor (color, opts) {
  1172 +
  1173 + color = (color) ? color : '';
  1174 + opts = opts || { };
  1175 +
  1176 + // If input is already a tinycolor, return itself
  1177 + if (color instanceof tinycolor) {
  1178 + return color;
  1179 + }
  1180 + // If we are called as a function, call using new instead
  1181 + if (!(this instanceof tinycolor)) {
  1182 + return new tinycolor(color, opts);
  1183 + }
  1184 +
  1185 + var rgb = inputToRGB(color);
  1186 + this._r = rgb.r,
  1187 + this._g = rgb.g,
  1188 + this._b = rgb.b,
  1189 + this._a = rgb.a,
  1190 + this._roundA = mathRound(100*this._a) / 100,
  1191 + this._format = opts.format || rgb.format;
  1192 + this._gradientType = opts.gradientType;
  1193 +
  1194 + // Don't let the range of [0,255] come back in [0,1].
  1195 + // Potentially lose a little bit of precision here, but will fix issues where
  1196 + // .5 gets interpreted as half of the total, instead of half of 1
  1197 + // If it was supposed to be 128, this was already taken care of by `inputToRgb`
  1198 + if (this._r < 1) { this._r = mathRound(this._r); }
  1199 + if (this._g < 1) { this._g = mathRound(this._g); }
  1200 + if (this._b < 1) { this._b = mathRound(this._b); }
  1201 +
  1202 + this._ok = rgb.ok;
  1203 + this._tc_id = tinyCounter++;
  1204 + };
  1205 +
  1206 + tinycolor.prototype = {
  1207 + isDark: function() {
  1208 + return this.getBrightness() < 128;
  1209 + },
  1210 + isLight: function() {
  1211 + return !this.isDark();
  1212 + },
  1213 + isValid: function() {
  1214 + return this._ok;
  1215 + },
  1216 + getFormat: function() {
  1217 + return this._format;
  1218 + },
  1219 + getAlpha: function() {
  1220 + return this._a;
  1221 + },
  1222 + getBrightness: function() {
  1223 + var rgb = this.toRgb();
  1224 + return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
  1225 + },
  1226 + setAlpha: function(value) {
  1227 + this._a = boundAlpha(value);
  1228 + this._roundA = mathRound(100*this._a) / 100;
  1229 + return this;
  1230 + },
  1231 + toHsv: function() {
  1232 + var hsv = rgbToHsv(this._r, this._g, this._b);
  1233 + return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
  1234 + },
  1235 + toHsvString: function() {
  1236 + var hsv = rgbToHsv(this._r, this._g, this._b);
  1237 + var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
  1238 + return (this._a == 1) ?
  1239 + "hsv(" + h + ", " + s + "%, " + v + "%)" :
  1240 + "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
  1241 + },
  1242 + toHsl: function() {
  1243 + var hsl = rgbToHsl(this._r, this._g, this._b);
  1244 + return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
  1245 + },
  1246 + toHslString: function() {
  1247 + var hsl = rgbToHsl(this._r, this._g, this._b);
  1248 + var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
  1249 + return (this._a == 1) ?
  1250 + "hsl(" + h + ", " + s + "%, " + l + "%)" :
  1251 + "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
  1252 + },
  1253 + toHex: function(allow3Char) {
  1254 + return rgbToHex(this._r, this._g, this._b, allow3Char);
  1255 + },
  1256 + toHexString: function(allow3Char) {
  1257 + return '#' + this.toHex(allow3Char);
  1258 + },
  1259 + toHex8: function() {
  1260 + return rgbaToHex(this._r, this._g, this._b, this._a);
  1261 + },
  1262 + toHex8String: function() {
  1263 + return '#' + this.toHex8();
  1264 + },
  1265 + toRgb: function() {
  1266 + return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
  1267 + },
  1268 + toRgbString: function() {
  1269 + return (this._a == 1) ?
  1270 + "rgb(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
  1271 + "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
  1272 + },
  1273 + toPercentageRgb: function() {
  1274 + return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
  1275 + },
  1276 + toPercentageRgbString: function() {
  1277 + return (this._a == 1) ?
  1278 + "rgb(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
  1279 + "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
  1280 + },
  1281 + toName: function() {
  1282 + if (this._a === 0) {
  1283 + return "transparent";
  1284 + }
  1285 +
  1286 + if (this._a < 1) {
  1287 + return false;
  1288 + }
  1289 +
  1290 + return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
  1291 + },
  1292 + toFilter: function(secondColor) {
  1293 + var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a);
  1294 + var secondHex8String = hex8String;
  1295 + var gradientType = this._gradientType ? "GradientType = 1, " : "";
  1296 +
  1297 + if (secondColor) {
  1298 + var s = tinycolor(secondColor);
  1299 + secondHex8String = s.toHex8String();
  1300 + }
  1301 +
  1302 + return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
  1303 + },
  1304 + toString: function(format) {
  1305 + var formatSet = !!format;
  1306 + format = format || this._format;
  1307 +
  1308 + var formattedString = false;
  1309 + var hasAlpha = this._a < 1 && this._a >= 0;
  1310 + var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
  1311 +
  1312 + if (needsAlphaFormat) {
  1313 + // Special case for "transparent", all other non-alpha formats
  1314 + // will return rgba when there is transparency.
  1315 + if (format === "name" && this._a === 0) {
  1316 + return this.toName();
  1317 + }
  1318 + return this.toRgbString();
  1319 + }
  1320 + if (format === "rgb") {
  1321 + formattedString = this.toRgbString();
  1322 + }
  1323 + if (format === "prgb") {
  1324 + formattedString = this.toPercentageRgbString();
  1325 + }
  1326 + if (format === "hex" || format === "hex6") {
  1327 + formattedString = this.toHexString();
  1328 + }
  1329 + if (format === "hex3") {
  1330 + formattedString = this.toHexString(true);
  1331 + }
  1332 + if (format === "hex8") {
  1333 + formattedString = this.toHex8String();
  1334 + }
  1335 + if (format === "name") {
  1336 + formattedString = this.toName();
  1337 + }
  1338 + if (format === "hsl") {
  1339 + formattedString = this.toHslString();
  1340 + }
  1341 + if (format === "hsv") {
  1342 + formattedString = this.toHsvString();
  1343 + }
  1344 +
  1345 + return formattedString || this.toHexString();
  1346 + },
  1347 +
  1348 + _applyModification: function(fn, args) {
  1349 + var color = fn.apply(null, [this].concat([].slice.call(args)));
  1350 + this._r = color._r;
  1351 + this._g = color._g;
  1352 + this._b = color._b;
  1353 + this.setAlpha(color._a);
  1354 + return this;
  1355 + },
  1356 + lighten: function() {
  1357 + return this._applyModification(lighten, arguments);
  1358 + },
  1359 + brighten: function() {
  1360 + return this._applyModification(brighten, arguments);
  1361 + },
  1362 + darken: function() {
  1363 + return this._applyModification(darken, arguments);
  1364 + },
  1365 + desaturate: function() {
  1366 + return this._applyModification(desaturate, arguments);
  1367 + },
  1368 + saturate: function() {
  1369 + return this._applyModification(saturate, arguments);
  1370 + },
  1371 + greyscale: function() {
  1372 + return this._applyModification(greyscale, arguments);
  1373 + },
  1374 + spin: function() {
  1375 + return this._applyModification(spin, arguments);
  1376 + },
  1377 +
  1378 + _applyCombination: function(fn, args) {
  1379 + return fn.apply(null, [this].concat([].slice.call(args)));
  1380 + },
  1381 + analogous: function() {
  1382 + return this._applyCombination(analogous, arguments);
  1383 + },
  1384 + complement: function() {
  1385 + return this._applyCombination(complement, arguments);
  1386 + },
  1387 + monochromatic: function() {
  1388 + return this._applyCombination(monochromatic, arguments);
  1389 + },
  1390 + splitcomplement: function() {
  1391 + return this._applyCombination(splitcomplement, arguments);
  1392 + },
  1393 + triad: function() {
  1394 + return this._applyCombination(triad, arguments);
  1395 + },
  1396 + tetrad: function() {
  1397 + return this._applyCombination(tetrad, arguments);
  1398 + }
  1399 + };
  1400 +
  1401 + // If input is an object, force 1 into "1.0" to handle ratios properly
  1402 + // String input requires "1.0" as input, so 1 will be treated as 1
  1403 + tinycolor.fromRatio = function(color, opts) {
  1404 + if (typeof color == "object") {
  1405 + var newColor = {};
  1406 + for (var i in color) {
  1407 + if (color.hasOwnProperty(i)) {
  1408 + if (i === "a") {
  1409 + newColor[i] = color[i];
  1410 + }
  1411 + else {
  1412 + newColor[i] = convertToPercentage(color[i]);
  1413 + }
  1414 + }
  1415 + }
  1416 + color = newColor;
  1417 + }
  1418 +
  1419 + return tinycolor(color, opts);
  1420 + };
  1421 +
  1422 + // Given a string or object, convert that input to RGB
  1423 + // Possible string inputs:
  1424 + //
  1425 + // "red"
  1426 + // "#f00" or "f00"
  1427 + // "#ff0000" or "ff0000"
  1428 + // "#ff000000" or "ff000000"
  1429 + // "rgb 255 0 0" or "rgb (255, 0, 0)"
  1430 + // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
  1431 + // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
  1432 + // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
  1433 + // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
  1434 + // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
  1435 + // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
  1436 + //
  1437 + function inputToRGB(color) {
  1438 +
  1439 + var rgb = { r: 0, g: 0, b: 0 };
  1440 + var a = 1;
  1441 + var ok = false;
  1442 + var format = false;
  1443 +
  1444 + if (typeof color == "string") {
  1445 + color = stringInputToObject(color);
  1446 + }
  1447 +
  1448 + if (typeof color == "object") {
  1449 + if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
  1450 + rgb = rgbToRgb(color.r, color.g, color.b);
  1451 + ok = true;
  1452 + format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
  1453 + }
  1454 + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
  1455 + color.s = convertToPercentage(color.s);
  1456 + color.v = convertToPercentage(color.v);
  1457 + rgb = hsvToRgb(color.h, color.s, color.v);
  1458 + ok = true;
  1459 + format = "hsv";
  1460 + }
  1461 + else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
  1462 + color.s = convertToPercentage(color.s);
  1463 + color.l = convertToPercentage(color.l);
  1464 + rgb = hslToRgb(color.h, color.s, color.l);
  1465 + ok = true;
  1466 + format = "hsl";
  1467 + }
  1468 +
  1469 + if (color.hasOwnProperty("a")) {
  1470 + a = color.a;
  1471 + }
  1472 + }
  1473 +
  1474 + a = boundAlpha(a);
  1475 +
  1476 + return {
  1477 + ok: ok,
  1478 + format: color.format || format,
  1479 + r: mathMin(255, mathMax(rgb.r, 0)),
  1480 + g: mathMin(255, mathMax(rgb.g, 0)),
  1481 + b: mathMin(255, mathMax(rgb.b, 0)),
  1482 + a: a
  1483 + };
  1484 + }
  1485 +
  1486 +
  1487 + // Conversion Functions
  1488 + // --------------------
  1489 +
  1490 + // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
  1491 + // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
  1492 +
  1493 + // `rgbToRgb`
  1494 + // Handle bounds / percentage checking to conform to CSS color spec
  1495 + // <http://www.w3.org/TR/css3-color/>
  1496 + // *Assumes:* r, g, b in [0, 255] or [0, 1]
  1497 + // *Returns:* { r, g, b } in [0, 255]
  1498 + function rgbToRgb(r, g, b){
  1499 + return {
  1500 + r: bound01(r, 255) * 255,
  1501 + g: bound01(g, 255) * 255,
  1502 + b: bound01(b, 255) * 255
  1503 + };
  1504 + }
  1505 +
  1506 + // `rgbToHsl`
  1507 + // Converts an RGB color value to HSL.
  1508 + // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
  1509 + // *Returns:* { h, s, l } in [0,1]
  1510 + function rgbToHsl(r, g, b) {
  1511 +
  1512 + r = bound01(r, 255);
  1513 + g = bound01(g, 255);
  1514 + b = bound01(b, 255);
  1515 +
  1516 + var max = mathMax(r, g, b), min = mathMin(r, g, b);
  1517 + var h, s, l = (max + min) / 2;
  1518 +
  1519 + if(max == min) {
  1520 + h = s = 0; // achromatic
  1521 + }
  1522 + else {
  1523 + var d = max - min;
  1524 + s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
  1525 + switch(max) {
  1526 + case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  1527 + case g: h = (b - r) / d + 2; break;
  1528 + case b: h = (r - g) / d + 4; break;
  1529 + }
  1530 +
  1531 + h /= 6;
  1532 + }
  1533 +
  1534 + return { h: h, s: s, l: l };
  1535 + }
  1536 +
  1537 + // `hslToRgb`
  1538 + // Converts an HSL color value to RGB.
  1539 + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
  1540 + // *Returns:* { r, g, b } in the set [0, 255]
  1541 + function hslToRgb(h, s, l) {
  1542 + var r, g, b;
  1543 +
  1544 + h = bound01(h, 360);
  1545 + s = bound01(s, 100);
  1546 + l = bound01(l, 100);
  1547 +
  1548 + function hue2rgb(p, q, t) {
  1549 + if(t < 0) t += 1;
  1550 + if(t > 1) t -= 1;
  1551 + if(t < 1/6) return p + (q - p) * 6 * t;
  1552 + if(t < 1/2) return q;
  1553 + if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
  1554 + return p;
  1555 + }
  1556 +
  1557 + if(s === 0) {
  1558 + r = g = b = l; // achromatic
  1559 + }
  1560 + else {
  1561 + var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
  1562 + var p = 2 * l - q;
  1563 + r = hue2rgb(p, q, h + 1/3);
  1564 + g = hue2rgb(p, q, h);
  1565 + b = hue2rgb(p, q, h - 1/3);
  1566 + }
  1567 +
  1568 + return { r: r * 255, g: g * 255, b: b * 255 };
  1569 + }
  1570 +
  1571 + // `rgbToHsv`
  1572 + // Converts an RGB color value to HSV
  1573 + // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
  1574 + // *Returns:* { h, s, v } in [0,1]
  1575 + function rgbToHsv(r, g, b) {
  1576 +
  1577 + r = bound01(r, 255);
  1578 + g = bound01(g, 255);
  1579 + b = bound01(b, 255);
  1580 +
  1581 + var max = mathMax(r, g, b), min = mathMin(r, g, b);
  1582 + var h, s, v = max;
  1583 +
  1584 + var d = max - min;
  1585 + s = max === 0 ? 0 : d / max;
  1586 +
  1587 + if(max == min) {
  1588 + h = 0; // achromatic
  1589 + }
  1590 + else {
  1591 + switch(max) {
  1592 + case r: h = (g - b) / d + (g < b ? 6 : 0); break;
  1593 + case g: h = (b - r) / d + 2; break;
  1594 + case b: h = (r - g) / d + 4; break;
  1595 + }
  1596 + h /= 6;
  1597 + }
  1598 + return { h: h, s: s, v: v };
  1599 + }
  1600 +
  1601 + // `hsvToRgb`
  1602 + // Converts an HSV color value to RGB.
  1603 + // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
  1604 + // *Returns:* { r, g, b } in the set [0, 255]
  1605 + function hsvToRgb(h, s, v) {
  1606 +
  1607 + h = bound01(h, 360) * 6;
  1608 + s = bound01(s, 100);
  1609 + v = bound01(v, 100);
  1610 +
  1611 + var i = math.floor(h),
  1612 + f = h - i,
  1613 + p = v * (1 - s),
  1614 + q = v * (1 - f * s),
  1615 + t = v * (1 - (1 - f) * s),
  1616 + mod = i % 6,
  1617 + r = [v, q, p, p, t, v][mod],
  1618 + g = [t, v, v, q, p, p][mod],
  1619 + b = [p, p, t, v, v, q][mod];
  1620 +
  1621 + return { r: r * 255, g: g * 255, b: b * 255 };
  1622 + }
  1623 +
  1624 + // `rgbToHex`
  1625 + // Converts an RGB color to hex
  1626 + // Assumes r, g, and b are contained in the set [0, 255]
  1627 + // Returns a 3 or 6 character hex
  1628 + function rgbToHex(r, g, b, allow3Char) {
  1629 +
  1630 + var hex = [
  1631 + pad2(mathRound(r).toString(16)),
  1632 + pad2(mathRound(g).toString(16)),
  1633 + pad2(mathRound(b).toString(16))
  1634 + ];
  1635 +
  1636 + // Return a 3 character hex if possible
  1637 + if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
  1638 + return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
  1639 + }
  1640 +
  1641 + return hex.join("");
  1642 + }
  1643 + // `rgbaToHex`
  1644 + // Converts an RGBA color plus alpha transparency to hex
  1645 + // Assumes r, g, b and a are contained in the set [0, 255]
  1646 + // Returns an 8 character hex
  1647 + function rgbaToHex(r, g, b, a) {
  1648 +
  1649 + var hex = [
  1650 + pad2(convertDecimalToHex(a)),
  1651 + pad2(mathRound(r).toString(16)),
  1652 + pad2(mathRound(g).toString(16)),
  1653 + pad2(mathRound(b).toString(16))
  1654 + ];
  1655 +
  1656 + return hex.join("");
  1657 + }
  1658 +
  1659 + // `equals`
  1660 + // Can be called with any tinycolor input
  1661 + tinycolor.equals = function (color1, color2) {
  1662 + if (!color1 || !color2) { return false; }
  1663 + return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
  1664 + };
  1665 + tinycolor.random = function() {
  1666 + return tinycolor.fromRatio({
  1667 + r: mathRandom(),
  1668 + g: mathRandom(),
  1669 + b: mathRandom()
  1670 + });
  1671 + };
  1672 +
  1673 +
  1674 + // Modification Functions
  1675 + // ----------------------
  1676 + // Thanks to less.js for some of the basics here
  1677 + // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
  1678 +
  1679 + function desaturate(color, amount) {
  1680 + amount = (amount === 0) ? 0 : (amount || 10);
  1681 + var hsl = tinycolor(color).toHsl();
  1682 + hsl.s -= amount / 100;
  1683 + hsl.s = clamp01(hsl.s);
  1684 + return tinycolor(hsl);
  1685 + }
  1686 +
  1687 + function saturate(color, amount) {
  1688 + amount = (amount === 0) ? 0 : (amount || 10);
  1689 + var hsl = tinycolor(color).toHsl();
  1690 + hsl.s += amount / 100;
  1691 + hsl.s = clamp01(hsl.s);
  1692 + return tinycolor(hsl);
  1693 + }
  1694 +
  1695 + function greyscale(color) {
  1696 + return tinycolor(color).desaturate(100);
  1697 + }
  1698 +
  1699 + function lighten (color, amount) {
  1700 + amount = (amount === 0) ? 0 : (amount || 10);
  1701 + var hsl = tinycolor(color).toHsl();
  1702 + hsl.l += amount / 100;
  1703 + hsl.l = clamp01(hsl.l);
  1704 + return tinycolor(hsl);
  1705 + }
  1706 +
  1707 + function brighten(color, amount) {
  1708 + amount = (amount === 0) ? 0 : (amount || 10);
  1709 + var rgb = tinycolor(color).toRgb();
  1710 + rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
  1711 + rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
  1712 + rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
  1713 + return tinycolor(rgb);
  1714 + }
  1715 +
  1716 + function darken (color, amount) {
  1717 + amount = (amount === 0) ? 0 : (amount || 10);
  1718 + var hsl = tinycolor(color).toHsl();
  1719 + hsl.l -= amount / 100;
  1720 + hsl.l = clamp01(hsl.l);
  1721 + return tinycolor(hsl);
  1722 + }
  1723 +
  1724 + // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
  1725 + // Values outside of this range will be wrapped into this range.
  1726 + function spin(color, amount) {
  1727 + var hsl = tinycolor(color).toHsl();
  1728 + var hue = (mathRound(hsl.h) + amount) % 360;
  1729 + hsl.h = hue < 0 ? 360 + hue : hue;
  1730 + return tinycolor(hsl);
  1731 + }
  1732 +
  1733 + // Combination Functions
  1734 + // ---------------------
  1735 + // Thanks to jQuery xColor for some of the ideas behind these
  1736 + // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
  1737 +
  1738 + function complement(color) {
  1739 + var hsl = tinycolor(color).toHsl();
  1740 + hsl.h = (hsl.h + 180) % 360;
  1741 + return tinycolor(hsl);
  1742 + }
  1743 +
  1744 + function triad(color) {
  1745 + var hsl = tinycolor(color).toHsl();
  1746 + var h = hsl.h;
  1747 + return [
  1748 + tinycolor(color),
  1749 + tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
  1750 + tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
  1751 + ];
  1752 + }
  1753 +
  1754 + function tetrad(color) {
  1755 + var hsl = tinycolor(color).toHsl();
  1756 + var h = hsl.h;
  1757 + return [
  1758 + tinycolor(color),
  1759 + tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
  1760 + tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
  1761 + tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
  1762 + ];
  1763 + }
  1764 +
  1765 + function splitcomplement(color) {
  1766 + var hsl = tinycolor(color).toHsl();
  1767 + var h = hsl.h;
  1768 + return [
  1769 + tinycolor(color),
  1770 + tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
  1771 + tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
  1772 + ];
  1773 + }
  1774 +
  1775 + function analogous(color, results, slices) {
  1776 + results = results || 6;
  1777 + slices = slices || 30;
  1778 +
  1779 + var hsl = tinycolor(color).toHsl();
  1780 + var part = 360 / slices;
  1781 + var ret = [tinycolor(color)];
  1782 +
  1783 + for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
  1784 + hsl.h = (hsl.h + part) % 360;
  1785 + ret.push(tinycolor(hsl));
  1786 + }
  1787 + return ret;
  1788 + }
  1789 +
  1790 + function monochromatic(color, results) {
  1791 + results = results || 6;
  1792 + var hsv = tinycolor(color).toHsv();
  1793 + var h = hsv.h, s = hsv.s, v = hsv.v;
  1794 + var ret = [];
  1795 + var modification = 1 / results;
  1796 +
  1797 + while (results--) {
  1798 + ret.push(tinycolor({ h: h, s: s, v: v}));
  1799 + v = (v + modification) % 1;
  1800 + }
  1801 +
  1802 + return ret;
  1803 + }
  1804 +
  1805 + // Utility Functions
  1806 + // ---------------------
  1807 +
  1808 + tinycolor.mix = function(color1, color2, amount) {
  1809 + amount = (amount === 0) ? 0 : (amount || 50);
  1810 +
  1811 + var rgb1 = tinycolor(color1).toRgb();
  1812 + var rgb2 = tinycolor(color2).toRgb();
  1813 +
  1814 + var p = amount / 100;
  1815 + var w = p * 2 - 1;
  1816 + var a = rgb2.a - rgb1.a;
  1817 +
  1818 + var w1;
  1819 +
  1820 + if (w * a == -1) {
  1821 + w1 = w;
  1822 + } else {
  1823 + w1 = (w + a) / (1 + w * a);
  1824 + }
  1825 +
  1826 + w1 = (w1 + 1) / 2;
  1827 +
  1828 + var w2 = 1 - w1;
  1829 +
  1830 + var rgba = {
  1831 + r: rgb2.r * w1 + rgb1.r * w2,
  1832 + g: rgb2.g * w1 + rgb1.g * w2,
  1833 + b: rgb2.b * w1 + rgb1.b * w2,
  1834 + a: rgb2.a * p + rgb1.a * (1 - p)
  1835 + };
  1836 +
  1837 + return tinycolor(rgba);
  1838 + };
  1839 +
  1840 +
  1841 + // Readability Functions
  1842 + // ---------------------
  1843 + // <http://www.w3.org/TR/AERT#color-contrast>
  1844 +
  1845 + // `readability`
  1846 + // Analyze the 2 colors and returns an object with the following properties:
  1847 + // `brightness`: difference in brightness between the two colors
  1848 + // `color`: difference in color/hue between the two colors
  1849 + tinycolor.readability = function(color1, color2) {
  1850 + var c1 = tinycolor(color1);
  1851 + var c2 = tinycolor(color2);
  1852 + var rgb1 = c1.toRgb();
  1853 + var rgb2 = c2.toRgb();
  1854 + var brightnessA = c1.getBrightness();
  1855 + var brightnessB = c2.getBrightness();
  1856 + var colorDiff = (
  1857 + Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
  1858 + Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
  1859 + Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
  1860 + );
  1861 +
  1862 + return {
  1863 + brightness: Math.abs(brightnessA - brightnessB),
  1864 + color: colorDiff
  1865 + };
  1866 + };
  1867 +
  1868 + // `readable`
  1869 + // http://www.w3.org/TR/AERT#color-contrast
  1870 + // Ensure that foreground and background color combinations provide sufficient contrast.
  1871 + // *Example*
  1872 + // tinycolor.isReadable("#000", "#111") => false
  1873 + tinycolor.isReadable = function(color1, color2) {
  1874 + var readability = tinycolor.readability(color1, color2);
  1875 + return readability.brightness > 125 && readability.color > 500;
  1876 + };
  1877 +
  1878 + // `mostReadable`
  1879 + // Given a base color and a list of possible foreground or background
  1880 + // colors for that base, returns the most readable color.
  1881 + // *Example*
  1882 + // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
  1883 + tinycolor.mostReadable = function(baseColor, colorList) {
  1884 + var bestColor = null;
  1885 + var bestScore = 0;
  1886 + var bestIsReadable = false;
  1887 + for (var i=0; i < colorList.length; i++) {
  1888 +
  1889 + // We normalize both around the "acceptable" breaking point,
  1890 + // but rank brightness constrast higher than hue.
  1891 +
  1892 + var readability = tinycolor.readability(baseColor, colorList[i]);
  1893 + var readable = readability.brightness > 125 && readability.color > 500;
  1894 + var score = 3 * (readability.brightness / 125) + (readability.color / 500);
  1895 +
  1896 + if ((readable && ! bestIsReadable) ||
  1897 + (readable && bestIsReadable && score > bestScore) ||
  1898 + ((! readable) && (! bestIsReadable) && score > bestScore)) {
  1899 + bestIsReadable = readable;
  1900 + bestScore = score;
  1901 + bestColor = tinycolor(colorList[i]);
  1902 + }
  1903 + }
  1904 + return bestColor;
  1905 + };
  1906 +
  1907 +
  1908 + // Big List of Colors
  1909 + // ------------------
  1910 + // <http://www.w3.org/TR/css3-color/#svg-color>
  1911 + var names = tinycolor.names = {
  1912 + aliceblue: "f0f8ff",
  1913 + antiquewhite: "faebd7",
  1914 + aqua: "0ff",
  1915 + aquamarine: "7fffd4",
  1916 + azure: "f0ffff",
  1917 + beige: "f5f5dc",
  1918 + bisque: "ffe4c4",
  1919 + black: "000",
  1920 + blanchedalmond: "ffebcd",
  1921 + blue: "00f",
  1922 + blueviolet: "8a2be2",
  1923 + brown: "a52a2a",
  1924 + burlywood: "deb887",
  1925 + burntsienna: "ea7e5d",
  1926 + cadetblue: "5f9ea0",
  1927 + chartreuse: "7fff00",
  1928 + chocolate: "d2691e",
  1929 + coral: "ff7f50",
  1930 + cornflowerblue: "6495ed",
  1931 + cornsilk: "fff8dc",
  1932 + crimson: "dc143c",
  1933 + cyan: "0ff",
  1934 + darkblue: "00008b",
  1935 + darkcyan: "008b8b",
  1936 + darkgoldenrod: "b8860b",
  1937 + darkgray: "a9a9a9",
  1938 + darkgreen: "006400",
  1939 + darkgrey: "a9a9a9",
  1940 + darkkhaki: "bdb76b",
  1941 + darkmagenta: "8b008b",
  1942 + darkolivegreen: "556b2f",
  1943 + darkorange: "ff8c00",
  1944 + darkorchid: "9932cc",
  1945 + darkred: "8b0000",
  1946 + darksalmon: "e9967a",
  1947 + darkseagreen: "8fbc8f",
  1948 + darkslateblue: "483d8b",
  1949 + darkslategray: "2f4f4f",
  1950 + darkslategrey: "2f4f4f",
  1951 + darkturquoise: "00ced1",
  1952 + darkviolet: "9400d3",
  1953 + deeppink: "ff1493",
  1954 + deepskyblue: "00bfff",
  1955 + dimgray: "696969",
  1956 + dimgrey: "696969",
  1957 + dodgerblue: "1e90ff",
  1958 + firebrick: "b22222",
  1959 + floralwhite: "fffaf0",
  1960 + forestgreen: "228b22",
  1961 + fuchsia: "f0f",
  1962 + gainsboro: "dcdcdc",
  1963 + ghostwhite: "f8f8ff",
  1964 + gold: "ffd700",
  1965 + goldenrod: "daa520",
  1966 + gray: "808080",
  1967 + green: "008000",
  1968 + greenyellow: "adff2f",
  1969 + grey: "808080",
  1970 + honeydew: "f0fff0",
  1971 + hotpink: "ff69b4",
  1972 + indianred: "cd5c5c",
  1973 + indigo: "4b0082",
  1974 + ivory: "fffff0",
  1975 + khaki: "f0e68c",
  1976 + lavender: "e6e6fa",
  1977 + lavenderblush: "fff0f5",
  1978 + lawngreen: "7cfc00",
  1979 + lemonchiffon: "fffacd",
  1980 + lightblue: "add8e6",
  1981 + lightcoral: "f08080",
  1982 + lightcyan: "e0ffff",
  1983 + lightgoldenrodyellow: "fafad2",
  1984 + lightgray: "d3d3d3",
  1985 + lightgreen: "90ee90",
  1986 + lightgrey: "d3d3d3",
  1987 + lightpink: "ffb6c1",
  1988 + lightsalmon: "ffa07a",
  1989 + lightseagreen: "20b2aa",
  1990 + lightskyblue: "87cefa",
  1991 + lightslategray: "789",
  1992 + lightslategrey: "789",
  1993 + lightsteelblue: "b0c4de",
  1994 + lightyellow: "ffffe0",
  1995 + lime: "0f0",
  1996 + limegreen: "32cd32",
  1997 + linen: "faf0e6",
  1998 + magenta: "f0f",
  1999 + maroon: "800000",
  2000 + mediumaquamarine: "66cdaa",
  2001 + mediumblue: "0000cd",
  2002 + mediumorchid: "ba55d3",
  2003 + mediumpurple: "9370db",
  2004 + mediumseagreen: "3cb371",
  2005 + mediumslateblue: "7b68ee",
  2006 + mediumspringgreen: "00fa9a",
  2007 + mediumturquoise: "48d1cc",
  2008 + mediumvioletred: "c71585",
  2009 + midnightblue: "191970",
  2010 + mintcream: "f5fffa",
  2011 + mistyrose: "ffe4e1",
  2012 + moccasin: "ffe4b5",
  2013 + navajowhite: "ffdead",
  2014 + navy: "000080",
  2015 + oldlace: "fdf5e6",
  2016 + olive: "808000",
  2017 + olivedrab: "6b8e23",
  2018 + orange: "ffa500",
  2019 + orangered: "ff4500",
  2020 + orchid: "da70d6",
  2021 + palegoldenrod: "eee8aa",
  2022 + palegreen: "98fb98",
  2023 + paleturquoise: "afeeee",
  2024 + palevioletred: "db7093",
  2025 + papayawhip: "ffefd5",
  2026 + peachpuff: "ffdab9",
  2027 + peru: "cd853f",
  2028 + pink: "ffc0cb",
  2029 + plum: "dda0dd",
  2030 + powderblue: "b0e0e6",
  2031 + purple: "800080",
  2032 + red: "f00",
  2033 + rosybrown: "bc8f8f",
  2034 + royalblue: "4169e1",
  2035 + saddlebrown: "8b4513",
  2036 + salmon: "fa8072",
  2037 + sandybrown: "f4a460",
  2038 + seagreen: "2e8b57",
  2039 + seashell: "fff5ee",
  2040 + sienna: "a0522d",
  2041 + silver: "c0c0c0",
  2042 + skyblue: "87ceeb",
  2043 + slateblue: "6a5acd",
  2044 + slategray: "708090",
  2045 + slategrey: "708090",
  2046 + snow: "fffafa",
  2047 + springgreen: "00ff7f",
  2048 + steelblue: "4682b4",
  2049 + tan: "d2b48c",
  2050 + teal: "008080",
  2051 + thistle: "d8bfd8",
  2052 + tomato: "ff6347",
  2053 + turquoise: "40e0d0",
  2054 + violet: "ee82ee",
  2055 + wheat: "f5deb3",
  2056 + white: "fff",
  2057 + whitesmoke: "f5f5f5",
  2058 + yellow: "ff0",
  2059 + yellowgreen: "9acd32"
  2060 + };
  2061 +
  2062 + // Make it easy to access colors via `hexNames[hex]`
  2063 + var hexNames = tinycolor.hexNames = flip(names);
  2064 +
  2065 +
  2066 + // Utilities
  2067 + // ---------
  2068 +
  2069 + // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
  2070 + function flip(o) {
  2071 + var flipped = { };
  2072 + for (var i in o) {
  2073 + if (o.hasOwnProperty(i)) {
  2074 + flipped[o[i]] = i;
  2075 + }
  2076 + }
  2077 + return flipped;
  2078 + }
  2079 +
  2080 + // Return a valid alpha value [0,1] with all invalid values being set to 1
  2081 + function boundAlpha(a) {
  2082 + a = parseFloat(a);
  2083 +
  2084 + if (isNaN(a) || a < 0 || a > 1) {
  2085 + a = 1;
  2086 + }
  2087 +
  2088 + return a;
  2089 + }
  2090 +
  2091 + // Take input from [0, n] and return it as [0, 1]
  2092 + function bound01(n, max) {
  2093 + if (isOnePointZero(n)) { n = "100%"; }
  2094 +
  2095 + var processPercent = isPercentage(n);
  2096 + n = mathMin(max, mathMax(0, parseFloat(n)));
  2097 +
  2098 + // Automatically convert percentage into number
  2099 + if (processPercent) {
  2100 + n = parseInt(n * max, 10) / 100;
  2101 + }
  2102 +
  2103 + // Handle floating point rounding errors
  2104 + if ((math.abs(n - max) < 0.000001)) {
  2105 + return 1;
  2106 + }
  2107 +
  2108 + // Convert into [0, 1] range if it isn't already
  2109 + return (n % max) / parseFloat(max);
  2110 + }
  2111 +
  2112 + // Force a number between 0 and 1
  2113 + function clamp01(val) {
  2114 + return mathMin(1, mathMax(0, val));
  2115 + }
  2116 +
  2117 + // Parse a base-16 hex value into a base-10 integer
  2118 + function parseIntFromHex(val) {
  2119 + return parseInt(val, 16);
  2120 + }
  2121 +
  2122 + // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
  2123 + // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
  2124 + function isOnePointZero(n) {
  2125 + return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
  2126 + }
  2127 +
  2128 + // Check to see if string passed in is a percentage
  2129 + function isPercentage(n) {
  2130 + return typeof n === "string" && n.indexOf('%') != -1;
  2131 + }
  2132 +
  2133 + // Force a hex value to have 2 characters
  2134 + function pad2(c) {
  2135 + return c.length == 1 ? '0' + c : '' + c;
  2136 + }
  2137 +
  2138 + // Replace a decimal with it's percentage value
  2139 + function convertToPercentage(n) {
  2140 + if (n <= 1) {
  2141 + n = (n * 100) + "%";
  2142 + }
  2143 +
  2144 + return n;
  2145 + }
  2146 +
  2147 + // Converts a decimal to a hex value
  2148 + function convertDecimalToHex(d) {
  2149 + return Math.round(parseFloat(d) * 255).toString(16);
  2150 + }
  2151 + // Converts a hex value to a decimal
  2152 + function convertHexToDecimal(h) {
  2153 + return (parseIntFromHex(h) / 255);
  2154 + }
  2155 +
  2156 + var matchers = (function() {
  2157 +
  2158 + // <http://www.w3.org/TR/css3-values/#integers>
  2159 + var CSS_INTEGER = "[-\\+]?\\d+%?";
  2160 +
  2161 + // <http://www.w3.org/TR/css3-values/#number-value>
  2162 + var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
  2163 +
  2164 + // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
  2165 + var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
  2166 +
  2167 + // Actual matching.
  2168 + // Parentheses and commas are optional, but not required.
  2169 + // Whitespace can take the place of commas or opening paren
  2170 + var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  2171 + var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
  2172 +
  2173 + return {
  2174 + rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
  2175 + rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
  2176 + hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
  2177 + hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
  2178 + hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
  2179 + hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
  2180 + hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
  2181 + hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
  2182 + };
  2183 + })();
  2184 +
  2185 + // `stringInputToObject`
  2186 + // Permissive string parsing. Take in a number of formats, and output an object
  2187 + // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
  2188 + function stringInputToObject(color) {
  2189 +
  2190 + color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
  2191 + var named = false;
  2192 + if (names[color]) {
  2193 + color = names[color];
  2194 + named = true;
  2195 + }
  2196 + else if (color == 'transparent') {
  2197 + return { r: 0, g: 0, b: 0, a: 0, format: "name" };
  2198 + }
  2199 +
  2200 + // Try to match string input using regular expressions.
  2201 + // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
  2202 + // Just return an object and let the conversion functions handle that.
  2203 + // This way the result will be the same whether the tinycolor is initialized with string or object.
  2204 + var match;
  2205 + if ((match = matchers.rgb.exec(color))) {
  2206 + return { r: match[1], g: match[2], b: match[3] };
  2207 + }
  2208 + if ((match = matchers.rgba.exec(color))) {
  2209 + return { r: match[1], g: match[2], b: match[3], a: match[4] };
  2210 + }
  2211 + if ((match = matchers.hsl.exec(color))) {
  2212 + return { h: match[1], s: match[2], l: match[3] };
  2213 + }
  2214 + if ((match = matchers.hsla.exec(color))) {
  2215 + return { h: match[1], s: match[2], l: match[3], a: match[4] };
  2216 + }
  2217 + if ((match = matchers.hsv.exec(color))) {
  2218 + return { h: match[1], s: match[2], v: match[3] };
  2219 + }
  2220 + if ((match = matchers.hex8.exec(color))) {
  2221 + return {
  2222 + a: convertHexToDecimal(match[1]),
  2223 + r: parseIntFromHex(match[2]),
  2224 + g: parseIntFromHex(match[3]),
  2225 + b: parseIntFromHex(match[4]),
  2226 + format: named ? "name" : "hex8"
  2227 + };
  2228 + }
  2229 + if ((match = matchers.hex6.exec(color))) {
  2230 + return {
  2231 + r: parseIntFromHex(match[1]),
  2232 + g: parseIntFromHex(match[2]),
  2233 + b: parseIntFromHex(match[3]),
  2234 + format: named ? "name" : "hex"
  2235 + };
  2236 + }
  2237 + if ((match = matchers.hex3.exec(color))) {
  2238 + return {
  2239 + r: parseIntFromHex(match[1] + '' + match[1]),
  2240 + g: parseIntFromHex(match[2] + '' + match[2]),
  2241 + b: parseIntFromHex(match[3] + '' + match[3]),
  2242 + format: named ? "name" : "hex"
  2243 + };
  2244 + }
  2245 +
  2246 + return false;
  2247 + }
  2248 +
  2249 + window.tinycolor = tinycolor;
  2250 + })();
  2251 +
  2252 +
  2253 + $(function () {
  2254 + if ($.fn.spectrum.load) {
  2255 + $.fn.spectrum.processNativeColorInputs();
  2256 + }
  2257 + });
  2258 +
  2259 +})(window, jQuery);
... ...
public/stylesheets/application.css
... ... @@ -2649,6 +2649,15 @@ div#activation_enterprise label, div#activation_enterprise input, div#activation
2649 2649 color: #585858;
2650 2650 font-size: 11px;
2651 2651 }
  2652 +.colorpicker_field {
  2653 + display: none;
  2654 +}
  2655 +.color_marker {
  2656 + display: inline-block;
  2657 + width: 10px;
  2658 + height: 10px;
  2659 + border: 1px solid;
  2660 +}
2652 2661 .formfield input {
2653 2662 text-indent: 5px;
2654 2663 padding: 2px 0px;
... ...
public/stylesheets/inputosaurus.css 0 → 100644
... ... @@ -0,0 +1,76 @@
  1 +.inputosaurus-container .ui-autocomplete-input {
  2 + background-color: #ffffff;
  3 +}
  4 +
  5 +.inputosaurus-container {
  6 + /*background-color: #fff;*/
  7 + /*border: 1px solid #bcbec0;*/
  8 + margin: 0;
  9 + padding: 0;
  10 + display: inline-block;
  11 + cursor: text;
  12 + /**font-size: 14px;**/
  13 + /**font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;**/
  14 + max-height: 300px;
  15 + overflow: hidden;
  16 + overflow-y: auto;
  17 +}
  18 +.inputosaurus-container li {
  19 + display: block;
  20 + float: left;
  21 + overflow: hidden;
  22 + margin: 2px 2px 0;
  23 + padding: 2px 3px;
  24 + white-space: nowrap;
  25 + overflow: hidden;
  26 + -o-text-overflow: ellipsis;
  27 + -ms-text-overflow: ellipsis;
  28 + text-overflow: ellipsis;
  29 + background-color: #e5eff7;
  30 + border: #a9cae4 solid 1px;
  31 + -webkit-border-radius: 2px;
  32 + -moz-border-radius: 2px;
  33 + border-radius: 2px;
  34 + color: #5b9bcd;
  35 + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  36 + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  37 + box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset;
  38 + line-height: 20px;
  39 + cursor: default;
  40 +}
  41 +.inputosaurus-container li.inputosaurus-selected { background-color: #bdd6eb; }
  42 +.inputosaurus-container li a {
  43 + font-size: 16px;
  44 + color: #5b9bcd;
  45 + padding: 1px;
  46 + text-decoration: none;
  47 + outline: none;
  48 +}
  49 +.inputosaurus-container .inputosaurus-input {
  50 + border: none;
  51 + box-shadow: none;
  52 +}
  53 +.inputosaurus-container .inputosaurus-input input {
  54 + border: none;
  55 + height: 23px;
  56 + font-size: 14px;
  57 + line-height: 20px;
  58 + color: #555;
  59 + margin: 0;
  60 + outline: none;
  61 + padding: 0 0 1px 1px;
  62 + width: 25px;
  63 + -webkit-box-shadow: none;
  64 + -moz-box-shadow: none;
  65 + box-shadow: none;
  66 + background-image: none;
  67 + text-indent: 0px;
  68 +}
  69 +
  70 +.inputosaurus-container .inputosaurus-input input:hover {
  71 + -webkit-box-shadow: none;
  72 + -moz-box-shadow: none;
  73 + box-shadow: none;
  74 + background-color: #f0f0f0;
  75 +}
  76 +.inputosaurus-input-hidden { display: none; }
... ...
public/stylesheets/spectrum.css 0 → 100644
... ... @@ -0,0 +1,506 @@
  1 +/***
  2 +Spectrum Colorpicker v1.4.1
  3 +https://github.com/bgrins/spectrum
  4 +Author: Brian Grinstead
  5 +License: MIT
  6 +***/
  7 +
  8 +.sp-container {
  9 + position:absolute;
  10 + top:0;
  11 + left:0;
  12 + display:inline-block;
  13 + *display: inline;
  14 + *zoom: 1;
  15 + /* https://github.com/bgrins/spectrum/issues/40 */
  16 + z-index: 9999994;
  17 + overflow: hidden;
  18 +}
  19 +.sp-container.sp-flat {
  20 + position: relative;
  21 +}
  22 +
  23 +/* Fix for * { box-sizing: border-box; } */
  24 +.sp-container,
  25 +.sp-container * {
  26 + -webkit-box-sizing: content-box;
  27 + -moz-box-sizing: content-box;
  28 + box-sizing: content-box;
  29 +}
  30 +
  31 +/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
  32 +.sp-top {
  33 + position:relative;
  34 + width: 100%;
  35 + display:inline-block;
  36 +}
  37 +.sp-top-inner {
  38 + position:absolute;
  39 + top:0;
  40 + left:0;
  41 + bottom:0;
  42 + right:0;
  43 +}
  44 +.sp-color {
  45 + position: absolute;
  46 + top:0;
  47 + left:0;
  48 + bottom:0;
  49 + right:20%;
  50 +}
  51 +.sp-hue {
  52 + position: absolute;
  53 + top:0;
  54 + right:0;
  55 + bottom:0;
  56 + left:84%;
  57 + height: 100%;
  58 +}
  59 +
  60 +.sp-clear-enabled .sp-hue {
  61 + top:33px;
  62 + height: 77.5%;
  63 +}
  64 +
  65 +.sp-fill {
  66 + padding-top: 80%;
  67 +}
  68 +.sp-sat, .sp-val {
  69 + position: absolute;
  70 + top:0;
  71 + left:0;
  72 + right:0;
  73 + bottom:0;
  74 +}
  75 +
  76 +.sp-alpha-enabled .sp-top {
  77 + margin-bottom: 18px;
  78 +}
  79 +.sp-alpha-enabled .sp-alpha {
  80 + display: block;
  81 +}
  82 +.sp-alpha-handle {
  83 + position:absolute;
  84 + top:-4px;
  85 + bottom: -4px;
  86 + width: 6px;
  87 + left: 50%;
  88 + cursor: pointer;
  89 + border: 1px solid black;
  90 + background: white;
  91 + opacity: .8;
  92 +}
  93 +.sp-alpha {
  94 + display: none;
  95 + position: absolute;
  96 + bottom: -14px;
  97 + right: 0;
  98 + left: 0;
  99 + height: 8px;
  100 +}
  101 +.sp-alpha-inner {
  102 + border: solid 1px #333;
  103 +}
  104 +
  105 +.sp-clear {
  106 + display: none;
  107 +}
  108 +
  109 +.sp-clear.sp-clear-display {
  110 + background-position: center;
  111 +}
  112 +
  113 +.sp-clear-enabled .sp-clear {
  114 + display: block;
  115 + position:absolute;
  116 + top:0px;
  117 + right:0;
  118 + bottom:0;
  119 + left:84%;
  120 + height: 28px;
  121 +}
  122 +
  123 +/* Don't allow text selection */
  124 +.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button {
  125 + -webkit-user-select:none;
  126 + -moz-user-select: -moz-none;
  127 + -o-user-select:none;
  128 + user-select: none;
  129 +}
  130 +
  131 +.sp-container.sp-input-disabled .sp-input-container {
  132 + display: none;
  133 +}
  134 +.sp-container.sp-buttons-disabled .sp-button-container {
  135 + display: none;
  136 +}
  137 +.sp-container.sp-palette-buttons-disabled .sp-palette-button-container {
  138 + display: none;
  139 +}
  140 +.sp-palette-only .sp-picker-container {
  141 + display: none;
  142 +}
  143 +.sp-palette-disabled .sp-palette-container {
  144 + display: none;
  145 +}
  146 +
  147 +.sp-initial-disabled .sp-initial {
  148 + display: none;
  149 +}
  150 +
  151 +
  152 +/* Gradients for hue, saturation and value instead of images. Not pretty... but it works */
  153 +.sp-sat {
  154 + background-image: -webkit-gradient(linear, 0 0, 100% 0, from(#FFF), to(rgba(204, 154, 129, 0)));
  155 + background-image: -webkit-linear-gradient(left, #FFF, rgba(204, 154, 129, 0));
  156 + background-image: -moz-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  157 + background-image: -o-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  158 + background-image: -ms-linear-gradient(left, #fff, rgba(204, 154, 129, 0));
  159 + background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
  160 + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
  161 + filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
  162 +}
  163 +.sp-val {
  164 + background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#000000), to(rgba(204, 154, 129, 0)));
  165 + background-image: -webkit-linear-gradient(bottom, #000000, rgba(204, 154, 129, 0));
  166 + background-image: -moz-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  167 + background-image: -o-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  168 + background-image: -ms-linear-gradient(bottom, #000, rgba(204, 154, 129, 0));
  169 + background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
  170 + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
  171 + filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
  172 +}
  173 +
  174 +.sp-hue {
  175 + background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  176 + background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  177 + background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  178 + background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
  179 + background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
  180 +}
  181 +
  182 +/* IE filters do not support multiple color stops.
  183 + Generate 6 divs, line them up, and do two color gradients for each.
  184 + Yes, really.
  185 + */
  186 +.sp-1 {
  187 + height:17%;
  188 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
  189 +}
  190 +.sp-2 {
  191 + height:16%;
  192 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
  193 +}
  194 +.sp-3 {
  195 + height:17%;
  196 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
  197 +}
  198 +.sp-4 {
  199 + height:17%;
  200 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
  201 +}
  202 +.sp-5 {
  203 + height:16%;
  204 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
  205 +}
  206 +.sp-6 {
  207 + height:17%;
  208 + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
  209 +}
  210 +
  211 +.sp-hidden {
  212 + display: none !important;
  213 +}
  214 +
  215 +/* Clearfix hack */
  216 +.sp-cf:before, .sp-cf:after { content: ""; display: table; }
  217 +.sp-cf:after { clear: both; }
  218 +.sp-cf { *zoom: 1; }
  219 +
  220 +/* Mobile devices, make hue slider bigger so it is easier to slide */
  221 +@media (max-device-width: 480px) {
  222 + .sp-color { right: 40%; }
  223 + .sp-hue { left: 63%; }
  224 + .sp-fill { padding-top: 60%; }
  225 +}
  226 +.sp-dragger {
  227 + border-radius: 5px;
  228 + height: 5px;
  229 + width: 5px;
  230 + border: 1px solid #fff;
  231 + background: #000;
  232 + cursor: pointer;
  233 + position:absolute;
  234 + top:0;
  235 + left: 0;
  236 +}
  237 +.sp-slider {
  238 + position: absolute;
  239 + top:0;
  240 + cursor:pointer;
  241 + height: 3px;
  242 + left: -1px;
  243 + right: -1px;
  244 + border: 1px solid #000;
  245 + background: white;
  246 + opacity: .8;
  247 +}
  248 +
  249 +/*
  250 +Theme authors:
  251 +Here are the basic themeable display options (colors, fonts, global widths).
  252 +See http://bgrins.github.io/spectrum/themes/ for instructions.
  253 +*/
  254 +
  255 +.sp-container {
  256 + border-radius: 0;
  257 + background-color: #ECECEC;
  258 + border: solid 1px #f0c49B;
  259 + padding: 0;
  260 +}
  261 +.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear {
  262 + font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
  263 + -webkit-box-sizing: border-box;
  264 + -moz-box-sizing: border-box;
  265 + -ms-box-sizing: border-box;
  266 + box-sizing: border-box;
  267 +}
  268 +.sp-top {
  269 + margin-bottom: 3px;
  270 +}
  271 +.sp-color, .sp-hue, .sp-clear {
  272 + border: solid 1px #666;
  273 +}
  274 +
  275 +/* Input */
  276 +.sp-input-container {
  277 + float:right;
  278 + width: 100px;
  279 + margin-bottom: 4px;
  280 +}
  281 +.sp-initial-disabled .sp-input-container {
  282 + width: 100%;
  283 +}
  284 +.sp-input {
  285 + font-size: 12px !important;
  286 + border: 1px inset;
  287 + padding: 4px 5px;
  288 + margin: 0;
  289 + width: 100%;
  290 + background:transparent;
  291 + border-radius: 3px;
  292 + color: #222;
  293 +}
  294 +.sp-input:focus {
  295 + border: 1px solid orange;
  296 +}
  297 +.sp-input.sp-validation-error {
  298 + border: 1px solid red;
  299 + background: #fdd;
  300 +}
  301 +.sp-picker-container , .sp-palette-container {
  302 + float:left;
  303 + position: relative;
  304 + padding: 10px;
  305 + padding-bottom: 300px;
  306 + margin-bottom: -290px;
  307 +}
  308 +.sp-picker-container {
  309 + width: 172px;
  310 + border-left: solid 1px #fff;
  311 +}
  312 +
  313 +/* Palettes */
  314 +.sp-palette-container {
  315 + border-right: solid 1px #ccc;
  316 +}
  317 +
  318 +.sp-palette-only .sp-palette-container {
  319 + border: 0;
  320 +}
  321 +
  322 +.sp-palette .sp-thumb-el {
  323 + display: block;
  324 + position:relative;
  325 + float:left;
  326 + width: 24px;
  327 + height: 15px;
  328 + margin: 3px;
  329 + cursor: pointer;
  330 + border:solid 2px transparent;
  331 +}
  332 +.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
  333 + border-color: orange;
  334 +}
  335 +.sp-thumb-el {
  336 + position:relative;
  337 +}
  338 +
  339 +/* Initial */
  340 +.sp-initial {
  341 + float: left;
  342 + border: solid 1px #333;
  343 +}
  344 +.sp-initial span {
  345 + width: 30px;
  346 + height: 25px;
  347 + border:none;
  348 + display:block;
  349 + float:left;
  350 + margin:0;
  351 +}
  352 +
  353 +.sp-initial .sp-clear-display {
  354 + background-position: center;
  355 +}
  356 +
  357 +/* Buttons */
  358 +.sp-palette-button-container,
  359 +.sp-button-container {
  360 + float: right;
  361 +}
  362 +
  363 +/* Replacer (the little preview div that shows up instead of the <input>) */
  364 +.sp-replacer {
  365 + margin:0;
  366 + overflow:hidden;
  367 + cursor:pointer;
  368 + padding: 4px;
  369 + display:inline-block;
  370 + *zoom: 1;
  371 + *display: inline;
  372 + border: solid 1px #91765d;
  373 + background: #eee;
  374 + color: #333;
  375 + vertical-align: middle;
  376 +}
  377 +.sp-replacer:hover, .sp-replacer.sp-active {
  378 + border-color: #F0C49B;
  379 + color: #111;
  380 +}
  381 +.sp-replacer.sp-disabled {
  382 + cursor:default;
  383 + border-color: silver;
  384 + color: silver;
  385 +}
  386 +.sp-dd {
  387 + padding: 2px 0;
  388 + height: 16px;
  389 + line-height: 16px;
  390 + float:left;
  391 + font-size:10px;
  392 +}
  393 +.sp-preview {
  394 + position:relative;
  395 + width:25px;
  396 + height: 20px;
  397 + border: solid 1px #222;
  398 + margin-right: 5px;
  399 + float:left;
  400 + z-index: 0;
  401 +}
  402 +
  403 +.sp-palette {
  404 + *width: 220px;
  405 + max-width: 220px;
  406 +}
  407 +.sp-palette .sp-thumb-el {
  408 + width:16px;
  409 + height: 16px;
  410 + margin:2px 1px;
  411 + border: solid 1px #d0d0d0;
  412 +}
  413 +
  414 +.sp-container {
  415 + padding-bottom:0;
  416 +}
  417 +
  418 +
  419 +/* Buttons: http://hellohappy.org/css3-buttons/ */
  420 +.sp-container button {
  421 + background-color: #eeeeee;
  422 + background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
  423 + background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
  424 + background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
  425 + background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
  426 + background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
  427 + border: 1px solid #ccc;
  428 + border-bottom: 1px solid #bbb;
  429 + border-radius: 3px;
  430 + color: #333;
  431 + font-size: 14px;
  432 + line-height: 1;
  433 + padding: 5px 4px;
  434 + text-align: center;
  435 + text-shadow: 0 1px 0 #eee;
  436 + vertical-align: middle;
  437 +}
  438 +.sp-container button:hover {
  439 + background-color: #dddddd;
  440 + background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
  441 + background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
  442 + background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
  443 + background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
  444 + background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
  445 + border: 1px solid #bbb;
  446 + border-bottom: 1px solid #999;
  447 + cursor: pointer;
  448 + text-shadow: 0 1px 0 #ddd;
  449 +}
  450 +.sp-container button:active {
  451 + border: 1px solid #aaa;
  452 + border-bottom: 1px solid #888;
  453 + -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  454 + -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  455 + -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  456 + -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  457 + box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
  458 +}
  459 +.sp-cancel {
  460 + font-size: 11px;
  461 + color: #d93f3f !important;
  462 + margin:0;
  463 + padding:2px;
  464 + margin-right: 5px;
  465 + vertical-align: middle;
  466 + text-decoration:none;
  467 +
  468 +}
  469 +.sp-cancel:hover {
  470 + color: #d93f3f !important;
  471 + text-decoration: underline;
  472 +}
  473 +
  474 +
  475 +.sp-palette span:hover, .sp-palette span.sp-thumb-active {
  476 + border-color: #000;
  477 +}
  478 +
  479 +.sp-preview, .sp-alpha, .sp-thumb-el {
  480 + position:relative;
  481 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
  482 +}
  483 +.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner {
  484 + display:block;
  485 + position:absolute;
  486 + top:0;left:0;bottom:0;right:0;
  487 +}
  488 +
  489 +.sp-palette .sp-thumb-inner {
  490 + background-position: 50% 50%;
  491 + background-repeat: no-repeat;
  492 +}
  493 +
  494 +.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner {
  495 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
  496 +}
  497 +
  498 +.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner {
  499 + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
  500 +}
  501 +
  502 +.sp-clear-display {
  503 + background-repeat:no-repeat;
  504 + background-position: center;
  505 + background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
  506 +}
... ...
script/noosfero-plugins
... ... @@ -77,8 +77,7 @@ run(){
77 77  
78 78 _enable(){
79 79 plugin="$1"
80   - cd $enabled_plugins_dir
81   - source="../../plugins/$plugin"
  80 + source="$available_plugins_dir/$plugin"
82 81 target="$enabled_plugins_dir/$plugin"
83 82 base="$base_plugins_dir/$plugin"
84 83 run "$source/before_enable.rb"
... ... @@ -101,15 +100,11 @@ _enable(){
101 100 fi
102 101 fi
103 102 if [ "$installation_ok" = true ] && [ "$dependencies_ok" = true ]; then
104   - ln -s "$source" "$plugin"
  103 + ln -s "$source" "$target"
105 104 plugins_public_dir="$NOOSFERO_DIR/public/plugins"
106 105 plugins_features_dir="$NOOSFERO_DIR/features/plugins"
107   - cd $plugins_public_dir
108   - test -d "$source/public" && ln -s "$source/public" "$plugin"
109   - if [ -d "$NOOSFERO_DIR/features" ]; then
110   - cd $plugins_features_dir
111   - test -d "$source/features" && ln -s "$source/features" "$plugin"
112   - fi
  106 + test -d "$target/public" && ln -s "$target/public" "$plugins_public_dir/$plugin"
  107 + test -d "$NOOSFERO_DIR/features" && test -d "$target/features" && ln -s "$target/features" "$plugins_features_dir/$plugin"
113 108 _say "$plugin enabled"
114 109 run "$source/after_enable.rb"
115 110 needs_migrate=true
... ...
script/quick-start
... ... @@ -85,6 +85,7 @@ fi
85 85 run rake db:schema:load
86 86 run rake db:data:minimal
87 87 run rake db:test:prepare
  88 +rails runner 'Environment.default.enable("skip_new_user_email_confirmation")'
88 89  
89 90 # FIXME compile translations depends on ruby-gettext-rails, please see debian/control
90 91 # run rake noosfero:translations:compile
... ...
test/functional/application_controller_test.rb
... ... @@ -166,7 +166,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
166 166  
167 167 should 'display only some categories in menu' do
168 168 @controller.stubs(:get_layout).returns('application')
169   - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true )
  169 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true )
170 170 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true )
171 171 get :index
172 172 assert_tag :tag => 'a', :content => /Category 2/
... ... @@ -174,7 +174,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
174 174  
175 175 should 'not display some categories in menu' do
176 176 @controller.stubs(:get_layout).returns('application')
177   - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true)
  177 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true)
178 178 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1)
179 179 get :index
180 180 assert_no_tag :tag => 'a', :content => /Category 2/
... ... @@ -239,7 +239,7 @@ class ApplicationControllerTest &lt; ActionController::TestCase
239 239  
240 240 should 'not display categories menu if categories feature disabled' do
241 241 Environment.any_instance.stubs(:enabled?).with(anything).returns(true)
242   - c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 1, :parent_id => nil, :display_in_menu => true )
  242 + c1 = Environment.default.categories.create!(:name => 'Category 1', :display_color => 'ffa500', :parent_id => nil, :display_in_menu => true )
243 243 c2 = Environment.default.categories.create!(:name => 'Category 2', :display_color => nil, :parent_id => c1.id, :display_in_menu => true )
244 244 get :index
245 245 assert_no_tag :tag => 'a', :content => /Category 2/
... ... @@ -579,4 +579,63 @@ class ApplicationControllerTest &lt; ActionController::TestCase
579 579  
580 580 assert_equal ['a', 'b', 'c'], controller.send(:find_suggestions, 'random', Environment.default, 'random')
581 581 end
  582 +
  583 + should 'redirect to login if environment is restrict to members' do
  584 + Environment.default.enable(:restrict_to_members)
  585 + get :index
  586 + assert_redirected_to :controller => 'account', :action => 'login'
  587 + end
  588 +
  589 + should 'do not allow member not included in whitelist to access an restricted environment' do
  590 + user = create_user
  591 + e = Environment.default
  592 + e.enable(:restrict_to_members)
  593 + e.members_whitelist_enabled = true
  594 + e.save!
  595 + login_as(user.login)
  596 + get :index
  597 + assert_response :forbidden
  598 + end
  599 +
  600 + should 'allow member in whitelist to access an environment' do
  601 + user = create_user
  602 + e = Environment.default
  603 + e.members_whitelist_enabled = true
  604 + e.members_whitelist = "#{user.person.id}"
  605 + e.save!
  606 + login_as(user.login)
  607 + get :index
  608 + assert_response :success
  609 + end
  610 +
  611 + should 'allow members to access an environment if whitelist is disabled' do
  612 + user = create_user
  613 + e = Environment.default
  614 + e.members_whitelist_enabled = false
  615 + e.save!
  616 + login_as(user.login)
  617 + get :index
  618 + assert_response :success
  619 + end
  620 +
  621 + should 'allow admin to access an environment if whitelist is enabled' do
  622 + e = Environment.default
  623 + e.members_whitelist_enabled = true
  624 + e.save!
  625 + login_as(create_admin_user(e))
  626 + get :index
  627 + assert_response :success
  628 + end
  629 +
  630 + should 'not check whitelist members if the environment is not restrict to members' do
  631 + e = Environment.default
  632 + e.disable(:restrict_to_members)
  633 + e.members_whitelist_enabled = true
  634 + e.save!
  635 + @controller.expects(:verify_members_whitelist).never
  636 + login_as create_user.login
  637 + get :index
  638 + assert_response :success
  639 + end
  640 +
582 641 end
... ...
test/functional/categories_controller_test.rb
... ... @@ -41,7 +41,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
41 41 end
42 42  
43 43 def test_edit_save
44   - post :edit, :id => cat1.id, :category => { :name => 'new name for category' }
  44 + post :edit, :id => cat1.id, :category => { :name => 'new name for category', :display_color => nil }
45 45 assert_redirected_to :action => 'index'
46 46 assert_equal 'new name for category', Category.find(cat1.id).name
47 47 end
... ... @@ -134,7 +134,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
134 134 env.save!
135 135 get :new
136 136  
137   - assert_no_tag :tag => 'select', :attributes => { :name => "category[display_color]" }
  137 + assert_no_tag :tag => 'input', :attributes => { :name => "category[display_color]" }
138 138 end
139 139  
140 140 should 'display color selection if environment.categories_menu is true' do
... ... @@ -142,7 +142,7 @@ class CategoriesControllerTest &lt; ActionController::TestCase
142 142 env.save!
143 143 get :new
144 144  
145   - assert_tag :tag => 'select', :attributes => { :name => "category[display_color]" }
  145 + assert_tag :tag => 'input', :attributes => { :name => "category[display_color]" }
146 146 end
147 147  
148 148 should 'not list regions and product categories' do
... ...
test/functional/cms_controller_test.rb
... ... @@ -1807,6 +1807,23 @@ class CmsControllerTest &lt; ActionController::TestCase
1807 1807 assert_template 'cms/publish'
1808 1808 end
1809 1809  
  1810 + should 'response of search_tags be json' do
  1811 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1812 + assert_equal 'application/json', @response.content_type
  1813 + end
  1814 +
  1815 + should 'return empty json if does not find tag' do
  1816 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1817 + assert_equal "[]", @response.body
  1818 + end
  1819 +
  1820 + should 'return tags found' do
  1821 + tag = mock; tag.stubs(:name).returns('linux')
  1822 + ActsAsTaggableOn::Tag.stubs(:find).returns([tag])
  1823 + get :search_tags, :profile => profile.identifier, :term => 'linux'
  1824 + assert_equal '[{"label":"linux","value":"linux"}]', @response.body
  1825 + end
  1826 +
1810 1827 protected
1811 1828  
1812 1829 # FIXME this is to avoid adding an extra dependency for a proper JSON parser.
... ...
test/functional/environment_design_controller_test.rb
... ... @@ -6,9 +6,7 @@ class EnvironmentDesignController; def rescue_action(e) raise e end; end
6 6  
7 7 class EnvironmentDesignControllerTest < ActionController::TestCase
8 8  
9   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
10   - # the Noosfero core soon, see ActionItem3045
11   - ALL_BLOCKS = [ArticleBlock, LoginBlock, EnvironmentStatisticsBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
  9 + ALL_BLOCKS = [ArticleBlock, LoginBlock, RecentDocumentsBlock, EnterprisesBlock, CommunitiesBlock, SellersSearchBlock, LinkListBlock, FeedReaderBlock, SlideshowBlock, HighlightsBlock, FeaturedProductsBlock, CategoriesBlock, RawHTMLBlock, TagsBlock ]
12 10  
13 11 def setup
14 12 @controller = EnvironmentDesignController.new
... ... @@ -77,18 +75,6 @@ class EnvironmentDesignControllerTest &lt; ActionController::TestCase
77 75 assert_tag :tag => 'p', :attributes => { :id => 'no_portal_community' }
78 76 end
79 77  
80   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
81   - # the Noosfero core soon, see ActionItem3045
82   - should 'be able to edit EnvironmentStatisticsBlock' do
83   - login_as(create_admin_user(Environment.default))
84   - b = EnvironmentStatisticsBlock.create!
85   - e = Environment.default
86   - e.boxes.create!
87   - e.boxes.first.blocks << b
88   - get :edit, :id => b.id
89   - assert_tag :tag => 'input', :attributes => { :id => 'block_title' }
90   - end
91   -
92 78 should 'be able to edit EnterprisesBlock' do
93 79 login_as(create_admin_user(Environment.default))
94 80 b = EnterprisesBlock.create!
... ...
test/functional/features_controller_test.rb
... ... @@ -146,4 +146,20 @@ class FeaturesControllerTest &lt; ActionController::TestCase
146 146 assert_equal true, e.custom_community_fields['contact_person']['required']
147 147 end
148 148  
  149 + should 'search members by name' do
  150 + uses_host 'anhetegua.net'
  151 + person = fast_create(Person, :environment_id => Environment.find(2).id)
  152 + xhr :get, :search_members, :q => person.name[0..2]
  153 + json_response = ActiveSupport::JSON.decode(@response.body)
  154 + assert_includes json_response, {"id"=>person.id, "name"=>person.name}
  155 + end
  156 +
  157 + should 'search members by identifier' do
  158 + uses_host 'anhetegua.net'
  159 + person = fast_create(Person, :name => 'Some Name', :identifier => 'person-identifier', :environment_id => Environment.find(2).id)
  160 + xhr :get, :search_members, :q => person.identifier
  161 + json_response = ActiveSupport::JSON.decode(@response.body)
  162 + assert_includes json_response, {"id"=>person.id, "name"=>person.name}
  163 + end
  164 +
149 165 end
... ...
test/unit/box_test.rb
... ... @@ -33,9 +33,6 @@ class BoxTest &lt; ActiveSupport::TestCase
33 33 assert blocks.include?('categories-block')
34 34 assert blocks.include?('communities-block')
35 35 assert blocks.include?('enterprises-block')
36   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
37   - # the Noosfero core soon, see ActionItem3045
38   - assert blocks.include?('environment-statistics-block')
39 36 assert blocks.include?('fans-block')
40 37 assert blocks.include?('favorite-enterprises-block')
41 38 assert blocks.include?('feed-reader-block')
... ... @@ -64,9 +61,6 @@ class BoxTest &lt; ActiveSupport::TestCase
64 61 assert blocks.include?('communities-block')
65 62 assert blocks.include?('disabled-enterprise-message-block')
66 63 assert blocks.include?('enterprises-block')
67   - # TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
68   - # the Noosfero core soon, see ActionItem3045
69   - assert blocks.include?('environment-statistics-block')
70 64 assert blocks.include?('fans-block')
71 65 assert blocks.include?('favorite-enterprises-block')
72 66 assert blocks.include?('featured-products-block')
... ...
test/unit/categories_helper_test.rb
... ... @@ -15,8 +15,26 @@ class CategoriesHelperTest &lt; ActiveSupport::TestCase
15 15 expects(:options_for_select).with([['General Category', 'Category'],[ 'Product Category', 'ProductCategory'],[ 'Region', 'Region' ]], 'fieldvalue').returns('OPTIONS')
16 16 expects(:select_tag).with('type', 'OPTIONS').returns('TAG')
17 17 expects(:labelled_form_field).with(anything, 'TAG').returns('RESULT')
18   -
  18 +
19 19 assert_equal 'RESULT', select_category_type('fieldname')
20 20 end
21 21  
  22 + should 'return category color if its defined' do
  23 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb')
  24 + assert_equal 'background-color: #fbfbfb;', category_color_style(category1)
  25 + end
  26 +
  27 + should 'not return category parent color if category color is not defined' do
  28 + e = fast_create(Environment)
  29 + category1 = fast_create(Category, :name => 'education', :display_color => 'fbfbfb', :environment_id => e.id)
  30 + category2 = fast_create(Category, :name => 'education', :display_color => nil, :parent_id => category1.id, :environment_id => e.id)
  31 + assert_equal '', category_color_style(category2)
  32 + end
  33 +
  34 + should 'not return category parent color if category is nil' do
  35 + assert_nothing_raised do
  36 + assert_equal '', category_color_style(nil)
  37 + end
  38 + end
  39 +
22 40 end
... ...
test/unit/category_test.rb
... ... @@ -159,36 +159,6 @@ class CategoryTest &lt; ActiveSupport::TestCase
159 159  
160 160 end
161 161  
162   - should "limit the possibile display colors" do
163   - c = build(Category, :name => 'test category', :environment_id => @env.id)
164   -
165   - c.display_color = 16
166   - c.valid?
167   - assert c.errors[:display_color.to_s].present?
168   -
169   - valid = (1..15).map { |item| item.to_i }
170   - valid.each do |item|
171   - c.display_color = item
172   - c.valid?
173   - assert !c.errors[:display_color.to_s].present?
174   - end
175   -
176   - end
177   -
178   - should 'avoid duplicated display colors' do
179   - c1 = fast_create(Category, :name => 'test category', :environment_id => @env.id, :display_color => 1)
180   -
181   - c = build(Category, :name => 'lalala', :environment_id => @env.id)
182   - c.display_color = 1
183   - assert !c.valid?
184   - assert c.errors[:display_color.to_s].present?
185   -
186   - c.display_color = 2
187   - c.valid?
188   - assert !c.errors[:display_color.to_s].present?
189   -
190   - end
191   -
192 162 should 'be able to get top ancestor' do
193 163 c1 = fast_create(Category, :name => 'test category', :environment_id => @env.id)
194 164 c2 = fast_create(Category, :name => 'test category', :environment_id => @env.id, :parent_id => c1.id)
... ... @@ -535,4 +505,28 @@ class CategoryTest &lt; ActiveSupport::TestCase
535 505 assert_includes Category.on_level(parent.id), category
536 506 end
537 507  
  508 + should 'return self if the category has display_color defined' do
  509 + c1 = fast_create(Category)
  510 + c2 = fast_create(Category, :parent_id => c1)
  511 + c3 = fast_create(Category, :parent_id => c2, :display_color => 'FFFFFF')
  512 + c4 = fast_create(Category, :parent_id => c3, :display_color => '000000')
  513 + assert_equal c4, c4.with_color
  514 + end
  515 +
  516 + should 'return first category on hierarchy with display_color defined' do
  517 + c1 = fast_create(Category, :display_color => '111111')
  518 + c2 = fast_create(Category, :parent_id => c1)
  519 + c3 = fast_create(Category, :parent_id => c2)
  520 + c4 = fast_create(Category, :parent_id => c3)
  521 + assert_equal c1, c4.with_color
  522 + end
  523 +
  524 + should 'return nil if no category on hierarchy has display_color defined' do
  525 + c1 = fast_create(Category)
  526 + c2 = fast_create(Category, :parent_id => c1)
  527 + c3 = fast_create(Category, :parent_id => c2)
  528 + c4 = fast_create(Category, :parent_id => c3)
  529 + assert_equal nil, c4.with_color
  530 + end
  531 +
538 532 end
... ...
test/unit/environment_statistics_block_test.rb
... ... @@ -1,99 +0,0 @@
1   -# TODO EnvironmentStatisticsBlock is DEPRECATED and will be removed from
2   -# the Noosfero core soon, see ActionItem3045
3   -
4   -require File.dirname(__FILE__) + '/../test_helper'
5   -
6   -class EnvironmentStatisticsBlockTest < ActiveSupport::TestCase
7   -
8   - should 'inherit from Block' do
9   - assert_kind_of Block, EnvironmentStatisticsBlock.new
10   - end
11   -
12   - should 'describe itself' do
13   - assert_not_equal Block.description, EnvironmentStatisticsBlock.description
14   - end
15   -
16   - should 'provide a default title' do
17   - owner = mock
18   - owner.expects(:name).returns('my environment')
19   -
20   - block = EnvironmentStatisticsBlock.new
21   - block.expects(:owner).returns(owner)
22   - assert_equal 'Statistics for my environment', block.title
23   - end
24   -
25   - should 'generate statistics' do
26   - env = create(Environment)
27   - user1 = create_user('testuser1', :environment_id => env.id)
28   - user2 = create_user('testuser2', :environment_id => env.id)
29   -
30   - fast_create(Enterprise, :environment_id => env.id)
31   - fast_create(Community, :environment_id => env.id)
32   -
33   - block = EnvironmentStatisticsBlock.new
34   - env.boxes.first.blocks << block
35   -
36   - content = block.content
37   -
38   - assert_match(/One enterprise/, content)
39   - assert_match(/2 users/, content)
40   - assert_match(/One community/, content)
41   - end
42   -
43   - should 'generate statistics including private profiles' do
44   - env = create(Environment)
45   - user1 = create_user('testuser1', :environment_id => env.id)
46   - user2 = create_user('testuser2', :environment_id => env.id)
47   - user3 = create_user('testuser3', :environment_id => env.id)
48   - p = user3.person; p.public_profile = false; p.save!
49   -
50   - fast_create(Enterprise, :environment_id => env.id)
51   - fast_create(Enterprise, :environment_id => env.id, :public_profile => false)
52   -
53   - fast_create(Community, :environment_id => env.id)
54   - fast_create(Community, :environment_id => env.id, :public_profile => false)
55   -
56   - block = EnvironmentStatisticsBlock.new
57   - env.boxes.first.blocks << block
58   -
59   - content = block.content
60   -
61   - assert_match /2 enterprises/, content
62   - assert_match /3 users/, content
63   - assert_match /2 communities/, content
64   - end
65   -
66   - should 'generate statistics but not for not visible profiles' do
67   - env = create(Environment)
68   - user1 = create_user('testuser1', :environment_id => env.id)
69   - user2 = create_user('testuser2', :environment_id => env.id)
70   - user3 = create_user('testuser3', :environment_id => env.id)
71   - p = user3.person; p.visible = false; p.save!
72   -
73   - fast_create(Enterprise, :environment_id => env.id)
74   - fast_create(Enterprise, :environment_id => env.id, :visible => false)
75   -
76   - fast_create(Community, :environment_id => env.id)
77   - fast_create(Community, :environment_id => env.id, :visible => false)
78   -
79   - block = EnvironmentStatisticsBlock.new
80   - env.boxes.first.blocks << block
81   -
82   - content = block.content
83   -
84   - assert_match /One enterprise/, content
85   - assert_match /2 users/, content
86   - assert_match /One community/, content
87   - end
88   -
89   - should 'not display enterprises if disabled' do
90   - env = fast_create(Environment)
91   - env.enable('disable_asset_enterprises', false)
92   -
93   - block = EnvironmentStatisticsBlock.new
94   - block.stubs(:owner).returns(env)
95   -
96   - assert_no_match /enterprises/i, block.content
97   - end
98   -
99   -end
test/unit/environment_test.rb
... ... @@ -156,7 +156,7 @@ class EnvironmentTest &lt; ActiveSupport::TestCase
156 156  
157 157 should 'list displayable categories' do
158 158 env = fast_create(Environment)
159   - cat1 = create(Category, :environment => env, :name => 'category one', :display_color => 1)
  159 + cat1 = create(Category, :environment => env, :name => 'category one', :display_color => 'ffa500')
160 160 assert ! cat1.new_record?
161 161  
162 162 # subcategories should be ignored
... ...
test/unit/moderate_user_registration_test.rb 0 → 100644
... ... @@ -0,0 +1,22 @@
  1 +# encoding: UTF-8
  2 +require File.dirname(__FILE__) + '/../test_helper'
  3 +
  4 +class ModerateUserRegistrationTest < ActiveSupport::TestCase
  5 + fixtures :users, :environments
  6 +
  7 + def test_should_on_perform_activate_user
  8 + user = User.new(:login => 'lalala', :email => 'lalala@example.com', :password => 'test', :password_confirmation => 'test')
  9 + user.save!
  10 + environment = Environment.default
  11 + t= ModerateUserRegistration.new
  12 + t.user_id = user.id
  13 + t.name = user.name
  14 + t.author_name = user.name
  15 + t.email = user.email
  16 + t.target= environment
  17 + t.save!
  18 + assert !user.activated?
  19 + t.perform
  20 + assert environment.users.find_by_id(user.id).activated?
  21 + end
  22 +end
... ...
vendor/plugins/delayed_job/lib/delayed/command.rb
... ... @@ -97,7 +97,7 @@ module Delayed
97 97 Dir.chdir(Rails.root)
98 98  
99 99 Delayed::Worker.after_fork
100   - Delayed::Worker.logger ||= Logger.new(File.join(Rails.root, 'log', 'delayed_job.log'))
  100 + Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', "#{Rails.env}_delayed_job.log"))
101 101  
102 102 worker = Delayed::Worker.new(@options)
103 103 worker.name_prefix = "#{worker_name} "
... ...