Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 73 additions & 98 deletions src/web/AdvancedTextInputMaskListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ class MaskedTextChangedListener {
public textField: Field | null = null;
public allowedKeys: string;

private afterText: string = '';

constructor(
primaryFormat: string,
affineFormats: string[] = [],
Expand All @@ -49,6 +47,41 @@ class MaskedTextChangedListener {
this.allowedKeys = allowedKeys;
}

private applyMaskAndSetText = (
textField: Field,
rawText: string,
caretPosition: number,
isDeletion: boolean,
autocomplete?: boolean
): MaskResult => {
const filteredText = this.allowedKeys
? [...rawText].filter((char) => this.allowedKeys.includes(char)).join('')
: rawText;

const caretGravity = {
type: isDeletion ? CaretGravityType.Backward : CaretGravityType.Forward,
autoskip: isDeletion ? this.autoskip : false,
autocomplete: isDeletion ? false : autocomplete ?? this.autocomplete,
};

const textAndCaret = new CaretString(
filteredText,
caretPosition,
caretGravity
);

const mask = this.pickMask(textAndCaret);
const result = mask.apply(textAndCaret);

textField.value = result.formattedText.string;
textField.setSelectionRange(
result.formattedText.caretPosition,
result.formattedText.caretPosition
);

return result;
};

public get primaryMask(): Mask {
return this.maskGetOrCreate(this.primaryFormat, this.customNotations);
}
Expand All @@ -58,26 +91,15 @@ class MaskedTextChangedListener {
return;
}

const newText = this.allowedKeys
? [...text].filter((char) => this.allowedKeys.includes(char)).join('')
: text;
const isDeletion = this.textField.value.length > text.length;

const useAutocomplete = autocomplete ?? this.autocomplete;
const textAndCaret = new CaretString(newText, newText.length, {
type: CaretGravityType.Forward,
autocomplete: useAutocomplete,
autoskip: false,
});

const result: MaskResult = this.pickMask(textAndCaret).apply(textAndCaret);
this.textField.value = result.formattedText.string;

this.textField.setSelectionRange(
result.formattedText.caretPosition,
result.formattedText.caretPosition
this.applyMaskAndSetText(
this.textField,
text,
text.length,
isDeletion,
autocomplete
);

this.afterText = result.formattedText.string;
}

public setAllowedKeys = (allowedKeys: string): void => {
Expand Down Expand Up @@ -127,30 +149,8 @@ class MaskedTextChangedListener {
const selectionStart = textField.selectionStart || 0;
const isInside = selectionStart < text.length;
const caretPosition = isDeletion || isInside ? selectionStart : text.length;
const useAutocomplete = isDeletion ? false : this.autocomplete;
const useAutoskip = isDeletion ? this.autoskip : false;

const caretGravity = {
type: isDeletion ? CaretGravityType.Backward : CaretGravityType.Forward,
autoskip: useAutoskip,
autocomplete: useAutocomplete,
};
const newText = this.allowedKeys
? [...text].filter((char) => this.allowedKeys.includes(char)).join('')
: text;
const textAndCaret = new CaretString(newText, caretPosition, caretGravity);
const mask = this.pickMask(textAndCaret);
const result = mask.apply(textAndCaret);

textField.value = result.formattedText.string;
textField.setSelectionRange(
result.formattedText.caretPosition,
result.formattedText.caretPosition
);

this.afterText = result.formattedText.string;

return result;
return this.applyMaskAndSetText(textField, text, caretPosition, isDeletion);
};

handleFocus = (
Expand All @@ -159,61 +159,44 @@ class MaskedTextChangedListener {
if (this.autocomplete) {
const textField = event.target as unknown as HTMLInputElement;
const text = textField.value.length > 0 ? textField.value : '';
const textAndCaret = new CaretString(text, text.length, {
type: CaretGravityType.Forward,
autocomplete: this.autocomplete,
autoskip: false,
});
const result = this.pickMask(textAndCaret).apply(textAndCaret);

this.afterText = result.formattedText.string;
textField.value = this.afterText;

textField.setSelectionRange(
result.formattedText.caretPosition,
result.formattedText.caretPosition
);

this.applyMaskAndSetText(textField, text, text.length, false, true);
}
};

pickMask = (text: CaretString): Mask => {
if (this.affineFormats.length === 0) {
if (!this.affineFormats.length) {
return this.primaryMask;
}

const primaryAffinity = this.calculateAffinity(this.primaryMask, text);
const masksAndAffinities: MaskAffinity[] = [];

for (const format of this.affineFormats) {
const candidateMask = this.maskGetOrCreate(format, this.customNotations);
const affinity = this.calculateAffinity(candidateMask, text);
masksAndAffinities.push({ mask: candidateMask, affinity });
}

masksAndAffinities.sort((a, b) => b.affinity - a.affinity);

let insertIndex = -1;
for (let i = 0; i < masksAndAffinities.length; i++) {
const affinity = masksAndAffinities[i]?.affinity;
if (affinity && primaryAffinity === affinity) {
insertIndex = i;
break;
}
}

if (insertIndex >= 0) {
masksAndAffinities.splice(insertIndex, 0, {
const candidates: MaskAffinity[] = [
{
mask: this.primaryMask,
affinity: primaryAffinity,
});
} else {
masksAndAffinities.push({
mask: this.primaryMask,
affinity: primaryAffinity,
});
}

return masksAndAffinities[0]!.mask;
affinity: calculateAffinityOfMask(
this.affinityCalculationStrategy,
this.primaryMask,
text
),
},
...this.affineFormats.map((format) => {
const candidateMask = this.maskGetOrCreate(
format,
this.customNotations
);
return {
mask: candidateMask,
affinity: calculateAffinityOfMask(
this.affinityCalculationStrategy,
candidateMask,
text
),
};
}),
];

candidates.sort((a, b) => b.affinity - a.affinity);

return candidates[0]!.mask;
};

private maskGetOrCreate = (
Expand All @@ -223,14 +206,6 @@ class MaskedTextChangedListener {
this.rightToLeft
? RTLMask.getOrCreate(format, customNotations)
: Mask.getOrCreate(format, customNotations);

private calculateAffinity(mask: Mask, text: CaretString): number {
return calculateAffinityOfMask(
this.affinityCalculationStrategy,
mask,
text
);
}
}

export default MaskedTextChangedListener;
21 changes: 21 additions & 0 deletions src/web/helper/AutoCompelitionStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Next } from '../model/types';

class AutocompletionStack extends Array<Next> {
push(item: Next | null): number {
if (item == null) {
this.length = 0;
return 0;
}
return super.push(item);
}

pop(): Next {
return super.pop()!;
}

empty(): boolean {
return this.length === 0;
}
}

export default AutocompletionStack;
65 changes: 40 additions & 25 deletions src/web/helper/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,22 @@ import ValueState from '../model/state/ValueState';
import type { StateType } from '../model/types';
import FormatSanitizer from './FormatSanitizer';
import type { Notation } from '../../types';
import { FIXED_STATE_TYPES, OPTIONAL_STATE_TYPES } from '../model/constants';
import {
CLOSE_CURLY_BRACKET,
CLOSE_SQUARE_BRACKET,
ELLIPSIS_CHARACTER,
ESCAPE_CHARACTER,
FIXED_ALPHA_NUMERIC_CHARACTER,
FIXED_LITERAL_CHARACTER,
FIXED_NUMERIC_CHARACTER,
FIXED_STATE_TYPES,
OPEN_CURLY_BRACKET,
OPEN_SQUARE_BRACKET,
OPTIONAL_ALPHA_NUMERIC_CHARACTER,
OPTIONAL_LITERAL_CHARACTER,
OPTIONAL_NUMERIC_CHARACTER,
OPTIONAL_STATE_TYPES,
} from '../model/constants';
import FormatError from './FormatError';

export default class Compiler {
Expand Down Expand Up @@ -35,19 +50,19 @@ export default class Compiler {
const char = formatString.charAt(0);

switch (char) {
case '[':
if (lastCharacter !== '\\') {
case OPEN_SQUARE_BRACKET:
if (lastCharacter !== ESCAPE_CHARACTER) {
return this.compileInternal(formatString.slice(1), true, false, char);
}
break;
case '{':
if (lastCharacter !== '\\') {
case OPEN_CURLY_BRACKET:
if (lastCharacter !== ESCAPE_CHARACTER) {
return this.compileInternal(formatString.slice(1), false, true, char);
}
break;
case ']':
case '}':
if (lastCharacter !== '\\') {
case CLOSE_CURLY_BRACKET:
case CLOSE_SQUARE_BRACKET:
if (lastCharacter !== ESCAPE_CHARACTER) {
return this.compileInternal(
formatString.slice(1),
false,
Expand All @@ -56,8 +71,8 @@ export default class Compiler {
);
}
break;
case '\\':
if (lastCharacter !== '\\') {
case ESCAPE_CHARACTER:
if (lastCharacter !== ESCAPE_CHARACTER) {
return this.compileInternal(
formatString.slice(1),
valuable,
Expand All @@ -70,38 +85,38 @@ export default class Compiler {

if (valuable) {
switch (char) {
case '0':
case FIXED_NUMERIC_CHARACTER:
return new ValueState(
this.compileInternal(formatString.substring(1), true, false, char),
FIXED_STATE_TYPES.numeric
);
case 'A':
case FIXED_LITERAL_CHARACTER:
return new ValueState(
this.compileInternal(formatString.substring(1), true, false, char),
FIXED_STATE_TYPES.literal
);
case '_':
case FIXED_ALPHA_NUMERIC_CHARACTER:
return new ValueState(
this.compileInternal(formatString.substring(1), true, false, char),
FIXED_STATE_TYPES.alphaNumeric
);
case '…':
case ELLIPSIS_CHARACTER:
// Ellipses remain elliptical: re-construct inherited type from lastCharacter
return new ValueState(
null,
this.determineInheritedType(lastCharacter)
);
case '9':
case OPTIONAL_NUMERIC_CHARACTER:
return new OptionalValueState(
this.compileInternal(formatString.substring(1), true, false, char),
OPTIONAL_STATE_TYPES.numeric
);
case 'a':
case OPTIONAL_LITERAL_CHARACTER:
return new OptionalValueState(
this.compileInternal(formatString.substring(1), true, false, char),
OPTIONAL_STATE_TYPES.literal
);
case '-':
case OPTIONAL_ALPHA_NUMERIC_CHARACTER:
return new OptionalValueState(
this.compileInternal(formatString.substring(1), true, false, char),
OPTIONAL_STATE_TYPES.alphaNumeric
Expand Down Expand Up @@ -132,16 +147,16 @@ export default class Compiler {
lastCharacter: string | null
): StateType | Notation {
switch (lastCharacter) {
case '0':
case '9':
case FIXED_NUMERIC_CHARACTER:
case OPTIONAL_NUMERIC_CHARACTER:
return FIXED_STATE_TYPES.numeric;
case 'A':
case 'a':
case FIXED_LITERAL_CHARACTER:
case OPTIONAL_LITERAL_CHARACTER:
return FIXED_STATE_TYPES.literal;
case '_':
case '-':
case '…':
case '[':
case FIXED_ALPHA_NUMERIC_CHARACTER:
case OPTIONAL_ALPHA_NUMERIC_CHARACTER:
case ELLIPSIS_CHARACTER:
case OPEN_SQUARE_BRACKET:
return FIXED_STATE_TYPES.alphaNumeric;
default:
return this.determineTypeWithCustomNotations(lastCharacter);
Expand Down
Loading