From 7b296a59b1300b8e104c59124505d20f9f624617 Mon Sep 17 00:00:00 2001 From: krutoo Date: Mon, 1 Jul 2024 13:45:15 +0500 Subject: [PATCH 1/3] refactor - deprecated features removed (major) - types moved to separated modules (patch) - some types are renamed (major) --- .vscode/settings.json | 1 + example/rsbuild.config.ts | 3 + example/src/index.tsx | 6 +- example/tsconfig.json | 4 ++ src/core/__test__/changes.test.ts | 50 +++++++------- src/core/changes.ts | 32 ++++----- src/core/mod.ts | 7 +- src/core/range.ts | 47 +++++++------ src/core/reducer.ts | 82 ++++++---------------- src/core/types.ts | 57 ++++++++++++++++ src/dom/__test__/utils.test.ts | 8 +-- src/dom/create-input-mask.ts | 82 ++++++++++++++++++++++ src/dom/mod.ts | 109 +----------------------------- src/dom/types.ts | 20 ++++++ src/dom/utils.ts | 82 +++++++++------------- 15 files changed, 298 insertions(+), 292 deletions(-) create mode 100644 src/core/types.ts create mode 100644 src/dom/create-input-mask.ts create mode 100644 src/dom/types.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 9ce867b..6f4c053 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "deno.enable": true, + "deno.lint": true, "editor.formatOnSave": true, "editor.defaultFormatter": "denoland.vscode-deno" } diff --git a/example/rsbuild.config.ts b/example/rsbuild.config.ts index 6b897c6..519d322 100644 --- a/example/rsbuild.config.ts +++ b/example/rsbuild.config.ts @@ -6,5 +6,8 @@ export default defineConfig({ polyfill: "off", assetPrefix: "/input-mask/", }, + html: { + title: "Example of @krutoo/input-mask", + }, plugins: [pluginReact()], }); diff --git a/example/src/index.tsx b/example/src/index.tsx index e2a5dff..b41334d 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,6 +1,6 @@ import { InputHTMLAttributes, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; -import { type InputMaskControl, InputMask } from "../../src/dom/mod"; +import { type InputMask, createInputMask } from "@krutoo/input-mask/dom"; import "./index.css"; const variants = [ @@ -44,11 +44,11 @@ function DemoBlock({ label: string; inputProps?: InputHTMLAttributes; }) { - const [inputMask, setInputMask] = useState(null); + const [inputMask, setInputMask] = useState(null); const ref = useRef(null); useEffect(() => { - const im = InputMask(ref.current!, { mask }); + const im = createInputMask(ref.current!, { mask }); setInputMask(im); diff --git a/example/tsconfig.json b/example/tsconfig.json index 33f586f..fe75734 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -8,6 +8,10 @@ "jsx": "react-jsx", "strict": true, "skipLibCheck": true, + "paths": { + "@krutoo/input-mask/core": ["../src/core/mod.ts"], + "@krutoo/input-mask/dom": ["../src/dom/mod.ts"] + } }, "exclude": ["node_modules"] } diff --git a/src/core/__test__/changes.test.ts b/src/core/__test__/changes.test.ts index 32c1133..def26cd 100644 --- a/src/core/__test__/changes.test.ts +++ b/src/core/__test__/changes.test.ts @@ -1,13 +1,13 @@ import { describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; import { defineChanges } from '../changes.ts'; -import { Range } from '../range.ts'; +import { RangeUtil } from '../range.ts'; describe('defineChanges()', () => { describe('insert cases', () => { test('insertion to empty field', () => { - const prev = { value: '', range: Range.of(0) }; - const next = { value: 'text', range: Range.of(4, 4) }; + const prev = { value: '', range: RangeUtil.of(0) }; + const next = { value: 'text', range: RangeUtil.of(4, 4) }; const result = defineChanges(prev, next); @@ -20,8 +20,8 @@ describe('defineChanges()', () => { }); test('insertion to end of non empty field', () => { - const prev = { value: 'foo', range: Range.of(3) }; - const next = { value: 'foobar', range: Range.of(6, 6) }; + const prev = { value: 'foo', range: RangeUtil.of(3) }; + const next = { value: 'foobar', range: RangeUtil.of(6, 6) }; const result = defineChanges(prev, next); @@ -34,8 +34,8 @@ describe('defineChanges()', () => { }); test('insertion to start of non empty field', () => { - const prev = { value: 'foo', range: Range.of(0) }; - const next = { value: 'barfoo', range: Range.of(3, 3) }; + const prev = { value: 'foo', range: RangeUtil.of(0) }; + const next = { value: 'barfoo', range: RangeUtil.of(3, 3) }; const result = defineChanges(prev, next); @@ -48,8 +48,8 @@ describe('defineChanges()', () => { }); test('insertion to middle of non empty field', () => { - const prev = { value: 'foobaz', range: Range.of(3) }; - const next = { value: 'foobarbaz', range: Range.of(6, 6) }; + const prev = { value: 'foobaz', range: RangeUtil.of(3) }; + const next = { value: 'foobarbaz', range: RangeUtil.of(6, 6) }; const result = defineChanges(prev, next); @@ -65,8 +65,8 @@ describe('defineChanges()', () => { describe('delete cases', () => { describe('delete backward cases', () => { test('delete from end: soft', () => { - const prev = { value: 'text', range: Range.of(4) }; - const next = { value: 'tex', range: Range.of(3) }; + const prev = { value: 'text', range: RangeUtil.of(4) }; + const next = { value: 'tex', range: RangeUtil.of(3) }; const result = defineChanges(prev, next); @@ -79,8 +79,8 @@ describe('defineChanges()', () => { }); test('delete from end: hard', () => { - const prev = { value: 'text', range: Range.of(4) }; - const next = { value: '', range: Range.of(0) }; + const prev = { value: 'text', range: RangeUtil.of(4) }; + const next = { value: '', range: RangeUtil.of(0) }; const result = defineChanges(prev, next); @@ -93,8 +93,8 @@ describe('defineChanges()', () => { }); test('delete from middle: soft', () => { - const prev = { value: 'abcdef', range: Range.of(4) }; - const next = { value: 'abcef', range: Range.of(3) }; + const prev = { value: 'abcdef', range: RangeUtil.of(4) }; + const next = { value: 'abcef', range: RangeUtil.of(3) }; const result = defineChanges(prev, next); @@ -107,8 +107,8 @@ describe('defineChanges()', () => { }); test('delete from middle: hard', () => { - const prev = { value: 'abcdef', range: Range.of(4) }; - const next = { value: 'ef', range: Range.of(0) }; + const prev = { value: 'abcdef', range: RangeUtil.of(4) }; + const next = { value: 'ef', range: RangeUtil.of(0) }; const result = defineChanges(prev, next); @@ -123,8 +123,8 @@ describe('defineChanges()', () => { describe('delete forward cases', () => { test('delete from start: soft', () => { - const prev = { value: 'abcdef', range: Range.of(0) }; - const next = { value: 'bcdef', range: Range.of(0) }; + const prev = { value: 'abcdef', range: RangeUtil.of(0) }; + const next = { value: 'bcdef', range: RangeUtil.of(0) }; const result = defineChanges(prev, next); @@ -137,8 +137,8 @@ describe('defineChanges()', () => { }); test('delete from start: hard', () => { - const prev = { value: 'abcdef', range: Range.of(0) }; - const next = { value: '', range: Range.of(0) }; + const prev = { value: 'abcdef', range: RangeUtil.of(0) }; + const next = { value: '', range: RangeUtil.of(0) }; const result = defineChanges(prev, next); @@ -151,8 +151,8 @@ describe('defineChanges()', () => { }); test('delete from middle: soft', () => { - const prev = { value: 'abcdef', range: Range.of(3) }; - const next = { value: 'abcef', range: Range.of(3) }; + const prev = { value: 'abcdef', range: RangeUtil.of(3) }; + const next = { value: 'abcef', range: RangeUtil.of(3) }; const result = defineChanges(prev, next); @@ -165,8 +165,8 @@ describe('defineChanges()', () => { }); test('delete from middle: hard', () => { - const prev = { value: 'abcdef', range: Range.of(3) }; - const next = { value: 'abc', range: Range.of(3) }; + const prev = { value: 'abcdef', range: RangeUtil.of(3) }; + const next = { value: 'abc', range: RangeUtil.of(3) }; const result = defineChanges(prev, next); diff --git a/src/core/changes.ts b/src/core/changes.ts index 534fe89..5db023f 100644 --- a/src/core/changes.ts +++ b/src/core/changes.ts @@ -1,11 +1,11 @@ -import { Range } from './range.ts'; -import type { ChangeAction, InputState } from './reducer.ts'; +import type { ChangeAction, InputState } from './types.ts'; +import { RangeUtil } from './range.ts'; /** * Получив предыдущее и новое состояния текстового поля определит тип изменений. * ВАЖНО: только определяет изменения, ничего не знает про маски. */ -export const defineChanges = (prev: InputState, next: InputState): ChangeAction => { +export function defineChanges(prev: InputState, next: InputState): ChangeAction { const hasChanges = prev.value !== next.value; let type: ChangeAction['type'] = 'UNKNOWN'; @@ -14,7 +14,7 @@ export const defineChanges = (prev: InputState, next: InputState): ChangeAction // define type if (hasChanges) { if (next.value.length > prev.value.length) { - if (Range.size(prev.range) > 0) { + if (RangeUtil.size(prev.range) > 0) { type = 'REPLACE'; } else { type = 'INSERT'; @@ -26,14 +26,14 @@ export const defineChanges = (prev: InputState, next: InputState): ChangeAction if ( restored === prev.value || - (Range.size(prev.range) === 0 && Range.size(next.range) === 0) + (RangeUtil.size(prev.range) === 0 && RangeUtil.size(next.range) === 0) ) { type = 'DELETE'; } else { type = 'REPLACE'; } } - } else if (!Range.equals(prev.range, next.range)) { + } else if (!RangeUtil.equals(prev.range, next.range)) { // вставили то же самое что уже было введено type = 'REPLACE'; } @@ -44,27 +44,27 @@ export const defineChanges = (prev: InputState, next: InputState): ChangeAction payload = { ...next, insertPosition: prev.range.start, - insertIndices: Range.spreadOf(prev.range.start, next.range.end), + insertIndices: RangeUtil.spreadOf(prev.range.start, next.range.end), }; break; case 'DELETE': { let deleteIndices: number[] = []; - if (Range.size(prev.range) === 0) { + if (RangeUtil.size(prev.range) === 0) { // удалили какую-то часть текста (delete forward/backward, hard/soft...) if (next.range.start === prev.range.start) { // удалили после каретки (aka delete) const delta = prev.value.length - next.value.length; - deleteIndices = Range.spreadOf(prev.range.start, prev.range.start + delta); + deleteIndices = RangeUtil.spreadOf(prev.range.start, prev.range.start + delta); } else { // удалили перед кареткой (aka backspace) - deleteIndices = Range.spreadOf(next.range.start, prev.range.start); + deleteIndices = RangeUtil.spreadOf(next.range.start, prev.range.start); } } else { // просто вырезали выделенную часть - deleteIndices = Range.spreadOf(prev.range.start, prev.range.end); + deleteIndices = RangeUtil.spreadOf(prev.range.start, prev.range.end); } payload = { @@ -81,16 +81,16 @@ export const defineChanges = (prev: InputState, next: InputState): ChangeAction payload = { ...next, replacePosition: prev.range.start, - deleteIndices: Range.spread(prev.range), - insertIndices: Range.spreadOf(prev.range.start, next.range.end), + deleteIndices: RangeUtil.spread(prev.range), + insertIndices: RangeUtil.spreadOf(prev.range.start, next.range.end), }; } else { // вставили то же самое что уже было введено payload = { ...next, replacePosition: prev.range.start, - deleteIndices: Range.spread(prev.range), - insertIndices: Range.spread(prev.range), + deleteIndices: RangeUtil.spread(prev.range), + insertIndices: RangeUtil.spread(prev.range), }; } break; @@ -101,4 +101,4 @@ export const defineChanges = (prev: InputState, next: InputState): ChangeAction } return { type, payload } as ChangeAction; -}; +} diff --git a/src/core/mod.ts b/src/core/mod.ts index 9c9f49d..50a82ec 100644 --- a/src/core/mod.ts +++ b/src/core/mod.ts @@ -1,3 +1,4 @@ -export * from './range.ts'; -export * from './changes.ts'; -export * from './reducer.ts'; +export type * from './types.ts'; +export { RangeUtil } from './range.ts'; +export { defineChanges } from './changes.ts'; +export { createReducer } from './reducer.ts'; diff --git a/src/core/range.ts b/src/core/range.ts index 8491529..2dc885c 100644 --- a/src/core/range.ts +++ b/src/core/range.ts @@ -1,39 +1,42 @@ -export interface IRange { - start: number; - end: number; - - /** @deprecated Use "start" instead. */ - head: number; - - /** @deprecated Use "end" instead. */ - last: number; -} +import type { Range } from './types.ts'; /** * Работа с числовыми диапазонами. */ -export const Range = { - of: (start: number, end = start): IRange => ({ start, end, head: start, last: end }), +export abstract class RangeUtil { + static of(start: number, end = start): Range { + return { start, end }; + } - clone: (r: IRange): IRange => ({ ...r }), + static clone(range: Range): Range { + return { ...range }; + } - map: (r: IRange, cb: (n: number) => number): IRange => Range.of(cb(r.start), cb(r.end)), + static map(range: Range, callback: (n: number) => number): Range { + return RangeUtil.of(callback(range.start), callback(range.end)); + } - equals: (a: IRange, b: IRange): boolean => a.start === b.start && a.end === b.end, + static equals(a: Range, b: Range): boolean { + return a.start === b.start && a.end === b.end; + } - size: (r: IRange) => Math.max(r.start, r.end) - Math.min(r.start, r.end), + static size(range: Range): number { + return Math.max(range.start, range.end) - Math.min(range.start, range.end); + } - spread: (r: IRange): number[] => { + static spread(range: Range): number[] { const result = []; - if (r.start !== r.end) { - for (let i = r.start; i < r.end; i++) { + if (range.start !== range.end) { + for (let i = range.start; i < range.end; i++) { result.push(i); } } return result; - }, + } - spreadOf: (start: number, end: number): number[] => Range.spread(Range.of(start, end)), -}; + static spreadOf(start: number, end: number): number[] { + return RangeUtil.spread(RangeUtil.of(start, end)); + } +} diff --git a/src/core/reducer.ts b/src/core/reducer.ts index f6af1ba..c75c5a4 100644 --- a/src/core/reducer.ts +++ b/src/core/reducer.ts @@ -1,64 +1,20 @@ -import { type IRange, Range } from './range.ts'; - -export interface InputState { - range: IRange; - value: string; -} - -interface BaseAction { - type: T; - payload: InputState & P; -} - -export type UnknownAction = BaseAction<'UNKNOWN'>; - -export type InsertAction = BaseAction< - 'INSERT', - { - insertPosition: number; - insertIndices: number[]; - } ->; - -export type DeleteAction = BaseAction< - 'DELETE', - { - deleteDirection: 'backward' | 'forward'; - deleteIndices: number[]; - } ->; - -export type ReplaceAction = BaseAction< - 'REPLACE', - { - replacePosition: number; - deleteIndices: number[]; - insertIndices: number[]; - } ->; - -export type ChangeAction = - | InsertAction - | DeleteAction - | ReplaceAction - | UnknownAction; - -interface Reducer { - (state: InputState | undefined, action: ChangeAction): InputState; -} - -export interface ReducerOptions { - mask: string; - pattern: RegExp; - placeholder: string; -} +import type { + ChangeAction, + DeleteAction, + InputState, + InsertAction, + Reducer, + ReducerOptions, + ReplaceAction, +} from './types.ts'; +import { RangeUtil } from './range.ts'; // @todo fix caret position: "+|7 (" with paste "(111) 222-33-44" -export const createReducer = ({ +export function createReducer({ mask, pattern, placeholder, -}: ReducerOptions): Reducer => { +}: ReducerOptions): Reducer { const placeIndices = mask.split('').reduce((acc: number[], char, i) => { char === placeholder && acc.push(i); return acc; @@ -111,7 +67,7 @@ export const createReducer = ({ cleanChars.splice(insertIndex, 0, ...insertChars); if (nextCaretPosition) { - range = Range.of(nextCaretPosition); + range = RangeUtil.of(nextCaretPosition); } } @@ -125,7 +81,7 @@ export const createReducer = ({ const cleanChars = getCleanChars(state.value); const isForward = payload.deleteDirection === 'forward'; const deleteIndex = Index.getNearestPlace(payload.range.start, isForward); - const range = Range.of( + const range = RangeUtil.of( Math.max(placeIndices[0], Index.toMasked(deleteIndex) || 0), ); @@ -152,7 +108,7 @@ export const createReducer = ({ if (state.value === payload.value) { return { ...state, - range: Range.of( + range: RangeUtil.of( Index.toMasked(Index.getNearestPlace(payload.range.end)) || state.value.length, ), @@ -171,7 +127,7 @@ export const createReducer = ({ } const value = toMasked(cleanChars); - const range = Range.of( + const range = RangeUtil.of( Index.toMasked(replaceIndex + addedValidChars.length) || value.length, ); @@ -209,12 +165,12 @@ export const createReducer = ({ const normalizeRange = (state: InputState): InputState => ({ ...state, - range: Range.map(state.range, (n) => Math.min(n, state.value.length)), + range: RangeUtil.map(state.range, (n) => Math.min(n, state.value.length)), }); const initialState: InputState = { value: '', - range: Range.of(0, 0), + range: RangeUtil.of(0, 0), }; return (state = initialState, action: ChangeAction) => { @@ -234,4 +190,4 @@ export const createReducer = ({ return nextState === state ? state : normalizeRange(nextState); }; -}; +} diff --git a/src/core/types.ts b/src/core/types.ts new file mode 100644 index 0000000..70e392e --- /dev/null +++ b/src/core/types.ts @@ -0,0 +1,57 @@ +export interface Range { + start: number; + end: number; +} + +export interface InputState { + range: Range; + value: string; +} + +export interface BaseAction { + type: T; + payload: InputState & P; +} + +export type UnknownAction = BaseAction<'UNKNOWN'>; + +export type InsertAction = BaseAction< + 'INSERT', + { + insertPosition: number; + insertIndices: number[]; + } +>; + +export type DeleteAction = BaseAction< + 'DELETE', + { + deleteDirection: 'backward' | 'forward'; + deleteIndices: number[]; + } +>; + +export type ReplaceAction = BaseAction< + 'REPLACE', + { + replacePosition: number; + deleteIndices: number[]; + insertIndices: number[]; + } +>; + +export type ChangeAction = + | InsertAction + | DeleteAction + | ReplaceAction + | UnknownAction; + +export interface Reducer { + (state: InputState | undefined, action: ChangeAction): InputState; +} + +export interface ReducerOptions { + mask: string; + pattern: RegExp; + placeholder: string; +} diff --git a/src/dom/__test__/utils.test.ts b/src/dom/__test__/utils.test.ts index 95c69f8..ac70402 100644 --- a/src/dom/__test__/utils.test.ts +++ b/src/dom/__test__/utils.test.ts @@ -1,10 +1,10 @@ import { describe, test } from '@std/testing/bdd'; import { expect } from '@std/expect'; -import { ReducerOptions } from '../../core/reducer.ts'; -import { Value } from '../utils.ts'; +import type { ReducerOptions } from '../../core/mod.ts'; +import { ValueUtil } from '../utils.ts'; describe('Value', () => { - test('toClean', () => { + test('maskedToClean', () => { const options: ReducerOptions = { mask: '+7 (___) ___-__-__', pattern: /\d/, @@ -14,6 +14,6 @@ describe('Value', () => { const input = '+7 (800) 555-35-35'; const output = '8005553535'; - expect(Value.toClean(options, input)).toBe(output); + expect(ValueUtil.maskedToClean(options, input)).toBe(output); }); }); diff --git a/src/dom/create-input-mask.ts b/src/dom/create-input-mask.ts new file mode 100644 index 0000000..c250066 --- /dev/null +++ b/src/dom/create-input-mask.ts @@ -0,0 +1,82 @@ +import { createReducer, defineChanges, type InputState, RangeUtil } from '../core/mod.ts'; +import type { InputMask, InputMaskOptions, InputMaskState } from './types.ts'; +import { StateUtil, ValueUtil } from './utils.ts'; + +const reducerDefaults = { + mask: '____', + placeholder: '_', + pattern: /\d/, +}; + +export function createInputMask( + element: HTMLInputElement, + { + onInput, + ...reducerOptions + }: InputMaskOptions = {}, +): InputMask { + const config = { ...reducerDefaults, ...reducerOptions }; + const reducer = createReducer(config); + const process = (a: InputState, b: InputState) => reducer(a, defineChanges(a, b)); + + let state = StateUtil.init(config); + let enabled = true; + + const getState = (): InputMaskState => { + const completed = state.value.length === config.mask.length; + + return { + value: state.value, + cleanValue: ValueUtil.maskedToClean(config, state.value), + completed, + ready: completed, + }; + }; + + StateUtil.apply(state, element); + + const onDocumentSelectionChange = () => { + if (element === document.activeElement) { + state = StateUtil.fromTarget(element); + } + }; + + const onElementInput = () => { + state = process(state, StateUtil.fromTarget(element)); + StateUtil.apply(state, element); + onInput?.(getState()); + }; + + document.addEventListener('selectionchange', onDocumentSelectionChange); + element.addEventListener('input', onElementInput); + + return { + getState, + + setValue(cleanValue: string) { + if (!enabled) return; + + // мы не знаем какое значение передано (clean или masked) поэтому берем из него только подходящие символы + const validCleanValue = cleanValue + .split('') + .filter((c) => c.match(config.pattern)) + .join(''); + + const newMaskedValue = ValueUtil.cleanToMasked(config, validCleanValue); + const firstPlace = config.mask.indexOf(config.placeholder); + + state = process( + StateUtil.of(state.value, RangeUtil.of(firstPlace, state.value.length)), + StateUtil.of(newMaskedValue, RangeUtil.of(newMaskedValue.length)), + ); + + StateUtil.apply(state, element); + }, + + disable() { + enabled = false; + document.removeEventListener('selectionchange', onDocumentSelectionChange); + element.removeEventListener('input', onElementInput); + }, + }; +} diff --git a/src/dom/mod.ts b/src/dom/mod.ts index f1ed63d..ca2d3be 100644 --- a/src/dom/mod.ts +++ b/src/dom/mod.ts @@ -1,106 +1,3 @@ -import { createReducer, defineChanges, type InputState, type ReducerOptions } from '../core/mod.ts'; -import { Range, State, Value } from './utils.ts'; - -interface Data { - value: string; - cleanValue: string; - completed: boolean; - - /** @deprecated Use "completed" instead. */ - ready: boolean; -} - -interface Options extends Partial { - onInput?: (data: Data) => void; - - /** @deprecated Use "onInput" option or "result.getData" instead. */ - onChange?: (data: Data) => void; -} - -const reducerDefaults = { - mask: '____', - placeholder: '_', - pattern: /\d/, -}; - -export interface InputMaskControl { - getData(): Data; - setValue(value: string): void; - disable(): void; -} - -export function InputMask( - element: HTMLInputElement, - { - onChange, - onInput, - ...reducerOptions - }: Options = {}, -): InputMaskControl { - const options = { ...reducerDefaults, ...reducerOptions }; - const reducer = createReducer(options); - const process = (a: InputState, b: InputState) => reducer(a, defineChanges(a, b)); - - let state = State.init(options); - let enabled = true; - - const getData = (): Data => { - const completed = state.value.length === options.mask.length; - - return { - value: state.value, - cleanValue: Value.maskedToClean(options, state.value), - completed, - ready: completed, - }; - }; - - State.apply(state, element); - - const onDocumentSelectionChange = () => { - if (element === document.activeElement) { - state = State.fromTarget(element); - } - }; - - const onElementInput = () => { - state = process(state, State.fromTarget(element)); - State.apply(state, element); - onInput?.(getData()); - onChange?.(getData()); - }; - - document.addEventListener('selectionchange', onDocumentSelectionChange); - element.addEventListener('input', onElementInput); - - return { - getData, - - setValue(cleanValue: string) { - if (!enabled) return; - - // мы не знаем какое значение передано (clean или masked) поэтому берем из него только подходящие символы - const validCleanValue = cleanValue - .split('') - .filter((c) => c.match(options.pattern)) - .join(''); - - const newMaskedValue = Value.cleanToMasked(options, validCleanValue); - const firstPlace = options.mask.indexOf(options.placeholder); - - state = process( - State.of(state.value, Range.of(firstPlace, state.value.length)), - State.of(newMaskedValue, Range.of(newMaskedValue.length)), - ); - - State.apply(state, element); - onChange?.(getData()); - }, - - disable() { - enabled = false; - document.removeEventListener('selectionchange', onDocumentSelectionChange); - element.removeEventListener('input', onElementInput); - }, - }; -} +export type * from './types.ts'; +export { createInputMask } from './create-input-mask.ts'; +export { rangeFromTarget, StateUtil, ValueUtil } from './utils.ts'; diff --git a/src/dom/types.ts b/src/dom/types.ts new file mode 100644 index 0000000..1be1f77 --- /dev/null +++ b/src/dom/types.ts @@ -0,0 +1,20 @@ +import type { ReducerOptions } from '../core/mod.ts'; + +export interface InputMaskOptions extends Partial { + onInput?: (state: InputMaskState) => void; +} + +export interface InputMaskState { + value: string; + cleanValue: string; + completed: boolean; + + /** @deprecated Use "completed" instead. */ + ready: boolean; +} + +export interface InputMask { + getState(): InputMaskState; + setValue(value: string): void; + disable(): void; +} diff --git a/src/dom/utils.ts b/src/dom/utils.ts index 7495956..10f4381 100644 --- a/src/dom/utils.ts +++ b/src/dom/utils.ts @@ -1,59 +1,55 @@ -import type { InputState, ReducerOptions } from '../core/reducer.ts'; -import { type IRange, Range as CoreRange } from '../core/range.ts'; +import type { InputState, ReducerOptions } from '../core/mod.ts'; +import { type Range, RangeUtil } from '../core/mod.ts'; -export const Range = { - ...CoreRange, +export function rangeFromTarget(target: HTMLInputElement): Range { + return RangeUtil.of(target.selectionStart || 0, target.selectionEnd || 0); +} - fromTarget(target: HTMLInputElement) { - return Range.of(target.selectionStart || 0, target.selectionEnd || 0); - }, -}; - -export const State = { - of(value: string, range: IRange = Range.of(value.length)): InputState { +export abstract class StateUtil { + static of(value: string, range: Range = RangeUtil.of(value.length)): InputState { return { value, range }; - }, + } - init({ mask, placeholder }: Omit): InputState { + static init({ mask, placeholder }: Omit): InputState { const firstPlace = mask.indexOf(placeholder); - return State.of(mask.slice(0, firstPlace)); - }, + return StateUtil.of(mask.slice(0, firstPlace)); + } - fromTarget(target: HTMLInputElement): InputState { - return State.of(target.value, Range.fromTarget(target)); - }, + static fromTarget(target: HTMLInputElement): InputState { + return StateUtil.of(target.value, rangeFromTarget(target)); + } - apply(state: InputState, target: HTMLInputElement): void { + static apply(state: InputState, target: HTMLInputElement): void { target.value = state.value; - State.applySelection(state, target); - }, + StateUtil.applySelection(state, target); + } - applyDiff(state: InputState, target: HTMLInputElement) { + static applyDiff(state: InputState, target: HTMLInputElement) { if (target.value !== state.value) { target.value = state.value; - State.applySelection(state, target); + StateUtil.applySelection(state, target); } else if ( target.selectionStart !== state.range.start || target.selectionEnd !== state.range.end ) { - State.applySelection(state, target); + StateUtil.applySelection(state, target); } - }, + } - applySelection(state: InputState, target: HTMLInputElement) { + static applySelection(state: InputState, target: HTMLInputElement) { // в Safari поле получает фокус при вызове setSelectionRange - проверяем необходимость установки if (target === document.activeElement) { target.setSelectionRange(state.range.start, state.range.end); } - }, -} as const; + } +} -export const Value = { - cleanToMasked( +export abstract class ValueUtil { + static cleanToMasked( { mask, placeholder }: Omit, cleanValue: string, - ) { + ): string { let result = ''; for (let i = 0, j = 0; i < mask.length; i++) { @@ -66,12 +62,12 @@ export const Value = { } return result; - }, + } - maskedToClean( + static maskedToClean( { mask, placeholder }: Omit, maskedValue: string, - ) { + ): string { let result = ''; for (let i = 0; i < maskedValue.length; i++) { @@ -81,19 +77,5 @@ export const Value = { } return result; - }, - - /** - * @deprecated Use "cleanToMasked" instead. - */ - toMasked(maskOptions: Omit, maskedValue: string) { - return Value.cleanToMasked(maskOptions, maskedValue); - }, - - /** - * @deprecated Use "maskedToClean" instead. - */ - toClean(maskOptions: Omit, maskedValue: string) { - return Value.maskedToClean(maskOptions, maskedValue); - }, -} as const; + } +} From 6abd54975887ec9d8e4dd7605aa24f45943d5587 Mon Sep 17 00:00:00 2001 From: krutoo Date: Mon, 1 Jul 2024 13:49:52 +0500 Subject: [PATCH 2/3] refactor - tests added to CI --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ deno.json | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6bc427b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Publish NPM package + +on: + release: + types: [published] + +jobs: + publish-npm: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + + - name: Lint + run: | + deno task lint + + - name: Test + run: | + deno test diff --git a/deno.json b/deno.json index 10c36ff..4ded5bc 100644 --- a/deno.json +++ b/deno.json @@ -2,7 +2,7 @@ "name": "@krutoo/input-mask", "version": "0.0.0", "tasks": { - "lint": "deno lint && deno check src/**/*.ts", + "lint": "deno check src/**/*.ts && deno lint && deno fmt --check", "build-npm": "deno run -A scripts/build-npm.ts" }, "imports": { From f7f3fbcc77c1d3b0c89ac7ab3f982c4995b33273 Mon Sep 17 00:00:00 2001 From: krutoo Date: Mon, 1 Jul 2024 13:51:11 +0500 Subject: [PATCH 3/3] refactor - fix tests workflow --- .github/workflows/tests.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6bc427b..5a83342 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,13 @@ -name: Publish NPM package +name: Test on: - release: - types: [published] + push: + branches: ['main'] + pull_request: + branches: ['main'] jobs: - publish-npm: + test: runs-on: ubuntu-latest steps: