Skip to content

Commit

Permalink
fix #941
Browse files Browse the repository at this point in the history
  • Loading branch information
uNmAnNeR committed Feb 27, 2024
1 parent 58f01fa commit 704bcf3
Show file tree
Hide file tree
Showing 15 changed files with 212 additions and 37 deletions.
22 changes: 16 additions & 6 deletions packages/imask/example.html
Expand Up @@ -12,12 +12,22 @@ <h1>IMask Core Demo</h1>
<!-- <script src="https://unpkg.com/imask"></script> -->
<script type="text/javascript">
const opts = {
mask: IMask.MaskedEnum,
enum: Array.from({ length: 12 }, (_, i) =>
new Date(0, i).toLocaleString(window.navigator.language, { month: 'long' })
),
lazy: false,
matchValue: (estr, istr, matchFrom) => IMask.MaskedEnum.DEFAULTS.matchValue(estr.toLowerCase(), istr.toLowerCase(), matchFrom),
mask: 'HH:MM',
blocks: {
HH: {
mask: IMask.MaskedRange,
from: 0,
to: 23,
maxLength: 2,
},
MM: {
mask: IMask.MaskedRange,
from: 0,
to: 59,
maxLength: 2,
},
},
autofix: 'pad',
};

const input = document.getElementById('input');
Expand Down
8 changes: 8 additions & 0 deletions packages/imask/src/core/change-details.ts
Expand Up @@ -56,6 +56,14 @@ class ChangeDetails {
get consumed (): boolean {
return Boolean(this.rawInserted) || this.skip;
}

equals (details: ChangeDetails): boolean {
return this.inserted === details.inserted &&
this.tailShift === details.tailShift &&
this.rawInserted === details.rawInserted &&
this.skip === details.skip
;
}
}


Expand Down
35 changes: 32 additions & 3 deletions packages/imask/src/masked/base.ts
Expand Up @@ -41,6 +41,7 @@ type MaskedOptions<M extends Masked=Masked, Props extends keyof M=never> = Parti
| 'overwrite'
| 'eager'
| 'skipInvalid'
| 'autofix'
| Props
>>;

Expand Down Expand Up @@ -75,6 +76,8 @@ abstract class Masked<Value=any> {
abstract eager?: boolean | 'remove' | 'append' | undefined;
/** */
abstract skipInvalid?: boolean | undefined;
/** */
abstract autofix?: boolean | 'pad' | undefined;

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

/** Appends char */
_appendChar (ch: string, flags: AppendFlags={}, checkTail?: TailDetails): ChangeDetails {
const consistentState: MaskedState = this.state;
const consistentState = this.state;
let details: ChangeDetails;
[ch, details] = this.doPrepareChar(ch, flags);

if (ch) details = details.aggregate(this._appendCharRaw(ch, flags));
if (ch) {
details = details.aggregate(this._appendCharRaw(ch, flags));

// TODO handle `skip`?

// try `autofix` lookahead
if (!details.rawInserted && this.autofix === 'pad') {
const noFixState = this.state;
this.state = consistentState;

let fixDetails = this.pad(flags);
const chDetails = this._appendCharRaw(ch, flags);
fixDetails = fixDetails.aggregate(chDetails);

// if fix was applied or
// if details are equal use skip restoring state optimization
if (chDetails.rawInserted || fixDetails.equals(details)) {
details = fixDetails;
} else {
this.state = noFixState;
}
}
}

if (details.inserted) {
let consistentTail;
Expand Down Expand Up @@ -373,7 +398,7 @@ abstract class Masked<Value=any> {
if (this.commit) this.commit(this.value, this);
}

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

Expand Down Expand Up @@ -433,6 +458,10 @@ abstract class Masked<Value=any> {
Masked.EMPTY_VALUES.includes(value) && Masked.EMPTY_VALUES.includes(tval) ||
(this.format ? this.format(value, this) === this.format(this.typedValue, this) : false);
}

pad (flags?: AppendFlags): ChangeDetails {
return new ChangeDetails();
}
}


Expand Down
9 changes: 1 addition & 8 deletions packages/imask/src/masked/date.ts
Expand Up @@ -82,13 +82,12 @@ class MaskedDate extends MaskedPattern<DateValue> {
declare min?: Date;
/** End date */
declare max?: Date;
/** */
declare autofix?: boolean | 'pad' | undefined;
/** Format typed value to string */
declare format: (value: DateValue, masked: Masked) => string;
/** Parse string to get typed value */
declare parse: (str: string, masked: Masked) => DateValue;


constructor (opts?: MaskedDateOptions) {
super(MaskedDate.extractPatternOptions({
...(MaskedDate.DEFAULTS as MaskedDateOptions),
Expand Down Expand Up @@ -122,12 +121,6 @@ class MaskedDate extends MaskedPattern<DateValue> {
}
Object.assign(patternBlocks, this.blocks, blocks);

// add autofix
Object.keys(patternBlocks).forEach(bk => {
const b = patternBlocks[bk];
if (!('autofix' in b) && 'autofix' in opts) b.autofix = opts.autofix;
});

super._update({
...patternOpts,
mask: isString(mask) ? mask : pattern,
Expand Down
11 changes: 11 additions & 0 deletions packages/imask/src/masked/dynamic.ts
Expand Up @@ -43,6 +43,7 @@ class MaskedDynamic<Value=any> extends Masked<Value> {
declare _overwrite?: this['overwrite'];
declare _eager?: this['eager'];
declare _skipInvalid?: this['skipInvalid'];
declare _autofix?: this['autofix'];

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

override get autofix (): boolean | 'pad' | undefined {
return this.currentMask ?
this.currentMask.autofix :
this._autofix;
}

override set autofix (autofix: boolean | 'pad' | undefined) {
this._autofix = autofix;
}

override maskEquals (mask: any): boolean {
return Array.isArray(mask) ?
this.compiledMasks.every((m, mi) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/imask/src/masked/function.ts
Expand Up @@ -16,6 +16,8 @@ class MaskedFunction<Value=any> extends Masked<Value> {
declare eager?: boolean | 'remove' | 'append' | undefined;
/** */
declare skipInvalid?: boolean | undefined;
/** */
declare autofix?: boolean | 'pad' | undefined;

override updateOptions (opts: Partial<MaskedFunctionOptions>) {
super.updateOptions(opts);
Expand Down
5 changes: 2 additions & 3 deletions packages/imask/src/masked/number.ts
Expand Up @@ -16,7 +16,6 @@ type MaskedNumberOptions = MaskedOptions<MaskedNumber,
| 'max'
| 'normalizeZeros'
| 'padFractionalZeros'
| 'autofix'
>;

/** Number mask */
Expand Down Expand Up @@ -62,12 +61,12 @@ class MaskedNumber extends Masked<number> {
declare eager?: boolean | 'remove' | 'append' | undefined;
/** */
declare skipInvalid?: boolean | undefined;
/** */
declare autofix?: boolean | 'pad' | undefined;
/** Format typed value to string */
declare format: (value: number, masked: Masked) => string;
/** Parse string to get typed value */
declare parse: (str: string, masked: Masked) => number;
/** */
declare autofix?: boolean;

declare _numberRegExp: RegExp;
declare _thousandsSeparatorRegExp: RegExp;
Expand Down
9 changes: 9 additions & 0 deletions packages/imask/src/masked/pattern.ts
Expand Up @@ -79,6 +79,8 @@ class MaskedPattern<Value=string> extends Masked<Value> {
declare eager?: boolean | 'remove' | 'append' | undefined;
/** */
declare skipInvalid?: boolean | undefined;
/** */
declare autofix?: boolean | 'pad' | undefined;

declare _blocks: Array<PatternBlock>;
declare _maskedBlocks: {[key: string]: Array<number>};
Expand Down Expand Up @@ -131,6 +133,7 @@ class MaskedPattern<Value=string> extends Masked<Value> {
placeholderChar: this.placeholderChar,
displayChar: this.displayChar,
overwrite: this.overwrite,
autofix: this.autofix,
...bOpts,
repeat,
parent: this,
Expand Down Expand Up @@ -532,6 +535,12 @@ class MaskedPattern<Value=string> extends Masked<Value> {
if (!indices) return [];
return indices.map(gi => this._blocks[gi]);
}

override pad (flags?: AppendFlags): ChangeDetails {
const details = new ChangeDetails();
this._forEachBlocksInRange(0, this.displayValue.length, b => details.aggregate(b.pad(flags)))
return details;
}
}


Expand Down
1 change: 1 addition & 0 deletions packages/imask/src/masked/pattern/block.ts
Expand Up @@ -30,4 +30,5 @@ interface PatternBlock<State=MaskedState> {
doCommit (): void;
nearestInputPos (cursorPos: number, direction: Direction): number;
totalInputPositions (fromPos?: number, toPos?: number): number;
pad (flags?: AppendFlags): ChangeDetails;
}
4 changes: 4 additions & 0 deletions packages/imask/src/masked/pattern/fixed-definition.ts
Expand Up @@ -152,4 +152,8 @@ class PatternFixedDefinition implements PatternBlock {
this._value = state._value;
this._isRawInput = Boolean(state._rawInputValue);
}

pad (flags?: AppendFlags): ChangeDetails {
return this._appendPlaceholder();
}
}
5 changes: 5 additions & 0 deletions packages/imask/src/masked/pattern/input-definition.ts
Expand Up @@ -58,6 +58,7 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo
/** */
declare displayChar: MaskedPattern['displayChar'];


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

Expand Down Expand Up @@ -201,4 +202,8 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo
_beforeTailState: flags?._beforeTailState?.masked || flags?._beforeTailState as unknown as MaskedState,
};
}

pad (flags?: AppendFlags): ChangeDetails {
return new ChangeDetails();
}
}
52 changes: 35 additions & 17 deletions packages/imask/src/masked/range.ts
@@ -1,11 +1,11 @@
import ChangeDetails from '../core/change-details';
import IMask from '../core/holder';
import { type AppendFlags } from './base';
import MaskedPattern, { type MaskedPatternOptions } from './pattern';
import MaskedPattern, { MaskedPatternState, type MaskedPatternOptions } from './pattern';


type MaskedRangePatternOptions = MaskedPatternOptions &
Pick<MaskedRange, 'from' | 'to' | 'autofix'> &
Pick<MaskedRange, 'from' | 'to'> &
Partial<Pick<MaskedRange, 'maxLength'>>;

export
Expand All @@ -24,8 +24,6 @@ class MaskedRange extends MaskedPattern {
declare from: number;
/** Max bound */
declare to: number;
/** */
declare autofix?: boolean | 'pad';

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

if (!this.autofix || !ch) {
details.skip = !this.isComplete;
return [ch, details];
}
if (!ch) details.skip = !this.isComplete;

return [ch, details];
}

override _appendCharRaw (ch: string, flags: AppendFlags<MaskedPatternState>={}): ChangeDetails {
if (!this.autofix || this.value.length + 1 > this.maxLength) return super._appendCharRaw(ch, flags);

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

const nextVal = this.value + ch;
if (nextVal.length > this.maxLength) return ['', details];

const [minstr, maxstr] = this.boundaries(nextVal);
const [minstr, maxstr] = this.boundaries(this.value + ch);

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

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

return [ch, details];
return super._appendCharRaw(ch, flags);
}

override doValidate (flags: AppendFlags): boolean {
Expand All @@ -121,6 +119,26 @@ class MaskedRange extends MaskedPattern {
return this.from <= Number(maxstr) && Number(minstr) <= this.to &&
super.doValidate(flags);
}

override pad (flags?: AppendFlags): ChangeDetails {
const details = new ChangeDetails();
if (this.value.length === this.maxLength) return details;

const value = this.value;
const padLength = this.maxLength - this.value.length;

if (padLength) {
this.reset();
for (let i=0; i < padLength; ++i) {
details.aggregate(super._appendCharRaw('0', flags as AppendFlags<MaskedPatternState>));
}

// append tail
value.split('').forEach(ch => this._appendCharRaw(ch));
}

return details;
}
}


Expand Down
2 changes: 2 additions & 0 deletions packages/imask/src/masked/regexp.ts
Expand Up @@ -16,6 +16,8 @@ class MaskedRegExp extends Masked<string> {
declare eager?: boolean | 'remove' | 'append' | undefined;
/** */
declare skipInvalid?: boolean | undefined;
/** */
declare autofix?: boolean | 'pad' | undefined;

override updateOptions (opts: Partial<MaskedRegExpOptions>) {
super.updateOptions(opts);
Expand Down

0 comments on commit 704bcf3

Please sign in to comment.