Skip to content

Commit

Permalink
fix #935
Browse files Browse the repository at this point in the history
  • Loading branch information
uNmAnNeR committed Dec 11, 2023
1 parent 8698766 commit 882e73b
Show file tree
Hide file tree
Showing 13 changed files with 530 additions and 92 deletions.
451 changes: 406 additions & 45 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -29,7 +29,7 @@
"rimraf": "^5.0.1",
"rollup": "^3.26.2",
"rollup-plugin-multi-input": "^1.4.1",
"tsx": "^3.12.7",
"tsx": "^4.6.2",
"typedoc": "^0.24.8",
"typescript": "^4.9.5"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/imask/package.json
Expand Up @@ -64,7 +64,7 @@
}
},
"scripts": {
"test": "cross-env NODE_ENV=test c8 node --test-reporter=spec --loader tsx --test test/**/*",
"test": "cross-env NODE_ENV=test c8 node --test-reporter=spec --import tsx --test test/**/*",
"watch": "rollup -c -w",
"lint": "eslint src",
"prebuild": "npm run lint -- --quiet && rimraf --glob \"{dist,esm}\"",
Expand Down
4 changes: 2 additions & 2 deletions packages/imask/src/controls/input.ts
@@ -1,4 +1,4 @@
import { objectIncludes, DIRECTION, type Selection } from '../core/utils';
import { DIRECTION, type Selection } from '../core/utils';
import ActionDetails from '../core/action-details';
import createMask, { type UpdateOpts, maskedClass, type FactoryArg, type FactoryReturnMasked } from '../masked/factory';
import Masked from '../masked/base';
Expand Down Expand Up @@ -211,7 +211,7 @@ class InputMask<Opts extends FactoryArg> {
const { mask, ...restOpts } = opts;

const updateMask = !this.maskEquals(mask);
const updateOpts = !objectIncludes(this.masked, restOpts);
const updateOpts = this.masked.optionsIsChanged(restOpts);

if (updateMask) this.mask = mask;
if (updateOpts) this.masked.updateOptions(restOpts); // TODO
Expand Down
3 changes: 2 additions & 1 deletion packages/imask/src/core/change-details.ts
Expand Up @@ -15,7 +15,7 @@ class ChangeDetails {
/** Inserted symbols */
declare inserted: string;
/** Can skip chars */
declare skip?: boolean;
declare skip: boolean;
/** Additional offset if any changes occurred before tail */
declare tailShift: number;
/** Raw inserted is used by dynamic mask */
Expand All @@ -32,6 +32,7 @@ class ChangeDetails {
Object.assign(this, {
inserted: '',
rawInserted: '',
skip: false,
tailShift: 0,
}, details);
}
Expand Down
14 changes: 7 additions & 7 deletions packages/imask/src/masked/base.ts
@@ -1,6 +1,6 @@
import ChangeDetails from '../core/change-details';
import ContinuousTailDetails from '../core/continuous-tail-details';
import { type Direction, DIRECTION, isString, forceDirection } from '../core/utils';
import { type Direction, DIRECTION, isString, forceDirection, objectIncludes } from '../core/utils';
import { type TailDetails } from '../core/tail-details';
import IMask from '../core/holder';

Expand Down Expand Up @@ -94,7 +94,7 @@ abstract class Masked<Value=any> {

/** Sets and applies new options */
updateOptions (opts: Partial<MaskedOptions>) {
if (!Object.keys(opts).length) return;
if (!this.optionsIsChanged(opts)) return;

this.withValueRefresh(this._update.bind(this, opts));
}
Expand Down Expand Up @@ -220,11 +220,7 @@ abstract class Masked<Value=any> {
let details: ChangeDetails;
[ch, details] = this.doPrepareChar(ch, flags);

if (ch) {
details = details.aggregate(this._appendCharRaw(ch, flags));
} else if (details.skip == null) {
details.skip = true;
}
if (ch) details = details.aggregate(this._appendCharRaw(ch, flags));

if (details.inserted) {
let consistentTail;
Expand Down Expand Up @@ -421,6 +417,10 @@ abstract class Masked<Value=any> {
return this.mask === mask;
}

optionsIsChanged (opts: Partial<MaskedOptions>): boolean {
return !objectIncludes(this, opts);
}

typedValueEquals (value: any): boolean {
const tval = this.typedValue;

Expand Down
21 changes: 14 additions & 7 deletions packages/imask/src/masked/date.ts
Expand Up @@ -67,6 +67,14 @@ class MaskedDate extends MaskedPattern<DateValue> {
},
} satisfies Partial<MaskedDateOptions>;

static extractPatternOptions (opts: Partial<MaskedDateOptions>): Partial<Omit<MaskedDateOptions, 'mask' | 'pattern'> & { mask: MaskedPatternOptions['mask'] }> {
const { mask, pattern, ...patternOpts } = opts;
return {
...patternOpts,
mask: isString(mask) ? mask : pattern,
};
}

/** Pattern mask for date according to {@link MaskedDate#format} */
declare pattern: string;
/** Start date */
Expand All @@ -81,15 +89,10 @@ class MaskedDate extends MaskedPattern<DateValue> {
declare parse: (str: string, masked: Masked) => DateValue;

constructor (opts?: MaskedDateOptions) {
const { mask, pattern, ...patternOpts } = {
super(MaskedDate.extractPatternOptions({
...(MaskedDate.DEFAULTS as MaskedDateOptions),
...opts,
};

super({
...patternOpts,
mask: isString(mask) ? mask : pattern,
});
}));
}

override updateOptions (opts: Partial<MaskedDateOptions>) {
Expand Down Expand Up @@ -164,6 +167,10 @@ class MaskedDate extends MaskedPattern<DateValue> {
override maskEquals (mask: any): boolean {
return mask === Date || super.maskEquals(mask);
}

override optionsIsChanged (opts: Partial<MaskedDateOptions>): boolean {
return super.optionsIsChanged(MaskedDate.extractPatternOptions(opts));
}
}


Expand Down
25 changes: 12 additions & 13 deletions packages/imask/src/masked/factory.ts
Expand Up @@ -75,18 +75,17 @@ export
type FactoryInstanceReturnMasked<Opts extends FactoryInstanceOpts> = Opts extends { mask: infer M } ? M : never;

export
type FactoryConstructorOpts = MaskedOptions & { mask:
| typeof Masked
| typeof MaskedDate
| typeof MaskedNumber
| typeof MaskedEnum
| typeof MaskedRange
| typeof MaskedRegExp
| typeof MaskedFunction
| typeof MaskedPattern
| typeof MaskedDynamic
| typeof MaskedRegExp
};
type FactoryConstructorOpts =
| { mask: typeof MaskedDate } & Omit<MaskedDateFactoryOptions, 'mask'>
| { mask: typeof MaskedNumber } & Omit<MaskedNumberOptions, 'mask'>
| { mask: typeof MaskedEnum } & Omit<MaskedEnumOptions, 'mask'>
| { mask: typeof MaskedRange } & Omit<MaskedRangeOptions, 'mask'>
| { mask: typeof MaskedRegExp } & Omit<MaskedRegExpOptions, 'mask'>
| { mask: typeof MaskedFunction } & Omit<MaskedFunctionOptions, 'mask'>
| { mask: typeof MaskedPattern } & Omit<MaskedPatternOptions, 'mask'>
| { mask: typeof MaskedDynamic } & Omit<MaskedDynamicOptions, 'mask'>
| { mask: typeof Masked } & Omit<MaskedOptions, 'mask'>
;

export
type FactoryConstructorReturnMasked<Opts extends FactoryConstructorOpts> =
Expand Down Expand Up @@ -327,7 +326,7 @@ function createMask<Opts extends FactoryArg> (opts: Opts): FactoryReturnMasked<O
const nOpts = normalizeOpts(opts);

const MaskedClass = maskedClass(nOpts.mask);
if (!MaskedClass) throw new Error('Masked class is not found for provided mask, appropriate module needs to be imported manually before creating mask.');
if (!MaskedClass) throw new Error(`Masked class is not found for provided mask ${nOpts.mask}, appropriate module needs to be imported manually before creating mask.`);

if (nOpts.mask === MaskedClass) delete nOpts.mask;
if ((nOpts as any)._mask) { nOpts.mask = (nOpts as any)._mask; delete (nOpts as any)._mask; }
Expand Down
4 changes: 1 addition & 3 deletions packages/imask/src/masked/number.ts
Expand Up @@ -317,9 +317,7 @@ class MaskedNumber extends Masked<number> {
}

override get unmaskedValue (): string {
return this._removeThousandsSeparators(
this._normalizeZeros(
this.value))
return this._removeThousandsSeparators(this._normalizeZeros(this.value))
.replace(this.radix, MaskedNumber.UNMASKED_RADIX);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/imask/test/masked/pattern/cursor.ts
Expand Up @@ -37,7 +37,7 @@ describe('Align LEFT', function () {
it('should align before XX with DIRECTION.LEFT', function () {
['XX*', 'XX[*]'].forEach(mask => {
masked.updateOptions({mask, lazy: true});
for (var pos=0; pos<masked._blocks.length-1; ++pos) {
for (let pos=0; pos<masked._blocks.length-1; ++pos) {
assert.equal(masked.nearestInputPos(pos, DIRECTION.LEFT), 0);
}
});
Expand All @@ -46,7 +46,7 @@ describe('Align LEFT', function () {
it('should align before XX', function () {
['*XX', '[*]XX'].forEach(mask => {
masked.updateOptions({mask});
for (var pos=0; pos<masked._blocks.length-1; ++pos) {
for (let pos=0; pos<masked._blocks.length-1; ++pos) {
assert.ok(masked.nearestInputPos(pos) <= 1);
}
});
Expand Down
3 changes: 3 additions & 0 deletions packages/imask/test/masked/pattern/index.ts
@@ -0,0 +1,3 @@
import './insert';
import './extract';
import './cursor';
32 changes: 29 additions & 3 deletions packages/imask/test/masked/pattern/insert.ts
@@ -1,9 +1,11 @@
import assert from 'assert';
import { describe, it, beforeEach, mock } from 'node:test';

import MaskedPattern from '../../../src/masked/pattern';
import { DIRECTION } from '../../../src/core/utils';
import '../../../src/masked/number';
import type MaskedNumber from '../../../src/masked/number';
import MaskedRange from '../../../src/masked/range';
import MaskedPattern from '../../../src/masked/pattern';


describe('Insert', function () {
Expand Down Expand Up @@ -61,8 +63,9 @@ describe('Insert', function () {
mask: '+{7}(000)000-00-00',
prepareChar: prepareStub
});
masked.value = '+79998887766';
assert.equal(prepareStub.mock.callCount(), 1);
const v = '+79998887766';
masked.value = v;
assert.equal(prepareStub.mock.callCount(), v.length);
});

it('should insert value in the middle', function () {
Expand Down Expand Up @@ -246,4 +249,27 @@ describe('Insert', function () {
assert.equal(masked.displayValue, '$321 aa');
})
});

describe('blocks', function () {
it('should shift char to another block', function () {
masked.updateOptions({
mask: 'num-num',
blocks: {
num: {
mask: MaskedRange,
from: 0,
to: 9,
autofix: true,
},
},
lazy: false,
overwrite: false,
});

masked.rawInputValue = '55';

console.log({ v: masked.value });
assert.equal(masked.value, '5-5');
});
});
});
57 changes: 50 additions & 7 deletions packages/react-imask/example.html
Expand Up @@ -9,7 +9,8 @@ <h1>React IMask Plugin Demo</h1>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6.15.0/babel.min.js"></script>
<script src="https://unpkg.com/prop-types@15/prop-types.js"></script>
<script src="https://unpkg.com/imask"></script>
<!-- <script src="https://unpkg.com/imask"></script> -->
<script src="dist/imask.js"></script>
<script src="dist/react-imask.js"></script>
<script type="text/babel">
const mask = Number;
Expand Down Expand Up @@ -37,28 +38,70 @@ <h1>React IMask Plugin Demo</h1>
return React.createElement('input', { ref, defaultValue: '10000000' });
}

const blocks = {
d: {
mask: IMask.MaskedRange,
from: 1,
to: 31,
maxLength: 2,
},
m: {
mask: IMask.MaskedRange,
from: 1,
to: 12,
maxLength: 2,
},
Y: {
mask: IMask.MaskedRange,
from: 1900,
to: 9999,
}
}
const format2 = date => {
let day = date.getDate();
let month = date.getMonth() + 1;
const year = date.getFullYear();

if (day < 10) day = "0" + day;
if (month < 10) month = "0" + month;

return [year, month, day].join('-');
}
const parse2 = str => {
const yearMonthDay = str.split('-');
return new Date(yearMonthDay[0], yearMonthDay[1] - 1, yearMonthDay[2]);
}
const min = new Date(2000, 0, 1);
const max = new Date(2020, 0, 1);

class App extends React.Component {
constructor (...args) {
super(...args);
this.state = {mask, value: 5};
this.state = {mask, value: '2000-12'};
}

render () {
return [
React.createElement(ReactIMask.IMaskInput, {
key: 'maskedinput',
mask: this.state.mask,
defaultValue: '50',
value: this.state.value,
unmask: 'typed',
mask: Date,
pattern: '`Y-`m-`d',
blocks: blocks,
format: format2,
parse: parse2,
min: min,
max: max,
autofix: true,
lazy: false,
overwrite: false,
// unmask: 'typed',
lazy: false,
onAccept: (value, mask) => {
console.log('accept', value, typeof value, mask);
this.setState({value});
},
onInput: (e) => {
if (!this.state.mask) this.setState({ value: e.target.value });
},
onComplete: (value, mask) => console.log('complete!', value, mask),

placeholder: 'Enter here',
Expand Down

0 comments on commit 882e73b

Please sign in to comment.