Skip to content

Commit

Permalink
feat(kit): Time & DateTime support AM / PM formats
Browse files Browse the repository at this point in the history
  • Loading branch information
nsbarsukov committed Sep 26, 2024
1 parent 732013e commit ed245df
Show file tree
Hide file tree
Showing 29 changed files with 668 additions and 61 deletions.
379 changes: 379 additions & 0 deletions projects/demo-integrations/src/tests/kit/time/time-meridiem.cy.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ describe('Time', () => {

describe('max hours 11', () => {
beforeEach(() => {
cy.visit(`/${DemoPath.Time}/API?mode=HH&timeSegmentMaxValues$=1`);
cy.visit(`/${DemoPath.Time}/API?mode=HH&timeSegmentMaxValues$=2`);
cy.get('#demo-content input')
.should('be.visible')
.first()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {DemoPath} from '@demo/constants';

import {range} from './utils';

describe('Time | [timeSegmentMaxValues] property', () => {
describe('{hours: 5, minutes: 5, seconds: 5, milliseconds: 5}', () => {
beforeEach(() => {
cy.visit(`/${DemoPath.Time}/API?mode=HH:MM&timeSegmentMaxValues$=2`);
cy.visit(`/${DemoPath.Time}/API?mode=HH:MM&timeSegmentMaxValues$=3`);
cy.get('#demo-content input')
.should('be.visible')
.first()
Expand Down Expand Up @@ -152,7 +154,3 @@ describe('Time | [timeSegmentMaxValues] property', () => {
});
});
});

function range(from: number, to: number): number[] {
return new Array(to - from + 1).fill(null).map((_, i) => from + i);
}
3 changes: 3 additions & 0 deletions projects/demo-integrations/src/tests/kit/time/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function range(from: number, to: number): number[] {
return new Array(to - from + 1).fill(null).map((_, i) => from + i);
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ export default class DateTimeMaskDocComponent implements GeneratorOptions {

protected readonly timeModeOptions = [
'HH:MM',
'HH:MM AA',
'HH:MM:SS',
'HH:MM:SS AA',
'HH:MM:SS.MSS',
'HH:MM:SS.MSS AA',
] as const satisfies readonly MaskitoTimeMode[];

protected readonly minMaxOptions = [
Expand Down
6 changes: 6 additions & 0 deletions projects/demo/src/pages/kit/time/time-mask-doc.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,26 @@ export default class TimeMaskDocComponent implements GeneratorOptions {

protected readonly modeOptions = [
'HH:MM',
'HH:MM AA',
'HH:MM:SS',
'HH:MM:SS AA',
'HH:MM:SS.MSS',
'HH:MM:SS.MSS AA',
'HH',
'HH AA',
'MM:SS.MSS',
'SS.MSS',
] as const satisfies readonly MaskitoTimeMode[];

protected readonly timeSegmentMaxValuesOptions = [
{},
{hours: 23, minutes: 59, seconds: 59, milliseconds: 999},
{hours: 11},
{hours: 5, minutes: 5, seconds: 5, milliseconds: 5},
] as const satisfies ReadonlyArray<Partial<MaskitoTimeSegments<number>>>;

public mode: MaskitoTimeMode = this.modeOptions[0];
public timeSegmentMinValues = {};
public timeSegmentMaxValues: Partial<MaskitoTimeSegments<number>> =
this.timeSegmentMaxValuesOptions[0];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ export const DEFAULT_TIME_SEGMENT_MAX_VALUES: MaskitoTimeSegments<number> = {
seconds: 59,
milliseconds: 999,
};

export const DEFAULT_TIME_SEGMENT_MIN_VALUES: MaskitoTimeSegments<number> = {
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
};
3 changes: 2 additions & 1 deletion projects/kit/src/lib/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from './date-segment-max-values';
export * from './default-decimal-pseudo-separators';
export * from './default-min-max-dates';
export * from './default-time-segment-max-values';
export * from './default-time-segment-bounds';
export * from './meridiem';
export * from './time-fixed-characters';
export * from './time-segment-value-lengths';
export * from './unicode-characters';
4 changes: 4 additions & 0 deletions projects/kit/src/lib/constants/meridiem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {CHAR_NO_BREAK_SPACE} from './unicode-characters';

export const ANY_MERIDIEM_CHARACTER_RE = new RegExp(`[${CHAR_NO_BREAK_SPACE}APM]+$`, 'g');
export const ALL_MERIDIEM_CHARACTERS_RE = new RegExp(`${CHAR_NO_BREAK_SPACE}[AP]M$`, 'g');
37 changes: 30 additions & 7 deletions projects/kit/src/lib/masks/date-time/date-time-mask.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
import type {MaskitoOptions} from '@maskito/core';
import {MASKITO_DEFAULT_OPTIONS} from '@maskito/core';

import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../constants';
import {createTimeSegmentsSteppingPlugin} from '../../plugins';
import {
DEFAULT_TIME_SEGMENT_MAX_VALUES,
DEFAULT_TIME_SEGMENT_MIN_VALUES,
} from '../../constants';
import {
createMeridiemSteppingPlugin,
createTimeSegmentsSteppingPlugin,
} from '../../plugins';
import {
createColonConvertPreprocessor,
createDateSegmentsZeroPaddingPostprocessor,
createFirstDateEndSeparatorPreprocessor,
createFullWidthToHalfWidthPreprocessor,
createInvalidTimeSegmentInsertionPreprocessor,
createMeridiemPostprocessor,
createMeridiemPreprocessor,
createZeroPlaceholdersPreprocessor,
normalizeDatePreprocessor,
} from '../../processors';
import type {MaskitoDateMode, MaskitoTimeMode} from '../../types';
import type {MaskitoDateMode, MaskitoTimeMode, MaskitoTimeSegments} from '../../types';
import {createTimeMaskExpression} from '../../utils/time';
import {DATE_TIME_SEPARATOR} from './constants';
import {createMinMaxDateTimePostprocessor} from './postprocessors';
import {createValidDateTimePreprocessor} from './preprocessors';
Expand All @@ -35,7 +44,17 @@ export function maskitoDateTimeOptionsGenerator({
dateTimeSeparator?: string;
timeStep?: number;
}): Required<MaskitoOptions> {
const hasMeridiem = timeMode.includes('AA');
const dateModeTemplate = dateMode.split('/').join(dateSeparator);
const timeSegmentMaxValues: MaskitoTimeSegments<number> = {
...DEFAULT_TIME_SEGMENT_MAX_VALUES,
...(hasMeridiem ? {hours: 12} : {}),
};
const timeSegmentMinValues: MaskitoTimeSegments<number> = {
...DEFAULT_TIME_SEGMENT_MIN_VALUES,
...(hasMeridiem ? {hours: 1} : {}),
};
const fullMode = `${dateModeTemplate}${dateTimeSeparator}${timeMode}`;

return {
...MASKITO_DEFAULT_OPTIONS,
Expand All @@ -44,9 +63,7 @@ export function maskitoDateTimeOptionsGenerator({
dateSeparator.includes(char) ? char : /\d/,
),
...dateTimeSeparator.split(''),
...Array.from(timeMode).map((char) =>
TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/,
),
...createTimeMaskExpression(timeMode),
],
overwriteMode: 'replace',
preprocessors: [
Expand All @@ -59,13 +76,16 @@ export function maskitoDateTimeOptionsGenerator({
pseudoFirstDateEndSeparators: dateTimeSeparator.split(''),
}),
createZeroPlaceholdersPreprocessor(),
createMeridiemPreprocessor(timeMode),
normalizeDatePreprocessor({
dateModeTemplate,
dateSegmentsSeparator: dateSeparator,
dateTimeSeparator,
}),
createInvalidTimeSegmentInsertionPreprocessor({
timeMode,
timeSegmentMinValues,
timeSegmentMaxValues,
parseValue: (x) => {
const [dateString, timeString] = parseDateTimeString(x, {
dateModeTemplate,
Expand All @@ -80,9 +100,11 @@ export function maskitoDateTimeOptionsGenerator({
dateSegmentsSeparator: dateSeparator,
dateTimeSeparator,
timeMode,
timeSegmentMaxValues,
}),
],
postprocessors: [
createMeridiemPostprocessor(timeMode),
createDateSegmentsZeroPaddingPostprocessor({
dateModeTemplate,
dateSegmentSeparator: dateSeparator,
Expand All @@ -109,9 +131,10 @@ export function maskitoDateTimeOptionsGenerator({
plugins: [
createTimeSegmentsSteppingPlugin({
step: timeStep,
fullMode: `${dateModeTemplate}${dateTimeSeparator}${timeMode}`,
fullMode,
timeSegmentMaxValues: DEFAULT_TIME_SEGMENT_MAX_VALUES,
}),
createMeridiemSteppingPlugin(fullMode.indexOf(' AA')),
],
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ export function createMinMaxDateTimePostprocessor({

const date = segmentsToDate(parsedDate, parsedTime);
const clampedDate = clamp(date, min, max);
// trailing segment separators or meridiem characters
const [trailingNonDigitCharacters = ''] = value.match(/\D+$/g) || [];

const validatedValue = toDateString(dateToSegments(clampedDate), {
dateMode: dateModeTemplate,
dateTimeSeparator,
timeMode,
});
const validatedValue =
toDateString(dateToSegments(clampedDate), {
dateMode: dateModeTemplate,
dateTimeSeparator,
timeMode,
}) + trailingNonDigitCharacters;

return {
selection,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type {MaskitoPreprocessor} from '@maskito/core';

import {TIME_FIXED_CHARACTERS} from '../../../constants';
import type {MaskitoTimeMode} from '../../../types';
import {escapeRegExp, validateDateString} from '../../../utils';
import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../../types';
import {validateDateString} from '../../../utils';
import {enrichTimeSegmentsWithZeroes} from '../../../utils/time';
import {parseDateTimeString} from '../utils';

Expand All @@ -11,18 +10,14 @@ export function createValidDateTimePreprocessor({
dateSegmentsSeparator,
dateTimeSeparator,
timeMode,
timeSegmentMaxValues,
}: {
dateModeTemplate: string;
dateSegmentsSeparator: string;
dateTimeSeparator: string;
timeMode: MaskitoTimeMode;
timeSegmentMaxValues: MaskitoTimeSegments<number>;
}): MaskitoPreprocessor {
const invalidCharsRegExp = new RegExp(
`[^\\d${TIME_FIXED_CHARACTERS.map(escapeRegExp).join('')}${escapeRegExp(
dateSegmentsSeparator,
)}]+`,
);

return ({elementState, data}) => {
const {value, selection} = elementState;

Expand All @@ -33,15 +28,15 @@ export function createValidDateTimePreprocessor({
};
}

const newCharacters = data.replace(invalidCharsRegExp, '');
const newDigits = data.replaceAll(/\D/g, '');

if (!newCharacters) {
return {elementState, data: ''};
if (!newDigits) {
return {elementState, data};
}

const [from, rawTo] = selection;
let to = rawTo + data.length;
const newPossibleValue = value.slice(0, from) + newCharacters + value.slice(to);
const newPossibleValue = value.slice(0, from) + newDigits + value.slice(to);

const [dateString, timeString] = parseDateTimeString(newPossibleValue, {
dateModeTemplate,
Expand All @@ -68,7 +63,7 @@ export function createValidDateTimePreprocessor({

const updatedTimeState = enrichTimeSegmentsWithZeroes(
{value: timeString, selection: [from, to]},
{mode: timeMode},
{mode: timeMode, timeSegmentMaxValues},
);

to = updatedTimeState.selection[1];
Expand Down
2 changes: 1 addition & 1 deletion projects/kit/src/lib/masks/time/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {maskitoTimeOptionsGenerator} from './time-mask';
export type {MaskitoTimeParams} from './time-options';
export type {MaskitoTimeParams} from './time-params';
export {maskitoParseTime, maskitoStringifyTime} from './utils';
35 changes: 27 additions & 8 deletions projects/kit/src/lib/masks/time/time-mask.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
import type {MaskitoOptions} from '@maskito/core';

import {DEFAULT_TIME_SEGMENT_MAX_VALUES, TIME_FIXED_CHARACTERS} from '../../constants';
import {createTimeSegmentsSteppingPlugin} from '../../plugins';
import {
DEFAULT_TIME_SEGMENT_MAX_VALUES,
DEFAULT_TIME_SEGMENT_MIN_VALUES,
} from '../../constants';
import {
createMeridiemSteppingPlugin,
createTimeSegmentsSteppingPlugin,
} from '../../plugins';
import {
createColonConvertPreprocessor,
createFullWidthToHalfWidthPreprocessor,
createInvalidTimeSegmentInsertionPreprocessor,
createMeridiemPostprocessor,
createMeridiemPreprocessor,
createZeroPlaceholdersPreprocessor,
} from '../../processors';
import {enrichTimeSegmentsWithZeroes} from '../../utils/time';
import type {MaskitoTimeParams} from './time-options';
import type {MaskitoTimeSegments} from '../../types';
import {createTimeMaskExpression, enrichTimeSegmentsWithZeroes} from '../../utils/time';
import type {MaskitoTimeParams} from './time-params';

export function maskitoTimeOptionsGenerator({
mode,
timeSegmentMaxValues = {},
timeSegmentMinValues = {},
step = 0,
}: MaskitoTimeParams): Required<MaskitoOptions> {
const enrichedTimeSegmentMaxValues = {
const hasMeridiem = mode.includes('AA');
const enrichedTimeSegmentMaxValues: MaskitoTimeSegments<number> = {
...DEFAULT_TIME_SEGMENT_MAX_VALUES,
...(hasMeridiem ? {hours: 12} : {}),
...timeSegmentMaxValues,
};
const enrichedTimeSegmentMinValues: MaskitoTimeSegments<number> = {
...DEFAULT_TIME_SEGMENT_MIN_VALUES,
...(hasMeridiem ? {hours: 1} : {}),
...timeSegmentMinValues,
};

return {
mask: Array.from(mode).map((char) =>
TIME_FIXED_CHARACTERS.includes(char) ? char : /\d/,
),
mask: createTimeMaskExpression(mode),
preprocessors: [
createFullWidthToHalfWidthPreprocessor(),
createColonConvertPreprocessor(),
createZeroPlaceholdersPreprocessor(),
createMeridiemPreprocessor(mode),
createInvalidTimeSegmentInsertionPreprocessor({
timeMode: mode,
timeSegmentMinValues: enrichedTimeSegmentMinValues,
timeSegmentMaxValues: enrichedTimeSegmentMaxValues,
}),
],
postprocessors: [
createMeridiemPostprocessor(mode),
(elementState) =>
enrichTimeSegmentsWithZeroes(elementState, {
mode,
Expand All @@ -47,6 +65,7 @@ export function maskitoTimeOptionsGenerator({
step,
timeSegmentMaxValues: enrichedTimeSegmentMaxValues,
}),
createMeridiemSteppingPlugin(mode.indexOf(' AA')),
],
overwriteMode: 'replace',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import type {MaskitoTimeMode, MaskitoTimeSegments} from '../../types';
export interface MaskitoTimeParams {
readonly mode: MaskitoTimeMode;
readonly timeSegmentMaxValues?: Partial<MaskitoTimeSegments<number>>;
readonly timeSegmentMinValues?: Partial<MaskitoTimeSegments<number>>;
readonly step?: number;
}
2 changes: 1 addition & 1 deletion projects/kit/src/lib/masks/time/utils/parse-time.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {DEFAULT_TIME_SEGMENT_MAX_VALUES} from '../../../constants';
import type {MaskitoTimeSegments} from '../../../types';
import {padEndTimeSegments, parseTimeString} from '../../../utils/time';
import type {MaskitoTimeParams} from '../time-options';
import type {MaskitoTimeParams} from '../time-params';

/**
* Converts a formatted time string to milliseconds based on the given `options.mode`.
Expand Down
2 changes: 1 addition & 1 deletion projects/kit/src/lib/masks/time/utils/stringify-time.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {DEFAULT_TIME_SEGMENT_MAX_VALUES} from '../../../constants';
import type {MaskitoTimeSegments} from '../../../types';
import {padStartTimeSegments} from '../../../utils/time';
import type {MaskitoTimeParams} from '../time-options';
import type {MaskitoTimeParams} from '../time-params';

/**
* Converts milliseconds to a formatted time string based on the given `options.mode`.
Expand Down
3 changes: 2 additions & 1 deletion projects/kit/src/lib/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export {maskitoCaretGuard} from './caret-guard';
export {maskitoEventHandler} from './event-handler';
export {maskitoRejectEvent} from './reject-event';
export {maskitoRemoveOnBlurPlugin} from './remove-on-blur';
export {createTimeSegmentsSteppingPlugin} from './time-segments-stepping';
export {createMeridiemSteppingPlugin} from './time/meridiem-stepping';
export {createTimeSegmentsSteppingPlugin} from './time/time-segments-stepping';
Loading

0 comments on commit ed245df

Please sign in to comment.