diff --git a/src/mask/mask.ts b/src/mask/mask.ts index fd1245d450..0ac663457e 100644 --- a/src/mask/mask.ts +++ b/src/mask/mask.ts @@ -1,20 +1,26 @@ -import { processValueWithPattern, getMaskedValueByPattern, getUnmaskedValueByPattern } from "./mask_utils"; +import { IMaskedValue } from "./mask_utils"; export class InputMaskBase { - private _prevSelectionStart: number; - constructor(private input: HTMLInputElement, private mask: string) { + protected _prevSelectionStart: number; + constructor(protected input: HTMLInputElement, protected mask: any) { this.applyValue(mask); this.addInputEventListener(); } - applyValue(mask: string) { + protected getMaskedValue(mask: any, option?: any): string { + return this.input.value; + } + protected processMaskedValue(mask: any, option?: any): IMaskedValue { + return { text: this.input.value, cursorPosition: this.input.selectionStart }; + } + applyValue(mask: any) { if(!!this.input) { - this.input.value = getMaskedValueByPattern(getUnmaskedValueByPattern(this.input.value, mask, false), mask); + this.input.value = this.getMaskedValue(mask); } } - updateMaskedString(mask: string): void { + protected updateMaskedString(mask: any): void { if(!!this.input) { - const result = processValueWithPattern(this.input.value, mask, this._prevSelectionStart, this.input.selectionStart); + const result = this.processMaskedValue(mask); this.input.value = result.text; this.input.setSelectionRange(result.cursorPosition, result.cursorPosition); } diff --git a/src/mask/mask_pattern.ts b/src/mask/mask_pattern.ts new file mode 100644 index 0000000000..05af4f53c8 --- /dev/null +++ b/src/mask/mask_pattern.ts @@ -0,0 +1,86 @@ +import { InputMaskBase } from "./mask"; +import { IMaskedValue, settings, syntacticAnalysisMask } from "./mask_utils"; + +export function getMaskedValueByPattern(str: string, pattern: string, matchWholeMask = true): string { + let result = ""; + let strIndex = 0; + + const parsedMask = syntacticAnalysisMask(pattern); + for(let maskIndex = 0; maskIndex < parsedMask.length; maskIndex++) { + if(parsedMask[maskIndex].type === "regex") { + const currentDefinition = settings.definitions[parsedMask[maskIndex].value]; + if(strIndex < str.length && str[strIndex].match(currentDefinition)) { + result += str[strIndex]; + } else if(matchWholeMask) { + result += settings.placeholderChar; + } else { + break; + } + strIndex++; + } else if(parsedMask[maskIndex].type === "const") { + result += parsedMask[maskIndex].value; + if(parsedMask[maskIndex].value === str[strIndex]) { + strIndex++; + } + } + } + return result; +} + +export function getUnmaskedValueByPattern(str: string, pattern: string, matchWholeMask: boolean): string { + let result = ""; + if(!str) return result; + + const parsedMask = syntacticAnalysisMask(pattern); + for(let index = 0; index < parsedMask.length; index++) { + if(parsedMask[index].type === "regex") { + const currentDefinition = settings.definitions[parsedMask[index].value]; + if(!!str[index] && str[index].match(currentDefinition)) { + result += str[index]; + } else if(matchWholeMask) { + result = ""; + break; + } else { + break; + } + } + } + return result; +} + +export function processValueWithPattern(str: string, pattern: string, prevСursorPosition: number, currentCursorPosition: number): IMaskedValue { + let result = ""; + if(!str) return { text: result, cursorPosition: currentCursorPosition }; + let leftPartResult = ""; + let rigthPartResult = ""; + let centerPart = ""; + let newCursorPosition = currentCursorPosition; + + const leftPartRange = Math.min(prevСursorPosition, currentCursorPosition, pattern.length - 1); + leftPartResult = getUnmaskedValueByPattern(str.substring(0, leftPartRange), pattern.substring(0, leftPartRange), false); + rigthPartResult = getUnmaskedValueByPattern(str.substring(currentCursorPosition), pattern.substring(prevСursorPosition), false); + if(currentCursorPosition > prevСursorPosition) { + centerPart = getUnmaskedValueByPattern(str.substring(leftPartRange, currentCursorPosition), pattern.substring(leftPartRange), false); + newCursorPosition = getMaskedValueByPattern(leftPartResult + centerPart, pattern, false).length; + + } + result = getMaskedValueByPattern(leftPartResult + centerPart + rigthPartResult, pattern); + return { text: result, cursorPosition: newCursorPosition }; +} + +export class InputMaskPattern extends InputMaskBase { + protected getMaskedValue(mask: string, option?: any): string { + return getMaskedValueByPattern(getUnmaskedValueByPattern(this.input.value, mask, false), mask); + } + protected processMaskedValue(mask: string): IMaskedValue { + return processValueWithPattern(this.input.value, mask, this._prevSelectionStart, this.input.selectionStart); + } + + protected updateMaskedString(mask: string, option?: any): void { + if(!!this.input) { + const result = this.processMaskedValue(mask); + this.input.value = result.text; + this.input.setSelectionRange(result.cursorPosition, result.cursorPosition); + } + } +} \ No newline at end of file diff --git a/src/mask/mask_utils.ts b/src/mask/mask_utils.ts index 74096cea9c..f015523709 100644 --- a/src/mask/mask_utils.ts +++ b/src/mask/mask_utils.ts @@ -1,4 +1,4 @@ -interface IMaskedValue { +export interface IMaskedValue { text: string; cursorPosition: number; } @@ -6,6 +6,13 @@ interface IMaskedValue { export var settings = { placeholderChar: "_", escapedChar: "\\", + numberOptions: { + decimal: ".", + thousands: ",", + precision: 2, + allowNegative: true, + align: "right" + }, definitions: <{ [key: string]: RegExp }>{ "9": /[0-9]/, "a": /[a-zA-Z]/, @@ -15,12 +22,14 @@ export var settings = { interface IMaskLiteral { type: "const" | "regex"; + repeat?: boolean; value: any; } export function syntacticAnalysisMask(mask: string): Array { const result: Array = []; let prevChartIsEscaped = false; + let prevChart; const definitionsKeys = Object.keys(settings.definitions); for(let index = 0; index < mask.length; index++) { @@ -30,118 +39,54 @@ export function syntacticAnalysisMask(mask: string): Array { } else if(prevChartIsEscaped) { prevChartIsEscaped = false; result.push({ type: "const", value: currentChar }); + } else if(currentChar === "+") { + result[result.length - 1].repeat = true; } else { result.push({ type: definitionsKeys.indexOf(currentChar) !== -1 ? "regex" : "const", value: currentChar }); } + prevChart = currentChar; } return result; } -export function getMaskedValueByPatternOld(str: string, pattern: string, matchWholeMask = true): string { - let result = ""; - let strIndex = 0; - for(let maskIndex = 0; maskIndex < pattern.length; maskIndex++) { - const currentDefinition = settings.definitions[pattern[maskIndex]]; - if(currentDefinition) { - if(strIndex < str.length && str[strIndex].match(currentDefinition)) { - result += str[strIndex]; - } else if(matchWholeMask) { - result += settings.placeholderChar; - } else { - break; - } - strIndex++; - } else { - result += pattern[maskIndex]; - } - } - return result; -} +// export function getMaskedValueByPatternOld(str: string, pattern: string, matchWholeMask = true): string { +// let result = ""; +// let strIndex = 0; +// for(let maskIndex = 0; maskIndex < pattern.length; maskIndex++) { +// const currentDefinition = settings.definitions[pattern[maskIndex]]; +// if(currentDefinition) { +// if(strIndex < str.length && str[strIndex].match(currentDefinition)) { +// result += str[strIndex]; +// } else if(matchWholeMask) { +// result += settings.placeholderChar; +// } else { +// break; +// } +// strIndex++; +// } else { +// result += pattern[maskIndex]; +// } +// } +// return result; +// } -export function getMaskedValueByPattern(str: string, pattern: string, matchWholeMask = true): string { - let result = ""; - let strIndex = 0; +// export function getUnmaskedValueByPatternOld(str: string, pattern: string, matchWholeMask: boolean): string { +// let result = ""; +// if(!str) return result; - const parsedMask = syntacticAnalysisMask(pattern); - for(let maskIndex = 0; maskIndex < parsedMask.length; maskIndex++) { - if(parsedMask[maskIndex].type === "regex") { - const currentDefinition = settings.definitions[parsedMask[maskIndex].value]; - if(strIndex < str.length && str[strIndex].match(currentDefinition)) { - result += str[strIndex]; - } else if(matchWholeMask) { - result += settings.placeholderChar; - } else { - break; - } - strIndex++; - } else if(parsedMask[maskIndex].type === "const") { - result += parsedMask[maskIndex].value; - if(parsedMask[maskIndex].value === str[strIndex]) { - strIndex++; - } - } - } - return result; -} - -export function getUnmaskedValueByPattern(str: string, pattern: string, matchWholeMask: boolean): string { - let result = ""; - if(!str) return result; - - const parsedMask = syntacticAnalysisMask(pattern); - for(let index = 0; index < parsedMask.length; index++) { - if(parsedMask[index].type === "regex") { - const currentDefinition = settings.definitions[parsedMask[index].value]; - if(!!str[index] && str[index].match(currentDefinition)) { - result += str[index]; - } else if(matchWholeMask) { - result = ""; - break; - } else { - break; - } - } - } - return result; -} - -export function getUnmaskedValueByPatternOld(str: string, pattern: string, matchWholeMask: boolean): string { - let result = ""; - if(!str) return result; - - for(let index = 0; index < pattern.length; index++) { - const currentDefinition = settings.definitions[pattern[index]]; - if(currentDefinition) { - if(!!str[index] && str[index].match(currentDefinition)) { - result += str[index]; - } else if(matchWholeMask) { - result = ""; - break; - } else { - break; - } - } - } - return result; -} - -export function processValueWithPattern(str: string, pattern: string, prevСursorPosition: number, currentCursorPosition: number): IMaskedValue { - let result = ""; - if(!str) return { text: result, cursorPosition: currentCursorPosition }; - let leftPartResult = ""; - let rigthPartResult = ""; - let centerPart = ""; - let newCursorPosition = currentCursorPosition; - - const leftPartRange = Math.min(prevСursorPosition, currentCursorPosition, pattern.length - 1); - leftPartResult = getUnmaskedValueByPattern(str.substring(0, leftPartRange), pattern.substring(0, leftPartRange), false); - rigthPartResult = getUnmaskedValueByPattern(str.substring(currentCursorPosition), pattern.substring(prevСursorPosition), false); - if(currentCursorPosition > prevСursorPosition) { - centerPart = getUnmaskedValueByPattern(str.substring(leftPartRange, currentCursorPosition), pattern.substring(leftPartRange), false); - newCursorPosition = getMaskedValueByPattern(leftPartResult + centerPart, pattern, false).length; - - } - result = getMaskedValueByPattern(leftPartResult + centerPart + rigthPartResult, pattern); - return { text: result, cursorPosition: newCursorPosition }; -} \ No newline at end of file +// for(let index = 0; index < pattern.length; index++) { +// const currentDefinition = settings.definitions[pattern[index]]; +// if(currentDefinition) { +// if(!!str[index] && str[index].match(currentDefinition)) { +// result += str[index]; +// } else if(matchWholeMask) { +// result = ""; +// break; +// } else { +// break; +// } +// } +// } +// return result; +// } diff --git a/src/mask/number_mask.ts b/src/mask/number_mask.ts new file mode 100644 index 0000000000..cdf37d62d7 --- /dev/null +++ b/src/mask/number_mask.ts @@ -0,0 +1,68 @@ +import { InputMaskBase } from "./mask"; +import { IMaskedValue, settings, syntacticAnalysisMask } from "./mask_utils"; + +interface INumberMaskOption { + mask?: string; + align?: "left" | "right"; + allowNegative?: boolean; + decimal?: string; + precision?: number; + thousands?: string; +} +interface INumericalСomposition { + integralPart: number; + fractionalPart?: number; +} + +export function parseNumber(str: any): INumericalСomposition { + const result: INumericalСomposition = { integralPart: 0, fractionalPart: 0 }; + const input = str.toString(); + + const parts = input.trim().split("."); + if(parts.length >= 2) { + result.integralPart = parseInt(parts[0].trim()); + result.fractionalPart = parseInt(parts[1].trim()); + } else if(parts.length == 1) { + result.integralPart = parseInt(parts[0].trim()); + } else { + result.integralPart = parseInt(input.trim()); + } + + return result; +} + +export function getNumberMaskedValue(str: string | number, mask: string, option?: INumberMaskOption) { + const decimalSeparator = option?.decimal || settings.numberOptions.decimal; + const parsedMask = syntacticAnalysisMask(mask); + + const input = str.toString(); + const parsedNumber = parseNumber(input); + + let result = ""; + let maskIndex = 0; + + for(let index = 0; index < parsedNumber.integralPart.toString().length; index++) { + + } + return result; +} + +export function getNumberUnmaskedValue(str: string, mask: string, option?: INumberMaskOption) { + return str; +} + +export class InputMaskNumber extends InputMaskBase { + + constructor(input: HTMLInputElement, mask?: INumberMaskOption) { + super(input, mask); + } + + protected getMaskedValue(mask: string, option: INumberMaskOption): string { + return getNumberMaskedValue(getNumberUnmaskedValue(this.input.value, mask, option), mask, option); + } + + protected processMaskedValue(mask: string): IMaskedValue { + // return processValueWithPattern(this.input.value, mask, this._prevSelectionStart, this.input.selectionStart); + return { text: this.input.value, cursorPosition: this.input.selectionStart }; + } +} \ No newline at end of file diff --git a/src/question_text.ts b/src/question_text.ts index bcf9e5bf96..2691ce8fe1 100644 --- a/src/question_text.ts +++ b/src/question_text.ts @@ -11,6 +11,8 @@ import { ExpressionRunner } from "./conditions"; import { SurveyModel } from "./survey"; import { CssClassBuilder } from "./utils/cssClassBuilder"; import { InputMaskBase } from "./mask/mask"; +import { InputMaskNumber } from "./mask/number_mask"; +import { InputMaskPattern } from "./mask/mask_pattern"; /** * A class that describes the Single-Line Input question type. @@ -469,7 +471,11 @@ export class QuestionTextModel extends QuestionTextBase { @property() mask: string; private updateMaskInstance() { if (!this.maskInstance) { - this.maskInstance = new InputMaskBase(this.input, this.mask); + if(this.mask === "decimal") { + this.maskInstance = new InputMaskNumber(this.input); + } else if(this.mask) { + this.maskInstance = new InputMaskPattern(this.input, this.mask); + } } else { this.maskInstance.updateInputElement(this.mask); } diff --git a/tests/entries/test.ts b/tests/entries/test.ts index 44b5fdeb38..d8dcaa7220 100644 --- a/tests/entries/test.ts +++ b/tests/entries/test.ts @@ -65,6 +65,7 @@ export * from "../responsivityTests"; export * from "../svgRegistryTests"; export * from "../utilstests"; export * from "../mask_pattern_tests"; +export * from "../mask_number_tests"; export * from "../stylesManagerTests"; export * from "../headerTests"; diff --git a/tests/mask_number_tests.ts b/tests/mask_number_tests.ts new file mode 100644 index 0000000000..4e6b59cca5 --- /dev/null +++ b/tests/mask_number_tests.ts @@ -0,0 +1,46 @@ +import { syntacticAnalysisMask } from "../src/mask/mask_utils"; +import { getNumberMaskedValue, getNumberUnmaskedValue, parseNumber } from "../src/mask/number_mask"; + +export default QUnit.module("Numeric mask"); + +QUnit.only("parseNumber", assert => { + assert.equal(parseNumber(123).integralPart, 123); + assert.equal(parseNumber(123).fractionalPart, 0); + assert.equal(parseNumber("123").integralPart, 123); + assert.equal(parseNumber("123").fractionalPart, 0); + + assert.equal(parseNumber(123.45).integralPart, 123); + assert.equal(parseNumber(123.45).fractionalPart, 45); + assert.equal(parseNumber("123.45").integralPart, 123); + assert.equal(parseNumber("123.45").fractionalPart, 45); + + assert.equal(parseNumber(".45").integralPart, 0); + assert.equal(parseNumber(".45").fractionalPart, 45); + assert.equal(parseNumber("123.").integralPart, 123); + assert.equal(parseNumber("123.").fractionalPart, 0); +}); + +QUnit.test("parsing numeric mask simple pattern", function(assert) { + const mask = "9+"; + let result = syntacticAnalysisMask(mask); + assert.equal(result.length, 1); + assert.equal(result[0].type, "regex"); + assert.equal(result[0].value, "9"); + assert.equal(result[0].repeat, true); +}); + +QUnit.test("get numeric masked valid text", function(assert) { + const customMask = "9+"; + assert.equal(getNumberMaskedValue(123, customMask), "123"); + assert.equal(getNumberMaskedValue(123456, customMask), "123,456"); + assert.equal(getNumberMaskedValue(123456.78, customMask), "123,456.78"); + assert.equal(getNumberMaskedValue(123456.789, customMask), "123,456.78"); +}); + +// QUnit.test("get numeric masked invalid text", function(assert) { +// const customMask = "9+"; +// assert.equal(getMaskedValueByPattern("", customMask, true), "0"); +// assert.equal(getMaskedValueByPattern("9", customMask, true), "9"); +// assert.equal(getMaskedValueByPattern("123A", customMask, true), "123"); +// assert.equal(getMaskedValueByPattern("123a", customMask, true), "123"); +// }); \ No newline at end of file diff --git a/tests/mask_pattern_tests.ts b/tests/mask_pattern_tests.ts index 1a612e308c..e390fa708b 100644 --- a/tests/mask_pattern_tests.ts +++ b/tests/mask_pattern_tests.ts @@ -1,4 +1,5 @@ -import { syntacticAnalysisMask, processValueWithPattern, getMaskedValueByPattern, getUnmaskedValueByPattern, settings } from "../src/mask/mask_utils"; +import { syntacticAnalysisMask, settings } from "../src/mask/mask_utils"; +import { processValueWithPattern, getMaskedValueByPattern, getUnmaskedValueByPattern } from "../src/mask/mask_pattern"; export default QUnit.module("Pattern mask");