Skip to content

Commit 704bcf3

Browse files
committed
fix #941
1 parent 58f01fa commit 704bcf3

File tree

15 files changed

+212
-37
lines changed

15 files changed

+212
-37
lines changed

packages/imask/example.html

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,22 @@ <h1>IMask Core Demo</h1>
1212
<!-- <script src="https://unpkg.com/imask"></script> -->
1313
<script type="text/javascript">
1414
const opts = {
15-
mask: IMask.MaskedEnum,
16-
enum: Array.from({ length: 12 }, (_, i) =>
17-
new Date(0, i).toLocaleString(window.navigator.language, { month: 'long' })
18-
),
19-
lazy: false,
20-
matchValue: (estr, istr, matchFrom) => IMask.MaskedEnum.DEFAULTS.matchValue(estr.toLowerCase(), istr.toLowerCase(), matchFrom),
15+
mask: 'HH:MM',
16+
blocks: {
17+
HH: {
18+
mask: IMask.MaskedRange,
19+
from: 0,
20+
to: 23,
21+
maxLength: 2,
22+
},
23+
MM: {
24+
mask: IMask.MaskedRange,
25+
from: 0,
26+
to: 59,
27+
maxLength: 2,
28+
},
29+
},
30+
autofix: 'pad',
2131
};
2232

2333
const input = document.getElementById('input');

packages/imask/src/core/change-details.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ class ChangeDetails {
5656
get consumed (): boolean {
5757
return Boolean(this.rawInserted) || this.skip;
5858
}
59+
60+
equals (details: ChangeDetails): boolean {
61+
return this.inserted === details.inserted &&
62+
this.tailShift === details.tailShift &&
63+
this.rawInserted === details.rawInserted &&
64+
this.skip === details.skip
65+
;
66+
}
5967
}
6068

6169

packages/imask/src/masked/base.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type MaskedOptions<M extends Masked=Masked, Props extends keyof M=never> = Parti
4141
| 'overwrite'
4242
| 'eager'
4343
| 'skipInvalid'
44+
| 'autofix'
4445
| Props
4546
>>;
4647

@@ -75,6 +76,8 @@ abstract class Masked<Value=any> {
7576
abstract eager?: boolean | 'remove' | 'append' | undefined;
7677
/** */
7778
abstract skipInvalid?: boolean | undefined;
79+
/** */
80+
abstract autofix?: boolean | 'pad' | undefined;
7881

7982
/** */
8083
declare _initialized: boolean;
@@ -216,11 +219,33 @@ abstract class Masked<Value=any> {
216219

217220
/** Appends char */
218221
_appendChar (ch: string, flags: AppendFlags={}, checkTail?: TailDetails): ChangeDetails {
219-
const consistentState: MaskedState = this.state;
222+
const consistentState = this.state;
220223
let details: ChangeDetails;
221224
[ch, details] = this.doPrepareChar(ch, flags);
222225

223-
if (ch) details = details.aggregate(this._appendCharRaw(ch, flags));
226+
if (ch) {
227+
details = details.aggregate(this._appendCharRaw(ch, flags));
228+
229+
// TODO handle `skip`?
230+
231+
// try `autofix` lookahead
232+
if (!details.rawInserted && this.autofix === 'pad') {
233+
const noFixState = this.state;
234+
this.state = consistentState;
235+
236+
let fixDetails = this.pad(flags);
237+
const chDetails = this._appendCharRaw(ch, flags);
238+
fixDetails = fixDetails.aggregate(chDetails);
239+
240+
// if fix was applied or
241+
// if details are equal use skip restoring state optimization
242+
if (chDetails.rawInserted || fixDetails.equals(details)) {
243+
details = fixDetails;
244+
} else {
245+
this.state = noFixState;
246+
}
247+
}
248+
}
224249

225250
if (details.inserted) {
226251
let consistentTail;
@@ -373,7 +398,7 @@ abstract class Masked<Value=any> {
373398
if (this.commit) this.commit(this.value, this);
374399
}
375400

376-
splice (start: number, deleteCount: number, inserted: string, removeDirection: Direction = DIRECTION.NONE, flags: AppendFlags = { input: true }): ChangeDetails {
401+
splice (start: number, deleteCount: number, inserted='', removeDirection: Direction = DIRECTION.NONE, flags: AppendFlags = { input: true }): ChangeDetails {
377402
const tailPos: number = start + deleteCount;
378403
const tail: TailDetails = this.extractTail(tailPos);
379404

@@ -433,6 +458,10 @@ abstract class Masked<Value=any> {
433458
Masked.EMPTY_VALUES.includes(value) && Masked.EMPTY_VALUES.includes(tval) ||
434459
(this.format ? this.format(value, this) === this.format(this.typedValue, this) : false);
435460
}
461+
462+
pad (flags?: AppendFlags): ChangeDetails {
463+
return new ChangeDetails();
464+
}
436465
}
437466

438467

packages/imask/src/masked/date.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,12 @@ class MaskedDate extends MaskedPattern<DateValue> {
8282
declare min?: Date;
8383
/** End date */
8484
declare max?: Date;
85-
/** */
86-
declare autofix?: boolean | 'pad' | undefined;
8785
/** Format typed value to string */
8886
declare format: (value: DateValue, masked: Masked) => string;
8987
/** Parse string to get typed value */
9088
declare parse: (str: string, masked: Masked) => DateValue;
9189

90+
9291
constructor (opts?: MaskedDateOptions) {
9392
super(MaskedDate.extractPatternOptions({
9493
...(MaskedDate.DEFAULTS as MaskedDateOptions),
@@ -122,12 +121,6 @@ class MaskedDate extends MaskedPattern<DateValue> {
122121
}
123122
Object.assign(patternBlocks, this.blocks, blocks);
124123

125-
// add autofix
126-
Object.keys(patternBlocks).forEach(bk => {
127-
const b = patternBlocks[bk];
128-
if (!('autofix' in b) && 'autofix' in opts) b.autofix = opts.autofix;
129-
});
130-
131124
super._update({
132125
...patternOpts,
133126
mask: isString(mask) ? mask : pattern,

packages/imask/src/masked/dynamic.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class MaskedDynamic<Value=any> extends Masked<Value> {
4343
declare _overwrite?: this['overwrite'];
4444
declare _eager?: this['eager'];
4545
declare _skipInvalid?: this['skipInvalid'];
46+
declare _autofix?: this['autofix'];
4647

4748
static DEFAULTS: typeof Masked.DEFAULTS & Pick<MaskedDynamic, 'dispatch'> = {
4849
...Masked.DEFAULTS,
@@ -402,6 +403,16 @@ class MaskedDynamic<Value=any> extends Masked<Value> {
402403
this._skipInvalid = skipInvalid;
403404
}
404405

406+
override get autofix (): boolean | 'pad' | undefined {
407+
return this.currentMask ?
408+
this.currentMask.autofix :
409+
this._autofix;
410+
}
411+
412+
override set autofix (autofix: boolean | 'pad' | undefined) {
413+
this._autofix = autofix;
414+
}
415+
405416
override maskEquals (mask: any): boolean {
406417
return Array.isArray(mask) ?
407418
this.compiledMasks.every((m, mi) => {

packages/imask/src/masked/function.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class MaskedFunction<Value=any> extends Masked<Value> {
1616
declare eager?: boolean | 'remove' | 'append' | undefined;
1717
/** */
1818
declare skipInvalid?: boolean | undefined;
19+
/** */
20+
declare autofix?: boolean | 'pad' | undefined;
1921

2022
override updateOptions (opts: Partial<MaskedFunctionOptions>) {
2123
super.updateOptions(opts);

packages/imask/src/masked/number.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ type MaskedNumberOptions = MaskedOptions<MaskedNumber,
1616
| 'max'
1717
| 'normalizeZeros'
1818
| 'padFractionalZeros'
19-
| 'autofix'
2019
>;
2120

2221
/** Number mask */
@@ -62,12 +61,12 @@ class MaskedNumber extends Masked<number> {
6261
declare eager?: boolean | 'remove' | 'append' | undefined;
6362
/** */
6463
declare skipInvalid?: boolean | undefined;
64+
/** */
65+
declare autofix?: boolean | 'pad' | undefined;
6566
/** Format typed value to string */
6667
declare format: (value: number, masked: Masked) => string;
6768
/** Parse string to get typed value */
6869
declare parse: (str: string, masked: Masked) => number;
69-
/** */
70-
declare autofix?: boolean;
7170

7271
declare _numberRegExp: RegExp;
7372
declare _thousandsSeparatorRegExp: RegExp;

packages/imask/src/masked/pattern.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ class MaskedPattern<Value=string> extends Masked<Value> {
7979
declare eager?: boolean | 'remove' | 'append' | undefined;
8080
/** */
8181
declare skipInvalid?: boolean | undefined;
82+
/** */
83+
declare autofix?: boolean | 'pad' | undefined;
8284

8385
declare _blocks: Array<PatternBlock>;
8486
declare _maskedBlocks: {[key: string]: Array<number>};
@@ -131,6 +133,7 @@ class MaskedPattern<Value=string> extends Masked<Value> {
131133
placeholderChar: this.placeholderChar,
132134
displayChar: this.displayChar,
133135
overwrite: this.overwrite,
136+
autofix: this.autofix,
134137
...bOpts,
135138
repeat,
136139
parent: this,
@@ -532,6 +535,12 @@ class MaskedPattern<Value=string> extends Masked<Value> {
532535
if (!indices) return [];
533536
return indices.map(gi => this._blocks[gi]);
534537
}
538+
539+
override pad (flags?: AppendFlags): ChangeDetails {
540+
const details = new ChangeDetails();
541+
this._forEachBlocksInRange(0, this.displayValue.length, b => details.aggregate(b.pad(flags)))
542+
return details;
543+
}
535544
}
536545

537546

packages/imask/src/masked/pattern/block.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ interface PatternBlock<State=MaskedState> {
3030
doCommit (): void;
3131
nearestInputPos (cursorPos: number, direction: Direction): number;
3232
totalInputPositions (fromPos?: number, toPos?: number): number;
33+
pad (flags?: AppendFlags): ChangeDetails;
3334
}

packages/imask/src/masked/pattern/fixed-definition.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,8 @@ class PatternFixedDefinition implements PatternBlock {
152152
this._value = state._value;
153153
this._isRawInput = Boolean(state._rawInputValue);
154154
}
155+
156+
pad (flags?: AppendFlags): ChangeDetails {
157+
return this._appendPlaceholder();
158+
}
155159
}

packages/imask/src/masked/pattern/input-definition.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo
5858
/** */
5959
declare displayChar: MaskedPattern['displayChar'];
6060

61+
6162
constructor(opts: PatternInputDefinitionOptions<Opts>) {
6263
const { parent, isOptional, placeholderChar, displayChar, lazy, eager, ...maskOpts } = opts;
6364

@@ -201,4 +202,8 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo
201202
_beforeTailState: flags?._beforeTailState?.masked || flags?._beforeTailState as unknown as MaskedState,
202203
};
203204
}
205+
206+
pad (flags?: AppendFlags): ChangeDetails {
207+
return new ChangeDetails();
208+
}
204209
}

packages/imask/src/masked/range.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import ChangeDetails from '../core/change-details';
22
import IMask from '../core/holder';
33
import { type AppendFlags } from './base';
4-
import MaskedPattern, { type MaskedPatternOptions } from './pattern';
4+
import MaskedPattern, { MaskedPatternState, type MaskedPatternOptions } from './pattern';
55

66

77
type MaskedRangePatternOptions = MaskedPatternOptions &
8-
Pick<MaskedRange, 'from' | 'to' | 'autofix'> &
8+
Pick<MaskedRange, 'from' | 'to'> &
99
Partial<Pick<MaskedRange, 'maxLength'>>;
1010

1111
export
@@ -24,8 +24,6 @@ class MaskedRange extends MaskedPattern {
2424
declare from: number;
2525
/** Max bound */
2626
declare to: number;
27-
/** */
28-
declare autofix?: boolean | 'pad';
2927

3028
get _matchFrom (): number {
3129
return this.maxLength - String(this.from).length;
@@ -85,29 +83,29 @@ class MaskedRange extends MaskedPattern {
8583
let details: ChangeDetails;
8684
[ch, details] = super.doPrepareChar(ch.replace(/\D/g, ''), flags);
8785

88-
if (!this.autofix || !ch) {
89-
details.skip = !this.isComplete;
90-
return [ch, details];
91-
}
86+
if (!ch) details.skip = !this.isComplete;
87+
88+
return [ch, details];
89+
}
90+
91+
override _appendCharRaw (ch: string, flags: AppendFlags<MaskedPatternState>={}): ChangeDetails {
92+
if (!this.autofix || this.value.length + 1 > this.maxLength) return super._appendCharRaw(ch, flags);
9293

9394
const fromStr = String(this.from).padStart(this.maxLength, '0');
9495
const toStr = String(this.to).padStart(this.maxLength, '0');
9596

96-
const nextVal = this.value + ch;
97-
if (nextVal.length > this.maxLength) return ['', details];
98-
99-
const [minstr, maxstr] = this.boundaries(nextVal);
97+
const [minstr, maxstr] = this.boundaries(this.value + ch);
10098

101-
if (Number(maxstr) < this.from) return [fromStr[nextVal.length - 1], details];
99+
if (Number(maxstr) < this.from) return super._appendCharRaw(fromStr[this.value.length], flags);
102100

103101
if (Number(minstr) > this.to) {
104-
if (this.autofix === 'pad' && nextVal.length < this.maxLength) {
105-
return ['', details.aggregate(this.append(fromStr[nextVal.length - 1]+ch, flags))];
102+
if (!flags.tail && this.autofix === 'pad' && this.value.length + 1 < this.maxLength) {
103+
return super._appendCharRaw(fromStr[this.value.length], flags).aggregate(this._appendCharRaw(ch, flags));
106104
}
107-
return [toStr[nextVal.length - 1], details];
105+
return super._appendCharRaw(toStr[this.value.length], flags);
108106
}
109107

110-
return [ch, details];
108+
return super._appendCharRaw(ch, flags);
111109
}
112110

113111
override doValidate (flags: AppendFlags): boolean {
@@ -121,6 +119,26 @@ class MaskedRange extends MaskedPattern {
121119
return this.from <= Number(maxstr) && Number(minstr) <= this.to &&
122120
super.doValidate(flags);
123121
}
122+
123+
override pad (flags?: AppendFlags): ChangeDetails {
124+
const details = new ChangeDetails();
125+
if (this.value.length === this.maxLength) return details;
126+
127+
const value = this.value;
128+
const padLength = this.maxLength - this.value.length;
129+
130+
if (padLength) {
131+
this.reset();
132+
for (let i=0; i < padLength; ++i) {
133+
details.aggregate(super._appendCharRaw('0', flags as AppendFlags<MaskedPatternState>));
134+
}
135+
136+
// append tail
137+
value.split('').forEach(ch => this._appendCharRaw(ch));
138+
}
139+
140+
return details;
141+
}
124142
}
125143

126144

packages/imask/src/masked/regexp.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class MaskedRegExp extends Masked<string> {
1616
declare eager?: boolean | 'remove' | 'append' | undefined;
1717
/** */
1818
declare skipInvalid?: boolean | undefined;
19+
/** */
20+
declare autofix?: boolean | 'pad' | undefined;
1921

2022
override updateOptions (opts: Partial<MaskedRegExpOptions>) {
2123
super.updateOptions(opts);

0 commit comments

Comments
 (0)