Use TextMask on CurrencyInput
This commit is contained in:
parent
70e82776e0
commit
a5b3d5d198
14 changed files with 566 additions and 53 deletions
|
@ -8,6 +8,8 @@ import {HoudiniField} from "../../lib/houdini_form";
|
|||
import ReactInput from "./form/ReactInput";
|
||||
import ReactSelect from './form/ReactSelect';
|
||||
import ReactTextarea from "./form/ReactTextarea";
|
||||
import ReactMaskedInput from "./form/ReactMaskedInput";
|
||||
import createNumberMask from "../../lib/createNumberMask";
|
||||
|
||||
|
||||
export const BasicField = observer((props:{field:Field, placeholder?:string, label?:string, wrapperClassName?:string, inputClassNames?:string}) =>{
|
||||
|
@ -44,17 +46,26 @@ export const TextareaField = observer((props:{field:Field, placeholder?:string,
|
|||
</LabeledFieldComponent>
|
||||
})
|
||||
|
||||
export const CurrencyField = observer((props:{field:Field,placeholder?:string, label?:string, currencySymbol?:string, wrapperClassName?:string, inputClassNames?:string}) => {
|
||||
export const CurrencyField = observer((props:{field:Field,placeholder?:string, label?:string, currencySymbol?:string, wrapperClassName?:string, inputClassNames?:string, mustBeNegative?:boolean, allowNegative?:boolean}) => {
|
||||
let field = props.field as HoudiniField
|
||||
let currencySymbolId = props.field.id + "_____currency_symbol"
|
||||
let currencySymbol = props.mustBeNegative ? "-$" : "$"
|
||||
let allowNegative = props.allowNegative || !props.mustBeNegative
|
||||
return <LabeledFieldComponent
|
||||
inputId={props.field.id} labelText={field.label} inError={field.hasError} error={field.error}
|
||||
inStickyError={field.hasServerError} stickyError={field.serverError}
|
||||
className={props.wrapperClassName} >
|
||||
<div className="input-group">
|
||||
<span className="input-group-addon" id={currencySymbolId}>{props.currencySymbol}</span>
|
||||
<ReactInput field={field} label={props.label} placeholder={props.placeholder} className={`form-control ${props.inputClassNames}`} aria-describedby={currencySymbolId}/>
|
||||
</div>
|
||||
|
||||
<ReactMaskedInput field={field} label={props.label} placeholder={props.placeholder}
|
||||
className={`form-control ${props.inputClassNames}`} guide={true}
|
||||
mask={createNumberMask({allowDecimal:true,
|
||||
requireDecimal:true,
|
||||
prefix:currencySymbol,
|
||||
allowNegative:allowNegative,
|
||||
fixedDecimalScale:true
|
||||
})}
|
||||
showMask={true} placeholderChar={'0'}
|
||||
/>
|
||||
|
||||
</LabeledFieldComponent>
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import * as React from 'react';
|
||||
import 'jest';
|
||||
import ReactMaskedInput from './ReactMaskedInput'
|
||||
|
||||
describe('ReactMaskedInput', () => {
|
||||
test('your test here', () => {
|
||||
expect(false).toBe(true)
|
||||
})
|
||||
})
|
81
javascripts/src/components/common/form/ReactMaskedInput.tsx
Normal file
81
javascripts/src/components/common/form/ReactMaskedInput.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import {ReactInputProps} from "./react_input_props";
|
||||
import {InputHTMLAttributes} from "react";
|
||||
import {action, observable} from "mobx";
|
||||
import {Field} from "mobx-react-form";
|
||||
import {castToNullIfUndef} from "../../../lib/utils";
|
||||
import MaskedInput, {maskArray} from "react-text-mask";
|
||||
|
||||
type InputTypes = ReactInputProps &
|
||||
InputHTMLAttributes<HTMLInputElement> & {
|
||||
mask?: maskArray | ((value: string) => maskArray);
|
||||
|
||||
guide?: boolean;
|
||||
|
||||
placeholderChar?: string;
|
||||
|
||||
keepCharPositions?: boolean;
|
||||
|
||||
pipe?: (
|
||||
conformedValue: string,
|
||||
config: any
|
||||
) => false | string | { value: string; indexesOfPipedChars: number[] };
|
||||
|
||||
showMask?: boolean;
|
||||
}
|
||||
|
||||
class ReactMaskedInput extends React.Component<InputTypes, {}> {
|
||||
|
||||
constructor(props:InputTypes){
|
||||
super(props)
|
||||
}
|
||||
|
||||
@observable
|
||||
field:Field
|
||||
|
||||
|
||||
@action.bound
|
||||
componentWillMount(){
|
||||
|
||||
this.field = this.props.field
|
||||
|
||||
|
||||
this.updateProps()
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<InputTypes>, prevState: Readonly<{}>): void {
|
||||
this.updateProps()
|
||||
}
|
||||
|
||||
@action.bound
|
||||
updateProps() {
|
||||
this.field.set('label', castToNullIfUndef(this.props.label))
|
||||
this.field.set('placeholder', castToNullIfUndef(this.props.placeholder))
|
||||
}
|
||||
|
||||
///Removes the properties we don't want to put into the input element
|
||||
@action.bound
|
||||
winnowProps(): InputTypes {
|
||||
let ourProps = {...this.props}
|
||||
delete ourProps.field
|
||||
delete ourProps.value
|
||||
return ourProps
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
return <MaskedInput {...this.winnowProps()} {...this.field.bind()}/>
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(ReactMaskedInput)
|
||||
|
||||
|
||||
|
||||
|
|
@ -7,11 +7,3 @@ exports[`ReactInput gets removed properly 1`] = `
|
|||
/>
|
||||
</form>
|
||||
`;
|
||||
|
||||
exports[`ReactInput no children passed in gets removed properly 1`] = `
|
||||
<form>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</form>
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import {Field} from "mobx-react-form";
|
||||
|
||||
export interface ReactInputProps
|
||||
|
|
|
@ -19,12 +19,14 @@ import {TwoColumnFields} from "../common/layout";
|
|||
import {Validations} from "../../lib/vjf_rules";
|
||||
import _ = require("lodash");
|
||||
import {Dedication, parseDedication, serializeDedication} from '../../lib/dedication';
|
||||
import blacklist = require("validator/lib/blacklist");
|
||||
import {createFieldDefinition} from "../../lib/mobx_utils";
|
||||
|
||||
|
||||
interface Charge {
|
||||
status: string
|
||||
}
|
||||
|
||||
|
||||
interface RecurringDonation {
|
||||
interval?: number
|
||||
time_unit?: string
|
||||
|
@ -79,7 +81,6 @@ export interface FundraiserInfo {
|
|||
|
||||
class EditPaymentPanelForm extends HoudiniForm {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -160,9 +161,7 @@ class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedInt
|
|||
|
||||
}
|
||||
|
||||
createDefinition<TInputType>(fieldDef:FieldDefinition<TInputType>) : FieldDefinition<TInputType> {
|
||||
return fieldDef;
|
||||
}
|
||||
|
||||
|
||||
@action.bound
|
||||
loadFormFromData() {
|
||||
|
@ -171,18 +170,24 @@ class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedInt
|
|||
let params: {[name:string]:FieldDefinition} = {
|
||||
'event': {name: 'event', label: 'Event', value: eventId},
|
||||
'campaign': {name: 'campaign', label: 'Campaign', value: campaignId},
|
||||
'gross_amount': {name: 'gross_amount', label: 'Gross Amount', value: centsToDollars(this.props.data.gross_amount)},
|
||||
'fee_total': {name: 'fee_total', label: 'Fees', value: centsToDollars(this.props.data.fee_total)},
|
||||
'date': this.createDefinition({name: 'date', label: 'Date',
|
||||
'gross_amount': createFieldDefinition({name: 'gross_amount', label: 'Gross Amount', value: this.props.data.gross_amount,
|
||||
input: (amount:number) => centsToDollars(amount),
|
||||
output: (dollarString:string) => parseFloat(blacklist(dollarString, '$,'))
|
||||
}),
|
||||
'fee_total': createFieldDefinition({name: 'fee_total', label: 'Fees', value: this.props.data.fee_total,
|
||||
input: (amount:number) => centsToDollars(amount),
|
||||
output: (dollarString:string) => parseFloat(blacklist(dollarString, '$,'))
|
||||
}),
|
||||
'date': createFieldDefinition({name: 'date', label: 'Date',
|
||||
value: this.props.data.date,
|
||||
input: (isoTime:string) => this.nonprofitTimezonedDates.readable_date(isoTime),
|
||||
output:(date:string) => this.nonprofitTimezonedDates.readable_date_time_to_iso(date)}),
|
||||
'dedication': {name: 'dedication', label: 'Dedication', fields: [
|
||||
this.createDefinition({name:'type', label: 'Dedication Type', value: this.dedication.type}),
|
||||
this.createDefinition({name: 'supporter_id', type: 'hidden', value: this.dedication.supporter_id}),
|
||||
this.createDefinition({name:'name', label:'Person dedicated for', value: this.dedication.name}),
|
||||
this.createDefinition({name: 'contact', type: 'hidden', value: this.dedication.contact}),
|
||||
this.createDefinition({name: 'note', value: this.dedication.note})
|
||||
createFieldDefinition({name:'type', label: 'Dedication Type', value: this.dedication.type}),
|
||||
createFieldDefinition({name: 'supporter_id', type: 'hidden', value: this.dedication.supporter_id}),
|
||||
createFieldDefinition({name:'name', label:'Person dedicated for', value: this.dedication.name}),
|
||||
createFieldDefinition({name: 'contact', type: 'hidden', value: this.dedication.contact}),
|
||||
createFieldDefinition({name: 'note', value: this.dedication.note})
|
||||
]},
|
||||
'designation': {name: 'designation', label: 'Designation', value: this.props.data.donation.designation},
|
||||
'comment': {name: 'comment', label: 'Note', value: this.props.data.donation.comment}
|
||||
|
@ -201,7 +206,7 @@ class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedInt
|
|||
|
||||
return new EditPaymentPanelForm({fields: _.values(params)}, {
|
||||
hooks: {
|
||||
onSubmit: async (e: Field) => {
|
||||
onSuccess: async (e: Field) => {
|
||||
await this.updateDonation()
|
||||
}
|
||||
}
|
||||
|
@ -346,7 +351,7 @@ class EditPaymentPane extends React.Component<EditPaymentPaneProps & InjectedInt
|
|||
|
||||
<TwoColumnFields>
|
||||
<CurrencyField field={this.form.$('gross_amount')} label={"Gross Amount"} currencySymbol={"$"}/>
|
||||
<CurrencyField field={this.form.$('fee_total')} label={"Processing Fees"}/>
|
||||
<CurrencyField field={this.form.$('fee_total')} label={"Processing Fees"} mustBeNegative={true}/>
|
||||
|
||||
</TwoColumnFields>
|
||||
|
||||
|
|
236
javascripts/src/lib/createNumberMask.spec.ts
Normal file
236
javascripts/src/lib/createNumberMask.spec.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
//from: https://github.com/text-mask/text-mask/pull/760/commits/f66c81b62c4894b7da43862bee5943f659dc7537
|
||||
// under: Unlicense
|
||||
import createNumberMask from './createNumberMask'
|
||||
describe('createNumberMask', () => {
|
||||
it('can returns a configured currency mask', () => {
|
||||
let numberMask = createNumberMask()
|
||||
|
||||
expect(typeof numberMask).toBe('function')
|
||||
})
|
||||
|
||||
it('takes a prefix', () => {
|
||||
let numberMask = createNumberMask({prefix: '$'})
|
||||
|
||||
expect(numberMask('12')).toEqual(['$', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('takes a suffix', () => {
|
||||
let numberMask = createNumberMask({suffix: ' $', prefix: ''})
|
||||
|
||||
expect(numberMask('12')).toEqual([/\d/, /\d/, ' ', '$'])
|
||||
})
|
||||
|
||||
it('works when the prefix contains numbers', () => {
|
||||
let numberMask = createNumberMask({prefix: 'm2 '})
|
||||
|
||||
expect(numberMask('m2 1')).toEqual(['m', '2', ' ', /\d/])
|
||||
})
|
||||
|
||||
it('works when the suffix contains numbers', () => {
|
||||
let numberMask = createNumberMask({prefix: '', suffix: ' m2'})
|
||||
|
||||
expect(numberMask('1 m2')).toEqual([/\d/, ' ', 'm', '2'])
|
||||
})
|
||||
|
||||
it('works when there is a decimal and the suffix contains numbers', () => {
|
||||
let numberMask = createNumberMask({prefix: '', suffix: ' m2', allowDecimal: true})
|
||||
|
||||
expect(numberMask('1.2 m2')).toEqual([/\d/, '[]', '.', '[]', /\d/, ' ', 'm', '2'])
|
||||
})
|
||||
|
||||
it('can be configured to add a thousands separator or not', () => {
|
||||
let numberMaskWithoutThousandsSeparator = createNumberMask({includeThousandsSeparator: false})
|
||||
expect(numberMaskWithoutThousandsSeparator('1000')).toEqual(['$', /\d/, /\d/, /\d/, /\d/])
|
||||
|
||||
let numberMaskWithThousandsSeparator = createNumberMask()
|
||||
expect(numberMaskWithThousandsSeparator('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('can be configured with a custom character for the thousands separator', () => {
|
||||
let numberMask = createNumberMask({thousandsSeparatorSymbol: '.'})
|
||||
|
||||
expect(numberMask('1000')).toEqual(['$', /\d/, '.', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('can be configured to accept a fraction and returns the fraction separator with caret traps', () => {
|
||||
let numberMask = createNumberMask({allowDecimal: true})
|
||||
|
||||
expect(numberMask('1000.')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]'])
|
||||
})
|
||||
|
||||
it('rejects fractions by default', () => {
|
||||
let numberMask = createNumberMask()
|
||||
|
||||
expect(numberMask('1000.')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('can be configured with a custom character for the fraction separator', () => {
|
||||
let numberMask = createNumberMask({
|
||||
allowDecimal: true,
|
||||
decimalSymbol: ',',
|
||||
thousandsSeparatorSymbol: '.'
|
||||
})
|
||||
|
||||
expect(numberMask('1000,')).toEqual(['$', /\d/, '.', /\d/, /\d/, /\d/, '[]', ',', '[]'])
|
||||
})
|
||||
|
||||
it('can limit the length of the fraction', () => {
|
||||
let numberMask = createNumberMask({allowDecimal: true, decimalLimit: 2})
|
||||
|
||||
expect(numberMask('1000.3823')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('can require a fraction', () => {
|
||||
let numberMask = createNumberMask({requireDecimal: true})
|
||||
|
||||
expect(numberMask('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/, '[]', '.', '[]'])
|
||||
})
|
||||
|
||||
it('accepts negative integers', function() {
|
||||
let numberMask = createNumberMask({allowNegative: true})
|
||||
expect(numberMask('-$12')).toEqual([/-/, '$', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('ignores multiple minus signs', function() {
|
||||
let numberMask = createNumberMask({allowNegative: true})
|
||||
expect(numberMask('--$12')).toEqual([/-/, '$', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('adds a digit placeholder if the input is nothing but a minus sign in order to attract the caret', () => {
|
||||
let numberMask = createNumberMask({allowNegative: true})
|
||||
expect(numberMask('-')).toEqual([/-/, '$', /\d/])
|
||||
})
|
||||
|
||||
it('starts with dot should be considered as decimal input', () => {
|
||||
let numberMask = createNumberMask({prefix: '$', allowDecimal: true})
|
||||
expect(numberMask('.')).toEqual(['$', '0', '.', /\d/])
|
||||
|
||||
numberMask = createNumberMask({prefix: '#', allowDecimal: true})
|
||||
expect(numberMask('.')).toEqual(['#', '0', '.', /\d/])
|
||||
|
||||
numberMask = createNumberMask({prefix: '', allowDecimal: true})
|
||||
expect(numberMask('.')).toEqual(['0', '.', /\d/])
|
||||
|
||||
numberMask = createNumberMask({allowDecimal: false})
|
||||
expect(numberMask('.')).toEqual(['$'])
|
||||
|
||||
numberMask = createNumberMask({prefix: '', suffix: '$', allowDecimal: true})
|
||||
expect(numberMask('.')).toEqual(['0', '.', /\d/, '$'])
|
||||
})
|
||||
|
||||
it('can allow leading zeroes', function() {
|
||||
let numberMask = createNumberMask({allowLeadingZeroes: true})
|
||||
expect(numberMask('012')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works with large numbers when leading zeroes is false', function() {
|
||||
let numberMask = createNumberMask({allowLeadingZeroes: false})
|
||||
expect(numberMask('111111111111111111111111')).toEqual([
|
||||
'$', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',',
|
||||
/\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/
|
||||
])
|
||||
})
|
||||
|
||||
describe('integer limiting', () => {
|
||||
it('can limit the length of the integer part', () => {
|
||||
let numberMask = createNumberMask({integerLimit: 3})
|
||||
expect(numberMask('1999')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works when there is a prefix', () => {
|
||||
let numberMask = createNumberMask({integerLimit: 3, prefix: '$'})
|
||||
expect(numberMask('$1999')).toEqual(['$', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works when there is a thousands separator', () => {
|
||||
expect(createNumberMask({integerLimit: 4, prefix: ''})('1,9995'))
|
||||
.toEqual([/\d/, ',', /\d/, /\d/, /\d/])
|
||||
|
||||
expect(createNumberMask({integerLimit: 7, prefix: ''})('1,000,0001'))
|
||||
.toEqual([/\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works when there is a decimal and a prefix', () => {
|
||||
let numberMask = createNumberMask({integerLimit: 3, allowDecimal: true})
|
||||
expect(numberMask('$199.34')).toEqual(['$', /\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works when there is a decimal and no prefix', () => {
|
||||
let numberMask = createNumberMask({integerLimit: 3, allowDecimal: true, prefix: ''})
|
||||
expect(numberMask('199.34')).toEqual([/\d/, /\d/, /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('works when thousandsSeparatorSymbol is a period', () => {
|
||||
let numberMask = createNumberMask({
|
||||
prefix: '',
|
||||
thousandsSeparatorSymbol: '.',
|
||||
decimalSymbol: ',',
|
||||
allowDecimal: true,
|
||||
requireDecimal: true,
|
||||
integerLimit: 5,
|
||||
decimalLimit: 3,
|
||||
})
|
||||
expect(numberMask('1234567890,12345678'))
|
||||
.toEqual([/\d/, /\d/, '.', /\d/, /\d/, /\d/, '[]', ',', '[]', /\d/, /\d/, /\d/])
|
||||
})
|
||||
})
|
||||
|
||||
describe('numberMask default behavior', () => {
|
||||
let numberMask:ReturnType<typeof createNumberMask> = null
|
||||
|
||||
beforeEach(() => {
|
||||
numberMask = createNumberMask()
|
||||
})
|
||||
|
||||
it('returns a mask that has the same number of digits as the given number', () => {
|
||||
expect(numberMask('20382')).toEqual(['$', /\d/, /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('uses the dollar symbol as the default prefix', () => {
|
||||
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||
})
|
||||
|
||||
it('adds no suffix by default', () => {
|
||||
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||
})
|
||||
|
||||
it('returns a mask that appends the currency symbol', () => {
|
||||
expect(numberMask('1')).toEqual(['$', /\d/])
|
||||
})
|
||||
|
||||
it('adds adds a comma after a thousand', () => {
|
||||
expect(numberMask('1000')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('adds as many commas as needed', () => {
|
||||
expect(numberMask('23984209342084'))
|
||||
.toEqual(
|
||||
['$', /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/, ',', /\d/, /\d/, /\d/]
|
||||
)
|
||||
})
|
||||
|
||||
it('accepts any string and strips out any non-digit characters', () => {
|
||||
expect(numberMask('h4x0r sp43k')).toEqual(['$', /\d/, ',', /\d/, /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('does not allow leading zeroes', function() {
|
||||
let numberMask = createNumberMask()
|
||||
expect(numberMask('012')).toEqual(['$', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('allows one leading zero followed by a fraction', function() {
|
||||
let numberMask = createNumberMask({allowDecimal: true})
|
||||
expect(numberMask('0.12')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||
})
|
||||
|
||||
it('ignores fixedDecimalScale when requireDecimal:false', () => {
|
||||
let numberMask = createNumberMask({allowDecimal: true, fixedDecimalScale:true})
|
||||
expect(numberMask('0.1')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/])
|
||||
})
|
||||
|
||||
it('fixedDecimalScale expands decimal', () => {
|
||||
let numberMask = createNumberMask({requireDecimal: true, fixedDecimalScale:true})
|
||||
expect(numberMask('0.1')).toEqual(['$', /\d/, '[]', '.', '[]', /\d/, /\d/])
|
||||
})
|
||||
})
|
||||
})
|
184
javascripts/src/lib/createNumberMask.ts
Normal file
184
javascripts/src/lib/createNumberMask.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
//from: https://github.com/text-mask/text-mask/pull/760/commits/f66c81b62c4894b7da43862bee5943f659dc7537
|
||||
// under: Unlicense
|
||||
const dollarSign = '$'
|
||||
const emptyString = ''
|
||||
const comma = ','
|
||||
const period = '.'
|
||||
const minus = '-'
|
||||
const minusRegExp = /-/
|
||||
const nonDigitsRegExp = /\D+/g
|
||||
const number = 'number'
|
||||
const digitRegExp = /\d/
|
||||
const caretTrap = '[]'
|
||||
|
||||
|
||||
interface NumberMaskProps {
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
includeThousandsSeparator?: boolean
|
||||
thousandsSeparatorSymbol?: string
|
||||
allowDecimal?:boolean
|
||||
decimalSymbol?: string
|
||||
decimalLimit?: number
|
||||
integerLimit?: number
|
||||
requireDecimal?: boolean
|
||||
allowNegative?: boolean
|
||||
allowLeadingZeroes?: boolean
|
||||
fixedDecimalScale?:boolean
|
||||
alwaysNegative?: boolean
|
||||
}
|
||||
|
||||
export default function createNumberMask({
|
||||
prefix = dollarSign,
|
||||
suffix = emptyString,
|
||||
includeThousandsSeparator = true,
|
||||
thousandsSeparatorSymbol = comma,
|
||||
allowDecimal = false,
|
||||
decimalSymbol = period,
|
||||
decimalLimit = 2,
|
||||
requireDecimal = false,
|
||||
allowNegative = false,
|
||||
allowLeadingZeroes = false,
|
||||
fixedDecimalScale = false,
|
||||
integerLimit = null,
|
||||
alwaysNegative = false
|
||||
}:NumberMaskProps = {}) {
|
||||
const prefixLength = prefix && prefix.length || 0
|
||||
const suffixLength = suffix && suffix.length || 0
|
||||
const thousandsSeparatorSymbolLength = thousandsSeparatorSymbol && thousandsSeparatorSymbol.length || 0
|
||||
|
||||
function numberMask(rawValue = emptyString) {
|
||||
const rawValueLength = rawValue.length
|
||||
|
||||
if (
|
||||
rawValue === emptyString ||
|
||||
(rawValue[0] === prefix[0] && rawValueLength === 1)
|
||||
) {
|
||||
return stringMaskArray(prefix.split(emptyString)).concat([digitRegExp]).concat(suffix.split(emptyString))
|
||||
} else if (
|
||||
rawValue === decimalSymbol &&
|
||||
allowDecimal
|
||||
) {
|
||||
return stringMaskArray(prefix.split(emptyString)).concat(['0', decimalSymbol, digitRegExp]).concat(suffix.split(emptyString))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const isNegative = ((rawValue[0] === minus) && allowNegative)
|
||||
//If negative remove "-" sign
|
||||
if (isNegative) {
|
||||
rawValue = rawValue.toString().substr(1)
|
||||
}
|
||||
|
||||
const indexOfLastDecimal = rawValue.lastIndexOf(decimalSymbol)
|
||||
const hasDecimal = indexOfLastDecimal !== -1
|
||||
|
||||
let integer
|
||||
let fraction
|
||||
let mask
|
||||
|
||||
// remove the suffix
|
||||
if (rawValue.slice(suffixLength * -1) === suffix) {
|
||||
rawValue = rawValue.slice(0, suffixLength * -1)
|
||||
}
|
||||
|
||||
if (hasDecimal && (allowDecimal || requireDecimal)) {
|
||||
integer = rawValue.slice(rawValue.slice(0, prefixLength) === prefix ? prefixLength : 0, indexOfLastDecimal)
|
||||
|
||||
fraction = rawValue.slice(indexOfLastDecimal + 1, rawValueLength)
|
||||
fraction = convertToMask(fraction.replace(nonDigitsRegExp, emptyString))
|
||||
} else {
|
||||
if (rawValue.slice(0, prefixLength) === prefix) {
|
||||
integer = rawValue.slice(prefixLength)
|
||||
} else {
|
||||
integer = rawValue
|
||||
}
|
||||
}
|
||||
|
||||
if (integerLimit && typeof integerLimit === number) {
|
||||
const thousandsSeparatorRegex = thousandsSeparatorSymbol === '.' ? '[.]' : `${thousandsSeparatorSymbol}`
|
||||
const numberOfThousandSeparators = (integer.match(new RegExp(thousandsSeparatorRegex, 'g')) || []).length
|
||||
|
||||
integer = integer.slice(0, integerLimit + (numberOfThousandSeparators * thousandsSeparatorSymbolLength))
|
||||
}
|
||||
|
||||
integer = integer.replace(nonDigitsRegExp, emptyString)
|
||||
|
||||
if (!allowLeadingZeroes) {
|
||||
integer = integer.replace(/^0+(0$|[^0])/, '$1')
|
||||
}
|
||||
|
||||
integer = (includeThousandsSeparator) ? addThousandsSeparator(integer, thousandsSeparatorSymbol) : integer
|
||||
|
||||
mask = convertToMask(integer)
|
||||
|
||||
if ((hasDecimal && allowDecimal) || requireDecimal === true) {
|
||||
if (rawValue[indexOfLastDecimal - 1] !== decimalSymbol) {
|
||||
mask.push(caretTrap)
|
||||
}
|
||||
|
||||
mask.push(decimalSymbol, caretTrap)
|
||||
|
||||
if (fraction) {
|
||||
if (typeof decimalLimit === number) {
|
||||
fraction = fraction.slice(0, decimalLimit)
|
||||
}
|
||||
mask = mask.concat(fraction)
|
||||
}
|
||||
|
||||
if (requireDecimal === true) {
|
||||
if (fixedDecimalScale === true) {
|
||||
const decimalLimitRemaining = fraction ? decimalLimit - fraction.length : decimalLimit
|
||||
for (var i = 0; i < decimalLimitRemaining; i++) {
|
||||
mask.push(digitRegExp)
|
||||
}
|
||||
} else if (rawValue[indexOfLastDecimal - 1] === decimalSymbol) {
|
||||
mask.push(digitRegExp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prefixLength > 0) {
|
||||
let res:(string|RegExp)[] = prefix.split(emptyString)
|
||||
mask = res.concat(mask)
|
||||
}
|
||||
|
||||
if (isNegative) {
|
||||
// If user is entering a negative number, add a mask placeholder spot to attract the caret to it.
|
||||
if (mask.length === prefixLength) {
|
||||
mask.push(digitRegExp)
|
||||
}
|
||||
|
||||
mask = stringMaskArray([minusRegExp]).concat(mask)
|
||||
}
|
||||
|
||||
if (suffix.length > 0) {
|
||||
mask = mask.concat(suffix.split(emptyString))
|
||||
}
|
||||
|
||||
return mask
|
||||
}
|
||||
|
||||
|
||||
|
||||
return numberMask
|
||||
}
|
||||
|
||||
function stringMaskArray(i:(string|(string|RegExp)[])):(string|RegExp)[] {
|
||||
if (typeof i === 'string'){
|
||||
return [i]
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
function convertToMask(strNumber:string):(string|RegExp)[] {
|
||||
return strNumber
|
||||
.split(emptyString)
|
||||
.map((char) => digitRegExp.test(char) ? digitRegExp : char)
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/a/10899795/604296
|
||||
function addThousandsSeparator(n:string, thousandsSeparatorSymbol:string) {
|
||||
return n.replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparatorSymbol)
|
||||
}
|
5
javascripts/src/lib/mobx_utils.ts
Normal file
5
javascripts/src/lib/mobx_utils.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {FieldDefinition} from "mobx-react-form";
|
||||
|
||||
export function createFieldDefinition<TInOut>(fieldDef:FieldDefinition<TInOut>) : FieldDefinition<TInOut> {
|
||||
return fieldDef;
|
||||
}
|
|
@ -55,7 +55,7 @@ export class Validations {
|
|||
{
|
||||
return ({field, validator}:ValidationInput) => {
|
||||
return [
|
||||
parseFloat(field.value) >= value,
|
||||
parseFloat(field.get('value')) >= value,
|
||||
`${field.label} must be at least ${value}`
|
||||
]
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export class Validations {
|
|||
static isLessThanOrEqualTo(value:number, flip:boolean=false) : ({field, validator}:ValidationInput) => StringBoolTuple
|
||||
{
|
||||
return ({field, validator}:ValidationInput) => {
|
||||
let float = parseFloat(field.value)
|
||||
let float = field.get('value')
|
||||
return [
|
||||
(flip ? -1 * float : float) <= value,
|
||||
`${field.label} must be no more than ${value}`
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -199,6 +199,15 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-text-mask": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-text-mask/-/react-text-mask-5.4.2.tgz",
|
||||
"integrity": "sha512-R4h07wAeOPh6xc6E9qYPMgYpeuUJ1w3rs3oWwp9oWIVPtnAGxPGDuCPEs2+ynlP5syeM7heZeZeM8saAHRgENA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/sinon": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"@types/react-dom": "^16.0.5",
|
||||
"@types/react-intl": "^2.3.7",
|
||||
"@types/react-test-renderer": "^16.0.1",
|
||||
"@types/react-text-mask": "^5.4.2",
|
||||
"@types/sinon": "^4.3.3",
|
||||
"@types/validator": "^9.4.1",
|
||||
"babel-core": "^6.26.0",
|
||||
|
|
20
types/react-text-mask/index.d.ts
vendored
20
types/react-text-mask/index.d.ts
vendored
|
@ -1,20 +0,0 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
import {Component} from 'react'
|
||||
|
||||
|
||||
export interface MaskedInputProps
|
||||
{
|
||||
mask: Array<any>|Function|Boolean | {mask: Array<any> | Function, pipe: Function}
|
||||
|
||||
guide?: Boolean
|
||||
value?: String| Number,
|
||||
pipe?: Function,
|
||||
placeholderChar?: String,
|
||||
keepCharPositions?: Boolean,
|
||||
showMask?: Boolean,
|
||||
[additionalProps: string] : any
|
||||
}
|
||||
|
||||
export class MaskedInput extends Component<MaskedInputProps, {}> {
|
||||
|
||||
}
|
2
types/text-mask/index.d.ts
vendored
2
types/text-mask/index.d.ts
vendored
|
@ -1,2 +0,0 @@
|
|||
// License: LGPL-3.0-or-later
|
||||
declare module "text-mask"
|
Loading…
Reference in a new issue