diff --git a/.size-snapshot.json b/.size-snapshot.json index b6d14a3..a3c2170 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,23 +1,23 @@ { "dist/rifm.umd.js": { - "bundled": 7480, - "minified": 1724, - "gzipped": 858 + "bundled": 8097, + "minified": 1984, + "gzipped": 973 }, "dist/rifm.min.js": { - "bundled": 7266, + "bundled": 7276, "minified": 1583, - "gzipped": 780 + "gzipped": 783 }, "dist/rifm.cjs.js": { - "bundled": 7008, - "minified": 1650, - "gzipped": 831 + "bundled": 7780, + "minified": 2016, + "gzipped": 961 }, "dist/rifm.esm.js": { - "bundled": 6937, - "minified": 1587, - "gzipped": 787, + "bundled": 7709, + "minified": 1953, + "gzipped": 917, "treeshaked": { "rollup": { "code": 14, @@ -29,9 +29,9 @@ } }, "dist/rifm.esm.production.js": { - "bundled": 6651, + "bundled": 6655, "minified": 1372, - "gzipped": 664, + "gzipped": 667, "treeshaked": { "rollup": { "code": 14, diff --git a/pages/case-enforcement/index.js b/pages/case-enforcement/index.js index 04bbae3..2381036 100644 --- a/pages/case-enforcement/index.js +++ b/pages/case-enforcement/index.js @@ -9,6 +9,7 @@ const Example = () /*:React.Node*/ => { const [uppercase, setUppercase] = React.useState(''); const [capitalized, setCapitalized] = React.useState(''); const [latinLetters, setLatinLetters] = React.useState(''); + const [comment, setComment] = React.useState(''); return ( @@ -16,7 +17,8 @@ const Example = () /*:React.Node*/ => {
Lower case
v.toLowerCase()} + format={v => v} + replace={v => v.toLowerCase()} value={lowercase} onChange={setLowercase} > @@ -28,7 +30,8 @@ const Example = () /*:React.Node*/ => {
Upper case
v.toUpperCase()} + format={v => v} + replace={v => v.toUpperCase()} value={uppercase} onChange={setUppercase} > @@ -40,7 +43,8 @@ const Example = () /*:React.Node*/ => {
Capital first letter
v.slice(0, 1).toUpperCase() + v.slice(1).toLowerCase()} + format={v => v} + replace={v => v.slice(0, 1).toUpperCase() + v.slice(1).toLowerCase()} value={capitalized} onChange={setCapitalized} > @@ -59,6 +63,23 @@ const Example = () /*:React.Node*/ => { {renderInput} + +
+
Leave a comment about Rifm
+ v} + replace={v => + 'Rifm is the best mask and formatting library. I love it! ' + .repeat(20) + .slice(0, v.length) + } + value={comment} + onChange={setComment} + > + {renderInput} + +
); }; diff --git a/src/Rifm.js b/src/Rifm.js index 37ebccc..e7bc475 100644 --- a/src/Rifm.js +++ b/src/Rifm.js @@ -7,6 +7,7 @@ type Props = {| onChange: string => void, format: (str: string) => string, mask?: boolean, + replace?: string => string, accept?: RegExp, children: ({ value: string, @@ -19,7 +20,10 @@ type Props = {| export const Rifm = (props: Props) => { const [, refresh] = React.useReducer(c => c + 1, 0); const valueRef = React.useRef(null); - const userValue = props.format(props.value); + const { replace } = props; + const userValue = replace + ? replace(props.format(props.value)) + : props.format(props.value); // state of delete button see comments below about inputType support const isDeleleteButtonDownRef = React.useRef(false); @@ -46,6 +50,18 @@ export const Rifm = (props: Props) => { userValue === props.format(eventValue), // isNoOperation ]; + if (process.env.NODE_ENV !== 'production') { + const formattedEventValue = props.format(eventValue); + if ( + eventValue !== formattedEventValue && + eventValue.toLowerCase() === formattedEventValue.toLowerCase() + ) { + console.warn( + 'Case enforcement does not work with format. Please use replace={value => value.toLowerCase()} instead' + ); + } + } + // The main trick is to update underlying input with non formatted value (= eventValue) // that allows us to calculate right cursor position after formatting (see getCursorPosition) // then we format new value and call props.onChange with masked/formatted value @@ -78,7 +94,7 @@ export const Rifm = (props: Props) => { const valueBeforeSelectionStart = clean( eventValue.substr(0, input.selectionStart) - ).toLowerCase(); + ); // trying to find cursor position in formatted value having knowledge about valueBeforeSelectionStart // This works because we assume that format doesn't change the order of accepted symbols. @@ -87,20 +103,16 @@ export const Rifm = (props: Props) => { // inputValue = 1'23'|4 so valueBeforeSelectionStart = 123 and formatted value = 1'2'3'4 // calling getCursorPosition("1'2'3'4") will give us position after 3, 1'2'3|'4 // so for formatting just this function to determine cursor position after formatting is enough - // with masking we need to do some additional checks see `replace` below + // with masking we need to do some additional checks see `mask` below const getCursorPosition = val => { let start = 0; let cleanPos = 0; for (let i = 0; i !== valueBeforeSelectionStart.length; ++i) { - let newPos = - val.toLowerCase().indexOf(valueBeforeSelectionStart[i], start) + 1; + let newPos = val.indexOf(valueBeforeSelectionStart[i], start) + 1; let newCleanPos = - clean(val.toLowerCase()).indexOf( - valueBeforeSelectionStart[i], - cleanPos - ) + 1; + clean(val).indexOf(valueBeforeSelectionStart[i], cleanPos) + 1; // this skips position change if accepted symbols order was broken // For example fixes edge case with fixed point numbers: @@ -137,7 +149,17 @@ export const Rifm = (props: Props) => { // if nothing changed for formatted value, just refresh so userValue will be used at render refresh(); } else { - props.onChange(formattedValue); + if (process.env.NODE_ENV !== 'production') { + const replaceValue = replace + ? replace(formattedValue) + : formattedValue; + + if (replaceValue.length !== formattedValue.length) { + console.warn('replace must preserve length'); + } + } + + props.onChange(replace ? replace(formattedValue) : formattedValue); } return () => { diff --git a/tests/RifmFormat.test.js b/tests/RifmFormat.test.js index bce9cf9..45bf111 100644 --- a/tests/RifmFormat.test.js +++ b/tests/RifmFormat.test.js @@ -132,3 +132,26 @@ test('format works even if state is not updated on equal vals', async () => { exec({ type: 'PUT_SYMBOL', payload: 'x' }).toMatchInlineSnapshot(`"123|’456"`); exec({ type: 'PUT_SYMBOL', payload: 'x' }).toMatchInlineSnapshot(`"123|’456"`); }); + +it('format can work with case changes', () => { + const exec = createExec({ + format: v => v, + replace: v => v.toLowerCase(), + accept: /.+/g, + }); + + exec({ type: 'PUT_SYMBOL', payload: 'HELLO WORLD' }).toMatchInlineSnapshot(`"hello world|"`); + exec({ type: 'MOVE_CARET', payload: -5 }).toMatchInlineSnapshot(`"hello |world"`); + exec({ type: 'PUT_SYMBOL', payload: 'BeAuTiFuL ' }).toMatchInlineSnapshot(`"hello beautiful |world"`); +}); + +it('replace is applied to input value', () => { + const exec = createExec({ + format: v => v, + replace: v => v.toLowerCase(), + accept: /.+/g, + initialValue: 'HeLLo', + }); + + exec({ type: 'MOVE_CARET', payload: -5 }).toMatchInlineSnapshot(`"|hello"`); +}); diff --git a/tests/utils/exec.js b/tests/utils/exec.js index 4a18ee9..1325682 100644 --- a/tests/utils/exec.js +++ b/tests/utils/exec.js @@ -14,7 +14,9 @@ type Props = {| // replace?: string => boolean, mask?: boolean, format: (str: string) => string, + replace?: (str: string) => string, maskFn?: string => boolean, + initialValue?: string, |}; export const createExec = (props: Props) => { @@ -23,7 +25,7 @@ export const createExec = (props: Props) => { let stateValue_ = null; TestRenderer.create( - + {input => { stateValue_ = input.value; @@ -33,6 +35,7 @@ export const createExec = (props: Props) => { onChange={input.set} accept={props.accept} format={props.format} + replace={props.replace} mask={ props.mask != null ? props.mask @@ -68,8 +71,11 @@ export const createExec = (props: Props) => { if (getVal == null || stateValue_ == null) { throw Error('rifm is not initialized'); } + const { replace } = props; - expect(props.format(stateValue_)).toEqual(getVal().value); + expect( + replace ? replace(props.format(stateValue_)) : props.format(stateValue_) + ).toEqual(getVal().value); return expect(renderInputState(getVal())); };