fc77ee76d6
The primary license of the project is changing to: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later The Additional Permission is designed to permit publicly distributed Javascript code to be relicensed under LGPL-3.0-or-later, but not server-side Javascript code. As such, we've relicensed here static Javscript files under LGPL-3.0-or-later, and those that run as part of build and/or server side under AGPL-3.0-or-later. Note that in future, Javascript files may be updated to be stronger copyleft license with the Additional Permission, particularly if they adapted to run on server side and/or turned into templates. Of course, we'd seek public discussion with the contributor community about such changes. This commit is one of the many steps to relicense the entire codebase. Documentation granting permission for this relicensing (from all past contributors who hold copyrights) is on file with Software Freedom Conservancy, Inc.
390 lines
11 KiB
JavaScript
390 lines
11 KiB
JavaScript
// 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')
|
|
|
|
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()
|
|
|
|
// Define the current payment plan tier for a signed-in nonprofit
|
|
appl.def('current_plan_tier', app.current_plan_tier)
|
|
|
|
appl.def("is_at_least_plan", function(tier) {
|
|
return app.current_plan_tier >= tier
|
|
})
|
|
|
|
// Open a modal given by its modal id (uses the modal div's 'id' attribute)
|
|
appl.def('open_modal', function(modalId) {
|
|
$('.modal').removeClass('inView')
|
|
$('#' + modalId).addClass('inView')
|
|
$('body').addClass('is-showingModal')
|
|
return appl
|
|
})
|
|
|
|
// Close any and all open modals
|
|
appl.def('close_modal', function() {
|
|
$('.modal').removeClass('inView')
|
|
$('body').removeClass('is-showingModal')
|
|
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)
|
|
})
|
|
|