Commit 69b83fad20c263cdf8b4ea41290cfcbde0a869fa

Authored by Braulio Bhavamitra
1 parent 3fdbbc29

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
plugins/fb_app/Gemfile 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +gem 'slim'
  2 +
  3 +# for backwards compatibility of serialized objects
  4 +gem 'fb_graph'
  5 +
  6 +gem 'fb_graph2'
  7 +
  8 +gem 'facebook-signed-request'
  9 +
  10 +
... ...
plugins/fb_app/config.yml.dist 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/install.rb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +system "script/noosfero-plugins -q enable oauth_client"
  2 +system "script/noosfero-plugins -q enable open_graph"
  3 +
... ...
plugins/fb_app/lib/ext/profile.rb 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/lib/fb_app_plugin.rb 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/lib/fb_app_plugin/base.rb 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/lib/fb_app_plugin/display_helper.rb 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/lib/fb_app_plugin/link_renderer.rb 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/lib/fb_app_plugin/publisher.rb 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/lib/fb_app_plugin/settings.rb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +class FbAppPlugin::Settings < OpenGraphPlugin::Settings
  2 +
  3 +end
  4 +
... ...
plugins/fb_app/locales/en-US.yml 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/locales/pt-BR.yml 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/models/fb_app_plugin/activity.rb 0 → 100644
... ... @@ -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
... ...
plugins/fb_app/models/fb_app_plugin/auth.rb 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/models/fb_app_plugin/page_tab.rb 0 → 100644
... ... @@ -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
... ... @@ -0,0 +1,12 @@
  1 +require_dependency 'action_tracker_model'
  2 +
  3 +class ActionTracker::Record
  4 +
  5 + after_create :fb_app_publish
  6 +
  7 + protected
  8 +
  9 + def fb_app_publish
  10 + raise 'here'
  11 + end
  12 +end
... ...
plugins/fb_app/public/images/FB-f-Logo__blue_48.png 0 → 100644

722 Bytes

plugins/fb_app/public/images/cirandasnoface.png 0 → 100644

3.31 KB

plugins/fb_app/public/images/control-panel.png 0 → 120000
... ... @@ -0,0 +1 @@
  1 +FB-f-Logo__blue_48.png
0 2 \ No newline at end of file
... ...
plugins/fb_app/public/images/loading.gif 0 → 100644

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">&times;</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 +}));
... ...
plugins/fb_app/public/javascripts/fb_app.js 0 → 100644
... ... @@ -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 +
... ...
plugins/fb_app/public/style.scss 0 → 120000
... ... @@ -0,0 +1 @@
  1 +stylesheets/style.scss
0 2 \ No newline at end of file
... ...
plugins/fb_app/public/stylesheets/_base.scss 0 → 100644
... ... @@ -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 +}
... ...
plugins/fb_app/public/stylesheets/style.scss 0 → 100644
... ... @@ -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 +}
... ...
plugins/fb_app/views/fb_app_plugin/_load.html.slim 0 → 100644
... ... @@ -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/index.html.erb 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +<div id="fb-app-wrapper">
  2 +</div>
  3 +
  4 +<%# render 'load' %>
... ...
plugins/fb_app/views/fb_app_plugin_layouts/default.html.slim 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +html
  2 + head
  3 + = noosfero_javascript
  4 + = javascript_include_tag 'fb_app'
  5 + = noosfero_stylesheets
  6 + = h stylesheet_link_tag(jquery_ui_theme_stylesheet_path)
  7 + body
  8 + = yield
... ...
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
... ... @@ -0,0 +1,7 @@
  1 +.fb-app-identity
  2 + - picture = auth.fb_user.picture
  3 + / fb_graph version 1 compatibility
  4 + - url = if picture.respond_to? :url then picture.url else picture end
  5 + = image_tag url
  6 + = auth.fb_user.name
  7 +
... ...
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
... ... @@ -0,0 +1,6 @@
  1 += render 'load'
  2 +javascript:
  3 + fb_app.auth.status = #{(@auth.status rescue FbAppPlugin::Auth::Status::NotAuthorized).to_json}
  4 +
  5 +#fb-app-settings
  6 + = render 'settings'
... ...
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
... ... @@ -0,0 +1,4 @@
  1 += f.label t("fb_app_plugin.views.myprofile.catalogs.query_label")
  2 +p= t'fb_app_plugin.views.myprofile.catalogs.query_help'
  3 += f.text_field :query, placeholder: t('fb_app_plugin.views.page_tab.query.placeholder'), class: 'form-control'
  4 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_footer.html.slim 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +#page-tab-footer
  2 + #page-tab-footer1.col-lg-6.col-md-6.col-sm-6.text-left
  3 + = t'fb_app_plugin.views.page_tab.footer1'
  4 + #page-tab-footer2.col-lg-6.col-md-6.col-sm-6.text-right
  5 + = t'fb_app_plugin.views.page_tab.footer2'
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_load.html.slim 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +- callback = '' unless defined? callback
  2 +
  3 += render 'fb_app_plugin/load', callback: callback
  4 +
  5 +javascript:
  6 + fb_app.page_tab.init();
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/_title_and_subtitle.html.slim 0 → 100644
... ... @@ -0,0 +1,4 @@
  1 +h1= @page_tab.title
  2 +
  3 +- if @page_tab.subtitle.present?
  4 + #page-tab-subtitle= @page_tab.subtitle
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/admin.html.slim 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +#fb-app-page-tab-admin
  2 + = render 'admin_intro' rescue nil
  3 +
  4 + = render 'config', page_tab: @page_tab, page_id: @page_id, signed_request: @signed_request
  5 + = render file: 'shared/tiny_mce', locals: {mode: 'simple'}
  6 +
... ...
plugins/fb_app/views/fb_app_plugin_page_tab/admin.js.erb 0 → 100644
... ... @@ -0,0 +1,2 @@
  1 +fb_app.page_tab.config.close(<%= @page_id.to_json %>);
  2 +
... ...
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
... ... @@ -0,0 +1,3 @@
  1 +javascript:
  2 + noosfero.modal.url(#{url_for(controller: :fb_app_plugin, action: :admin, page_id: @page_ids).to_json})
  3 +
... ...
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'
... ...