Commit 69b83fad20c263cdf8b4ea41290cfcbde0a869fa
1 parent
3fdbbc29
Exists in
fix_gallery_image_url
and in
3 other branches
Add plugin to create FB page tabs and share to FB timeline
Showing
55 changed files
with
3429 additions
and
0 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,47 @@ |
| 1 | +page_tab: | |
| 2 | + use_test_app: false | |
| 3 | +timeline: | |
| 4 | + use_test_app: true | |
| 5 | + | |
| 6 | +test_users: | |
| 7 | + - identifier1 | |
| 8 | + - identifier1 | |
| 9 | + | |
| 10 | +app: | |
| 11 | + id: xxx | |
| 12 | + secret: xxx | |
| 13 | + domain: domainconfigured.net | |
| 14 | + | |
| 15 | + open_graph: | |
| 16 | + namespace: app_name | |
| 17 | + objects: | |
| 18 | + blog_post: article | |
| 19 | + community: community | |
| 20 | + enterprise: sse_initiative | |
| 21 | + favorite_enterprise: sse_initiative | |
| 22 | + forum: discussion | |
| 23 | + event: event | |
| 24 | + friend: friend | |
| 25 | + gallery_image: picture | |
| 26 | + person: user | |
| 27 | + product: sse_product | |
| 28 | + uploaded_file: document | |
| 29 | + actions: | |
| 30 | + add: add | |
| 31 | + comment: comment | |
| 32 | + create: create | |
| 33 | + favorite: favorite | |
| 34 | + like: like | |
| 35 | + make_friendship: make_friendship | |
| 36 | + upload: upload | |
| 37 | + update: update | |
| 38 | + start: start | |
| 39 | + announce_creation: announce_creation | |
| 40 | + announce_new: announce_new | |
| 41 | + announce_update: announce_update | |
| 42 | + announce_news: announce_news | |
| 43 | + | |
| 44 | +test_app: | |
| 45 | + test_id: xxx | |
| 46 | + test_secret: xxx | |
| 47 | + | ... | ... |
plugins/fb_app/controllers/myprofile/fb_app_plugin_myprofile_controller.rb
0 → 100644
| ... | ... | @@ -0,0 +1,59 @@ |
| 1 | +class FbAppPluginMyprofileController < OpenGraphPlugin::MyprofileController | |
| 2 | + | |
| 3 | + no_design_blocks | |
| 4 | + | |
| 5 | + before_filter :load_provider | |
| 6 | + before_filter :load_auth | |
| 7 | + | |
| 8 | + def index | |
| 9 | + if params[:tabs_added] | |
| 10 | + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added], params[:page_tab] | |
| 11 | + @page_tab = @page_tabs.first | |
| 12 | + redirect_to @page_tab.facebook_url | |
| 13 | + end | |
| 14 | + end | |
| 15 | + | |
| 16 | + def show_login | |
| 17 | + @status = params[:auth].delete :status | |
| 18 | + @logged_auth = FbAppPlugin::Auth.new params[:auth] | |
| 19 | + @logged_auth.fetch_user | |
| 20 | + if @auth.connected? | |
| 21 | + render partial: 'identity', locals: {auth: @logged_auth} | |
| 22 | + else | |
| 23 | + render nothing: true | |
| 24 | + end | |
| 25 | + end | |
| 26 | + | |
| 27 | + def save_auth | |
| 28 | + @status = params[:auth].delete :status rescue FbAppPlugin::Auth::Status::Unknown | |
| 29 | + if @status == FbAppPlugin::Auth::Status::Connected | |
| 30 | + @auth.attributes = params[:auth] | |
| 31 | + @auth.save! if @auth.changed? | |
| 32 | + else | |
| 33 | + @auth.destroy if @auth and @auth.persisted? | |
| 34 | + @auth = new_auth | |
| 35 | + end | |
| 36 | + | |
| 37 | + render partial: 'settings' | |
| 38 | + end | |
| 39 | + | |
| 40 | + protected | |
| 41 | + | |
| 42 | + def load_provider | |
| 43 | + @provider = FbAppPlugin.oauth_provider_for environment | |
| 44 | + end | |
| 45 | + | |
| 46 | + def load_auth | |
| 47 | + @auth = FbAppPlugin::Auth.where(profile_id: profile.id, provider_id: @provider.id).first | |
| 48 | + @auth ||= new_auth | |
| 49 | + end | |
| 50 | + | |
| 51 | + def new_auth | |
| 52 | + FbAppPlugin::Auth.new profile: profile, provider: @provider | |
| 53 | + end | |
| 54 | + | |
| 55 | + def context | |
| 56 | + :fb_app | |
| 57 | + end | |
| 58 | + | |
| 59 | +end | ... | ... |
plugins/fb_app/controllers/public/fb_app_plugin_controller.rb
0 → 100644
| ... | ... | @@ -0,0 +1,24 @@ |
| 1 | +class FbAppPluginController < PublicController | |
| 2 | + | |
| 3 | + no_design_blocks | |
| 4 | + | |
| 5 | + def index | |
| 6 | + end | |
| 7 | + | |
| 8 | + def myprofile_config | |
| 9 | + if logged_in? | |
| 10 | + redirect_to controller: :fb_app_plugin_myprofile, profile: user.identifier | |
| 11 | + else | |
| 12 | + redirect_to controller: :account, action: :login, return_to: url_for(controller: :fb_app_plugin, action: :myprofile_config) | |
| 13 | + end | |
| 14 | + end | |
| 15 | + | |
| 16 | + protected | |
| 17 | + | |
| 18 | + # prevent session reset because X-CSRF not being passed by FB | |
| 19 | + # see also https://gist.github.com/toretore/911886 | |
| 20 | + def handle_unverified_request | |
| 21 | + end | |
| 22 | + | |
| 23 | +end | |
| 24 | + | ... | ... |
plugins/fb_app/controllers/public/fb_app_plugin_page_tab_controller.rb
0 → 100644
| ... | ... | @@ -0,0 +1,173 @@ |
| 1 | +class FbAppPluginPageTabController < FbAppPluginController | |
| 2 | + | |
| 3 | + no_design_blocks | |
| 4 | + | |
| 5 | + before_filter :change_theme | |
| 6 | + before_filter :disable_cache | |
| 7 | + | |
| 8 | + include CatalogHelper | |
| 9 | + | |
| 10 | + helper ManageProductsHelper | |
| 11 | + helper FbAppPlugin::DisplayHelper | |
| 12 | + | |
| 13 | + def index | |
| 14 | + return unless load_page_tabs | |
| 15 | + | |
| 16 | + if params[:tabs_added] | |
| 17 | + @page_tabs = FbAppPlugin::PageTab.create_from_tabs_added params[:tabs_added] | |
| 18 | + @page_tab = @page_tabs.first | |
| 19 | + redirect_to @page_tab.facebook_url | |
| 20 | + elsif @signed_request or @page_id | |
| 21 | + if @page_tab.present? | |
| 22 | + if product_id = params[:product_id] | |
| 23 | + @product = environment.products.find product_id | |
| 24 | + @profile = @product.profile | |
| 25 | + @inputs = @product.inputs | |
| 26 | + @allowed_user = false | |
| 27 | + load_catalog | |
| 28 | + | |
| 29 | + render action: 'product' | |
| 30 | + elsif @page_tab.config_type.in? [:profile, :own_profile] | |
| 31 | + @profile = @page_tab.value | |
| 32 | + | |
| 33 | + load_catalog | |
| 34 | + render action: 'catalog' unless performed? | |
| 35 | + else | |
| 36 | + # fake profile for catalog controller | |
| 37 | + @profile = environment.enterprise_default_template | |
| 38 | + @profile.shopping_cart_settings.enabled = true | |
| 39 | + | |
| 40 | + base_query = @page_tab.value | |
| 41 | + params[:base_query] = base_query | |
| 42 | + params[:scope] = 'all' | |
| 43 | + | |
| 44 | + load_catalog | |
| 45 | + render action: 'catalog' unless performed? | |
| 46 | + end | |
| 47 | + else | |
| 48 | + render action: 'first_load' | |
| 49 | + end | |
| 50 | + else | |
| 51 | + # render template | |
| 52 | + render action: 'index' | |
| 53 | + end | |
| 54 | + end | |
| 55 | + | |
| 56 | + def search_autocomplete | |
| 57 | + load_page_tabs | |
| 58 | + load_search_autocomplete | |
| 59 | + respond_to do |format| | |
| 60 | + format.json{ render 'catalog/search_autocomplete' } | |
| 61 | + end | |
| 62 | + end | |
| 63 | + | |
| 64 | + def admin | |
| 65 | + return redirect_to '/plugin/fb_app/myprofile_config' if params[:page_id].blank? and params[:signed_request].blank? | |
| 66 | + return unless load_page_tabs | |
| 67 | + | |
| 68 | + if request.put? and @page_id.present? | |
| 69 | + create_page_tabs if @page_tab.nil? | |
| 70 | + | |
| 71 | + @page_tab.update_attributes! params[:page_tab] | |
| 72 | + | |
| 73 | + respond_to do |format| | |
| 74 | + format.js{ render action: 'admin' } | |
| 75 | + end | |
| 76 | + end | |
| 77 | + end | |
| 78 | + | |
| 79 | + def destroy | |
| 80 | + @page_tab = FbAppPlugin::PageTab.find params[:id] | |
| 81 | + return render_access_denied unless user.present? and (user.is_admin?(environment) or user.is_admin? @page_tab.profile) | |
| 82 | + @page_tab.destroy | |
| 83 | + render nothing: true | |
| 84 | + end | |
| 85 | + | |
| 86 | + def uninstall | |
| 87 | + render text: params.to_yaml | |
| 88 | + end | |
| 89 | + | |
| 90 | + def enterprise_search | |
| 91 | + scope = environment.enterprises.enabled.public | |
| 92 | + @query = params[:query] | |
| 93 | + @profiles = scope.limit(10).order('name ASC'). | |
| 94 | + where(['name ILIKE ? OR name ILIKE ? OR identifier LIKE ?', "#{@query}%", "% #{@query}%", "#{@query}%"]) | |
| 95 | + render partial: 'open_graph_plugin/myprofile/profile_search', locals: {profiles: @profiles} | |
| 96 | + end | |
| 97 | + | |
| 98 | + # unfortunetely, this needs to be public | |
| 99 | + def profile | |
| 100 | + @profile | |
| 101 | + end | |
| 102 | + | |
| 103 | + protected | |
| 104 | + | |
| 105 | + def default_url_options | |
| 106 | + {profile: @profile.identifier} if @profile | |
| 107 | + end | |
| 108 | + | |
| 109 | + def load_page_tabs | |
| 110 | + @signed_requests = read_param params[:signed_request] | |
| 111 | + if @signed_requests.present? | |
| 112 | + @datas = [] | |
| 113 | + @page_ids = @signed_requests.map do |signed_request| | |
| 114 | + @data = FbAppPlugin::Auth.parse_signed_request signed_request | |
| 115 | + @datas << @data | |
| 116 | + page_id = @data[:page][:id] rescue nil | |
| 117 | + if page_id.blank? | |
| 118 | + render_not_found | |
| 119 | + return false | |
| 120 | + end | |
| 121 | + page_id | |
| 122 | + end | |
| 123 | + else | |
| 124 | + @page_ids = read_param params[:page_id] | |
| 125 | + end | |
| 126 | + | |
| 127 | + @page_tabs = FbAppPlugin::PageTab.where page_id: @page_ids | |
| 128 | + | |
| 129 | + @signed_request = @signed_requests.first | |
| 130 | + @page_id = @page_ids.first | |
| 131 | + @page_tab = @page_tabs.first | |
| 132 | + @new_request = @page_tab.blank? | |
| 133 | + | |
| 134 | + true | |
| 135 | + end | |
| 136 | + | |
| 137 | + def create_page_tabs | |
| 138 | + @page_tabs = FbAppPlugin::PageTab.create_from_page_ids @page_ids | |
| 139 | + @page_tab ||= @page_tabs.first | |
| 140 | + end | |
| 141 | + | |
| 142 | + def change_theme | |
| 143 | + # move to config | |
| 144 | + unless theme_responsive? | |
| 145 | + @current_theme = 'ees' | |
| 146 | + @theme_responsive = true | |
| 147 | + end | |
| 148 | + @without_pure_chat = true | |
| 149 | + end | |
| 150 | + def get_layout | |
| 151 | + return nil if request.format == :js or request.xhr? | |
| 152 | + | |
| 153 | + return 'application-responsive' | |
| 154 | + end | |
| 155 | + | |
| 156 | + def disable_cache | |
| 157 | + @disable_cache_theme_navigation = true | |
| 158 | + end | |
| 159 | + | |
| 160 | + def load_catalog options = {} | |
| 161 | + @use_show_more = true | |
| 162 | + catalog_load_index options | |
| 163 | + end | |
| 164 | + | |
| 165 | + def read_param param | |
| 166 | + if param.is_a? Hash | |
| 167 | + param.values | |
| 168 | + else | |
| 169 | + Array(param).select{ |p| p.present? } | |
| 170 | + end | |
| 171 | + end | |
| 172 | + | |
| 173 | +end | ... | ... |
plugins/fb_app/db/migrate/20140319135819_create_fb_app_page_tab_config.rb
0 → 100644
| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | +class CreateFbAppPageTabConfig < ActiveRecord::Migration | |
| 2 | + | |
| 3 | + def change | |
| 4 | + create_table :fb_app_plugin_page_tab_configs do |t| | |
| 5 | + t.string :page_id | |
| 6 | + t.text :config, default: {}.to_yaml | |
| 7 | + t.integer :profile_id | |
| 8 | + | |
| 9 | + t.timestamps | |
| 10 | + end | |
| 11 | + add_index :fb_app_plugin_page_tab_configs, [:profile_id] | |
| 12 | + add_index :fb_app_plugin_page_tab_configs, [:page_id] | |
| 13 | + add_index :fb_app_plugin_page_tab_configs, [:page_id, :profile_id] | |
| 14 | + end | |
| 15 | + | |
| 16 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | +require_dependency 'profile' | |
| 2 | +# hate to wrte this, but without Noosfero::Plugin::Settings is loaded instead | |
| 3 | +require 'fb_app_plugin/settings' | |
| 4 | + | |
| 5 | +# attr_accessible must be defined on subclasses | |
| 6 | +Profile.descendants.each do |subclass| | |
| 7 | + subclass.class_eval do | |
| 8 | + attr_accessible :fb_app_settings | |
| 9 | + end | |
| 10 | +end | |
| 11 | + | |
| 12 | +class Profile | |
| 13 | + | |
| 14 | + def fb_app_settings attrs = {} | |
| 15 | + @fb_app_settings ||= FbAppPlugin::Settings.new self, attrs | |
| 16 | + attrs.each{ |a, v| @fb_app_settings.send "#{a}=", v } | |
| 17 | + @fb_app_settings | |
| 18 | + end | |
| 19 | + alias_method :fb_app_settings=, :fb_app_settings | |
| 20 | + | |
| 21 | + has_many :fb_app_page_tabs, class_name: 'FbAppPlugin::PageTab' | |
| 22 | + | |
| 23 | + def fb_app_auth | |
| 24 | + provider = FbAppPlugin.oauth_provider_for self.environment | |
| 25 | + self.oauth_auths.where(provider_id: provider.id).first | |
| 26 | + end | |
| 27 | + | |
| 28 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,87 @@ |
| 1 | +module FbAppPlugin | |
| 2 | + | |
| 3 | + extend Noosfero::Plugin::ParentMethods | |
| 4 | + | |
| 5 | + def self.plugin_name | |
| 6 | + I18n.t 'fb_app_plugin.lib.plugin.name' | |
| 7 | + end | |
| 8 | + | |
| 9 | + def self.plugin_description | |
| 10 | + I18n.t 'fb_app_plugin.lib.plugin.description' | |
| 11 | + end | |
| 12 | + | |
| 13 | + def self.config | |
| 14 | + @config ||= HashWithIndifferentAccess.new(YAML.load File.read("#{File.dirname __FILE__}/../config.yml")) rescue {} | |
| 15 | + end | |
| 16 | + | |
| 17 | + def self.test_users | |
| 18 | + @test_users ||= self.config[:test_users] | |
| 19 | + end | |
| 20 | + def self.test_user? user | |
| 21 | + user and (self.test_users.blank? or self.test_users.include? user.identifier) | |
| 22 | + end | |
| 23 | + | |
| 24 | + def self.debug? actor=nil | |
| 25 | + self.test_user? actor | |
| 26 | + end | |
| 27 | + | |
| 28 | + def self.scope user | |
| 29 | + if self.test_user? user then 'publish_actions' else '' end | |
| 30 | + end | |
| 31 | + | |
| 32 | + def self.oauth_provider_for environment | |
| 33 | + return unless self.config.present? | |
| 34 | + | |
| 35 | + @oauth_providers ||= {} | |
| 36 | + @oauth_providers[environment] ||= begin | |
| 37 | + app_id = self.timeline_app_credentials[:id].to_s | |
| 38 | + app_secret = self.timeline_app_credentials[:secret].to_s | |
| 39 | + | |
| 40 | + client = environment.oauth_providers.where(client_id: app_id).first | |
| 41 | + # attributes that may be changed by the user | |
| 42 | + client ||= OauthClientPlugin::Provider.new strategy: 'facebook', | |
| 43 | + name: 'FB App', site: 'https://facebook.com' | |
| 44 | + | |
| 45 | + # attributes that should not change | |
| 46 | + client.attributes = { | |
| 47 | + client_id: app_id, client_secret: app_secret, | |
| 48 | + environment_id: environment.id, | |
| 49 | + } | |
| 50 | + client.save! if client.changed? | |
| 51 | + | |
| 52 | + client | |
| 53 | + end | |
| 54 | + end | |
| 55 | + | |
| 56 | + def self.open_graph_config | |
| 57 | + return unless self.config.present? | |
| 58 | + | |
| 59 | + @open_graph_config ||= begin | |
| 60 | + key = if self.config[:timeline][:use_test_app] then :test_app else :app end | |
| 61 | + self.config[key][:open_graph] | |
| 62 | + end | |
| 63 | + end | |
| 64 | + | |
| 65 | + def self.credentials app = :app | |
| 66 | + return unless self.config.present? | |
| 67 | + {id: self.config[app][:id], secret: self.config[app][:secret]} | |
| 68 | + end | |
| 69 | + | |
| 70 | + def self.timeline_app_credentials | |
| 71 | + return unless self.config.present? | |
| 72 | + @timeline_app_credentials ||= begin | |
| 73 | + key = if self.config[:timeline][:use_test_app] then :test_app else :app end | |
| 74 | + self.credentials key | |
| 75 | + end | |
| 76 | + end | |
| 77 | + | |
| 78 | + def self.page_tab_app_credentials | |
| 79 | + return unless self.config.present? | |
| 80 | + @page_tab_app_credentials ||= begin | |
| 81 | + key = if self.config[:page_tab][:use_test_app] then :test_app else :app end | |
| 82 | + self.credentials key | |
| 83 | + end | |
| 84 | + end | |
| 85 | + | |
| 86 | +end | |
| 87 | + | ... | ... |
| ... | ... | @@ -0,0 +1,35 @@ |
| 1 | +class FbAppPlugin::Base < Noosfero::Plugin | |
| 2 | + | |
| 3 | + def stylesheet? | |
| 4 | + true | |
| 5 | + end | |
| 6 | + | |
| 7 | + def js_files | |
| 8 | + ['fb_app.js'].map{ |j| "javascripts/#{j}" } | |
| 9 | + end | |
| 10 | + | |
| 11 | + def head_ending | |
| 12 | + return unless FbAppPlugin.config.present? | |
| 13 | + lambda do | |
| 14 | + tag 'meta', property: 'fb:app_id', content: FbAppPlugin.config[:app][:id] | |
| 15 | + end | |
| 16 | + end | |
| 17 | + | |
| 18 | + def control_panel_buttons | |
| 19 | + return unless FbAppPlugin.config.present? | |
| 20 | + { title: FbAppPlugin.plugin_name, icon: 'fb-app', url: {host: FbAppPlugin.config[:app][:domain], profile: profile.identifier, controller: :fb_app_plugin_myprofile} } | |
| 21 | + end | |
| 22 | + | |
| 23 | +end | |
| 24 | + | |
| 25 | +ActiveSupport.on_load :open_graph_plugin do | |
| 26 | + OpenGraphPlugin::Stories.register_publisher FbAppPlugin::Publisher.default | |
| 27 | +end | |
| 28 | +ActiveSupport.on_load :metadata_plugin do | |
| 29 | + MetadataPlugin::Controllers.class_eval do | |
| 30 | + def fb_app_plugin_page_tab | |
| 31 | + :@product | |
| 32 | + end | |
| 33 | + end | |
| 34 | +end | |
| 35 | + | ... | ... |
| ... | ... | @@ -0,0 +1,51 @@ |
| 1 | +module FbAppPlugin::DisplayHelper | |
| 2 | + | |
| 3 | + extend CatalogHelper | |
| 4 | + | |
| 5 | + def fb_url_options options | |
| 6 | + options.merge! page_id: @page_ids, signed_request: @signed_requests, id: nil | |
| 7 | + end | |
| 8 | + | |
| 9 | + def url_for options = {} | |
| 10 | + return super unless options.is_a? Hash | |
| 11 | + if options[:controller] == :catalog | |
| 12 | + options[:controller] = :fb_app_plugin_page_tab | |
| 13 | + options = fb_url_options options | |
| 14 | + end | |
| 15 | + super | |
| 16 | + end | |
| 17 | + | |
| 18 | + protected | |
| 19 | + | |
| 20 | + def product_url_options product, options = {} | |
| 21 | + options = options.merge! product.url | |
| 22 | + options = options.merge! controller: :fb_app_plugin_page_tab, product_id: product.id, action: :index | |
| 23 | + options = fb_url_options options | |
| 24 | + unless Rails.env.development? | |
| 25 | + domain = FbAppPlugin.config[:app][:domain] | |
| 26 | + options[:host] = domain if domain.present? | |
| 27 | + options[:protocol] = '//' | |
| 28 | + end | |
| 29 | + options | |
| 30 | + end | |
| 31 | + def product_path product, options = {} | |
| 32 | + url = url_for product_url_options(product, options = {}) | |
| 33 | + url | |
| 34 | + end | |
| 35 | + | |
| 36 | + def link_to_product product, opts = {} | |
| 37 | + url_opts = opts.delete(:url_options) || {} | |
| 38 | + url_opts = product_url_options product, url_opts | |
| 39 | + url = params.merge url_opts | |
| 40 | + link_to content_tag('span', product.name), url, | |
| 41 | + opts.merge(target: '') | |
| 42 | + end | |
| 43 | + | |
| 44 | + def link_to name = nil, options = nil, html_options = nil, &block | |
| 45 | + html_options ||= {} | |
| 46 | + options[:protocol] = '//' if options.is_a? Hash | |
| 47 | + html_options[:target] ||= '_parent' | |
| 48 | + super | |
| 49 | + end | |
| 50 | + | |
| 51 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +# add target attribute to links | |
| 2 | +class FbAppPlugin::LinkRenderer < WillPaginate::ActionView::LinkRenderer | |
| 3 | + | |
| 4 | + def prepare collection, options, template | |
| 5 | + super | |
| 6 | + end | |
| 7 | + | |
| 8 | + protected | |
| 9 | + | |
| 10 | + def default_url_params | |
| 11 | + {target: ''} | |
| 12 | + end | |
| 13 | + | |
| 14 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,17 @@ |
| 1 | +# Publishing examples on console | |
| 2 | +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; a=Article.find 307591 | |
| 3 | +# pub.publish_story a, u, :announce_news_from_a_sse_initiative | |
| 4 | +# | |
| 5 | +# pub=FbAppPlugin::Publisher.default; u=Profile['brauliobo']; f=FavoriteEnterprisePerson.last | |
| 6 | +# pub.publish_story f, u, :favorite_a_sse_initiative | |
| 7 | +# | |
| 8 | +class FbAppPlugin::Publisher < OpenGraphPlugin::Publisher | |
| 9 | + | |
| 10 | + def publish_story object_data, actor, story | |
| 11 | + OpenGraphPlugin.context = FbAppPlugin::Activity.context | |
| 12 | + a = FbAppPlugin::Activity.new object_data: object_data, actor: actor, story: story | |
| 13 | + a.dispatch_publications | |
| 14 | + a.save | |
| 15 | + end | |
| 16 | + | |
| 17 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,83 @@ |
| 1 | + | |
| 2 | +"en-US": &en-US | |
| 3 | + | |
| 4 | + fb_app_plugin: | |
| 5 | + lib: | |
| 6 | + plugin: | |
| 7 | + name: 'Facebook integration' | |
| 8 | + description: 'Use the app for Facebook!' | |
| 9 | + models: | |
| 10 | + page_tab: | |
| 11 | + types: | |
| 12 | + own_profile: 'Catalog from this enterprise' | |
| 13 | + profile: 'Catalog from a single SSE enterprise' | |
| 14 | + other_profile: 'Catalog from other SSE enterprise' | |
| 15 | + profiles: 'Catalog from more than one SSE enterprise' | |
| 16 | + query: 'Catalog of products chosen by filter or free search' | |
| 17 | + views: | |
| 18 | + myprofile: | |
| 19 | + checking_auth: 'Checking authorization' | |
| 20 | + different_login: 'But you are logged in on facebook as:' | |
| 21 | + current_login: 'You are logged in on facebook as:' | |
| 22 | + connect: 'I want to install the app for Facebook' | |
| 23 | + logged_connect: 'Connect this Facebook account with my user %{profile}' | |
| 24 | + disconnect: 'Disconnect' | |
| 25 | + connect_to_another: 'Conect to another Facebook account' | |
| 26 | + reconnect: 'Reconnect' | |
| 27 | + | |
| 28 | + timeline: | |
| 29 | + heading: 'Publishing in your facebook' | |
| 30 | + add: 'Save timeline post settings' | |
| 31 | + explanation_title: 'Soon!' | |
| 32 | + explanation_text: 'In a short time, your actions will become new posts in your Facebook timeline!' | |
| 33 | + organization_redirect: '%{redirect_link} to post updates %{type}, conect your personal profile to Facebook.' | |
| 34 | + organization_from_enterprise: 'from this enterprise' | |
| 35 | + organization_from_community: 'from this community' | |
| 36 | + redirect_link: 'Click here' | |
| 37 | + | |
| 38 | + catalogs: | |
| 39 | + heading: 'Solidarity Economy Catalog' | |
| 40 | + new: 'Add new catalog' | |
| 41 | + catalog_title_label: 'Title' | |
| 42 | + catalog_subtitle_label: 'Subtitle' | |
| 43 | + catalog_type_chooser_label: 'Type' | |
| 44 | + profile_chooser_label: 'Enterprise' | |
| 45 | + profiles_chooser_label: 'Enterprises' | |
| 46 | + query_label: "Criteria for the catalog's products" | |
| 47 | + query_help: "Write the words separated by space, and click at the button below to save." | |
| 48 | + edit_button: "Edit catalog '%{catalog_title}'" | |
| 49 | + remove_button: "Remove catalog '%{catalog_title}'" | |
| 50 | + cancel_button: 'Cancel' | |
| 51 | + confirm_removal: "<p>Warning: To really remove a catalog, you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and remove the catalog from our settings.</p><p>Are you sure you want to remove this catalog?</p>" | |
| 52 | + confirm_removal_button: 'Yes, I want to delete this catalog' | |
| 53 | + confirm_disconnect: "<p>Warning: If you disconnect your account to this Facebook profile, all your catalogs will also be removed</p><p>And then, to really remove your catalogs (if they were created), you must go to the facebook page where it is, click at '<u>add or remove tabs</u>' and then remove the tab.</p><p>After that, you can come here and disconnect.</p><p>Are you sure you want to disconnect your account to this Facebook profile?</p>" | |
| 54 | + confirm_disconnect_button: 'Yes, I want to disconnect my account from this Facebook profile' | |
| 55 | + | |
| 56 | + catalog: | |
| 57 | + see_page: 'See page on Facebook' | |
| 58 | + | |
| 59 | + error: | |
| 60 | + empty_title: 'Please add a title to your catalog.' | |
| 61 | + empty_settings: 'Please choose SSE enterprises or search terms or filters for your catalog.' | |
| 62 | + | |
| 63 | + page_tab: | |
| 64 | + edit_catalog: 'Edit catalog' | |
| 65 | + add: 'Add catalog to one of my pages on Facebook' | |
| 66 | + save: 'Save' | |
| 67 | + added_notice: "Congratulations: You've just published a new SSE catalog in Facebook!" | |
| 68 | + profile: | |
| 69 | + placeholder: "select an enterprise" | |
| 70 | + profiles: | |
| 71 | + placeholder: "select the enterprises" | |
| 72 | + query: | |
| 73 | + placeholder: "select the category or type the search terms" | |
| 74 | + back_to_catalog: 'Back to catalog' | |
| 75 | + footer1: "" | |
| 76 | + footer2: "" | |
| 77 | + | |
| 78 | + | |
| 79 | +'en_US': | |
| 80 | + <<: *en-US | |
| 81 | +'en': | |
| 82 | + <<: *en-US | |
| 83 | + | ... | ... |
| ... | ... | @@ -0,0 +1,82 @@ |
| 1 | + | |
| 2 | +"pt-BR": &pt-BR | |
| 3 | + | |
| 4 | + fb_app_plugin: | |
| 5 | + lib: | |
| 6 | + plugin: | |
| 7 | + name: 'App Facebook' | |
| 8 | + description: 'Divulgue suas ações no Facebook!' | |
| 9 | + models: | |
| 10 | + page_tab: | |
| 11 | + types: | |
| 12 | + own_profile: 'Vitrine deste empreendimento' | |
| 13 | + profile: 'Vitrine de um único empreendimento' | |
| 14 | + other_profile: 'Vitrine de outro empreendimento' | |
| 15 | + profiles: 'Vitrine de mais de um empreendimento' | |
| 16 | + query: 'Vitrine de produtos ou serviços escolhidos por busca livre' | |
| 17 | + views: | |
| 18 | + myprofile: | |
| 19 | + checking_auth: 'Verificando autorização de acesso' | |
| 20 | + different_login: 'Mas você está logado no facebook como:' | |
| 21 | + current_login: 'Você está logado no facebook como:' | |
| 22 | + connect: 'Quero instalar o App Facebook' | |
| 23 | + logged_connect: 'Quero conectar esta conta do Facebook com meu usuário %{profile}' | |
| 24 | + disconnect: 'Desconectar' | |
| 25 | + connect_to_another: 'Conectar a outra conta do Facebook' | |
| 26 | + reconnect: 'Reconectar' | |
| 27 | + | |
| 28 | + timeline: | |
| 29 | + heading: 'Postar ações no facebook automaticamente:' | |
| 30 | + add: 'Salvar configuração de postagens na timeline' | |
| 31 | + explanation_title: 'Aguarde!' | |
| 32 | + explanation_text: 'Em breve, este aplicativo poderá postar automaticamente no seu face as ações que você fizer! Por exemplo: quando você postar novos conteúdos no seu blog, enviar uma imagem, etc.' | |
| 33 | + organization_redirect: '%{redirect_link} para postar atualizações %{type}, conecte o seu perfil pessoal ao facebook.' | |
| 34 | + organization_from_enterprise: 'deste empreendimento' | |
| 35 | + organization_from_community: 'desta comunidade' | |
| 36 | + redirect_link: 'Clique aqui' | |
| 37 | + | |
| 38 | + catalogs: | |
| 39 | + heading: 'Vitrine' | |
| 40 | + new: 'Criar nova vitrine' | |
| 41 | + catalog_title_label: 'Título' | |
| 42 | + catalog_subtitle_label: 'Subtítulo' | |
| 43 | + catalog_type_chooser_label: 'Tipo' | |
| 44 | + profile_chooser_label: 'Empreendimento' | |
| 45 | + profiles_chooser_label: 'Empreendimentos' | |
| 46 | + query_label: "Critérios para os produtos/serviços da vitrine" | |
| 47 | + query_help: "Escreva as palavras separadas por espaço, e clique no botão abaixo para salvar." | |
| 48 | + edit_button: "Editar vitrine '%{catalog_title}'" | |
| 49 | + remove_button: "Remover vitrine '%{catalog_title}'" | |
| 50 | + cancel_button: 'Cancelar' | |
| 51 | + confirm_removal: "<p><b>Atenção:</b> Para realmente apagar uma vitrine, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>. Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e pedir para remover a vitrine.</p><p>Se você já removeu lá no Facebook, tem certeza que quer remover esta vitrine?</p>" | |
| 52 | + confirm_removal_button: 'Sim, quero remover esta vitrine' | |
| 53 | + confirm_disconnect: "<p><b>Atenção:</b> Se você desconectar, vai apagar todas as vitrines. Portanto, para realmente apagar suas vitrines, você deve primeiro ir para a página do Facebook onde está a sua vitrine e seguir este roteiro:</p><ul><li>Clique em '<i>Mais</i>' na barra da página, depois em '<i>Gerenciar guias</i>', e então em '<i>Adicionar ou remover guias</i>'.</li><li>Clique no X para remover.</li></ul><p>Depois disso, você pode vir aqui e desconectar.</p><p>Tem certeza que quer desconectar?</p>" | |
| 54 | + confirm_disconnect_button: 'Sim, quero desconectar' | |
| 55 | + | |
| 56 | + catalog: | |
| 57 | + see_page: 'Ver página no facebook' | |
| 58 | + | |
| 59 | + error: | |
| 60 | + empty_title: 'Por favor, coloque um título para a sua vitrine.' | |
| 61 | + empty_settings: 'Por favor, selecione os empreendimentos solidários ou os termos de busca para sua vitrine.' | |
| 62 | + | |
| 63 | + page_tab: | |
| 64 | + edit_catalog: 'Editar vitrine' | |
| 65 | + add: 'Criar vitrine em uma página sua no Facebook' | |
| 66 | + save: 'Salvar' | |
| 67 | + added_notice: 'Parabéns: você acaba de publicar uma nova vitrine da Economia Solidária no Facebook!' | |
| 68 | + profile: | |
| 69 | + placeholder: "selecione um empreendimento" | |
| 70 | + profiles: | |
| 71 | + placeholder: "selecione os empreendimentos" | |
| 72 | + query: | |
| 73 | + placeholder: "escolha os produtos e serviços por palavras de busca" | |
| 74 | + back_to_catalog: 'Voltar à vitrine' | |
| 75 | + footer1: "" | |
| 76 | + footer2: "" | |
| 77 | + | |
| 78 | +'pt_BR': | |
| 79 | + <<: *pt-BR | |
| 80 | +'pt': | |
| 81 | + <<: *pt-BR | |
| 82 | + | ... | ... |
| ... | ... | @@ -0,0 +1,54 @@ |
| 1 | +class FbAppPlugin::Activity < OpenGraphPlugin::Activity | |
| 2 | + | |
| 3 | + self.context = :fb_app | |
| 4 | + self.actions = FbAppPlugin.open_graph_config[:actions] | |
| 5 | + self.objects = FbAppPlugin.open_graph_config[:objects] | |
| 6 | + | |
| 7 | + # this avoid to many saves for frequent fail cases | |
| 8 | + attr_accessor :should_save | |
| 9 | + validates_presence_of :should_save | |
| 10 | + | |
| 11 | + def self.scrape object_data_url | |
| 12 | + params = {id: object_data_url, scrape: true, method: 'post'} | |
| 13 | + url = "http://graph.facebook.com?#{params.to_query}" | |
| 14 | + Net::HTTP.get URI.parse(url) | |
| 15 | + end | |
| 16 | + def scrape | |
| 17 | + self.class.scrape self.object_data_url | |
| 18 | + end | |
| 19 | + | |
| 20 | + def publish! actor = self.actor | |
| 21 | + print_debug "fb_app: action #{self.action}, object_type #{self.object_type}" if debug? actor | |
| 22 | + | |
| 23 | + auth = actor.fb_app_auth | |
| 24 | + return if auth.blank? or auth.expired? | |
| 25 | + print_debug "fb_app: Auth found and is valid" if debug? actor | |
| 26 | + | |
| 27 | + # always update the object to expire facebook cache | |
| 28 | + Thread.new{ self.scrape } | |
| 29 | + | |
| 30 | + return if self.defs[:on] == :update and self.recent_publish? actor, self.object_type, self.object_data_url | |
| 31 | + print_debug "fb_app: no recent publication found, making new" if debug? actor | |
| 32 | + | |
| 33 | + self.should_save = true | |
| 34 | + | |
| 35 | + namespace = FbAppPlugin.open_graph_config[:namespace] | |
| 36 | + # to_str is needed to ensure String, see https://github.com/nov/fb_graph2/issues/88 | |
| 37 | + params = {self.object_type => self.object_data_url.to_str} | |
| 38 | + params['fb:explicitly_shared'] = 'true' unless self.defs[:tracker] | |
| 39 | + print_debug "fb_app: publishing with params #{params.inspect}" if debug? actor | |
| 40 | + | |
| 41 | + me = FbGraph2::User.me auth.access_token | |
| 42 | + me.og_action! "#{namespace}:#{action}", params | |
| 43 | + | |
| 44 | + self.published_at = Time.now | |
| 45 | + print_debug "fb_app: published with success" if debug? actor | |
| 46 | + end | |
| 47 | + | |
| 48 | + protected | |
| 49 | + | |
| 50 | + def debug? actor=nil | |
| 51 | + super or FbAppPlugin.debug? actor | |
| 52 | + end | |
| 53 | + | |
| 54 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,89 @@ |
| 1 | +class FbAppPlugin::Auth < OauthClientPlugin::Auth | |
| 2 | + | |
| 3 | + module Status | |
| 4 | + Connected = 'connected' | |
| 5 | + NotAuthorized = 'not_authorized' | |
| 6 | + Unknown = 'unknown' | |
| 7 | + end | |
| 8 | + | |
| 9 | + settings_items :signed_request | |
| 10 | + settings_items :fb_user | |
| 11 | + | |
| 12 | + attr_accessible :provider_user_id, :signed_request | |
| 13 | + | |
| 14 | + before_create :update_user | |
| 15 | + before_create :exchange_token | |
| 16 | + after_create :schedule_exchange_token | |
| 17 | + after_destroy :destroy_page_tabs | |
| 18 | + before_validation :set_enabled | |
| 19 | + | |
| 20 | + validates_presence_of :provider_user_id | |
| 21 | + validates_uniqueness_of :provider_user_id, scope: :profile_id | |
| 22 | + | |
| 23 | + def self.parse_signed_request signed_request, credentials = FbAppPlugin.page_tab_app_credentials | |
| 24 | + secret = credentials[:secret] rescue '' | |
| 25 | + request = Facebook::SignedRequest.new signed_request, secret: secret | |
| 26 | + request.data | |
| 27 | + end | |
| 28 | + | |
| 29 | + def status | |
| 30 | + if self.access_token.present? and self.not_expired? then Status::Connected else Status::NotAuthorized end | |
| 31 | + end | |
| 32 | + def not_authorized? | |
| 33 | + self.status == Status::NotAuthorized | |
| 34 | + end | |
| 35 | + def connected? | |
| 36 | + self.status == Status::Connected | |
| 37 | + end | |
| 38 | + | |
| 39 | + def exchange_token | |
| 40 | + app_id = FbAppPlugin.timeline_app_credentials[:id] | |
| 41 | + app_secret = FbAppPlugin.timeline_app_credentials[:secret] | |
| 42 | + fb_auth = FbGraph2::Auth.new app_id, app_secret | |
| 43 | + fb_auth.fb_exchange_token = self.access_token | |
| 44 | + | |
| 45 | + access_token = fb_auth.access_token! | |
| 46 | + self.access_token = access_token.access_token | |
| 47 | + self.expires_in = access_token.expires_in | |
| 48 | + # refresh user and its stored access token | |
| 49 | + self.fetch_user | |
| 50 | + end | |
| 51 | + | |
| 52 | + def exchange_token! | |
| 53 | + self.exchange_token | |
| 54 | + self.save! | |
| 55 | + end | |
| 56 | + | |
| 57 | + def signed_request_data | |
| 58 | + self.class.parse_signed_request self.signed_request | |
| 59 | + end | |
| 60 | + | |
| 61 | + def fetch_user | |
| 62 | + fb_user = FbGraph2::User.me self.access_token | |
| 63 | + self.fb_user = fb_user.fetch | |
| 64 | + end | |
| 65 | + def update_user | |
| 66 | + self.fb_user = self.fetch_user | |
| 67 | + end | |
| 68 | + | |
| 69 | + protected | |
| 70 | + | |
| 71 | + def destroy_page_tabs | |
| 72 | + self.profile.fb_app_page_tabs.destroy_all | |
| 73 | + end | |
| 74 | + | |
| 75 | + def exchange_token_and_reschedule! | |
| 76 | + self.exchange_token! | |
| 77 | + self.schedule_exchange_token | |
| 78 | + end | |
| 79 | + | |
| 80 | + def schedule_exchange_token | |
| 81 | + self.delay(run_at: self.expires_at - 2.weeks).exchange_token_and_reschedule! | |
| 82 | + end | |
| 83 | + | |
| 84 | + def set_enabled | |
| 85 | + self.enabled = self.not_expired? | |
| 86 | + end | |
| 87 | + | |
| 88 | +end | |
| 89 | + | ... | ... |
| ... | ... | @@ -0,0 +1,111 @@ |
| 1 | +class FbAppPlugin::PageTab < ActiveRecord::Base | |
| 2 | + | |
| 3 | + # FIXME: rename table to match model | |
| 4 | + self.table_name = :fb_app_plugin_page_tab_configs | |
| 5 | + | |
| 6 | + attr_accessible :owner_profile, :profile_id, :page_id, | |
| 7 | + :config_type, :profile_ids, :query, | |
| 8 | + :title, :subtitle | |
| 9 | + | |
| 10 | + belongs_to :owner_profile, foreign_key: :profile_id, class_name: 'Profile' | |
| 11 | + | |
| 12 | + acts_as_having_settings field: :config | |
| 13 | + | |
| 14 | + ConfigTypes = [:profile, :profiles, :query] | |
| 15 | + EnterpriseConfigTypes = [:own_profile] + ConfigTypes | |
| 16 | + | |
| 17 | + validates_presence_of :page_id | |
| 18 | + validates_uniqueness_of :page_id | |
| 19 | + validates_inclusion_of :config_type, in: ConfigTypes + EnterpriseConfigTypes | |
| 20 | + | |
| 21 | + def self.page_ids_from_tabs_added tabs_added | |
| 22 | + tabs_added.map{ |id, value| id } | |
| 23 | + end | |
| 24 | + | |
| 25 | + def self.create_from_page_ids page_ids, attrs = {} | |
| 26 | + attrs.delete :page_id | |
| 27 | + page_ids.map do |page_id| | |
| 28 | + page_tab = FbAppPlugin::PageTab.where(page_id: page_id).first | |
| 29 | + page_tab ||= FbAppPlugin::PageTab.new page_id: page_id | |
| 30 | + page_tab.update_attributes! attrs | |
| 31 | + page_tab | |
| 32 | + end | |
| 33 | + end | |
| 34 | + def self.create_from_tabs_added tabs_added, attrs = {} | |
| 35 | + page_ids = self.page_ids_from_tabs_added tabs_added | |
| 36 | + self.create_from_page_ids page_ids, attrs | |
| 37 | + end | |
| 38 | + | |
| 39 | + def self.facebook_url page_id | |
| 40 | + "https://facebook.com/#{page_id}?sk=app_#{FbAppPlugin.page_tab_app_credentials[:id]}" | |
| 41 | + end | |
| 42 | + | |
| 43 | + def facebook_url | |
| 44 | + self.class.facebook_url self.page_id | |
| 45 | + end | |
| 46 | + | |
| 47 | + def types | |
| 48 | + if self.owner_profile.present? and self.owner_profile.enterprise? then EnterpriseConfigTypes else ConfigTypes end | |
| 49 | + end | |
| 50 | + | |
| 51 | + def config_type | |
| 52 | + self.config[:type] || (self.owner_profile ? :own_profile : :profile) | |
| 53 | + end | |
| 54 | + def config_type= value | |
| 55 | + self.config[:type] = value.to_sym | |
| 56 | + end | |
| 57 | + | |
| 58 | + def value | |
| 59 | + case self.config_type | |
| 60 | + when :profiles | |
| 61 | + self.profiles.map(&:identifier).join(' OR ') | |
| 62 | + else | |
| 63 | + self.send self.config_type | |
| 64 | + end | |
| 65 | + end | |
| 66 | + def blank? | |
| 67 | + self.value.blank? rescue true | |
| 68 | + end | |
| 69 | + | |
| 70 | + def own_profile | |
| 71 | + self.owner_profile | |
| 72 | + end | |
| 73 | + def profiles | |
| 74 | + Profile.where(id: self.config[:profile_ids]) | |
| 75 | + end | |
| 76 | + def profile | |
| 77 | + self.profiles.first | |
| 78 | + end | |
| 79 | + def profile_ids | |
| 80 | + self.profiles.map(&:id) | |
| 81 | + end | |
| 82 | + def query | |
| 83 | + self.config[:query] | |
| 84 | + end | |
| 85 | + | |
| 86 | + def title | |
| 87 | + self.config[:title] | |
| 88 | + end | |
| 89 | + def title= value | |
| 90 | + self.config[:title] = value | |
| 91 | + end | |
| 92 | + | |
| 93 | + def subtitle | |
| 94 | + self.config[:subtitle] | |
| 95 | + end | |
| 96 | + def subtitle= value | |
| 97 | + self.config[:subtitle] = value | |
| 98 | + end | |
| 99 | + | |
| 100 | + def profile_ids= ids | |
| 101 | + ids = ids.to_s.split(',') | |
| 102 | + self.config[:type] = if ids.size == 1 then :profile else :profiles end | |
| 103 | + self.config[:profile_ids] = ids | |
| 104 | + end | |
| 105 | + | |
| 106 | + def query= value | |
| 107 | + self.config[:type] = :query | |
| 108 | + self.config[:query] = value | |
| 109 | + end | |
| 110 | + | |
| 111 | +end | ... | ... |
plugins/fb_app/plugins/fb_app/lib/ext/action_tracker_model.rb
0 → 100644
722 Bytes
3.31 KB
18.3 KB
plugins/fb_app/public/javascripts/bootstrap-tokenfield.js
0 → 100644
| ... | ... | @@ -0,0 +1,1032 @@ |
| 1 | +/*! | |
| 2 | + * bootstrap-tokenfield | |
| 3 | + * https://github.com/sliptree/bootstrap-tokenfield | |
| 4 | + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT | |
| 5 | + */ | |
| 6 | + | |
| 7 | +(function (factory) { | |
| 8 | + if (typeof define === 'function' && define.amd) { | |
| 9 | + // AMD. Register as an anonymous module. | |
| 10 | + define(['jquery'], factory); | |
| 11 | + } else if (typeof exports === 'object') { | |
| 12 | + // For CommonJS and CommonJS-like environments where a window with jQuery | |
| 13 | + // is present, execute the factory with the jQuery instance from the window object | |
| 14 | + // For environments that do not inherently posses a window with a document | |
| 15 | + // (such as Node.js), expose a Tokenfield-making factory as module.exports | |
| 16 | + // This accentuates the need for the creation of a real window or passing in a jQuery instance | |
| 17 | + // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($); | |
| 18 | + module.exports = global.window && global.window.$ ? | |
| 19 | + factory( global.window.$ ) : | |
| 20 | + function( input ) { | |
| 21 | + if ( !input.$ && !input.fn ) { | |
| 22 | + throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" ); | |
| 23 | + } | |
| 24 | + return factory( input.$ || input ); | |
| 25 | + }; | |
| 26 | + } else { | |
| 27 | + // Browser globals | |
| 28 | + factory(jQuery, window); | |
| 29 | + } | |
| 30 | +}(function ($, window) { | |
| 31 | + | |
| 32 | + "use strict"; // jshint ;_; | |
| 33 | + | |
| 34 | + /* TOKENFIELD PUBLIC CLASS DEFINITION | |
| 35 | + * ============================== */ | |
| 36 | + | |
| 37 | + var Tokenfield = function (element, options) { | |
| 38 | + var _self = this | |
| 39 | + | |
| 40 | + this.$element = $(element) | |
| 41 | + this.textDirection = this.$element.css('direction'); | |
| 42 | + | |
| 43 | + // Extend options | |
| 44 | + this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options) | |
| 45 | + | |
| 46 | + // Setup delimiters and trigger keys | |
| 47 | + this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter | |
| 48 | + this._triggerKeys = $.map(this._delimiters, function (delimiter) { | |
| 49 | + return delimiter.charCodeAt(0); | |
| 50 | + }); | |
| 51 | + this._firstDelimiter = this._delimiters[0]; | |
| 52 | + | |
| 53 | + // Check for whitespace, dash and special characters | |
| 54 | + var whitespace = $.inArray(' ', this._delimiters) | |
| 55 | + , dash = $.inArray('-', this._delimiters) | |
| 56 | + | |
| 57 | + if (whitespace >= 0) | |
| 58 | + this._delimiters[whitespace] = '\\s' | |
| 59 | + | |
| 60 | + if (dash >= 0) { | |
| 61 | + delete this._delimiters[dash] | |
| 62 | + this._delimiters.unshift('-') | |
| 63 | + } | |
| 64 | + | |
| 65 | + var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] | |
| 66 | + $.each(this._delimiters, function (index, character) { | |
| 67 | + var pos = $.inArray(character, specialCharacters) | |
| 68 | + if (pos >= 0) _self._delimiters[index] = '\\' + character; | |
| 69 | + }); | |
| 70 | + | |
| 71 | + // Store original input width | |
| 72 | + var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null | |
| 73 | + , elStyleWidth = element.style.width | |
| 74 | + , elCSSWidth | |
| 75 | + , elWidth = this.$element.width() | |
| 76 | + | |
| 77 | + if (elRules) { | |
| 78 | + $.each( elRules, function (i, rule) { | |
| 79 | + if (rule.style.width) { | |
| 80 | + elCSSWidth = rule.style.width; | |
| 81 | + } | |
| 82 | + }); | |
| 83 | + } | |
| 84 | + | |
| 85 | + // Move original input out of the way | |
| 86 | + var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', | |
| 87 | + originalStyles = { position: this.$element.css('position') }; | |
| 88 | + originalStyles[hidingPosition] = this.$element.css(hidingPosition); | |
| 89 | + | |
| 90 | + this.$element | |
| 91 | + .data('original-styles', originalStyles) | |
| 92 | + .data('original-tabindex', this.$element.prop('tabindex')) | |
| 93 | + .css('position', 'absolute') | |
| 94 | + .css(hidingPosition, '-10000px') | |
| 95 | + .prop('tabindex', -1) | |
| 96 | + | |
| 97 | + // Create a wrapper | |
| 98 | + this.$wrapper = $('<div class="tokenfield form-control" />') | |
| 99 | + if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') | |
| 100 | + if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') | |
| 101 | + if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') | |
| 102 | + | |
| 103 | + // Create a new input | |
| 104 | + var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) | |
| 105 | + this.$input = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />') | |
| 106 | + .appendTo( this.$wrapper ) | |
| 107 | + .prop( 'placeholder', this.$element.prop('placeholder') ) | |
| 108 | + .prop( 'id', id + '-tokenfield' ) | |
| 109 | + .prop( 'tabindex', this.$element.data('original-tabindex') ) | |
| 110 | + | |
| 111 | + // Re-route original input label to new input | |
| 112 | + var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) | |
| 113 | + if ( $label.length ) { | |
| 114 | + $label.prop( 'for', this.$input.prop('id') ) | |
| 115 | + } | |
| 116 | + | |
| 117 | + // Set up a copy helper to handle copy & paste | |
| 118 | + this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) | |
| 119 | + | |
| 120 | + // Set wrapper width | |
| 121 | + if (elStyleWidth) { | |
| 122 | + this.$wrapper.css('width', elStyleWidth); | |
| 123 | + } | |
| 124 | + else if (elCSSWidth) { | |
| 125 | + this.$wrapper.css('width', elCSSWidth); | |
| 126 | + } | |
| 127 | + // If input is inside inline-form with no width set, set fixed width | |
| 128 | + else if (this.$element.parents('.form-inline').length) { | |
| 129 | + this.$wrapper.width( elWidth ) | |
| 130 | + } | |
| 131 | + | |
| 132 | + // Set tokenfield disabled, if original or fieldset input is disabled | |
| 133 | + if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { | |
| 134 | + this.disable(); | |
| 135 | + } | |
| 136 | + | |
| 137 | + // Set tokenfield readonly, if original input is readonly | |
| 138 | + if (this.$element.prop('readonly')) { | |
| 139 | + this.readonly(); | |
| 140 | + } | |
| 141 | + | |
| 142 | + // Set up mirror for input auto-sizing | |
| 143 | + this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>'); | |
| 144 | + this.$input.css('min-width', this.options.minWidth + 'px') | |
| 145 | + $.each([ | |
| 146 | + 'fontFamily', | |
| 147 | + 'fontSize', | |
| 148 | + 'fontWeight', | |
| 149 | + 'fontStyle', | |
| 150 | + 'letterSpacing', | |
| 151 | + 'textTransform', | |
| 152 | + 'wordSpacing', | |
| 153 | + 'textIndent' | |
| 154 | + ], function (i, val) { | |
| 155 | + _self.$mirror[0].style[val] = _self.$input.css(val); | |
| 156 | + }); | |
| 157 | + this.$mirror.appendTo( 'body' ) | |
| 158 | + | |
| 159 | + // Insert tokenfield to HTML | |
| 160 | + this.$wrapper.insertBefore( this.$element ) | |
| 161 | + this.$element.prependTo( this.$wrapper ) | |
| 162 | + | |
| 163 | + // Calculate inner input width | |
| 164 | + this.update() | |
| 165 | + | |
| 166 | + // Create initial tokens, if any | |
| 167 | + this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) | |
| 168 | + | |
| 169 | + // Start listening to events | |
| 170 | + this.listen() | |
| 171 | + | |
| 172 | + // Initialize autocomplete, if necessary | |
| 173 | + if ( ! $.isEmptyObject( this.options.autocomplete ) ) { | |
| 174 | + var side = this.textDirection === 'rtl' ? 'right' : 'left' | |
| 175 | + , autocompleteOptions = $.extend({ | |
| 176 | + minLength: this.options.showAutocompleteOnFocus ? 0 : null, | |
| 177 | + position: { my: side + " top", at: side + " bottom", of: this.$wrapper } | |
| 178 | + }, this.options.autocomplete ) | |
| 179 | + | |
| 180 | + this.$input.autocomplete( autocompleteOptions ) | |
| 181 | + } | |
| 182 | + | |
| 183 | + // Initialize typeahead, if necessary | |
| 184 | + if ( ! $.isEmptyObject( this.options.typeahead ) ) { | |
| 185 | + | |
| 186 | + var typeaheadOptions = this.options.typeahead | |
| 187 | + , defaults = { | |
| 188 | + minLength: this.options.showAutocompleteOnFocus ? 0 : null | |
| 189 | + } | |
| 190 | + , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] | |
| 191 | + | |
| 192 | + args[0] = $.extend( {}, defaults, args[0] ) | |
| 193 | + | |
| 194 | + this.$input.typeahead.apply( this.$input, args ) | |
| 195 | + this.typeahead = true | |
| 196 | + } | |
| 197 | + } | |
| 198 | + | |
| 199 | + Tokenfield.prototype = { | |
| 200 | + | |
| 201 | + constructor: Tokenfield | |
| 202 | + | |
| 203 | + , createToken: function (attrs, triggerChange) { | |
| 204 | + var _self = this | |
| 205 | + | |
| 206 | + if (typeof attrs === 'string') { | |
| 207 | + attrs = { value: attrs, label: attrs } | |
| 208 | + } else { | |
| 209 | + // Copy objects to prevent contamination of data sources. | |
| 210 | + attrs = $.extend( {}, attrs ) | |
| 211 | + } | |
| 212 | + | |
| 213 | + if (typeof triggerChange === 'undefined') { | |
| 214 | + triggerChange = true | |
| 215 | + } | |
| 216 | + | |
| 217 | + // Normalize label and value | |
| 218 | + attrs.value = $.trim(attrs.value.toString()); | |
| 219 | + attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value | |
| 220 | + | |
| 221 | + // Bail out if has no value or label, or label is too short | |
| 222 | + if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return | |
| 223 | + | |
| 224 | + // Bail out if maximum number of tokens is reached | |
| 225 | + if (this.options.limit && this.getTokens().length >= this.options.limit) return | |
| 226 | + | |
| 227 | + // Allow changing token data before creating it | |
| 228 | + var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) | |
| 229 | + this.$element.trigger(createEvent) | |
| 230 | + | |
| 231 | + // Bail out if there if attributes are empty or event was defaultPrevented | |
| 232 | + if (!createEvent.attrs || createEvent.isDefaultPrevented()) return | |
| 233 | + | |
| 234 | + var $token = $('<div class="token" />') | |
| 235 | + .append('<span class="token-label" />') | |
| 236 | + .append('<a href="#" class="close" tabindex="-1">×</a>') | |
| 237 | + .data('attrs', attrs) | |
| 238 | + | |
| 239 | + // Insert token into HTML | |
| 240 | + if (this.$input.hasClass('tt-input')) { | |
| 241 | + // If the input has typeahead enabled, insert token before it's parent | |
| 242 | + this.$input.parent().before( $token ) | |
| 243 | + } else { | |
| 244 | + this.$input.before( $token ) | |
| 245 | + } | |
| 246 | + | |
| 247 | + // Temporarily set input width to minimum | |
| 248 | + this.$input.css('width', this.options.minWidth + 'px') | |
| 249 | + | |
| 250 | + var $tokenLabel = $token.find('.token-label') | |
| 251 | + , $closeButton = $token.find('.close') | |
| 252 | + | |
| 253 | + // Determine maximum possible token label width | |
| 254 | + if (!this.maxTokenWidth) { | |
| 255 | + this.maxTokenWidth = | |
| 256 | + this.$wrapper.width() - $closeButton.outerWidth() - | |
| 257 | + parseInt($closeButton.css('margin-left'), 10) - | |
| 258 | + parseInt($closeButton.css('margin-right'), 10) - | |
| 259 | + parseInt($token.css('border-left-width'), 10) - | |
| 260 | + parseInt($token.css('border-right-width'), 10) - | |
| 261 | + parseInt($token.css('padding-left'), 10) - | |
| 262 | + parseInt($token.css('padding-right'), 10) | |
| 263 | + parseInt($tokenLabel.css('border-left-width'), 10) - | |
| 264 | + parseInt($tokenLabel.css('border-right-width'), 10) - | |
| 265 | + parseInt($tokenLabel.css('padding-left'), 10) - | |
| 266 | + parseInt($tokenLabel.css('padding-right'), 10) | |
| 267 | + parseInt($tokenLabel.css('margin-left'), 10) - | |
| 268 | + parseInt($tokenLabel.css('margin-right'), 10) | |
| 269 | + } | |
| 270 | + | |
| 271 | + //$tokenLabel.css('max-width', this.maxTokenWidth) | |
| 272 | + if (this.options.html) | |
| 273 | + $tokenLabel.html(attrs.label) | |
| 274 | + else | |
| 275 | + $tokenLabel.text(attrs.label) | |
| 276 | + | |
| 277 | + // Listen to events on token | |
| 278 | + $token | |
| 279 | + .on('mousedown', function (e) { | |
| 280 | + if (_self._disabled || _self._readonly) return false | |
| 281 | + _self.preventDeactivation = true | |
| 282 | + }) | |
| 283 | + .on('click', function (e) { | |
| 284 | + if (_self._disabled || _self._readonly) return false | |
| 285 | + _self.preventDeactivation = false | |
| 286 | + | |
| 287 | + if (e.ctrlKey || e.metaKey) { | |
| 288 | + e.preventDefault() | |
| 289 | + return _self.toggle( $token ) | |
| 290 | + } | |
| 291 | + | |
| 292 | + _self.activate( $token, e.shiftKey, e.shiftKey ) | |
| 293 | + }) | |
| 294 | + .on('dblclick', function (e) { | |
| 295 | + if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false | |
| 296 | + _self.edit( $token ) | |
| 297 | + }) | |
| 298 | + | |
| 299 | + $closeButton | |
| 300 | + .on('click', $.proxy(this.remove, this)) | |
| 301 | + | |
| 302 | + // Trigger createdtoken event on the original field | |
| 303 | + // indicating that the token is now in the DOM | |
| 304 | + this.$element.trigger($.Event('tokenfield:createdtoken', { | |
| 305 | + attrs: attrs, | |
| 306 | + relatedTarget: $token.get(0) | |
| 307 | + })) | |
| 308 | + | |
| 309 | + // Trigger change event on the original field | |
| 310 | + if (triggerChange) { | |
| 311 | + this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) | |
| 312 | + } | |
| 313 | + | |
| 314 | + // Update tokenfield dimensions | |
| 315 | + this.update() | |
| 316 | + | |
| 317 | + // Return original element | |
| 318 | + return this.$element.get(0) | |
| 319 | + } | |
| 320 | + | |
| 321 | + , setTokens: function (tokens, add, triggerChange) { | |
| 322 | + if (!tokens) return | |
| 323 | + | |
| 324 | + if (!add) this.$wrapper.find('.token').remove() | |
| 325 | + | |
| 326 | + if (typeof triggerChange === 'undefined') { | |
| 327 | + triggerChange = true | |
| 328 | + } | |
| 329 | + | |
| 330 | + if (typeof tokens === 'string') { | |
| 331 | + if (this._delimiters.length) { | |
| 332 | + // Split based on delimiters | |
| 333 | + tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) | |
| 334 | + } else { | |
| 335 | + tokens = [tokens]; | |
| 336 | + } | |
| 337 | + } | |
| 338 | + | |
| 339 | + var _self = this | |
| 340 | + $.each(tokens, function (i, attrs) { | |
| 341 | + _self.createToken(attrs, triggerChange) | |
| 342 | + }) | |
| 343 | + | |
| 344 | + return this.$element.get(0) | |
| 345 | + } | |
| 346 | + | |
| 347 | + , getTokenData: function($token) { | |
| 348 | + var data = $token.map(function() { | |
| 349 | + var $token = $(this); | |
| 350 | + return $token.data('attrs') | |
| 351 | + }).get(); | |
| 352 | + | |
| 353 | + if (data.length == 1) { | |
| 354 | + data = data[0]; | |
| 355 | + } | |
| 356 | + | |
| 357 | + return data; | |
| 358 | + } | |
| 359 | + | |
| 360 | + , getTokens: function(active) { | |
| 361 | + var self = this | |
| 362 | + , tokens = [] | |
| 363 | + , activeClass = active ? '.active' : '' // get active tokens only | |
| 364 | + this.$wrapper.find( '.token' + activeClass ).each( function() { | |
| 365 | + tokens.push( self.getTokenData( $(this) ) ) | |
| 366 | + }) | |
| 367 | + return tokens | |
| 368 | + } | |
| 369 | + | |
| 370 | + , getTokensList: function(delimiter, beautify, active) { | |
| 371 | + delimiter = delimiter || this._firstDelimiter | |
| 372 | + beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify | |
| 373 | + | |
| 374 | + var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') | |
| 375 | + return $.map( this.getTokens(active), function (token) { | |
| 376 | + return token.value | |
| 377 | + }).join(separator) | |
| 378 | + } | |
| 379 | + | |
| 380 | + , getInput: function() { | |
| 381 | + return this.$input.val() | |
| 382 | + } | |
| 383 | + | |
| 384 | + , listen: function () { | |
| 385 | + var _self = this | |
| 386 | + | |
| 387 | + this.$element | |
| 388 | + .on('change', $.proxy(this.change, this)) | |
| 389 | + | |
| 390 | + this.$wrapper | |
| 391 | + .on('mousedown',$.proxy(this.focusInput, this)) | |
| 392 | + | |
| 393 | + this.$input | |
| 394 | + .on('focus', $.proxy(this.focus, this)) | |
| 395 | + .on('blur', $.proxy(this.blur, this)) | |
| 396 | + .on('paste', $.proxy(this.paste, this)) | |
| 397 | + .on('keydown', $.proxy(this.keydown, this)) | |
| 398 | + .on('keypress', $.proxy(this.keypress, this)) | |
| 399 | + .on('keyup', $.proxy(this.keyup, this)) | |
| 400 | + | |
| 401 | + this.$copyHelper | |
| 402 | + .on('focus', $.proxy(this.focus, this)) | |
| 403 | + .on('blur', $.proxy(this.blur, this)) | |
| 404 | + .on('keydown', $.proxy(this.keydown, this)) | |
| 405 | + .on('keyup', $.proxy(this.keyup, this)) | |
| 406 | + | |
| 407 | + // Secondary listeners for input width calculation | |
| 408 | + this.$input | |
| 409 | + .on('keypress', $.proxy(this.update, this)) | |
| 410 | + .on('keyup', $.proxy(this.update, this)) | |
| 411 | + | |
| 412 | + this.$input | |
| 413 | + .on('autocompletecreate', function() { | |
| 414 | + // Set minimum autocomplete menu width | |
| 415 | + var $_menuElement = $(this).data('ui-autocomplete').menu.element | |
| 416 | + | |
| 417 | + var minWidth = _self.$wrapper.outerWidth() - | |
| 418 | + parseInt( $_menuElement.css('border-left-width'), 10 ) - | |
| 419 | + parseInt( $_menuElement.css('border-right-width'), 10 ) | |
| 420 | + | |
| 421 | + $_menuElement.css( 'min-width', minWidth + 'px' ) | |
| 422 | + }) | |
| 423 | + .on('autocompleteselect', function (e, ui) { | |
| 424 | + if (_self.createToken( ui.item )) { | |
| 425 | + _self.$input.val('') | |
| 426 | + if (_self.$input.data( 'edit' )) { | |
| 427 | + _self.unedit(true) | |
| 428 | + } | |
| 429 | + } | |
| 430 | + return false | |
| 431 | + }) | |
| 432 | + .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { | |
| 433 | + // Create token | |
| 434 | + if (_self.createToken( datum )) { | |
| 435 | + _self.$input.typeahead('val', '') | |
| 436 | + if (_self.$input.data( 'edit' )) { | |
| 437 | + _self.unedit(true) | |
| 438 | + } | |
| 439 | + } | |
| 440 | + }) | |
| 441 | + | |
| 442 | + // Listen to window resize | |
| 443 | + $(window).on('resize', $.proxy(this.update, this )) | |
| 444 | + | |
| 445 | + } | |
| 446 | + | |
| 447 | + , keydown: function (e) { | |
| 448 | + | |
| 449 | + if (!this.focused) return | |
| 450 | + | |
| 451 | + var _self = this | |
| 452 | + | |
| 453 | + switch(e.keyCode) { | |
| 454 | + case 8: // backspace | |
| 455 | + if (!this.$input.is(document.activeElement)) break | |
| 456 | + this.lastInputValue = this.$input.val() | |
| 457 | + break | |
| 458 | + | |
| 459 | + case 37: // left arrow | |
| 460 | + leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) | |
| 461 | + break | |
| 462 | + | |
| 463 | + case 38: // up arrow | |
| 464 | + upDown('prev') | |
| 465 | + break | |
| 466 | + | |
| 467 | + case 39: // right arrow | |
| 468 | + leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) | |
| 469 | + break | |
| 470 | + | |
| 471 | + case 40: // down arrow | |
| 472 | + upDown('next') | |
| 473 | + break | |
| 474 | + | |
| 475 | + case 65: // a (to handle ctrl + a) | |
| 476 | + if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break | |
| 477 | + this.activateAll() | |
| 478 | + e.preventDefault() | |
| 479 | + break | |
| 480 | + | |
| 481 | + case 9: // tab | |
| 482 | + case 13: // enter | |
| 483 | + | |
| 484 | + // We will handle creating tokens from autocomplete in autocomplete events | |
| 485 | + if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break | |
| 486 | + | |
| 487 | + // We will handle creating tokens from typeahead in typeahead events | |
| 488 | + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break | |
| 489 | + if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break | |
| 490 | + | |
| 491 | + // Create token | |
| 492 | + if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { | |
| 493 | + return this.createTokensFromInput(e, this.$input.data('edit')); | |
| 494 | + } | |
| 495 | + | |
| 496 | + // Edit token | |
| 497 | + if (e.keyCode === 13) { | |
| 498 | + if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break | |
| 499 | + if (!_self.options.allowEditing) break | |
| 500 | + this.edit( this.$wrapper.find('.token.active') ) | |
| 501 | + } | |
| 502 | + } | |
| 503 | + | |
| 504 | + function leftRight(direction) { | |
| 505 | + if (_self.$input.is(document.activeElement)) { | |
| 506 | + if (_self.$input.val().length > 0) return | |
| 507 | + | |
| 508 | + direction += 'All' | |
| 509 | + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') | |
| 510 | + if (!$token.length) return | |
| 511 | + | |
| 512 | + _self.preventInputFocus = true | |
| 513 | + _self.preventDeactivation = true | |
| 514 | + | |
| 515 | + _self.activate( $token ) | |
| 516 | + e.preventDefault() | |
| 517 | + | |
| 518 | + } else { | |
| 519 | + _self[direction]( e.shiftKey ) | |
| 520 | + e.preventDefault() | |
| 521 | + } | |
| 522 | + } | |
| 523 | + | |
| 524 | + function upDown(direction) { | |
| 525 | + if (!e.shiftKey) return | |
| 526 | + | |
| 527 | + if (_self.$input.is(document.activeElement)) { | |
| 528 | + if (_self.$input.val().length > 0) return | |
| 529 | + | |
| 530 | + var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') | |
| 531 | + if (!$token.length) return | |
| 532 | + | |
| 533 | + _self.activate( $token ) | |
| 534 | + } | |
| 535 | + | |
| 536 | + var opposite = direction === 'prev' ? 'next' : 'prev' | |
| 537 | + , position = direction === 'prev' ? 'first' : 'last' | |
| 538 | + | |
| 539 | + _self.$firstActiveToken[opposite + 'All']('.token').each(function() { | |
| 540 | + _self.deactivate( $(this) ) | |
| 541 | + }) | |
| 542 | + | |
| 543 | + _self.activate( _self.$wrapper.find('.token:' + position), true, true ) | |
| 544 | + e.preventDefault() | |
| 545 | + } | |
| 546 | + | |
| 547 | + this.lastKeyDown = e.keyCode | |
| 548 | + } | |
| 549 | + | |
| 550 | + , keypress: function(e) { | |
| 551 | + | |
| 552 | + // Comma | |
| 553 | + if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { | |
| 554 | + if (this.$input.val()) { | |
| 555 | + this.createTokensFromInput(e) | |
| 556 | + } | |
| 557 | + return false; | |
| 558 | + } | |
| 559 | + } | |
| 560 | + | |
| 561 | + , keyup: function (e) { | |
| 562 | + this.preventInputFocus = false | |
| 563 | + | |
| 564 | + if (!this.focused) return | |
| 565 | + | |
| 566 | + switch(e.keyCode) { | |
| 567 | + case 8: // backspace | |
| 568 | + if (this.$input.is(document.activeElement)) { | |
| 569 | + if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break | |
| 570 | + | |
| 571 | + this.preventDeactivation = true | |
| 572 | + var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') | |
| 573 | + | |
| 574 | + if (!$prevToken.length) break | |
| 575 | + | |
| 576 | + this.activate( $prevToken ) | |
| 577 | + } else { | |
| 578 | + this.remove(e) | |
| 579 | + } | |
| 580 | + break | |
| 581 | + | |
| 582 | + case 46: // delete | |
| 583 | + this.remove(e, 'next') | |
| 584 | + break | |
| 585 | + } | |
| 586 | + this.lastKeyUp = e.keyCode | |
| 587 | + } | |
| 588 | + | |
| 589 | + , focus: function (e) { | |
| 590 | + this.focused = true | |
| 591 | + this.$wrapper.addClass('focus') | |
| 592 | + | |
| 593 | + if (this.$input.is(document.activeElement)) { | |
| 594 | + this.$wrapper.find('.active').removeClass('active') | |
| 595 | + this.$firstActiveToken = null | |
| 596 | + | |
| 597 | + if (this.options.showAutocompleteOnFocus) { | |
| 598 | + this.search() | |
| 599 | + } | |
| 600 | + } | |
| 601 | + } | |
| 602 | + | |
| 603 | + , blur: function (e) { | |
| 604 | + | |
| 605 | + this.focused = false | |
| 606 | + this.$wrapper.removeClass('focus') | |
| 607 | + | |
| 608 | + if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { | |
| 609 | + this.$wrapper.find('.active').removeClass('active') | |
| 610 | + this.$firstActiveToken = null | |
| 611 | + } | |
| 612 | + | |
| 613 | + if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { | |
| 614 | + this.createTokensFromInput(e) | |
| 615 | + } | |
| 616 | + | |
| 617 | + this.preventDeactivation = false | |
| 618 | + this.preventCreateTokens = false | |
| 619 | + } | |
| 620 | + | |
| 621 | + , paste: function (e) { | |
| 622 | + var _self = this | |
| 623 | + | |
| 624 | + // Add tokens to existing ones | |
| 625 | + if (_self.options.allowPasting) { | |
| 626 | + setTimeout(function () { | |
| 627 | + _self.createTokensFromInput(e) | |
| 628 | + }, 1) | |
| 629 | + } | |
| 630 | + } | |
| 631 | + | |
| 632 | + , change: function (e) { | |
| 633 | + if ( e.initiator === 'tokenfield' ) return // Prevent loops | |
| 634 | + | |
| 635 | + this.setTokens( this.$element.val() ) | |
| 636 | + } | |
| 637 | + | |
| 638 | + , createTokensFromInput: function (e, focus) { | |
| 639 | + if (this.$input.val().length < this.options.minLength) | |
| 640 | + return // No input, simply return | |
| 641 | + | |
| 642 | + var tokensBefore = this.getTokensList() | |
| 643 | + this.setTokens( this.$input.val(), true ) | |
| 644 | + | |
| 645 | + if (tokensBefore == this.getTokensList() && this.$input.val().length) | |
| 646 | + return false // No tokens were added, do nothing (prevent form submit) | |
| 647 | + | |
| 648 | + if (this.$input.hasClass('tt-input')) { | |
| 649 | + // Typeahead acts weird when simply setting input value to empty, | |
| 650 | + // so we set the query to empty instead | |
| 651 | + this.$input.typeahead('val', '') | |
| 652 | + } else { | |
| 653 | + this.$input.val('') | |
| 654 | + } | |
| 655 | + | |
| 656 | + if (this.$input.data( 'edit' )) { | |
| 657 | + this.unedit(focus) | |
| 658 | + } | |
| 659 | + | |
| 660 | + return false // Prevent form being submitted | |
| 661 | + } | |
| 662 | + | |
| 663 | + , next: function (add) { | |
| 664 | + if (add) { | |
| 665 | + var $firstActiveToken = this.$wrapper.find('.active:first') | |
| 666 | + , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false | |
| 667 | + | |
| 668 | + if (deactivate) return this.deactivate( $firstActiveToken ) | |
| 669 | + } | |
| 670 | + | |
| 671 | + var $lastActiveToken = this.$wrapper.find('.active:last') | |
| 672 | + , $nextToken = $lastActiveToken.nextAll('.token:first') | |
| 673 | + | |
| 674 | + if (!$nextToken.length) { | |
| 675 | + this.$input.focus() | |
| 676 | + return | |
| 677 | + } | |
| 678 | + | |
| 679 | + this.activate($nextToken, add) | |
| 680 | + } | |
| 681 | + | |
| 682 | + , prev: function (add) { | |
| 683 | + | |
| 684 | + if (add) { | |
| 685 | + var $lastActiveToken = this.$wrapper.find('.active:last') | |
| 686 | + , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false | |
| 687 | + | |
| 688 | + if (deactivate) return this.deactivate( $lastActiveToken ) | |
| 689 | + } | |
| 690 | + | |
| 691 | + var $firstActiveToken = this.$wrapper.find('.active:first') | |
| 692 | + , $prevToken = $firstActiveToken.prevAll('.token:first') | |
| 693 | + | |
| 694 | + if (!$prevToken.length) { | |
| 695 | + $prevToken = this.$wrapper.find('.token:first') | |
| 696 | + } | |
| 697 | + | |
| 698 | + if (!$prevToken.length && !add) { | |
| 699 | + this.$input.focus() | |
| 700 | + return | |
| 701 | + } | |
| 702 | + | |
| 703 | + this.activate( $prevToken, add ) | |
| 704 | + } | |
| 705 | + | |
| 706 | + , activate: function ($token, add, multi, remember) { | |
| 707 | + | |
| 708 | + if (!$token) return | |
| 709 | + | |
| 710 | + if (typeof remember === 'undefined') var remember = true | |
| 711 | + | |
| 712 | + if (multi) var add = true | |
| 713 | + | |
| 714 | + this.$copyHelper.focus() | |
| 715 | + | |
| 716 | + if (!add) { | |
| 717 | + this.$wrapper.find('.active').removeClass('active') | |
| 718 | + if (remember) { | |
| 719 | + this.$firstActiveToken = $token | |
| 720 | + } else { | |
| 721 | + delete this.$firstActiveToken | |
| 722 | + } | |
| 723 | + } | |
| 724 | + | |
| 725 | + if (multi && this.$firstActiveToken) { | |
| 726 | + // Determine first active token and the current tokens indicies | |
| 727 | + // Account for the 1 hidden textarea by subtracting 1 from both | |
| 728 | + var i = this.$firstActiveToken.index() - 2 | |
| 729 | + , a = $token.index() - 2 | |
| 730 | + , _self = this | |
| 731 | + | |
| 732 | + this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { | |
| 733 | + _self.activate( $(this), true ) | |
| 734 | + }) | |
| 735 | + } | |
| 736 | + | |
| 737 | + $token.addClass('active') | |
| 738 | + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() | |
| 739 | + } | |
| 740 | + | |
| 741 | + , activateAll: function() { | |
| 742 | + var _self = this | |
| 743 | + | |
| 744 | + this.$wrapper.find('.token').each( function (i) { | |
| 745 | + _self.activate($(this), i !== 0, false, false) | |
| 746 | + }) | |
| 747 | + } | |
| 748 | + | |
| 749 | + , deactivate: function($token) { | |
| 750 | + if (!$token) return | |
| 751 | + | |
| 752 | + $token.removeClass('active') | |
| 753 | + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() | |
| 754 | + } | |
| 755 | + | |
| 756 | + , toggle: function($token) { | |
| 757 | + if (!$token) return | |
| 758 | + | |
| 759 | + $token.toggleClass('active') | |
| 760 | + this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() | |
| 761 | + } | |
| 762 | + | |
| 763 | + , edit: function ($token) { | |
| 764 | + if (!$token) return | |
| 765 | + | |
| 766 | + var attrs = $token.data('attrs') | |
| 767 | + | |
| 768 | + // Allow changing input value before editing | |
| 769 | + var options = { attrs: attrs, relatedTarget: $token.get(0) } | |
| 770 | + var editEvent = $.Event('tokenfield:edittoken', options) | |
| 771 | + this.$element.trigger( editEvent ) | |
| 772 | + | |
| 773 | + // Edit event can be cancelled if default is prevented | |
| 774 | + if (editEvent.isDefaultPrevented()) return | |
| 775 | + | |
| 776 | + $token.find('.token-label').text(attrs.value) | |
| 777 | + var tokenWidth = $token.outerWidth() | |
| 778 | + | |
| 779 | + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input | |
| 780 | + | |
| 781 | + $token.replaceWith( $_input ) | |
| 782 | + | |
| 783 | + this.preventCreateTokens = true | |
| 784 | + | |
| 785 | + this.$input.val( attrs.value ) | |
| 786 | + .select() | |
| 787 | + .data( 'edit', true ) | |
| 788 | + .width( tokenWidth ) | |
| 789 | + | |
| 790 | + this.update(); | |
| 791 | + | |
| 792 | + // Indicate that token is now being edited, and is replaced with an input field in the DOM | |
| 793 | + this.$element.trigger($.Event('tokenfield:editedtoken', options )) | |
| 794 | + } | |
| 795 | + | |
| 796 | + , unedit: function (focus) { | |
| 797 | + var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input | |
| 798 | + $_input.appendTo( this.$wrapper ) | |
| 799 | + | |
| 800 | + this.$input.data('edit', false) | |
| 801 | + this.$mirror.text('') | |
| 802 | + | |
| 803 | + this.update() | |
| 804 | + | |
| 805 | + // Because moving the input element around in DOM | |
| 806 | + // will cause it to lose focus, we provide an option | |
| 807 | + // to re-focus the input after appending it to the wrapper | |
| 808 | + if (focus) { | |
| 809 | + var _self = this | |
| 810 | + setTimeout(function () { | |
| 811 | + _self.$input.focus() | |
| 812 | + }, 1) | |
| 813 | + } | |
| 814 | + } | |
| 815 | + | |
| 816 | + , remove: function (e, direction) { | |
| 817 | + if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return | |
| 818 | + | |
| 819 | + var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') | |
| 820 | + | |
| 821 | + if (e.type !== 'click') { | |
| 822 | + if (!direction) var direction = 'prev' | |
| 823 | + this[direction]() | |
| 824 | + | |
| 825 | + // Was it the first token? | |
| 826 | + if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 | |
| 827 | + } | |
| 828 | + | |
| 829 | + // Prepare events and their options | |
| 830 | + var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } | |
| 831 | + , removeEvent = $.Event('tokenfield:removetoken', options) | |
| 832 | + | |
| 833 | + this.$element.trigger(removeEvent); | |
| 834 | + | |
| 835 | + // Remove event can be intercepted and cancelled | |
| 836 | + if (removeEvent.isDefaultPrevented()) return | |
| 837 | + | |
| 838 | + var removedEvent = $.Event('tokenfield:removedtoken', options) | |
| 839 | + , changeEvent = $.Event('change', { initiator: 'tokenfield' }) | |
| 840 | + | |
| 841 | + // Remove token from DOM | |
| 842 | + $token.remove() | |
| 843 | + | |
| 844 | + // Trigger events | |
| 845 | + this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) | |
| 846 | + | |
| 847 | + // Focus, when necessary: | |
| 848 | + // When there are no more tokens, or if this was the first token | |
| 849 | + // and it was removed with backspace or it was clicked on | |
| 850 | + if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() | |
| 851 | + | |
| 852 | + // Adjust input width | |
| 853 | + this.$input.css('width', this.options.minWidth + 'px') | |
| 854 | + this.update() | |
| 855 | + | |
| 856 | + // Cancel original event handlers | |
| 857 | + e.preventDefault() | |
| 858 | + e.stopPropagation() | |
| 859 | + } | |
| 860 | + | |
| 861 | + /** | |
| 862 | + * Update tokenfield dimensions | |
| 863 | + */ | |
| 864 | + , update: function (e) { | |
| 865 | + var value = this.$input.val() | |
| 866 | + , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) | |
| 867 | + , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) | |
| 868 | + , inputPadding = inputPaddingLeft + inputPaddingRight | |
| 869 | + | |
| 870 | + if (this.$input.data('edit')) { | |
| 871 | + | |
| 872 | + if (!value) { | |
| 873 | + value = this.$input.prop("placeholder") | |
| 874 | + } | |
| 875 | + if (value === this.$mirror.text()) return | |
| 876 | + | |
| 877 | + this.$mirror.text(value) | |
| 878 | + | |
| 879 | + var mirrorWidth = this.$mirror.width() + 10; | |
| 880 | + if ( mirrorWidth > this.$wrapper.width() ) { | |
| 881 | + return this.$input.width( this.$wrapper.width() ) | |
| 882 | + } | |
| 883 | + | |
| 884 | + this.$input.width( mirrorWidth ) | |
| 885 | + } | |
| 886 | + else { | |
| 887 | + var w = (this.textDirection === 'rtl') | |
| 888 | + ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 | |
| 889 | + : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding; | |
| 890 | + // | |
| 891 | + // some usecases pre-render widget before attaching to DOM, | |
| 892 | + // dimensions returned by jquery will be NaN -> we default to 100% | |
| 893 | + // so placeholder won't be cut off. | |
| 894 | + isNaN(w) ? this.$input.width('100%') : this.$input.width(w); | |
| 895 | + } | |
| 896 | + } | |
| 897 | + | |
| 898 | + , focusInput: function (e) { | |
| 899 | + if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return | |
| 900 | + // Focus only after the current call stack has cleared, | |
| 901 | + // otherwise has no effect. | |
| 902 | + // Reason: mousedown is too early - input will lose focus | |
| 903 | + // after mousedown. However, since the input may be moved | |
| 904 | + // in DOM, there may be no click or mouseup event triggered. | |
| 905 | + var _self = this | |
| 906 | + setTimeout(function() { | |
| 907 | + _self.$input.focus() | |
| 908 | + }, 0) | |
| 909 | + } | |
| 910 | + | |
| 911 | + , search: function () { | |
| 912 | + if ( this.$input.data('ui-autocomplete') ) { | |
| 913 | + this.$input.autocomplete('search') | |
| 914 | + } | |
| 915 | + } | |
| 916 | + | |
| 917 | + , disable: function () { | |
| 918 | + this.setProperty('disabled', true); | |
| 919 | + } | |
| 920 | + | |
| 921 | + , enable: function () { | |
| 922 | + this.setProperty('disabled', false); | |
| 923 | + } | |
| 924 | + | |
| 925 | + , readonly: function () { | |
| 926 | + this.setProperty('readonly', true); | |
| 927 | + } | |
| 928 | + | |
| 929 | + , writeable: function () { | |
| 930 | + this.setProperty('readonly', false); | |
| 931 | + } | |
| 932 | + | |
| 933 | + , setProperty: function(property, value) { | |
| 934 | + this['_' + property] = value; | |
| 935 | + this.$input.prop(property, value); | |
| 936 | + this.$element.prop(property, value); | |
| 937 | + this.$wrapper[ value ? 'addClass' : 'removeClass' ](property); | |
| 938 | + } | |
| 939 | + | |
| 940 | + , destroy: function() { | |
| 941 | + // Set field value | |
| 942 | + this.$element.val( this.getTokensList() ); | |
| 943 | + // Restore styles and properties | |
| 944 | + this.$element.css( this.$element.data('original-styles') ); | |
| 945 | + this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); | |
| 946 | + | |
| 947 | + // Re-route tokenfield label to original input | |
| 948 | + var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) | |
| 949 | + if ( $label.length ) { | |
| 950 | + $label.prop( 'for', this.$element.prop('id') ) | |
| 951 | + } | |
| 952 | + | |
| 953 | + // Move original element outside of tokenfield wrapper | |
| 954 | + this.$element.insertBefore( this.$wrapper ); | |
| 955 | + | |
| 956 | + // Remove tokenfield-related data | |
| 957 | + this.$element.removeData('original-styles') | |
| 958 | + .removeData('original-tabindex') | |
| 959 | + .removeData('bs.tokenfield'); | |
| 960 | + | |
| 961 | + // Remove tokenfield from DOM | |
| 962 | + this.$wrapper.remove(); | |
| 963 | + this.$mirror.remove(); | |
| 964 | + | |
| 965 | + var $_element = this.$element; | |
| 966 | + | |
| 967 | + return $_element; | |
| 968 | + } | |
| 969 | + | |
| 970 | + } | |
| 971 | + | |
| 972 | + | |
| 973 | + /* TOKENFIELD PLUGIN DEFINITION | |
| 974 | + * ======================== */ | |
| 975 | + | |
| 976 | + var old = $.fn.tokenfield | |
| 977 | + | |
| 978 | + $.fn.tokenfield = function (option, param) { | |
| 979 | + var value | |
| 980 | + , args = [] | |
| 981 | + | |
| 982 | + Array.prototype.push.apply( args, arguments ); | |
| 983 | + | |
| 984 | + var elements = this.each(function () { | |
| 985 | + var $this = $(this) | |
| 986 | + , data = $this.data('bs.tokenfield') | |
| 987 | + , options = typeof option == 'object' && option | |
| 988 | + | |
| 989 | + if (typeof option === 'string' && data && data[option]) { | |
| 990 | + args.shift() | |
| 991 | + value = data[option].apply(data, args) | |
| 992 | + } else { | |
| 993 | + if (!data && typeof option !== 'string' && !param) { | |
| 994 | + $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) | |
| 995 | + $this.trigger('tokenfield:initialize') | |
| 996 | + } | |
| 997 | + } | |
| 998 | + }) | |
| 999 | + | |
| 1000 | + return typeof value !== 'undefined' ? value : elements; | |
| 1001 | + } | |
| 1002 | + | |
| 1003 | + $.fn.tokenfield.defaults = { | |
| 1004 | + minWidth: 60, | |
| 1005 | + minLength: 0, | |
| 1006 | + html: true, | |
| 1007 | + allowEditing: true, | |
| 1008 | + allowPasting: true, | |
| 1009 | + limit: 0, | |
| 1010 | + autocomplete: {}, | |
| 1011 | + typeahead: {}, | |
| 1012 | + showAutocompleteOnFocus: false, | |
| 1013 | + createTokensOnBlur: false, | |
| 1014 | + delimiter: ',', | |
| 1015 | + beautify: true, | |
| 1016 | + inputType: 'text' | |
| 1017 | + } | |
| 1018 | + | |
| 1019 | + $.fn.tokenfield.Constructor = Tokenfield | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + /* TOKENFIELD NO CONFLICT | |
| 1023 | + * ================== */ | |
| 1024 | + | |
| 1025 | + $.fn.tokenfield.noConflict = function () { | |
| 1026 | + $.fn.tokenfield = old | |
| 1027 | + return this | |
| 1028 | + } | |
| 1029 | + | |
| 1030 | + return Tokenfield; | |
| 1031 | + | |
| 1032 | +})); | ... | ... |
| ... | ... | @@ -0,0 +1,312 @@ |
| 1 | +fb_app = { | |
| 2 | + current_url: '', | |
| 3 | + | |
| 4 | + locales: { | |
| 5 | + | |
| 6 | + }, | |
| 7 | + | |
| 8 | + config: { | |
| 9 | + url_prefix: '', | |
| 10 | + save_auth_url: '', | |
| 11 | + show_login_url: '', | |
| 12 | + | |
| 13 | + init: function() { | |
| 14 | + | |
| 15 | + }, | |
| 16 | + | |
| 17 | + }, | |
| 18 | + | |
| 19 | + timeline: { | |
| 20 | + appId: '', | |
| 21 | + app_scope: 'publish_actions', | |
| 22 | + | |
| 23 | + loading: function() { | |
| 24 | + jQuery('#fb-app-connect-status').empty().addClass('loading').height(150) | |
| 25 | + }, | |
| 26 | + | |
| 27 | + connect: function() { | |
| 28 | + this.loading(); | |
| 29 | + fb_app.fb.scope = this.app_scope | |
| 30 | + fb_app.fb.connect(function (response) { | |
| 31 | + fb_app.auth.receive(response) | |
| 32 | + }); | |
| 33 | + }, | |
| 34 | + | |
| 35 | + disconnect: function() { | |
| 36 | + // 'not_authorized' is used to disconnect from facebook | |
| 37 | + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html( | |
| 38 | + fb_app.locales.confirm_disconnect | |
| 39 | + ) | |
| 40 | + jQuery('#fb-app-modal-wrap .modal-button-no') | |
| 41 | + .html(fb_app.locales.cancel_button) | |
| 42 | + .attr('onClick', 'noosfero.modal.close(); return false') | |
| 43 | + jQuery('#fb-app-modal-wrap .modal-button-yes') | |
| 44 | + .html(fb_app.locales.confirm_disconnect_button) | |
| 45 | + .attr('onClick', 'fb_app.timeline.disconnect_confirmed();noosfero.modal.close(); return false') | |
| 46 | + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html()) | |
| 47 | + }, | |
| 48 | + | |
| 49 | + disconnect_confirmed: function() { | |
| 50 | + this.loading(); | |
| 51 | + fb_app.auth.receive({status: 'not_authorized'}) | |
| 52 | + }, | |
| 53 | + | |
| 54 | + connect_to_another: function() { | |
| 55 | + this.disconnect(); | |
| 56 | + fb_app.fb.connect_to_another(this.connect) | |
| 57 | + }, | |
| 58 | + }, | |
| 59 | + | |
| 60 | + page_tab: { | |
| 61 | + appId: '', | |
| 62 | + nextUrl: '', | |
| 63 | + | |
| 64 | + init: function() { | |
| 65 | + FB.Canvas.scrollTo(0,140); | |
| 66 | + // While Braulio doesnt make the catalog-options-bar work in a product page, I'll hide it. User will have to go back to the catalog to see this bar, provisorily... | |
| 67 | + jQuery('#product-page').prev('form').hide(); | |
| 68 | + }, | |
| 69 | + | |
| 70 | + config: { | |
| 71 | + | |
| 72 | + init: function() { | |
| 73 | + this.change_type($('select#page_tab_config_type')) | |
| 74 | + | |
| 75 | + }, | |
| 76 | + | |
| 77 | + edit: function(button) { | |
| 78 | + var page_tab = button.parents('.page-tab') | |
| 79 | + page_tab.find('form').toggle(400) | |
| 80 | + }, | |
| 81 | + | |
| 82 | + remove: function(button, url) { | |
| 83 | + var page_tab = button.parents('.page-tab') | |
| 84 | + var name = page_tab.find('#page_tab_name').val() | |
| 85 | + //jQuery('#fb-app-modal-catalog-name').text(name) | |
| 86 | + jQuery('#fb-app-modal-wrap #fb-app-modal-intro').html( | |
| 87 | + fb_app.locales.confirm_removal | |
| 88 | + ) | |
| 89 | + jQuery('#fb-app-modal-wrap .modal-button-no') | |
| 90 | + .html(fb_app.locales.cancel_button) | |
| 91 | + .attr('onClick', 'noosfero.modal.close(); return false') | |
| 92 | + jQuery('#fb-app-modal-wrap .modal-button-yes') | |
| 93 | + .html(fb_app.locales.confirm_removal_button) | |
| 94 | + .attr('onClick', 'fb_app.page_tab.config.remove_confirmed(this);noosfero.modal.close(); return false') | |
| 95 | + .attr('target_url',url) | |
| 96 | + .attr('target_id','#'+page_tab.attr('id')) | |
| 97 | + noosfero.modal.html(jQuery('#fb-app-modal-wrap').html()) | |
| 98 | + }, | |
| 99 | + | |
| 100 | + remove_confirmed: function(el) { | |
| 101 | + el = jQuery(el) | |
| 102 | + jQuery.post(el.attr('target_url'), function() { | |
| 103 | + var page_tab = jQuery(el.attr('target_id')) | |
| 104 | + page_tab.remove() | |
| 105 | + }) | |
| 106 | + }, | |
| 107 | + | |
| 108 | + close: function(pageId) { | |
| 109 | + noosfero.modal.close() | |
| 110 | + jQuery('#content').html('').addClass('loading') | |
| 111 | + fb_app.fb.redirect_to_tab(pageId, fb_app.page_tab.appId) | |
| 112 | + }, | |
| 113 | + | |
| 114 | + validate: function(form) { | |
| 115 | + for (var i=0; tinymce.editors[i]; i++) { | |
| 116 | + var editor = tinymce.editors[i] | |
| 117 | + var textarea = editor.getElement() | |
| 118 | + textarea.value = editor.getContent() | |
| 119 | + } | |
| 120 | + | |
| 121 | + if (form.find('#page_tab_title').val().trim()=='') { | |
| 122 | + noosfero.modal.html('<div id="fb-app-error">'+fb_app.locales.error_empty_title+'</div>') | |
| 123 | + return false | |
| 124 | + } else { | |
| 125 | + var selected_type = form.find('#page_tab_config_type').val() | |
| 126 | + var sub_option = form.find('.config-type-'+selected_type+' input') | |
| 127 | + if (sub_option.length > 0 && sub_option.val().trim()=='') { | |
| 128 | + noosfero.modal.html('<div id="fb-app-error">'+fb_app.locales.error_empty_settings+'</div>') | |
| 129 | + return false | |
| 130 | + } | |
| 131 | + } | |
| 132 | + return true | |
| 133 | + }, | |
| 134 | + | |
| 135 | + add: function (form) { | |
| 136 | + if (!this.validate(form)) | |
| 137 | + return false | |
| 138 | + // this checks if the user is using FB as a page and offer a switch | |
| 139 | + FB.login(function(response) { | |
| 140 | + if (response.status != 'connected') return | |
| 141 | + var nextUrl = fb_app.page_tab.nextUrl + '?' + form.serialize() | |
| 142 | + window.location.href = fb_app.fb.add_tab_url(fb_app.page_tab.appId, nextUrl) | |
| 143 | + }) | |
| 144 | + return false | |
| 145 | + }, | |
| 146 | + | |
| 147 | + save: function(form) { | |
| 148 | + if (!this.validate(form)) | |
| 149 | + return false | |
| 150 | + jQuery(form).ajaxSubmit({ | |
| 151 | + dataType: 'script', | |
| 152 | + }) | |
| 153 | + return false | |
| 154 | + }, | |
| 155 | + | |
| 156 | + change_type: function(select) { | |
| 157 | + select = jQuery(select) | |
| 158 | + var page_tab = select.parents('.page-tab') | |
| 159 | + var config_selector = '.config-type-'+select.val() | |
| 160 | + var config = page_tab.find(config_selector) | |
| 161 | + var to_show = config | |
| 162 | + var to_hide = page_tab.find('.config-type:not('+config_selector+')') | |
| 163 | + | |
| 164 | + to_show.show(). | |
| 165 | + find('input').prop('disabled', false) | |
| 166 | + to_show.find('.tokenfield').removeClass('disabled') | |
| 167 | + to_hide.hide(). | |
| 168 | + find('input').prop('disabled', true) | |
| 169 | + }, | |
| 170 | + | |
| 171 | + profile: { | |
| 172 | + | |
| 173 | + onchange: function(input) { | |
| 174 | + if (input.val()) | |
| 175 | + input.removeAttr('placeholder') | |
| 176 | + else | |
| 177 | + input.attr('placeholder', input.attr('data-placeholder')) | |
| 178 | + }, | |
| 179 | + }, | |
| 180 | + }, | |
| 181 | + | |
| 182 | + }, | |
| 183 | + | |
| 184 | + auth: { | |
| 185 | + status: 'not_authorized', | |
| 186 | + | |
| 187 | + load: function (html) { | |
| 188 | + jQuery('#fb-app-settings').html(html) | |
| 189 | + }, | |
| 190 | + loadLogin: function (html) { | |
| 191 | + if (this.status == 'not_authorized') | |
| 192 | + jQuery('#fb-app-connect').html(html).removeClass('loading') | |
| 193 | + else | |
| 194 | + jQuery('#fb-app-login').html(html) | |
| 195 | + }, | |
| 196 | + | |
| 197 | + receive: function(response) { | |
| 198 | + fb_app.fb.authResponse = response | |
| 199 | + fb_app.auth.save(response) | |
| 200 | + jQuery('html,body').animate({ scrollTop: jQuery('#fb-app-settings').offset().top-100 }, 400) | |
| 201 | + }, | |
| 202 | + | |
| 203 | + transformParams: function(response) { | |
| 204 | + var authResponse = response.authResponse | |
| 205 | + if (!authResponse) | |
| 206 | + return {auth: {status: response.status}} | |
| 207 | + else | |
| 208 | + return { | |
| 209 | + auth: { | |
| 210 | + status: response.status, | |
| 211 | + access_token: authResponse.accessToken, | |
| 212 | + expires_in: authResponse.expiresIn, | |
| 213 | + signed_request: authResponse.signedRequest, | |
| 214 | + provider_user_id: authResponse.userID, | |
| 215 | + } | |
| 216 | + } | |
| 217 | + }, | |
| 218 | + | |
| 219 | + showLogin: function(response) { | |
| 220 | + jQuery.get(fb_app.config.show_login_url, this.transformParams(response), this.loadLogin) | |
| 221 | + }, | |
| 222 | + | |
| 223 | + save: function(response) { | |
| 224 | + jQuery.post(fb_app.config.save_auth_url, this.transformParams(response), this.load) | |
| 225 | + }, | |
| 226 | + }, | |
| 227 | + | |
| 228 | + | |
| 229 | + // interface to facebook's SDK | |
| 230 | + fb: { | |
| 231 | + appId: '', | |
| 232 | + scope: '', | |
| 233 | + inited: false, | |
| 234 | + initCode: null, | |
| 235 | + | |
| 236 | + prepareAsyncInit: function(appId, asyncInitCode) { | |
| 237 | + this.id = appId | |
| 238 | + this.initCode = asyncInitCode | |
| 239 | + | |
| 240 | + window.fbAsyncInit = function() { | |
| 241 | + FB.init({ | |
| 242 | + appId: appId, | |
| 243 | + cookie: true, | |
| 244 | + xfbml: true, | |
| 245 | + status: true, | |
| 246 | + }) | |
| 247 | + | |
| 248 | + // automatic iframe's resize | |
| 249 | + // FIXME: move to page tab embed code | |
| 250 | + fb_app.fb.size_change() | |
| 251 | + jQuery(document).on('DOMNodeInserted', fb_app.fb.size_change) | |
| 252 | + | |
| 253 | + if (asyncInitCode) | |
| 254 | + jQuery.globalEval(asyncInitCode) | |
| 255 | + | |
| 256 | + fb_app.fb.inited = true | |
| 257 | + } | |
| 258 | + }, | |
| 259 | + | |
| 260 | + init: function() { | |
| 261 | + // the SDK is loaded on views/fb_app_plugin/_load.html.slim and then call window.fbAsyncInit | |
| 262 | + }, | |
| 263 | + | |
| 264 | + size_change: function() { | |
| 265 | + FB.Canvas.setSize({height: jQuery('body').height()+100}) | |
| 266 | + }, | |
| 267 | + | |
| 268 | + redirect_to_tab: function(pageId, appId) { | |
| 269 | + window.location.href = 'https://facebook.com/' + pageId + '?sk=app_' + appId | |
| 270 | + }, | |
| 271 | + | |
| 272 | + add_tab_url: function (appId, nextUrl) { | |
| 273 | + return 'https://www.facebook.com/dialog/pagetab?' + jQuery.param({app_id: appId, next: nextUrl}) | |
| 274 | + }, | |
| 275 | + | |
| 276 | + connect: function(callback) { | |
| 277 | + FB.login(function(response) { | |
| 278 | + if (callback) callback(response) | |
| 279 | + }, {scope: fb_app.fb.scope}) | |
| 280 | + }, | |
| 281 | + | |
| 282 | + connect_to_another: function(callback) { | |
| 283 | + this.logout(this.connect(callback)) | |
| 284 | + }, | |
| 285 | + | |
| 286 | + logout: function(callback) { | |
| 287 | + // this checks if the user is using FB as a page and offer a switch | |
| 288 | + FB.login(function(response) { | |
| 289 | + FB.logout(function(response) { | |
| 290 | + if (callback) callback(response) | |
| 291 | + }) | |
| 292 | + }) | |
| 293 | + }, | |
| 294 | + | |
| 295 | + // not to be used | |
| 296 | + delete: function(callback) { | |
| 297 | + FB.api("/me/permissions", "DELETE", function(response) { | |
| 298 | + if (callback) callback(response) | |
| 299 | + }) | |
| 300 | + }, | |
| 301 | + | |
| 302 | + checkLoginStatus: function() { | |
| 303 | + FB.getLoginStatus(function(response) { | |
| 304 | + // don't do nothing, this is just to fetch auth after init | |
| 305 | + }) | |
| 306 | + }, | |
| 307 | + | |
| 308 | + }, | |
| 309 | + | |
| 310 | +} | |
| 311 | + | |
| 312 | + | ... | ... |
| ... | ... | @@ -0,0 +1,214 @@ |
| 1 | +/* use with @extend, CSS clear bugfix */ | |
| 2 | +.clean { | |
| 3 | + clear: both; | |
| 4 | +} | |
| 5 | +.container-clean { | |
| 6 | + overflow: hidden; | |
| 7 | + display: inline-block; /* Necessary to trigger "hasLayout" in IE */ | |
| 8 | + display: block; /* Sets element back to block */ | |
| 9 | +} | |
| 10 | + | |
| 11 | +/* layout base parameters */ | |
| 12 | +$modules: 12; | |
| 13 | +$base: 8px; | |
| 14 | +$wireframe: 1040px; | |
| 15 | + | |
| 16 | +/* heights should only use multiples of this */ | |
| 17 | +$height: $base; | |
| 18 | + | |
| 19 | +/* base measurements */ | |
| 20 | +$intercolumn: 2*$base; | |
| 21 | +$module: $wireframe/$modules - $intercolumn; | |
| 22 | + | |
| 23 | +/* widths should only use one of these */ | |
| 24 | +$module01: 01*$module + 00*$intercolumn; | |
| 25 | +$module02: 02*$module + 01*$intercolumn; | |
| 26 | +$module03: 03*$module + 02*$intercolumn; | |
| 27 | +$module04: 04*$module + 03*$intercolumn; | |
| 28 | +$module05: 05*$module + 04*$intercolumn; | |
| 29 | +$module06: 06*$module + 05*$intercolumn; | |
| 30 | +$module07: 07*$module + 06*$intercolumn; | |
| 31 | +$module08: 08*$module + 07*$intercolumn; | |
| 32 | +$module09: 09*$module + 08*$intercolumn; | |
| 33 | +$module09: 09*$module + 08*$intercolumn; | |
| 34 | +$module10: 10*$module + 09*$intercolumn; | |
| 35 | +$module11: 11*$module + 10*$intercolumn; | |
| 36 | +$module12: 12*$module + 11*$intercolumn; | |
| 37 | +$module01p: percentage($module01/$wireframe); | |
| 38 | +$module02p: percentage($module02/$wireframe); | |
| 39 | +$module03p: percentage($module03/$wireframe); | |
| 40 | +$module04p: percentage($module04/$wireframe); | |
| 41 | +$module05p: percentage($module05/$wireframe); | |
| 42 | +$module06p: percentage($module06/$wireframe); | |
| 43 | +$module07p: percentage($module07/$wireframe); | |
| 44 | +$module08p: percentage($module08/$wireframe); | |
| 45 | +$module09p: percentage($module09/$wireframe); | |
| 46 | +$module10p: percentage($module10/$wireframe); | |
| 47 | +$module11p: percentage($module11/$wireframe); | |
| 48 | +$module12p: percentage($module12/$wireframe); | |
| 49 | + | |
| 50 | +/* paddings and margins should only use one of these | |
| 51 | + Ps. 1: disccount the borders size from padding, as borders uses padding's space. | |
| 52 | + Ps. 2: because of W3C's content-box default box sizing, padding sums to width size. If your | |
| 53 | + box doesn't have a padding, then sum $intercolumn to the width. | |
| 54 | + */ | |
| 55 | +$margin: $intercolumn; | |
| 56 | +$half-margin: $margin/2; | |
| 57 | +$padding: $intercolumn/2; | |
| 58 | +$half-padding: $padding/2; | |
| 59 | +$marginp: percentage($margin/$wireframe); | |
| 60 | +$half-marginp: percentage($half-margin/$wireframe); | |
| 61 | +$paddingp: percentage($padding/$wireframe); | |
| 62 | +$half-paddingp: percentage($half-padding/$wireframe); | |
| 63 | + | |
| 64 | +$wireframe-padding: 5*$padding; | |
| 65 | + | |
| 66 | +/* use for borders */ | |
| 67 | +$border: 1px; | |
| 68 | +$border-radius: 5px; | |
| 69 | + | |
| 70 | +/* use for text shadows */ | |
| 71 | +$shadow: 2px; | |
| 72 | + | |
| 73 | +/* Colors */ | |
| 74 | + | |
| 75 | +$border-action-button: #F4A439; | |
| 76 | +$bg-action-button: #FBCA47; | |
| 77 | +$bg-selection-button: white; | |
| 78 | + | |
| 79 | +/* Fonts */ | |
| 80 | + | |
| 81 | +/* Paragraphs Styles (use with @extend) */ | |
| 82 | + | |
| 83 | +.pstyle-none { | |
| 84 | + font-size: 12px; | |
| 85 | +} | |
| 86 | +.pstyle-basic { | |
| 87 | + font-size: 16px; | |
| 88 | +} | |
| 89 | +.pstyle-button { | |
| 90 | + font-size: 16px; | |
| 91 | +} | |
| 92 | +.pstyle-button-small { | |
| 93 | + font-size: 13px; | |
| 94 | +} | |
| 95 | +.pstyle-title { | |
| 96 | + font-size: 72px; | |
| 97 | +} | |
| 98 | +.pstyle-h1 { | |
| 99 | + font-size: 34px; | |
| 100 | +} | |
| 101 | +.pstyle-h2 { | |
| 102 | + font-size: 26px; | |
| 103 | +} | |
| 104 | +.pstyle-h3 { | |
| 105 | + font-size: 21px; | |
| 106 | +} | |
| 107 | +.pstyle-h4 { | |
| 108 | + font-size: 16px; | |
| 109 | +} | |
| 110 | +.pstyle-h5 { | |
| 111 | + font-size: 13px; | |
| 112 | +} | |
| 113 | +.pstyle-title-section { | |
| 114 | + font-size: 92px; | |
| 115 | +} | |
| 116 | +.pstyle-field { | |
| 117 | + font-size: 13px; | |
| 118 | +} | |
| 119 | +.pstyle-menu-big-selected { | |
| 120 | + font-size: 21px; | |
| 121 | +} | |
| 122 | +.pstyle-menu-big-unselected { | |
| 123 | + font-size: 21px; | |
| 124 | +} | |
| 125 | +.pstyle-menu-medium-selected { | |
| 126 | + font-size: 16px; | |
| 127 | +} | |
| 128 | +.pstyle-menu-medium-unselected { | |
| 129 | + font-size: 16px; | |
| 130 | +} | |
| 131 | +.pstyle-menu-small-selected { | |
| 132 | + font-size: 13px; | |
| 133 | +} | |
| 134 | +.pstyle-menu-small-unselected { | |
| 135 | + font-size: 13px; | |
| 136 | +} | |
| 137 | +.pstyle-tp4 { | |
| 138 | + font-size: 42px; | |
| 139 | +} | |
| 140 | +.pstyle-tp3 { | |
| 141 | + font-size: 34px; | |
| 142 | +} | |
| 143 | +.pstyle-tp2 { | |
| 144 | + font-size: 26px; | |
| 145 | +} | |
| 146 | +.pstyle-tp1 { | |
| 147 | + font-size: 21px; | |
| 148 | +} | |
| 149 | +.pstyle-tm1 { | |
| 150 | + font-size: 13px; | |
| 151 | +} | |
| 152 | +.pstyle-tm2 { | |
| 153 | + font-size: 10px; | |
| 154 | +} | |
| 155 | +.subtitle { | |
| 156 | + @extend .pstyle-tm2; | |
| 157 | +} | |
| 158 | + | |
| 159 | +/* Images */ | |
| 160 | + | |
| 161 | +$profile-thumb-size: 4*$base; | |
| 162 | +$profile-portrait-size: 10*$base; | |
| 163 | + | |
| 164 | +/* profile-image that can be centered and resized with aspect ratio */ | |
| 165 | +.profile-image { | |
| 166 | + display: inline-block; | |
| 167 | + | |
| 168 | + &.thumb { | |
| 169 | + width: $profile-thumb-size; | |
| 170 | + height: $profile-thumb-size; | |
| 171 | + } | |
| 172 | + &.portrait { | |
| 173 | + width: $profile-portrait-size; | |
| 174 | + height: $profile-portrait-size; | |
| 175 | + } | |
| 176 | + | |
| 177 | + /* do not put padding in this as background size will consider it. */ | |
| 178 | + .inner { | |
| 179 | + display: block; | |
| 180 | + width: 100%; | |
| 181 | + height: 100%; | |
| 182 | + background-repeat: no-repeat; | |
| 183 | + background-position: center; | |
| 184 | + background-size: 100%; | |
| 185 | + background-size: contain; /* css3 enabled */ | |
| 186 | + } | |
| 187 | +} | |
| 188 | + | |
| 189 | +/* Buttons */ | |
| 190 | + | |
| 191 | +.action-button { | |
| 192 | + display: inline-block; | |
| 193 | + padding: $half-padding $padding; | |
| 194 | + height: auto; | |
| 195 | + width: auto; | |
| 196 | + //&:visited, &:active, &:hover { color: white; } | |
| 197 | + background: $bg-action-button; | |
| 198 | + border: $border solid $border-action-button; | |
| 199 | + cursor: pointer; | |
| 200 | + color: black; | |
| 201 | + font-weight: bold; | |
| 202 | + line-height: 2*$height; | |
| 203 | + text-align: center; | |
| 204 | + text-decoration: none; | |
| 205 | + text-transform: uppercase; | |
| 206 | + text-shadow: none; | |
| 207 | + border-radius: $border-radius; | |
| 208 | +} | |
| 209 | + | |
| 210 | +.selection-button { | |
| 211 | + @extend .action-button; | |
| 212 | + background: $bg-selection-button; | |
| 213 | +} | |
| 214 | + | ... | ... |
plugins/fb_app/public/stylesheets/bootstrap-tokenfield.css
0 → 100644
| ... | ... | @@ -0,0 +1,209 @@ |
| 1 | +/*! | |
| 2 | + * bootstrap-tokenfield | |
| 3 | + * https://github.com/sliptree/bootstrap-tokenfield | |
| 4 | + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT | |
| 5 | + */ | |
| 6 | +@-webkit-keyframes blink { | |
| 7 | + 0% { | |
| 8 | + border-color: #ededed; | |
| 9 | + } | |
| 10 | + 100% { | |
| 11 | + border-color: #b94a48; | |
| 12 | + } | |
| 13 | +} | |
| 14 | +@-moz-keyframes blink { | |
| 15 | + 0% { | |
| 16 | + border-color: #ededed; | |
| 17 | + } | |
| 18 | + 100% { | |
| 19 | + border-color: #b94a48; | |
| 20 | + } | |
| 21 | +} | |
| 22 | +@keyframes blink { | |
| 23 | + 0% { | |
| 24 | + border-color: #ededed; | |
| 25 | + } | |
| 26 | + 100% { | |
| 27 | + border-color: #b94a48; | |
| 28 | + } | |
| 29 | +} | |
| 30 | +.tokenfield { | |
| 31 | + height: auto; | |
| 32 | + min-height: 34px; | |
| 33 | + padding-bottom: 0px; | |
| 34 | +} | |
| 35 | +.tokenfield.focus { | |
| 36 | + border-color: #66afe9; | |
| 37 | + outline: 0; | |
| 38 | + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); | |
| 39 | + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); | |
| 40 | +} | |
| 41 | +.tokenfield .token { | |
| 42 | + -webkit-box-sizing: border-box; | |
| 43 | + -moz-box-sizing: border-box; | |
| 44 | + box-sizing: border-box; | |
| 45 | + -webkit-border-radius: 3px; | |
| 46 | + -moz-border-radius: 3px; | |
| 47 | + border-radius: 3px; | |
| 48 | + display: inline-block; | |
| 49 | + border: 1px solid #d9d9d9; | |
| 50 | + background-color: #ededed; | |
| 51 | + white-space: nowrap; | |
| 52 | + margin: -1px 5px 5px 0; | |
| 53 | + height: 22px; | |
| 54 | + vertical-align: top; | |
| 55 | + cursor: default; | |
| 56 | +} | |
| 57 | +.tokenfield .token:hover { | |
| 58 | + border-color: #b9b9b9; | |
| 59 | +} | |
| 60 | +.tokenfield .token.active { | |
| 61 | + border-color: #52a8ec; | |
| 62 | + border-color: rgba(82, 168, 236, 0.8); | |
| 63 | +} | |
| 64 | +.tokenfield .token.duplicate { | |
| 65 | + border-color: #ebccd1; | |
| 66 | + -webkit-animation-name: blink; | |
| 67 | + animation-name: blink; | |
| 68 | + -webkit-animation-duration: 0.1s; | |
| 69 | + animation-duration: 0.1s; | |
| 70 | + -webkit-animation-direction: normal; | |
| 71 | + animation-direction: normal; | |
| 72 | + -webkit-animation-timing-function: ease; | |
| 73 | + animation-timing-function: ease; | |
| 74 | + -webkit-animation-iteration-count: infinite; | |
| 75 | + animation-iteration-count: infinite; | |
| 76 | +} | |
| 77 | +.tokenfield .token.invalid { | |
| 78 | + background: none; | |
| 79 | + border: 1px solid transparent; | |
| 80 | + -webkit-border-radius: 0; | |
| 81 | + -moz-border-radius: 0; | |
| 82 | + border-radius: 0; | |
| 83 | + border-bottom: 1px dotted #d9534f; | |
| 84 | +} | |
| 85 | +.tokenfield .token.invalid.active { | |
| 86 | + background: #ededed; | |
| 87 | + border: 1px solid #ededed; | |
| 88 | + -webkit-border-radius: 3px; | |
| 89 | + -moz-border-radius: 3px; | |
| 90 | + border-radius: 3px; | |
| 91 | +} | |
| 92 | +.tokenfield .token .token-label { | |
| 93 | + display: inline-block; | |
| 94 | + overflow: hidden; | |
| 95 | + text-overflow: ellipsis; | |
| 96 | + padding-left: 4px; | |
| 97 | + vertical-align: top; | |
| 98 | +} | |
| 99 | +.tokenfield .token .close { | |
| 100 | + font-family: Arial; | |
| 101 | + display: inline-block; | |
| 102 | + line-height: 100%; | |
| 103 | + font-size: 1.1em; | |
| 104 | + line-height: 1.49em; | |
| 105 | + margin-left: 5px; | |
| 106 | + float: none; | |
| 107 | + height: 100%; | |
| 108 | + vertical-align: top; | |
| 109 | + padding-right: 4px; | |
| 110 | +} | |
| 111 | +.tokenfield .token-input { | |
| 112 | + background: none; | |
| 113 | + width: 60px; | |
| 114 | + min-width: 60px; | |
| 115 | + border: 0; | |
| 116 | + height: 20px; | |
| 117 | + padding: 0; | |
| 118 | + margin-bottom: 6px; | |
| 119 | + -webkit-box-shadow: none; | |
| 120 | + box-shadow: none; | |
| 121 | +} | |
| 122 | +.tokenfield .token-input:focus { | |
| 123 | + border-color: transparent; | |
| 124 | + outline: 0; | |
| 125 | + /* IE6-9 */ | |
| 126 | + -webkit-box-shadow: none; | |
| 127 | + box-shadow: none; | |
| 128 | +} | |
| 129 | +.tokenfield.disabled { | |
| 130 | + cursor: not-allowed; | |
| 131 | + background-color: #eeeeee; | |
| 132 | +} | |
| 133 | +.tokenfield.disabled .token-input { | |
| 134 | + cursor: not-allowed; | |
| 135 | +} | |
| 136 | +.tokenfield.disabled .token:hover { | |
| 137 | + cursor: not-allowed; | |
| 138 | + border-color: #d9d9d9; | |
| 139 | +} | |
| 140 | +.tokenfield.disabled .token:hover .close { | |
| 141 | + cursor: not-allowed; | |
| 142 | + opacity: 0.2; | |
| 143 | + filter: alpha(opacity=20); | |
| 144 | +} | |
| 145 | +.has-warning .tokenfield.focus { | |
| 146 | + border-color: #66512c; | |
| 147 | + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; | |
| 148 | + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; | |
| 149 | +} | |
| 150 | +.has-error .tokenfield.focus { | |
| 151 | + border-color: #843534; | |
| 152 | + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; | |
| 153 | + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; | |
| 154 | +} | |
| 155 | +.has-success .tokenfield.focus { | |
| 156 | + border-color: #2b542c; | |
| 157 | + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; | |
| 158 | + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; | |
| 159 | +} | |
| 160 | +.tokenfield.input-sm, | |
| 161 | +.input-group-sm .tokenfield { | |
| 162 | + min-height: 30px; | |
| 163 | + padding-bottom: 0px; | |
| 164 | +} | |
| 165 | +.input-group-sm .token, | |
| 166 | +.tokenfield.input-sm .token { | |
| 167 | + height: 20px; | |
| 168 | + margin-bottom: 4px; | |
| 169 | +} | |
| 170 | +.input-group-sm .token-input, | |
| 171 | +.tokenfield.input-sm .token-input { | |
| 172 | + height: 18px; | |
| 173 | + margin-bottom: 5px; | |
| 174 | +} | |
| 175 | +.tokenfield.input-lg, | |
| 176 | +.input-group-lg .tokenfield { | |
| 177 | + min-height: 45px; | |
| 178 | + padding-bottom: 4px; | |
| 179 | +} | |
| 180 | +.input-group-lg .token, | |
| 181 | +.tokenfield.input-lg .token { | |
| 182 | + height: 25px; | |
| 183 | +} | |
| 184 | +.input-group-lg .token-label, | |
| 185 | +.tokenfield.input-lg .token-label { | |
| 186 | + line-height: 23px; | |
| 187 | +} | |
| 188 | +.input-group-lg .token .close, | |
| 189 | +.tokenfield.input-lg .token .close { | |
| 190 | + line-height: 1.3em; | |
| 191 | +} | |
| 192 | +.input-group-lg .token-input, | |
| 193 | +.tokenfield.input-lg .token-input { | |
| 194 | + height: 23px; | |
| 195 | + line-height: 23px; | |
| 196 | + margin-bottom: 6px; | |
| 197 | + vertical-align: top; | |
| 198 | +} | |
| 199 | +.tokenfield.rtl { | |
| 200 | + direction: rtl; | |
| 201 | + text-align: right; | |
| 202 | +} | |
| 203 | +.tokenfield.rtl .token { | |
| 204 | + margin: -1px 0 5px 5px; | |
| 205 | +} | |
| 206 | +.tokenfield.rtl .token .token-label { | |
| 207 | + padding-left: 0px; | |
| 208 | + padding-right: 4px; | |
| 209 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,238 @@ |
| 1 | +@import 'base'; | |
| 2 | + | |
| 3 | +body.controller-fb_app_plugin_page_tab { | |
| 4 | + | |
| 5 | + /* Catalog Tab in Facebook */ | |
| 6 | + background: #fff; | |
| 7 | + | |
| 8 | + #top-bar, #theme-footer, #product-category, #product-page .button-bar { | |
| 9 | + display: none; | |
| 10 | + } | |
| 11 | + .navbar-static-top { | |
| 12 | + border-radius: 4px; | |
| 13 | + } | |
| 14 | + #wrap-1 { | |
| 15 | + box-shadow: none; | |
| 16 | + } | |
| 17 | + .container { | |
| 18 | + width: 100%; | |
| 19 | + } | |
| 20 | + #content .no-boxes { | |
| 21 | + padding: 0; | |
| 22 | + width: 100%; | |
| 23 | + } | |
| 24 | + #content-inner { | |
| 25 | + padding-top: 0; | |
| 26 | + } | |
| 27 | + #content h1 { | |
| 28 | + margin: 0; | |
| 29 | + } | |
| 30 | + #product-list { | |
| 31 | + margin: 0 -10px; | |
| 32 | + } | |
| 33 | + #page-tab-subtitle { | |
| 34 | + margin-bottom: 15px; | |
| 35 | + background-color: rgb(241, 255, 107); | |
| 36 | + border: 1px solid #ccc; | |
| 37 | + border-top: none; | |
| 38 | + border-radius: 0px 0px 6px 6px; | |
| 39 | + font-style: italic; | |
| 40 | + padding: 10px 10px 0px; | |
| 41 | + } | |
| 42 | + #page-tab-subtitle p { | |
| 43 | + margin-bottom: 10px; | |
| 44 | + } | |
| 45 | + #product-owner { | |
| 46 | + display:block; | |
| 47 | + font-size: 120%; | |
| 48 | + font-weight: bold; | |
| 49 | + clear: both; | |
| 50 | + } | |
| 51 | + #product-list li.product { | |
| 52 | + width: 190px; | |
| 53 | + padding: 10px; | |
| 54 | + } | |
| 55 | + #product-list .product-big { | |
| 56 | + width: 160px; | |
| 57 | + } | |
| 58 | + #product-list .product-image-link { | |
| 59 | + height: 170px; | |
| 60 | + } | |
| 61 | + #product-list .expand-box { | |
| 62 | + width: 162px; | |
| 63 | + } | |
| 64 | + #theme-footer { | |
| 65 | + border: none; | |
| 66 | + } | |
| 67 | + #page-tab-footer { | |
| 68 | + font-size: 11px; | |
| 69 | + border-top: 3px solid rgb(241, 255, 107); | |
| 70 | + margin-top: 70px; | |
| 71 | + padding-top: 5px; | |
| 72 | + } | |
| 73 | + #page-tab-footer1 { | |
| 74 | + background-size: 50px; | |
| 75 | + padding-left: 50px; | |
| 76 | + } | |
| 77 | + | |
| 78 | + /* End of Catalog Tab in Facebook */ | |
| 79 | + | |
| 80 | + #profile-title, | |
| 81 | + #profile-header, | |
| 82 | + #profile-theme-header, | |
| 83 | + #profile-footer, | |
| 84 | + #theme-header { | |
| 85 | + display: none; | |
| 86 | + } | |
| 87 | + .product-catalog-ctrl { | |
| 88 | + float: right; | |
| 89 | + margin-left: 3px; | |
| 90 | + } | |
| 91 | + #product-catalog-actions { | |
| 92 | + text-align: right; | |
| 93 | + } | |
| 94 | + #manage-fb-store-ctrl { | |
| 95 | + margin-bottom: 15px; | |
| 96 | + } | |
| 97 | + .modal-content { | |
| 98 | + #fb-app-page-tab-admin { | |
| 99 | + height: 400px; | |
| 100 | + } | |
| 101 | + } | |
| 102 | + | |
| 103 | +} | |
| 104 | + | |
| 105 | +body.controller-fb_app_plugin_page_tab, | |
| 106 | +body.controller-fb_app_plugin_myprofile { | |
| 107 | + | |
| 108 | + input.small-loading { | |
| 109 | + background: transparent url(/images/loading-small.gif) no-repeat scroll right center; | |
| 110 | + } | |
| 111 | + | |
| 112 | + .loading { | |
| 113 | + background: white url(/plugins/fb_app/images/loading.gif) no-repeat center center; | |
| 114 | + width: 80%; | |
| 115 | + height: 300px; | |
| 116 | + margin: auto; | |
| 117 | + } | |
| 118 | +} | |
| 119 | + | |
| 120 | +/* control panel - general */ | |
| 121 | +#fb-app-modal-wrap { | |
| 122 | + display: none; | |
| 123 | +} | |
| 124 | +#fb-app-error, #fb-app-modal { | |
| 125 | + padding: 70px 30px; | |
| 126 | + font-size: 120%; | |
| 127 | +} | |
| 128 | +#fb-app-error { | |
| 129 | + color: #E44444; | |
| 130 | + font-style: italic; | |
| 131 | +} | |
| 132 | +.controller-profile_editor a.control-panel-fb-app { | |
| 133 | + background-image: url(/plugins/fb_app/images/control-panel.png); | |
| 134 | +} | |
| 135 | +#fb-app-intro { | |
| 136 | + margin: 0 40px 20px 40px; | |
| 137 | +} | |
| 138 | +#fb-app-intro-text { | |
| 139 | + border:2px #666 solid; | |
| 140 | + padding: 10px; | |
| 141 | + border-radius: 8px; | |
| 142 | +} | |
| 143 | +#fb-app-connect-status { | |
| 144 | + background-color: #eee; | |
| 145 | + border: 1px solid #ccc; | |
| 146 | + margin: 30px 0; | |
| 147 | + padding: 30px; | |
| 148 | +} | |
| 149 | +.fb-app-connection-button { | |
| 150 | + margin-top: 15px; | |
| 151 | +} | |
| 152 | +#fb-app-auth { | |
| 153 | + text-align: center; | |
| 154 | + min-height: 60px; | |
| 155 | +} | |
| 156 | +#fb-connected { | |
| 157 | + font-size: 36px; | |
| 158 | + margin: 0 30px; | |
| 159 | + color: #99f; | |
| 160 | +} | |
| 161 | +#fb-app-wrapper { | |
| 162 | + padding: 40px; | |
| 163 | + font-size: 20px; | |
| 164 | +} | |
| 165 | +#fb-app-wrapper h1 { | |
| 166 | + font-size: 39px; | |
| 167 | + margin-bottom: 50px; | |
| 168 | +} | |
| 169 | + | |
| 170 | +/* Control panel - catalog settings */ | |
| 171 | +#page-tab-new { | |
| 172 | + margin-top: 30px; | |
| 173 | +} | |
| 174 | +#page-tab-new h3 { | |
| 175 | + border-top: 2px solid #ddd; | |
| 176 | + padding-top: 10px; | |
| 177 | +} | |
| 178 | +#page-tab-new h3, .edit-page-tab { | |
| 179 | + display: none; | |
| 180 | +} | |
| 181 | +.edit-page-tab { | |
| 182 | + background-color: #eee; | |
| 183 | + padding: 15px; | |
| 184 | + border-radius: 8px; | |
| 185 | +} | |
| 186 | +.edit-tab-button { | |
| 187 | + float: right; | |
| 188 | + margin-left: 10px; | |
| 189 | +} | |
| 190 | +#fb-app-timeline, #fb-app-catalogs { | |
| 191 | + border: 1px solid #999; | |
| 192 | + border-radius: 8px; | |
| 193 | + padding: 10px; | |
| 194 | +} | |
| 195 | +#content #fb-app-catalogs h3 { | |
| 196 | + font-size: 120%; | |
| 197 | + color: inherit; | |
| 198 | + margin-top: 20px; | |
| 199 | +} | |
| 200 | +#fb-app-catalogs label { | |
| 201 | + margin-top: 20px; | |
| 202 | +} | |
| 203 | +.fb-app-submit-page-tab-options { | |
| 204 | + margin-top: 20px; | |
| 205 | +} | |
| 206 | +.fb-app-final-back-button { | |
| 207 | + margin-top: 70px; | |
| 208 | +} | |
| 209 | +.tokenfield .token { | |
| 210 | + height: auto !important; | |
| 211 | +} | |
| 212 | + | |
| 213 | +@media (min-width: 768px) { | |
| 214 | + #noosfero-identity { | |
| 215 | + float: left; | |
| 216 | + } | |
| 217 | + #fb-connected { | |
| 218 | + font-size: 36px; | |
| 219 | + float:left; | |
| 220 | + margin: 0 30px; | |
| 221 | + color: #99f; | |
| 222 | + } | |
| 223 | + #fb-identity { | |
| 224 | + float: left; | |
| 225 | + } | |
| 226 | + #fb-app-timeline, #fb-app-catalogs { | |
| 227 | + width: 50%-$marginp; | |
| 228 | + float: left; | |
| 229 | + } | |
| 230 | + #fb-app-settings { | |
| 231 | + overflow: hidden; | |
| 232 | + padding-bottom: 400px; | |
| 233 | + } | |
| 234 | + #fb-app-catalogs { | |
| 235 | + margin-right: $marginp; | |
| 236 | + } | |
| 237 | +} | |
| 238 | + | ... | ... |
plugins/fb_app/public/stylesheets/tokenfield-typeahead.css
0 → 100644
| ... | ... | @@ -0,0 +1,141 @@ |
| 1 | +/*! | |
| 2 | + * bootstrap-tokenfield | |
| 3 | + * https://github.com/sliptree/bootstrap-tokenfield | |
| 4 | + * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT | |
| 5 | + */ | |
| 6 | +/* General Typeahead styling, from http://jsfiddle.net/ragulka/Dy9au/1/ */ | |
| 7 | +.twitter-typeahead { | |
| 8 | + width: 100%; | |
| 9 | + position: relative; | |
| 10 | + vertical-align: top; | |
| 11 | +} | |
| 12 | +.twitter-typeahead .tt-input, | |
| 13 | +.twitter-typeahead .tt-hint { | |
| 14 | + margin: 0; | |
| 15 | + width: 100%; | |
| 16 | + vertical-align: middle; | |
| 17 | + background-color: #ffffff; | |
| 18 | +} | |
| 19 | +.twitter-typeahead .tt-hint { | |
| 20 | + color: #999999; | |
| 21 | + z-index: 1; | |
| 22 | + border: 1px solid transparent; | |
| 23 | +} | |
| 24 | +.twitter-typeahead .tt-input { | |
| 25 | + color: #555555; | |
| 26 | + z-index: 2; | |
| 27 | +} | |
| 28 | +.twitter-typeahead .tt-input, | |
| 29 | +.twitter-typeahead .tt-hint { | |
| 30 | + height: 34px; | |
| 31 | + padding: 6px 12px; | |
| 32 | + font-size: 14px; | |
| 33 | + line-height: 1.428571429; | |
| 34 | +} | |
| 35 | +.twitter-typeahead .input-sm.tt-input, | |
| 36 | +.twitter-typeahead .hint-sm.tt-hint { | |
| 37 | + border-radius: 3px; | |
| 38 | +} | |
| 39 | +.twitter-typeahead .input-lg.tt-input, | |
| 40 | +.twitter-typeahead .hint-lg.tt-hint { | |
| 41 | + border-radius: 6px; | |
| 42 | +} | |
| 43 | +.input-group .twitter-typeahead:first-child .tt-input, | |
| 44 | +.input-group .twitter-typeahead:first-child .tt-hint { | |
| 45 | + border-radius: 4px 0 0 4px !important; | |
| 46 | +} | |
| 47 | +.input-group .twitter-typeahead:last-child .tt-input, | |
| 48 | +.input-group .twitter-typeahead:last-child .tt-hint { | |
| 49 | + border-radius: 0 4px 4px 0 !important; | |
| 50 | +} | |
| 51 | +.input-group.input-group-sm .twitter-typeahead:first-child .tt-input, | |
| 52 | +.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint { | |
| 53 | + border-radius: 3px 0 0 3px !important; | |
| 54 | +} | |
| 55 | +.input-group.input-group-sm .twitter-typeahead:last-child .tt-input, | |
| 56 | +.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint { | |
| 57 | + border-radius: 0 3px 3px 0 !important; | |
| 58 | +} | |
| 59 | +.input-sm.tt-input, | |
| 60 | +.hint-sm.tt-hint, | |
| 61 | +.input-group.input-group-sm .tt-input, | |
| 62 | +.input-group.input-group-sm .tt-hint { | |
| 63 | + height: 30px; | |
| 64 | + padding: 5px 10px; | |
| 65 | + font-size: 12px; | |
| 66 | + line-height: 1.5; | |
| 67 | +} | |
| 68 | +.input-group.input-group-lg .twitter-typeahead:first-child .tt-input, | |
| 69 | +.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint { | |
| 70 | + border-radius: 6px 0 0 6px !important; | |
| 71 | +} | |
| 72 | +.input-group.input-group-lg .twitter-typeahead:last-child .tt-input, | |
| 73 | +.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint { | |
| 74 | + border-radius: 0 6px 6px 0 !important; | |
| 75 | +} | |
| 76 | +.input-lg.tt-input, | |
| 77 | +.hint-lg.tt-hint, | |
| 78 | +.input-group.input-group-lg .tt-input, | |
| 79 | +.input-group.input-group-lg .tt-hint { | |
| 80 | + height: 45px; | |
| 81 | + padding: 10px 16px; | |
| 82 | + font-size: 18px; | |
| 83 | + line-height: 1.33; | |
| 84 | +} | |
| 85 | +.tt-dropdown-menu { | |
| 86 | + width: 100%; | |
| 87 | + min-width: 160px; | |
| 88 | + margin-top: 2px; | |
| 89 | + padding: 5px 0; | |
| 90 | + background-color: #ffffff; | |
| 91 | + border: 1px solid #ccc; | |
| 92 | + border: 1px solid rgba(0, 0, 0, 0.15); | |
| 93 | + *border-right-width: 2px; | |
| 94 | + *border-bottom-width: 2px; | |
| 95 | + border-radius: 6px; | |
| 96 | + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); | |
| 97 | + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); | |
| 98 | + -webkit-background-clip: padding-box; | |
| 99 | + -moz-background-clip: padding; | |
| 100 | + background-clip: padding-box; | |
| 101 | +} | |
| 102 | +.tt-suggestion { | |
| 103 | + display: block; | |
| 104 | + padding: 3px 20px; | |
| 105 | +} | |
| 106 | +.tt-suggestion.tt-cursor { | |
| 107 | + color: #262626; | |
| 108 | + background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); | |
| 109 | + background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); | |
| 110 | + background-repeat: repeat-x; | |
| 111 | + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); | |
| 112 | +} | |
| 113 | +.tt-suggestion.tt-cursor a { | |
| 114 | + color: #ffffff; | |
| 115 | +} | |
| 116 | +.tt-suggestion p { | |
| 117 | + margin: 0; | |
| 118 | +} | |
| 119 | +/* Tokenfield-specific Typeahead styling */ | |
| 120 | +.tokenfield .twitter-typeahead { | |
| 121 | + width: auto; | |
| 122 | +} | |
| 123 | +.tokenfield .twitter-typeahead .tt-hint { | |
| 124 | + padding: 0; | |
| 125 | + height: 20px; | |
| 126 | +} | |
| 127 | +.tokenfield.input-sm .twitter-typeahead .tt-input, | |
| 128 | +.tokenfield.input-sm .twitter-typeahead .tt-hint { | |
| 129 | + height: 18px; | |
| 130 | + font-size: 12px; | |
| 131 | + line-height: 1.5; | |
| 132 | +} | |
| 133 | +.tokenfield.input-lg .twitter-typeahead .tt-input, | |
| 134 | +.tokenfield.input-lg .twitter-typeahead .tt-hint { | |
| 135 | + height: 23px; | |
| 136 | + font-size: 18px; | |
| 137 | + line-height: 1.33; | |
| 138 | +} | |
| 139 | +.tokenfield .twitter-typeahead .tt-suggestions { | |
| 140 | + font-size: 14px; | |
| 141 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,39 @@ |
| 1 | += content_for :head do | |
| 2 | + = javascript_include_tag 'typeahead.bundle.js' | |
| 3 | + = stylesheet_link_tag 'typeahead' | |
| 4 | + = stylesheet_link_tag 'plugins/fb_app/stylesheets/bootstrap-tokenfield.css' | |
| 5 | + = stylesheet_link_tag 'plugins/fb_app/stylesheets/tokenfield-typeahead.css' | |
| 6 | + = javascript_include_tag 'plugins/fb_app/javascripts/bootstrap-tokenfield.js' | |
| 7 | + = javascript_include_tag 'plugins/open_graph/javascripts/open_graph.js' | |
| 8 | + | |
| 9 | +- callback = '' unless defined? callback | |
| 10 | + | |
| 11 | +#fb-root | |
| 12 | +#fb-app-modal-wrap style="display:none" | |
| 13 | + #fb-app-modal | |
| 14 | + #fb-app-modal-intro | |
| 15 | + = button_to_function 'cancel', '', "ff()", class: 'modal-button-no' | |
| 16 | + = button_to_function 'ok', '', "ff()", class: 'modal-button-yes' | |
| 17 | + | |
| 18 | +javascript: | |
| 19 | + // Adding locales: | |
| 20 | + fb_app.locales.error_empty_title = #{t('fb_app_plugin.views.myprofile.error.empty_title').to_json} | |
| 21 | + fb_app.locales.error_empty_settings = #{t('fb_app_plugin.views.myprofile.error.empty_settings').to_json} | |
| 22 | + fb_app.locales.cancel_button = #{t('fb_app_plugin.views.myprofile.catalogs.cancel_button').to_json} | |
| 23 | + fb_app.locales.confirm_removal = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal').to_json} | |
| 24 | + fb_app.locales.confirm_removal_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_removal_button').to_json} | |
| 25 | + fb_app.locales.confirm_disconnect = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect').to_json} | |
| 26 | + fb_app.locales.confirm_disconnect_button = #{t('fb_app_plugin.views.myprofile.catalogs.confirm_disconnect_button').to_json} | |
| 27 | + | |
| 28 | + // General settings: | |
| 29 | + fb_app.current_url = #{url_for(params).to_json}; | |
| 30 | + fb_app.base_url = 'https://#{environment.default_hostname}/plugin/fb_app'; | |
| 31 | + | |
| 32 | + fb_app.page_tab.appId = #{FbAppPlugin.page_tab_app_credentials[:id].to_json}, | |
| 33 | + fb_app.timeline.appId = #{FbAppPlugin.timeline_app_credentials[:id].to_json}, | |
| 34 | + fb_app.page_tab.nextUrl = #{url_for(protocol: 'https', only_path: false).to_json} | |
| 35 | + | |
| 36 | + fb_app.fb.prepareAsyncInit(fb_app.timeline.appId, #{callback.to_json}); | |
| 37 | + fb_app.fb.init(); | |
| 38 | +/ must come after window.fbAsyncInit is defined | |
| 39 | += javascript_include_tag "https://connect.facebook.net/en_US/all.js" | ... | ... |
plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_myprofile/_auth.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | +- if @auth.connected? | |
| 2 | + #noosfero-identity | |
| 3 | + = profile_image profile | |
| 4 | + = profile.name | |
| 5 | + span#fb-connected.fa.fa-arrows-h | |
| 6 | + #fb-identity | |
| 7 | + = render 'identity', auth: @auth | |
| 8 | + | |
| 9 | + = button_to_function 'close', t('fb_app_plugin.views.myprofile.disconnect'), 'fb_app.timeline.disconnect()', class:'fb-app-connection-button' | |
| 10 | + | |
| 11 | +- elsif @auth.not_authorized? | |
| 12 | + = button_to_function 'login', t('fb_app_plugin.views.myprofile.connect'), 'fb_app.timeline.connect()', size: '', option: 'primary', class:'fb-app-connection-button' | |
| 13 | +- elsif @auth.expired? | |
| 14 | + = button_to_function 'login', t('fb_app_plugin.views.myprofile.reconnect'), 'fb_app.timeline.reconnect()', class:'fb-app-connection-button' | ... | ... |
plugins/fb_app/views/fb_app_plugin_myprofile/_catalogs.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | +h2 | |
| 2 | + = t'fb_app_plugin.views.myprofile.catalogs.heading' | |
| 3 | + = render 'catalogs_help' rescue nil | |
| 4 | + | |
| 5 | +- profile.fb_app_page_tabs.each do |page_tab| | |
| 6 | + = render 'fb_app_plugin_page_tab/config', page_tab: page_tab | |
| 7 | + | |
| 8 | +#new-catalog | |
| 9 | + = render 'fb_app_plugin_page_tab/config', page_tab: profile.fb_app_page_tabs.build(owner_profile: profile) | |
| 10 | + | |
| 11 | += render file: 'shared/tiny_mce', locals: {mode: 'simple'} | ... | ... |
plugins/fb_app/views/fb_app_plugin_myprofile/_identity.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_myprofile/_load.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | += render 'fb_app_plugin/load', callback: 'fb_app.fb.checkLoginStatus()' | |
| 2 | + | |
| 3 | +javascript: | |
| 4 | + fb_app.config.url_prefix = #{url_for(action: :index).to_json} | |
| 5 | + fb_app.config.save_auth_url = #{url_for(action: :save_auth).to_json} | |
| 6 | + fb_app.config.show_login_url = #{url_for(action: :show_login).to_json} | |
| 7 | + | ... | ... |
plugins/fb_app/views/fb_app_plugin_myprofile/_settings.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | +h1= t'fb_app_plugin.lib.plugin.name' | |
| 2 | += button :back, _('Back to control panel'), controller: 'profile_editor' | |
| 3 | + | |
| 4 | +#fb-app-connect-status | |
| 5 | + = render 'intro' rescue nil if @auth.not_authorized? | |
| 6 | + #fb-app-auth | |
| 7 | + = render 'auth' | |
| 8 | + | |
| 9 | +- if @auth.connected? or Rails.env.development? | |
| 10 | + #fb-app-catalogs | |
| 11 | + = render 'catalogs' | |
| 12 | + #fb-app-timeline | |
| 13 | + - if profile.person? | |
| 14 | + h2= t'fb_app_plugin.views.myprofile.timeline.heading' | |
| 15 | + | |
| 16 | + - unless FbAppPlugin.test_user? user | |
| 17 | + h3= t'fb_app_plugin.views.myprofile.timeline.explanation_title' | |
| 18 | + p= t'fb_app_plugin.views.myprofile.timeline.explanation_text' | |
| 19 | + - else | |
| 20 | + #track-form | |
| 21 | + = render 'track_form', context: :fb_app | |
| 22 | + - else | |
| 23 | + = t'fb_app_plugin.views.myprofile.timeline.organization_redirect', | |
| 24 | + type: t("fb_app_plugin.views.myprofile.timeline.organization_from_#{profile.class.name.underscore}"), | |
| 25 | + redirect_link: link_to(t('fb_app_plugin.views.myprofile.timeline.redirect_link'), host: FbAppPlugin.config[:app][:domain], profile: user.identifier, controller: :fb_app_plugin_myprofile) | |
| 26 | +.clean | |
| 27 | + | |
| 28 | += button :back, _('Back to control panel'), {controller: 'profile_editor'}, class: 'fb-app-final-back-button' | ... | ... |
plugins/fb_app/views/fb_app_plugin_myprofile/index.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_config.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | +.page-tab id="page-tab-#{page_tab.id || 'new'}" | |
| 2 | + - if page_tab.page_id | |
| 3 | + h3 | |
| 4 | + = page_tab.title | |
| 5 | + = button_to_function_without_text 'edit', t('fb_app_plugin.views.myprofile.catalogs.edit_button') % {catalog_title: page_tab.title}, | |
| 6 | + "fb_app.page_tab.config.edit($(this))", class: 'edit-tab-button' | |
| 7 | + = button_to_function_without_text 'remove', t('fb_app_plugin.views.myprofile.catalogs.remove_button') % {catalog_title: page_tab.title}, | |
| 8 | + "fb_app.page_tab.config.remove($(this), '#{url_for(controller: :fb_app_plugin_page_tab, action: :destroy, id: page_tab.id)}')", class: 'edit-tab-button' | |
| 9 | + = button_without_text 'eyes', t('fb_app_plugin.views.myprofile.catalog.see_page'), page_tab.facebook_url, target: '_blank', class: 'edit-tab-button' | |
| 10 | + - else | |
| 11 | + = button_to_function 'add', t('fb_app_plugin.views.myprofile.catalogs.new'), "$(this).toggle(); $('#page-tab-new h3, #add_tab').toggle(400)" | |
| 12 | + | |
| 13 | + = render 'fb_app_plugin_page_tab/configure_form', page_tab: page_tab, signed_request: nil | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_button.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | +#manage-fb-store-ctrl | |
| 2 | + - if @page_tab.owner_profile | |
| 3 | + - if logged_in? and (user.is_admin? environment or user.is_admin? @page_tab.owner_profile) | |
| 4 | + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_myprofile, profile: @page_tab.owner_profile.identifier}, | |
| 5 | + target: '_parent' | |
| 6 | + - elsif (@data[:page][:admin] rescue false) | |
| 7 | + = button :edit, t('fb_app_plugin.views.page_tab.edit_catalog'), {controller: :fb_app_plugin_page_tab, action: :admin, signed_request: @signed_requests, page_id: @page_ids}, onclick: "noosfero.modal.url(this.href); return false;" | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_form.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,37 @@ |
| 1 | +- form_id = if page_tab.id then "edit_tab_#{page_tab.id}" else "add_tab" end | |
| 2 | + | |
| 3 | +- unless page_tab.id | |
| 4 | + h3= t'fb_app_plugin.views.myprofile.catalogs.new' | |
| 5 | + | |
| 6 | += form_for page_tab, as: :page_tab, url: {controller: :fb_app_plugin_page_tab, action: :admin}, | |
| 7 | + html: {id: form_id, class: "edit-page-tab", onsubmit: "return fb_app.page_tab.config.save($(this))"} do |f| | |
| 8 | + | |
| 9 | + = hidden_field_tag :signed_request, signed_request | |
| 10 | + = hidden_field_tag :page_id, page_tab.page_id | |
| 11 | + = f.hidden_field :profile_id, value: profile.id | |
| 12 | + = f.hidden_field :page_id | |
| 13 | + | |
| 14 | + = f.label :title, t("fb_app_plugin.views.myprofile.catalogs.catalog_title_label") | |
| 15 | + = f.text_field :title, class: 'form-control' | |
| 16 | + | |
| 17 | + = f.label :subtitle, t("fb_app_plugin.views.myprofile.catalogs.catalog_subtitle_label") | |
| 18 | + = f.text_area :subtitle, class: 'form-control mceEditor', id: "page-tab-subtitle-#{page_tab.id}" | |
| 19 | + | |
| 20 | + = f.label :config_type, t("fb_app_plugin.views.myprofile.catalogs.catalog_type_chooser_label") | |
| 21 | + = f.select :config_type, | |
| 22 | + page_tab.types.map{ |type| [t("fb_app_plugin.models.page_tab.types.#{if profile.enterprise? and type == :profile then :other_profile else type end}"), type] }, | |
| 23 | + {}, onchange: 'fb_app.page_tab.config.change_type($(this))', class: 'form-control' | |
| 24 | + | |
| 25 | + - page_tab.types.each do |type| | |
| 26 | + div class="config-type config-type-#{type}" | |
| 27 | + = render "fb_app_plugin_page_tab/configure_#{type}", f: f, page_tab: page_tab | |
| 28 | + | |
| 29 | + - if page_tab.new_record? | |
| 30 | + = submit_button :add, t('fb_app_plugin.views.page_tab.add'), onclick: 'return fb_app.page_tab.config.add($(this.form))', class: 'fb-app-submit-page-tab-options' | |
| 31 | + - else | |
| 32 | + = submit_button :save, t('fb_app_plugin.views.page_tab.save'), class: 'fb-app-submit-page-tab-options' | |
| 33 | + | |
| 34 | +javascript: | |
| 35 | + $('document').ready(function() { | |
| 36 | + fb_app.page_tab.config.init(); | |
| 37 | + }); | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_own_profile.html.slim
0 → 100644
No preview for this file type
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profile.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | += f.label t("fb_app_plugin.views.myprofile.catalogs.profile_chooser_label") | |
| 2 | += f.text_field :profile_ids, "data-limit" => "1", "data-placeholder" => t('fb_app_plugin.views.page_tab.profile.placeholder'), onchange: 'fb_app.page_tab.config.profile.onchange($(this))' | |
| 3 | + | |
| 4 | +javascript: | |
| 5 | + $(document).ready(function() { | |
| 6 | + var selector = '#page-tab-#{page_tab.id || 'new'} .config-type-profile #page_tab_profile_ids' | |
| 7 | + | |
| 8 | + fb_app.page_tab.config.profile.onchange($(selector)) | |
| 9 | + | |
| 10 | + open_graph.autocomplete.init( | |
| 11 | + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json}, | |
| 12 | + selector, | |
| 13 | + #{[page_tab.profile].compact.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json}, | |
| 14 | + {tokenfield: {limit: 1}} | |
| 15 | + ) | |
| 16 | + | |
| 17 | + if (#{page_tab.profile.present?.to_json}) | |
| 18 | + $(selector+'-tokenfield').hide() | |
| 19 | + | |
| 20 | + $(selector) | |
| 21 | + .on('tokenfield:createdtoken', function (e) { | |
| 22 | + $(selector+'-tokenfield').hide(); | |
| 23 | + }) | |
| 24 | + .on('tokenfield:removedtoken', function (e) { | |
| 25 | + $(selector+'-tokenfield').show(); | |
| 26 | + }) | |
| 27 | + }) | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_profiles.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | += f.label t("fb_app_plugin.views.myprofile.catalogs.profiles_chooser_label") | |
| 2 | += f.text_field :profile_ids, placeholder: t('fb_app_plugin.views.page_tab.profiles.placeholder') | |
| 3 | + | |
| 4 | +javascript: | |
| 5 | + $(document).ready(function() { | |
| 6 | + open_graph.autocomplete.init( | |
| 7 | + #{url_for(controller: :fb_app_plugin_page_tab, action: :enterprise_search).to_json}, | |
| 8 | + '#page-tab-#{page_tab.id || 'new'} .config-type-profiles #page_tab_profile_ids', | |
| 9 | + #{page_tab.profiles.map{ |p| {value: p.id, label: render('open_graph_plugin/myprofile/ac_profile', profile: p), } }.to_json} | |
| 10 | + ) | |
| 11 | + }) | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/_configure_query.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/catalog.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | +#fb-app-catalog-wrapper class=('fb-app-standalone' if @signed_request.blank?) | |
| 2 | + | |
| 3 | + = render 'load' | |
| 4 | + = render 'title_and_subtitle' | |
| 5 | + | |
| 6 | + #product-catalog | |
| 7 | + #product-catalog-actions | |
| 8 | + - if @page_tab.config_type == :profile | |
| 9 | + - if profile and user.present? and (user.is_admin?(environment) or user.is_admin?(profile)) | |
| 10 | + .product-catalog-ctrl | |
| 11 | + = button :add, _('Add product or service'), controller: :manage_products, action: :new, profile: profile.identifier | |
| 12 | + = render 'configure_button' | |
| 13 | + = content_for :product_page do | |
| 14 | + = render 'catalog/results' | |
| 15 | + = render 'catalog/search' | |
| 16 | + = render 'catalog/javascripts', external: false | |
| 17 | + | |
| 18 | + = render 'footer' | |
| 19 | + | |
| 20 | + javascript: | |
| 21 | + catalog.base_url_path = #{url_for(controller: :fb_app_plugin_page_tab, action: :index, page_id: params[:page_id], signed_request: params[:signed_request]).to_json} + '&' | ... | ... |
plugins/fb_app/views/fb_app_plugin_page_tab/first_load.html.slim
0 → 100644
plugins/fb_app/views/fb_app_plugin_page_tab/product.html.slim
0 → 100644
| ... | ... | @@ -0,0 +1,11 @@ |
| 1 | += render 'title_and_subtitle' | |
| 2 | + | |
| 3 | += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: '' | |
| 4 | + | |
| 5 | += render file: "#{Rails.root}/app/views/manage_products/show.html.erb" | |
| 6 | + | |
| 7 | += button :back, t('fb_app_plugin.views.page_tab.back_to_catalog'), params.except(:product_id), target: '' | |
| 8 | + | |
| 9 | += render 'footer' | |
| 10 | + | |
| 11 | += render 'load' | ... | ... |