Skip to content

Commit

Permalink
Merge pull request #1001 from uNmAnNeR/v8
Browse files Browse the repository at this point in the history
fix #979
  • Loading branch information
uNmAnNeR committed Feb 8, 2024
2 parents b22cdb0 + 57a0080 commit cbc9d86
Show file tree
Hide file tree
Showing 10 changed files with 109 additions and 60 deletions.
12 changes: 4 additions & 8 deletions packages/imask/example.html
Expand Up @@ -12,14 +12,10 @@ <h1>IMask Core Demo</h1>
<!-- <script src="https://unpkg.com/imask"></script> -->
<script type="text/javascript">
const opts = {
lazy: false,
mask: 'r0',
blocks: {
r: {
mask: 'a',
repeat: [2, 5],
},
},
mask: Number,
min: -10,
max: 2000,
autofix: true,
};

const input = document.getElementById('input');
Expand Down
18 changes: 12 additions & 6 deletions packages/imask/src/core/change-details.ts
Expand Up @@ -4,22 +4,23 @@ import IMask from "./holder";
export
type ChangeDetailsOptions = Pick<ChangeDetails,
| 'inserted'
| 'skip'
| 'tailShift'
| 'rawInserted'
| 'skip'
>;

/** Provides details of changing model value */
export default
class ChangeDetails {
/** Inserted symbols */
declare inserted: string;
/** Can skip chars */
declare skip: boolean;
/** Additional offset if any changes occurred before tail */
declare tailShift: number;
/** Raw inserted is used by dynamic mask */
declare rawInserted: string;
/** Can skip chars */
declare skip: boolean;


static normalize (prep: string | [string, ChangeDetails]): [string, ChangeDetails] {
return Array.isArray(prep) ? prep : [
Expand All @@ -32,24 +33,29 @@ class ChangeDetails {
Object.assign(this, {
inserted: '',
rawInserted: '',
skip: false,
tailShift: 0,
skip: false,
}, details);
}

/** Aggregate changes */
aggregate (details: ChangeDetails): this {
this.rawInserted += details.rawInserted;
this.skip = this.skip || details.skip;
this.inserted += details.inserted;
this.rawInserted += details.rawInserted;
this.tailShift += details.tailShift;
this.skip = this.skip || details.skip;

return this;
}

/** Total offset considering all changes */
get offset (): number {
return this.tailShift + this.inserted.length;
}

get consumed (): boolean {
return Boolean(this.rawInserted) || this.skip;
}
}


Expand Down
7 changes: 4 additions & 3 deletions packages/imask/src/masked/dynamic.ts
Expand Up @@ -120,12 +120,12 @@ class MaskedDynamic<Value=any> extends Masked<Value> {
this.currentMask.reset();

if (insertValue) {
const d = this.currentMask.append(insertValue, {raw: true});
details.tailShift = d.inserted.length - prevValueBeforeTail.length;
this.currentMask.append(insertValue, { raw: true });
details.tailShift = this.currentMask.value.length - prevValueBeforeTail.length;
}

if (tailValue) {
details.tailShift += this.currentMask.append(tailValue, {raw: true, tail: true}).tailShift;
details.tailShift += this.currentMask.append(tailValue, { raw: true, tail: true }).tailShift;
}
} else if (prevMaskState) {
// Dispatch can do something bad with state, so
Expand Down Expand Up @@ -283,6 +283,7 @@ class MaskedDynamic<Value=any> extends Masked<Value> {

override remove (fromPos?: number, toPos?: number): ChangeDetails {
const details: ChangeDetails = new ChangeDetails();

if (this.currentMask) {
details.aggregate(this.currentMask.remove(fromPos, toPos))
// update with dispatch
Expand Down
59 changes: 38 additions & 21 deletions packages/imask/src/masked/number.ts
Expand Up @@ -16,6 +16,7 @@ type MaskedNumberOptions = MaskedOptions<MaskedNumber,
| 'max'
| 'normalizeZeros'
| 'padFractionalZeros'
| 'autofix'
>;

/** Number mask */
Expand Down Expand Up @@ -64,6 +65,8 @@ class MaskedNumber extends Masked<number> {
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 Expand Up @@ -156,15 +159,47 @@ class MaskedNumber extends Masked<number> {


override _appendCharRaw (ch: string, flags: AppendFlags={}): ChangeDetails {
if (!this.thousandsSeparator) return super._appendCharRaw(ch, flags);

const prevBeforeTailValue = flags.tail && flags._beforeTailState ?
flags._beforeTailState._value :
this._value;
const prevBeforeTailSeparatorsCount = this._separatorsCountFromSlice(prevBeforeTailValue);
this._value = this._removeThousandsSeparators(this.value);

const appendDetails = super._appendCharRaw(ch, flags);
const oldValue = this._value;

this._value += ch;

const num = this.number;
let accepted = !isNaN(num);
let skip = false;

if (accepted) {
let fixedNum;
if (this.min != null && this.min < 0 && this.number < this.min) fixedNum = this.min;
if (this.max != null && this.max > 0 && this.number > this.max) fixedNum = this.max;

if (fixedNum != null) {
if (this.autofix) {
this._value = this.format(fixedNum, this).replace(MaskedNumber.UNMASKED_RADIX, this.radix);
skip ||= oldValue === this._value && !flags.tail; // if not changed on tail it's still ok to proceed
} else {
accepted = false;
}
}
accepted &&= Boolean(this._value.match(this._numberRegExp));
}

let appendDetails;
if (!accepted) {
this._value = oldValue;
appendDetails = new ChangeDetails();
} else {
appendDetails = new ChangeDetails({
inserted: this._value.slice(oldValue.length),
rawInserted: skip ? '' : ch,
skip,
});
}

this._value = this._insertThousandsSeparators(this._value);
const beforeTailValue = flags.tail && flags._beforeTailState ?
Expand All @@ -173,7 +208,6 @@ class MaskedNumber extends Masked<number> {
const beforeTailSeparatorsCount = this._separatorsCountFromSlice(beforeTailValue);

appendDetails.tailShift += (beforeTailSeparatorsCount - prevBeforeTailSeparatorsCount) * this.thousandsSeparator.length;
appendDetails.skip = !appendDetails.rawInserted && ch === this.thousandsSeparator;
return appendDetails;
}

Expand Down Expand Up @@ -243,23 +277,6 @@ class MaskedNumber extends Masked<number> {
return cursorPos;
}

override doValidate (flags: AppendFlags): boolean {
// validate as string
let valid = Boolean(this._removeThousandsSeparators(this.value).match(this._numberRegExp));

if (valid) {
// validate as number
const number = this.number;
valid = valid && !isNaN(number) &&
// check min bound for negative values
(this.min == null || this.min >= 0 || this.min <= this.number) &&
// check max bound for positive values
(this.max == null || this.max <= 0 || this.number <= this.max);
}

return valid && super.doValidate(flags);
}

override doCommit () {
if (this.value) {
const number = this.number;
Expand Down
6 changes: 2 additions & 4 deletions packages/imask/src/masked/pattern.ts
Expand Up @@ -320,7 +320,7 @@ class MaskedPattern<Value=string> extends Masked<Value> {

details.aggregate(blockDetails);

if (blockDetails.skip || blockDetails.rawInserted) break; // go next char
if (blockDetails.consumed) break; // go next char
}

return details;
Expand Down Expand Up @@ -378,9 +378,7 @@ class MaskedPattern<Value=string> extends Masked<Value> {
this._blocks.slice(startBlockIndex, endBlockIndex)
.forEach(b => {
if (!b.lazy || toBlockIndex != null) {
const bDetails = b._appendPlaceholder((b as MaskedPattern)._blocks?.length);
this._value += bDetails.inserted;
details.aggregate(bDetails);
details.aggregate(b._appendPlaceholder((b as MaskedPattern)._blocks?.length));
}
});

Expand Down
7 changes: 2 additions & 5 deletions packages/imask/src/masked/pattern/chunk-tail-details.ts
Expand Up @@ -78,7 +78,7 @@ class ChunksTailDetails implements TailDetails {

const details = new ChangeDetails();

for (let ci=0; ci < this.chunks.length && !details.skip; ++ci) {
for (let ci=0; ci < this.chunks.length; ++ci) {
const chunk = this.chunks[ci];

const lastBlockIter = masked._mapPosToBlock(masked.displayValue.length);
Expand All @@ -93,17 +93,14 @@ class ChunksTailDetails implements TailDetails {
// for continuous block also check if stop is exist
masked._stops.indexOf(stop) >= 0
) {
const phDetails = masked._appendPlaceholder(stop);
details.aggregate(phDetails);
details.aggregate(masked._appendPlaceholder(stop));
}
chunkBlock = chunk instanceof ChunksTailDetails && masked._blocks[stop];
}

if (chunkBlock) {
const tailDetails = chunkBlock.appendTail(chunk);
tailDetails.skip = false; // always ignore skip, it will be set on last
details.aggregate(tailDetails);
masked._value += tailDetails.inserted;

// get not inserted chars
const remainChars = chunk.toString().slice(tailDetails.rawInserted.length);
Expand Down
11 changes: 6 additions & 5 deletions packages/imask/src/masked/pattern/fixed-definition.ts
Expand Up @@ -92,15 +92,16 @@ class PatternFixedDefinition implements PatternBlock {
}

_appendChar (ch: string, flags: AppendFlags={}): ChangeDetails {
const details = new ChangeDetails();

if (this.isFilled) return details;
if (this.isFilled) return new ChangeDetails();
const appendEager = this.eager === true || this.eager === 'append';

const appended = this.char === ch;
const isResolved = appended && (this.isUnmasking || flags.input || flags.raw) && (!flags.raw || !appendEager) && !flags.tail;
if (isResolved) details.rawInserted = this.char;
this._value = details.inserted = this.char;
const details = new ChangeDetails({
inserted: this.char,
rawInserted: isResolved ? this.char: '',
})
this._value = this.char;
this._isRawInput = isResolved && (flags.raw || flags.input);

return details;
Expand Down
11 changes: 4 additions & 7 deletions packages/imask/src/masked/pattern/input-definition.ts
Expand Up @@ -107,10 +107,10 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo

const state = this.masked.state;
// simulate input
const details = this.masked._appendChar(ch, this.currentMaskFlags(flags));
let details = this.masked._appendChar(ch, this.currentMaskFlags(flags));

if (details.inserted && this.doValidate(flags) === false) {
details.inserted = details.rawInserted = '';
details = new ChangeDetails();
this.masked.state = state;
}

Expand All @@ -129,13 +129,10 @@ class PatternInputDefinition<Opts extends FactoryOpts=any> implements PatternBlo
}

_appendPlaceholder (): ChangeDetails {
const details = new ChangeDetails();

if (this.isFilled || this.isOptional) return details;
if (this.isFilled || this.isOptional) return new ChangeDetails();

this.isFilled = true;
details.inserted = this.placeholderChar;
return details;
return new ChangeDetails({ inserted: this.placeholderChar });
}

_appendEager (): ChangeDetails {
Expand Down
2 changes: 1 addition & 1 deletion packages/imask/src/masked/repeat.ts
Expand Up @@ -87,7 +87,7 @@ class RepeatBlock<M extends FactoryArg> extends MaskedPattern {

details.aggregate(blockDetails);

if (blockDetails.skip || blockDetails.rawInserted) break; // go next char
if (blockDetails.consumed) break; // go next char
}

return details;
Expand Down
36 changes: 36 additions & 0 deletions packages/imask/test/masked/number.ts
Expand Up @@ -13,6 +13,8 @@ describe('MaskedNumber', function () {
thousandsSeparator: '',
radix: ',',
scale: 2,
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
});
masked.unmaskedValue = '';
});
Expand Down Expand Up @@ -113,4 +115,38 @@ describe('MaskedNumber', function () {
masked.updateOptions({ scale: 0 });
assert.strictEqual(masked.value, '99');
});

describe('#autofix', function () {
const max = 200;
const min = -10;

beforeEach(function () {
masked.updateOptions({
autofix: true,
max,
min,
});
masked.unmaskedValue = '100';
});

it('should round to max on input at the end', function () {
masked.splice(3, 0, '1', DIRECTION.NONE, { input: true, raw: true });
assert.strictEqual(masked.typedValue, max);
});

it('should round to max on input at the beginning', function () {
masked.splice(0, 0, '1', DIRECTION.NONE, { input: true, raw: true });
assert.strictEqual(masked.typedValue, max);
});

it('should round to max on input in the middle', function () {
masked.splice(1, 0, '1', DIRECTION.NONE, { input: true, raw: true });
assert.strictEqual(masked.typedValue, max);
});

it('should round to min on input sign at the beginning', function () {
masked.splice(0, 0, '-', DIRECTION.NONE, { input: true, raw: true });
assert.strictEqual(masked.typedValue, min);
});
});
});

0 comments on commit cbc9d86

Please sign in to comment.