client fix
This commit is contained in:
parent
cf7eceee13
commit
87c15f0a0b
307 changed files with 21282 additions and 74 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import { NotifyReporter } from "@jest/reporters";
|
||||||
|
|
||||||
const donate_css = require('../../assets/stylesheets/donate-button/donate-button.v2.css');
|
const donate_css = require('../../assets/stylesheets/donate-button/donate-button.v2.css');
|
||||||
|
|
||||||
const iframeHost = 'https://us.commitchange.com'
|
const iframeHost = 'https://us.commitchange.com'
|
||||||
|
@ -10,13 +12,14 @@ function on_ios11() {
|
||||||
return has11 && hasMacOS;
|
return has11 && hasMacOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.commitchange = {
|
const windowAsAny = window as any;
|
||||||
|
windowAsAny.commitchange = {
|
||||||
iframes: []
|
iframes: []
|
||||||
, modalIframe: null
|
, modalIframe: null
|
||||||
}
|
}
|
||||||
|
const commitchange = windowAsAny.commitchange;
|
||||||
commitchange.getParamsFromUrl = (whitelist) => {
|
commitchange.getParamsFromUrl = (whitelist:any) => {
|
||||||
var result = {},
|
var result:any = {},
|
||||||
tmp = [];
|
tmp = [];
|
||||||
var items = location.search.substr(1).split("&");
|
var items = location.search.substr(1).split("&");
|
||||||
for (var index = 0; index < items.length; index++) {
|
for (var index = 0; index < items.length; index++) {
|
||||||
|
@ -26,8 +29,8 @@ function on_ios11() {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
commitchange.openDonationModal = (iframe, overlay) => {
|
commitchange.openDonationModal = (iframe:HTMLIFrameElement, overlay:HTMLElement) => {
|
||||||
return (event) => {
|
return (event:Event) => {
|
||||||
overlay.className = 'commitchange-overlay commitchange-open'
|
overlay.className = 'commitchange-overlay commitchange-open'
|
||||||
iframe.className = 'commitchange-iframe commitchange-open'
|
iframe.className = 'commitchange-iframe commitchange-open'
|
||||||
if (on_ios11()) {
|
if (on_ios11()) {
|
||||||
|
@ -44,7 +47,7 @@ function on_ios11() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically set the params of the appended iframe donate window
|
// Dynamically set the params of the appended iframe donate window
|
||||||
commitchange.setParams = (params, iframe) => {
|
commitchange.setParams = (params:any, iframe:HTMLIFrameElement) => {
|
||||||
params.command = 'setDonationParams'
|
params.command = 'setDonationParams'
|
||||||
params.sender = 'commitchange'
|
params.sender = 'commitchange'
|
||||||
iframe.contentWindow.postMessage(JSON.stringify(params), fullHost)
|
iframe.contentWindow.postMessage(JSON.stringify(params), fullHost)
|
||||||
|
@ -69,7 +72,7 @@ function on_ios11() {
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|
||||||
commitchange.createIframe = (source) => {
|
commitchange.createIframe = (source:string) => {
|
||||||
let i = document.createElement('iframe')
|
let i = document.createElement('iframe')
|
||||||
const url = document.location.href
|
const url = document.location.href
|
||||||
i.setAttribute('class', 'commitchange-closed commitchange-iframe')
|
i.setAttribute('class', 'commitchange-closed commitchange-iframe')
|
||||||
|
@ -79,8 +82,8 @@ function on_ios11() {
|
||||||
|
|
||||||
// Given a button with a bunch of data parameters
|
// Given a button with a bunch of data parameters
|
||||||
// return an object of key/vals corresponing to each param
|
// return an object of key/vals corresponing to each param
|
||||||
commitchange.getParamsFromButton = (elem) => {
|
commitchange.getParamsFromButton = (elem:HTMLElement) => {
|
||||||
let options = {
|
let options: {[props:string]:any} = {
|
||||||
offsite: 't'
|
offsite: 't'
|
||||||
, type: elem.getAttribute('data-type')
|
, type: elem.getAttribute('data-type')
|
||||||
, custom_amounts: elem.getAttribute('data-custom-amounts') || elem.getAttribute('data-amounts')
|
, custom_amounts: elem.getAttribute('data-custom-amounts') || elem.getAttribute('data-amounts')
|
||||||
|
@ -126,7 +129,7 @@ function on_ios11() {
|
||||||
let elems = document.querySelectorAll('.commitchange-donate')
|
let elems = document.querySelectorAll('.commitchange-donate')
|
||||||
|
|
||||||
for(let i = 0; i < elems.length; ++i) {
|
for(let i = 0; i < elems.length; ++i) {
|
||||||
let elem = elems[i]
|
let elem:any = elems[i]
|
||||||
let source = baseSource
|
let source = baseSource
|
||||||
|
|
||||||
let optionsButton = commitchange.getParamsFromButton(elem)
|
let optionsButton = commitchange.getParamsFromButton(elem)
|
||||||
|
@ -145,6 +148,8 @@ function on_ios11() {
|
||||||
iframe.setAttribute('class', 'commitchange-iframe-embedded')
|
iframe.setAttribute('class', 'commitchange-iframe-embedded')
|
||||||
commitchange.iframes.push(iframe)
|
commitchange.iframes.push(iframe)
|
||||||
} else {
|
} else {
|
||||||
|
let overlay = commitchange.overlay()
|
||||||
|
let iframe
|
||||||
// Show the CommitChange-branded button if it's not set to custom.
|
// Show the CommitChange-branded button if it's not set to custom.
|
||||||
if(!elem.hasAttribute('data-custom') && !elem.hasAttribute('data-custom-button')) {
|
if(!elem.hasAttribute('data-custom') && !elem.hasAttribute('data-custom-button')) {
|
||||||
let btn_iframe = document.createElement('iframe')
|
let btn_iframe = document.createElement('iframe')
|
||||||
|
@ -160,8 +165,7 @@ function on_ios11() {
|
||||||
// Create the iframe overlay for this button
|
// Create the iframe overlay for this button
|
||||||
let modal = document.createElement('div')
|
let modal = document.createElement('div')
|
||||||
modal.className = 'commitchange-modal'
|
modal.className = 'commitchange-modal'
|
||||||
let overlay = commitchange.overlay()
|
|
||||||
let iframe
|
|
||||||
if(commitchange.modalIframe) {
|
if(commitchange.modalIframe) {
|
||||||
iframe = commitchange.modalIframe
|
iframe = commitchange.modalIframe
|
||||||
} else {
|
} else {
|
||||||
|
@ -211,8 +215,8 @@ function on_ios11() {
|
||||||
commitchange.loadStylesheet()
|
commitchange.loadStylesheet()
|
||||||
commitchange.appendMarkup()
|
commitchange.appendMarkup()
|
||||||
})
|
})
|
||||||
} else if(window.jQuery) {
|
} else if(windowAsAny.jQuery) {
|
||||||
window.jQuery(document).ready(() => {
|
windowAsAny.jQuery(document).ready(() => {
|
||||||
commitchange.loadStylesheet()
|
commitchange.loadStylesheet()
|
||||||
commitchange.appendMarkup()
|
commitchange.appendMarkup()
|
||||||
})
|
})
|
75
app/javascript/legacy/bank_accounts/confirm/index.es6
Normal file
75
app/javascript/legacy/bank_accounts/confirm/index.es6
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('virtual-dom/h')
|
||||||
|
const request = require('../../common/super-agent-frp')
|
||||||
|
const view = require('vvvview')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const flatMap = require('flyd/module/flatmap')
|
||||||
|
const thunk = require('vdom-thunk')
|
||||||
|
const format = require('../../common/format')
|
||||||
|
const Im = require('immutable')
|
||||||
|
const fromJS = Im.fromJS
|
||||||
|
const Map = Im.Map
|
||||||
|
|
||||||
|
var npURL = '/nonprofits/' + app.nonprofit_id
|
||||||
|
|
||||||
|
|
||||||
|
const root = state => {
|
||||||
|
var headerContent = ''
|
||||||
|
if(state.get('loading')) {
|
||||||
|
headerContent = [h('i.fa.fa-spin.fa-gear'), ' Confirming Bank Account...']
|
||||||
|
} else if(!state.get('loading') && !state.get('pending_verification')) {
|
||||||
|
headerContent = [h('i.fa.fa-check'), ' Bank Account Confirmed!']
|
||||||
|
} else { // not loading and unable to confirm
|
||||||
|
headerContent = ['Unable to confirm bank account.']
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmedMsg = !state.get('pending_verification')
|
||||||
|
? h('p', [
|
||||||
|
'Your bank account connection has been confirmed with your email address. ',
|
||||||
|
h("br"),
|
||||||
|
h('a', {href: npURL + '/payouts'}, [h('i.fa.fa-return'), 'Return to your payouts dashboard'])
|
||||||
|
])
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return h('div', [
|
||||||
|
h('h2', headerContent),
|
||||||
|
confirmedMsg,
|
||||||
|
thunk(accountInfo, state),
|
||||||
|
h('hr'),
|
||||||
|
h('p', [
|
||||||
|
'If any of this looks incorrect, please contact: ',
|
||||||
|
h('a', {href: 'mailto:support@commitchange.com'}, 'support@commitchange.com')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const accountInfo = state =>
|
||||||
|
h('div.well', [
|
||||||
|
h('p', ['Nonprofit: ', h('strong', state.getIn(['nonprofit', 'name'])), ]),
|
||||||
|
h('p', ['New bank account: ', h('strong', state.get('name')), ]),
|
||||||
|
h('p', ['User who made the change: ', h('strong', state.get('email')), ]),
|
||||||
|
h('p', ['Date and time of update: ', h('strong', format.date.toSimple(state.get('created_at'))), ]),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
var state = fromJS(app.bankAccount).set('loading', true)
|
||||||
|
|
||||||
|
var confirmView = view(root, document.querySelector('.js-view-confirm'), state)
|
||||||
|
|
||||||
|
|
||||||
|
if(app.bankAccount.pending_verification) {
|
||||||
|
var $confirmResponse = request.post(npURL + '/bank_account/confirm')
|
||||||
|
.send({token: utils.get_param('t')})
|
||||||
|
.perform()
|
||||||
|
|
||||||
|
var $state = flyd.scan(
|
||||||
|
(state, resp) => state.set('loading', false).set('pending_verification', false)
|
||||||
|
, state
|
||||||
|
, $confirmResponse)
|
||||||
|
|
||||||
|
flyd.map(confirmView, $state)
|
||||||
|
} else {
|
||||||
|
confirmView(state.set('loading', false).set('pending_verification', false))
|
||||||
|
}
|
||||||
|
|
2
app/javascript/legacy/bank_accounts/confirm/page.js
Normal file
2
app/javascript/legacy/bank_accounts/confirm/page.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('./index.es6')
|
75
app/javascript/legacy/bank_accounts/create.es6
Executable file
75
app/javascript/legacy/bank_accounts/create.es6
Executable file
|
@ -0,0 +1,75 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var request = require('../common/super-agent-promise')
|
||||||
|
var format_err = require('../common/format_response_error')
|
||||||
|
|
||||||
|
module.exports = create_bank_account
|
||||||
|
|
||||||
|
function create_bank_account(form_data, el) {
|
||||||
|
return new Promise((resolve, reject) =>{
|
||||||
|
appl.def('new_bank_account', {loading: true, error: ''})
|
||||||
|
return confirm_auth(form_data)
|
||||||
|
.then(tokenize_with_stripe)
|
||||||
|
.then(create_record)
|
||||||
|
.then(complete)
|
||||||
|
.catch(display_err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Post to confirm user's password
|
||||||
|
function confirm_auth(form_data) {
|
||||||
|
|
||||||
|
return request.post('/users/confirm_auth').send({password: form_data.user_password})
|
||||||
|
.perform()
|
||||||
|
.then((resp) =>{ return {token: resp.body.token, form: form_data}})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Post to stripe to get back a stripe_bank_account_token
|
||||||
|
function tokenize_with_stripe(data) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
Stripe.bankAccount.createToken(data.form, function(status, resp) {
|
||||||
|
data.stripe_resp = resp
|
||||||
|
if(resp.error) reject(resp.error.message)
|
||||||
|
else resolve(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 'data' must have a stripe response as '.stripe_resp' and a user password confirmation token as '.token
|
||||||
|
function create_record(data) {
|
||||||
|
return request.post('/nonprofits/' + app.nonprofit_id + '/bank_account')
|
||||||
|
.send({
|
||||||
|
pw_token: data.token,
|
||||||
|
bank_account: {
|
||||||
|
stripe_bank_account_token: data.stripe_resp.id,
|
||||||
|
stripe_bank_account_id: data.stripe_resp.bank_account.id,
|
||||||
|
name: data.stripe_resp.bank_account.bank_name + ' *' + data.stripe_resp.bank_account.last4,
|
||||||
|
email: app.user.email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.perform()
|
||||||
|
}
|
||||||
|
|
||||||
|
function complete() {
|
||||||
|
appl.is_loading()
|
||||||
|
appl.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_err(resp) {
|
||||||
|
|
||||||
|
var error_message = null;
|
||||||
|
|
||||||
|
if (typeof resp == 'string')
|
||||||
|
error_message = resp
|
||||||
|
else
|
||||||
|
error_message = format_err(resp)
|
||||||
|
|
||||||
|
appl.def('new_bank_account', {error: error_message, loading: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var request = require('../common/super-agent-frp')
|
||||||
|
|
||||||
|
var a = document.querySelector(".js-event-resendBankConfirmEmail")
|
||||||
|
|
||||||
|
if(a) a.addEventListener('click', resendBankConfirmation)
|
||||||
|
|
||||||
|
function resendBankConfirmation() {
|
||||||
|
request.post('/nonprofits/' + app.nonprofit_id + '/bank_account/resend_confirmation').perform()
|
||||||
|
appl.open_modal('bankConfirmResendModal')
|
||||||
|
}
|
4
app/javascript/legacy/campaigns/index/page.js
Normal file
4
app/javascript/legacy/campaigns/index/page.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
if(app.user)
|
||||||
|
require('../new/wizard')
|
||||||
|
|
43
app/javascript/legacy/campaigns/new/peer_to_peer_wizard.js
Normal file
43
app/javascript/legacy/campaigns/new/peer_to_peer_wizard.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
//This is used for federated p2p campaigns
|
||||||
|
require('../../components/wizard')
|
||||||
|
var format_err = require('../../common/format_response_error')
|
||||||
|
|
||||||
|
appl.def('advance_p2p_campaign_name_step', function(form_obj) {
|
||||||
|
var name = form_obj['campaign[name]']
|
||||||
|
appl.def('new_p2p_campaign', form_obj)
|
||||||
|
appl.wizard.advance('new_p2p_campaign_wiz')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Post a new campaign.
|
||||||
|
appl.def('create_p2p_campaign', function(el) {
|
||||||
|
var form_data = utils.toFormData(appl.prev_elem(el))
|
||||||
|
form_data = utils.mergeFormData(form_data, appl.new_p2p_campaign)
|
||||||
|
appl.def('new_p2p_campaign_wiz.loading', true)
|
||||||
|
|
||||||
|
post_p2p_campaign(form_data)
|
||||||
|
.then(function(req) {
|
||||||
|
appl.notify("Redirecting to your campaign...")
|
||||||
|
appl.redirect(JSON.parse(req.response).url)
|
||||||
|
})
|
||||||
|
.catch(function(req) {
|
||||||
|
appl.def('new_p2p_campaign_wiz.loading', false)
|
||||||
|
appl.def('new_p2p_campaign_wiz.error', req.responseText)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
|
||||||
|
function post_p2p_campaign(form_data) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var req = new XMLHttpRequest()
|
||||||
|
req.open("POST", '/nonprofits/' + app.nonprofit_id + '/campaigns')
|
||||||
|
req.setRequestHeader('X-CSRF-Token', window._csrf)
|
||||||
|
console.log(form_data)
|
||||||
|
req.send(form_data)
|
||||||
|
req.onload = function(ev) {
|
||||||
|
if(req.status === 200) resolve(req)
|
||||||
|
else reject(req)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
57
app/javascript/legacy/campaigns/new/wizard.js
Normal file
57
app/javascript/legacy/campaigns/new/wizard.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('../../common/pikaday-timepicker')
|
||||||
|
require('../../components/wizard')
|
||||||
|
require('../../common/image_uploader')
|
||||||
|
var checkName = require('../../common/ajax/check_campaign_or_event_name')
|
||||||
|
var format_err = require('../../common/format_response_error')
|
||||||
|
|
||||||
|
|
||||||
|
appl.def('advance_campaign_name_step', function(form_obj) {
|
||||||
|
var name = form_obj['campaign[name]']
|
||||||
|
checkName(name, 'campaign', function(){
|
||||||
|
appl.def('new_campaign', form_obj)
|
||||||
|
appl.wizard.advance('new_campaign_wiz')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Post a new campaign.
|
||||||
|
appl.def('create_campaign', function(el) {
|
||||||
|
var form_data = utils.toFormData(appl.prev_elem(el))
|
||||||
|
form_data = utils.mergeFormData(form_data, appl.new_campaign)
|
||||||
|
appl.def('new_campaign_wiz.loading', true)
|
||||||
|
|
||||||
|
// TODO: for p2p capmaigns, merge with preset campaing params
|
||||||
|
|
||||||
|
post_campaign(form_data)
|
||||||
|
.then(function(req) {
|
||||||
|
appl.notify("Redirecting to your campaign...")
|
||||||
|
appl.redirect(JSON.parse(req.response).url)
|
||||||
|
})
|
||||||
|
.catch(function(req) {
|
||||||
|
appl.def('new_campaign_wiz.loading', false)
|
||||||
|
appl.def('new_campaign_wiz.error', req.responseText)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
var Pikaday = require('pikaday')
|
||||||
|
var moment = require('moment')
|
||||||
|
new Pikaday({
|
||||||
|
field: document.querySelector('.js-date-picker'),
|
||||||
|
format: 'M/D/YYYY',
|
||||||
|
minDate: moment().toDate()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
|
||||||
|
function post_campaign(form_data) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
var req = new XMLHttpRequest()
|
||||||
|
req.open("POST", '/nonprofits/' + app.nonprofit_id + '/campaigns')
|
||||||
|
req.setRequestHeader('X-CSRF-Token', window._csrf)
|
||||||
|
req.send(form_data)
|
||||||
|
req.onload = function(ev) {
|
||||||
|
if(req.status === 200) resolve(req)
|
||||||
|
else reject(req)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
129
app/javascript/legacy/campaigns/peer_to_peer/page.js
Normal file
129
app/javascript/legacy/campaigns/peer_to_peer/page.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('../new/peer_to_peer_wizard')
|
||||||
|
require('../new/wizard.js')
|
||||||
|
require('../../common/image_uploader')
|
||||||
|
|
||||||
|
var request = require("../../common/client")
|
||||||
|
|
||||||
|
appl.def('undelete_p2p', function (url){
|
||||||
|
appl.def('loading', true)
|
||||||
|
request.put(url + '/soft_delete', {delete: false}).end(function(err, resp) {
|
||||||
|
if (err) {
|
||||||
|
appl.def('loading', false)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
window.location = url
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// setting up some default values
|
||||||
|
appl.def('is_signing_up', true)
|
||||||
|
.def('selected_result_index', -1)
|
||||||
|
|
||||||
|
|
||||||
|
appl.def('search_nonprofits', function(value){
|
||||||
|
// keyCode 13 is the return key.
|
||||||
|
// this conditional just clears the dropdown
|
||||||
|
if(event.keyCode === 13) {
|
||||||
|
appl.def('search_results', [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// when the user starts typing,
|
||||||
|
// it sets the selected_results key to false
|
||||||
|
appl.def('selected_result', false)
|
||||||
|
|
||||||
|
// if the the input is empty, it clears the dropdown
|
||||||
|
if (!value) {
|
||||||
|
appl.def('search_results', [])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// logic for controlling the dropdown options with up
|
||||||
|
// and down arrows
|
||||||
|
if (returnUpOrDownArrow() && appl.search_results && appl.search_results.length) {
|
||||||
|
event.preventDefault()
|
||||||
|
setIndexWithArrows(returnUpOrDownArrow())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the input is not an up or down arrow or an empty string
|
||||||
|
// or a return key, then it searches for nonprofits
|
||||||
|
utils.delay(300, function(){ajax_nonprofit_search(value)})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function ajax_nonprofit_search(value){
|
||||||
|
request.get('/nonprofits/search?npo_name=' + value).end(function(err, resp){
|
||||||
|
if(!resp.body) {
|
||||||
|
appl.def('search_results', [])
|
||||||
|
appl.notify("Sorry, we couldn't find any nonprofits containing the word '" + value + "'")
|
||||||
|
} else {
|
||||||
|
appl.def('selected_result_index', -1)
|
||||||
|
appl.def('search_results', resp.body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function returnUpOrDownArrow() {
|
||||||
|
var keyCode = event.keyCode
|
||||||
|
if(keyCode === 38)
|
||||||
|
return 'up'
|
||||||
|
if(keyCode === 40)
|
||||||
|
return 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setIndexWithArrows(dir) {
|
||||||
|
if(dir === 'down') {
|
||||||
|
var search_length = appl.search_results.length -1
|
||||||
|
appl.def('selected_result_index', appl.selected_result_index === search_length
|
||||||
|
? search_length
|
||||||
|
: appl.selected_result_index += 1)
|
||||||
|
} else {
|
||||||
|
appl.def('selected_result_index', appl.selected_result_index === 0
|
||||||
|
? 0
|
||||||
|
: appl.selected_result_index -= 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appl.def('select_result', {
|
||||||
|
with_arrows: function(i, node) {
|
||||||
|
addSelectedClass(appl.prev_elem(node))
|
||||||
|
var selected = appl.search_results[appl.selected_result_index]
|
||||||
|
app.nonprofit_id = selected.id
|
||||||
|
appl.def('selected_result', selected)
|
||||||
|
utils.change_url_param('npo_id', selected.id, '/peer-to-peer')
|
||||||
|
},
|
||||||
|
with_click: function(i, node) {
|
||||||
|
appl.def('selected_result_index', i)
|
||||||
|
addSelectedClass(appl.prev_elem(node))
|
||||||
|
var selected = appl.search_results[i]
|
||||||
|
app.nonprofit_id = selected.id
|
||||||
|
appl.def('selected_result', selected)
|
||||||
|
appl.def('search_results', [])
|
||||||
|
utils.change_url_param('npo_id', selected.id, '/peer-to-peer')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function addSelectedClass(node) {
|
||||||
|
if(!node || !node.parentElement) return
|
||||||
|
var siblings = node.parentElement.querySelectorAll('li')
|
||||||
|
var len = siblings.length
|
||||||
|
while(len--){siblings[len].className=''}
|
||||||
|
node.className = 'is-selected'
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is for clearing the dropdown
|
||||||
|
var main = document.querySelector('main')
|
||||||
|
|
||||||
|
main.onclick = function(ev) {
|
||||||
|
var node = ev.target.nodeName
|
||||||
|
if(node === 'INPUT' || node === 'BUTTON') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
appl.def('search_results', [])
|
||||||
|
}
|
113
app/javascript/legacy/campaigns/show/admin.js
Normal file
113
app/javascript/legacy/campaigns/show/admin.js
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('../../common/pikaday-timepicker')
|
||||||
|
require('../../common/restful_resource')
|
||||||
|
const request = require('../../common/client')
|
||||||
|
const formatErr = require('../../common/format_response_error')
|
||||||
|
require('../../common/image_uploader')
|
||||||
|
require('./tour')
|
||||||
|
const dupeIt = require('../../components/duplicate_fundraiser')
|
||||||
|
|
||||||
|
dupeIt(`/nonprofits/${app.nonprofit_id}/campaigns`, app.campaign_id)
|
||||||
|
|
||||||
|
var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
|
||||||
|
var create_supporter = require('../../nonprofits/supporters/create')
|
||||||
|
var create_offline_donation = require('../../donations/create_offline')
|
||||||
|
|
||||||
|
require('../../components/ajax/toggle_soft_delete')(url, 'campaign')
|
||||||
|
|
||||||
|
// Initialize the froala wysiwyg
|
||||||
|
var editable = require('../../common/editable')
|
||||||
|
if (app.is_parent_campaign) {
|
||||||
|
editable($('#js-campaignBody'), {
|
||||||
|
sticky: true,
|
||||||
|
placeholder: "Add your campaign's story here. We strongly recommend that this section is filled out with at least 250 words. It will be saved automatically as you type. You can add images, videos and custom HTML too."
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
editable($('#js-customReceipt'), {
|
||||||
|
button: ["bold", "italic", "formatBlock", "align", "createLink",
|
||||||
|
"insertImage", "insertUnorderedList", "insertOrderedList",
|
||||||
|
"undo", "redo", "insert_donate_button", "html"]
|
||||||
|
, placeholder: "Add optional message here. It will be saved automatically as you type."
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var path = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
|
||||||
|
|
||||||
|
|
||||||
|
appl.def('remove_banner_image', function() {
|
||||||
|
var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
|
||||||
|
var notification = 'Removing banner image...'
|
||||||
|
var payload = {remove_banner_image : true}
|
||||||
|
appl.remove_image(url, 'campaign', notification, payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('remove_background_image', function() {
|
||||||
|
var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id
|
||||||
|
var notification = 'Removing background image...'
|
||||||
|
var payload = {remove_background_image : true}
|
||||||
|
appl.remove_image(url, 'campaign', notification, payload)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('count_story_words', function() {
|
||||||
|
var wysiwyg = document.querySelector(".editable")
|
||||||
|
appl.def('has_story', wysiwyg.textContent.split(' ').length > 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('highlight_body', function(){
|
||||||
|
appl.def('body_is_highlighted', true)
|
||||||
|
appl.close_modal()
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.count_story_words(document.querySelector('.campaign-body'))
|
||||||
|
|
||||||
|
appl.def('track_launch', function() {
|
||||||
|
window.location.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('create_offline_donation', function(data, el) {
|
||||||
|
create_supporter({supporter: data.supporter}, createSupporterUI)
|
||||||
|
.then(function(resp) {
|
||||||
|
data.supporter_id = resp.body.id
|
||||||
|
delete data.supporter
|
||||||
|
return create_offline_donation(data, createDonationUI)
|
||||||
|
}).then(function(el){
|
||||||
|
appl.ajax_metrics.index()
|
||||||
|
appl.prev_elem(el).reset()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
var createSupporterUI = {
|
||||||
|
start: function() {
|
||||||
|
appl.is_loading()
|
||||||
|
},
|
||||||
|
success: function() {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
fail: function(msg) {
|
||||||
|
appl.def('error', formatErr(msg))
|
||||||
|
appl.not_loading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var createDonationUI = {
|
||||||
|
start: function() { },
|
||||||
|
success: function(resp) {
|
||||||
|
appl.not_loading()
|
||||||
|
appl.close_modal()
|
||||||
|
appl.notify('Campaign Donation Saved!')
|
||||||
|
},
|
||||||
|
fail: function(msg){
|
||||||
|
appl.def('error', formatErr(msg))
|
||||||
|
appl.not_loading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(app.vimeo_id) {
|
||||||
|
request.get('http://vimeo.com/api/v2/video/' + app.vimeo_id + '.json')
|
||||||
|
.end(function(err, resp){
|
||||||
|
appl.def('vimeo_image_url', "background-image:url('" + resp.body[0].thumbnail_small + "')")
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
const soldOut = require('./is-sold-out')
|
||||||
|
const giftButton = require('./gift-option-button')
|
||||||
|
|
||||||
|
const giftOption = g => h('option', { props: {value: g.id} }, g.name)
|
||||||
|
|
||||||
|
const setDisplayGift = (state, gifts) => ev => {
|
||||||
|
var id = Number(ev.target.value)
|
||||||
|
state.selectedModalGift$(R.find(R.propEq('id', id))(gifts))
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseGift = (state, gifts) =>
|
||||||
|
h('div.pastelBox--grey.u-padding--10', [
|
||||||
|
h('select.u-margin--0', {on: {change: setDisplayGift(state, gifts)}}
|
||||||
|
, R.concat([h('option', 'Choose a gift option')], R.map(giftOption, gifts)))
|
||||||
|
, h('div.sideGifts',
|
||||||
|
state.selectedModalGift$() && state.selectedModalGift$().id
|
||||||
|
? [
|
||||||
|
h('p.u-marginTop--10', state.selectedModalGift$().description || '')
|
||||||
|
, giftButton(state.giftOptions, state.selectedModalGift$())
|
||||||
|
]
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
const regularContribution = state => {
|
||||||
|
if (app.campaign.hide_custom_amounts) return ''
|
||||||
|
return h('div.u-marginTop--15.centered', [
|
||||||
|
h('a', {on: {click: state.clickRegularContribution$}}, 'Contribute with no gift option')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = state => {
|
||||||
|
var gifts = R.filter(g => !soldOut(g), state.giftOptions.giftOptions$() || [])
|
||||||
|
return h('div.u-padding--15', [
|
||||||
|
chooseGift(state, gifts)
|
||||||
|
, regularContribution(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
57
app/javascript/legacy/campaigns/show/gift-option-button.js
Normal file
57
app/javascript/legacy/campaigns/show/gift-option-button.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const branding = require('../../components/nonprofit-branding')
|
||||||
|
const format = require('../../common/format')
|
||||||
|
const soldOut = require('./is-sold-out')
|
||||||
|
|
||||||
|
// function prepareForIOS11()
|
||||||
|
// {
|
||||||
|
// bad_elements = $('.ff-modalBackdrop')
|
||||||
|
// for(var i = 0; i < bad_elements.length; i++)
|
||||||
|
// {
|
||||||
|
// bad_elements[i].classList.add('ios-force-absolute-positioning')
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// $('body').scrollTop(195) // so incredibly hacky
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = (state, gift) => {
|
||||||
|
if(state.timeRemaining$() <= 0) return '' // dont show gift options button if the campaign has ended
|
||||||
|
return h('table', {
|
||||||
|
class: {'u-hide': !gift.amount_one_time && !gift.amount_recurring}
|
||||||
|
}, [
|
||||||
|
h('tr', [
|
||||||
|
gift.amount_one_time
|
||||||
|
? h('td', [
|
||||||
|
h('button.button--small.button--gift', {
|
||||||
|
on: {click: ev => {
|
||||||
|
|
||||||
|
|
||||||
|
state.clickOption$([gift, gift.amount_one_time, 'one-time'])}
|
||||||
|
}
|
||||||
|
|
||||||
|
, style: {background: branding.dark}
|
||||||
|
, props: {title: `Contribute towards ${gift.name}`}
|
||||||
|
, class: {disabled: soldOut(gift)}
|
||||||
|
}, [ h('span.dollar', '$ ') , format.centsToDollars(gift.amount_one_time), h('br'), h('small', 'One-time') ])
|
||||||
|
])
|
||||||
|
: '' // no one-time amount
|
||||||
|
, gift.amount_recurring && gift.amount_one_time ? h('td.orWithLine') : '' // whether to show the cool OR graphic between buttons
|
||||||
|
, gift.amount_recurring
|
||||||
|
? h('td', [
|
||||||
|
h('button.button--small.button--gift', {
|
||||||
|
on: {click: ev => {
|
||||||
|
|
||||||
|
state.clickOption$([gift, gift.amount_recurring, 'recurring'])}
|
||||||
|
}
|
||||||
|
, style: {background: branding.dark}
|
||||||
|
, props: {title: `Contribute monthly towards ${gift.name}`}
|
||||||
|
, class: {disabled: soldOut(gift)}
|
||||||
|
}, [h('span.dollar', '$ '), format.centsToDollars(gift.amount_recurring), h('br'), h('small', 'Monthly') ])
|
||||||
|
])
|
||||||
|
: '' // no recurring amount
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
81
app/javascript/legacy/campaigns/show/gift-option-list.js
Normal file
81
app/javascript/legacy/campaigns/show/gift-option-list.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
const request = require('../../common/request')
|
||||||
|
const format = require('../../common/format')
|
||||||
|
const branding = require('../../components/nonprofit-branding')
|
||||||
|
flyd.mergeAll = require('flyd/module/mergeall')
|
||||||
|
|
||||||
|
const quantityLeft = require('./gift-option-quantity-left')
|
||||||
|
const giftButton = require('./gift-option-button')
|
||||||
|
|
||||||
|
// Pass in a stream that has a value when the gift options need to be refreshed, so we know when to refresh em!
|
||||||
|
function init(giftsNeedRefresh$, parentState) {
|
||||||
|
var state = {
|
||||||
|
timeRemaining$: parentState.timeRemaining$
|
||||||
|
, clickOption$: flyd.stream()
|
||||||
|
, openEditGiftModal$: flyd.stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX some legacy viewscript mixed in here
|
||||||
|
flyd.map(gift => {
|
||||||
|
appl.open_modal('giftOptionFormModal')
|
||||||
|
appl.def('gift_options', {current: gift, is_updating: true})
|
||||||
|
appl.def('gift_option_action', 'Edit')
|
||||||
|
}, state.openEditGiftModal$)
|
||||||
|
|
||||||
|
const pageloadGifts$ = index()
|
||||||
|
const refreshedGifts$ = flyd.flatMap(index, giftsNeedRefresh$)
|
||||||
|
state.giftOptions$ = flyd.mergeAll([
|
||||||
|
pageloadGifts$
|
||||||
|
, refreshedGifts$
|
||||||
|
, flyd.stream([]) // default before ajax loads
|
||||||
|
])
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function index() {
|
||||||
|
const path = `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign_id}/campaign_gift_options`
|
||||||
|
return flyd.map(
|
||||||
|
req => req.body.data
|
||||||
|
, request({path, method: 'get'}).load
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function view(state) {
|
||||||
|
return h('aside.sideGifts.u-marginBottom--15', {
|
||||||
|
class: {'u-hide': !state.giftOptions$().length}
|
||||||
|
}, R.map(giftBox(state), state.giftOptions$())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const giftBox = state => gift => {
|
||||||
|
return h('section.u-relative', [
|
||||||
|
h('div.sideGift.pastelBox--grey--dark', [
|
||||||
|
h('h5.u-marginTop--0', gift.name)
|
||||||
|
, totalContributions(gift)
|
||||||
|
, quantityLeft(gift)
|
||||||
|
, h('p.u-marginBottom--15', gift.description)
|
||||||
|
, h('div', [ giftButton(state, gift) ])
|
||||||
|
])
|
||||||
|
, (app.current_campaign_editor && app.is_parent_campaign) // Show edit button only if the current user is a parent campaign editor
|
||||||
|
? h('button.button--tiny.absolute.edit.hasShadow', {
|
||||||
|
on: {click: ev => state.openEditGiftModal$(gift)}
|
||||||
|
}, [
|
||||||
|
h('i.fa.fa-pencil')
|
||||||
|
, ' Edit Gift'
|
||||||
|
])
|
||||||
|
: '' // do not show gift edit button
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalContributions = gift => {
|
||||||
|
if(gift.hide_contributions) return ''
|
||||||
|
return h('p', [
|
||||||
|
h('i.fa.fa-star', { style: { color: branding.base} })
|
||||||
|
, ` ${format.numberWithCommas(gift.total_gifts)} Contribution${gift.total_gifts === 1 ? '' : 's'}`
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {view, init}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const soldOut = require('./is-sold-out')
|
||||||
|
|
||||||
|
module.exports = gift => {
|
||||||
|
if(gift.hide_contributions || !gift.quantity) return ''
|
||||||
|
|
||||||
|
if(soldOut(gift)) {
|
||||||
|
return h('p', [
|
||||||
|
h('small.strong.highlight--white--small', 'SOLD OUT')
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
return h('p', [
|
||||||
|
h('small.strong.highlight--white--small', [ `${gift.quantity - gift.total_gifts} Left` ])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
3
app/javascript/legacy/campaigns/show/is-sold-out.js
Normal file
3
app/javascript/legacy/campaigns/show/is-sold-out.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = g => g.quantity && (g.quantity - g.total_gifts <= 0)
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const format = require('../../common/format')
|
||||||
|
const branding = require('../../components/nonprofit-branding')
|
||||||
|
|
||||||
|
// This is the box currently at the top right that shows some big metrics for
|
||||||
|
// the campaign, a big Contribute button (if enabled to show), days remaining
|
||||||
|
// (and a "campaign is done" message if no days remaining)
|
||||||
|
|
||||||
|
function init(parentState) {
|
||||||
|
var state = {
|
||||||
|
clickContribute$: flyd.stream()
|
||||||
|
, timeRemaining$: parentState.timeRemaining$
|
||||||
|
, metrics$: parentState.metrics$
|
||||||
|
, loading$: parentState.loadingMetrics$
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function view(state) {
|
||||||
|
return h('div.pastelBox--grey--dark.u-relative.u-marginBottom--15.u-padding--15', [
|
||||||
|
metrics(state)
|
||||||
|
, endedMessage(state)
|
||||||
|
, progressBar(state)
|
||||||
|
, contributeButton(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = state => {
|
||||||
|
return h('div.campaignMetrics', [
|
||||||
|
totalSupporters(state)
|
||||||
|
, totalRaised(state)
|
||||||
|
, daysLeft(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSupporters = state => {
|
||||||
|
if(!app.campaign.show_total_count) return ''
|
||||||
|
return h('div', [
|
||||||
|
h('h4', [
|
||||||
|
state.loading$() ? h('i.fa.fa-spin.fa-spinner') : format.numberWithCommas(state.metrics$().supporters_count)
|
||||||
|
])
|
||||||
|
, h('p', 'supporters')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalRaised = state => {
|
||||||
|
if(!app.campaign.show_total_raised) return ''
|
||||||
|
return h('div', [
|
||||||
|
h('h4', [
|
||||||
|
state.loading$() ? h('i.fa.fa-spin.fa-spinner') : '$' + format.centsToDollars(state.metrics$().total_raised, {noCents: true})
|
||||||
|
])
|
||||||
|
, h('p', [
|
||||||
|
'raised'
|
||||||
|
, app.campaign.hide_goal
|
||||||
|
? ''
|
||||||
|
: ' of $' + format.centsToDollars(app.campaign.goal_amount) + ' goal'
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysLeft = state => {
|
||||||
|
if(!state.timeRemaining$()) return ''
|
||||||
|
return h('div', [
|
||||||
|
h('h4', state.timeRemaining$())
|
||||||
|
, h('p', 'remaining')
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const endedMessage = state => {
|
||||||
|
if(state.timeRemaining$()) return ''
|
||||||
|
return h('p', [
|
||||||
|
`This campaign has ended, but you can still contribute by clicking the button below.`
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressBar = state => {
|
||||||
|
if(app.campaign.hide_thermometer) return ''
|
||||||
|
return h('div.progressBar--medium.u-marginBottom--15', [
|
||||||
|
h('div.progressBar--medium-fill', {
|
||||||
|
style: {
|
||||||
|
width: R.clamp(1,100, format.percent(
|
||||||
|
state.metrics$().goal_amount
|
||||||
|
, state.metrics$().total_raised
|
||||||
|
) + '%')
|
||||||
|
, 'background-color': branding.light
|
||||||
|
, transition: 'width 1s'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const contributeButton = state => {
|
||||||
|
return h('a.js-contributeButton.button--jumbo.u-width--full', {
|
||||||
|
style: {'background-color': branding.base}
|
||||||
|
, on: {click: state.clickContribute$}
|
||||||
|
}, [ 'Contribute' ])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, view }
|
||||||
|
|
169
app/javascript/legacy/campaigns/show/page.js
Executable file
169
app/javascript/legacy/campaigns/show/page.js
Executable file
|
@ -0,0 +1,169 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const R = require('ramda')
|
||||||
|
const donateWiz = require('../../nonprofits/donate/wizard')
|
||||||
|
const render = require('ff-core/render')
|
||||||
|
const snabbdom = require('snabbdom')
|
||||||
|
const modal = require('ff-core/modal')
|
||||||
|
flyd.mergeAll = require('flyd/module/mergeall')
|
||||||
|
flyd.scanMerge = require('flyd/module/scanmerge')
|
||||||
|
const format = require('../../common/format')
|
||||||
|
const giftOptions = require('./gift-option-list')
|
||||||
|
const chooseGiftOptionsModal = require('./choose-gift-options-modal')
|
||||||
|
const metricsAndContributeBox = require('./metrics-and-contribute-box')
|
||||||
|
const timeRemaining = require('../../common/time-remaining')
|
||||||
|
const request = require('../../common/request')
|
||||||
|
|
||||||
|
const activities = require('../../components/public-activities')
|
||||||
|
|
||||||
|
|
||||||
|
// Viewscript legacy side effect stuff
|
||||||
|
require('../../components/branded_fundraising')
|
||||||
|
require('../../common/on-change-sanitize-slug')
|
||||||
|
require('../../common/fundraiser_metrics')
|
||||||
|
require('../../components/fundraising/add_header_image')
|
||||||
|
require('../../common/restful_resource')
|
||||||
|
require('../../gift_options/index')
|
||||||
|
const on_ios11 = require('../../common/on-ios11')
|
||||||
|
const noScroll = require('no-scroll')
|
||||||
|
appl.ajax_gift_options.index()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Campaign editor only functionality
|
||||||
|
if(app.current_campaign_editor) {
|
||||||
|
require('./admin')
|
||||||
|
appl.def('current_campaign_editor', true)
|
||||||
|
require('../../gift_options/admin')
|
||||||
|
var create_info_card = require('../../supporters/info-card.es6')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the state for the top-level campaign component
|
||||||
|
// This includes the metrics, contribute button, gift options listing, and the donate wizard (most of right sidebar)
|
||||||
|
// Later can include the other viewscript pieces
|
||||||
|
function init() {
|
||||||
|
var state = {
|
||||||
|
timeRemaining$: timeRemaining(app.end_date_time, app.timezone),
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(window.navigator.userAgent)
|
||||||
|
state.giftOptions = giftOptions.init(flyd.stream(), state)
|
||||||
|
|
||||||
|
const metricsResp$ = flyd.map(R.prop('body'), request({
|
||||||
|
method: 'get'
|
||||||
|
, path: `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign.id}/metrics`
|
||||||
|
}).load)
|
||||||
|
state.loadingMetrics$ = flyd.mergeAll([
|
||||||
|
flyd.map(_ => false, metricsResp$)
|
||||||
|
, flyd.stream(true)
|
||||||
|
])
|
||||||
|
state.metrics$ = flyd.merge(
|
||||||
|
flyd.stream({goal_amount: 0, total_raised: 0, supporters_count: 0})
|
||||||
|
, metricsResp$
|
||||||
|
)
|
||||||
|
state.metrics = metricsAndContributeBox.init(state)
|
||||||
|
|
||||||
|
state.activities = activities.init('campaign', `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign_id}/activities`)
|
||||||
|
|
||||||
|
|
||||||
|
const contributeModalType$ = R.compose(
|
||||||
|
flyd.map(_ =>
|
||||||
|
state.timeRemaining$() && state.giftOptions.giftOptions$().length
|
||||||
|
? 'gifts' : 'regular')
|
||||||
|
)(state.metrics.clickContribute$)
|
||||||
|
|
||||||
|
const clickContributeGifts$ = flyd.filter(x => x === 'gifts', contributeModalType$)
|
||||||
|
|
||||||
|
const clickContributeRegular$ = flyd.filter(x => x === 'regular', contributeModalType$)
|
||||||
|
|
||||||
|
state.clickRegularContribution$ = flyd.stream()
|
||||||
|
|
||||||
|
const startWiz$ = flyd.mergeAll([
|
||||||
|
state.giftOptions.clickOption$
|
||||||
|
, clickContributeRegular$
|
||||||
|
, state.clickRegularContribution$
|
||||||
|
])
|
||||||
|
|
||||||
|
state.selectedModalGift$ = flyd.stream({})
|
||||||
|
|
||||||
|
state.modalID$ = flyd.merge(
|
||||||
|
flyd.map(R.always('chooseGiftOptionsModal'), clickContributeGifts$)
|
||||||
|
, flyd.map(R.always('donationModal'), startWiz$))
|
||||||
|
|
||||||
|
flyd.on((id) => {
|
||||||
|
if (on_ios11() && id === null) {
|
||||||
|
noScroll.off()
|
||||||
|
}
|
||||||
|
}, state.modalID$)
|
||||||
|
|
||||||
|
flyd.on((id) => {
|
||||||
|
if (on_ios11() && id !== null) {
|
||||||
|
noScroll.on()
|
||||||
|
}
|
||||||
|
}, state.modalID$)
|
||||||
|
|
||||||
|
// Stream of which gift option you have selected
|
||||||
|
const giftOption$ = flyd.map(setGiftParams, state.giftOptions.clickOption$)
|
||||||
|
const donateParam$ = flyd.scanMerge([
|
||||||
|
[state.metrics.clickContribute$, resetDonateForm]
|
||||||
|
, [giftOption$, setGiftOption]
|
||||||
|
], {campaign_id: app.campaign.id} )
|
||||||
|
|
||||||
|
state.donateWiz = donateWiz.init(donateParam$)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetDonateForm = (params, _) => R.merge(params, {
|
||||||
|
single_amount: undefined
|
||||||
|
, gift_option: undefined
|
||||||
|
, type: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const setGiftOption = (params, gift) => R.merge(params, {
|
||||||
|
single_amount: gift.amount / 100
|
||||||
|
, gift_option: gift
|
||||||
|
, type: gift.type
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Set the donate wizard parameters using data from a gift option
|
||||||
|
const setGiftParams = (triple) => {
|
||||||
|
var [gift, amount, type] = triple
|
||||||
|
return { amount: amount, type: type , id: gift.id, name: gift.name, to_ship: gift.to_ship}
|
||||||
|
}
|
||||||
|
|
||||||
|
function view(state) {
|
||||||
|
return h('div', [
|
||||||
|
metricsAndContributeBox.view(state.metrics)
|
||||||
|
, giftOptions.view(state.giftOptions)
|
||||||
|
, activities.view(state.activities)
|
||||||
|
, h('div.donationModal', [
|
||||||
|
modal({
|
||||||
|
thisID: 'donationModal'
|
||||||
|
, id$: state.modalID$
|
||||||
|
, body: donateWiz.view(state.donateWiz)
|
||||||
|
// , notCloseable: state.donateWiz.paymentStep.cardForm.loading$()
|
||||||
|
})
|
||||||
|
, modal({
|
||||||
|
thisID: 'chooseGiftOptionsModal'
|
||||||
|
, title: 'Contribute'
|
||||||
|
, id$: state.modalID$
|
||||||
|
, body: chooseGiftOptionsModal(state)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Render to the page
|
||||||
|
|
||||||
|
const patch = snabbdom.init([
|
||||||
|
require('snabbdom/modules/eventlisteners')
|
||||||
|
, require('snabbdom/modules/class')
|
||||||
|
, require('snabbdom/modules/props')
|
||||||
|
, require('snabbdom/modules/style')
|
||||||
|
])
|
||||||
|
|
||||||
|
render({state: init(), view, patch, container: document.querySelector('.ff-sidebar')})
|
||||||
|
|
35
app/javascript/legacy/campaigns/show/tour.js
Normal file
35
app/javascript/legacy/campaigns/show/tour.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('../../common/vendor/bootstrap-tour-standalone')
|
||||||
|
|
||||||
|
var tour_campaign = new Tour({
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
orphan: true,
|
||||||
|
title: 'Welcome to your new campaign!',
|
||||||
|
content: "Click 'Next' to find out how you can flesh out your campaign before sharing it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Manage your campaign',
|
||||||
|
placement: 'bottom',
|
||||||
|
element: '.tour-admin',
|
||||||
|
content: "You can manage your campaign by clicking on these buttons at the top of the page."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.froala-box',
|
||||||
|
title: 'Write your story',
|
||||||
|
content: "Every successful campaign has a powerful story. Write and edit your story in the area to the left. You can add formatting by clicking the icons at the top of this box."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
orphan: true,
|
||||||
|
title: 'You’re on your way!',
|
||||||
|
content: "Once you’ve written your campaign story and added gift options, you can start sharing it with all your contacts. We’re excited for it to succeed!"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if($.cookie('tour_campaign') === String(app.nonprofit_id)) {
|
||||||
|
$.removeCookie('tour_campaign', {path: '/'})
|
||||||
|
tour_campaign.init()
|
||||||
|
tour_campaign.restart()
|
||||||
|
}
|
||||||
|
|
63
app/javascript/legacy/campaigns/supporters/index/index.es6
Normal file
63
app/javascript/legacy/campaigns/supporters/index/index.es6
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const request = require('../../../common/super-agent-frp')
|
||||||
|
const view = require('vvvview')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const scanMerge = require('flyd/module/scanmerge')
|
||||||
|
const flatMap = require('flyd/module/flatmap')
|
||||||
|
const Im = require('immutable')
|
||||||
|
const Map = Im.Map
|
||||||
|
const fromJS = Im.fromJS
|
||||||
|
|
||||||
|
const list = require('./supporter-list.es6')
|
||||||
|
|
||||||
|
var el = document.querySelector('.js-view-supporters')
|
||||||
|
var state = Map({loading: true})
|
||||||
|
var listView = view(list.root, el, state)
|
||||||
|
|
||||||
|
// Given a query object, return an ajax stream
|
||||||
|
const request_index = query =>
|
||||||
|
request
|
||||||
|
.get(`/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/supporters`)
|
||||||
|
.query(query)
|
||||||
|
.perform()
|
||||||
|
|
||||||
|
var $searchResponses = flatMap(request_index, list.$streams.searches)
|
||||||
|
|
||||||
|
const appendPage = (state, resp) => {
|
||||||
|
var oldSupporters = state.getIn(['supporters', 'data'])
|
||||||
|
var newData = fromJS(resp.body)
|
||||||
|
if(oldSupporters) newData = newData.set('data', oldSupporters.concat(newData.get('data')))
|
||||||
|
return state
|
||||||
|
.set('supporters', newData)
|
||||||
|
.set('moreLoading', false)
|
||||||
|
.set('loading', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const $showMorePages = flyd.scan(
|
||||||
|
count => count + 1
|
||||||
|
, 1
|
||||||
|
, list.$streams.showMore)
|
||||||
|
|
||||||
|
const $newPages = flatMap(
|
||||||
|
page => request_index({page: page})
|
||||||
|
, $showMorePages)
|
||||||
|
|
||||||
|
const setResults = (state, resp) =>
|
||||||
|
state.set('supporters', fromJS(resp.body)).set('loading', false)
|
||||||
|
|
||||||
|
var $giftLevelResponses =
|
||||||
|
request.get(`/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/campaign_gift_options`).perform()
|
||||||
|
|
||||||
|
var $state = flyd.immediate(scanMerge([
|
||||||
|
[list.$streams.searches, state => state.set('loading', true).set('isSearching', true)],
|
||||||
|
[list.$streams.showMore, state => state.set('moreLoading', true).set('page', state.get('page') + 1)],
|
||||||
|
[$newPages, appendPage],
|
||||||
|
[$searchResponses, setResults],
|
||||||
|
[$giftLevelResponses, (state, resp) => state.set('gift_levels', fromJS(resp.body))],
|
||||||
|
], state))
|
||||||
|
|
||||||
|
window.$state =$state
|
||||||
|
window.$giftLevelResponses= $giftLevelResponses
|
||||||
|
|
||||||
|
flyd.map(listView, $state)
|
||||||
|
|
27
app/javascript/legacy/campaigns/supporters/index/meta.es6
Normal file
27
app/javascript/legacy/campaigns/supporters/index/meta.es6
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// Table meta for the supporter listing under Campaigns
|
||||||
|
const h = require('virtual-dom/h')
|
||||||
|
const thunk = require('vdom-thunk')
|
||||||
|
const search = require('../../../components/tables/search.es6')
|
||||||
|
|
||||||
|
const root = state =>
|
||||||
|
h('div.container', [
|
||||||
|
thunk(search.root, state),
|
||||||
|
h('a.table-meta-button.white', {
|
||||||
|
href: `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/donations.csv`,
|
||||||
|
target: '_blank',
|
||||||
|
}, [ h('i.fa.fa-file-text'), ' Export ' ]),
|
||||||
|
/*
|
||||||
|
h('a.table-meta-button.green', {
|
||||||
|
onclick: $.showEmailModal
|
||||||
|
}, [ h('i.fa.fa-envelope'), ' Email ' ])
|
||||||
|
*/
|
||||||
|
])
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: root,
|
||||||
|
$streams: {
|
||||||
|
searches: search.$streams.searches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
app/javascript/legacy/campaigns/supporters/index/metrics.es6
Normal file
35
app/javascript/legacy/campaigns/supporters/index/metrics.es6
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const view = require('vvvview')
|
||||||
|
const h = require('virtual-dom/h')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const scanMerge = require('flyd/module/scanmerge')
|
||||||
|
const thunk = require('vdom-thunk')
|
||||||
|
const request = require('../../../common/super-agent-frp')
|
||||||
|
const format = require('../../../common/format')
|
||||||
|
const Im = require('immutable')
|
||||||
|
const Map = Im.Map
|
||||||
|
const fromJS = Im.fromJS
|
||||||
|
|
||||||
|
const root = state => {
|
||||||
|
if(!state || !state.get('data')) return h('span')
|
||||||
|
return h('table.table--plaid', [
|
||||||
|
h('thead', [
|
||||||
|
h('tr', [h('th', 'Gift option'), h('th', 'Count'), h('th', 'One time'), h('th', 'Recurring')]),
|
||||||
|
]),
|
||||||
|
h('tbody', state.get('data').map(gift => thunk(giftRow, gift)).toJS())
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const giftRow = gift => {
|
||||||
|
|
||||||
|
var name = gift.get('name')
|
||||||
|
name = !name || !name.length ? 'No Gift Option Chosen' : name
|
||||||
|
return h('tr', [
|
||||||
|
h('td', h('strong', name)),
|
||||||
|
h('td', (gift.get('total_donations') || 0) + ''),
|
||||||
|
h('td', '$' + utils.cents_to_dollars(gift.get('total_one_time'))),
|
||||||
|
h('td', '$' + utils.cents_to_dollars(gift.get('total_recurring')))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { root: root, $streams: $ }
|
5
app/javascript/legacy/campaigns/supporters/index/page.js
Normal file
5
app/javascript/legacy/campaigns/supporters/index/page.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
require('../../timeline')
|
||||||
|
require('../../totals')
|
||||||
|
require('./index.es6')
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('virtual-dom/h')
|
||||||
|
const thunk = require('vdom-thunk')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
|
||||||
|
|
||||||
|
const metrics = require('./metrics.es6')
|
||||||
|
const meta = require('./meta.es6')
|
||||||
|
const supporterTable = require('./supporter-table.es6')
|
||||||
|
|
||||||
|
var $ = {
|
||||||
|
showMore: supporterTable.$streams.showMore,
|
||||||
|
searches: meta.$streams.searches,
|
||||||
|
showEmailModal: flyd.stream(),
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = state =>
|
||||||
|
h('div', [
|
||||||
|
h('section.table-meta', thunk(meta.root, state)),
|
||||||
|
h('section.metrics.container', thunk(metrics.root, state.get('gift_levels'))),
|
||||||
|
h('hr'),
|
||||||
|
h('section.container', thunk(supporterTable.root, state)),
|
||||||
|
h('hr'),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Table meta for the supporter listing under Campaigns
|
||||||
|
|
||||||
|
module.exports = {root: root, $streams: $}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const thunk = require('vdom-thunk')
|
||||||
|
const h = require('virtual-dom/h')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const showMoreBtn = require('../../../components/show-more-button.es6')
|
||||||
|
const format = require("../../../common/format")
|
||||||
|
const date = format.date
|
||||||
|
const sql = format.sql
|
||||||
|
|
||||||
|
const root = state => {
|
||||||
|
console.log({state})
|
||||||
|
var supporters = state.get('supporters')
|
||||||
|
if(state.get('loading')) {
|
||||||
|
return h('p.noResults', ' Loading...')
|
||||||
|
} else if(supporters.get('data').count()) {
|
||||||
|
return h('div', [
|
||||||
|
h('table.table--plaid', [
|
||||||
|
h('thead', [
|
||||||
|
h('th', 'Name'),
|
||||||
|
h('th', 'Total'),
|
||||||
|
h('th', 'Gift options'),
|
||||||
|
h('th', 'Latest gift'),
|
||||||
|
h('th', 'Campaign creator')
|
||||||
|
]),
|
||||||
|
thunk(trs, supporters.get('data')),
|
||||||
|
]),
|
||||||
|
thunk(showMoreBtn.root, state.get('moreLoading'), supporters.get('remaining'))
|
||||||
|
])
|
||||||
|
} else if (state.get('isSearching')) {
|
||||||
|
return h('p.noResults', ["Supporter not found."])
|
||||||
|
} else {
|
||||||
|
return h('p.noResults', ["No donors yet. ", h('a', {href: './'}, 'Return to the campaign page.')])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trs = supporters =>
|
||||||
|
h('tbody', supporters.map(supp => thunk(supporterRow, supp)).toJS())
|
||||||
|
|
||||||
|
const supporterRow = supporter =>
|
||||||
|
h('tr', [
|
||||||
|
h('td',
|
||||||
|
h('a'
|
||||||
|
, {
|
||||||
|
href: `/nonprofits/${app.nonprofit_id}/supporters?sid=${supporter.get('id')}`
|
||||||
|
, target: '_blank'
|
||||||
|
}
|
||||||
|
, [supporter.get('name')
|
||||||
|
, h('br')
|
||||||
|
, h('small', supporter.get('email'))
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, h('td', '$' + utils.cents_to_dollars(supporter.get('total_raised'))),
|
||||||
|
h('td', supporter.get('campaign_gift_names').toJS().join(', ')),
|
||||||
|
h('td', supporter.get('latest_gift')),
|
||||||
|
h('td', {}, supporter.get('campaign_creator_emails').toJS().map(
|
||||||
|
function(i, index, array) {
|
||||||
|
return h('a', {href: `mailto:${i}`},
|
||||||
|
i + ((i < (array.length - 1)) ? ", " : ""))
|
||||||
|
})),
|
||||||
|
])
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: root,
|
||||||
|
$streams: {
|
||||||
|
showMore: showMoreBtn.$streams.nextPageClicks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
93
app/javascript/legacy/campaigns/timeline.js
Normal file
93
app/javascript/legacy/campaigns/timeline.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const request = require('../common/client')
|
||||||
|
const R = require('ramda')
|
||||||
|
const Chart = require('chart.js')
|
||||||
|
const moment = require('moment')
|
||||||
|
const dateRange = require('../components/date-range')
|
||||||
|
const chartOptions = require('../components/chart-options')
|
||||||
|
|
||||||
|
var url = `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/timeline`
|
||||||
|
|
||||||
|
function query() {
|
||||||
|
appl.def('loading_chart', true)
|
||||||
|
request.get(url)
|
||||||
|
.end(function(err, resp) {
|
||||||
|
appl.def('loading_chart', false)
|
||||||
|
var ctx = document.getElementById('js-timeline').getContext('2d')
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line'
|
||||||
|
, data: formatData(cumulative(resp.body))
|
||||||
|
, options: chartOptions.dollars
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cumulative(data) {
|
||||||
|
var moments = dateRange(R.head(data).date, R.last(data).date, 'days')
|
||||||
|
var dateStrings = R.map((m) => m.format('YYYY-MM-DD'), moments)
|
||||||
|
|
||||||
|
var proto = {
|
||||||
|
offsite_cents: 0
|
||||||
|
, onetime_cents: 0
|
||||||
|
, recurring_cents: 0
|
||||||
|
, total_cents: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateDictionary = R.reduce((a,b) => {
|
||||||
|
a[b] = R.merge(proto, {date: b})
|
||||||
|
return a
|
||||||
|
}, {}, dateStrings)
|
||||||
|
|
||||||
|
R.reduce((a, b) => {
|
||||||
|
a[b.date] = b
|
||||||
|
return a
|
||||||
|
}, dateDictionary, data)
|
||||||
|
|
||||||
|
return R.tail(R.reduce((a, b) => {
|
||||||
|
var last = R.last(a)
|
||||||
|
b.offsite_cents += last.offsite_cents
|
||||||
|
b.onetime_cents += last.onetime_cents
|
||||||
|
b.recurring_cents += last.recurring_cents
|
||||||
|
b.total_cents += last.total_cents
|
||||||
|
return R.append(b, a)
|
||||||
|
}, [proto], R.values(dateDictionary)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatData(data) {
|
||||||
|
return {
|
||||||
|
labels: R.map((st) => moment(st).format('M/D/YYYY'), R.pluck('date', data))
|
||||||
|
, datasets: [
|
||||||
|
dataset('Total'
|
||||||
|
, 'total_cents'
|
||||||
|
, '190, 190, 190'
|
||||||
|
, data)
|
||||||
|
, dataset('One time'
|
||||||
|
, 'onetime_cents'
|
||||||
|
, '66, 179, 223'
|
||||||
|
, data)
|
||||||
|
, dataset('Recurring'
|
||||||
|
, 'recurring_cents'
|
||||||
|
, '240, 205, 108'
|
||||||
|
, data)
|
||||||
|
, dataset('Offsite'
|
||||||
|
, 'offsite_cents'
|
||||||
|
, '95, 184, 141'
|
||||||
|
, data)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dataset(label, key, rgb, data) {
|
||||||
|
return {
|
||||||
|
label: label
|
||||||
|
, data: R.pluck(key, data)
|
||||||
|
, borderColor: `rgb(${rgb})`
|
||||||
|
, backgroundColor: `rgba(${rgb},0.2)`
|
||||||
|
, fill: false
|
||||||
|
, pointRadius: 0
|
||||||
|
, pointHitRadius: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query()
|
||||||
|
|
15
app/javascript/legacy/campaigns/totals.js
Normal file
15
app/javascript/legacy/campaigns/totals.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const request = require('../common/request')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
var path = `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/totals`
|
||||||
|
|
||||||
|
const resp$ = flyd.map(R.prop('body'), request({path, method: 'GET'}).load)
|
||||||
|
|
||||||
|
appl.def('loading_totals', true)
|
||||||
|
|
||||||
|
flyd.map(response => {
|
||||||
|
appl.def('loading_totals', false)
|
||||||
|
appl.def('campaign_totals', response)
|
||||||
|
}, resp$)
|
||||||
|
|
11
app/javascript/legacy/cards/create-frp.es6
Normal file
11
app/javascript/legacy/cards/create-frp.es6
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const flyd_flatMap = require('flyd/module/flatmap')
|
||||||
|
|
||||||
|
// Given an object of card data, return a stream of stripe tokenization responses
|
||||||
|
module.exports = obj => {
|
||||||
|
var $ = flyd.stream()
|
||||||
|
Stripe.card.createToken(obj, (status, resp) => $(resp))
|
||||||
|
return $
|
||||||
|
}
|
||||||
|
|
129
app/javascript/legacy/cards/create.js
Normal file
129
app/javascript/legacy/cards/create.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// Include the cards/fields partial to use with this.
|
||||||
|
// Call appl.card_form.create(card_obj) to start the card creation process.
|
||||||
|
// Use the appl.card_form.on_fail callback to handle failures.
|
||||||
|
// Use the appl.card_form.on_complete callback to handle completion.
|
||||||
|
// This does not create any donations -- do the donation creation inside appl.card_form.on_complete.
|
||||||
|
|
||||||
|
// Not namespacing card_form; only show one card form on the page at any time
|
||||||
|
|
||||||
|
var request = require('../common/super-agent-promise')
|
||||||
|
var format_err = require('../common/format_response_error')
|
||||||
|
|
||||||
|
module.exports = create_card
|
||||||
|
|
||||||
|
// UI state defaults
|
||||||
|
appl.def('card_form', {
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
status: '',
|
||||||
|
on_complete: function() {},
|
||||||
|
on_fail: function() {},
|
||||||
|
progress_width: '0%' // Width of the progress bar
|
||||||
|
})
|
||||||
|
|
||||||
|
// Define some status messages and progress bar widths for each step of the process
|
||||||
|
var statuses = {
|
||||||
|
before_tokenization: {
|
||||||
|
progress_width: '20%',
|
||||||
|
status: 'Double-checking your card...'
|
||||||
|
},
|
||||||
|
before_create: {
|
||||||
|
progress_width: '75%',
|
||||||
|
status: 'Looks good! Sending the carrier pigeons...'
|
||||||
|
},
|
||||||
|
on_complete: {
|
||||||
|
progress_width: '100%',
|
||||||
|
status: 'Processing payment...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenize with stripe, then save to our db.
|
||||||
|
// The first argument must be a holder object that has 'type' and 'id' keys.
|
||||||
|
// eg: {holder: {type: 'Nonprofit', id: 1}}
|
||||||
|
function create_card(holder, card_obj, options) {
|
||||||
|
options = options || {}
|
||||||
|
if(appl.card_form.loading) return
|
||||||
|
appl.def('card_form', { loading: true, error: false })
|
||||||
|
appl.def('card_form', statuses.before_tokenization)
|
||||||
|
|
||||||
|
// Delete the cvc key from card_obj if
|
||||||
|
// the value of cvc is a blank string.
|
||||||
|
// Otherwise, Stripe will return an error for
|
||||||
|
// incorrect security code.
|
||||||
|
if(card_obj.cvc === '') {
|
||||||
|
delete card_obj['cvc']
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, tokenize the card with Stripe.js
|
||||||
|
return tokenize_with_stripe(card_obj)
|
||||||
|
.catch(display_stripe_err)
|
||||||
|
// Then, save a Card record in our db
|
||||||
|
.then(function(stripe_resp) {
|
||||||
|
appl.def('card_form', statuses.before_create)
|
||||||
|
return create_record(holder, stripe_resp, options)
|
||||||
|
})
|
||||||
|
.then(function(resp) {
|
||||||
|
appl.def('card_form', statuses.on_complete)
|
||||||
|
return resp.body
|
||||||
|
})
|
||||||
|
.catch(display_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post to stripe to get back a stripe_card_token
|
||||||
|
function tokenize_with_stripe(card_obj) {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
Stripe.card.createToken(card_obj, function(status, resp) {
|
||||||
|
if(resp.error) reject(resp)
|
||||||
|
else resolve(resp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save a record of the card in our own db
|
||||||
|
function create_record(holder, stripe_resp, options={}) {
|
||||||
|
var output = {card: {
|
||||||
|
holder_type: holder.type,
|
||||||
|
holder_id: holder.id,
|
||||||
|
email: holder.email,
|
||||||
|
cardholders_name: stripe_resp.name,
|
||||||
|
name: stripe_resp.card.brand + ' *' + stripe_resp.card.last4,
|
||||||
|
stripe_card_token: stripe_resp.id,
|
||||||
|
stripe_card_id: stripe_resp.card.id
|
||||||
|
}}
|
||||||
|
if (options['event_id'])
|
||||||
|
{
|
||||||
|
output['event_id'] = options['event_id']
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.post(options.path || '/cards')
|
||||||
|
.send(output)
|
||||||
|
.perform()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set UI state to display an error in the card form.
|
||||||
|
function display_err(resp) {
|
||||||
|
if(resp && resp.body) {
|
||||||
|
appl.def('card_form', {
|
||||||
|
loading: false,
|
||||||
|
error: true,
|
||||||
|
status: format_err(resp),
|
||||||
|
progress_width: '0%'
|
||||||
|
})
|
||||||
|
appl.def('loading', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function display_stripe_err(resp) {
|
||||||
|
if(resp && resp.error) {
|
||||||
|
appl.def('card_form', {
|
||||||
|
loading: false,
|
||||||
|
error: true,
|
||||||
|
status: resp.error.message,
|
||||||
|
progress_width: '0%'
|
||||||
|
})
|
||||||
|
appl.def('loading', false)
|
||||||
|
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var R = require('ramda')
|
||||||
|
var request = require('../client')
|
||||||
|
|
||||||
|
module.exports = function(name, event_or_campaign, callback) {
|
||||||
|
request.get(`/nonprofits/${app.nonprofit_id}/${event_or_campaign}s/name_and_id`)
|
||||||
|
.end(function(err, resp){
|
||||||
|
var names = resp.body.map(x => x.name)
|
||||||
|
if(R.contains(name, names)) {
|
||||||
|
appl.notify(`Oops. It looks like you already have ${event_or_campaign === 'campaign' ? 'a' : 'an'} ${event_or_campaign} named '${name}'. Please choose a different name and try again.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var request = require('../client')
|
||||||
|
|
||||||
|
module.exports = function(npo_id) {
|
||||||
|
var campaignsPath = '/nonprofits/' + npo_id + '/campaigns/name_and_id'
|
||||||
|
var eventsPath = '/nonprofits/' + npo_id + '/events/name_and_id'
|
||||||
|
|
||||||
|
request.get(campaignsPath).end(function(err, resp){
|
||||||
|
var dataResponse = []
|
||||||
|
|
||||||
|
if (!err) {
|
||||||
|
resp.body.unshift(false)
|
||||||
|
dataResponse = resp.body.map((i) => {
|
||||||
|
if (i.isChildCampaign)
|
||||||
|
{
|
||||||
|
return {id: i.id, name: i.name + " - " + i.creator}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return {id: i.id, name: i.name}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
appl.def('campaigns.data', dataResponse)
|
||||||
|
})
|
||||||
|
|
||||||
|
request.get(eventsPath).end(function(err, resp){
|
||||||
|
var dataResponse = []
|
||||||
|
if(!err) {
|
||||||
|
resp.body.unshift(false)
|
||||||
|
dataResponse = resp.body
|
||||||
|
}
|
||||||
|
|
||||||
|
appl.def('events.data', dataResponse)
|
||||||
|
})
|
||||||
|
}
|
393
app/javascript/legacy/common/application_view.js
Normal file
393
app/javascript/legacy/common/application_view.js
Normal file
|
@ -0,0 +1,393 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var confirmation = require('./confirmation')
|
||||||
|
var notification = require('./notification')
|
||||||
|
var request = require("superagent")
|
||||||
|
var moment = require('moment-timezone')
|
||||||
|
var client = require('./client')
|
||||||
|
var appl = require('view-script')
|
||||||
|
const on_ios11 = require('./on-ios11')
|
||||||
|
const noScroll = require('no-scroll')
|
||||||
|
|
||||||
|
module.exports = appl
|
||||||
|
|
||||||
|
// A couple short convenience functions for disabling/enabling the global
|
||||||
|
// loading state
|
||||||
|
appl.is_loading = function() {appl.def('loading', true)}
|
||||||
|
appl.not_loading = function() {appl.def('loading', false)}
|
||||||
|
appl.not_loading()
|
||||||
|
// Open a modal given by its modal id (uses the modal div's 'id' attribute)
|
||||||
|
appl.def('open_modal', function(modalId) {
|
||||||
|
$('.modal').removeClass('inView')
|
||||||
|
|
||||||
|
//$('body').scrollTop(0)
|
||||||
|
$('#' + modalId).addClass('inView')
|
||||||
|
|
||||||
|
$('body').addClass('is-showingModal')
|
||||||
|
if (on_ios11()){
|
||||||
|
noScroll.on()
|
||||||
|
}
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
// Close any and all open modals
|
||||||
|
appl.def('close_modal', function() {
|
||||||
|
$('.modal').removeClass('inView')
|
||||||
|
$('body').removeClass('is-showingModal')
|
||||||
|
if (on_ios11()) {
|
||||||
|
noScroll.off()
|
||||||
|
}
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
// Open a given modal id only when the User's Account is confirmed via email
|
||||||
|
// If the user's account is not confirmed, then show an informational modal
|
||||||
|
// about confirming their account
|
||||||
|
appl.def('open_modal_if_confirmed', function(modalId){
|
||||||
|
if (app.user && app.user.confirmed)
|
||||||
|
appl.open_modal(modalId)
|
||||||
|
else if (app.user && !app.user.confirmed)
|
||||||
|
appl.open_modal('emailConfirmationModal')
|
||||||
|
else
|
||||||
|
appl.open_modal('signUpModal')
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Open a confirmation modal for the user to click 'yes' or 'no'
|
||||||
|
// Optionally pass in a string message as the first arg (default is 'Are you sure?')
|
||||||
|
// The last argument is the function to execute when 'yes' is clicked
|
||||||
|
// Clicking 'no' simply closes the modal
|
||||||
|
appl.def_lazy('confirm', function() {
|
||||||
|
var msg, expr, node, self = this
|
||||||
|
if(arguments.length === 2) {
|
||||||
|
msg = 'Are you sure?'
|
||||||
|
expr = arguments[0]
|
||||||
|
node = arguments[1]
|
||||||
|
} else {
|
||||||
|
msg = appl.vs(arguments[0])
|
||||||
|
expr = arguments[1]
|
||||||
|
node = arguments[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = confirmation(msg)
|
||||||
|
result.confirmed = function() { appl.vs(expr, node) }
|
||||||
|
return self
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Display a temporary notification message at the bottom of the window
|
||||||
|
appl.def('notify', function(msg) {
|
||||||
|
notification(msg)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert cents to dollars
|
||||||
|
appl.def('cents_to_dollars', function(cents) {
|
||||||
|
return utils.cents_to_dollars(cents)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const momentTz = date =>
|
||||||
|
moment.tz(date, "YYYY-MM-DD HH:mm:ss", 'UTC').tz(ENV.nonprofitTimezone || 'UTC')
|
||||||
|
|
||||||
|
// Return a date in the format MM/DD/YY for a given date string or moment obj
|
||||||
|
appl.def('readable_date', function(date) {
|
||||||
|
if(!date) return
|
||||||
|
return momentTz(date).format("MM/DD/YY")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Given a created_at string (eg. Charge.last.created_at.to_s), convert it to a readable date-time string
|
||||||
|
appl.def('readable_date_time', function(date) {
|
||||||
|
if(!date) return
|
||||||
|
return momentTz(date).format("MM/DD/YY H:mma z")
|
||||||
|
})
|
||||||
|
|
||||||
|
// converts the return value of readable_date_time to it's ISO equivalent
|
||||||
|
appl.def('readable_date_time_to_iso', date => {
|
||||||
|
if(!date) return
|
||||||
|
return moment.tz(date, 'MM/DD/YY H:mma z', ENV.nonprofitTimezone || 'UTC')
|
||||||
|
.tz('UTC')
|
||||||
|
.toISOString()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the month number (eg 01,02...) for the given date string (or moment obj)
|
||||||
|
appl.def('get_month', function(date) {
|
||||||
|
var monthNum = moment(date).month()
|
||||||
|
return moment().month(monthNum).format('MMM')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the year (eg 2017) for the given date string (or moment obj)
|
||||||
|
appl.def('get_year', function(date) {
|
||||||
|
return moment(date).year()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get the day (number in the month) for the given date string (or moment obj)
|
||||||
|
appl.def('get_day', function(date) {
|
||||||
|
return moment(date).date()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Get the percentage of x over y
|
||||||
|
// eg: appl.percentage(34, 69) -> "49.28%"
|
||||||
|
appl.def('percentage', function(x, y) {
|
||||||
|
return String(x / y * 100) + '%'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Given a quantity and a plural word describing that quantity,
|
||||||
|
// return the proper version of that word for that quantitiy
|
||||||
|
// eg: appl.pluralize(4, 'tomatoes') -> "4 tomatoes"
|
||||||
|
// appl.pluralize(1, 'donors') -> "1 donor"
|
||||||
|
appl.def('pluralize', function(quantity, plural_word) {
|
||||||
|
var str = String(quantity) + ' '
|
||||||
|
if(quantity !== 1) return str+plural_word
|
||||||
|
else return str + appl.to_singular(plural_word)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Convert (most) words from their plural to their singular form
|
||||||
|
// Works with simple s-endings, ies-endings, and oes-endings
|
||||||
|
appl.def('to_singular', function(plural_word) {
|
||||||
|
return plural_word
|
||||||
|
.replace(/ies$/, 'y')
|
||||||
|
.replace(/oes$/, 'o')
|
||||||
|
.replace(/s$/, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Truncate a text and add ellipsis to the end
|
||||||
|
appl.def('append_ellipsis', function(text, length) {
|
||||||
|
if(text.length <= length) return text
|
||||||
|
return text.slice(0,length).replace(/ [^ ]+$/, ' ...')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// General viewscript utilities
|
||||||
|
// All of these are to be added to the actual viewscript package in the future
|
||||||
|
|
||||||
|
// Push a given value into the arr given by the property name 'arr_key'
|
||||||
|
// Mutates the array stored at 'arr_key'
|
||||||
|
// appl.def('arr', [1,2,3])
|
||||||
|
// appl.push('arr', 4)
|
||||||
|
// appl.arr == [1,2,3,4]
|
||||||
|
appl.def('push', function(val, arr_key, node) {
|
||||||
|
var arr = appl.vs(arr_key, node)
|
||||||
|
if(!arr || !arr.length) arr = []
|
||||||
|
arr.push(val)
|
||||||
|
appl.def(arr_key, arr)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Concatenate two arrays (this is mutating)
|
||||||
|
// The first array is given by its property name and will be mutated
|
||||||
|
// The second array is the array itself to concatenate
|
||||||
|
// appl.def('arr1', [1,2,3])
|
||||||
|
// appl.concat('arr1', [4,5,6])
|
||||||
|
// appl.arr1 == [1,2,3,4,5,6]
|
||||||
|
appl.def('concat', function(arr1_key, arr2, node) {
|
||||||
|
var arr1 = appl.vs(arr1_key, node)
|
||||||
|
appl.def(arr1_key, arr1.concat(arr2))
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Merge all key/vals from set_obj into all objects in the array given by the property 'arr_key'
|
||||||
|
// eg:
|
||||||
|
// appl.def('arr_of_objs', [{id: 1, name: 'Bob'}, {id: 2, name: 'Holga'}]
|
||||||
|
// appl.update_all('arr_of_objs', {name: 'Morty'})
|
||||||
|
// appl.arr_of_objs == [{id: 1, name: 'Morty'}, {id: 2, name: 'Morty'}]
|
||||||
|
appl.def('update_all', function(arr_key, set_obj, node) {
|
||||||
|
appl.def(arr_key, appl.vs(arr_key).map(function(obj) {
|
||||||
|
for(var key in set_obj) obj[key] = set_obj[key]
|
||||||
|
return obj
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Given an array of objects in the view state (with property name 'arr_key'),
|
||||||
|
// and given an object to match on ('obj_matcher'),
|
||||||
|
// and given an object with values to set ('set_obj'),
|
||||||
|
// then set each object that matches key/vals in the obj_matcher to the key/vals in set_obj
|
||||||
|
//
|
||||||
|
// eg, if val at arr_key is: [{id: 1, name: 'Bob'}, {id: 2, name: 'Holga'}]
|
||||||
|
// and obj_matcher is: {id: 1}
|
||||||
|
// and set_obj is: {name: 'Gertrude'}
|
||||||
|
// then result will be: [{id: 1, name: 'Gertrude'}, {id: 2, name: 'Holga'}]
|
||||||
|
appl.def('find_and_set', function(arr_key, obj_matcher, set_obj, node) {
|
||||||
|
var arr = appl.vs(arr_key)
|
||||||
|
if(!arr) return appl
|
||||||
|
var result = arr.map(function(obj) {
|
||||||
|
for (var key in obj_matcher) {
|
||||||
|
if(obj_matcher[key] === obj[key]) {
|
||||||
|
return utils.merge(obj, set_obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
appl.def(arr_key, result)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('find_and_remove', function(arr_key, obj_matcher, set_obj, node) {
|
||||||
|
var arr = appl.vs(arr_key)
|
||||||
|
if(!arr) return appl
|
||||||
|
var result = arr.reduce(function(new_arr, obj) {
|
||||||
|
for (var key in obj_matcher) {
|
||||||
|
if(obj_matcher[key] === obj[key]) {
|
||||||
|
return new_arr
|
||||||
|
} else {
|
||||||
|
new_arr.push(obj)
|
||||||
|
return new_arr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
appl.def(arr_key, result)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Return a boolean whether the parent input is checked (must be a type checkbox)
|
||||||
|
appl.def('is_checked', function(node) {
|
||||||
|
return appl.prev_elem(node).checked
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check a parent input node (must be type checkbox)
|
||||||
|
appl.def('check', function(node) {
|
||||||
|
appl.prev_elem(node).checked = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Uncheck a parent input node (must be type checkbox)
|
||||||
|
appl.def('uncheck', function(node) {
|
||||||
|
appl.prev_elem(node).checked = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check the parent node if the predicate is true
|
||||||
|
appl.def('checked_if', function(pred, node) {
|
||||||
|
if(pred) appl.prev_elem(node).checked = true
|
||||||
|
else appl.prev_elem(node).checked = false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove an attribute from the parent node
|
||||||
|
appl.def('remove_attr', function(attr, node) {
|
||||||
|
appl.prev_elem(node).removeAttribute(attr)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('remove_attr_if', function(pred, attr, node) {
|
||||||
|
if(!node) return
|
||||||
|
var n = appl.prev_elem(node)
|
||||||
|
if(pred) {
|
||||||
|
if(!n.hasAttribute('data-attr-' + attr)) n.setAttribute('data-attr-' + attr, n.getAttribute(attr)) // cache attr to add back in
|
||||||
|
n.removeAttribute(attr)
|
||||||
|
} else if(!n.hasAttribute(attr)) {
|
||||||
|
var val = n.getAttribute('data-attr-' + attr)
|
||||||
|
n.setAttribute(attr, val)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Map over the given list and update it in the view
|
||||||
|
appl.transform = function(name, fn) {
|
||||||
|
var result = appl.vs(name).map(fn)
|
||||||
|
appl.def(name, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the current URL path
|
||||||
|
appl.def('pathname', function() { return window.location.pathname })
|
||||||
|
// Return the root url
|
||||||
|
appl.def('root_url', function() { return window.location.origin })
|
||||||
|
|
||||||
|
// Trigger a property to get updated in the view
|
||||||
|
appl.def('trigger_update', function(prop) {
|
||||||
|
return appl.def(prop, appl.vs(prop))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
appl.def('snake_case', function(string) {
|
||||||
|
return string.replace(/ /g,'_')
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('sort_arr_of_objs_by_key', function(arr_of_objs, key) {
|
||||||
|
return arr_of_objs.sort(function(a, b) {
|
||||||
|
return a[key].localeCompare(b[key]);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Convert a positive integer into an ordinal (1st, 2nd, 3rd...)
|
||||||
|
appl.def('ordinalize', function(n) {
|
||||||
|
if(n <= 0) return n
|
||||||
|
// Deal with the preteen punks first
|
||||||
|
if([11,12,13].indexOf(n) !== -1) return String(n) + 'th'
|
||||||
|
var str = String(n)
|
||||||
|
var lst = str[str.length-1]
|
||||||
|
if(lst === '1') return String(n) + 'st'
|
||||||
|
else if(lst === '2') return String(n) + 'nd'
|
||||||
|
else if(lst === '3') return String(n) + 'rd'
|
||||||
|
else return String(n) + 'th'
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('toggle_side_nav', function(){
|
||||||
|
if(appl.side_nav_is_open)
|
||||||
|
appl.def('side_nav_is_open', false)
|
||||||
|
else
|
||||||
|
appl.def('side_nav_is_open', true)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('head', function(arr) {
|
||||||
|
if(arr === undefined) return undefined
|
||||||
|
return arr[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('select_drop_down', function(node) {
|
||||||
|
var $li = $(node).parent()
|
||||||
|
var $dropDown = $li.parents('.dropDown')
|
||||||
|
$dropDown.find('li').removeClass('is-selected')
|
||||||
|
$dropDown.find('.dropDown-toggle').removeClass('is-droppedDown')
|
||||||
|
$li.addClass('is-selected')
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('clear_drop_down', function(node){
|
||||||
|
var $dropDown = $(node).parents('.dropDown')
|
||||||
|
$dropDown.find('li').removeClass('is-selected')
|
||||||
|
$dropDown.find('.dropDown-toggle').removeClass('is-droppedDown')
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('strip_tags', function(html){
|
||||||
|
if(!html) return
|
||||||
|
return html.replace(/(<([^>]+)>)/ig," ")
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('replace', function(string, matcher, replacer) {
|
||||||
|
if(!string) return
|
||||||
|
// the new RegExp constructor takes a string
|
||||||
|
// and returns a regex: new RegExp("a|b", "i") becomes /a|b/i
|
||||||
|
return string.replace(new RegExp(matcher, 'g'), replacer)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('number_with_commas', function(n){
|
||||||
|
if(!n){return}
|
||||||
|
return utils.number_with_commas(n)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('remove_commas', function(s) {
|
||||||
|
return s.replace(/,/g, '')
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('percentage', function(x, y, number_of_decimals){
|
||||||
|
if(!x || !y) return 0
|
||||||
|
number_of_decimals = number_of_decimals || 2
|
||||||
|
return Number((y/x * 100).toFixed(number_of_decimals))
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('clean_url', function(string){
|
||||||
|
return string.replace(/.*?:\/\//g, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('address_with_commas', function(address, city, state){
|
||||||
|
return utils.address_with_commas(address, city, state)
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('format_phone', function(st) {
|
||||||
|
return utils.pretty_phone(st)
|
||||||
|
})
|
||||||
|
|
14
app/javascript/legacy/common/apply-pikaday.js
Normal file
14
app/javascript/legacy/common/apply-pikaday.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const bind = require('attr-binder')
|
||||||
|
const Pikaday = require('pikaday')
|
||||||
|
const moment = require('moment')
|
||||||
|
|
||||||
|
bind('apply-pikaday', function(field, format) {
|
||||||
|
const setDefaultDate = field.getAttribute('pikaday-setDefaultDate')
|
||||||
|
const maxDate_str = field.getAttribute('pikaday-maxDate')
|
||||||
|
const maxDate = maxDate_str ? moment(maxDate_str) : undefined
|
||||||
|
const defaultDate_str = field.getAttribute('pikaday-defaultDate')
|
||||||
|
const defaultDate = defaultDate_str ? moment(defaultDate_str) : undefined
|
||||||
|
new Pikaday({format, setDefaultDate, field, maxDate, defaultDate})
|
||||||
|
})
|
||||||
|
|
72
app/javascript/legacy/common/autosubmit.js
Normal file
72
app/javascript/legacy/common/autosubmit.js
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var confirmation = require('./confirmation')
|
||||||
|
var notification = require('./notification')
|
||||||
|
|
||||||
|
$('form[autosubmit]').submit(function(e) {
|
||||||
|
var self = this
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if(this.hasAttribute('data-confirm')) {
|
||||||
|
var result = confirmation()
|
||||||
|
result.confirmed = function() {
|
||||||
|
submit_form(e.currentTarget)
|
||||||
|
}
|
||||||
|
} else submit_form(e.currentTarget)
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit_form(form_el, on_success) {
|
||||||
|
var path = form_el.getAttribute('action')
|
||||||
|
var method = form_el.getAttribute('method')
|
||||||
|
var form_data = new FormData(form_el)
|
||||||
|
$(form_el).find('button[type="submit"]').loading()
|
||||||
|
$(form_el).find('.error').text('')
|
||||||
|
|
||||||
|
var notice = form_el.getAttribute('notice')
|
||||||
|
if(notice) $.cookie('notice', notice, {path: '/'})
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: method,
|
||||||
|
url: path,
|
||||||
|
data: form_data,
|
||||||
|
dataType: 'json',
|
||||||
|
processData: false,
|
||||||
|
contentType: false
|
||||||
|
})
|
||||||
|
.done(function(d) {
|
||||||
|
if(form_el.hasAttribute('data-reload-with-slug'))
|
||||||
|
window.location = d['url']
|
||||||
|
else if(form_el.hasAttribute('data-reload'))
|
||||||
|
window.location.reload()
|
||||||
|
else if(form_el.hasAttribute('data-redirect')) {
|
||||||
|
var redirect = form_el.getAttribute('data-redirect')
|
||||||
|
if(redirect) window.location.href = redirect
|
||||||
|
else if(d.url) window.location.href = d.url
|
||||||
|
} else {
|
||||||
|
var msg = form_el.getAttribute('data-success-message')
|
||||||
|
if(msg) notification(msg)
|
||||||
|
$(form_el).find('button[type="submit"]').disableLoading()
|
||||||
|
}
|
||||||
|
if(on_success) on_success(d)
|
||||||
|
})
|
||||||
|
.fail(function(d) {
|
||||||
|
$(form_el).find('.error').text(utils.print_error(d))
|
||||||
|
$(form_el).find('button[type="submit"]').disableLoading()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third closure
|
||||||
|
|
||||||
|
appl.def_lazy('autosubmit', function(callback, node) {
|
||||||
|
if(!node || !node.parentNode) return
|
||||||
|
|
||||||
|
var self = this, parent = node.parentNode
|
||||||
|
|
||||||
|
parent.onsubmit = function(ev) {
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
if(parent.hasAttribute('data-confirm'))
|
||||||
|
confirmation().confirmed = function() { submit_form(parent, function() {appl.vs(callback)}) }
|
||||||
|
else submit_form(parent, function() { appl.vs(callback) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
8
app/javascript/legacy/common/brand-fonts.js
Normal file
8
app/javascript/legacy/common/brand-fonts.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = {
|
||||||
|
helvetica: {family: "'Helvetica Neue', Helvetica, Arial, sans-serif", name: 'Helvetica'},
|
||||||
|
futura: {family: "'Futura', Arial, sans-serif", name: 'Futura'},
|
||||||
|
open: {family: "Open Sans, 'Helvetica Neue', Arial, sans-serif", name: 'Open Sans'},
|
||||||
|
georgia: {family: "Georgia, serif", name: 'Georgia'},
|
||||||
|
bitter: {family: "'Bitter', serif", name: 'Bitter'}
|
||||||
|
}
|
8
app/javascript/legacy/common/class-object.js
Normal file
8
app/javascript/legacy/common/class-object.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
module.exports = (classes='') => R.reduce(
|
||||||
|
(a, b) => {a[b] = true; return a}
|
||||||
|
, {}
|
||||||
|
, R.drop(1, classes.split('.')))
|
||||||
|
|
25
app/javascript/legacy/common/client.js
Normal file
25
app/javascript/legacy/common/client.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// superapi wrapper with our api defaults
|
||||||
|
|
||||||
|
var request = require('superagent')
|
||||||
|
|
||||||
|
var wrapper = {}
|
||||||
|
|
||||||
|
wrapper.post = function() {
|
||||||
|
return request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.put = function() {
|
||||||
|
return request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.del = function() {
|
||||||
|
return request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.get = function(path) {
|
||||||
|
return request.get.call(this, path).accept('json')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = wrapper
|
||||||
|
|
59
app/javascript/legacy/common/colors.js
Normal file
59
app/javascript/legacy/common/colors.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = {
|
||||||
|
// BLUES
|
||||||
|
'$dark-turquoise': "#306563"
|
||||||
|
, '$turquoise': "#a6d5d3"
|
||||||
|
, '$sea-foam': "#669092"
|
||||||
|
, '$light-sea-foam': "#5FA6AA"
|
||||||
|
, '$sky': "#97c2c4"
|
||||||
|
, '$faded-sky': "#B6D3D4"
|
||||||
|
, '$light-logo-blue': "#69B4CF"
|
||||||
|
, '$logo-blue': "#42B3DF"
|
||||||
|
, '$dark-logo-blue': "#479EBE"
|
||||||
|
, '$baby-blue': "#E2F8F8"
|
||||||
|
, '$blue-grey': "rgba(136, 148, 152, 1)"
|
||||||
|
, '$cloudy': "rgba(67, 164, 202, 0.3)"
|
||||||
|
|
||||||
|
// GREENS
|
||||||
|
, '$light-grass': "#589E73"
|
||||||
|
, '$bluegrass': "#5FB88D"
|
||||||
|
, '$bluegrass--light': "lighten($bluegrass, 10)"
|
||||||
|
, '$grass': "#2d8f60"
|
||||||
|
, '$dark-grass': "#35865E"
|
||||||
|
, '$sage': "#E9F5E8"
|
||||||
|
, '$sage--dark': "#D5E4D4"
|
||||||
|
, '$thankYou-green': "#7EC981"
|
||||||
|
, '$mint': "#DEEFE7"
|
||||||
|
|
||||||
|
// YELLOWS - ORANGES
|
||||||
|
, '$manila': "#FFFCE3"
|
||||||
|
, '$pollen': "#F0CD6C"
|
||||||
|
, '$light-pollen': "#FFE397"
|
||||||
|
, '$oj': "#FDC785"
|
||||||
|
, '$looseleaf': "lighten(#FFFEF6, 0.5)"
|
||||||
|
|
||||||
|
// PINKS, REDS, PURPLES
|
||||||
|
, '$watermelon': "#EE8480"
|
||||||
|
, '$lavender': "#A57B9E"
|
||||||
|
, '$blush': "rgba(253, 168, 133, 0.1)"
|
||||||
|
, '$red': "#FF4F4F"
|
||||||
|
|
||||||
|
// NEUTRALS
|
||||||
|
, '$charcoal': "#494949"
|
||||||
|
, '$charcoal--light': "lighten(#494949, 10)"
|
||||||
|
, '$grey': "rgb(128, 128, 128)"
|
||||||
|
, '$lightGrey': "lighten($grey, 20)"
|
||||||
|
, '$sepia': "rgba(65, 65, 65, 0.7)"
|
||||||
|
, '$shark': "rgb(162, 162, 162)"
|
||||||
|
, '$fog': "#fbfbfb"
|
||||||
|
, '$shade': "rgba(0,0,0,0.02)"
|
||||||
|
|
||||||
|
, '$trans': "rgba(255,255,255,0)"
|
||||||
|
|
||||||
|
, '$defaultShadow': "0 0 4px 1px rgba($grey, 0.5)"
|
||||||
|
|
||||||
|
// SOCIAL
|
||||||
|
, '$facebook': "#236094"
|
||||||
|
, '$twitter': "#3199cb"
|
||||||
|
, '$google': "#dd4b39"
|
||||||
|
}
|
46
app/javascript/legacy/common/confirmation.js
Normal file
46
app/javascript/legacy/common/confirmation.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var confirmation = function(msg, success_cb) {
|
||||||
|
var $confirm_modal = $('#confirmation-modal')
|
||||||
|
var $msg = $confirm_modal.find('.msg')
|
||||||
|
if(msg && msg.length > 15) $msg.css('font-size', '16px')
|
||||||
|
var cb = {
|
||||||
|
confirmed: function() {},
|
||||||
|
denied: function() {}
|
||||||
|
}
|
||||||
|
var $previousModal = $('.modal.inView')
|
||||||
|
$('.modal').removeClass('inView')
|
||||||
|
var $body = $('body')
|
||||||
|
$body.addClass('is-showingModal')
|
||||||
|
|
||||||
|
function hide_confirmation_and_show_previous(){
|
||||||
|
$('#confirmation-modal').removeClass('inView')
|
||||||
|
if ($previousModal.length){
|
||||||
|
$previousModal.addClass('inView')
|
||||||
|
$body.addClass('is-showingModal')
|
||||||
|
}
|
||||||
|
else
|
||||||
|
$body.removeClass('is-showingModal')
|
||||||
|
}
|
||||||
|
|
||||||
|
$confirm_modal.addClass('inView')
|
||||||
|
.off('click', '.yes')
|
||||||
|
.off('click', '.no')
|
||||||
|
|
||||||
|
.on('click', '.yes', function(e) {
|
||||||
|
hide_confirmation_and_show_previous()
|
||||||
|
if(success_cb) {
|
||||||
|
success_cb()
|
||||||
|
} else {
|
||||||
|
cb.confirmed(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('click', '.no', function(e) {
|
||||||
|
$('#confirmation-modal').removeClass('inView')
|
||||||
|
hide_confirmation_and_show_previous()
|
||||||
|
cb.denied(e)
|
||||||
|
})
|
||||||
|
$msg.text(msg || 'Are you sure?')
|
||||||
|
return cb
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = confirmation
|
25
app/javascript/legacy/common/credit-card-validator.js
Normal file
25
app/javascript/legacy/common/credit-card-validator.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
// Reference: https://en.wikipedia.org/wiki/Luhn_algorithm
|
||||||
|
|
||||||
|
module.exports = val => {
|
||||||
|
val = val.replace(/[-\s]/g, '')
|
||||||
|
return val.match(/^[0-9-\s]+$/) && luhnCheck(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
const luhnCheck =
|
||||||
|
R.compose(
|
||||||
|
R.equals(0)
|
||||||
|
, R.modulo(R.__, 10)
|
||||||
|
, R.sum
|
||||||
|
, R.map(n => n > 9 ? n - 9 : n) // Subtract 9 from those digits greater than 9
|
||||||
|
, R.addIndex(R.map)((n, i) => i % 2 === 0 ? n : n * 2) // Double the value of every second digit
|
||||||
|
, R.map(ch => Number(ch))
|
||||||
|
, R.reverse)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Luhn check in haskell:
|
||||||
|
luhn = (0 ==) . (`mod` 10) . sum . map (uncurry (+) . (`divMod` 10)) .
|
||||||
|
zipWith (*) (cycle [1,2]) . map digitToInt . reverse
|
||||||
|
*/
|
8
app/javascript/legacy/common/css-gradient.js
Normal file
8
app/javascript/legacy/common/css-gradient.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = (dir, to, from) =>
|
||||||
|
` background-image: -webkit-linear-gradient(${dir}, ${to}, ${from});
|
||||||
|
background-image: -moz-linear-gradient(${dir}, ${to}, ${from});
|
||||||
|
background-image: -ms-linear-gradient(${dir}, ${to}, ${from});
|
||||||
|
background-image: linear-gradient(${dir}, ${to}, ${from});
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr=${to}, endColorstr=${from});`
|
||||||
|
|
41
app/javascript/legacy/common/direct-to-s3-upload.es6
Normal file
41
app/javascript/legacy/common/direct-to-s3-upload.es6
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
|
||||||
|
// local
|
||||||
|
const request = require('./super-agent-frp')
|
||||||
|
const postFormData = require('./post-form-data')
|
||||||
|
|
||||||
|
|
||||||
|
// Pass in a stream of Input Nodes with type file
|
||||||
|
// Make a post request to our server to start the import
|
||||||
|
// Will create a backgrounded job and email the user when
|
||||||
|
// completed
|
||||||
|
// Returns a stream of {uri: 'uri of uploaded file on s3', formData: 'original form data'}
|
||||||
|
const uploadFile = R.curry(input => {
|
||||||
|
// We need to get an AWS presigned post thing to so we can upload files
|
||||||
|
// Stream of pairs of [formObjData, presignedPostObj]
|
||||||
|
var withPresignedPost$ = flyd.map(
|
||||||
|
resp => [input, resp.body]
|
||||||
|
, request.post('/aws_presigned_posts').perform()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stream of upload responses from s3
|
||||||
|
return flyd.flatMap(
|
||||||
|
pair => {
|
||||||
|
var [input, presignedPost] = pair
|
||||||
|
var url = `https://${presignedPost.s3_direct_url.host}`
|
||||||
|
var file = input.files[0]
|
||||||
|
var fileUrl = `${url}/tmp/${presignedPost.s3_uuid}/${file.name}`
|
||||||
|
var urlWithPort = `${url}:${presignedPost.s3_direct_url.port}`
|
||||||
|
var payload = R.merge(JSON.parse(presignedPost.s3_presigned_post), {file})
|
||||||
|
|
||||||
|
return flyd.map(resp => ({uri: fileUrl, file}), postFormData(url, payload))
|
||||||
|
}
|
||||||
|
, withPresignedPost$)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = uploadFile
|
||||||
|
|
30
app/javascript/legacy/common/dynamic_form.js
Normal file
30
app/javascript/legacy/common/dynamic_form.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var notification = require('./notification')
|
||||||
|
|
||||||
|
$('form.dynamic').submit(function(e) {
|
||||||
|
var self = this
|
||||||
|
e.preventDefault()
|
||||||
|
var path = this.getAttribute('action')
|
||||||
|
var meth = this.getAttribute('method')
|
||||||
|
var form_data = new FormData(this)
|
||||||
|
$(this).find('button[type="submit"]').loading()
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
type: meth,
|
||||||
|
url: path,
|
||||||
|
data: form_data,
|
||||||
|
dataType: 'json',
|
||||||
|
processData: false,
|
||||||
|
contentType: false
|
||||||
|
})
|
||||||
|
.done(function(d) {
|
||||||
|
$('.modal').modal('hide')
|
||||||
|
notification(d.notification)
|
||||||
|
})
|
||||||
|
.fail(function(d) {
|
||||||
|
$(self).find('.error').text(utils.print_error(d))
|
||||||
|
})
|
||||||
|
.complete(function() {
|
||||||
|
$(self).find('button[type="submit"]').disableLoading()
|
||||||
|
})
|
||||||
|
})
|
10
app/javascript/legacy/common/editable.js
Normal file
10
app/javascript/legacy/common/editable.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// if you are instantiating more than one WYSIWYG on a page,
|
||||||
|
// be sure to give them id's to differentiate them
|
||||||
|
// to avoid unwanted display side effects
|
||||||
|
|
||||||
|
|
||||||
|
if (app.editor === 'froala')
|
||||||
|
module.exports = require('./editor/froala.es6')
|
||||||
|
else if (app.editor === 'quill')
|
||||||
|
module.exports = require('./editor/quill.es6')
|
150
app/javascript/legacy/common/editor/froala.es6
Normal file
150
app/javascript/legacy/common/editor/froala.es6
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var view = require("vvvview")
|
||||||
|
var savingIndicator = require('../../components/saving_indicator')
|
||||||
|
var savingState = {hide: true}
|
||||||
|
var renderSavingIndicator = view(savingIndicator, document.body, savingState)
|
||||||
|
|
||||||
|
var donate_button_markup = "<a class='button' target='_blank' href='" + location.origin + "/nonprofits/" + app.nonprofit_id + "/donate' "
|
||||||
|
|
||||||
|
if(app.nonprofit && app.nonprofit.brand_color)
|
||||||
|
donate_button_markup += "style='background-color:" + app.nonprofit.brand_color + ";'>Donate</a>"
|
||||||
|
else
|
||||||
|
donate_button_markup += ">Donate</a>"
|
||||||
|
|
||||||
|
var email_buttons = ["bold", "italic", "formatBlock", "align", "createLink",
|
||||||
|
"insertImage", "insertUnorderedList", "insertOrderedList",
|
||||||
|
"undo", "redo", "insert_donate_button", "insert_name", "html"]
|
||||||
|
|
||||||
|
var froala = function($el, options) {
|
||||||
|
$el.editable({
|
||||||
|
key: app.froala_key,
|
||||||
|
placeholder: options.placeholder || 'Edit text here',
|
||||||
|
buttons: options.email_buttons ? email_buttons : options.buttons ? options.buttons : ["bold", "italic", "formatBlock", "align", "createLink", "insertImage", "insertVideo", "insertUnorderedList", "insertOrderedList", "undo", "redo", "html"],
|
||||||
|
inlineMode: false,
|
||||||
|
beautifyCode: true,
|
||||||
|
plainPaste: true,
|
||||||
|
blockTags: {p: 'Normal', h5: "Heading", small: 'Caption'},
|
||||||
|
allowedAttrs: ["accept","accept-charset","accesskey","action","align","alt","async","autocomplete","autofocus","autoplay","autosave","background","bgcolor","border","charset","cellpadding","cellspacing","checked","cite","class","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","data","data-.*","datetime","default","defer","dir","dirname","disabled","download","draggable","dropzone","enctype","for","form","formaction","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","ismap","itemprop","keytype","kind","label","lang","language","list","loop","low","max","maxlength","media","method","min","multiple", "muted", "name","novalidate","open","optimum","pattern","ping","placeholder","poster","preload","pubdate","radiogroup","readonly","rel","required","reversed","rows","rowspan","sandbox","scope","scoped","scrolling","seamless","selected","shape","size","sizes","span","src","srcdoc","srclang","srcset","start","step","summary","spellcheck","style","tabindex","target","title","type","translate","usemap","value","valign","width","wrap"],
|
||||||
|
imageUploadURL: '/image_attachments.json',
|
||||||
|
imageUploadParams: {
|
||||||
|
authenticity_token: $("meta[name='csrf-token']").attr('content')
|
||||||
|
},
|
||||||
|
imageDeleteURL: '/image_attachments/remove.json',
|
||||||
|
imageErrorCallback: function (d) {
|
||||||
|
},
|
||||||
|
afterRemoveImageCallback: function ($img) {
|
||||||
|
this.options.imageDeleteParams = {src: $img.attr('src')};
|
||||||
|
this.deleteImage($img);
|
||||||
|
},
|
||||||
|
customButtons: {
|
||||||
|
format_code: {
|
||||||
|
title: 'format code',
|
||||||
|
icon: {
|
||||||
|
type: 'font',
|
||||||
|
value: 'fa fa-bolt'
|
||||||
|
},
|
||||||
|
callback: function () {
|
||||||
|
// used to show code snippets.
|
||||||
|
// takes selected text, including typed html tags
|
||||||
|
// and wraps each text line in a <div>
|
||||||
|
// and appends all of the <div>s into a <pre> tag
|
||||||
|
// and then replaces that selected text with the
|
||||||
|
// newly created <pre> tag
|
||||||
|
|
||||||
|
var lines_of_code = this.text().split("\n")
|
||||||
|
var pre = document.createElement('pre')
|
||||||
|
pre.className = 'codeText'
|
||||||
|
|
||||||
|
// created <div>s for each new line and appends them to <pre>
|
||||||
|
lines_of_code.map(function (line) {
|
||||||
|
var div = document.createElement('div')
|
||||||
|
div.appendChild(document.createTextNode(line))
|
||||||
|
pre.appendChild(div)
|
||||||
|
})
|
||||||
|
|
||||||
|
var selected_elements = this.getSelectionElements()
|
||||||
|
var first_selected_element = selected_elements[0]
|
||||||
|
var parent_node = document.getElementsByClassName('froala-element')[0]
|
||||||
|
|
||||||
|
// inserts pre before selection
|
||||||
|
parent_node.insertBefore(pre, first_selected_element)
|
||||||
|
|
||||||
|
// inserts <br>s before and after <pre>
|
||||||
|
parent_node.insertBefore(document.createElement('br'), pre)
|
||||||
|
parent_node.insertBefore(document.createElement('br'), pre.nextSibling)
|
||||||
|
|
||||||
|
// deletes selection
|
||||||
|
selected_elements.map(function (el) {
|
||||||
|
parent_node.removeChild(el)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.saveUndoStep()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insert_donate_button: {
|
||||||
|
title: 'Donate Button',
|
||||||
|
icon: {
|
||||||
|
type: 'font',
|
||||||
|
value: 'fa fa-heart'
|
||||||
|
},
|
||||||
|
callback: function () {
|
||||||
|
this.insertHTML(donate_button_markup)
|
||||||
|
this.saveUndoStep()
|
||||||
|
},
|
||||||
|
refresh: function () {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insert_name: {
|
||||||
|
title: 'Insert recipient name',
|
||||||
|
icon: {
|
||||||
|
type: 'txt',
|
||||||
|
value: 'Name'
|
||||||
|
},
|
||||||
|
callback: function () {
|
||||||
|
this.insertHTML("{{NAME}}")
|
||||||
|
this.saveUndoStep()
|
||||||
|
},
|
||||||
|
refresh: function () {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
videoAllowedAttrs: ["src","width","height","frameborder","allowfullscreen","webkitallowfullscreen","mozallowfullscreen","href","target","id","controls","value","name", "autoplay", "loop", "muted"]
|
||||||
|
})
|
||||||
|
|
||||||
|
$('.froala-popup').parents('.froala-editor').css('z-index', 99999)
|
||||||
|
|
||||||
|
if (!options.noUpdateOnChange) {
|
||||||
|
$el.on('editable.contentChanged', function (e, editor) {
|
||||||
|
utils.delay(100, function () {
|
||||||
|
var key = $el.data('key')
|
||||||
|
var data = {}
|
||||||
|
var path = $el.data('path')
|
||||||
|
data[key] = $el.find('.froala-element').html()
|
||||||
|
renderSavingIndicator({hide: false, text: 'Saving...'})
|
||||||
|
$.ajax({type: 'put', url: path, data: data})
|
||||||
|
.done(function () {
|
||||||
|
renderSavingIndicator({text: 'Saved'})
|
||||||
|
window.setTimeout(function () {
|
||||||
|
renderSavingIndicator({hide: true})
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.sticky) {
|
||||||
|
window.onload = function () {
|
||||||
|
var makeEditorStick = require('../scroll_toggle_class')
|
||||||
|
var id = $el.attr('id') ? '#' + $el.attr('id') : false
|
||||||
|
var parent = id ? id : '.froala-box'
|
||||||
|
var child = id ? id + ' .froala-editor' : '.froala-editor'
|
||||||
|
makeEditorStick(child, 'is-stuck', parent)
|
||||||
|
$(child).css('width', $(parent).width())
|
||||||
|
$(window).resize(function () {
|
||||||
|
$(child).css('width', $(parent).width())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = froala;
|
45
app/javascript/legacy/common/editor/quill.es6
Normal file
45
app/javascript/legacy/common/editor/quill.es6
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var view = require("vvvview")
|
||||||
|
var savingIndicator = require('../../components/saving_indicator')
|
||||||
|
var savingState = {hide: true}
|
||||||
|
var renderSavingIndicator = view(savingIndicator, document.body, savingState)
|
||||||
|
|
||||||
|
|
||||||
|
const Quill = require('quill')
|
||||||
|
|
||||||
|
function initializeQuill($el, options)
|
||||||
|
{
|
||||||
|
var editor = new Quill($el, {
|
||||||
|
theme: 'bubble',
|
||||||
|
placeholder: options.placeholder
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!options.noUpdateOnChange) {
|
||||||
|
editor.on('text-change', function () {
|
||||||
|
utils.delay(100, function () {
|
||||||
|
var key = $el.getAttribute('data-key')
|
||||||
|
var data = {}
|
||||||
|
var path = $el.getAttribute('data-path')
|
||||||
|
data[key] = editor.root.innerHTML
|
||||||
|
renderSavingIndicator({hide: false, text: 'Saving...'})
|
||||||
|
$.ajax({type: 'put', url: path, data: data})
|
||||||
|
.done(function () {
|
||||||
|
renderSavingIndicator({text: 'Saved'})
|
||||||
|
window.setTimeout(function () {
|
||||||
|
renderSavingIndicator({hide: true})
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var quill = function($el, options) {
|
||||||
|
for (var i =0; i < $el.length; i++)
|
||||||
|
{
|
||||||
|
initializeQuill($el[i], options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = quill
|
30
app/javascript/legacy/common/el_swapo.js
Normal file
30
app/javascript/legacy/common/el_swapo.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var el_swapo = {}
|
||||||
|
|
||||||
|
$('*[swap-in]').each(function(i) {
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
$(this).on('click', function(e) {
|
||||||
|
var swap_class = self.getAttribute('swap-class')
|
||||||
|
var new_class = self.getAttribute('swap-in')
|
||||||
|
swap(swap_class, new_class)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function swap(swap_class, new_class) {
|
||||||
|
$('*[swap-class="' + swap_class + '"]').removeClass('active')
|
||||||
|
$('*[swap-in="' + new_class + '"]').addClass('active')
|
||||||
|
$('.' + swap_class).hide()
|
||||||
|
$('.' + new_class).fadeIn()
|
||||||
|
utils.change_url_param('p', new_class)
|
||||||
|
utils.change_url_param('s', swap_class)
|
||||||
|
}
|
||||||
|
|
||||||
|
var current_page = utils.get_param('p')
|
||||||
|
var current_swap = utils.get_param('s')
|
||||||
|
if(current_page && current_swap) {
|
||||||
|
swap(current_swap, current_page)
|
||||||
|
setTimeout(() => document.querySelector(`[swap-in='${current_page}']`).click(), 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = el_swapo
|
14
app/javascript/legacy/common/event.js
Normal file
14
app/javascript/legacy/common/event.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var actions = [ 'change', 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseenter', 'mouseleave', 'scroll', 'blur', 'focus', 'input', 'submit', 'keydown', 'keypress', 'keyup' ]
|
||||||
|
|
||||||
|
function event(id, fn) {
|
||||||
|
// Find all classes ending in the event id
|
||||||
|
actions.forEach(function(action) {
|
||||||
|
$('*[on-' + action + '="' + id + '"]').each(function() {
|
||||||
|
if(this.getAttribute('on-' + action).indexOf(id) !== -1)
|
||||||
|
$(this).on(action, fn)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = event
|
180
app/javascript/legacy/common/ff-form-validation/index.es6
Normal file
180
app/javascript/legacy/common/ff-form-validation/index.es6
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const R = require('ramda')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const serializeForm = require('form-serialize')
|
||||||
|
flyd.filter = require('flyd/module/filter')
|
||||||
|
flyd.mergeAll = require('flyd/module/mergeall')
|
||||||
|
flyd.keepWhen = require('flyd/module/keepwhen')
|
||||||
|
flyd.sampleOn = require('flyd/module/sampleon')
|
||||||
|
const readableProp = require('./lib/readable-prop.es6')
|
||||||
|
const emailRegex = require('./lib/email-regex.es6')
|
||||||
|
const currencyRegex = require('./lib/currency-regex.es6')
|
||||||
|
|
||||||
|
// constraints: a hash of key/vals where each key is the name of an input
|
||||||
|
// and each value is an object of validator names and arguments
|
||||||
|
//
|
||||||
|
// validators: a hash of validation names mapped to boolean functions
|
||||||
|
//
|
||||||
|
// messages: a hash of validator names and field names mapped to error messages
|
||||||
|
//
|
||||||
|
// messages match on the most specific thing in the messages hash
|
||||||
|
// - first checks if there is an exact match on field name
|
||||||
|
// - then checks for match on validator name
|
||||||
|
//
|
||||||
|
// Given a constraint like:
|
||||||
|
// {name: {required: true}}
|
||||||
|
//
|
||||||
|
// All of the following will set an error for above, starting with most specific first:
|
||||||
|
// {name: {required: 'Please enter a valid name'}
|
||||||
|
// {name: 'Please enter your name'}
|
||||||
|
// {required: 'This is required'}
|
||||||
|
|
||||||
|
|
||||||
|
function init(state) {
|
||||||
|
state = R.merge({
|
||||||
|
validators: R.merge(defaultValidators, state.validators || {})
|
||||||
|
, messages: R.merge(defaultMessages, state.messages || {})
|
||||||
|
, focus$: flyd.stream()
|
||||||
|
, change$: flyd.stream()
|
||||||
|
, submit$: flyd.stream()
|
||||||
|
}, state || {})
|
||||||
|
const valField = validateField(state)
|
||||||
|
const valForm = validateForm(state)
|
||||||
|
|
||||||
|
const fieldErr$ = flyd.map(valField, state.change$)
|
||||||
|
const formErr$ = flyd.map(valForm, state.submit$)
|
||||||
|
const clearErr$ = flyd.map(ev => [ev.target.name, null], state.focus$)
|
||||||
|
const allErrs$ = flyd.mergeAll([fieldErr$, formErr$, clearErr$])
|
||||||
|
|
||||||
|
// Stream of all errors combined into one object
|
||||||
|
state.errors$ = flyd.scan(R.assoc, {}, allErrs$)
|
||||||
|
|
||||||
|
// Stream of field names and new values and whole form data
|
||||||
|
state.nameVal$ = flyd.map(node => [node.name, node.value], state.change$)
|
||||||
|
state.data$ = flyd.scan(R.assoc, {}, state.nameVal$)
|
||||||
|
|
||||||
|
// Streams of errors and data on form submit
|
||||||
|
const errorsOnSubmit$ = flyd.sampleOn(state.submit$, state.errors$)
|
||||||
|
state.validSubmit$ = flyd.filter(R.compose(R.none, R.values), errorsOnSubmit$)
|
||||||
|
state.validData$ = flyd.keepWhen(state.validSubmit$, state.data$)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Pass in an array of validation functions and the event object
|
||||||
|
// Will return a pair of [name, errorMsg] (errorMsg will be null if no errors present)
|
||||||
|
const validateField = R.curry((state, node) => {
|
||||||
|
const value = node.value
|
||||||
|
const name = node.name
|
||||||
|
if(!state.constraints[name]) return [name, null] // no validators for this field present
|
||||||
|
|
||||||
|
// Find the first constraint that fails its validator
|
||||||
|
for(var valName in state.constraints[name]) {
|
||||||
|
const arg = state.constraints[name][valName]
|
||||||
|
if(!state.validators[valName]) {
|
||||||
|
console.warn("Form validation constraint does not exist:", valName)
|
||||||
|
} else if(!validators[valName](value, arg)) {
|
||||||
|
const msg = getErr(messages, name, valName, arg)
|
||||||
|
return [name, String(msg)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [name, null] // no error found
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Given the messages object, the validator argument, the field name, and the validator name
|
||||||
|
// Retrieve and apply the error message function
|
||||||
|
const getErr = (messages, name, valName, arg) => {
|
||||||
|
const err = messages[name]
|
||||||
|
? messages[name][valName] || messages[name]
|
||||||
|
: messages[valName]
|
||||||
|
if(typeof err === 'function') return err(arg)
|
||||||
|
else return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Retrieve errors for the entire set of form data, used on form submit events,
|
||||||
|
// using the form data saved into the state
|
||||||
|
const validateForm = R.curry((state, node) => {
|
||||||
|
const formData = serializeForm(node, {hash: true})
|
||||||
|
for(var fieldName in constraints) { // not using higher order .map or reduce so we can break and return early
|
||||||
|
for(var valName in constraints[fieldName]) {
|
||||||
|
const arg = constraints[fieldName][valName]
|
||||||
|
if(!validators[valName]) {
|
||||||
|
console.warn("Form validation constraint does not exist:", valName)
|
||||||
|
} else if(!validators[valName](value, arg)) {
|
||||||
|
const msg = getErr(messages, name, valName, arg)
|
||||||
|
return [name, String(msg)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- Views
|
||||||
|
|
||||||
|
const validatedForm = R.curry((state, elm) => {
|
||||||
|
elm.data = R.merge(elm.data, {
|
||||||
|
on: {submit: ev => {ev.preventDefault(); state.submit$(ev.currentTarget)}}
|
||||||
|
})
|
||||||
|
return elm
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// A single form field
|
||||||
|
// Data takes normal snabbdom data for the input/select/textarea (eg props, style, on)
|
||||||
|
const validatedField = R.curry((state, elm) => {
|
||||||
|
if(!elm.data.props || !elm.data.props.name) throw new Error(`You need to provide a field name for validation (using the 'props.name' property)`)
|
||||||
|
var err = state.errors$()[elm.data.props.name]
|
||||||
|
var invalid = err && err.length
|
||||||
|
|
||||||
|
elm.data = R.merge(elm.data, {
|
||||||
|
on: {
|
||||||
|
focus: state.focus$
|
||||||
|
, change: ev => state.change$([ev.currentTarget, state])
|
||||||
|
}
|
||||||
|
, class: { invalid }
|
||||||
|
})
|
||||||
|
|
||||||
|
return h('div', {
|
||||||
|
props: {className: 'ff-field' + (invalid ? ' ff-field--invalid' : ' ff-field--valid')}
|
||||||
|
}, [
|
||||||
|
invalid ? h('p.ff-field-errorMessage', err) : ''
|
||||||
|
, elm
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
var defaultMessages = {
|
||||||
|
email: 'Please enter a valid email address'
|
||||||
|
, required: 'This field is required'
|
||||||
|
, currency: 'Please enter valid currency'
|
||||||
|
, format: "This doesn't look like the right format"
|
||||||
|
, isNumber: 'This should be a number'
|
||||||
|
, max: n => `This should be less than ${n}`
|
||||||
|
, min: n => `This should be at least ${n}`
|
||||||
|
, equalTo: n => `This should be equal to ${n}`
|
||||||
|
, maxLength: n => `This should be no longer than ${n}`
|
||||||
|
, minLength: n => `This should be longer than ${n}`
|
||||||
|
, lengthEquals: n => `This should have a length of ${n}`
|
||||||
|
, includedIn: arr => `This should be one of: ${arr.join(', ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultValidators = {
|
||||||
|
email: val => val.match(emailRegex)
|
||||||
|
, present: val => Boolean(val)
|
||||||
|
, currency: val => String(val).match(currencyRegex)
|
||||||
|
, format: (val, arg) => String(val).match(arg)
|
||||||
|
, isNumber: val => !isNaN(val)
|
||||||
|
, max: (val, n) => val <= n
|
||||||
|
, min: (val, n) => val >= n
|
||||||
|
, equalTo: (val, n) => n === val
|
||||||
|
, maxLength: (val, n) => val.length <= n
|
||||||
|
, minLength: (val, n) => val.length >= n
|
||||||
|
, lengthEquals: (val, n) => val.length === n
|
||||||
|
, includedIn: (val, arr) => arr.indexOf(val) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {init, validatedField, validatedForm}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = /^(?!0\.00)\d{1,3}(,\d{3})*(\.\d\d)?$/
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
|
||||||
|
module.exports = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// "email_address" => "email address"
|
||||||
|
// "emailAddress" => "email address"
|
||||||
|
|
||||||
|
module.exports = str =>
|
||||||
|
str
|
||||||
|
.replace('_', ' ')
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
|
.replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
|
17
app/javascript/legacy/common/file-input-stream.js
Normal file
17
app/javascript/legacy/common/file-input-stream.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
// Given an input element, return a stream of the input file data as text
|
||||||
|
|
||||||
|
module.exports = R.curry(node => {
|
||||||
|
var $stream = flyd.stream()
|
||||||
|
var file = node.files[0]
|
||||||
|
var reader = new FileReader()
|
||||||
|
if(file instanceof Blob) {
|
||||||
|
reader.readAsText(file)
|
||||||
|
reader.onload = e => $stream(reader.result)
|
||||||
|
}
|
||||||
|
return $stream
|
||||||
|
})
|
||||||
|
|
44
app/javascript/legacy/common/form-to-object.js
Normal file
44
app/javascript/legacy/common/form-to-object.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// Convert a form to an object literal
|
||||||
|
module.exports = function(form) {
|
||||||
|
if(form === undefined) throw new Error("form is undefined")
|
||||||
|
var result = {}
|
||||||
|
var fields = toArr(form.querySelectorAll('input, textarea, select'))
|
||||||
|
.filter(function(n) { return n.hasAttribute('name') })
|
||||||
|
.map(function(n) {
|
||||||
|
var name = n.getAttribute('name')
|
||||||
|
var keys = n.getAttribute('name').split('.')
|
||||||
|
if(n.value && n.value.toString().length) { // won't set empty strings for empty vals
|
||||||
|
if(n.getAttribute('type') === 'checkbox') {
|
||||||
|
deepSet(keys, n.checked, result)
|
||||||
|
} else if(n.getAttribute('type') === 'radio') {
|
||||||
|
if(n.checked) deepSet(keys, n.value, result)
|
||||||
|
} else {
|
||||||
|
deepSet(keys, n.value, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArr(x) { return Array.prototype.slice.call(x) }
|
||||||
|
|
||||||
|
// Given an array of nested keys, a value, and a target object:
|
||||||
|
// Set the value into the object at the last nested key
|
||||||
|
function deepSet(keys, val, obj, options) {
|
||||||
|
var exceptLast = keys.slice(0, keys.length-1)
|
||||||
|
var last = keys[keys.length-1]
|
||||||
|
var nested = exceptLast.reduce(function(nestedObj, key) {
|
||||||
|
if(nestedObj[key] === undefined) {
|
||||||
|
nestedObj[key] = {}
|
||||||
|
return nestedObj[key]
|
||||||
|
} else {
|
||||||
|
return nestedObj[key]
|
||||||
|
}
|
||||||
|
}, obj)
|
||||||
|
// if(nested[last] === undefined) nested[last] = {}
|
||||||
|
nested[last] = val
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
20
app/javascript/legacy/common/form.js
Normal file
20
app/javascript/legacy/common/form.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var form = module.exports = {
|
||||||
|
loading: loading,
|
||||||
|
showErr: showErr,
|
||||||
|
clear: clear
|
||||||
|
}
|
||||||
|
|
||||||
|
function loading(formEl) {
|
||||||
|
$(formEl).find('button[type="submit"]').loading()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErr(msg, el) {
|
||||||
|
$(el).find('.status').addClass('error').text(msg)
|
||||||
|
$(el).find('button[type="submit"]').disableLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear(el) {
|
||||||
|
$(el).find('.status').removeClass('error').text('')
|
||||||
|
$(el).find('button[type="submit"]').disableLoading()
|
||||||
|
}
|
126
app/javascript/legacy/common/format.js
Normal file
126
app/javascript/legacy/common/format.js
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var moment = require('moment')
|
||||||
|
var format = {}
|
||||||
|
|
||||||
|
module.exports = format
|
||||||
|
|
||||||
|
// Convert a snake-case phrase (eg. 'requested_by_customer') to a readable phrase (eg. 'Requested by customer')
|
||||||
|
format.snake_to_words = function(snake, options) {
|
||||||
|
if(!snake) return snake
|
||||||
|
return snake.replace(/_/g, ' ').replace(/^./, function(m) {return m.toUpperCase()})
|
||||||
|
}
|
||||||
|
|
||||||
|
format.camelToWords = function(str, os) {
|
||||||
|
if(!str) return str
|
||||||
|
return str.replace(/([A-Z])/g, " $1")
|
||||||
|
}
|
||||||
|
|
||||||
|
format.dollarsToCents = function(dollars) {
|
||||||
|
dollars = dollars.toString().replace(/[$,]/g, '')
|
||||||
|
if(!isNaN(dollars) && dollars.match(/^-?\d+\.\d$/)) {
|
||||||
|
// could we use toFixed instead? Probably but this is straightforward.
|
||||||
|
dollars = dollars + "0"
|
||||||
|
}
|
||||||
|
if(isNaN(dollars) || !dollars.match(/^-?\d+(\.\d\d)?$/)) throw "Invalid dollar amount: " + dollars
|
||||||
|
return Math.round(Number(dollars) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
format.centsToDollars = function(cents, options={}) {
|
||||||
|
if(cents === undefined) return '0'
|
||||||
|
return format.numberWithCommas((Number(cents) / 100.0).toFixed(options.noCents ? 0 : 2).toString()).replace(/\.00$/,'')
|
||||||
|
}
|
||||||
|
|
||||||
|
format.weeklyToMonthly = function(amount) {
|
||||||
|
if (amount === undefined) return 0;
|
||||||
|
return Math.round(4.3 * amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
format.numberWithCommas = function(n) {
|
||||||
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
format.percent = function(x, y) {
|
||||||
|
if(!x || !y) return 0
|
||||||
|
return Math.round(y / x * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
format.pluralize = function(quantity, plural_word) {
|
||||||
|
if(quantity === undefined || quantity === null) return '0 '+plural_word
|
||||||
|
var str = String(quantity) + ' '
|
||||||
|
if(quantity !== 1) return str+plural_word
|
||||||
|
else return str + appl.to_singular(plural_word)
|
||||||
|
}
|
||||||
|
|
||||||
|
format.capitalize = function (string) {
|
||||||
|
return string.split(' ')
|
||||||
|
.map(function(s) { return s.charAt(0).toUpperCase() + s.slice(1) })
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
format.toSentence = function(arr) {
|
||||||
|
if(arr.length < 2) return arr
|
||||||
|
if(arr.length === 2) return arr[0] + ' and ' + arr[1]
|
||||||
|
var last = arr.length - 1
|
||||||
|
return arr.slice(0, last).join(', ') + ', and ' + arr[last]
|
||||||
|
}
|
||||||
|
|
||||||
|
format.zeroPad = function(num, size) {
|
||||||
|
var str = num + ""
|
||||||
|
while(str.length < size) str = "0" + str
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
format.sanitizeHtml = function(html) {
|
||||||
|
if(!html) return
|
||||||
|
var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*'
|
||||||
|
var tagOrComment = new RegExp(
|
||||||
|
'<(?:'
|
||||||
|
// Comment body.
|
||||||
|
+ '!--(?:(?:-*[^->])*--+|-?)'
|
||||||
|
// Special "raw text" elements whose content should be elided.
|
||||||
|
+ '|script\\b' + tagBody + '>[\\s\\S]*?</script\\s*'
|
||||||
|
+ '|style\\b' + tagBody + '>[\\s\\S]*?</style\\s*'
|
||||||
|
// Regular name
|
||||||
|
+ '|/?[a-z]'
|
||||||
|
+ tagBody
|
||||||
|
+ ')>',
|
||||||
|
'gi')
|
||||||
|
return html.replace(tagOrComment, '').replace(/</g, '<')
|
||||||
|
}
|
||||||
|
|
||||||
|
format.sql = {}
|
||||||
|
|
||||||
|
format.sql.format_sql_array = function(str) {
|
||||||
|
if(!str) return ''
|
||||||
|
return format.toSentence(
|
||||||
|
str.replace(/[""{}]/g,'')
|
||||||
|
.split(',')
|
||||||
|
.filter(function(str) {return str !== 'NULL'})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
format.date = {}
|
||||||
|
|
||||||
|
format.date.readableWithTime = function(str) {
|
||||||
|
return moment(str).format("YYYY-MM-DD h:MMa")
|
||||||
|
}
|
||||||
|
|
||||||
|
format.date.toStandard = function(str) {
|
||||||
|
return moment(str).format("YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
|
||||||
|
format.date.toSimple = function(str) {
|
||||||
|
if(!str || !str.length) return ''
|
||||||
|
var d = new Date(str)
|
||||||
|
return format.zeroPad(d.getMonth() + 1, 2) + '/' +
|
||||||
|
format.zeroPad(d.getDate(), 2) + '/' +
|
||||||
|
format.zeroPad(d.getFullYear(), 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
format.geography = {}
|
||||||
|
|
||||||
|
format.geography.isUS = function(str) {
|
||||||
|
return Boolean(str.match(/(^united states( of america)?$)|(^u\.?s\.?a?\.?$)/i))
|
||||||
|
}
|
20
app/javascript/legacy/common/format_response_error.js
Normal file
20
app/javascript/legacy/common/format_response_error.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// This is a little utility to convert a superagent response that has an error
|
||||||
|
// into a readable single string message
|
||||||
|
//
|
||||||
|
// This should work both with 422 unprocessable entities as well as 500 server errors
|
||||||
|
|
||||||
|
module.exports = show_err
|
||||||
|
|
||||||
|
var err_msg = "We're sorry, but something went wrong. Please try again soon."
|
||||||
|
|
||||||
|
function show_err(resp) {
|
||||||
|
console.error(resp)
|
||||||
|
|
||||||
|
if(resp.body && resp.body.error) { return resp.body.error }
|
||||||
|
if(resp.body && resp.body.errors && resp.body.errors.length) { return resp.body.errors[0] }
|
||||||
|
if(resp.body) { return resp.body }
|
||||||
|
if(resp.error) { return resp.error }
|
||||||
|
return err_msg
|
||||||
|
}
|
||||||
|
|
16
app/javascript/legacy/common/fundraiser_metrics.js
Normal file
16
app/javascript/legacy/common/fundraiser_metrics.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const format = require('../common/format')
|
||||||
|
require('../common/restful_resource')
|
||||||
|
|
||||||
|
appl.def('ajax_metrics', {
|
||||||
|
index: function() {
|
||||||
|
appl.ajax.index('metrics').then(function(resp) {
|
||||||
|
appl.def('metrics.percentage_funded', R.clamp(1,100, format.percent(
|
||||||
|
resp.body.goal_amount
|
||||||
|
, resp.body.total_raised
|
||||||
|
))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
15
app/javascript/legacy/common/geography.js
Normal file
15
app/javascript/legacy/common/geography.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
var geo = {}
|
||||||
|
|
||||||
|
geo.stateCodes = [ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'PR', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY' ]
|
||||||
|
|
||||||
|
geo.countries = ["(select)","Austria", "Belgium", "Bosnia and Herzegowina", "Bulgaria", "Croatia (Hrvatska)", "Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Iceland", "India", "Indonesia", "Iran (Islamic Republic of)", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao, People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia, The Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia (Slovak Republic)", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "St. Helena", "St. Pierre and Miquelon", "Sudan", "Suriname", "Svalbard and Jan Mayen Islands", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Virgin Islands (British)", "Virgin Islands (U.S.)", "Wallis and Futuna Islands", "Western Sahara", "Yemen", "Yugoslavia", "Zambia", "Zimbabwe"]
|
||||||
|
|
||||||
|
geo.countriesWithUSFirst = ["United States", "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia (Hrvatska)", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Eritrea", "Estonia", "Ethiopia", "Falkland Islands (Malvinas)", "Faroe Islands", "Fiji", "Finland", "France", "France Metropolitan", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard and Mc Donald Islands", "Holy See (Vatican City State)", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iran (Islamic Republic of)", "Iraq", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Democratic People's Republic of", "Korea, Republic of", "Kuwait", "Kyrgyzstan", "Lao, People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Libyan Arab Jamahiriya", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia, The Former Yugoslav Republic of", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russian Federation", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia (Slovak Republic)", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "Spain", "Sri Lanka", "St. Helena", "St. Pierre and Miquelon", "Sudan", "Suriname", "Svalbard and Jan Mayen Islands", "Swaziland", "Sweden", "Switzerland", "Syrian Arab Republic", "Taiwan, Province of China", "Tajikistan", "Tanzania, United Republic of", "Thailand", "Togo", "Tokelau", "Tonga", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Venezuela", "Vietnam", "Virgin Islands (British)", "Virgin Islands (U.S.)", "Wallis and Futuna Islands", "Western Sahara", "Yemen", "Yugoslavia", "Zambia", "Zimbabwe"]
|
||||||
|
|
||||||
|
geo.isUSA = function(str) {
|
||||||
|
return Boolean(str.match(/(^united states( of america)?$)|(^u\.?s\.?a?\.?$)/i))
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = geo
|
11
app/javascript/legacy/common/get-valid-data.js
Normal file
11
app/javascript/legacy/common/get-valid-data.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flimflam/flyd')
|
||||||
|
const request = require("../common/request")
|
||||||
|
|
||||||
|
module.exports = (path, query) => {
|
||||||
|
const url = '/'
|
||||||
|
const response$ = request({method: 'GET', url, path, query}).load
|
||||||
|
const valid$ = flyd.filter(x => x.status === 200, response$)
|
||||||
|
return flyd.map(x => x.body, valid$)
|
||||||
|
}
|
||||||
|
|
34
app/javascript/legacy/common/image_uploader.js
Normal file
34
app/javascript/legacy/common/image_uploader.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
$('.image-upload input').change(function(e) {
|
||||||
|
var self = this
|
||||||
|
appl.def('image_upload.is_selecting', true)
|
||||||
|
if(this.files && this.files[0]) {
|
||||||
|
var reader = new FileReader()
|
||||||
|
reader.onload = function(e) {
|
||||||
|
if(e.valueOf().loaded >= 3000000) {
|
||||||
|
appl.def('image_upload.error', 'Please select a file smaller than 3mb')
|
||||||
|
} else {
|
||||||
|
appl.def('image_upload.error', '')
|
||||||
|
}
|
||||||
|
$(self).parent().css('background-image', "url('" + e.target.result + "')")
|
||||||
|
$(self).parent().addClass('live-preview')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(this.files[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('remove_image', function(url, resource, notification, payload) {
|
||||||
|
var data = {}
|
||||||
|
data[resource] = payload
|
||||||
|
appl.notify(notification)
|
||||||
|
appl.def('loading', true)
|
||||||
|
$.ajax({
|
||||||
|
type: 'put',
|
||||||
|
url: url,
|
||||||
|
data: data,
|
||||||
|
})
|
||||||
|
.done(function() {
|
||||||
|
appl.reload()
|
||||||
|
})
|
||||||
|
.fail(function(e) { })
|
||||||
|
})
|
32
app/javascript/legacy/common/jquery_additions.js
vendored
Normal file
32
app/javascript/legacy/common/jquery_additions.js
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
$.fn.serializeObject = function() {
|
||||||
|
return this.serializeArray().reduce(function(obj, field) {
|
||||||
|
if(field.value)
|
||||||
|
var val = field.value
|
||||||
|
else if(field.files && field.files[0])
|
||||||
|
var val = field.files[0]
|
||||||
|
obj[field.name] = val
|
||||||
|
return obj
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a button enter the ajax loading state, where it's disabled and has a little spinner.
|
||||||
|
$.fn.loading = function(message) {
|
||||||
|
this.each(function() {
|
||||||
|
var msg = message || this.getAttribute('data-loading')
|
||||||
|
this.setAttribute('data-text', this.innerHTML)
|
||||||
|
this.innerHTML = "<i class='fa fa-spin fa-spinner'></i> " + msg
|
||||||
|
this.setAttribute('disabled', 'disabled')
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn.disableLoading = function() {
|
||||||
|
this.each(function() {
|
||||||
|
if(!this.hasAttribute('disabled')) return
|
||||||
|
var old_text = this.getAttribute('data-text')
|
||||||
|
this.innerHTML = old_text
|
||||||
|
this.removeAttribute('disabled')
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
}
|
13
app/javascript/legacy/common/notification.js
Normal file
13
app/javascript/legacy/common/notification.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var notification = function(msg, err) {
|
||||||
|
var el = document.getElementById('js-notification')
|
||||||
|
if(err) {el.className = 'show error'}
|
||||||
|
else {el.className = 'show'}
|
||||||
|
el.innerText = msg
|
||||||
|
window.setTimeout(function() {
|
||||||
|
el.className = ''
|
||||||
|
el.innerText = ''
|
||||||
|
}, 7000)
|
||||||
|
}
|
||||||
|
module.exports = notification
|
||||||
|
|
11
app/javascript/legacy/common/on-change-sanitize-slug.js
Normal file
11
app/javascript/legacy/common/on-change-sanitize-slug.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const sanitize = require('./sanitize-slug')
|
||||||
|
|
||||||
|
// Just a hacky way to automatically sanitize slug inputs when they are changed
|
||||||
|
|
||||||
|
var inputs = document.querySelectorAll('.js-sanitizeSlug')
|
||||||
|
|
||||||
|
R.map(
|
||||||
|
inp => inp.addEventListener('change', ev => ev.currentTarget.value = sanitize(ev.currentTarget.value || ev.currentTarget.getAttribute('data-slug-default')))
|
||||||
|
, inputs )
|
11
app/javascript/legacy/common/on-ios11.js
Normal file
11
app/javascript/legacy/common/on-ios11.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
function calculateIOS()
|
||||||
|
{
|
||||||
|
var userAgent = window.navigator.userAgent;
|
||||||
|
var has11 = userAgent.search("OS 11_\\d") > 0
|
||||||
|
var hasMacOS = userAgent.search(" like Mac OS X") > 0
|
||||||
|
|
||||||
|
return has11 && hasMacOS;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = calculateIOS
|
280
app/javascript/legacy/common/onboard.js
Normal file
280
app/javascript/legacy/common/onboard.js
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flimflam/flyd')
|
||||||
|
const h = require('flimflam/h')
|
||||||
|
const R = require('ramda')
|
||||||
|
const modal = require('flimflam/ui/modal')
|
||||||
|
const render = require('flimflam/render')
|
||||||
|
const wizard = require('flimflam/ui/wizard')
|
||||||
|
const validatedForm = require('flimflam/ui/validated-form')
|
||||||
|
const request = require('./request')
|
||||||
|
const notification = require('flimflam/ui/notification')
|
||||||
|
const fieldWithError = require('../components/field-with-error')
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const orgForm = validatedForm.init({constraints: constraints.org})
|
||||||
|
const contactForm = validatedForm.init({constraints: constraints.contact})
|
||||||
|
const infoForm = validatedForm.init({constraints: constraints.info})
|
||||||
|
const currentStep$ = flyd.mergeAll([
|
||||||
|
flyd.stream(0)
|
||||||
|
, flyd.map(R.always(1), orgForm.validSubmit$)
|
||||||
|
, flyd.map(R.always(2), infoForm.validSubmit$)
|
||||||
|
])
|
||||||
|
const wiz = wizard.init({currentStep$})
|
||||||
|
const openModal$ = flyd.stream()
|
||||||
|
document.querySelectorAll('[data-ff-open-onboard]')
|
||||||
|
.forEach(x => {x.addEventListener('click', openModal$)})
|
||||||
|
|
||||||
|
//flyd.map(trackGA, openModal$)
|
||||||
|
|
||||||
|
const response$ = flyd.flatMap(postData(orgForm, infoForm, contactForm), contactForm.validData$)
|
||||||
|
const respOk$ = flyd.filter(resp => resp.status === 200, response$)
|
||||||
|
const respErr$ = flyd.filter(resp => resp.status !== 200, response$)
|
||||||
|
const loading$ = flyd.mergeAll([
|
||||||
|
flyd.map(R.always(true), contactForm.validSubmit$)
|
||||||
|
, flyd.map(R.always(false), response$)
|
||||||
|
])
|
||||||
|
|
||||||
|
const message$ = flyd.mergeAll([
|
||||||
|
flyd.map(R.always("Saving your data..."), contactForm.validSubmit$)
|
||||||
|
, flyd.map(R.always("Thank you! Now redirecting..."), respOk$)
|
||||||
|
, flyd.map(resp => `There was an error: ${resp.body.error}`, respErr$)
|
||||||
|
])
|
||||||
|
|
||||||
|
const notif = notification.init({message$, hideDelay: 20000})
|
||||||
|
flyd.map(resp => {setTourCookies(resp.body.nonprofit); window.location = '/'}, respOk$)
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
openModal$
|
||||||
|
, currentStep$
|
||||||
|
, wiz
|
||||||
|
, orgForm
|
||||||
|
, contactForm
|
||||||
|
, infoForm
|
||||||
|
, loading$
|
||||||
|
, notif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const trackGA = () => {
|
||||||
|
// if(!ga) return
|
||||||
|
// ga('send', {
|
||||||
|
// hitType: 'event',
|
||||||
|
// eventCategory: 'ClickSignUp',
|
||||||
|
// eventAction: 'click',
|
||||||
|
// eventLabel: location.pathname
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
const setTourCookies = nonprofit => {
|
||||||
|
document.cookie = `tour_dashboard=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_campaign=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_event=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_profile=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_transactions=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_supporters=${nonprofit.id};path=/`
|
||||||
|
document.cookie = `tour_subscribers=${nonprofit.id};path=/`
|
||||||
|
}
|
||||||
|
|
||||||
|
const postData = (orgForm, infoForm) => contactFormData => {
|
||||||
|
const send = {
|
||||||
|
nonprofit: orgForm.validData$()
|
||||||
|
, extraInfo: infoForm.validData$()
|
||||||
|
, user: contactFormData
|
||||||
|
}
|
||||||
|
return request({
|
||||||
|
method: 'post'
|
||||||
|
, path: '/nonprofits/onboard'
|
||||||
|
, send
|
||||||
|
}).load
|
||||||
|
}
|
||||||
|
|
||||||
|
const constraints = {
|
||||||
|
org: {
|
||||||
|
name: {required: true}
|
||||||
|
, city: {required: true}
|
||||||
|
, state_code: {required: true}
|
||||||
|
, zip_code: {required: true}
|
||||||
|
}
|
||||||
|
, contact: {
|
||||||
|
email: {required: true, email: true}
|
||||||
|
, name: {required: true}
|
||||||
|
, phone: {required: true}
|
||||||
|
, password: {required: true, minLength: 7}
|
||||||
|
, password_confirmation: {required: true, matchesField: 'password'}
|
||||||
|
}
|
||||||
|
, info: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = state => {
|
||||||
|
return h('div', [
|
||||||
|
modal({
|
||||||
|
show$: state.openModal$
|
||||||
|
, body: onboardWizard(state)
|
||||||
|
, title: 'Get started'
|
||||||
|
})
|
||||||
|
, notification.view(state.notif)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onboardWizard = state => {
|
||||||
|
const labels = [ 'Org', 'Info', 'Contact' ]
|
||||||
|
const steps = [ orgForm(state) , infoForm(state), contactForm(state) ]
|
||||||
|
return h('div', [
|
||||||
|
wizard.labels(state.wiz, labels)
|
||||||
|
, wizard.content(state.wiz, steps)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricingDetails = h('div.u-marginTop--15.u-padding--10.u-background--fog', [
|
||||||
|
h('p', [
|
||||||
|
"CommitChange uses "
|
||||||
|
, h('a.strong', {props: {href: 'https://www.stripe.com/', target :'_blank'}}, 'Stripe')
|
||||||
|
, ' to process transactions. Stripe takes a '
|
||||||
|
, h('strong', `${ENV.feeRate}% + ${ENV.perTransaction}¢`)
|
||||||
|
, ' processing fee on every transaction.'])
|
||||||
|
, h('p', [
|
||||||
|
'In order to support operations, feature development, and community building, '
|
||||||
|
, 'CommitChange takes an additional fee of '
|
||||||
|
, h('strong', `${ENV.platformFeeRate}%.`)
|
||||||
|
])
|
||||||
|
, h('p.u-marginBottom--0', [
|
||||||
|
"Our fee scales down as your transaction volume scales up. "
|
||||||
|
, h('a.strong', {props: {href: 'mailto:support@commitchange.com'}}, 'Contact us')
|
||||||
|
, " to chat about volume discounts."
|
||||||
|
])
|
||||||
|
])
|
||||||
|
|
||||||
|
const orgForm = state => {
|
||||||
|
const form = validatedForm.form(state.orgForm)
|
||||||
|
const field = fieldWithError(state.orgForm)
|
||||||
|
return h('div', [
|
||||||
|
form(h('form', [
|
||||||
|
h('fieldset', [
|
||||||
|
h('label', 'Organization Name')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'name', placeholder: ''}}))
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('label', 'Website URL')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'website', placeholder: 'https://your-website.org'}}))
|
||||||
|
])
|
||||||
|
, h('div.clearfix', [
|
||||||
|
h('fieldset.col-left-6.u-paddingRight--10', [
|
||||||
|
h('label', 'Org Email (public)')
|
||||||
|
, field(h('input', {props: {type: 'email', name: 'email', placeholder: 'example@name.org'}}))
|
||||||
|
])
|
||||||
|
, h('fieldset.col-left-6', [
|
||||||
|
h('label', 'Org Phone (public)')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'phone', placeholder: '(XXX) XXX-XXXX'}}))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
, h('div.clearfix', [
|
||||||
|
h('fieldset.col-left-6.u-paddingRight--10', [
|
||||||
|
h('label', 'City')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'city', placeholder: ''}}))
|
||||||
|
])
|
||||||
|
, h('fieldset.col-left-3.u-paddingRight--10', [
|
||||||
|
h('label', 'State')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'state_code', placeholder: 'NY'}}))
|
||||||
|
])
|
||||||
|
, h('fieldset.col-left-3', [
|
||||||
|
h('label', 'Zip Code')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'zip_code', placeholder: ''}}))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
, h('div', [
|
||||||
|
h('button.button', 'Next')
|
||||||
|
])
|
||||||
|
]))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoForm = state => {
|
||||||
|
const form = validatedForm.form(state.infoForm)
|
||||||
|
const field = fieldWithError(state.infoForm)
|
||||||
|
|
||||||
|
return h('div', [
|
||||||
|
form(h('form', [
|
||||||
|
h('div.u-marginBottom--20', [
|
||||||
|
h('fieldset', [
|
||||||
|
h('label', {props: {htmlFor: 'registered-npo-checkbox'}}, 'What kind of entity are you fundraising for?')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'radio', name: 'entity_type', value: 'nonprofit', id: 'onboard-entity-nonprofit'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-entity-nonprofit'}}, 'A registered nonprofit')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'radio', name: 'entity_type', value: 'forprofit', id: 'onboard-entity-forprofit'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-entity-forprofit'}}, 'A for-profit company')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'radio', name: 'entity_type', value: 'unregistered', id: 'onboard-entity-unregistered'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-entity-unregistered'}}, 'An unregistered project, group, club, or other cause')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
, h('div.u-marginBottom--20', [
|
||||||
|
h('fieldset', [
|
||||||
|
h('label', 'How do you want to use CommitChange?')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'checkbox', name: 'use_donations', id: 'onboard-use-donations'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-use-donations'}}, 'Donation processing')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'checkbox', name: 'use_crm', id: 'onboard-use-crm'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-use-crm'}}, 'Supporter relationship management')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'checkbox', name: 'use_campaigns', id: 'onboard-use-campaigns'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-use-campaigns'}}, 'Campaign fundriasing')
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('input', {props: {type: 'checkbox', name: 'use_events', id: 'onboard-use-events'}})
|
||||||
|
, h('label', {props: {htmlFor: 'onboard-use-events'}}, 'Event pages and ticketing')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('label', 'How did you hear about CommitChange?')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'how_they_heard', placeholder: 'Google, radio, referral, etc'}}))
|
||||||
|
])
|
||||||
|
, h('button.button', 'Next')
|
||||||
|
]))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactForm = state => {
|
||||||
|
const form = validatedForm.form(state.contactForm)
|
||||||
|
const field = fieldWithError(state.contactForm)
|
||||||
|
return h('div', [
|
||||||
|
form(h('form', [
|
||||||
|
h('div.clearfix', [
|
||||||
|
h('fieldset.col-left-6.u-paddingRight--10', [
|
||||||
|
h('label', 'Your Name')
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'name', placeholder: 'Full Name'}}))
|
||||||
|
])
|
||||||
|
, h('fieldset.col-left-6', [
|
||||||
|
h('label', 'Your Email (used for login)')
|
||||||
|
, field(h('input', {props: {type: 'email', name: 'email', placeholder: 'youremail@example.com'}}))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('label', 'New Password')
|
||||||
|
, field(h('input', {props: {type: 'password', name: 'password', placeholder: ''}}))
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('label', 'Retype Password')
|
||||||
|
, field(h('input', {props: {type: 'password', name: 'password_confirmation', placeholder: ''}}))
|
||||||
|
])
|
||||||
|
, h('fieldset', [
|
||||||
|
h('label', ['Your Phone', h('small', ' (for account recovery)')])
|
||||||
|
, field(h('input', {props: {type: 'text', name: 'phone', placeholder: '(XXX) XXX-XXXX'}}))
|
||||||
|
])
|
||||||
|
, h('button.button', {props: {disabled: state.loading$()}}, 'Save & Finish')
|
||||||
|
]))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.querySelector("#ff-render-onboard")
|
||||||
|
render(view, init(), container)
|
||||||
|
|
71
app/javascript/legacy/common/panels_layout.js
Normal file
71
app/javascript/legacy/common/panels_layout.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var $panelsLayout = $('.panelsLayout'),
|
||||||
|
$panelsLayoutBody = $panelsLayout.find('.panelsLayout-body'),
|
||||||
|
$sidePanel = $panelsLayoutBody.find('.sidePanel'),
|
||||||
|
$mainPanel = $panelsLayoutBody.find('.mainPanel'),
|
||||||
|
filterButton = document.getElementById('button--openFilter'),
|
||||||
|
$tableMeta = $('.table-meta--main'),
|
||||||
|
win = window
|
||||||
|
|
||||||
|
function setPanelsLayoutBodyHeight(){
|
||||||
|
var bodyOffsetTop = $panelsLayoutBody.offset().top
|
||||||
|
var winInnerHeight = win.innerHeight
|
||||||
|
var calculatedHeight = (winInnerHeight - bodyOffsetTop) + 'px'
|
||||||
|
|
||||||
|
if($('.filterPanel').length)
|
||||||
|
$('.filterPanel, .sidePanel, .mainPanel').css('height', calculatedHeight)
|
||||||
|
else
|
||||||
|
$('.sidePanel, .mainPanel').css('height', calculatedHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanelsLayoutBodyHeight()
|
||||||
|
$(win).resize(setPanelsLayoutBodyHeight)
|
||||||
|
|
||||||
|
appl.def('open_side_panel', function(){
|
||||||
|
appl.def('is_showing_side_panel', true)
|
||||||
|
$panelsLayout.removeClass('is-showingFilterPanel')
|
||||||
|
$sidePanel.scrollTop(0)
|
||||||
|
$panelsLayout.addClass('is-showingSidePanel')
|
||||||
|
setPanelsLayoutBodyHeight()
|
||||||
|
$mainPanel.css({
|
||||||
|
left: '0px',
|
||||||
|
right: 'initial'
|
||||||
|
})
|
||||||
|
if (filterButton)
|
||||||
|
filterButton.removeAttribute('data-selected')
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('close_side_panel', function(){
|
||||||
|
appl.def('is_showing_side_panel', false)
|
||||||
|
$mainPanel.find('tr').removeAttr('data-selected')
|
||||||
|
$panelsLayout.removeClass('is-showingSidePanel')
|
||||||
|
setPanelsLayoutBodyHeight()
|
||||||
|
window.history.pushState({},'index', win.location.pathname)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('open_filter_panel', function(){
|
||||||
|
$panelsLayout.removeClass('is-showingSidePanel')
|
||||||
|
$panelsLayout.addClass('is-showingFilterPanel')
|
||||||
|
$mainPanel.find('tr').removeAttr('data-selected')
|
||||||
|
$mainPanel.css({
|
||||||
|
right: '0px',
|
||||||
|
left: 'initial'
|
||||||
|
})
|
||||||
|
filterButton.setAttribute('data-selected', '')
|
||||||
|
window.history.pushState({},'index', win.location.pathname)
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('close_filter_panel', function(){
|
||||||
|
$panelsLayout.removeClass('is-showingFilterPanel')
|
||||||
|
filterButton.removeAttribute('data-selected')
|
||||||
|
return appl
|
||||||
|
})
|
||||||
|
|
||||||
|
appl.def('scroll_main_panel', function(){
|
||||||
|
var main_panel = document.querySelector('.mainPanel')
|
||||||
|
main_panel.scrollTop = main_panel.scrollHeight
|
||||||
|
})
|
||||||
|
|
30
app/javascript/legacy/common/pikaday-timepicker.js
Normal file
30
app/javascript/legacy/common/pikaday-timepicker.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const bind = require('attr-binder')
|
||||||
|
const Pikaday = require('pikaday-time')
|
||||||
|
const moment = require('moment')
|
||||||
|
|
||||||
|
bind('pikaday-timepicker', function(container, format) {
|
||||||
|
const button = container.querySelector('a')
|
||||||
|
const input = container.querySelector('input')
|
||||||
|
input.readOnly = true
|
||||||
|
|
||||||
|
const maxDate_str = input.getAttribute('pikaday-maxDate')
|
||||||
|
const maxDate = maxDate_str ? moment(maxDate_str) : undefined
|
||||||
|
const defaultDate_str = input.getAttribute('pikaday-defaultDate')
|
||||||
|
const defaultDate = defaultDate_str ? moment(defaultDate_str) : undefined
|
||||||
|
new Pikaday({
|
||||||
|
showTime: true
|
||||||
|
, showMinutes: true
|
||||||
|
, showSeconds: false
|
||||||
|
, autoClose: false
|
||||||
|
, timeLabel: 'Time'
|
||||||
|
, format
|
||||||
|
, setDefaultDate: Boolean(defaultDate)
|
||||||
|
, field: input
|
||||||
|
, maxDate
|
||||||
|
, defaultDate
|
||||||
|
, trigger: button
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
11
app/javascript/legacy/common/polyfills.js
Normal file
11
app/javascript/legacy/common/polyfills.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// Console fallback
|
||||||
|
if (!window.console) {
|
||||||
|
window.console = new function() {
|
||||||
|
this.log = function(str) {}
|
||||||
|
this.dir = function(str) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promises polyfill
|
||||||
|
require('es6-promise').polyfill()
|
26
app/javascript/legacy/common/post-form-data.es6
Normal file
26
app/javascript/legacy/common/post-form-data.es6
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
|
||||||
|
// Returns a flyd stream
|
||||||
|
|
||||||
|
module.exports = R.curryN(2, (url, object) => {
|
||||||
|
var stream = flyd.stream()
|
||||||
|
var req = new XMLHttpRequest()
|
||||||
|
var formData = new FormData()
|
||||||
|
R.mapObjIndexed((val, key) => {
|
||||||
|
if(val.constructor === Object) val = JSON.stringify(val)
|
||||||
|
formData.append(key, val)
|
||||||
|
}, object)
|
||||||
|
req.open("POST", url)
|
||||||
|
// req.setRequestHeader('X-CSRF-Token', window._csrf)
|
||||||
|
req.send(formData)
|
||||||
|
req.onload = ev => {
|
||||||
|
var body = {}
|
||||||
|
try { body = JSON.parse(req.response) } catch(e) { }
|
||||||
|
stream( {status: req.status, body: body } )
|
||||||
|
}
|
||||||
|
return stream
|
||||||
|
})
|
||||||
|
|
27
app/javascript/legacy/common/post-form-data.js
Normal file
27
app/javascript/legacy/common/post-form-data.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
// TODO make this use flyd-ajax
|
||||||
|
// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
|
||||||
|
// Returns a flyd stream
|
||||||
|
|
||||||
|
module.exports = R.curryN(2, (url, object) => {
|
||||||
|
var stream = flyd.stream()
|
||||||
|
var req = new XMLHttpRequest()
|
||||||
|
var formData = new FormData()
|
||||||
|
R.mapObjIndexed((val, key) => {
|
||||||
|
if(val.constructor === Object) val = JSON.stringify(val)
|
||||||
|
formData.append(key, val)
|
||||||
|
}, object)
|
||||||
|
req.open("POST", url)
|
||||||
|
// req.setRequestHeader('X-CSRF-Token', window._csrf)
|
||||||
|
req.send(formData)
|
||||||
|
req.onload = ev => {
|
||||||
|
var body = {}
|
||||||
|
try { body = JSON.parse(req.response) } catch(e) { }
|
||||||
|
stream( {status: req.status, body: body } )
|
||||||
|
}
|
||||||
|
return stream
|
||||||
|
})
|
||||||
|
|
11
app/javascript/legacy/common/request.js
Normal file
11
app/javascript/legacy/common/request.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const request = require('flyd-ajax')
|
||||||
|
|
||||||
|
module.exports = options => {
|
||||||
|
options.headers = R.merge({
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
, 'X-CSRF-Token': window._csrf
|
||||||
|
}, options.headers || {})
|
||||||
|
return request(options)
|
||||||
|
}
|
139
app/javascript/legacy/common/restful_resource.js
Normal file
139
app/javascript/legacy/common/restful_resource.js
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
/* A simple module for dealing with ajax-based resources in viewscript
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Define a 'resource object' in appl that has these properties
|
||||||
|
* resource_name: 'donations' (plural name that matches the model)
|
||||||
|
* path_prefix: '/' (optional, defaults to empty string, or relative path)
|
||||||
|
* query: object of parameters to use for indexing (eg search queries)
|
||||||
|
* after_action: function callback run after the request (where action is fetch, index, etc)
|
||||||
|
* after_action_failure: callback for failed requests (where action is fetch, index, etc)
|
||||||
|
*
|
||||||
|
* Call the ajax functions like this:
|
||||||
|
* in js:
|
||||||
|
* appl.ajax.index(appl.resource_object)
|
||||||
|
* appl.ajax.create(appl.donations, {amount: 420})
|
||||||
|
* in viewscript in the dom:
|
||||||
|
* ajax.index resource_object
|
||||||
|
* ajax.create donations form_object
|
||||||
|
*/
|
||||||
|
|
||||||
|
var request = require('../common/client')
|
||||||
|
|
||||||
|
var restful_resource = {}
|
||||||
|
module.exports = restful_resource
|
||||||
|
|
||||||
|
|
||||||
|
appl.def('ajax', {
|
||||||
|
index: function(prop, node) {
|
||||||
|
var resource = appl.vs(prop) || {}
|
||||||
|
var name = resource.resource_name || prop
|
||||||
|
var path = resource.path_prefix || ''
|
||||||
|
before_request(prop)
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
request.get(path + name).query(resource.query)
|
||||||
|
.end(function(err, resp) {
|
||||||
|
var tmp = resource.data
|
||||||
|
after_request(prop, err, resp)
|
||||||
|
if(resp.ok) {
|
||||||
|
if(resource.query && resource.query.page > 1 && resource.concat_data) {
|
||||||
|
appl.def(prop + '.data', tmp.concat(resp.body.data))
|
||||||
|
}
|
||||||
|
resolve(resp)
|
||||||
|
} else {
|
||||||
|
reject(resp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
fetch: function(prop, id, node) {
|
||||||
|
var resource = appl.vs(prop) || {}
|
||||||
|
var name = resource.resource_name || prop
|
||||||
|
var path = resource.path_prefix || ''
|
||||||
|
before_request(prop)
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
request.get(path + name + '/' + id).query(resource.query)
|
||||||
|
.end(function(err, resp) {
|
||||||
|
after_request(prop, err, resp)
|
||||||
|
if(resp.ok) resolve(resp)
|
||||||
|
else reject(resp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
create: function(prop, form_obj, node) {
|
||||||
|
var resource = appl.vs(prop) || {}
|
||||||
|
var name = resource.resource_name || prop
|
||||||
|
var path = resource.path_prefix || ''
|
||||||
|
before_request(prop)
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
request.post(path + name).send(nested_obj(name, form_obj))
|
||||||
|
.end(function(err, resp) {
|
||||||
|
after_request(prop, err, resp)
|
||||||
|
if(resp.ok) resolve(resp)
|
||||||
|
else reject(resp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(prop, id, form_obj, node) {
|
||||||
|
var resource = appl.vs(prop) || {}
|
||||||
|
var name = resource.resource_name || prop
|
||||||
|
var path = resource.path_prefix || ''
|
||||||
|
before_request(prop)
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
request.put(path + name + '/' + id).send(nested_obj(name, form_obj))
|
||||||
|
.end(function(err, resp) {
|
||||||
|
after_request(prop, err, resp)
|
||||||
|
if(resp.ok) resolve(resp)
|
||||||
|
else reject(resp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
del: function(prop, id, node) {
|
||||||
|
var resource = appl.vs(prop) || {}
|
||||||
|
var path = (resource.path_prefix || '') + (resource.resource_name || prop)
|
||||||
|
before_request(prop)
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
request.del(path + '/' + id)
|
||||||
|
.end(function(err, resp) {
|
||||||
|
after_request(prop, err, resp)
|
||||||
|
if(resp.ok) resolve(resp)
|
||||||
|
else reject(resp)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Given a viewscript property, set some state before every request.
|
||||||
|
// Eg. appl.ajax.index('donations') will cause appl.donations.loading to be
|
||||||
|
// true before the request finishes
|
||||||
|
function before_request(prop) {
|
||||||
|
appl.def(prop + '.loading', true)
|
||||||
|
appl.def(prop + '.error', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Set some data after each request.
|
||||||
|
function after_request(prop, err, resp) {
|
||||||
|
appl.def(prop + '.loading', false)
|
||||||
|
if(resp.ok) {
|
||||||
|
appl.def(prop, resp.body)
|
||||||
|
} else {
|
||||||
|
appl.def(prop + '.error', resp.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Simply return an object nested under 'name'
|
||||||
|
// Will singularize the given name if plural
|
||||||
|
// eg: given 'donations' and {amount: 111}, return {donation: {amount: 111}}
|
||||||
|
function nested_obj(name, child_obj) {
|
||||||
|
var parent_obj = {}
|
||||||
|
parent_obj[appl.to_singular(name)] = child_obj
|
||||||
|
return parent_obj
|
||||||
|
}
|
||||||
|
|
5
app/javascript/legacy/common/sanitize-slug.js
Normal file
5
app/javascript/legacy/common/sanitize-slug.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = str =>
|
||||||
|
str.trim().toLowerCase()
|
||||||
|
.replace(/\s*[^A-Za-z0-9\-]\s*/g, '-') // Replace any oddballs with a hyphen
|
||||||
|
.replace(/-+$/g,'').replace(/^-+/, '').replace(/-+/, '-') // Remove starting/trailing and repeated hyphens
|
32
app/javascript/legacy/common/scroll_toggle_class.js
Normal file
32
app/javascript/legacy/common/scroll_toggle_class.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
module.exports = function(el, className, parentClass) {
|
||||||
|
var $el = $(el)
|
||||||
|
var elPxFromTop = $el.offset().top
|
||||||
|
var $parent = $el.parents(parentClass).length ? $el.parents(parentClass) : $el.parent()
|
||||||
|
|
||||||
|
var parentHeightPlusTop = $parent.height() + $parent.offset().top - $el.height()
|
||||||
|
var $elToToggle
|
||||||
|
|
||||||
|
if (parentClass === undefined) {
|
||||||
|
parentClass = ''
|
||||||
|
$elToToggle = $el
|
||||||
|
} else {
|
||||||
|
$elToToggle = $parent
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// the parentClass param is optional but if it is passed
|
||||||
|
// then the className is applied to it instead of the el
|
||||||
|
|
||||||
|
$(window).scroll(function() {
|
||||||
|
var scrollPosition = $(window).scrollTop()
|
||||||
|
|
||||||
|
if(scrollPosition >= elPxFromTop)
|
||||||
|
$elToToggle.addClass(className)
|
||||||
|
else
|
||||||
|
$elToToggle.removeClass(className)
|
||||||
|
|
||||||
|
if(parentClass && scrollPosition >= parentHeightPlusTop)
|
||||||
|
$elToToggle.removeClass(className)
|
||||||
|
})
|
||||||
|
}
|
52
app/javascript/legacy/common/search-data.js
Normal file
52
app/javascript/legacy/common/search-data.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const h = require('flimflam/h')
|
||||||
|
const flyd = require('flimflam/flyd')
|
||||||
|
const getValidData = require('../common/get-valid-data')
|
||||||
|
|
||||||
|
const getCurry = path => query => getValidData(path, query)
|
||||||
|
|
||||||
|
module.exports = (path, pageLength) => {
|
||||||
|
const get = getCurry(path)
|
||||||
|
const searchLessQuery$ = flyd.stream()
|
||||||
|
|
||||||
|
const submitSearch$ = flyd.stream()
|
||||||
|
const searchQuery$ = flyd.map(searchQuery(pageLength), submitSearch$)
|
||||||
|
|
||||||
|
const searchLessResults$ = flyd.flatMap(q => get(q), searchLessQuery$)
|
||||||
|
const searchResults$ = flyd.flatMap(q => get(q), searchQuery$)
|
||||||
|
|
||||||
|
const allResults$ = flyd.merge(searchLessResults$, searchResults$)
|
||||||
|
|
||||||
|
const hasMoreResults$ = flyd.map(x => x && x.length >= pageLength, allResults$)
|
||||||
|
|
||||||
|
const data$ = flyd.scanMerge([
|
||||||
|
[searchLessResults$, (data, results) => R.concat(data, results)]
|
||||||
|
, [searchResults$, (data, results) => results]
|
||||||
|
], [])
|
||||||
|
|
||||||
|
searchLessQuery$({page: 1, page_length: pageLength, search: ''})
|
||||||
|
|
||||||
|
const loading$ = flyd.mergeAll([
|
||||||
|
flyd.map(R.always(true), submitSearch$)
|
||||||
|
, flyd.map(R.always(true), searchLessQuery$)
|
||||||
|
, flyd.map(R.always(false), allResults$)
|
||||||
|
, flyd.stream(true)
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
data$
|
||||||
|
, searchLessQuery$
|
||||||
|
, loading$
|
||||||
|
, pageLength
|
||||||
|
, hasMoreResults$
|
||||||
|
, submitSearch$
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchQuery = pageLength => ev => {
|
||||||
|
ev.preventDefault()
|
||||||
|
const search = ev.target.querySelector('input').value
|
||||||
|
return {page: 1, search, page_length: pageLength}
|
||||||
|
}
|
||||||
|
|
33
app/javascript/legacy/common/super-agent-frp.js
Normal file
33
app/javascript/legacy/common/super-agent-frp.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// super-agent with default json and csrf wrappers
|
||||||
|
// Also has a FRP api (using flyd) rather than the default '.end'
|
||||||
|
// Every call to .perform() returns a flyd stream
|
||||||
|
|
||||||
|
var request = require('superagent')
|
||||||
|
var flyd = require("flyd")
|
||||||
|
|
||||||
|
var wrapper = {
|
||||||
|
post: function() {
|
||||||
|
return injectFlyd(request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
|
||||||
|
}
|
||||||
|
, put: function() {
|
||||||
|
return injectFlyd(request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
|
||||||
|
}
|
||||||
|
, del: function() {
|
||||||
|
return injectFlyd(request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
|
||||||
|
}
|
||||||
|
, get: function() {
|
||||||
|
return injectFlyd(request.get.apply(this, arguments).accept('json'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function injectFlyd(req) {
|
||||||
|
req.perform = function() {
|
||||||
|
var $stream = flyd.stream()
|
||||||
|
req.end(function(err, resp) { $stream(resp) })
|
||||||
|
return $stream
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = wrapper
|
41
app/javascript/legacy/common/super-agent-promise.js
Normal file
41
app/javascript/legacy/common/super-agent-promise.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// super-agent with default json and csrf wrappers
|
||||||
|
// Also has a Promise api ('.then' and '.catch') rather than the default '.end'
|
||||||
|
|
||||||
|
var request = require('superagent')
|
||||||
|
|
||||||
|
var wrapper = {}
|
||||||
|
module.exports = wrapper
|
||||||
|
|
||||||
|
wrapper.post = function() {
|
||||||
|
var req = request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
return convert_to_promise(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.put = function() {
|
||||||
|
var req = request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
return convert_to_promise(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.del = function() {
|
||||||
|
var req = request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
|
||||||
|
return convert_to_promise(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.get = function(path) {
|
||||||
|
var req = request.get.call(this, path).accept('json')
|
||||||
|
return convert_to_promise(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
function convert_to_promise(req) {
|
||||||
|
req.perform = function() {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
req.end(function(err, resp) {
|
||||||
|
if(resp && resp.ok) { resolve(resp) }
|
||||||
|
else { reject(resp) }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
39
app/javascript/legacy/common/time-remaining.js
Normal file
39
app/javascript/legacy/common/time-remaining.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const flyd_every = require('flyd/module/every')
|
||||||
|
const moment = require('moment-timezone')
|
||||||
|
const format = require('../common/format')
|
||||||
|
const pluralize = format.pluralize
|
||||||
|
|
||||||
|
// Given an end dateTime ("2015-11-17 19:00") and a time-zone ("America/Los_Angeles"),
|
||||||
|
// if the end dateTime has passed, return false
|
||||||
|
// if the end dateTime is more than a day away
|
||||||
|
// then return the number of days away
|
||||||
|
// if the end dateTime is less than a day away
|
||||||
|
// then return a countdown stream with seconds precision
|
||||||
|
//
|
||||||
|
// This function returns a stream.
|
||||||
|
//
|
||||||
|
// This function takes a timezone in the format "Country/City"
|
||||||
|
// See here: http://momentjs.com/timezone/
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
const timeRemaining = (endDateTime, tz) => {
|
||||||
|
if(!endDateTime) return flyd.stream(false)
|
||||||
|
const format = "YYYY-MM-DD hh:mm:ss zz"
|
||||||
|
tz = tz || ENV.nonprofitTimezone || 'America/Los_Angeles'
|
||||||
|
const [now, end] = [moment().tz(tz), moment(endDateTime, format).tz(tz).seconds(59)]
|
||||||
|
console.log({now, end})
|
||||||
|
if(end.isBefore(now)) return flyd.stream(false)
|
||||||
|
|
||||||
|
if(end.diff(now, 'hours') <= 24) {
|
||||||
|
return flyd.map(
|
||||||
|
t => moment.utc(end.diff(moment(t))).format("HH:mm:ss")
|
||||||
|
, flyd_every(1000))
|
||||||
|
} else {
|
||||||
|
return flyd.stream(pluralize(end.diff(now, 'days'), 'days'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = timeRemaining
|
180
app/javascript/legacy/common/utilities.js
Executable file
180
app/javascript/legacy/common/utilities.js
Executable file
|
@ -0,0 +1,180 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// Utilities!
|
||||||
|
// XXX remove this whole file and split into modules with specific concerns
|
||||||
|
const phoneFormatter = require('phone-formatter');
|
||||||
|
const R = require('ramda')
|
||||||
|
|
||||||
|
var utils = {}
|
||||||
|
|
||||||
|
module.exports = utils
|
||||||
|
|
||||||
|
// XXX remove
|
||||||
|
utils.capitalize = string =>
|
||||||
|
string.charAt(0).toUpperCase() + string.slice(1)
|
||||||
|
|
||||||
|
// Print a single message for Rails error responses
|
||||||
|
// XXX remove
|
||||||
|
utils.print_error = function (response) {
|
||||||
|
var msg = 'Sorry! We encountered an error.'
|
||||||
|
if(!response) return msg
|
||||||
|
if(response.status === 500) return msg
|
||||||
|
else if(response.status === 404) return "404 - Not found"
|
||||||
|
else if(response.status === 422 || response.status === 401) {
|
||||||
|
if(!response.responseJSON) return msg
|
||||||
|
|
||||||
|
var json = response.responseJSON
|
||||||
|
if(json.length) return json[0]
|
||||||
|
|
||||||
|
else if(json.errors)
|
||||||
|
for (var key in json.errors)
|
||||||
|
return key + ' ' + json.errors[key][0]
|
||||||
|
|
||||||
|
else if(json.error) return json.error
|
||||||
|
|
||||||
|
else return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a URL parameter
|
||||||
|
// XXX remove
|
||||||
|
utils.get_param = function(name) {
|
||||||
|
var param = decodeURI((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search) || [null])[1])
|
||||||
|
return (param == 'undefined') ? undefined : param
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX remove
|
||||||
|
utils.change_url_param = function(key, value) {
|
||||||
|
if (!history || !history.replaceState) return
|
||||||
|
history.replaceState({}, "", utils.update_param(key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX remove. Depended on only by 'change_url_param' above
|
||||||
|
utils.update_param = function(key, value, url) {
|
||||||
|
if(!url) url = window.location.href
|
||||||
|
var re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi")
|
||||||
|
|
||||||
|
if(re.test(url)) {
|
||||||
|
if(typeof value !== 'undefined' && value !== null)
|
||||||
|
return url.replace(re, '$1' + key + "=" + value + '$2$3')
|
||||||
|
else {
|
||||||
|
var hash = url.split('#')
|
||||||
|
url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '')
|
||||||
|
if(typeof hash[1] !== 'undefined' && hash[1] !== null)
|
||||||
|
url += '#' + hash[1]
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof value !== 'undefined' && value !== null) {
|
||||||
|
var separator = url.indexOf('?') !== -1 ? '&' : '?',
|
||||||
|
hash = url.split('#')
|
||||||
|
url = hash[0] + separator + key + '=' + value
|
||||||
|
if(typeof hash[1] !== 'undefined' && hash[1] !== null)
|
||||||
|
url += '#' + hash[1]
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
else return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad a number with leading zeros
|
||||||
|
// XXX remove
|
||||||
|
utils.zero_pad = function(num, size) {
|
||||||
|
var str = num + ""
|
||||||
|
while (str.length < size) str = "0" + str
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// for doing an action after the user pauses for a second after an event
|
||||||
|
// XXX remove
|
||||||
|
utils.delay = (function() {
|
||||||
|
var timer = 0
|
||||||
|
return function(ms, callback) {
|
||||||
|
clearTimeout(timer)
|
||||||
|
timer = setTimeout(callback, ms)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
utils.number_with_commas = function(n) {
|
||||||
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge x's properties with y (mutating)
|
||||||
|
utils.merge = function(x, y) {
|
||||||
|
for (var key in y) { x[key] = y[key]; }
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = require('./format')
|
||||||
|
utils.dollars_to_cents = format.dollarsToCents
|
||||||
|
utils.cents_to_dollars = format.centsToDollars
|
||||||
|
|
||||||
|
// Create a single FormData object from any number of inputs and forms (not bound to a single form)
|
||||||
|
// Kind of a re-implementation of: http://www.w3.org/html/wg/drafts/html/master/forms.html#constructing-the-form-data-set
|
||||||
|
// XXX remove
|
||||||
|
utils.toFormData = function(form_el) {
|
||||||
|
var form_data = new FormData()
|
||||||
|
$(form_el).find('input, select, textarea').each(function(index) {
|
||||||
|
if(!this.name) return
|
||||||
|
if(this.files && this.files[0])
|
||||||
|
form_data.append(this.name, this.files[0])
|
||||||
|
else if(this.getAttribute("type") === "checkbox")
|
||||||
|
form_data.append(this.name, this.checked)
|
||||||
|
else if(this.value)
|
||||||
|
form_data.append(this.name, this.value)
|
||||||
|
})
|
||||||
|
return form_data
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.mergeFormData = function(formData, obj) {
|
||||||
|
for(var key in obj) formData.append(key, obj[key])
|
||||||
|
return formData
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Given an array of values, return an array only with unique values
|
||||||
|
// XXX remove
|
||||||
|
utils.uniq = function(arr) {
|
||||||
|
var obj = {}
|
||||||
|
var index
|
||||||
|
var len = arr.length
|
||||||
|
var result = [];
|
||||||
|
for(index = 0; index < len; index += 1) obj[arr[index]] = arr[index];
|
||||||
|
for(index in obj) result.push(obj[index]);
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX remove
|
||||||
|
utils.address_with_commas = function(street, city, state){
|
||||||
|
var address = [street, city, state]
|
||||||
|
var pretty_print_add = []
|
||||||
|
for(var i = 0; i < address.length; i += 1) {
|
||||||
|
if (address[i] !== '' && address[i] != null) pretty_print_add.push(address[i])
|
||||||
|
}
|
||||||
|
return pretty_print_add.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.pretty_phone = function(phone){
|
||||||
|
if(!phone) {return false}
|
||||||
|
|
||||||
|
// first remove any non-digit characters globally
|
||||||
|
// and get length of phone number
|
||||||
|
var clean = String(phone).replace(/\D/g, '')
|
||||||
|
var len = clean.length
|
||||||
|
|
||||||
|
var format = "(NNN) NNN-NNNN"
|
||||||
|
|
||||||
|
// then format based on length
|
||||||
|
if(len === 10) {
|
||||||
|
return phoneFormatter.format(clean, format)
|
||||||
|
}
|
||||||
|
if(len > 10) {
|
||||||
|
var first = clean.substring(0, len - 10)
|
||||||
|
var last10 = clean.substring(len - 10)
|
||||||
|
return `+${first} ${phoneFormatter.format(last10, format)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// if number is less than 10, don't apply any formatting
|
||||||
|
// and just return it
|
||||||
|
return clean
|
||||||
|
}
|
||||||
|
|
3476
app/javascript/legacy/common/vendor/Chart.min.js
vendored
Executable file
3476
app/javascript/legacy/common/vendor/Chart.min.js
vendored
Executable file
File diff suppressed because it is too large
Load diff
1288
app/javascript/legacy/common/vendor/bootstrap-tour-standalone.js
vendored
Normal file
1288
app/javascript/legacy/common/vendor/bootstrap-tour-standalone.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
334
app/javascript/legacy/common/vendor/bootstrap.js
vendored
Normal file
334
app/javascript/legacy/common/vendor/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
/*!
|
||||||
|
* Bootstrap v3.3.2 (http://getbootstrap.com)
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=0d2d6ea77a31113c4876)
|
||||||
|
* Config saved to config.json and https://gist.github.com/0d2d6ea77a31113c4876
|
||||||
|
*/
|
||||||
|
if (typeof jQuery === 'undefined') {
|
||||||
|
throw new Error('Bootstrap\'s JavaScript requires jQuery')
|
||||||
|
}
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
var version = $.fn.jquery.split(' ')[0].split('.')
|
||||||
|
if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) {
|
||||||
|
throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher')
|
||||||
|
}
|
||||||
|
}(jQuery);
|
||||||
|
|
||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: carousel.js v3.3.2
|
||||||
|
* http://getbootstrap.com/javascript/#carousel
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// CAROUSEL CLASS DEFINITION
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
var Carousel = function (element, options) {
|
||||||
|
this.$element = $(element)
|
||||||
|
this.$indicators = this.$element.find('.carousel-indicators')
|
||||||
|
this.options = options
|
||||||
|
this.paused =
|
||||||
|
this.sliding =
|
||||||
|
this.interval =
|
||||||
|
this.$active =
|
||||||
|
this.$items = null
|
||||||
|
|
||||||
|
this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this))
|
||||||
|
|
||||||
|
this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element
|
||||||
|
.on('mouseenter.bs.carousel', $.proxy(this.pause, this))
|
||||||
|
.on('mouseleave.bs.carousel', $.proxy(this.cycle, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.VERSION = '3.3.2'
|
||||||
|
|
||||||
|
Carousel.TRANSITION_DURATION = 600
|
||||||
|
|
||||||
|
Carousel.DEFAULTS = {
|
||||||
|
interval: 5000,
|
||||||
|
pause: 'hover',
|
||||||
|
wrap: true,
|
||||||
|
keyboard: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.keydown = function (e) {
|
||||||
|
if (/input|textarea/i.test(e.target.tagName)) return
|
||||||
|
switch (e.which) {
|
||||||
|
case 37: this.prev(); break
|
||||||
|
case 39: this.next(); break
|
||||||
|
default: return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.cycle = function (e) {
|
||||||
|
e || (this.paused = false)
|
||||||
|
|
||||||
|
this.interval && clearInterval(this.interval)
|
||||||
|
|
||||||
|
this.options.interval
|
||||||
|
&& !this.paused
|
||||||
|
&& (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.getItemIndex = function (item) {
|
||||||
|
this.$items = item.parent().children('.item')
|
||||||
|
return this.$items.index(item || this.$active)
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.getItemForDirection = function (direction, active) {
|
||||||
|
var activeIndex = this.getItemIndex(active)
|
||||||
|
var willWrap = (direction == 'prev' && activeIndex === 0)
|
||||||
|
|| (direction == 'next' && activeIndex == (this.$items.length - 1))
|
||||||
|
if (willWrap && !this.options.wrap) return active
|
||||||
|
var delta = direction == 'prev' ? -1 : 1
|
||||||
|
var itemIndex = (activeIndex + delta) % this.$items.length
|
||||||
|
return this.$items.eq(itemIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.to = function (pos) {
|
||||||
|
var that = this
|
||||||
|
var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active'))
|
||||||
|
|
||||||
|
if (pos > (this.$items.length - 1) || pos < 0) return
|
||||||
|
|
||||||
|
if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid"
|
||||||
|
if (activeIndex == pos) return this.pause().cycle()
|
||||||
|
|
||||||
|
return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.pause = function (e) {
|
||||||
|
e || (this.paused = true)
|
||||||
|
|
||||||
|
if (this.$element.find('.next, .prev').length && $.support.transition) {
|
||||||
|
this.$element.trigger($.support.transition.end)
|
||||||
|
this.cycle(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.interval = clearInterval(this.interval)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.next = function () {
|
||||||
|
if (this.sliding) return
|
||||||
|
return this.slide('next')
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.prev = function () {
|
||||||
|
if (this.sliding) return
|
||||||
|
return this.slide('prev')
|
||||||
|
}
|
||||||
|
|
||||||
|
Carousel.prototype.slide = function (type, next) {
|
||||||
|
var $active = this.$element.find('.item.active')
|
||||||
|
var $next = next || this.getItemForDirection(type, $active)
|
||||||
|
var isCycling = this.interval
|
||||||
|
var direction = type == 'next' ? 'left' : 'right'
|
||||||
|
var that = this
|
||||||
|
|
||||||
|
if ($next.hasClass('active')) return (this.sliding = false)
|
||||||
|
|
||||||
|
var relatedTarget = $next[0]
|
||||||
|
var slideEvent = $.Event('slide.bs.carousel', {
|
||||||
|
relatedTarget: relatedTarget,
|
||||||
|
direction: direction
|
||||||
|
})
|
||||||
|
this.$element.trigger(slideEvent)
|
||||||
|
if (slideEvent.isDefaultPrevented()) return
|
||||||
|
|
||||||
|
this.sliding = true
|
||||||
|
|
||||||
|
isCycling && this.pause()
|
||||||
|
|
||||||
|
if (this.$indicators.length) {
|
||||||
|
this.$indicators.find('.active').removeClass('active')
|
||||||
|
var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)])
|
||||||
|
$nextIndicator && $nextIndicator.addClass('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
|
||||||
|
if ($.support.transition && this.$element.hasClass('slide')) {
|
||||||
|
$next.addClass(type)
|
||||||
|
$next[0].offsetWidth // force reflow
|
||||||
|
$active.addClass(direction)
|
||||||
|
$next.addClass(direction)
|
||||||
|
$active
|
||||||
|
.one('bsTransitionEnd', function () {
|
||||||
|
$next.removeClass([type, direction].join(' ')).addClass('active')
|
||||||
|
$active.removeClass(['active', direction].join(' '))
|
||||||
|
that.sliding = false
|
||||||
|
setTimeout(function () {
|
||||||
|
that.$element.trigger(slidEvent)
|
||||||
|
}, 0)
|
||||||
|
})
|
||||||
|
.emulateTransitionEnd(Carousel.TRANSITION_DURATION)
|
||||||
|
} else {
|
||||||
|
$active.removeClass('active')
|
||||||
|
$next.addClass('active')
|
||||||
|
this.sliding = false
|
||||||
|
this.$element.trigger(slidEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
isCycling && this.cycle()
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL PLUGIN DEFINITION
|
||||||
|
// ==========================
|
||||||
|
|
||||||
|
function Plugin(option) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
var data = $this.data('bs.carousel')
|
||||||
|
var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
||||||
|
var action = typeof option == 'string' ? option : options.slide
|
||||||
|
|
||||||
|
if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
|
||||||
|
if (typeof option == 'number') data.to(option)
|
||||||
|
else if (action) data[action]()
|
||||||
|
else if (options.interval) data.pause().cycle()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var old = $.fn.carousel
|
||||||
|
|
||||||
|
$.fn.carousel = Plugin
|
||||||
|
$.fn.carousel.Constructor = Carousel
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL NO CONFLICT
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
$.fn.carousel.noConflict = function () {
|
||||||
|
$.fn.carousel = old
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CAROUSEL DATA-API
|
||||||
|
// =================
|
||||||
|
|
||||||
|
var clickHandler = function (e) {
|
||||||
|
var href
|
||||||
|
var $this = $(this)
|
||||||
|
var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7
|
||||||
|
if (!$target.hasClass('carousel')) return
|
||||||
|
var options = $.extend({}, $target.data(), $this.data())
|
||||||
|
var slideIndex = $this.attr('data-slide-to')
|
||||||
|
if (slideIndex) options.interval = false
|
||||||
|
|
||||||
|
Plugin.call($target, options)
|
||||||
|
|
||||||
|
if (slideIndex) {
|
||||||
|
$target.data('bs.carousel').to(slideIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.on('click.bs.carousel.data-api', '[data-slide]', clickHandler)
|
||||||
|
.on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler)
|
||||||
|
|
||||||
|
$(window).on('load', function () {
|
||||||
|
$('[data-ride="carousel"]').each(function () {
|
||||||
|
var $carousel = $(this)
|
||||||
|
Plugin.call($carousel, $carousel.data())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
|
|
||||||
|
|
||||||
|
/* ========================================================================
|
||||||
|
* Bootstrap: transition.js v3.3.2
|
||||||
|
* http://getbootstrap.com/javascript/#transitions
|
||||||
|
* ========================================================================
|
||||||
|
* Copyright 2011-2015 Twitter, Inc.
|
||||||
|
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||||
|
* ======================================================================== */
|
||||||
|
|
||||||
|
|
||||||
|
+function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function transitionEnd() {
|
||||||
|
var el = document.createElement('bootstrap')
|
||||||
|
|
||||||
|
var transEndEventNames = {
|
||||||
|
WebkitTransition : 'webkitTransitionEnd',
|
||||||
|
MozTransition : 'transitionend',
|
||||||
|
OTransition : 'oTransitionEnd otransitionend',
|
||||||
|
transition : 'transitionend'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var name in transEndEventNames) {
|
||||||
|
if (el.style[name] !== undefined) {
|
||||||
|
return { end: transEndEventNames[name] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false // explicit for ie8 ( ._.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://blog.alexmaccaw.com/css-transitions
|
||||||
|
$.fn.emulateTransitionEnd = function (duration) {
|
||||||
|
var called = false
|
||||||
|
var $el = this
|
||||||
|
$(this).one('bsTransitionEnd', function () { called = true })
|
||||||
|
var callback = function () { if (!called) $($el).trigger($.support.transition.end) }
|
||||||
|
setTimeout(callback, duration)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
$.support.transition = transitionEnd()
|
||||||
|
|
||||||
|
if (!$.support.transition) return
|
||||||
|
|
||||||
|
$.event.special.bsTransitionEnd = {
|
||||||
|
bindType: $.support.transition.end,
|
||||||
|
delegateType: $.support.transition.end,
|
||||||
|
handle: function (e) {
|
||||||
|
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}(jQuery);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
setTimeout(function(){
|
||||||
|
var $progressBar = $('#progress')
|
||||||
|
var width = $progressBar.attr('data-perc')
|
||||||
|
$progressBar.css('width', width + '%')
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
517
app/javascript/legacy/common/vendor/colpick.js
vendored
Normal file
517
app/javascript/legacy/common/vendor/colpick.js
vendored
Normal file
|
@ -0,0 +1,517 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
/*
|
||||||
|
colpick Color Picker
|
||||||
|
Copyright 2013 Jose Vargas. Licensed under GPL license. Based on Stefan Petre's Color Picker www.eyecon.ro, dual licensed under the MIT and GPL licenses
|
||||||
|
|
||||||
|
For usage and examples: colpick.com/plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
var colpick = function () {
|
||||||
|
var
|
||||||
|
tpl = '<div class="colpick"><div class="colpick_color"><div class="colpick_color_overlay1"><div class="colpick_color_overlay2"><div class="colpick_selector_outer"><div class="colpick_selector_inner"></div></div></div></div></div><div class="colpick_hue"><div class="colpick_hue_arrs"><div class="colpick_hue_larr"></div><div class="colpick_hue_rarr"></div></div></div><div class="colpick_new_color"></div><div class="colpick_current_color"></div><div class="colpick_hex_field"><div class="colpick_field_letter">#</div><input type="text" maxlength="6" size="6" /></div><div class="colpick_rgb_r colpick_field"><div class="colpick_field_letter">R</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_rgb_g colpick_field"><div class="colpick_field_letter">G</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_rgb_b colpick_field"><div class="colpick_field_letter">B</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_h colpick_field"><div class="colpick_field_letter">H</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_s colpick_field"><div class="colpick_field_letter">S</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_hsb_b colpick_field"><div class="colpick_field_letter">B</div><input type="text" maxlength="3" size="3" /><div class="colpick_field_arrs"><div class="colpick_field_uarr"></div><div class="colpick_field_darr"></div></div></div><div class="colpick_submit"></div></div>',
|
||||||
|
defaults = {
|
||||||
|
showEvent: 'click',
|
||||||
|
onShow: function () {},
|
||||||
|
onBeforeShow: function(){},
|
||||||
|
onHide: function () {},
|
||||||
|
onChange: function () {},
|
||||||
|
onSubmit: function () {},
|
||||||
|
colorScheme: 'light',
|
||||||
|
color: '3289c7',
|
||||||
|
livePreview: true,
|
||||||
|
flat: false,
|
||||||
|
layout: 'full',
|
||||||
|
submit: 1,
|
||||||
|
submitText: 'OK',
|
||||||
|
height: 156
|
||||||
|
},
|
||||||
|
//Fill the inputs of the plugin
|
||||||
|
fillRGBFields = function (hsb, cal) {
|
||||||
|
var rgb = hsbToRgb(hsb);
|
||||||
|
$(cal).data('colpick').fields
|
||||||
|
.eq(1).val(rgb.r).end()
|
||||||
|
.eq(2).val(rgb.g).end()
|
||||||
|
.eq(3).val(rgb.b).end();
|
||||||
|
},
|
||||||
|
fillHSBFields = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').fields
|
||||||
|
.eq(4).val(Math.round(hsb.h)).end()
|
||||||
|
.eq(5).val(Math.round(hsb.s)).end()
|
||||||
|
.eq(6).val(Math.round(hsb.b)).end();
|
||||||
|
},
|
||||||
|
fillHexFields = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').fields.eq(0).val(hsbToHex(hsb));
|
||||||
|
},
|
||||||
|
//Set the round selector position
|
||||||
|
setSelector = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').selector.css('backgroundColor', '#' + hsbToHex({h: hsb.h, s: 100, b: 100}));
|
||||||
|
$(cal).data('colpick').selectorIndic.css({
|
||||||
|
left: parseInt($(cal).data('colpick').height * hsb.s/100, 10),
|
||||||
|
top: parseInt($(cal).data('colpick').height * (100-hsb.b)/100, 10)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
//Set the hue selector position
|
||||||
|
setHue = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').hue.css('top', parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h/360, 10));
|
||||||
|
},
|
||||||
|
//Set current and new colors
|
||||||
|
setCurrentColor = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').currentColor.css('backgroundColor', '#' + hsbToHex(hsb));
|
||||||
|
},
|
||||||
|
setNewColor = function (hsb, cal) {
|
||||||
|
$(cal).data('colpick').newColor.css('backgroundColor', '#' + hsbToHex(hsb));
|
||||||
|
},
|
||||||
|
//Called when the new color is changed
|
||||||
|
change = function (ev) {
|
||||||
|
var cal = $(this).parent().parent(), col;
|
||||||
|
if (this.parentNode.className.indexOf('_hex') > 0) {
|
||||||
|
cal.data('colpick').color = col = hexToHsb(fixHex(this.value));
|
||||||
|
fillRGBFields(col, cal.get(0));
|
||||||
|
fillHSBFields(col, cal.get(0));
|
||||||
|
} else if (this.parentNode.className.indexOf('_hsb') > 0) {
|
||||||
|
cal.data('colpick').color = col = fixHSB({
|
||||||
|
h: parseInt(cal.data('colpick').fields.eq(4).val(), 10),
|
||||||
|
s: parseInt(cal.data('colpick').fields.eq(5).val(), 10),
|
||||||
|
b: parseInt(cal.data('colpick').fields.eq(6).val(), 10)
|
||||||
|
});
|
||||||
|
fillRGBFields(col, cal.get(0));
|
||||||
|
fillHexFields(col, cal.get(0));
|
||||||
|
} else {
|
||||||
|
cal.data('colpick').color = col = rgbToHsb(fixRGB({
|
||||||
|
r: parseInt(cal.data('colpick').fields.eq(1).val(), 10),
|
||||||
|
g: parseInt(cal.data('colpick').fields.eq(2).val(), 10),
|
||||||
|
b: parseInt(cal.data('colpick').fields.eq(3).val(), 10)
|
||||||
|
}));
|
||||||
|
fillHexFields(col, cal.get(0));
|
||||||
|
fillHSBFields(col, cal.get(0));
|
||||||
|
}
|
||||||
|
setSelector(col, cal.get(0));
|
||||||
|
setHue(col, cal.get(0));
|
||||||
|
setNewColor(col, cal.get(0));
|
||||||
|
cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]);
|
||||||
|
},
|
||||||
|
//Change style on blur and on focus of inputs
|
||||||
|
blur = function (ev) {
|
||||||
|
$(this).parent().removeClass('colpick_focus');
|
||||||
|
},
|
||||||
|
focus = function () {
|
||||||
|
$(this).parent().parent().data('colpick').fields.parent().removeClass('colpick_focus');
|
||||||
|
$(this).parent().addClass('colpick_focus');
|
||||||
|
},
|
||||||
|
//Increment/decrement arrows functions
|
||||||
|
downIncrement = function (ev) {
|
||||||
|
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||||
|
var field = $(this).parent().find('input').focus();
|
||||||
|
var current = {
|
||||||
|
el: $(this).parent().addClass('colpick_slider'),
|
||||||
|
max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255),
|
||||||
|
y: ev.pageY,
|
||||||
|
field: field,
|
||||||
|
val: parseInt(field.val(), 10),
|
||||||
|
preview: $(this).parent().parent().data('colpick').livePreview
|
||||||
|
};
|
||||||
|
$(document).mouseup(current, upIncrement);
|
||||||
|
$(document).mousemove(current, moveIncrement);
|
||||||
|
},
|
||||||
|
moveIncrement = function (ev) {
|
||||||
|
ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val - ev.pageY + ev.data.y, 10))));
|
||||||
|
if (ev.data.preview) {
|
||||||
|
change.apply(ev.data.field.get(0), [true]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
upIncrement = function (ev) {
|
||||||
|
change.apply(ev.data.field.get(0), [true]);
|
||||||
|
ev.data.el.removeClass('colpick_slider').find('input').focus();
|
||||||
|
$(document).off('mouseup', upIncrement);
|
||||||
|
$(document).off('mousemove', moveIncrement);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
//Hue slider functions
|
||||||
|
downHue = function (ev) {
|
||||||
|
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||||
|
var current = {
|
||||||
|
cal: $(this).parent(),
|
||||||
|
y: $(this).offset().top
|
||||||
|
};
|
||||||
|
$(document).on('mouseup touchend',current,upHue);
|
||||||
|
$(document).on('mousemove touchmove',current,moveHue);
|
||||||
|
|
||||||
|
var pageY = ((ev.type == 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY );
|
||||||
|
change.apply(
|
||||||
|
current.cal.data('colpick')
|
||||||
|
.fields.eq(4).val(parseInt(360*(current.cal.data('colpick').height - (pageY - current.y))/current.cal.data('colpick').height, 10))
|
||||||
|
.get(0),
|
||||||
|
[current.cal.data('colpick').livePreview]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
moveHue = function (ev) {
|
||||||
|
var pageY = ((ev.type == 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY );
|
||||||
|
change.apply(
|
||||||
|
ev.data.cal.data('colpick')
|
||||||
|
.fields.eq(4).val(parseInt(360*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.y))))/ev.data.cal.data('colpick').height, 10))
|
||||||
|
.get(0),
|
||||||
|
[ev.data.preview]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
upHue = function (ev) {
|
||||||
|
fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
|
||||||
|
fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
|
||||||
|
$(document).off('mouseup touchend',upHue);
|
||||||
|
$(document).off('mousemove touchmove',moveHue);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
//Color selector functions
|
||||||
|
downSelector = function (ev) {
|
||||||
|
ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
|
||||||
|
var current = {
|
||||||
|
cal: $(this).parent(),
|
||||||
|
pos: $(this).offset()
|
||||||
|
};
|
||||||
|
current.preview = current.cal.data('colpick').livePreview;
|
||||||
|
|
||||||
|
$(document).on('mouseup touchend',current,upSelector);
|
||||||
|
$(document).on('mousemove touchmove',current,moveSelector);
|
||||||
|
|
||||||
|
if(ev.type == 'touchstart') {
|
||||||
|
var pageX = ev.originalEvent.changedTouches[0].pageX;
|
||||||
|
var pageY = ev.originalEvent.changedTouches[0].pageY;
|
||||||
|
} else {
|
||||||
|
var pageX = ev.pageX;
|
||||||
|
var pageY = ev.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
change.apply(
|
||||||
|
current.cal.data('colpick').fields
|
||||||
|
.eq(6).val(parseInt(100*(current.cal.data('colpick').height - (pageY - current.pos.top))/current.cal.data('colpick').height, 10)).end()
|
||||||
|
.eq(5).val(parseInt(100*(pageX - current.pos.left)/current.cal.data('colpick').height, 10))
|
||||||
|
.get(0),
|
||||||
|
[current.preview]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
moveSelector = function (ev) {
|
||||||
|
if(ev.type == 'touchmove') {
|
||||||
|
var pageX = ev.originalEvent.changedTouches[0].pageX;
|
||||||
|
var pageY = ev.originalEvent.changedTouches[0].pageY;
|
||||||
|
} else {
|
||||||
|
var pageX = ev.pageX;
|
||||||
|
var pageY = ev.pageY;
|
||||||
|
}
|
||||||
|
|
||||||
|
change.apply(
|
||||||
|
ev.data.cal.data('colpick').fields
|
||||||
|
.eq(6).val(parseInt(100*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.pos.top))))/ev.data.cal.data('colpick').height, 10)).end()
|
||||||
|
.eq(5).val(parseInt(100*(Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageX - ev.data.pos.left))))/ev.data.cal.data('colpick').height, 10))
|
||||||
|
.get(0),
|
||||||
|
[ev.data.preview]
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
upSelector = function (ev) {
|
||||||
|
fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
|
||||||
|
fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0));
|
||||||
|
$(document).off('mouseup touchend',upSelector);
|
||||||
|
$(document).off('mousemove touchmove',moveSelector);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
//Submit button
|
||||||
|
clickSubmit = function (ev) {
|
||||||
|
var cal = $(this).parent();
|
||||||
|
var col = cal.data('colpick').color;
|
||||||
|
cal.data('colpick').origColor = col;
|
||||||
|
setCurrentColor(col, cal.get(0));
|
||||||
|
cal.data('colpick').onSubmit(col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el);
|
||||||
|
},
|
||||||
|
//Show/hide the color picker
|
||||||
|
show = function (ev) {
|
||||||
|
// Prevent the trigger of any direct parent
|
||||||
|
ev.stopPropagation();
|
||||||
|
var cal = $('#' + $(this).data('colpickId'));
|
||||||
|
cal.data('colpick').onBeforeShow.apply(this, [cal.get(0)]);
|
||||||
|
var pos = $(this).offset();
|
||||||
|
var top = pos.top + this.offsetHeight;
|
||||||
|
var left = pos.left;
|
||||||
|
var viewPort = getViewport();
|
||||||
|
var calW = cal.width();
|
||||||
|
if (left + calW > viewPort.l + viewPort.w) {
|
||||||
|
left -= calW;
|
||||||
|
}
|
||||||
|
cal.css({left: left + 'px', top: top + 'px'});
|
||||||
|
if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) != false) {
|
||||||
|
cal.show();
|
||||||
|
}
|
||||||
|
//Hide when user clicks outside
|
||||||
|
$('html').mousedown({cal:cal}, hide);
|
||||||
|
cal.mousedown(function(ev){ev.stopPropagation();})
|
||||||
|
},
|
||||||
|
hide = function (ev) {
|
||||||
|
if (ev.data.cal.data('colpick').onHide.apply(this, [ev.data.cal.get(0)]) != false) {
|
||||||
|
ev.data.cal.hide();
|
||||||
|
}
|
||||||
|
$('html').off('mousedown', hide);
|
||||||
|
},
|
||||||
|
getViewport = function () {
|
||||||
|
var m = document.compatMode == 'CSS1Compat';
|
||||||
|
return {
|
||||||
|
l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft),
|
||||||
|
w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
//Fix the values if the user enters a negative or high value
|
||||||
|
fixHSB = function (hsb) {
|
||||||
|
return {
|
||||||
|
h: Math.min(360, Math.max(0, hsb.h)),
|
||||||
|
s: Math.min(100, Math.max(0, hsb.s)),
|
||||||
|
b: Math.min(100, Math.max(0, hsb.b))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fixRGB = function (rgb) {
|
||||||
|
return {
|
||||||
|
r: Math.min(255, Math.max(0, rgb.r)),
|
||||||
|
g: Math.min(255, Math.max(0, rgb.g)),
|
||||||
|
b: Math.min(255, Math.max(0, rgb.b))
|
||||||
|
};
|
||||||
|
},
|
||||||
|
fixHex = function (hex) {
|
||||||
|
var len = 6 - hex.length;
|
||||||
|
if (len > 0) {
|
||||||
|
var o = [];
|
||||||
|
for (var i=0; i<len; i++) {
|
||||||
|
o.push('0');
|
||||||
|
}
|
||||||
|
o.push(hex);
|
||||||
|
hex = o.join('');
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
},
|
||||||
|
restoreOriginal = function () {
|
||||||
|
var cal = $(this).parent();
|
||||||
|
var col = cal.data('colpick').origColor;
|
||||||
|
cal.data('colpick').color = col;
|
||||||
|
fillRGBFields(col, cal.get(0));
|
||||||
|
fillHexFields(col, cal.get(0));
|
||||||
|
fillHSBFields(col, cal.get(0));
|
||||||
|
setSelector(col, cal.get(0));
|
||||||
|
setHue(col, cal.get(0));
|
||||||
|
setNewColor(col, cal.get(0));
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
init: function (opt) {
|
||||||
|
opt = $.extend({}, defaults, opt||{});
|
||||||
|
//Set color
|
||||||
|
if (typeof opt.color == 'string') {
|
||||||
|
opt.color = hexToHsb(opt.color);
|
||||||
|
} else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) {
|
||||||
|
opt.color = rgbToHsb(opt.color);
|
||||||
|
} else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) {
|
||||||
|
opt.color = fixHSB(opt.color);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
//For each selected DOM element
|
||||||
|
return this.each(function () {
|
||||||
|
//If the element does not have an ID
|
||||||
|
if (!$(this).data('colpickId')) {
|
||||||
|
var options = $.extend({}, opt);
|
||||||
|
options.origColor = opt.color;
|
||||||
|
//Generate and assign a random ID
|
||||||
|
var id = 'collorpicker_' + parseInt(Math.random() * 1000);
|
||||||
|
$(this).data('colpickId', id);
|
||||||
|
//Set the tpl's ID and get the HTML
|
||||||
|
var cal = $(tpl).attr('id', id);
|
||||||
|
//Add class according to layout
|
||||||
|
cal.addClass('colpick_'+options.layout+(options.submit?'':' colpick_'+options.layout+'_ns'));
|
||||||
|
//Add class if the color scheme is not default
|
||||||
|
if(options.colorScheme != 'light') {
|
||||||
|
cal.addClass('colpick_'+options.colorScheme);
|
||||||
|
}
|
||||||
|
//Setup submit button
|
||||||
|
cal.find('div.colpick_submit').html(options.submitText).click(clickSubmit);
|
||||||
|
//Setup input fields
|
||||||
|
options.fields = cal.find('input').change(change).blur(blur).focus(focus);
|
||||||
|
cal.find('div.colpick_field_arrs').mousedown(downIncrement).end().find('div.colpick_current_color').click(restoreOriginal);
|
||||||
|
//Setup hue selector
|
||||||
|
options.selector = cal.find('div.colpick_color').on('mousedown touchstart',downSelector);
|
||||||
|
options.selectorIndic = options.selector.find('div.colpick_selector_outer');
|
||||||
|
//Store parts of the plugin
|
||||||
|
options.el = this;
|
||||||
|
options.hue = cal.find('div.colpick_hue_arrs');
|
||||||
|
var huebar = options.hue.parent();
|
||||||
|
//Paint the hue bar
|
||||||
|
var UA = navigator.userAgent.toLowerCase();
|
||||||
|
var isIE = navigator.appName === 'Microsoft Internet Explorer';
|
||||||
|
var IEver = isIE ? parseFloat( UA.match( /msie ([0-9]{1,}[\.0-9]{0,})/ )[1] ) : 0;
|
||||||
|
var ngIE = ( isIE && IEver < 10 );
|
||||||
|
var stops = ['#ff0000','#ff0080','#ff00ff','#8000ff','#0000ff','#0080ff','#00ffff','#00ff80','#00ff00','#80ff00','#ffff00','#ff8000','#ff0000'];
|
||||||
|
if(ngIE) {
|
||||||
|
var i, div;
|
||||||
|
for(i=0; i<=11; i++) {
|
||||||
|
div = $('<div></div>').attr('style','height:8.333333%; filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+')";');
|
||||||
|
huebar.append(div);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var stopList = stops.join(',');
|
||||||
|
huebar.attr('style','background:-webkit-linear-gradient(top,'+stopList+'); background: -o-linear-gradient(top,'+stopList+'); background: -ms-linear-gradient(top,'+stopList+'); background:-moz-linear-gradient(top,'+stopList+'); -webkit-linear-gradient(top,'+stopList+'); background:linear-gradient(to bottom,'+stopList+'); ');
|
||||||
|
}
|
||||||
|
cal.find('div.colpick_hue').on('mousedown touchstart',downHue);
|
||||||
|
options.newColor = cal.find('div.colpick_new_color');
|
||||||
|
options.currentColor = cal.find('div.colpick_current_color');
|
||||||
|
//Store options and fill with default color
|
||||||
|
cal.data('colpick', options);
|
||||||
|
fillRGBFields(options.color, cal.get(0));
|
||||||
|
fillHSBFields(options.color, cal.get(0));
|
||||||
|
fillHexFields(options.color, cal.get(0));
|
||||||
|
setHue(options.color, cal.get(0));
|
||||||
|
setSelector(options.color, cal.get(0));
|
||||||
|
setCurrentColor(options.color, cal.get(0));
|
||||||
|
setNewColor(options.color, cal.get(0));
|
||||||
|
//Append to body if flat=false, else show in place
|
||||||
|
if (options.flat) {
|
||||||
|
cal.appendTo(this).show();
|
||||||
|
cal.css({
|
||||||
|
position: 'relative',
|
||||||
|
display: 'block'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
cal.appendTo(document.body);
|
||||||
|
$(this).on(options.showEvent, show);
|
||||||
|
cal.css({
|
||||||
|
position:'absolute'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
//Shows the picker
|
||||||
|
showPicker: function() {
|
||||||
|
return this.each( function () {
|
||||||
|
if ($(this).data('colpickId')) {
|
||||||
|
show.apply(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
//Hides the picker
|
||||||
|
hidePicker: function() {
|
||||||
|
return this.each( function () {
|
||||||
|
if ($(this).data('colpickId')) {
|
||||||
|
$('#' + $(this).data('colpickId')).hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
//Sets a color as new and current (default)
|
||||||
|
setColor: function(col, setCurrent) {
|
||||||
|
setCurrent = (typeof setCurrent === "undefined") ? 1 : setCurrent;
|
||||||
|
if (typeof col == 'string') {
|
||||||
|
col = hexToHsb(col);
|
||||||
|
} else if (col.r != undefined && col.g != undefined && col.b != undefined) {
|
||||||
|
col = rgbToHsb(col);
|
||||||
|
} else if (col.h != undefined && col.s != undefined && col.b != undefined) {
|
||||||
|
col = fixHSB(col);
|
||||||
|
} else {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return this.each(function(){
|
||||||
|
if ($(this).data('colpickId')) {
|
||||||
|
var cal = $('#' + $(this).data('colpickId'));
|
||||||
|
cal.data('colpick').color = col;
|
||||||
|
cal.data('colpick').origColor = col;
|
||||||
|
fillRGBFields(col, cal.get(0));
|
||||||
|
fillHSBFields(col, cal.get(0));
|
||||||
|
fillHexFields(col, cal.get(0));
|
||||||
|
setHue(col, cal.get(0));
|
||||||
|
setSelector(col, cal.get(0));
|
||||||
|
|
||||||
|
setNewColor(col, cal.get(0));
|
||||||
|
cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]);
|
||||||
|
if(setCurrent) {
|
||||||
|
setCurrentColor(col, cal.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
//Color space convertions
|
||||||
|
var hexToRgb = function (hex) {
|
||||||
|
var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
|
||||||
|
return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)};
|
||||||
|
};
|
||||||
|
var hexToHsb = function (hex) {
|
||||||
|
return rgbToHsb(hexToRgb(hex));
|
||||||
|
};
|
||||||
|
var rgbToHsb = function (rgb) {
|
||||||
|
var hsb = {h: 0, s: 0, b: 0};
|
||||||
|
var min = Math.min(rgb.r, rgb.g, rgb.b);
|
||||||
|
var max = Math.max(rgb.r, rgb.g, rgb.b);
|
||||||
|
var delta = max - min;
|
||||||
|
hsb.b = max;
|
||||||
|
hsb.s = max != 0 ? 255 * delta / max : 0;
|
||||||
|
if (hsb.s != 0) {
|
||||||
|
if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta;
|
||||||
|
else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta;
|
||||||
|
else hsb.h = 4 + (rgb.r - rgb.g) / delta;
|
||||||
|
} else hsb.h = -1;
|
||||||
|
hsb.h *= 60;
|
||||||
|
if (hsb.h < 0) hsb.h += 360;
|
||||||
|
hsb.s *= 100/255;
|
||||||
|
hsb.b *= 100/255;
|
||||||
|
return hsb;
|
||||||
|
};
|
||||||
|
var hsbToRgb = function (hsb) {
|
||||||
|
var rgb = {};
|
||||||
|
var h = hsb.h;
|
||||||
|
var s = hsb.s*255/100;
|
||||||
|
var v = hsb.b*255/100;
|
||||||
|
if(s == 0) {
|
||||||
|
rgb.r = rgb.g = rgb.b = v;
|
||||||
|
} else {
|
||||||
|
var t1 = v;
|
||||||
|
var t2 = (255-s)*v/255;
|
||||||
|
var t3 = (t1-t2)*(h%60)/60;
|
||||||
|
if(h==360) h = 0;
|
||||||
|
if(h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3}
|
||||||
|
else if(h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3}
|
||||||
|
else if(h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3}
|
||||||
|
else if(h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3}
|
||||||
|
else if(h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3}
|
||||||
|
else if(h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3}
|
||||||
|
else {rgb.r=0; rgb.g=0; rgb.b=0}
|
||||||
|
}
|
||||||
|
return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)};
|
||||||
|
};
|
||||||
|
var rgbToHex = function (rgb) {
|
||||||
|
var hex = [
|
||||||
|
rgb.r.toString(16),
|
||||||
|
rgb.g.toString(16),
|
||||||
|
rgb.b.toString(16)
|
||||||
|
];
|
||||||
|
$.each(hex, function (nr, val) {
|
||||||
|
if (val.length == 1) {
|
||||||
|
hex[nr] = '0' + val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return hex.join('');
|
||||||
|
};
|
||||||
|
var hsbToHex = function (hsb) {
|
||||||
|
return rgbToHex(hsbToRgb(hsb));
|
||||||
|
};
|
||||||
|
$.fn.extend({
|
||||||
|
colpick: colpick.init,
|
||||||
|
colpickHide: colpick.hidePicker,
|
||||||
|
colpickShow: colpick.showPicker,
|
||||||
|
colpickSetColor: colpick.setColor
|
||||||
|
});
|
||||||
|
$.extend({
|
||||||
|
colpick:{
|
||||||
|
rgbToHex: rgbToHex,
|
||||||
|
rgbToHsb: rgbToHsb,
|
||||||
|
hsbToHex: hsbToHex,
|
||||||
|
hsbToRgb: hsbToRgb,
|
||||||
|
hexToHsb: hexToHsb,
|
||||||
|
hexToRgb: hexToRgb
|
||||||
|
}
|
||||||
|
});
|
110
app/javascript/legacy/common/vendor/jquery.cookie.js
vendored
Executable file
110
app/javascript/legacy/common/vendor/jquery.cookie.js
vendored
Executable file
|
@ -0,0 +1,110 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
/*!
|
||||||
|
* jQuery Cookie Plugin v1.4.1
|
||||||
|
* https://github.com/carhartl/jquery-cookie
|
||||||
|
*
|
||||||
|
* Copyright 2013 Klaus Hartl
|
||||||
|
* Released under the MIT license
|
||||||
|
*/
|
||||||
|
(function (factory) {
|
||||||
|
factory(jQuery);
|
||||||
|
}(function ($) {
|
||||||
|
|
||||||
|
var pluses = /\+/g;
|
||||||
|
|
||||||
|
function encode(s) {
|
||||||
|
return config.raw ? s : encodeURIComponent(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function decode(s) {
|
||||||
|
return config.raw ? s : decodeURIComponent(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringifyCookieValue(value) {
|
||||||
|
return encode(config.json ? JSON.stringify(value) : String(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCookieValue(s) {
|
||||||
|
if (s.indexOf('"') === 0) {
|
||||||
|
// This is a quoted cookie as according to RFC2068, unescape...
|
||||||
|
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Replace server-side written pluses with spaces.
|
||||||
|
// If we can't decode the cookie, ignore it, it's unusable.
|
||||||
|
// If we can't parse the cookie, ignore it, it's unusable.
|
||||||
|
s = decodeURIComponent(s.replace(pluses, ' '));
|
||||||
|
return config.json ? JSON.parse(s) : s;
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(s, converter) {
|
||||||
|
var value = config.raw ? s : parseCookieValue(s);
|
||||||
|
return $.isFunction(converter) ? converter(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = $.cookie = function (key, value, options) {
|
||||||
|
|
||||||
|
// Write
|
||||||
|
|
||||||
|
if (value !== undefined && !$.isFunction(value)) {
|
||||||
|
options = $.extend({}, config.defaults, options);
|
||||||
|
|
||||||
|
if (typeof options.expires === 'number') {
|
||||||
|
var days = options.expires, t = options.expires = new Date();
|
||||||
|
t.setTime(+t + days * 864e+5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (document.cookie = [
|
||||||
|
encode(key), '=', stringifyCookieValue(value),
|
||||||
|
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
|
||||||
|
options.path ? '; path=' + options.path : '',
|
||||||
|
options.domain ? '; domain=' + options.domain : '',
|
||||||
|
options.secure ? '; secure' : ''
|
||||||
|
].join(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read
|
||||||
|
|
||||||
|
var result = key ? undefined : {};
|
||||||
|
|
||||||
|
// To prevent the for loop in the first place assign an empty array
|
||||||
|
// in case there are no cookies at all. Also prevents odd result when
|
||||||
|
// calling $.cookie().
|
||||||
|
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
||||||
|
|
||||||
|
for (var i = 0, l = cookies.length; i < l; i++) {
|
||||||
|
var parts = cookies[i].split('=');
|
||||||
|
var name = decode(parts.shift());
|
||||||
|
var cookie = parts.join('=');
|
||||||
|
|
||||||
|
if (key && key === name) {
|
||||||
|
// If second argument (value) is a function it's a converter...
|
||||||
|
result = read(cookie, value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent storing a cookie that we couldn't decode.
|
||||||
|
if (!key && (cookie = read(cookie)) !== undefined) {
|
||||||
|
result[name] = cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
config.defaults = {};
|
||||||
|
|
||||||
|
|
||||||
|
$.removeCookie = function (key, options) {
|
||||||
|
if ($.cookie(key) === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must not alter options, thus extending a fresh object...
|
||||||
|
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
|
||||||
|
return !$.cookie(key);
|
||||||
|
};
|
||||||
|
|
||||||
|
}));
|
9
app/javascript/legacy/common/vendor/masonry.js
vendored
Normal file
9
app/javascript/legacy/common/vendor/masonry.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3
app/javascript/legacy/components/activity_feed.js
Normal file
3
app/javascript/legacy/components/activity_feed.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const R = require('ramda')
|
||||||
|
flyd.lift = require('flyd/module/lift')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function init(state, params$) {
|
||||||
|
state = state||{}
|
||||||
|
state = R.merge({
|
||||||
|
isManual$: flyd.stream(false)
|
||||||
|
, data$: flyd.stream(app.profile ? R.pick(['address', 'city', 'state_code', 'zip_code'], app.profile) : {})
|
||||||
|
, autocompleteInputInserted$: flyd.stream()
|
||||||
|
}, state)
|
||||||
|
|
||||||
|
state.isManual$ = flyd.stream(true)
|
||||||
|
state.params$ = params$
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateToShip(state)
|
||||||
|
{
|
||||||
|
return state.params$().gift_option && state.params$().gift_option.to_ship
|
||||||
|
}
|
||||||
|
|
||||||
|
function view(state) {
|
||||||
|
return h('section.u-padding--5.pastelBox--grey clearfix', [
|
||||||
|
calculateToShip(state)
|
||||||
|
? h('label.u-centered.u-marginBottom--5', [
|
||||||
|
'Shipping address (required)'
|
||||||
|
])
|
||||||
|
: ''
|
||||||
|
, manualFields(state)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const manualFields = state => {
|
||||||
|
return h('div', [
|
||||||
|
h('fieldset.col-8.u-fontSize--14', [
|
||||||
|
h('input.u-marginBottom--0', {props: {
|
||||||
|
type: 'text'
|
||||||
|
, title: 'Street Addresss'
|
||||||
|
, name: 'address'
|
||||||
|
, placeholder: 'Street Address'
|
||||||
|
, value: state.data$().address
|
||||||
|
, required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined
|
||||||
|
}})
|
||||||
|
])
|
||||||
|
, h('fieldset.col-right-4.u-fontSize--15', [
|
||||||
|
h('input.u-marginBottom--0', {props: {
|
||||||
|
type: 'text'
|
||||||
|
, name: 'city'
|
||||||
|
, title: 'City'
|
||||||
|
, placeholder: 'City'
|
||||||
|
, value: state.data$().city
|
||||||
|
, required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined
|
||||||
|
}})
|
||||||
|
])
|
||||||
|
, h('fieldset.u-marginBottom--0.u-floatL.col-4', [
|
||||||
|
h('input.u-marginBottom--0', {props: {
|
||||||
|
type: 'text'
|
||||||
|
, name: 'state_code'
|
||||||
|
, title: 'State/Region'
|
||||||
|
, placeholder: 'State/Region'
|
||||||
|
, value: state.data$().state_code
|
||||||
|
, required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined
|
||||||
|
}})
|
||||||
|
])
|
||||||
|
, h('fieldset.u-marginBottom--0.u-floatL.col-right-4.u-fontSize--14', [
|
||||||
|
h('input.u-marginBottom--0', {props: {
|
||||||
|
type: 'text'
|
||||||
|
, title: 'Zip/Postal'
|
||||||
|
, name: 'zip_code'
|
||||||
|
, placeholder: 'Zip/Postal'
|
||||||
|
, value: state.data$().zip_code
|
||||||
|
, required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined
|
||||||
|
}})
|
||||||
|
])
|
||||||
|
, h('fieldset.u-marginBottom--0.u-floatL.col-right-4', [
|
||||||
|
h('input.u-marginBottom--0', {props: {
|
||||||
|
type: 'text'
|
||||||
|
, title: 'Country'
|
||||||
|
, name: 'country'
|
||||||
|
, placeholder: 'Country'
|
||||||
|
, value: state.data$().country
|
||||||
|
, required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined
|
||||||
|
}})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {init, view}
|
||||||
|
|
||||||
|
|
81
app/javascript/legacy/components/address-autocomplete.js
Normal file
81
app/javascript/legacy/components/address-autocomplete.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
|
||||||
|
// Stream that has true when google script is loaded
|
||||||
|
const loaded$ = flyd.stream()
|
||||||
|
// Stream of autocomplete data
|
||||||
|
const data$ = flyd.stream()
|
||||||
|
|
||||||
|
function initScript() {
|
||||||
|
// if(document.getElementById('googleAutocomplete')) return
|
||||||
|
// var script = document.createElement('script')
|
||||||
|
// script.type = 'text/javascript'
|
||||||
|
// script.id = 'googleAutocomplete'
|
||||||
|
// document.body.appendChild(script)
|
||||||
|
// script.src = `https://maps.googleapis.com/maps/api/js?key=${app.google_api}&libraries=places&callback=initGoogleAutocomplete`
|
||||||
|
return loaded$
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initGoogleAutocomplete = () => loaded$(true)
|
||||||
|
|
||||||
|
function initInput(input) {
|
||||||
|
var autocomplete = new google.maps.places.Autocomplete(input, {types: ['geocode']})
|
||||||
|
autocomplete.addListener('place_changed', fillInAddress(autocomplete))
|
||||||
|
input.addEventListener('focus', geolocate(autocomplete))
|
||||||
|
input.addEventListener('keydown', e => { if(e.which === 13) e.preventDefault() })
|
||||||
|
return data$
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptedTypes = {
|
||||||
|
street_number: 'short_name'
|
||||||
|
, route: 'long_name'
|
||||||
|
, locality: 'long_name'
|
||||||
|
, administrative_area_level_1: 'short_name'
|
||||||
|
, country: 'long_name'
|
||||||
|
, postal_code: 'short_name'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fillInAddress = autocomplete => () => {
|
||||||
|
var place = { components: autocomplete.getPlace().address_components}
|
||||||
|
if(!place.components) return
|
||||||
|
place.types = R.map(x => x.types[0], place.components)
|
||||||
|
var address = placeData(place, 'street_number')
|
||||||
|
? placeData(place, 'street_number') + ' ' + placeData(place, 'route')
|
||||||
|
: ''
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
address: address
|
||||||
|
, city: placeData(place, 'locality')
|
||||||
|
, state_code: placeData(place, 'administrative_area_level_1')
|
||||||
|
, country: placeData(place, 'country')
|
||||||
|
, zip_code: placeData(place, 'postal_code')
|
||||||
|
}
|
||||||
|
data$(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeData(place, key) {
|
||||||
|
const i = R.findIndex(R.equals(key), place.types)
|
||||||
|
if(i >= 0) return place.components[i][acceptedTypes[key]]
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bias the autocomplete object to the user's geographical location,
|
||||||
|
// as supplied by the browser's 'navigator.geolocation' object.
|
||||||
|
const geolocate = autocomplete => () => {
|
||||||
|
if(!navigator || !navigator.geolocation) return
|
||||||
|
navigator.geolocation.getCurrentPosition(pos => {
|
||||||
|
var geolocation = {
|
||||||
|
lat: pos.coords.latitude
|
||||||
|
, lng: pos.coords.longitude
|
||||||
|
}
|
||||||
|
var circle = new google.maps.Circle({
|
||||||
|
center: geolocation
|
||||||
|
, radius: pos.coords.accuracy
|
||||||
|
})
|
||||||
|
autocomplete.setBounds(circle.getBounds())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {initScript, initInput, data$, loaded$}
|
15
app/javascript/legacy/components/ajax/toggle_soft_delete.js
Normal file
15
app/javascript/legacy/components/ajax/toggle_soft_delete.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var request = require('../../common/client')
|
||||||
|
|
||||||
|
module.exports = function(url, type) {
|
||||||
|
appl.def('toggle_soft_delete', function(bool) {
|
||||||
|
appl.def('loading', true)
|
||||||
|
var action = bool ? 'deleted.' : 'undeleted.'
|
||||||
|
request.put(url + '/soft_delete', {delete: bool}).end(function(err, resp) {
|
||||||
|
appl.def('loading', false)
|
||||||
|
.def(type + '_is_deleted', bool)
|
||||||
|
.notify('Successfully ' + action)
|
||||||
|
.close_modal()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
13
app/javascript/legacy/components/b64.js
Normal file
13
app/javascript/legacy/components/b64.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// see https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|
||||||
|
// used for encoded and decoding data for email text
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
encode: str =>
|
||||||
|
btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g
|
||||||
|
, (match, p1) => String.fromCharCode('0x' + p1))).replace(/\//g,'_').replace(/\+/g,'-')
|
||||||
|
, decode: str =>
|
||||||
|
decodeURIComponent(Array.prototype.map.call(atob(str.replace(/-/g, '+').replace(/_/g, '/'))
|
||||||
|
, c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''))
|
||||||
|
}
|
||||||
|
|
10
app/javascript/legacy/components/branded_fundraising.js
Normal file
10
app/javascript/legacy/components/branded_fundraising.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const brandColors = require('../components/nonprofit-branding')
|
||||||
|
|
||||||
|
$('[if-branded]').each(function() {
|
||||||
|
var params = this.getAttribute("if-branded").split(',').map(function(s) { return s.trim() })
|
||||||
|
$(this).css(params[0], brandColors[params[1]])
|
||||||
|
})
|
||||||
|
|
||||||
|
exports = brandColors
|
||||||
|
|
190
app/javascript/legacy/components/card-form.es6
Normal file
190
app/javascript/legacy/components/card-form.es6
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
// npm
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const R = require('ramda')
|
||||||
|
const validatedForm = require('ff-core/validated-form')
|
||||||
|
const button = require('ff-core/button')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
flyd.flatMap = require('flyd/module/flatmap')
|
||||||
|
flyd.filter = require('flyd/module/filter')
|
||||||
|
flyd.mergeAll = require('flyd/module/mergeall')
|
||||||
|
const scanMerge = require('flyd/module/scanmerge')
|
||||||
|
// local
|
||||||
|
const request = require('../common/request')
|
||||||
|
const formatErr = require('../common/format_response_error')
|
||||||
|
const createCardStream = require('../cards/create-frp.es6')
|
||||||
|
const serializeForm = require('form-serialize')
|
||||||
|
const luhnCheck = require('../common/credit-card-validator.js')
|
||||||
|
|
||||||
|
// A component for filling out card data, validating it, saving the card to
|
||||||
|
// stripe, and then saving a tokenized copy to our servers.
|
||||||
|
|
||||||
|
// Form validation constraints, validator functions, and error messages:
|
||||||
|
var constraints = {
|
||||||
|
address_zip: {required: true}
|
||||||
|
, name: {required: true}
|
||||||
|
, number: {required: true, cardNumber: true}
|
||||||
|
, exp_month: {required: true, format: /\d\d?/}
|
||||||
|
, exp_year: {required: true, format: /\d\d?/}
|
||||||
|
, cvc: {required: true, format: /\d\d\d\d?/}
|
||||||
|
}
|
||||||
|
var validators = { cardNumber: luhnCheck }
|
||||||
|
var messages = {
|
||||||
|
number: {
|
||||||
|
required: I18n.t('nonprofits.donate.payment.card.errors.number.presence')
|
||||||
|
, cardNumber: I18n.t('nonprofits.donate.payment.card.errors.number.format')
|
||||||
|
}
|
||||||
|
, required: I18n.t('nonprofits.donate.payment.card.errors.field.presence')
|
||||||
|
, email: I18n.t('nonprofits.donate.payment.card.errors.email.format')
|
||||||
|
, format: I18n.t('nonprofits.donate.payment.card.errors.field.format')
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can pass in the .hideButton boolean if you want to control whether the submit button is shown/hidden
|
||||||
|
// Pass in a .card object, which can have default objects for the card form (name, number, cvc, exp_month, etc)
|
||||||
|
// Pass in .path to set the endpoint for saving the card
|
||||||
|
// Pass in .payload for default data to send to the server for every card save request (such as a request token)
|
||||||
|
const init = (state) => {
|
||||||
|
state = state || {}
|
||||||
|
// set defaults
|
||||||
|
state = R.merge({
|
||||||
|
payload$: flyd.stream(state.payload || {})
|
||||||
|
, path$: flyd.stream(state.path || '/cards')
|
||||||
|
}, state)
|
||||||
|
|
||||||
|
state.form = validatedForm.init({constraints, validators, messages})
|
||||||
|
state.card$ = flyd.merge(flyd.stream(state.card || {}), state.form.validData$)
|
||||||
|
|
||||||
|
// streams of stripe tokenization responses
|
||||||
|
const stripeResp$ = flyd.flatMap(createCardStream, state.form.validData$)
|
||||||
|
state.stripeRespOk$ = flyd.filter(r => !r.error, stripeResp$)
|
||||||
|
const stripeError$ = flyd.map(r => r.error.message, flyd.filter(r => r.error, stripeResp$))
|
||||||
|
|
||||||
|
// Save the card as a card table on our own db
|
||||||
|
// streams of responses
|
||||||
|
state.resp$ = flyd.flatMap(
|
||||||
|
resp => saveCard(state.payload$(), state.path$(), resp) // cheating on the streams here..
|
||||||
|
, state.stripeRespOk$ )
|
||||||
|
|
||||||
|
const ccError$ = flyd.map(R.prop('error'), flyd.filter(resp => resp.error, state.resp$))
|
||||||
|
state.saved$ = flyd.filter(resp => !resp.error, state.resp$)
|
||||||
|
state.error$ = flyd.merge(stripeError$, ccError$)
|
||||||
|
|
||||||
|
state.loading$ = scanMerge([
|
||||||
|
[state.form.validSubmit$, R.always(true)]
|
||||||
|
, [state.error$, R.always(false)]
|
||||||
|
, [state.saved$, R.always(false)]
|
||||||
|
], false)
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- Stream-related functions
|
||||||
|
|
||||||
|
// Save the card to our own servers, and return a response stream
|
||||||
|
const saveCard = (send, path, resp) => {
|
||||||
|
send.card = R.merge(send.card, {
|
||||||
|
cardholders_name: resp.name
|
||||||
|
, name: `${resp.card.brand} *${resp.card.last4}`
|
||||||
|
, stripe_card_token: resp.id
|
||||||
|
, stripe_card_id: resp.card.id
|
||||||
|
})
|
||||||
|
return flyd.map(R.prop('body'), request({ path, send, method: 'post' }).load)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -- Virtual DOM
|
||||||
|
|
||||||
|
const view = state => {
|
||||||
|
var field = validatedForm.field(state.form)
|
||||||
|
return validatedForm.form(state.form, h('form.cardForm', [
|
||||||
|
h('div.u-background--grey.group.u-padding--8', [
|
||||||
|
nameInput(field, state.card$().name)
|
||||||
|
, numberInput(field)
|
||||||
|
, cvcInput(field)
|
||||||
|
, expMonthInput(field)
|
||||||
|
, expYearInput(field)
|
||||||
|
, zipInput(field, state.card$().address_zip)
|
||||||
|
, profileInput(field, app.profile_id) // XXX global
|
||||||
|
])
|
||||||
|
, h('div.u-centered.u-marginTop--20', [
|
||||||
|
state.hideButton ? '' : button({
|
||||||
|
error$: state.hideErrors ? flyd.stream() : state.error$
|
||||||
|
, loading$: state.loading$
|
||||||
|
, buttonText: I18n.t('nonprofits.donate.payment.card.submit')
|
||||||
|
, loadingText: ` ${I18n.t('nonprofits.donate.payment.card.loading')}`
|
||||||
|
})
|
||||||
|
, h('p.u-fontSize--12.u-marginBottom--0.u-marginTop--10.u-color--grey', [ h('i.fa.fa-lock'), ` ${I18n.t('nonprofits.donate.payment.card.secure_info')}`])
|
||||||
|
])
|
||||||
|
]) )
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const nameInput = (field, name) =>
|
||||||
|
h('fieldset', [ field(h('input', { props: { name: 'name' , value: name || '', placeholder: I18n.t('nonprofits.donate.payment.card.name') } })) ])
|
||||||
|
|
||||||
|
|
||||||
|
const numberInput = field =>
|
||||||
|
h('fieldset.col-8', [ field(h('input', {props: { type: 'text' , name: 'number' , placeholder: I18n.t('nonprofits.donate.payment.card.number') } })) ])
|
||||||
|
|
||||||
|
|
||||||
|
const cvcInput = field =>
|
||||||
|
h('fieldset.col-right-4.u-relative', [
|
||||||
|
field(h('input', { props: { name: 'cvc' , placeholder: I18n.t('nonprofits.donate.payment.card.cvc') } } ))
|
||||||
|
, h('img.security-code-image', {
|
||||||
|
src: `${app.asset_path}/graphics/cc-security-code.png`
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
const expMonthInput = field => {
|
||||||
|
var options = R.prepend(
|
||||||
|
h('option.default', {props: {value: undefined, selected: true}}, I18n.t('nonprofits.donate.payment.card.month'))
|
||||||
|
, R.range(1, 13).map(n => h('option', String(n)))
|
||||||
|
)
|
||||||
|
return h('fieldset.col-3.u-margin--0', [
|
||||||
|
field(h('select.select'
|
||||||
|
, { props: {name: 'exp_month'} }
|
||||||
|
, options))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const expYearInput = field => {
|
||||||
|
var yearRange = R.range(new Date().getFullYear(), new Date().getFullYear() + 15)
|
||||||
|
var options = R.prepend(
|
||||||
|
h('option.default', {props: {value: undefined, selected: true}}, I18n.t('nonprofits.donate.payment.card.year'))
|
||||||
|
, R.map(y => h('option', String(y)), yearRange)
|
||||||
|
)
|
||||||
|
return h('fieldset.col-left-3.u-margin--0', [
|
||||||
|
field(h('select.select'
|
||||||
|
, {props: {name: 'exp_year'}}
|
||||||
|
, options))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const zipInput = (field, zip) =>
|
||||||
|
h('fieldset.col-right-6.u-margin--0', [
|
||||||
|
field(h('input'
|
||||||
|
, { props: {
|
||||||
|
type: 'text'
|
||||||
|
, name: 'address_zip'
|
||||||
|
, value: zip || ''
|
||||||
|
, placeholder: I18n.t('nonprofits.donate.payment.card.postal_code')
|
||||||
|
}}
|
||||||
|
))
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
const profileInput = (field, profile_id) =>
|
||||||
|
field(h('input'
|
||||||
|
, { props: {
|
||||||
|
type: 'hidden'
|
||||||
|
, name: 'profile_id'
|
||||||
|
, value: profile_id || ''
|
||||||
|
}}
|
||||||
|
))
|
||||||
|
|
||||||
|
module.exports = {view, init}
|
||||||
|
|
29
app/javascript/legacy/components/chart-options.js
Normal file
29
app/javascript/legacy/components/chart-options.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var chartOptions = {}
|
||||||
|
|
||||||
|
chartOptions.default = {
|
||||||
|
defaultFontFamily: "'Open Sans', 'Helvetica Neue', 'Arial', 'sans-serif'"
|
||||||
|
, scales: {
|
||||||
|
yAxes: [{ ticks: { min: 0 }}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chartOptions.dollars = {
|
||||||
|
defaultFontFamily: "'Open Sans', 'Helvetica Neue', 'Arial', 'sans-serif'"
|
||||||
|
, scales: {
|
||||||
|
yAxes: [{ ticks: {
|
||||||
|
min: 0
|
||||||
|
, callback: (val) => '$' + utils.cents_to_dollars(val)
|
||||||
|
} }]
|
||||||
|
}
|
||||||
|
, tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
label: (item, data) =>
|
||||||
|
data.datasets[item.datasetIndex].label +
|
||||||
|
': $' + utils.cents_to_dollars(item.yLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = chartOptions
|
||||||
|
|
15
app/javascript/legacy/components/checkbox.js
Normal file
15
app/javascript/legacy/components/checkbox.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('flimflam/h')
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
|
// example:
|
||||||
|
// checkbox({name: 'anonymous', value: 'true', label: 'Donate anonymously?'})
|
||||||
|
|
||||||
|
module.exports = obj => {
|
||||||
|
const id = uuid.v1()
|
||||||
|
return h('div', [
|
||||||
|
h('input', {props: {type: 'checkbox', id, value: obj.value, name: obj.name}})
|
||||||
|
, h('label', {attrs: {for: id}}, [h('span.pl-1.sub.font-weight-1', obj.label ? obj.label : obj.value)])
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
32
app/javascript/legacy/components/color-picker.es6
Normal file
32
app/javascript/legacy/components/color-picker.es6
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const R = require('ramda')
|
||||||
|
require('../common/vendor/colpick') // XXX jquery
|
||||||
|
|
||||||
|
// Color picker UI component, wrapping the colpick jquery plugin
|
||||||
|
// You can use colorPicker.streams.color to access a stream of hex color values selected by the user
|
||||||
|
// Will also set colorPicker.state.color for every selected color value
|
||||||
|
|
||||||
|
function init(defaultColor) {
|
||||||
|
var logoBlue = '#42B3DF'
|
||||||
|
return {color$: flyd.stream(defaultColor || logoBlue)}
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = state =>
|
||||||
|
h('div.colPick-wrapper.inner#colorpicker', {
|
||||||
|
hook: {
|
||||||
|
insert: (vnode) => {
|
||||||
|
$(vnode.elm).colpick({
|
||||||
|
flat: true
|
||||||
|
, layout: 'hex'
|
||||||
|
, submit: false
|
||||||
|
, color: state.color$()
|
||||||
|
, onChange: (hsb, hex, rgb, el, bySetColor) => state.color$('#' + hex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = {init, view}
|
||||||
|
|
44
app/javascript/legacy/components/confirmation-modal.js
Normal file
44
app/javascript/legacy/components/confirmation-modal.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const R = require('ramda')
|
||||||
|
const h = require('snabbdom/h')
|
||||||
|
const uuid = require('uuid')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const modal = require('ff-core/modal')
|
||||||
|
const mergeAll = require('flyd/module/mergeall')
|
||||||
|
|
||||||
|
// show$ is the stream that shows the confirmation modal
|
||||||
|
const init = show$ => {
|
||||||
|
const state = {
|
||||||
|
confirm$: flyd.stream()
|
||||||
|
, unconfirm$: flyd.stream()
|
||||||
|
, ID: uuid.v1()
|
||||||
|
}
|
||||||
|
|
||||||
|
state.modalID$ = mergeAll([
|
||||||
|
flyd.map(R.always(state.ID), show$)
|
||||||
|
, flyd.map(R.always(null), state.unconfirm$)
|
||||||
|
, flyd.map(R.always(null), state.confirm$)])
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
// msg is optional
|
||||||
|
const view = (state, msg) =>
|
||||||
|
modal({
|
||||||
|
id$: state.modalID$
|
||||||
|
, thisID: state.ID
|
||||||
|
, notCloseable: true
|
||||||
|
, body: h('div', [
|
||||||
|
h('h4', msg || 'Are you sure?')
|
||||||
|
, h('div', [
|
||||||
|
h('button', {attrs: {'data-ff-confirm': true}, on: {click: state.confirm$}}
|
||||||
|
, 'Yes')
|
||||||
|
, h('button', {attrs: {'data-ff-confirm': false}, on: {click: state.unconfirm$}}
|
||||||
|
, 'No')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {init, view}
|
||||||
|
|
11
app/javascript/legacy/components/date-range.js
Normal file
11
app/javascript/legacy/components/date-range.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const moment = require('moment')
|
||||||
|
require('moment-range')
|
||||||
|
|
||||||
|
// returns an array of moments
|
||||||
|
// timeSpan is one of 'day, 'week', 'month', 'year' (see moment.js docs)
|
||||||
|
module.exports = (startDate, endDate, timeSpan) => {
|
||||||
|
var dates = [moment(startDate), moment(endDate)]
|
||||||
|
return moment.range(dates).toArray(timeSpan)
|
||||||
|
}
|
||||||
|
|
32
app/javascript/legacy/components/date_range_picker.js
Normal file
32
app/javascript/legacy/components/date_range_picker.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
var Pikaday = require('pikaday')
|
||||||
|
var moment = require('moment')
|
||||||
|
|
||||||
|
var el = document.querySelector('#dateRange')
|
||||||
|
if(el) {
|
||||||
|
var before_date = el.querySelector('#beforeDate')
|
||||||
|
var after_date = el.querySelector('#afterDate')
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_date(el) {
|
||||||
|
return function(date) {
|
||||||
|
el.value = moment(date).format('MM/DD/YYYY')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(el && before_date) {
|
||||||
|
new Pikaday({
|
||||||
|
field: before_date,
|
||||||
|
format: 'MM/DD/YYYY',
|
||||||
|
onSelect: format_date(before_date)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if(el && after_date) {
|
||||||
|
new Pikaday({
|
||||||
|
field: after_date,
|
||||||
|
format: 'MM/DD/YYYY',
|
||||||
|
onSelect: format_date(after_date)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
15
app/javascript/legacy/components/dollar-input.js
Normal file
15
app/javascript/legacy/components/dollar-input.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const h = require('flimflam/h')
|
||||||
|
|
||||||
|
module.exports = (name, placeholder, value) => {
|
||||||
|
return h('input.dollar-input.max-width-2', {
|
||||||
|
props: {
|
||||||
|
type: 'number'
|
||||||
|
, step: 'any'
|
||||||
|
, min: 0
|
||||||
|
, name
|
||||||
|
, placeholder
|
||||||
|
, value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
37
app/javascript/legacy/components/drag-to-reorder.js
Normal file
37
app/javascript/legacy/components/drag-to-reorder.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
const dragula = require('dragula')
|
||||||
|
const serialize = require('form-serialize')
|
||||||
|
const R = require('ramda')
|
||||||
|
const request = require('../common/request')
|
||||||
|
const flyd = require('flyd')
|
||||||
|
const flatMap = require('flyd/module/flatmap')
|
||||||
|
|
||||||
|
const mapIndex = R.addIndex(R.map)
|
||||||
|
|
||||||
|
module.exports = function(path, containerId, afterUpdateFunction) {
|
||||||
|
|
||||||
|
// Stream of dragged elements
|
||||||
|
const draggedEls$ = flyd.stream()
|
||||||
|
|
||||||
|
dragula([document.getElementById(containerId)]).on('dragend', draggedEls$)
|
||||||
|
|
||||||
|
// Make a stream of objects with .id and .order
|
||||||
|
const giftOptions$ = flyd.map( getIdAndOrder , draggedEls$)
|
||||||
|
|
||||||
|
function getIdAndOrder(el) {
|
||||||
|
var form = el.querySelector('input').form
|
||||||
|
var ids = serialize(form, {hash: true}).id
|
||||||
|
return {data: mapIndex((v, i) => ({id: v, order: i}), ids)}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOrdering = send => flyd.map(R.prop('body'), request({path, method: 'put' , send}).load)
|
||||||
|
|
||||||
|
const response$ = flatMap(updateOrdering, giftOptions$)
|
||||||
|
|
||||||
|
// Optional after update function
|
||||||
|
if(afterUpdateFunction) {
|
||||||
|
flyd.map(afterUpdateFunction, response$)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue