mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
8f5270899f
Transform frontend module format from AMD to ESM GitOrigin-RevId: 9adbcdc95e819a54114010c6fd3521d8f58ef2fe
714 lines
17 KiB
JavaScript
714 lines
17 KiB
JavaScript
/* eslint-disable
|
|
max-len,
|
|
no-useless-escape,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS103: Rewrite code to no longer use __guard__
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import App from '../base'
|
|
const defaultFormat = /(\d{1,4})/g
|
|
const defaultInputFormat = /(?:^|\s)(\d{4})$/
|
|
|
|
const cards = [
|
|
// Credit cards
|
|
{
|
|
type: 'visa',
|
|
patterns: [4],
|
|
format: defaultFormat,
|
|
length: [13, 16],
|
|
cvcLength: [3],
|
|
luhn: true
|
|
},
|
|
{
|
|
type: 'mastercard',
|
|
patterns: [51, 52, 53, 54, 55, 22, 23, 24, 25, 26, 27],
|
|
format: defaultFormat,
|
|
length: [16],
|
|
cvcLength: [3],
|
|
luhn: true
|
|
},
|
|
{
|
|
type: 'amex',
|
|
patterns: [34, 37],
|
|
format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
|
|
length: [15],
|
|
cvcLength: [3, 4],
|
|
luhn: true
|
|
},
|
|
{
|
|
type: 'dinersclub',
|
|
patterns: [30, 36, 38, 39],
|
|
format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
|
|
length: [14],
|
|
cvcLength: [3],
|
|
luhn: true
|
|
},
|
|
{
|
|
type: 'discover',
|
|
patterns: [60, 64, 65, 622],
|
|
format: defaultFormat,
|
|
length: [16],
|
|
cvcLength: [3],
|
|
luhn: true
|
|
},
|
|
{
|
|
type: 'unionpay',
|
|
patterns: [62, 88],
|
|
format: defaultFormat,
|
|
length: [16, 17, 18, 19],
|
|
cvcLength: [3],
|
|
luhn: false
|
|
},
|
|
{
|
|
type: 'jcb',
|
|
patterns: [35],
|
|
format: defaultFormat,
|
|
length: [16],
|
|
cvcLength: [3],
|
|
luhn: true
|
|
}
|
|
]
|
|
|
|
App.factory('ccUtils', function() {
|
|
const cardFromNumber = function(num) {
|
|
num = (num + '').replace(/\D/g, '')
|
|
for (let card of Array.from(cards)) {
|
|
for (let pattern of Array.from(card.patterns)) {
|
|
const p = pattern + ''
|
|
if (num.substr(0, p.length) === p) {
|
|
return card
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const cardFromType = function(type) {
|
|
for (let card of Array.from(cards)) {
|
|
if (card.type === type) {
|
|
return card
|
|
}
|
|
}
|
|
}
|
|
|
|
const cardType = function(num) {
|
|
if (!num) {
|
|
return null
|
|
}
|
|
return __guard__(cardFromNumber(num), x => x.type) || null
|
|
}
|
|
|
|
const formatCardNumber = function(num) {
|
|
num = num.replace(/\D/g, '')
|
|
const card = cardFromNumber(num)
|
|
if (!card) {
|
|
return num
|
|
}
|
|
|
|
const upperLength = card.length[card.length.length - 1]
|
|
num = num.slice(0, upperLength)
|
|
|
|
if (card.format.global) {
|
|
return __guard__(num.match(card.format), x => x.join(' '))
|
|
} else {
|
|
let groups = card.format.exec(num)
|
|
if (groups == null) {
|
|
return
|
|
}
|
|
groups.shift()
|
|
groups = $.grep(groups, n => n) // Filter empty groups
|
|
return groups.join(' ')
|
|
}
|
|
}
|
|
|
|
const formatExpiry = function(expiry) {
|
|
const parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
|
|
if (!parts) {
|
|
return ''
|
|
}
|
|
|
|
let mon = parts[1] || ''
|
|
let sep = parts[2] || ''
|
|
const year = parts[3] || ''
|
|
|
|
if (year.length > 0) {
|
|
sep = ' / '
|
|
} else if (sep === ' /') {
|
|
mon = mon.substring(0, 1)
|
|
sep = ''
|
|
} else if (mon.length === 2 || sep.length > 0) {
|
|
sep = ' / '
|
|
} else if (mon.length === 1 && !['0', '1'].includes(mon)) {
|
|
mon = `0${mon}`
|
|
sep = ' / '
|
|
}
|
|
|
|
return mon + sep + year
|
|
}
|
|
|
|
const parseExpiry = function(value) {
|
|
if (value == null) {
|
|
value = ''
|
|
}
|
|
let [month, year] = Array.from(value.split(/[\s\/]+/, 2))
|
|
|
|
// Allow for year shortcut
|
|
if ((year != null ? year.length : undefined) === 2 && /^\d+$/.test(year)) {
|
|
let prefix = new Date().getFullYear()
|
|
prefix = prefix.toString().slice(0, 2)
|
|
year = prefix + year
|
|
}
|
|
|
|
month = parseInt(month, 10)
|
|
year = parseInt(year, 10)
|
|
|
|
if (!!isNaN(month) || !!isNaN(year)) {
|
|
return
|
|
}
|
|
|
|
return { month, year }
|
|
}
|
|
|
|
return {
|
|
fromNumber: cardFromNumber,
|
|
fromType: cardFromType,
|
|
cardType,
|
|
formatExpiry,
|
|
formatCardNumber,
|
|
defaultFormat,
|
|
defaultInputFormat,
|
|
parseExpiry
|
|
}
|
|
})
|
|
|
|
App.factory('ccFormat', function(ccUtils, $filter) {
|
|
const hasTextSelected = function($target) {
|
|
// If some text is selected
|
|
if (
|
|
$target.prop('selectionStart') != null &&
|
|
$target.prop('selectionStart') !== $target.prop('selectionEnd')
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// If some text is selected in IE
|
|
if (
|
|
__guard__(
|
|
typeof document !== 'undefined' && document !== null
|
|
? document.selection
|
|
: undefined,
|
|
x => x.createRange
|
|
) != null
|
|
) {
|
|
if (document.selection.createRange().text) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
const safeVal = function(value, $target) {
|
|
let cursor
|
|
try {
|
|
cursor = $target.prop('selectionStart')
|
|
} catch (error) {
|
|
cursor = null
|
|
}
|
|
|
|
const last = $target.val()
|
|
$target.val(value)
|
|
|
|
if (cursor !== null && $target.is(':focus')) {
|
|
if (cursor === last.length) {
|
|
cursor = value.length
|
|
}
|
|
|
|
// This hack looks for scenarios where we are changing an input's value such
|
|
// that "X| " is replaced with " |X" (where "|" is the cursor). In those
|
|
// scenarios, we want " X|".
|
|
//
|
|
// For example:
|
|
// 1. Input field has value "4444| "
|
|
// 2. User types "1"
|
|
// 3. Input field has value "44441| "
|
|
// 4. Reformatter changes it to "4444 |1"
|
|
// 5. By incrementing the cursor, we make it "4444 1|"
|
|
//
|
|
// This is awful, and ideally doesn't go here, but given the current design
|
|
// of the system there does not appear to be a better solution.
|
|
//
|
|
// Note that we can't just detect when the cursor-1 is " ", because that
|
|
// would incorrectly increment the cursor when backspacing, e.g. pressing
|
|
// backspace in this scenario: "4444 1|234 5".
|
|
if (last !== value) {
|
|
const prevPair = last.slice(cursor - 1, +cursor + 1 || undefined)
|
|
const currPair = value.slice(cursor - 1, +cursor + 1 || undefined)
|
|
const digit = value[cursor]
|
|
if (
|
|
/\d/.test(digit) &&
|
|
prevPair === `${digit} ` &&
|
|
currPair === ` ${digit}`
|
|
) {
|
|
cursor = cursor + 1
|
|
}
|
|
}
|
|
|
|
$target.prop('selectionStart', cursor)
|
|
return $target.prop('selectionEnd', cursor)
|
|
}
|
|
}
|
|
|
|
// Replace Full-Width Chars
|
|
const replaceFullWidthChars = function(str) {
|
|
if (str == null) {
|
|
str = ''
|
|
}
|
|
const fullWidth =
|
|
'\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
|
|
const halfWidth = '0123456789'
|
|
|
|
let value = ''
|
|
const chars = str.split('')
|
|
|
|
// Avoid using reserved word `char`
|
|
for (let chr of Array.from(chars)) {
|
|
const idx = fullWidth.indexOf(chr)
|
|
if (idx > -1) {
|
|
chr = halfWidth[idx]
|
|
}
|
|
value += chr
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
// Format Numeric
|
|
const reFormatNumeric = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
return setTimeout(function() {
|
|
let value = $target.val()
|
|
value = replaceFullWidthChars(value)
|
|
value = value.replace(/\D/g, '')
|
|
return safeVal(value, $target)
|
|
})
|
|
}
|
|
|
|
// Format Card Number
|
|
const reFormatCardNumber = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
return setTimeout(function() {
|
|
let value = $target.val()
|
|
value = replaceFullWidthChars(value)
|
|
value = ccUtils.formatCardNumber(value)
|
|
return safeVal(value, $target)
|
|
})
|
|
}
|
|
|
|
const formatCardNumber = function(e) {
|
|
// Only format if input is a number
|
|
let re
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
const $target = $(e.currentTarget)
|
|
const value = $target.val()
|
|
const card = ccUtils.fromNumber(value + digit)
|
|
const { length } = value.replace(/\D/g, '') + digit
|
|
|
|
let upperLength = 16
|
|
if (card) {
|
|
upperLength = card.length[card.length.length - 1]
|
|
}
|
|
if (length >= upperLength) {
|
|
return
|
|
}
|
|
|
|
// Return if focus isn't at the end of the text
|
|
if (
|
|
$target.prop('selectionStart') != null &&
|
|
$target.prop('selectionStart') !== value.length
|
|
) {
|
|
return
|
|
}
|
|
|
|
if (card && card.type === 'amex') {
|
|
// AMEX cards are formatted differently
|
|
re = /^(\d{4}|\d{4}\s\d{6})$/
|
|
} else {
|
|
re = /(?:^|\s)(\d{4})$/
|
|
}
|
|
|
|
// If '4242' + 4
|
|
if (re.test(value)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(value + ' ' + digit))
|
|
|
|
// If '424' + 2
|
|
} else if (re.test(value + digit)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(value + digit + ' '))
|
|
}
|
|
}
|
|
|
|
const formatBackCardNumber = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const value = $target.val()
|
|
|
|
// Return unless backspacing
|
|
if (e.which !== 8) {
|
|
return
|
|
}
|
|
|
|
// Return if focus isn't at the end of the text
|
|
if (
|
|
$target.prop('selectionStart') != null &&
|
|
$target.prop('selectionStart') !== value.length
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Remove the digit + trailing space
|
|
if (/\d\s$/.test(value)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(value.replace(/\d\s$/, '')))
|
|
// Remove digit if ends in space + digit
|
|
} else if (/\s\d?$/.test(value)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(value.replace(/\d$/, '')))
|
|
}
|
|
}
|
|
|
|
const getFormattedCardNumber = function(num) {
|
|
num = num.replace(/\D/g, '')
|
|
const card = ccUtils.fromNumber(num)
|
|
if (!card) {
|
|
return num
|
|
}
|
|
|
|
const upperLength = card.length[card.length.length - 1]
|
|
num = num.slice(0, upperLength)
|
|
|
|
if (card.format.global) {
|
|
return __guard__(num.match(card.format), x => x.join(' '))
|
|
} else {
|
|
let groups = card.format.exec(num)
|
|
if (groups == null) {
|
|
return
|
|
}
|
|
groups.shift()
|
|
groups = $.grep(groups, n => n) // Filter empty groups
|
|
return groups.join(' ')
|
|
}
|
|
}
|
|
|
|
const parseCardNumber = function(value) {
|
|
if (value != null) {
|
|
return value.replace(/\s/g, '')
|
|
} else {
|
|
return value
|
|
}
|
|
}
|
|
|
|
// Format Expiry
|
|
const reFormatExpiry = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
return setTimeout(function() {
|
|
let value = $target.val()
|
|
value = replaceFullWidthChars(value)
|
|
value = ccUtils.formatExpiry(value)
|
|
return safeVal(value, $target)
|
|
})
|
|
}
|
|
|
|
const formatExpiry = function(e) {
|
|
// Only format if input is a number
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
const $target = $(e.currentTarget)
|
|
const val = $target.val() + digit
|
|
|
|
if (/^\d$/.test(val) && !['0', '1'].includes(val)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(`0${val} / `))
|
|
} else if (/^\d\d$/.test(val)) {
|
|
e.preventDefault()
|
|
return setTimeout(function() {
|
|
// Split for months where we have the second digit > 2 (past 12) and turn
|
|
// that into (m1)(m2) => 0(m1) / (m2)
|
|
const m1 = parseInt(val[0], 10)
|
|
const m2 = parseInt(val[1], 10)
|
|
if (m2 > 2 && m1 !== 0) {
|
|
return $target.val(`0${m1} / ${m2}`)
|
|
} else {
|
|
return $target.val(`${val} / `)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
const formatForwardExpiry = function(e) {
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
const $target = $(e.currentTarget)
|
|
const val = $target.val()
|
|
|
|
if (/^\d\d$/.test(val)) {
|
|
return $target.val(`${val} / `)
|
|
}
|
|
}
|
|
|
|
const formatForwardSlash = function(e) {
|
|
const which = String.fromCharCode(e.which)
|
|
if (which !== '/' && which !== ' ') {
|
|
return
|
|
}
|
|
|
|
const $target = $(e.currentTarget)
|
|
const val = $target.val()
|
|
|
|
if (/^\d$/.test(val) && val !== '0') {
|
|
return $target.val(`0${val} / `)
|
|
}
|
|
}
|
|
|
|
const formatBackExpiry = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const value = $target.val()
|
|
|
|
// Return unless backspacing
|
|
if (e.which !== 8) {
|
|
return
|
|
}
|
|
|
|
// Return if focus isn't at the end of the text
|
|
if (
|
|
$target.prop('selectionStart') != null &&
|
|
$target.prop('selectionStart') !== value.length
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Remove the trailing space + last digit
|
|
if (/\d\s\/\s$/.test(value)) {
|
|
e.preventDefault()
|
|
return setTimeout(() => $target.val(value.replace(/\d\s\/\s$/, '')))
|
|
}
|
|
}
|
|
|
|
const parseExpiry = function(value) {
|
|
if (value != null) {
|
|
const dateAsObj = ccUtils.parseExpiry(value)
|
|
|
|
if (dateAsObj == null) {
|
|
return
|
|
}
|
|
|
|
const expiry = new Date(dateAsObj.year, dateAsObj.month - 1)
|
|
|
|
return $filter('date')(expiry, 'MM/yyyy')
|
|
}
|
|
}
|
|
|
|
// Format CVC
|
|
const reFormatCVC = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
return setTimeout(function() {
|
|
let value = $target.val()
|
|
value = replaceFullWidthChars(value)
|
|
value = value.replace(/\D/g, '').slice(0, 4)
|
|
return safeVal(value, $target)
|
|
})
|
|
}
|
|
|
|
// Restrictions
|
|
const restrictNumeric = function(e) {
|
|
// Key event is for a browser shortcut
|
|
if (e.metaKey || e.ctrlKey) {
|
|
return true
|
|
}
|
|
|
|
// If keycode is a space
|
|
if (e.which === 32) {
|
|
return false
|
|
}
|
|
|
|
// If keycode is a special char (WebKit)
|
|
if (e.which === 0) {
|
|
return true
|
|
}
|
|
|
|
// If char is a special char (Firefox)
|
|
if (e.which < 33) {
|
|
return true
|
|
}
|
|
|
|
const input = String.fromCharCode(e.which)
|
|
|
|
// Char is a number or a space
|
|
return !!/[\d\s]/.test(input)
|
|
}
|
|
|
|
const restrictCardNumber = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
if (hasTextSelected($target)) {
|
|
return
|
|
}
|
|
|
|
// Restrict number of digits
|
|
const value = ($target.val() + digit).replace(/\D/g, '')
|
|
const card = ccUtils.fromNumber(value)
|
|
|
|
if (card) {
|
|
return value.length <= card.length[card.length.length - 1]
|
|
} else {
|
|
// All other cards are 16 digits long
|
|
return value.length <= 16
|
|
}
|
|
}
|
|
|
|
const restrictExpiry = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
if (hasTextSelected($target)) {
|
|
return
|
|
}
|
|
|
|
let value = $target.val() + digit
|
|
value = value.replace(/\D/g, '')
|
|
|
|
if (value.length > 6) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const restrictCVC = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const digit = String.fromCharCode(e.which)
|
|
if (!/^\d+$/.test(digit)) {
|
|
return
|
|
}
|
|
|
|
if (hasTextSelected($target)) {
|
|
return
|
|
}
|
|
|
|
const val = $target.val() + digit
|
|
return val.length <= 4
|
|
}
|
|
|
|
const setCardType = function(e) {
|
|
const $target = $(e.currentTarget)
|
|
const val = $target.val()
|
|
const cardType = ccUtils.cardType(val) || 'unknown'
|
|
|
|
if (!$target.hasClass(cardType)) {
|
|
const allTypes = Array.from(cards).map(card => card.type)
|
|
|
|
$target.removeClass('unknown')
|
|
$target.removeClass(allTypes.join(' '))
|
|
|
|
$target.addClass(cardType)
|
|
$target.toggleClass('identified', cardType !== 'unknown')
|
|
return $target.trigger('payment.cardType', cardType)
|
|
}
|
|
}
|
|
|
|
return {
|
|
hasTextSelected,
|
|
replaceFullWidthChars,
|
|
reFormatNumeric,
|
|
reFormatCardNumber,
|
|
formatCardNumber,
|
|
formatBackCardNumber,
|
|
getFormattedCardNumber,
|
|
parseCardNumber,
|
|
reFormatExpiry,
|
|
formatExpiry,
|
|
formatForwardExpiry,
|
|
formatForwardSlash,
|
|
formatBackExpiry,
|
|
parseExpiry,
|
|
reFormatCVC,
|
|
restrictNumeric,
|
|
restrictCardNumber,
|
|
restrictExpiry,
|
|
restrictCVC,
|
|
setCardType
|
|
}
|
|
})
|
|
|
|
App.directive('ccFormatExpiry', ccFormat => ({
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link(scope, el, attrs, ngModel) {
|
|
el.on('keypress', ccFormat.restrictNumeric)
|
|
el.on('keypress', ccFormat.restrictExpiry)
|
|
el.on('keypress', ccFormat.formatExpiry)
|
|
el.on('keypress', ccFormat.formatForwardSlash)
|
|
el.on('keypress', ccFormat.formatForwardExpiry)
|
|
el.on('keydown', ccFormat.formatBackExpiry)
|
|
el.on('change', ccFormat.reFormatExpiry)
|
|
el.on('input', ccFormat.reFormatExpiry)
|
|
el.on('paste', ccFormat.reFormatExpiry)
|
|
|
|
ngModel.$parsers.push(ccFormat.parseExpiry)
|
|
return ngModel.$formatters.push(ccFormat.parseExpiry)
|
|
}
|
|
}))
|
|
|
|
App.directive('ccFormatCardNumber', ccFormat => ({
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link(scope, el, attrs, ngModel) {
|
|
el.on('keypress', ccFormat.restrictNumeric)
|
|
el.on('keypress', ccFormat.restrictCardNumber)
|
|
el.on('keypress', ccFormat.formatCardNumber)
|
|
el.on('keydown', ccFormat.formatBackCardNumber)
|
|
el.on('paste', ccFormat.reFormatCardNumber)
|
|
|
|
ngModel.$parsers.push(ccFormat.parseCardNumber)
|
|
return ngModel.$formatters.push(ccFormat.getFormattedCardNumber)
|
|
}
|
|
}))
|
|
|
|
export default App.directive('ccFormatSecCode', ccFormat => ({
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link(scope, el, attrs, ngModel) {
|
|
el.on('keypress', ccFormat.restrictNumeric)
|
|
el.on('keypress', ccFormat.restrictCVC)
|
|
el.on('paste', ccFormat.reFormatCVC)
|
|
el.on('change', ccFormat.reFormatCVC)
|
|
return el.on('input', ccFormat.reFormatCVC)
|
|
}
|
|
}))
|
|
|
|
function __guard__(value, transform) {
|
|
return typeof value !== 'undefined' && value !== null
|
|
? transform(value)
|
|
: undefined
|
|
}
|