paypal support

This commit is contained in:
Seth Call 2016-12-15 12:47:08 -06:00
parent 458637b1de
commit 5e04f72d8e
36 changed files with 1668 additions and 703 deletions

View File

@ -369,4 +369,5 @@ second_ed.sql
second_ed_v2.sql
retailers_v2.sql
retailer_interest.sql
connection_role.sql
connection_role.sql
retailer_payment_split.sql

View File

@ -0,0 +1,2 @@
ALTER TABLE retailers ADD COLUMN payment VARCHAR;
ALTER TABLE lesson_bookings ADD COLUMN payment VARCHAR;

View File

@ -40,6 +40,7 @@ require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/errors/jam_record_not_found"
require "jam_ruby/errors/conflict_error"
require "jam_ruby/errors/pay_pal_client_error"
require "jam_ruby/lib/app_config"
require "jam_ruby/lib/s3_manager_mixin"
require "jam_ruby/lib/s3_public_manager_mixin"

View File

@ -0,0 +1,19 @@
module JamRuby
class PayPalClientError < StandardError
attr_accessor :errors
def initialize(data)
if data.respond_to?('has_key?')
self.errors = data
else
self.errors = {:message=>data.to_s}
end
end # initialize
def to_s
s=super
s << ", errors: #{errors.inspect}" if self.errors.any?
s
end
end
end

View File

@ -531,7 +531,7 @@ module JamRuby
distribution = teacher_distribution_price_in_cents(target)
if education
(distribution * 0.0625).round
(distribution * 0.0625).round # 0.0625 is 1/4th of 25%
else
distribution
end

View File

@ -216,11 +216,20 @@ module JamRuby
self.status = STATUS_COMPLETED
# RETAILERPAY2
if success && lesson_booking.requires_teacher_distribution?(self)
is_education_school_on_school = lesson_booking.school_on_school_payment?
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, false)
if lesson_booking.school_on_school_payment?
if is_education_school_on_school
self.teacher_distributions << TeacherDistribution.create_for_lesson(self, true)
end
# this is a bit of a hack, in how the code is structured.
# but basically, the distributions calculated are too dynamic for the above code.
# if this is a retailer
end
if self.save

View File

@ -9,6 +9,7 @@ module JamRuby
SOURCE_RECURLY = 'recurly'
SOURCE_IOS = 'ios'
SOURCE_PAYPAL = 'paypal'
belongs_to :retailer, class_name: 'JamRuby::Retailer'
belongs_to :user, class_name: 'JamRuby::User'
@ -166,7 +167,7 @@ module JamRuby
# individual subscriptions will end up create their own sale (you can't have N subscriptions in one sale--recurly limitation)
# jamtracks however can be piled onto the same sale as adjustments (VRFS-3028)
# so this method may create 1 or more sales, , where 2 or more sales can occur if there are more than one subscriptions or subscription + jamtrack
def self.place_order(current_user, shopping_carts)
def self.place_order(current_user, shopping_carts, paypal = false)
sales = []
@ -176,7 +177,7 @@ module JamRuby
# return sales
#end
jam_track_sale = order_jam_tracks(current_user, shopping_carts)
jam_track_sale = order_jam_tracks(current_user, shopping_carts, paypal)
sales << jam_track_sale if jam_track_sale
# TODO: process shopping_carts_subscriptions
@ -377,7 +378,7 @@ module JamRuby
# this method will either return a valid sale, or throw a RecurlyClientError or ActiveRecord validation error (save! failed)
# it may return an nil sale if the JamTrack(s) specified by the shopping carts are already owned
def self.order_jam_tracks(current_user, shopping_carts)
def self.order_jam_tracks(current_user, shopping_carts, is_paypal)
shopping_carts_jam_tracks = []
shopping_carts_subscriptions = []
@ -395,11 +396,9 @@ module JamRuby
end
end
client = RecurlyClient.new
sale = nil
Sale.transaction do
sale = create_jam_track_sale(current_user, SOURCE_RECURLY)
sale = create_jam_track_sale(current_user, is_paypal ? SOURCE_PAYPAL : SOURCE_RECURLY)
if sale.valid?
if is_only_freebie(shopping_carts)
@ -429,61 +428,142 @@ module JamRuby
else
account = client.get_account(current_user)
if account.present?
if is_paypal
purge_pending_adjustments(account)
sale.process_shopping_carts(current_user, shopping_carts)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
paypal_auth = current_user.paypal_auth
# now invoice the sale ... almost done
@api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
@@log.info("User #{current_user.email}, GetExpressCheckout: #{@response.inspect}")
tax = false
if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
end
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@do_express_checkout_payment = @api.build_do_express_checkout_payment({
:DoExpressCheckoutPaymentRequestDetails => {
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
],
:Token => paypal_auth.token,
:PayerID => paypal_auth.uid, }})
@pay_response = @api.do_express_checkout_payment(@do_express_checkout_payment)
@@log.info("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
# #<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseType:0x007fe511dd9b88
# @Timestamp=Sun, 11 Dec 2016 02:09:31 +0000, @Ack="Success",
# @CorrelationID="b28faf6bd90d9", @Version="117.0", @Build="000000",
# @DoExpressCheckoutPaymentResponseDetails=#<PayPal::SDK::Merchant::DataTypes::DoExpressCheckoutPaymentResponseDetailsType:0x007fe511dd38c8
# @Token="EC-7A4606566T700564B",
# @PaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::PaymentInfoType:0x007fe511dd3008 @TransactionID="63C410710F2619403", @ParentTransactionID=nil,
# @ReceiptID=nil, @TransactionType="express-checkout", @PaymentType="instant", @PaymentDate=Sun, 11 Dec 2016 02:09:31 +0000,
# @GrossAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd0c90 @currencyID="USD", @value="1.99">,
# @FeeAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dd04e8 @currencyID="USD", @value="0.36">,
# @TaxAmount=#<PayPal::SDK::Merchant::DataTypes::BasicAmountType:0x007fe511dcbe98 @currencyID="USD", @value="0.00">,
# @ExchangeRate=nil, @PaymentStatus="Completed", @PendingReason="none", @ReasonCode="none", @ProtectionEligibility="Eligible",
# @ProtectionEligibilityType="ItemNotReceivedEligible,UnauthorizedPaymentEligible", @SellerDetails=#<PayPal::SDK::Merchant::DataTypes::SellerDetailsType:0x007fe511dcb358 @SecureMerchantAccountID="6MB486RSBRMJ2">>],
# @SuccessPageRedirectRequested="false", @CoupledPaymentInfo=[#<PayPal::SDK::Merchant::DataTypes::CoupledPaymentInfoType:0x007fe511dca7a0>]>>
if @pay_response.Ack == 'Success'
details = @pay_response.DoExpressCheckoutPaymentResponseDetails.PaymentInfo[0]
sale.recurly_invoice_id = details.TransactionID
sale.recurly_invoice_number = details.ReceiptID
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
sale.recurly_subtotal_in_cents = ((details.GrossAmount.value.to_f - details.TaxAmount.value.to_f) * 100).to_i
sale.recurly_tax_in_cents = (details.TaxAmount.value.to_f * 100).to_i
sale.recurly_total_in_cents = (details.GrossAmount.value.to_f * 100).to_i
sale.recurly_currency = details.GrossAmount.currencyID
unless sale.save
puts "WTF"
raise RecurlyClientError, "Invalid sale (at end)."
puts "Invalid sale (at end)."
raise PayPalClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
rescue => e
puts "UNKNOWN E #{e}"
else
@@log.error("User #{current_user.email}, DoExpressCheckoutPayment: #{@pay_response.inspect}")
raise PayPalClientError, @pay_response.Errors[0].LongMessage
end
else
raise RecurlyClientError, "Could not find account to place order."
client = RecurlyClient.new
account = client.get_account(current_user)
if account.present?
purge_pending_adjustments(account)
created_adjustments = sale.process_shopping_carts(current_user, shopping_carts, account)
# now invoice the sale ... almost done
begin
invoice = account.invoice!
sale.recurly_invoice_id = invoice.uuid
sale.recurly_invoice_number = invoice.invoice_number
# now slap in all the real tax/purchase totals
sale.recurly_subtotal_in_cents = invoice.subtotal_in_cents
sale.recurly_tax_in_cents = invoice.tax_in_cents
sale.recurly_total_in_cents = invoice.total_in_cents
sale.recurly_currency = invoice.currency
# and resolve against sale_line_items
sale.sale_line_items.each do |sale_line_item|
found_line_item = false
invoice.line_items.each do |line_item|
if line_item.uuid == sale_line_item.recurly_adjustment_uuid
sale_line_item.recurly_tax_in_cents = line_item.tax_in_cents
sale_line_item.recurly_total_in_cents =line_item.total_in_cents
sale_line_item.recurly_currency = line_item.currency
sale_line_item.recurly_discount_in_cents = line_item.discount_in_cents
found_line_item = true
break
end
end
if !found_line_item
@@log.error("can't find line item #{sale_line_item.recurly_adjustment_uuid}")
puts "CANT FIND LINE ITEM"
end
end
unless sale.save
puts "Invalid sale (at end)."
raise RecurlyClientError, "Invalid sale (at end)."
end
rescue Recurly::Resource::Invalid => e
# this exception is thrown by invoice! if the invoice is invalid
sale.rollback_adjustments(current_user, created_adjustments)
sale = nil
raise ActiveRecord::Rollback # kill all db activity, but don't break outside logic
rescue => e
puts "UNKNOWN E #{e}"
end
else
raise RecurlyClientError, "Could not find account to place order."
end
end
end
else
@ -493,7 +573,7 @@ module JamRuby
sale
end
def process_shopping_carts(current_user, shopping_carts, account)
def process_shopping_carts(current_user, shopping_carts, account = nil)
created_adjustments = []
@ -515,7 +595,7 @@ module JamRuby
end
def process_shopping_cart(current_user, shopping_cart, account, created_adjustments)
def process_shopping_cart(current_user, shopping_cart, recurly_account, created_adjustments)
recurly_adjustment_uuid = nil
recurly_adjustment_credit_uuid = nil
@ -536,14 +616,14 @@ module JamRuby
end
if account
if recurly_account
# ask the shopping cart to create the correct Recurly adjustment attributes for a JamTrack
adjustments = shopping_cart.create_adjustment_attributes(current_user)
adjustments.each do |adjustment|
# create the adjustment at Recurly (this may not look like it, but it is a REST API)
created_adjustment = account.adjustments.new(adjustment)
created_adjustment = recurly_account.adjustments.new(adjustment)
created_adjustment.save
# if the adjustment could not be made, bail

View File

@ -1912,6 +1912,13 @@ module JamRuby
stats
end
def shopping_cart_total
total = 0
shopping_carts.each do |shopping_cart|
total += shopping_cart.product_info[:total_price]
end
total
end
def destroy_all_shopping_carts
ShoppingCart.where("user_id=?", self).destroy_all
end
@ -2053,6 +2060,15 @@ module JamRuby
user_authorizations.where(provider: "stripe_connect").first
end
def paypal_auth
user_authorizations.where(provider: 'paypal').first
end
def has_paypal_auth?
auth = paypal_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now)
end
def has_stripe_connect?
auth = stripe_auth
auth && (!auth.token_expiration || auth.token_expiration > Time.now)

View File

@ -28,6 +28,8 @@ gem 'sprockets-rails', '2.3.2'
gem 'non-stupid-digest-assets'
#gem 'license_finder'
gem 'pg_migrate', '0.1.14'
#gem 'paypal-sdk-rest'
gem 'paypal-sdk-merchant', github: 'sylv3rblade/merchant-sdk-ruby'
gem 'kickbox'
gem 'oj', '2.10.2'
gem 'builder'

View File

@ -27,7 +27,7 @@
//= require jquery.Jcrop
//= require jquery.naturalsize
//= require jquery.queryparams
//= require jquery.clipboard
//= require clipboard
//= require jquery.timeago
//= require jquery.easydropdown
//= require jquery.scrollTo

View File

@ -11,6 +11,7 @@
var userDetail = null;
var entity = null;
var remainingCap = 140 - 22 - 1; // 140 tweet max, minus 22 for link size, minus 1 for space
var clipboard = null;
function showSpinner() {
$(dialogId + ' .dialog-inner').hide();
@ -444,27 +445,6 @@
function afterShow() {
$("#shareType").text(entityType);
if(context.JK.hasFlash()) {
$("#btn-share-copy").clipboard({
path: '/assets/jquery.clipboard.swf',
copy: function() {
// Return text in closest element (useful when you have multiple boxes that can be copied)
return $(".link-contents").text();
}
});
}
else {
if(context.jamClient) {
// uses bridge call to ultimately access QClipboard
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($(".link-contents").text());
return false;
})
}
else {
logger.debug("no copy-to-clipboard capabilities")
}
}
}
function afterHide() {
@ -486,6 +466,20 @@
//initDialog();
facebookHelper.deferredLoginStatus().done(function(response) { handleFbStateChange(response); });
if(context.jamClient.IsNativeClient()) {
$("#btn-share-copy").unbind('click').click(function() {
context.jamClient.SaveToClipboard($("#link-contents").text());
return false;
})
}
else {
clipboard = new Clipboard('#btn-share-copy', {
text: function(trigger) {
return $("#link-contents").text();
}
})
}
}
this.initialize = initialize;

View File

@ -2734,6 +2734,27 @@
})
}
function paypalDetail(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/detail',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function paypalPlaceOrder(options) {
options = options || {}
return $.ajax({
type: 'POST',
url: '/api/paypal/checkout/confirm',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify(options)
})
}
function initialize() {
return self;
@ -2977,6 +2998,8 @@
this.posaActivate = posaActivate;
this.posaClaim = posaClaim;
this.sendRetailerCustomerEmail = sendRetailerCustomerEmail;
this.paypalDetail = paypalDetail;
this.paypalPlaceOrder = paypalPlaceOrder;
return this;
};
})(window,jQuery);

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams
//= require jquery.hoverIntent
//= require jquery.cookie
//= require jquery.clipboard
//= require clipboard
//= require jquery.easydropdown
//= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9

View File

@ -213,7 +213,7 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
)
.fail(() =>
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false})
)

View File

@ -326,12 +326,12 @@ MIX_MODES = context.JK.MIX_MODES
.done((response) =>
@setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count})
)
.fail(() =>
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false})
)
)
.fail(() =>
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Search Unavailable'
@setState({searching: false, first_search: false})
)

View File

@ -0,0 +1,160 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@PayPalConfirmationScreen = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
content = null
if this.state.sold
if context.jamClient && context.jamClient.IsNativeClient()
platformMessage = `<div>
<p> To play your purchased JamTrack, start a session and then open the JamTrack</p>
<a className="download-jamkazam-wrapper" href="/client#/createSession">
<div className="download-jamkazam">
Click Here to Start a Session
</div>
</a>
</div>`
else
platformMessage =
`<div>
<a href='/client#/jamtrack' className="download-jamkazam-wrapper jt-popup">
<div className="download-jamkazam">
Click Here to Start Using Your JamTrack
</div>
</a>
<a href='/downloads' rel="external" className="download-jamkazam-wrapper" target="_blank">
<div>
Do More With Your JamTrack - Click Here to Download Our Application
</div>
</a>
<a className="back-to-browsing" href="/client#/jamtrack">
or click here to browse more jamtracks
</a>
</div>`
content = `<div className="sold-notice">
<h2>Thank you for your order!</h2>
{platformMessage}
</div>`
else
orderButtons = {"button-orange": true, "place-order-btn": true, disabled: this.state.ordering }
cancelButtons = {"button-grey": true, "cancel": true, disabled: this.state.ordering }
content = `<div>
<h2 className="confirm-header">Confirm PayPal Payment</h2>
<p>You have not yet made a payment via PayPal. Please review your purchase and confirm or cancel.</p>
<div className="controls">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
<ShoppingCartContents carts={this.state.carts}/>
<div className="controls bottom">
<a href="#" className={classNames(orderButtons)} onClick={this.placeOrder}>CONFIRM PURCHASE WITH
PAYPAL</a>
<a href="#" className={classNames(cancelButtons)} onClick={this.cancel}>CANCEL</a>
<div className="clearall"/>
</div>
</div>`
`<div className="PayPalConfirmationScreen">
<div className="content-body-scroller">
{content}
</div>
</div>`
placeOrder: (e) ->
e.preventDefault()
if this.state.ordering
return
@setState({ordering: true})
console.log("placing order with paypal")
@rest.paypalPlaceOrder()
.done((response) =>
console.log("paypal detail obtained", response)
@setState({sold: true, ordering: false})
context.JK.JamTrackUtils.checkShoppingCart()
@app.refreshUser()
)
.fail((jqXHR) =>
@setState({ordering: false})
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
)
cancelOrder: (e) ->
e.preventDefault()
window.location = '/client#/jamtrack'
getInitialState: () ->
{}
componentDidMount: () ->
componentDidUpdate: () ->
afterShow: (data) ->
rest.getShoppingCarts()
.done((carts) =>
@setState({carts: carts})
if carts.length == 0
window.location = '/client#/jamtrack'
return
@rest.paypalDetail()
.done((response) =>
console.log("paypal detail obtained", response)
)
.fail((jqXHR) =>
if jqXHR.status == 404
context.JK.Banner.showAlert('PayPal Session Over', 'Your PayPal authorization has expired. Please restart the PayPal confirmation process. <a href="/client#/checkoutPayment">Click Here to Checkout Again.</a>')
else if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
context.JK.Banner.showAlert('PayPal Purchase Error', 'PayPal: ' + response.message)
else
context.JK.Banner.showAlert('PayPal/Sales Error', 'Please contact support@jamkazam.com')
@app.notifyServerError jqXHR, 'PayPal Communication Error'
)
)
.fail((jqXHR) =>
@app.notifyServerError jqXHR, 'Unable to fetch carts'
)
beforeShow: () ->
this.setState({sold: false})
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
screenBindings =
'beforeShow': @beforeShow
'afterShow': @afterShow
@app.bindScreen('paypal/confirm', screenBindings)
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -0,0 +1,106 @@
context = window
MIX_MODES = context.JK.MIX_MODES
@ShoppingCartContents = React.createClass({
mixins: [Reflux.listenTo(@AppStore, "onAppInit"), Reflux.listenTo(@UserStore, "onUserChanged")]
render: () ->
carts = []
if this.props.carts?
if this.props.carts.length == 0
carts = `<div className="no-cart-items">You have nothing in your cart</div>`
else
taxRate = 0
if this.props.tax
taxRate = 0.0825
estimatedTax = 0
estimatedTotal = 0
for cart in this.props.carts
cart_quantity = cart.product_info.quantity - cart.product_info.marked_for_redeem
estimatedTax += cart.product_info.price * cart_quantity * taxRate
estimatedTotal += cart.product_info.price * cart_quantity
estimatedTax = Math.round(estimatedTax * 100) / 100
estimatedTotal = Math.round((estimatedTotal + estimatedTax) * 100) / 100
for cart in this.props.carts
console.log("CART", cart)
freeNotice = null
if cart.product_info.free
freeNotice = `<span className="first-one-free">| (first one free)</span>`
carts.push(`<div className="cart-item" key={cart.id}>
<div className="cart-item-caption">
<span>{cart.product_info.sale_display}</span>
{freeNotice}
</div>
<div className="cart-item-price">
$ {Number(cart.product_info.real_price).toFixed(2)}
</div>
<div className="cart-item-quantity">
{cart.quantity}
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item tax-total" key={'tax'}>
<div className="cart-item-caption">
<span>Tax</span>
</div>
<div className="cart-item-price">
$ {estimatedTax.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
carts.push(`<div className="cart-item total" key={'total'}>
<div className="cart-item-caption">
<span>Total</span>
</div>
<div className="cart-item-price">
$ {estimatedTotal.toFixed(2)}
</div>
<div className="cart-item-quantity">
</div>
<div className="clearall"/>
</div>`)
else
carts = `<div className="loading-indicator">Loading...</div>`
`<div className="shopping-cart-contents">
<div className="order-items-page">
<div className="cart-items">
<div className="cart-item-caption">
<span>YOUR ORDER INCLUDES:</span>
</div>
<div className="cart-item-price">
<span>PRICE</span>
</div>
<div className="cart-item-quantity">
<span>QUANTITY</span>
</div>
<div className="clearall"></div>
{carts}
<div className="clearall"></div>
</div>
</div>
</div>`
onAppInit: (@app) ->
@EVENTS = context.JK.EVENTS
@rest = context.JK.Rest()
@logger = context.JK.logger
onUserChanged: (userState) ->
@user = userState?.user
})

View File

@ -43,6 +43,7 @@ proficiencyDescriptionMap = {
TILE_RATINGS: 'ratings'
TILE_PRICES: 'prices'
visible: false
profileClipboard: null
TILES: ['about', 'experience', 'samples', 'ratings', 'prices']
@ -65,11 +66,26 @@ proficiencyDescriptionMap = {
@root = $(@getDOMNode())
@screen = $('#teacher-profile')
@starbox()
@clipboard()
componentDidUpdate:() ->
@starbox()
context.JK.popExternalLinks(@root)
@clipboard()
clipboard: () ->
$profileLink = @root.find('.copy-profile-link')
if $profileLink.length > 0 && !@profileClipboard?
# mount it
@profileClipboard = new Clipboard($profileLink.get(0), {
text: =>
return context.JK.makeAbsolute('/client#/teacher/profile/' + @state.user.teacher?.id)
})
else if $profileLink.length == 0 && @profileClipboard?
@profileClipboard.destroy()
@profileClipboard = null
starbox:() ->
$ratings = @root.find('.ratings-box')
@ -230,7 +246,9 @@ proficiencyDescriptionMap = {
biography = biography.replace(/\n/g, "<br/>")
`<div className="section bio">
<h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3>
<a className="copy-profile-link button-orange" href='#' onClick={this.copyProfileLink}>
COPY PROFILE URL TO CLIPBOARD
</a><h3>Teacher Profile {this.editProfileLink('edit profile', 'introduction')}</h3>
<div className="section-content">
<div dangerouslySetInnerHTML={{__html: biography}}></div>
</div>
@ -694,6 +712,16 @@ proficiencyDescriptionMap = {
</div>
</div>`
copyProfileLink: (e) ->
e.preventDefault()
@app.layout.notify({
title: 'Teacher Profile Link Copied',
text: "Your clipboard now has a link to this teacher that you can share with anyone."
})
selectionMade: (selection, e) ->
e.preventDefault()

View File

@ -102,7 +102,7 @@
var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false }
$self.triggerHandler('startedRecording', details);
currentlyRecording = false;
context.RecordingActions.startedRecording(details);
context.RecordingActions.startedRecording(details);
})

View File

@ -9,7 +9,7 @@
//= require jquery.queryparams
//= require jquery.hoverIntent
//= require jquery.cookie
//= require jquery.clipboard
//= require clipboard
//= require jquery.easydropdown
//= require jquery.carousel-1.1
//= require jquery.mousewheel-3.1.9

View File

@ -33,6 +33,24 @@
}
}
.paypal-region {
text-align: center;
margin:10px auto 0;
/**margin: 10px auto 0;
padding: 10px 10px 5px;
background-color: white;
border-radius: 8px;
border-color: #ccc;
border-style: solid;
border-width: 3px;
width:145px;*/
}
.or-text {
margin: 60px auto 0;
text-align:center;
}
h2 {
color:white;
background-color:#4d4d4d;

View File

@ -0,0 +1,78 @@
@import "client/common.scss";
[data-react-class="PayPalConfirmationScreen"] {
height: 100%;
overflow: scroll;
.content-body-scroller {
height: calc(100% - 30px) ! important; // 15px top and bottom padding, and 48px used by .controls
padding: 15px 30px;
}
.confirm-header {
color: white;
font-size: 20px;
}
.controls.bottom {
margin-top: 20px;
}
.place-order-btn {
text-align: center;
margin-right:0;
}
.or-holder {
margin-top: 20px;
text-align: center;
}
.cancel-order-btn {
margin-top: 20px;
text-align: center;
}
.shopping-cart-contents {
@include border-box_sizing;
width: 50%;
margin-top:20px;
}
.controls {
@include border-box_sizing;
width:50%;
a {
float:right;
}
}
.loading-indicator {
margin-bottom:20px;
padding-bottom:20px;
}
.sold-notice {
h2 {
font-size:30px;
text-align:center;
}
}
.download-jamkazam {
color:$ColorLink;
border-radius: 4px;
border-style:solid;
border-color:#AAA;
border-width:1px;
padding:10px;
margin-top:20px;
display:inline-block;
}
.download-jamkazam-wrapper, .back-to-browsing {
text-align:center;
display:block;
margin-top:35px;
&.hidden {
display:none;
}
}
}

View File

@ -0,0 +1,64 @@
@import "client/common.scss";
.shopping-cart-contents {
background-color:#262626;
border-width:0 1px 0 0;
border-style:solid;
border-color:#333;
padding:20px 20px 0;
.cart-item-caption {
width: 50%;
text-align: left;
float: left;
margin-bottom: 10px;
@include border_box_sizing;
}
.first-one-free {
font-size: 14px;
font-style: italic;
margin-left: 15px;
}
.cart-item-price {
width: 25%;
text-align: right;
float: left;
padding: 0 10px;
margin-bottom: 10px;
@include border_box_sizing;
}
.cart-item-quantity {
width: 10%;
text-align: right;
float: left;
padding: 0 10px;
margin-bottom: 10px;
@include border_box_sizing;
}
.cart-items {
margin-top: 10px;
padding-left: 10px;
}
.cart-item {
margin-top: 10px;
}
.no-cart-items {
}
.tax-total {
margin-top:10px;
border-width:1px 0 0 0;
border-color:white;
border-style:solid;
padding-top:10px;
}
.cart-item.total {
margin-top:5px;
}
}

View File

@ -199,6 +199,9 @@
position:absolute;
}
.copy-profile-link {
float:right;
}
.spinner-large {
width:200px;

View File

@ -15,7 +15,6 @@ class ApiLessonSessionsController < ApiController
render "api_lesson_sessions/index", :layout => nil
end
def show
end
@ -72,13 +71,13 @@ class ApiLessonSessionsController < ApiController
if params[:update_all]
# check if the next scheduled lesson is doable
if 24.hours.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start
if 15.minutes.from_now > @lesson_session.lesson_booking.next_lesson.music_session.scheduled_start
response = {message: 'time_limit'}
render :json => response, :status => 422
return
end
else
if 24.hours.from_now > @lesson_session.music_session.scheduled_start
if 15.minutes.from_now > @lesson_session.music_session.scheduled_start
response = {message: 'time_limit'}
render :json => response, :status => 422
return

View File

@ -0,0 +1,136 @@
class ApiPayPalController < ApiController
before_filter :api_signed_in_user
respond_to :json
def log
@log || Logging.logger[VanillaForumsController]
end
def start_checkout
cancel_path = params[:path] ? params[:path] : ERB::Util.url_encode('/client#/checkoutPayment')
tax = true
tax_rate = tax ? 0.0825 : 0
total = current_user.shopping_cart_total.round(2)
tax_total = (total * tax_rate).round(2)
total = total + tax_total
total = total.round(2)
@api = PayPal::SDK::Merchant::API.new
@set_express_checkout = @api.build_set_express_checkout(
{
:Version => "117.0",
:SetExpressCheckoutRequestDetails =>
{
:ReturnURL => ApplicationHelper.base_uri(request) + '/auth/paypal/checkout',
:CancelURL => ApplicationHelper.base_uri(request) + '/auth/paypal/checkout?cancel=1&path=' + cancel_path,
# :NoShipping => "1",
# :ReqConfirmShipping => "0",
# :ReqBillingAddress => "1",
:PaymentDetails =>
[
{
:OrderTotal => {
:currencyID => "USD",
:value => total
},
:PaymentAction => "Sale"
}
]
}
}
)
@set_express_checkout_response = @api.set_express_checkout(@set_express_checkout)
log.info("User #{current_user.email}, SetExpressCheckout #{@set_express_checkout_response.inspect}")
if @set_express_checkout_response.Ack == 'Failure'
render json: {message: @set_express_checkout_response.Errors[0].LongMessage}, status: 422
return
end
redirect_to Rails.configuration.paypal_express_url + '&token=' + ERB::Util.url_encode(@set_express_checkout_response.Token)
end
# called by frontend after the user comes back from initial express page
def checkout_detail
# here we can see if they will pay tax
if !current_user.has_paypal_auth?
render json: {}, :status => 404
return
end
paypal_auth = current_user.paypal_auth
@api = PayPal::SDK::Merchant::API.new
@get_express_checkout_details = @api.build_get_express_checkout_details({:Token => paypal_auth.token})
@response = @api.get_express_checkout_details(@get_express_checkout_details)
puts @response.inspect
tax = false
if @response.Ack == 'Success'
payerInfo = @response.GetExpressCheckoutDetailsResponseDetails.PayerInfo
if payerInfo.Address && ( payerInfo.Address.Country == 'US' && payerInfo.Address.StateOrProvince == 'TX')
# we need to ask for taxes
tax = true
end
else
render json: {message: @response.Errors[0].LongMessage}, status: 422
return
end
log.debug("User #{current_user.email}, GetExpressCheckout: #{@get_express_checkout_details_response.inspect}")
render json: {tax: tax}
end
# called by frontend when the user selects finally 'confirm purchase' (PLACE ORDER btn)
def confirm_purchase
if !current_user.has_paypal_auth?
render json: {}, :status => 404
return
end
error = nil
response = {jam_tracks: [], gift_cards: []}
#if Sale.is_mixed(current_user.shopping_carts)
# msg = "has free and non-free items. Try removing non-free items."
# render json: {message: "Cart " + msg, errors: {cart: [msg]}}, :status => 404
# return
#end
begin
sales = Sale.place_order(current_user, current_user.shopping_carts, true)
rescue RecurlyClientError => e
render json: {message: e.errors[:message]}, :status => 422
return
rescue PayPalClientError => x
render json: {message: x.errors[:message]}, :status => 422
return
end
sales.each do |sale|
sale.sale_line_items.each do |line_item|
if line_item.is_jam_track?
jam_track = line_item.product
jam_track_right = jam_track.right_for_user(current_user)
response[:jam_tracks] << {name: jam_track.name, id: jam_track.id, jam_track_right_id: jam_track_right.id, version: jam_track.version}
elsif line_item.is_gift_card?
gift_card = line_item.product
response[:gift_cards] << {name: gift_card.name, id: gift_card.id}
else
raise 'unknown sale line item type: ' + line_item.product_type
end
end
end
set_purchased_jamtrack_cookie
render :json => response, :status => 200
end
end

View File

@ -1,6 +1,8 @@
# this is not a jam session - this is an 'auth session'
class SessionsController < ApplicationController
before_filter :api_signed_in_user, only: :paypal_express_checkout
layout "web"
def signin
@ -37,6 +39,42 @@ class SessionsController < ApplicationController
end
end
def paypal_express_checkout
# should get 'token' and 'PayerID' on success
# on failure, cancel=1
if params[:cancel] == '1' || params[:cancel] == 1
redirect_to params[:path] ? params[:path] : '/client#/jamtrack'
return
end
authorization = current_user.paypal_auth
# Always make and save a new authorization. This is because they expire, and honestly there's no cost
# to just making and saving it.
user_auth_hash = {
:provider => 'paypal',
:uid => params[:PayerID],
:token => params[:token],
:refresh_token => nil,
:token_expiration => 3.hours.from_now, # according to paypal docs, a token is good for 3 hours
:secret => nil
}
if authorization.nil?
authorization = current_user.user_authorizations.build(user_auth_hash)
authorization.save
else
authorization.token = user_auth_hash[:token]
authorization.token_expiration = user_auth_hash[:token_expiration]
authorization.uid = user_auth_hash[:uid]
authorization.save
end
redirect_to '/client#/paypal/confirm'
end
# OAuth docs
# http://net.tutsplus.com/tutorials/ruby/how-to-use-omniauth-to-authenticate-your-users/

View File

@ -120,6 +120,13 @@ div layout="screen" layout-id="checkoutPayment" id="checkoutPaymentScreen" class
.divSaveCardHelper
label for="save-card" Save card for future use
.clearall
- if !Rails.application.config.paypal_admin_only || any_user.admin
.or-text or instead use:
.paypal-region
a href="/paypal/checkout/start" data-paypal-button="true"
img src="https://www.paypalobjects.com/webstatic/en_US/i/btn/png/gold-pill-paypalcheckout-34px.png" alt="PayPal Checkout"
a
.clearall
.clearall
.row.second

View File

@ -0,0 +1,8 @@
.screen.secondary layout='screen' layout-id='paypal/confirm'
.content
.content-head
.content-icon=image_tag("content/icon_jamtracks.png", height: 19, width: 19)
h1 confirm payment
= render "screen_navigation"
.content-body
= react_component 'PayPalConfirmationScreen', {}

View File

@ -58,6 +58,7 @@
<%= render "jamtrack_search" %>
<%= render "jamtrack_filter" %>
<%= render "jamtrack_landing" %>
<%= render "paypal_confirmation" %>
<%= render "shopping_cart" %>
<%= render "checkout_signin" %>
<%= render "checkout_payment" %>

View File

@ -38,7 +38,7 @@
<div class="share-link border-bottom">
<h3>Share a Link:</h3>
<div class="link-contents">
<div class="link-contents" id="link-contents">
</div>
<div class="right"><a id="btn-share-copy" class="button-orange">COPY LINK</a></div>

View File

@ -452,6 +452,17 @@ if defined?(Bundler)
# This would transparently migrate your existing Marshal-serialized cookies into the new JSON-based format.
config.action_dispatch.cookies_serializer = :hybrid
config.jam_class_card_wait_period_year = 1
config.jam_class_card_wait_period_year = 1
config.paypal_mode = 'sandbox' #sandbox or live
config.app_id = 'APP-80W284485P519543T' # this is constant across all sandbox accts
config.paypal_username = 'seth+ppmerchant_api1.jamkazam.com' # seth+ppmerchant@jamkazam.com' # 'seth+ppmerchant_api1.jamkazam.com'
config.paypal_client_id = 'AZ5CCd8lHNntZ0ddxk_Wvo6LUaACd-bsMr7OPu_M1oI9vPN3d89mdSKswns9GEKB57qIwTT9_NyyK1c8'
config.paypal_client_secret = 'EIK0KWr8G5ntzYyJzbGCrsdr62aaJw2PdtXJrQddI9WuraBxJfiL4aMtKWu7Cyo4ACt13hLvCqg84HjP'
config.paypal_password = 'UXDKMPLYNE5YRLMK'
config.paypal_signature = 'AFcWxV21C7fd0v3bYYYRCpSSRl31AmvhBjN40M8etif4EA.L8EyMxdsu'
config.paypal_debug = true
config.paypal_express_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_express-checkout' # drop sandbox for production
config.paypal_admin_only = true
end
end

View File

@ -0,0 +1,13 @@
PayPal::SDK.configure(
:mode => Rails.configuration.paypal_mode,
:username => Rails.configuration.paypal_username,
:password => Rails.configuration.paypal_password,
:signature => Rails.configuration.paypal_signature,
:ssl_options => { } )
# :client_id => Rails.configuration.paypal_client_id,
# :client_secret => Rails.configuration.paypal_client_secret,
PayPal::SDK.logger = Logging.logger['PayPal']
PayPal::SDK.logger.level = Rails.configuration.paypal_debug ? Logger::DEBUG : Logger::INFO

View File

@ -77,6 +77,7 @@ Rails.application.routes.draw do
get '/auth/:provider/callback', :to => 'sessions#oauth_callback'
get '/auth/failure', :to => 'sessions#failure'
get '/auth/has_google_auth', :to => 'sessions#has_google_auth'
get '/auth/paypal/checkout', :to => 'sessions#paypal_express_checkout'
# session info page
get '/sessions/:id/details' => 'music_sessions#session_info', :as => 'music_scheduled_session_detail'
@ -125,6 +126,8 @@ Rails.application.routes.draw do
get '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse'
get '/paypal/checkout/start' => 'api_pay_pal#start_checkout'
# embed resque-web if this is development mode
if Rails.env == "development" || Rails.application.config.allow_spikes
require 'resque/server'
@ -260,7 +263,6 @@ Rails.application.routes.draw do
match '/music_notations/:id' => 'api_music_notations#download', :via => :get, :as => :download_music_notation
match '/music_notations/:id' => 'api_music_notations#delete', :via => :delete, :as => :delete_music_notation
# Backing track_show
match '/backing_tracks' => 'api_backing_tracks#index', :via => :get, :as => 'api_backing_tracks_list'
@ -363,6 +365,10 @@ Rails.application.routes.draw do
match '/recurly/place_order' => 'api_recurly#place_order', :via => :post
match '/ios/order_placed' => 'api_jam_tracks#ios_order_placed', :via => :post
# paypal
match '/paypal/checkout/detail' => 'api_pay_pal#checkout_detail', :via => :post
match '/paypal/checkout/confirm' => 'api_pay_pal#confirm_purchase', :via => :post
# sale info
match '/payment_histories' => 'api_payment_histories#index', :via => :get

View File

@ -0,0 +1,753 @@
/*!
* clipboard.js v1.5.15
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* A polyfill for Element.matches()
*/
if (Element && !Element.prototype.matches) {
var proto = Element.prototype;
proto.matches = proto.matchesSelector ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.oMatchesSelector ||
proto.webkitMatchesSelector;
}
/**
* Finds the closest parent that matches a selector.
*
* @param {Element} element
* @param {String} selector
* @return {Function}
*/
function closest (element, selector) {
while (element && element !== document) {
if (element.matches(selector)) return element;
element = element.parentNode;
}
}
module.exports = closest;
},{}],2:[function(require,module,exports){
var closest = require('./closest');
/**
* Delegates event to a selector.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @param {Boolean} useCapture
* @return {Object}
*/
function delegate(element, selector, type, callback, useCapture) {
var listenerFn = listener.apply(this, arguments);
element.addEventListener(type, listenerFn, useCapture);
return {
destroy: function() {
element.removeEventListener(type, listenerFn, useCapture);
}
}
}
/**
* Finds closest match and invokes callback.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Function}
*/
function listener(element, selector, type, callback) {
return function(e) {
e.delegateTarget = closest(e.target, selector);
if (e.delegateTarget) {
callback.call(element, e);
}
}
}
module.exports = delegate;
},{"./closest":1}],3:[function(require,module,exports){
/**
* Check if argument is a HTML element.
*
* @param {Object} value
* @return {Boolean}
*/
exports.node = function(value) {
return value !== undefined
&& value instanceof HTMLElement
&& value.nodeType === 1;
};
/**
* Check if argument is a list of HTML elements.
*
* @param {Object} value
* @return {Boolean}
*/
exports.nodeList = function(value) {
var type = Object.prototype.toString.call(value);
return value !== undefined
&& (type === '[object NodeList]' || type === '[object HTMLCollection]')
&& ('length' in value)
&& (value.length === 0 || exports.node(value[0]));
};
/**
* Check if argument is a string.
*
* @param {Object} value
* @return {Boolean}
*/
exports.string = function(value) {
return typeof value === 'string'
|| value instanceof String;
};
/**
* Check if argument is a function.
*
* @param {Object} value
* @return {Boolean}
*/
exports.fn = function(value) {
var type = Object.prototype.toString.call(value);
return type === '[object Function]';
};
},{}],4:[function(require,module,exports){
var is = require('./is');
var delegate = require('delegate');
/**
* Validates all params and calls the right
* listener function based on its target type.
*
* @param {String|HTMLElement|HTMLCollection|NodeList} target
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listen(target, type, callback) {
if (!target && !type && !callback) {
throw new Error('Missing required arguments');
}
if (!is.string(type)) {
throw new TypeError('Second argument must be a String');
}
if (!is.fn(callback)) {
throw new TypeError('Third argument must be a Function');
}
if (is.node(target)) {
return listenNode(target, type, callback);
}
else if (is.nodeList(target)) {
return listenNodeList(target, type, callback);
}
else if (is.string(target)) {
return listenSelector(target, type, callback);
}
else {
throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
}
}
/**
* Adds an event listener to a HTML element
* and returns a remove listener function.
*
* @param {HTMLElement} node
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNode(node, type, callback) {
node.addEventListener(type, callback);
return {
destroy: function() {
node.removeEventListener(type, callback);
}
}
}
/**
* Add an event listener to a list of HTML elements
* and returns a remove listener function.
*
* @param {NodeList|HTMLCollection} nodeList
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNodeList(nodeList, type, callback) {
Array.prototype.forEach.call(nodeList, function(node) {
node.addEventListener(type, callback);
});
return {
destroy: function() {
Array.prototype.forEach.call(nodeList, function(node) {
node.removeEventListener(type, callback);
});
}
}
}
/**
* Add an event listener to a selector
* and returns a remove listener function.
*
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenSelector(selector, type, callback) {
return delegate(document.body, selector, type, callback);
}
module.exports = listen;
},{"./is":3,"delegate":2}],5:[function(require,module,exports){
function select(element) {
var selectedText;
if (element.nodeName === 'SELECT') {
element.focus();
selectedText = element.value;
}
else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
element.focus();
element.setSelectionRange(0, element.value.length);
selectedText = element.value;
}
else {
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
module.exports = select;
},{}],6:[function(require,module,exports){
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
},{}],7:[function(require,module,exports){
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['module', 'select'], factory);
} else if (typeof exports !== "undefined") {
factory(module, require('select'));
} else {
var mod = {
exports: {}
};
factory(mod, global.select);
global.clipboardAction = mod.exports;
}
})(this, function (module, _select) {
'use strict';
var _select2 = _interopRequireDefault(_select);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var ClipboardAction = function () {
/**
* @param {Object} options
*/
function ClipboardAction(options) {
_classCallCheck(this, ClipboardAction);
this.resolveOptions(options);
this.initSelection();
}
/**
* Defines base properties passed from constructor.
* @param {Object} options
*/
_createClass(ClipboardAction, [{
key: 'resolveOptions',
value: function resolveOptions() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.action = options.action;
this.emitter = options.emitter;
this.target = options.target;
this.text = options.text;
this.trigger = options.trigger;
this.selectedText = '';
}
}, {
key: 'initSelection',
value: function initSelection() {
if (this.text) {
this.selectFake();
} else if (this.target) {
this.selectTarget();
}
}
}, {
key: 'selectFake',
value: function selectFake() {
var _this = this;
var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
this.removeFake();
this.fakeHandlerCallback = function () {
return _this.removeFake();
};
this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true;
this.fakeElem = document.createElement('textarea');
// Prevent zooming on iOS
this.fakeElem.style.fontSize = '12pt';
// Reset box model
this.fakeElem.style.border = '0';
this.fakeElem.style.padding = '0';
this.fakeElem.style.margin = '0';
// Move element out of screen horizontally
this.fakeElem.style.position = 'absolute';
this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
// Move element to the same position vertically
var yPosition = window.pageYOffset || document.documentElement.scrollTop;
this.fakeElem.addEventListener('focus', window.scrollTo(0, yPosition));
this.fakeElem.style.top = yPosition + 'px';
this.fakeElem.setAttribute('readonly', '');
this.fakeElem.value = this.text;
document.body.appendChild(this.fakeElem);
this.selectedText = (0, _select2.default)(this.fakeElem);
this.copyText();
}
}, {
key: 'removeFake',
value: function removeFake() {
if (this.fakeHandler) {
document.body.removeEventListener('click', this.fakeHandlerCallback);
this.fakeHandler = null;
this.fakeHandlerCallback = null;
}
if (this.fakeElem) {
document.body.removeChild(this.fakeElem);
this.fakeElem = null;
}
}
}, {
key: 'selectTarget',
value: function selectTarget() {
this.selectedText = (0, _select2.default)(this.target);
this.copyText();
}
}, {
key: 'copyText',
value: function copyText() {
var succeeded = void 0;
try {
succeeded = document.execCommand(this.action);
} catch (err) {
succeeded = false;
}
this.handleResult(succeeded);
}
}, {
key: 'handleResult',
value: function handleResult(succeeded) {
this.emitter.emit(succeeded ? 'success' : 'error', {
action: this.action,
text: this.selectedText,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
}, {
key: 'clearSelection',
value: function clearSelection() {
if (this.target) {
this.target.blur();
}
window.getSelection().removeAllRanges();
}
}, {
key: 'destroy',
value: function destroy() {
this.removeFake();
}
}, {
key: 'action',
set: function set() {
var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy';
this._action = action;
if (this._action !== 'copy' && this._action !== 'cut') {
throw new Error('Invalid "action" value, use either "copy" or "cut"');
}
},
get: function get() {
return this._action;
}
}, {
key: 'target',
set: function set(target) {
if (target !== undefined) {
if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {
if (this.action === 'copy' && target.hasAttribute('disabled')) {
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
}
if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
}
this._target = target;
} else {
throw new Error('Invalid "target" value, use a valid Element');
}
}
},
get: function get() {
return this._target;
}
}]);
return ClipboardAction;
}();
module.exports = ClipboardAction;
});
},{"select":5}],8:[function(require,module,exports){
(function (global, factory) {
if (typeof define === "function" && define.amd) {
define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory);
} else if (typeof exports !== "undefined") {
factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener'));
} else {
var mod = {
exports: {}
};
factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener);
global.clipboard = mod.exports;
}
})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) {
'use strict';
var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
var _goodListener2 = _interopRequireDefault(_goodListener);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Clipboard = function (_Emitter) {
_inherits(Clipboard, _Emitter);
/**
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
* @param {Object} options
*/
function Clipboard(trigger, options) {
_classCallCheck(this, Clipboard);
var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this));
_this.resolveOptions(options);
_this.listenClick(trigger);
return _this;
}
/**
* Defines if attributes would be resolved using internal setter functions
* or custom functions that were passed in the constructor.
* @param {Object} options
*/
_createClass(Clipboard, [{
key: 'resolveOptions',
value: function resolveOptions() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
this.text = typeof options.text === 'function' ? options.text : this.defaultText;
}
}, {
key: 'listenClick',
value: function listenClick(trigger) {
var _this2 = this;
this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) {
return _this2.onClick(e);
});
}
}, {
key: 'onClick',
value: function onClick(e) {
var trigger = e.delegateTarget || e.currentTarget;
if (this.clipboardAction) {
this.clipboardAction = null;
}
this.clipboardAction = new _clipboardAction2.default({
action: this.action(trigger),
target: this.target(trigger),
text: this.text(trigger),
trigger: trigger,
emitter: this
});
}
}, {
key: 'defaultAction',
value: function defaultAction(trigger) {
return getAttributeValue('action', trigger);
}
}, {
key: 'defaultTarget',
value: function defaultTarget(trigger) {
var selector = getAttributeValue('target', trigger);
if (selector) {
return document.querySelector(selector);
}
}
}, {
key: 'defaultText',
value: function defaultText(trigger) {
return getAttributeValue('text', trigger);
}
}, {
key: 'destroy',
value: function destroy() {
this.listener.destroy();
if (this.clipboardAction) {
this.clipboardAction.destroy();
this.clipboardAction = null;
}
}
}]);
return Clipboard;
}(_tinyEmitter2.default);
/**
* Helper function to retrieve attribute value.
* @param {String} suffix
* @param {Element} element
*/
function getAttributeValue(suffix, element) {
var attribute = 'data-clipboard-' + suffix;
if (!element.hasAttribute(attribute)) {
return;
}
return element.getAttribute(attribute);
}
module.exports = Clipboard;
});
},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)
});

View File

@ -1,611 +0,0 @@
/*
* jQuery Clipboard :: Fork of zClip :: Uses ZeroClipboard v1.2.3
*
* https://github.com/valeriansaliou/jquery.clipboard
* http://www.steamdev.com/zclip/
*
* Copyright 2013, Valérian Saliou
* Copyright 2011, SteamDev
*
* Released under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* Version: v1.2
* Date: Sun Dec 1, 2013
*/
/* Component: jQuery Clipboard */
(function ($) {
var $clip = null;
var $is_loaded = false;
$.fn.clipboard = function (params) {
if ((typeof params == 'object' && !params.length) || (typeof params == 'undefined')) {
var settings = $.extend({
path: 'jquery.clipboard.swf',
copy: null,
beforeCopy: null,
afterCopy: null,
clickAfter: true
}, (params || {}));
return this.each(function () {
var o = $(this);
if (o.is(':visible') && (typeof settings.copy == 'string' || $.isFunction(settings.copy))) {
if ($.isFunction(settings.copy)) {
o.bind('Clipboard_copy',settings.copy);
}
if ($.isFunction(settings.beforeCopy)) {
o.bind('Clipboard_beforeCopy',settings.beforeCopy);
}
if ($.isFunction(settings.afterCopy)) {
o.bind('Clipboard_afterCopy',settings.afterCopy);
}
if($clip === null) {
$clip = new ZeroClipboard(null, {
moviePath: settings.path,
trustedDomains: '*',
hoverClass: 'hover',
activeClass: 'active'
});
$clip.on('load', function(client) {
client.on('mouseover', function (client) {
$(this).trigger('mouseenter');
});
client.on('mouseout', function (client) {
$(this).trigger('mouseleave');
});
client.on('mousedown', function (client) {
$(this).trigger('mousedown');
if (!$.isFunction(settings.copy)) {
client.setText(settings.copy);
} else {
client.setText($(this).triggerHandler('Clipboard_copy'));
}
if ($.isFunction(settings.beforeCopy)) {
$(this).trigger('Clipboard_beforeCopy');
}
});
client.on('complete', function (client, args) {
if ($.isFunction(settings.afterCopy)) {
$(this).trigger('Clipboard_afterCopy');
} else {
$(this).removeClass('hover');
}
if (settings.clickAfter) {
$(this).trigger('click');
}
});
});
}
$clip.glue(o[0]);
}
});
}
};
})(jQuery);
/* Component: ZeroClipboard */
(function() {
"use strict";
var _camelizeCssPropName = function() {
var matcherRegex = /\-([a-z])/g, replacerFn = function(match, group) {
return group.toUpperCase();
};
return function(prop) {
return prop.replace(matcherRegex, replacerFn);
};
}();
var _getStyle = function(el, prop) {
var value, camelProp, tagName, possiblePointers, i, len;
if (window.getComputedStyle) {
value = window.getComputedStyle(el, null).getPropertyValue(prop);
} else {
camelProp = _camelizeCssPropName(prop);
if (el.currentStyle) {
value = el.currentStyle[camelProp];
} else {
value = el.style[camelProp];
}
}
if (prop === "cursor") {
if (!value || value === "auto") {
tagName = el.tagName.toLowerCase();
possiblePointers = [ "a" ];
for (i = 0, len = possiblePointers.length; i < len; i++) {
if (tagName === possiblePointers[i]) {
return "pointer";
}
}
}
}
return value;
};
var _elementMouseOver = function(event) {
if (!ZeroClipboard.prototype._singleton) return;
if (!event) {
event = window.event;
}
var target;
if (this !== window) {
target = this;
} else if (event.target) {
target = event.target;
} else if (event.srcElement) {
target = event.srcElement;
}
ZeroClipboard.prototype._singleton.setCurrent(target);
};
var _addEventHandler = function(element, method, func) {
if (element.addEventListener) {
element.addEventListener(method, func, false);
} else if (element.attachEvent) {
element.attachEvent("on" + method, func);
}
};
var _removeEventHandler = function(element, method, func) {
if (element.removeEventListener) {
element.removeEventListener(method, func, false);
} else if (element.detachEvent) {
element.detachEvent("on" + method, func);
}
};
var _addClass = function(element, value) {
if (element.addClass) {
element.addClass(value);
return element;
}
if (value && typeof value === "string") {
var classNames = (value || "").split(/\s+/);
if (element.nodeType === 1) {
if (!element.className) {
element.className = value;
} else {
var className = " " + element.className + " ", setClass = element.className;
for (var c = 0, cl = classNames.length; c < cl; c++) {
if (className.indexOf(" " + classNames[c] + " ") < 0) {
setClass += " " + classNames[c];
}
}
element.className = setClass.replace(/^\s+|\s+$/g, "");
}
}
}
return element;
};
var _removeClass = function(element, value) {
if (element.removeClass) {
element.removeClass(value);
return element;
}
if (value && typeof value === "string" || value === undefined) {
var classNames = (value || "").split(/\s+/);
if (element.nodeType === 1 && element.className) {
if (value) {
var className = (" " + element.className + " ").replace(/[\n\t]/g, " ");
for (var c = 0, cl = classNames.length; c < cl; c++) {
className = className.replace(" " + classNames[c] + " ", " ");
}
element.className = className.replace(/^\s+|\s+$/g, "");
} else {
element.className = "";
}
}
}
return element;
};
var _getZoomFactor = function() {
var rect, physicalWidth, logicalWidth, zoomFactor = 1;
if (typeof document.body.getBoundingClientRect === "function") {
rect = document.body.getBoundingClientRect();
physicalWidth = rect.right - rect.left;
logicalWidth = document.body.offsetWidth;
zoomFactor = Math.round(physicalWidth / logicalWidth * 100) / 100;
}
return zoomFactor;
};
var _getDOMObjectPosition = function(obj) {
var info = {
left: 0,
top: 0,
width: 0,
height: 0,
zIndex: 999999999
};
var zi = _getStyle(obj, "z-index");
if (zi && zi !== "auto") {
info.zIndex = parseInt(zi, 10);
}
if (obj.getBoundingClientRect) {
var rect = obj.getBoundingClientRect();
var pageXOffset, pageYOffset, zoomFactor;
if ("pageXOffset" in window && "pageYOffset" in window) {
pageXOffset = window.pageXOffset;
pageYOffset = window.pageYOffset;
} else {
zoomFactor = _getZoomFactor();
pageXOffset = Math.round(document.documentElement.scrollLeft / zoomFactor);
pageYOffset = Math.round(document.documentElement.scrollTop / zoomFactor);
}
var leftBorderWidth = document.documentElement.clientLeft || 0;
var topBorderWidth = document.documentElement.clientTop || 0;
info.left = rect.left + pageXOffset - leftBorderWidth;
info.top = rect.top + pageYOffset - topBorderWidth;
info.width = "width" in rect ? rect.width : rect.right - rect.left;
info.height = "height" in rect ? rect.height : rect.bottom - rect.top;
}
return info;
};
var _noCache = function(path, options) {
var useNoCache = !(options && options.useNoCache === false);
if (useNoCache) {
return (path.indexOf("?") === -1 ? "?" : "&") + "nocache=" + new Date().getTime();
} else {
return "";
}
};
var _vars = function(options) {
var str = [];
var origins = [];
if (options.trustedOrigins) {
if (typeof options.trustedOrigins === "string") {
origins.push(options.trustedOrigins);
} else if (typeof options.trustedOrigins === "object" && "length" in options.trustedOrigins) {
origins = origins.concat(options.trustedOrigins);
}
}
if (options.trustedDomains) {
if (typeof options.trustedDomains === "string") {
origins.push(options.trustedDomains);
} else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) {
origins = origins.concat(options.trustedDomains);
}
}
if (origins.length) {
str.push("trustedOrigins=" + encodeURIComponent(origins.join(",")));
}
if (typeof options.amdModuleId === "string" && options.amdModuleId) {
str.push("amdModuleId=" + encodeURIComponent(options.amdModuleId));
}
if (typeof options.cjsModuleId === "string" && options.cjsModuleId) {
str.push("cjsModuleId=" + encodeURIComponent(options.cjsModuleId));
}
return str.join("&");
};
var _inArray = function(elem, array) {
if (array.indexOf) {
return array.indexOf(elem);
}
for (var i = 0, length = array.length; i < length; i++) {
if (array[i] === elem) {
return i;
}
}
return -1;
};
var _prepGlue = function(elements) {
if (typeof elements === "string") throw new TypeError("ZeroClipboard doesn't accept query strings.");
if (!elements.length) return [ elements ];
return elements;
};
var _dispatchCallback = function(func, element, instance, args, async) {
if (async) {
window.setTimeout(function() {
func.call(element, instance, args);
}, 0);
} else {
func.call(element, instance, args);
}
};
var currentElement, gluedElements = [], flashState = {};
var ZeroClipboard = function(elements, options) {
if (elements) (ZeroClipboard.prototype._singleton || this).glue(elements);
if (ZeroClipboard.prototype._singleton) return ZeroClipboard.prototype._singleton;
ZeroClipboard.prototype._singleton = this;
this.options = {};
for (var kd in _defaults) this.options[kd] = _defaults[kd];
for (var ko in options) this.options[ko] = options[ko];
this.handlers = {};
if (!flashState.hasOwnProperty(this.options.moviePath)) {
flashState[this.options.moviePath] = {
noflash: !ZeroClipboard.detectFlashSupport(),
wrongflash: false,
ready: false,
version: "0.0.0"
};
}
if (flashState[this.options.moviePath].noflash === false) {
_bridge();
}
};
ZeroClipboard.prototype.setCurrent = function(element) {
currentElement = element;
this.reposition();
var titleAttr = element.getAttribute("title");
if (titleAttr) {
this.setTitle(titleAttr);
}
var useHandCursor = this.options.forceHandCursor === true || _getStyle(element, "cursor") === "pointer";
_setHandCursor.call(this, useHandCursor);
return this;
};
ZeroClipboard.prototype.setText = function(newText) {
if (newText && newText !== "") {
this.options.text = newText;
if (this.ready()) this.flashBridge.setText(newText);
}
return this;
};
ZeroClipboard.prototype.setTitle = function(newTitle) {
if (newTitle && newTitle !== "") this.htmlBridge.setAttribute("title", newTitle);
return this;
};
ZeroClipboard.prototype.setSize = function(width, height) {
if (this.ready()) this.flashBridge.setSize(width, height);
return this;
};
ZeroClipboard.prototype.setHandCursor = function(enabled) {
enabled = typeof enabled === "boolean" ? enabled : !!enabled;
_setHandCursor.call(this, enabled);
this.options.forceHandCursor = enabled;
return this;
};
var _setHandCursor = function(enabled) {
if (this.ready()) this.flashBridge.setHandCursor(enabled);
};
ZeroClipboard.version = "1.2.3";
var _defaults = {
moviePath: "ZeroClipboard.swf",
trustedOrigins: null,
text: null,
hoverClass: "zeroclipboard-is-hover",
activeClass: "zeroclipboard-is-active",
allowScriptAccess: "sameDomain",
useNoCache: true,
forceHandCursor: false
};
ZeroClipboard.setDefaults = function(options) {
for (var ko in options) _defaults[ko] = options[ko];
};
ZeroClipboard.destroy = function() {
if (ZeroClipboard.prototype._singleton) {
ZeroClipboard.prototype._singleton.unglue(gluedElements);
var bridge = ZeroClipboard.prototype._singleton.htmlBridge;
if (bridge && bridge.parentNode) {
bridge.parentNode.removeChild(bridge);
}
delete ZeroClipboard.prototype._singleton;
}
};
ZeroClipboard.detectFlashSupport = function() {
var hasFlash = false;
if (typeof ActiveXObject === "function") {
try {
if (new ActiveXObject("ShockwaveFlash.ShockwaveFlash")) {
hasFlash = true;
}
} catch (error) {}
}
if (!hasFlash && navigator.mimeTypes["application/x-shockwave-flash"]) {
hasFlash = true;
}
return hasFlash;
};
var _amdModuleId = null;
var _cjsModuleId = null;
var _bridge = function() {
var flashBridge, len;
var client = ZeroClipboard.prototype._singleton;
var container = document.getElementById("global-zeroclipboard-html-bridge");
if (!container) {
var opts = {};
for (var ko in client.options) opts[ko] = client.options[ko];
opts.amdModuleId = _amdModuleId;
opts.cjsModuleId = _cjsModuleId;
var flashvars = _vars(opts);
var html = ' <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" id="global-zeroclipboard-flash-bridge" width="100%" height="100%"> <param name="movie" value="' + client.options.moviePath + _noCache(client.options.moviePath, client.options) + '"/> <param name="allowScriptAccess" value="' + client.options.allowScriptAccess + '"/> <param name="scale" value="exactfit"/> <param name="loop" value="false"/> <param name="menu" value="false"/> <param name="quality" value="best" /> <param name="bgcolor" value="#ffffff"/> <param name="wmode" value="transparent"/> <param name="flashvars" value="' + flashvars + '"/> <embed src="' + client.options.moviePath + _noCache(client.options.moviePath, client.options) + '" loop="false" menu="false" quality="best" bgcolor="#ffffff" width="100%" height="100%" name="global-zeroclipboard-flash-bridge" allowScriptAccess="always" allowFullScreen="false" type="application/x-shockwave-flash" wmode="transparent" pluginspage="http://www.macromedia.com/go/getflashplayer" flashvars="' + flashvars + '" scale="exactfit"> </embed> </object>';
container = document.createElement("div");
container.id = "global-zeroclipboard-html-bridge";
container.setAttribute("class", "global-zeroclipboard-container");
container.style.position = "absolute";
container.style.left = "0px";
container.style.top = "-9999px";
container.style.width = "15px";
container.style.height = "15px";
container.style.zIndex = "9999";
document.body.appendChild(container);
container.innerHTML = html;
}
client.htmlBridge = container;
flashBridge = document["global-zeroclipboard-flash-bridge"];
if (flashBridge && (len = flashBridge.length)) {
flashBridge = flashBridge[len - 1];
}
client.flashBridge = flashBridge || container.children[0].lastElementChild;
};
ZeroClipboard.prototype.resetBridge = function() {
if (this.htmlBridge) {
this.htmlBridge.style.left = "0px";
this.htmlBridge.style.top = "-9999px";
this.htmlBridge.removeAttribute("title");
}
if (currentElement) {
_removeClass(currentElement, this.options.activeClass);
currentElement = null;
}
this.options.text = null;
return this;
};
ZeroClipboard.prototype.ready = function() {
return flashState[this.options.moviePath].ready === true;
};
ZeroClipboard.prototype.reposition = function() {
if (!currentElement) return false;
var pos = _getDOMObjectPosition(currentElement);
this.htmlBridge.style.top = pos.top + "px";
this.htmlBridge.style.left = pos.left + "px";
this.htmlBridge.style.width = pos.width + "px";
this.htmlBridge.style.height = pos.height + "px";
this.htmlBridge.style.zIndex = pos.zIndex + 1;
this.setSize(pos.width, pos.height);
return this;
};
ZeroClipboard.dispatch = function(eventName, args) {
ZeroClipboard.prototype._singleton.receiveEvent(eventName, args);
};
ZeroClipboard.prototype.on = function(eventName, func) {
var events = eventName.toString().split(/\s/g), added = {};
for (var i = 0, len = events.length; i < len; i++) {
eventName = events[i].toLowerCase().replace(/^on/, "");
added[eventName] = true;
if (!this.handlers[eventName]) {
this.handlers[eventName] = func;
}
}
if (added.noflash && flashState[this.options.moviePath].noflash) {
this.receiveEvent("onNoFlash", {});
}
if (added.wrongflash && flashState[this.options.moviePath].wrongflash) {
this.receiveEvent("onWrongFlash", {
flashVersion: flashState[this.options.moviePath].version
});
}
if (added.load && flashState[this.options.moviePath].ready) {
this.receiveEvent("onLoad", {
flashVersion: flashState[this.options.moviePath].version
});
}
return this;
};
ZeroClipboard.prototype.addEventListener = ZeroClipboard.prototype.on;
ZeroClipboard.prototype.off = function(eventName, func) {
var events = eventName.toString().split(/\s/g);
for (var i = 0; i < events.length; i++) {
eventName = events[i].toLowerCase().replace(/^on/, "");
for (var event in this.handlers) {
if (event === eventName && this.handlers[event] === func) {
delete this.handlers[event];
}
}
}
return this;
};
ZeroClipboard.prototype.removeEventListener = ZeroClipboard.prototype.off;
ZeroClipboard.prototype.receiveEvent = function(eventName, args) {
eventName = eventName.toString().toLowerCase().replace(/^on/, "");
var element = currentElement;
var performCallbackAsync = true;
switch (eventName) {
case "load":
if (args && args.flashVersion) {
if (!_isFlashVersionSupported(args.flashVersion)) {
this.receiveEvent("onWrongFlash", {
flashVersion: args.flashVersion
});
return;
}
flashState[this.options.moviePath].ready = true;
flashState[this.options.moviePath].version = args.flashVersion;
}
break;
case "wrongflash":
if (args && args.flashVersion && !_isFlashVersionSupported(args.flashVersion)) {
flashState[this.options.moviePath].wrongflash = true;
flashState[this.options.moviePath].version = args.flashVersion;
}
break;
case "mouseover":
_addClass(element, this.options.hoverClass);
break;
case "mouseout":
_removeClass(element, this.options.hoverClass);
this.resetBridge();
break;
case "mousedown":
_addClass(element, this.options.activeClass);
break;
case "mouseup":
_removeClass(element, this.options.activeClass);
break;
case "datarequested":
var targetId = element.getAttribute("data-clipboard-target"), targetEl = !targetId ? null : document.getElementById(targetId);
if (targetEl) {
var textContent = targetEl.value || targetEl.textContent || targetEl.innerText;
if (textContent) {
this.setText(textContent);
}
} else {
var defaultText = element.getAttribute("data-clipboard-text");
if (defaultText) {
this.setText(defaultText);
}
}
performCallbackAsync = false;
break;
case "complete":
this.options.text = null;
break;
}
if (this.handlers[eventName]) {
var func = this.handlers[eventName];
if (typeof func === "string" && typeof window[func] === "function") {
func = window[func];
}
if (typeof func === "function") {
_dispatchCallback(func, element, this, args, performCallbackAsync);
}
}
};
ZeroClipboard.prototype.glue = function(elements) {
elements = _prepGlue(elements);
for (var i = 0; i < elements.length; i++) {
if (elements[i] && elements[i].nodeType === 1) {
if (_inArray(elements[i], gluedElements) == -1) {
gluedElements.push(elements[i]);
_addEventHandler(elements[i], "mouseover", _elementMouseOver);
}
}
}
return this;
};
ZeroClipboard.prototype.unglue = function(elements) {
elements = _prepGlue(elements);
for (var i = 0; i < elements.length; i++) {
_removeEventHandler(elements[i], "mouseover", _elementMouseOver);
var arrayIndex = _inArray(elements[i], gluedElements);
if (arrayIndex != -1) gluedElements.splice(arrayIndex, 1);
}
return this;
};
function _isFlashVersionSupported(flashVersion) {
return parseFloat(flashVersion.replace(/,/g, ".").replace(/[^0-9\.]/g, "")) >= 10;
}
if (typeof define === "function" && define.amd) {
define([ "require", "exports", "module" ], function(require, exports, module) {
_amdModuleId = module && module.id || null;
return ZeroClipboard;
});
} else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) {
_cjsModuleId = module.id || null;
module.exports = ZeroClipboard;
} else {
window.ZeroClipboard = ZeroClipboard;
}
})();