prlg-ui 1.8.132 → 1.8.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FileIcon-BE4ItwkK.cjs +1 -0
- package/dist/FileIcon-maHE2Nhr.js +101 -0
- package/dist/Image-BHDBSn7B.cjs +1 -0
- package/dist/Image-CAGIshx9.js +259 -0
- package/dist/QuestionIcon-DptFSXX2.cjs +1 -0
- package/dist/QuestionIcon-tK1kUB_h.js +340 -0
- package/dist/SendIcon-CH6S0QWh.cjs +1 -0
- package/dist/SendIcon-Cqdt2QWN.js +88 -0
- package/dist/blocks/index.cjs.js +1 -0
- package/dist/blocks/index.es.js +186 -0
- package/dist/blocks.d.ts +35 -0
- package/dist/eventBus.util-K9Yq6hZm.cjs +1 -0
- package/dist/eventBus.util-msbJpg6N.js +75 -0
- package/dist/fonts/Roboto/Roboto-Black.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Black.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-Bold.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Bold.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-ExtraBold.woff +0 -0
- package/dist/fonts/Roboto/Roboto-ExtraBold.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-ExtraLight.woff +0 -0
- package/dist/fonts/Roboto/Roboto-ExtraLight.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-Light.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Light.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-Medium.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Medium.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-Regular.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Regular.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-SemiBold.woff +0 -0
- package/dist/fonts/Roboto/Roboto-SemiBold.woff2 +0 -0
- package/dist/fonts/Roboto/Roboto-Thin.woff +0 -0
- package/dist/fonts/Roboto/Roboto-Thin.woff2 +0 -0
- package/dist/icons/index.cjs.js +1 -0
- package/dist/icons/index.es.js +1487 -0
- package/dist/icons.d.ts +220 -0
- package/dist/index.d.ts +2096 -0
- package/dist/parseFileSize.util-Bg1rLRLQ.cjs +1 -0
- package/dist/parseFileSize.util-CxVk4CvB.js +785 -0
- package/dist/prlg-ui.cjs.js +1 -0
- package/dist/prlg-ui.css +1 -0
- package/dist/prlg-ui.es.js +6228 -0
- package/dist/scss/animations.scss +30 -0
- package/dist/scss/colors.scss +135 -0
- package/dist/scss/fonts.scss +3 -0
- package/dist/scss/main.scss +36 -0
- package/dist/scss/mixins.scss +177 -0
- package/dist/scss/reset.scss +51 -0
- package/dist/scss/root-vars.scss +12 -0
- package/dist/types/index.cjs.js +1 -0
- package/dist/types/index.es.js +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/uploadFile.util-DCFkx3w3.cjs +1 -0
- package/dist/uploadFile.util-DhavPrlY.js +37 -0
- package/dist/utils/date.util.ts +30 -0
- package/dist/utils/dayjs.util.ts +32 -0
- package/dist/utils/eventBus.util.ts +43 -0
- package/dist/utils/index.cjs.js +1 -0
- package/dist/utils/index.es.js +1891 -0
- package/dist/utils/index.ts +3 -0
- package/dist/utils/isClient.util.ts +3 -0
- package/dist/utils/mask.util.test.ts +170 -0
- package/dist/utils/mask.util.ts +217 -0
- package/dist/utils/onClickOutside.util.ts +78 -0
- package/dist/utils/parseDate.util.ts +41 -0
- package/dist/utils/parseFileSize.util.ts +38 -0
- package/dist/utils/price.util.ts +28 -0
- package/dist/utils/typeFile.util.ts +32 -0
- package/dist/utils/uploadFile.util.ts +94 -0
- package/dist/utils/useBodyScroll.util.ts +41 -0
- package/dist/utils.d.ts +141 -0
- package/dist/vite.svg +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { Mask } from './mask.util'
|
|
3
|
+
|
|
4
|
+
// Mock HTMLInputElement
|
|
5
|
+
class MockHTMLInputElement {
|
|
6
|
+
value: string = ''
|
|
7
|
+
|
|
8
|
+
constructor() {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('Mask', () => {
|
|
12
|
+
let mask: Mask
|
|
13
|
+
let mockInput: MockHTMLInputElement
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mask = new Mask()
|
|
17
|
+
mockInput = new MockHTMLInputElement()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const createEvent = (value: string): Event => {
|
|
21
|
+
mockInput.value = value
|
|
22
|
+
return { target: mockInput } as unknown as Event
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('getDateMask', () => {
|
|
26
|
+
it('должен ограничивать ввод только цифрами', () => {
|
|
27
|
+
const event = createEvent('abc123def')
|
|
28
|
+
mask.getDateMask(event)
|
|
29
|
+
expect(mockInput.value).toBe('12.3')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('должен применять маску dd.mm.yyyy по умолчанию', () => {
|
|
33
|
+
const event = createEvent('12065678')
|
|
34
|
+
mask.getDateMask(event)
|
|
35
|
+
expect(mockInput.value).toBe('12.06.5678')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('должен ограничивать длину ввода согласно формату', () => {
|
|
39
|
+
const event = createEvent('120656789012345')
|
|
40
|
+
mask.getDateMask(event)
|
|
41
|
+
expect(mockInput.value).toBe('12.06.5678')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('должен корректировать неправильный день больше 31', () => {
|
|
45
|
+
const event = createEvent('3201')
|
|
46
|
+
mask.getDateMask(event)
|
|
47
|
+
expect(mockInput.value).toBe('31.01')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('должен корректировать день 00 на 01', () => {
|
|
51
|
+
const event = createEvent('0001')
|
|
52
|
+
mask.getDateMask(event)
|
|
53
|
+
expect(mockInput.value).toBe('01.01')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('должен корректировать неправильный месяц больше 12', () => {
|
|
57
|
+
const event = createEvent('0113')
|
|
58
|
+
mask.getDateMask(event)
|
|
59
|
+
expect(mockInput.value).toBe('01.12')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('должен корректировать месяц 00 на 01', () => {
|
|
63
|
+
const event = createEvent('0100')
|
|
64
|
+
mask.getDateMask(event)
|
|
65
|
+
expect(mockInput.value).toBe('01.01')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('должен корректировать 31.06 на 30.06 (июнь имеет 30 дней)', () => {
|
|
69
|
+
const event = createEvent('3106')
|
|
70
|
+
mask.getDateMask(event)
|
|
71
|
+
expect(mockInput.value).toBe('30.06')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('должен корректировать 31.04 на 30.04 (апрель имеет 30 дней)', () => {
|
|
75
|
+
const event = createEvent('3104')
|
|
76
|
+
mask.getDateMask(event)
|
|
77
|
+
expect(mockInput.value).toBe('30.04')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('должен разрешать 31.01 (январь имеет 31 день)', () => {
|
|
81
|
+
const event = createEvent('3101')
|
|
82
|
+
mask.getDateMask(event)
|
|
83
|
+
expect(mockInput.value).toBe('31.01')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('должен разрешать 29.02 (максимум для февраля)', () => {
|
|
87
|
+
const event = createEvent('2902')
|
|
88
|
+
mask.getDateMask(event)
|
|
89
|
+
expect(mockInput.value).toBe('29.02')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('должен корректировать 30.02 на 29.02', () => {
|
|
93
|
+
const event = createEvent('3002')
|
|
94
|
+
mask.getDateMask(event)
|
|
95
|
+
expect(mockInput.value).toBe('29.02')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('должен не подставлять год если пользователь ввел только день и месяц', () => {
|
|
99
|
+
const event = createEvent('1206')
|
|
100
|
+
mask.getDateMask(event)
|
|
101
|
+
expect(mockInput.value).toBe('12.06')
|
|
102
|
+
expect(mockInput.value.length).toBe(5)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('должен использовать автокоррекцию dayjs когда начинает вводить год', () => {
|
|
106
|
+
const event = createEvent('31062025')
|
|
107
|
+
mask.getDateMask(event)
|
|
108
|
+
// dayjs скорректирует 31.06.2025 на 01.07.2025
|
|
109
|
+
expect(mockInput.value).toBe('01.07.2025')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('должен работать с частично введенным годом', () => {
|
|
113
|
+
const event = createEvent('3106202')
|
|
114
|
+
mask.getDateMask(event)
|
|
115
|
+
// При частичном годе (7 символов, больше yearStart=6) используется dayjs
|
|
116
|
+
// dayjs корректирует 31.06.2020 -> 01.07.2020, затем обрезается до 7 символов
|
|
117
|
+
expect(mockInput.value).toBe('01.07.202')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('должен принимать параметры minDate и maxDate без ошибок', () => {
|
|
121
|
+
const event = createEvent('01012020')
|
|
122
|
+
expect(() => {
|
|
123
|
+
mask.getDateMask(event, 'dd.mm.yyyy', '01.01.2021')
|
|
124
|
+
}).not.toThrow()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('должен принимать параметры maxDate без ошибок', () => {
|
|
128
|
+
const event = createEvent('01012025')
|
|
129
|
+
expect(() => {
|
|
130
|
+
mask.getDateMask(event, 'dd.mm.yyyy', undefined, '31.12.2024')
|
|
131
|
+
}).not.toThrow()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('должен не изменять дату если она в пределах minDate и maxDate', () => {
|
|
135
|
+
const event = createEvent('15062023')
|
|
136
|
+
mask.getDateMask(event, 'dd.mm.yyyy', '01.01.2023', '31.12.2023')
|
|
137
|
+
expect(mockInput.value).toBe('15.06.2023')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('должен работать с форматом mm.dd.yyyy', () => {
|
|
141
|
+
const event = createEvent('12312023')
|
|
142
|
+
mask.getDateMask(event, 'mm.dd.yyyy')
|
|
143
|
+
expect(mockInput.value).toBe('12.31.2023')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('должен корректировать неправильную дату в формате mm.dd.yyyy', () => {
|
|
147
|
+
const event = createEvent('13312023')
|
|
148
|
+
mask.getDateMask(event, 'mm.dd.yyyy')
|
|
149
|
+
expect(mockInput.value).toBe('12.31.2023')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('должен возвращать пустое значение если target не HTMLInputElement', () => {
|
|
153
|
+
const event = { target: null } as unknown as Event
|
|
154
|
+
mask.getDateMask(event)
|
|
155
|
+
// Не должно выбросить ошибку
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('должен обрабатывать пустой ввод', () => {
|
|
159
|
+
const event = createEvent('')
|
|
160
|
+
mask.getDateMask(event)
|
|
161
|
+
expect(mockInput.value).toBe('')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('должен обрабатывать ввод одной цифры', () => {
|
|
165
|
+
const event = createEvent('1')
|
|
166
|
+
mask.getDateMask(event)
|
|
167
|
+
expect(mockInput.value).toBe('1')
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { dayjs } from '../dayjs.util'
|
|
2
|
+
|
|
3
|
+
export class Mask {
|
|
4
|
+
public constructor() {
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
public getPhoneMask(_e: Event, _format: string = '(999) 999-99-99') {
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public getDateMask(e: Event, format: string = 'dd.mm.yyyy', minDate?: string, maxDate?: string) {
|
|
12
|
+
const input = e.target as HTMLInputElement
|
|
13
|
+
if (!input) return
|
|
14
|
+
|
|
15
|
+
let value = input.value.replace(/\D/g, '')
|
|
16
|
+
|
|
17
|
+
const formatLength = format.replace(/\W/g, '').length
|
|
18
|
+
|
|
19
|
+
if (value.length > formatLength) {
|
|
20
|
+
value = value.slice(0, formatLength)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
value = this.correctDateValues(value, format)
|
|
24
|
+
|
|
25
|
+
let maskedValue = ''
|
|
26
|
+
let valueIndex = 0
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < format.length && valueIndex < value.length; i++) {
|
|
29
|
+
const formatChar = format[i]
|
|
30
|
+
|
|
31
|
+
if (formatChar === 'd' || formatChar === 'm' || formatChar === 'y') {
|
|
32
|
+
maskedValue += value[valueIndex]
|
|
33
|
+
valueIndex++
|
|
34
|
+
} else {
|
|
35
|
+
maskedValue += formatChar
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (maskedValue.length >= format.length) {
|
|
40
|
+
const parsedDate = this.parseDate(maskedValue, format)
|
|
41
|
+
if (parsedDate && parsedDate.isValid()) {
|
|
42
|
+
const correctedDate = this.applyDateLimits(maskedValue, format, minDate, maxDate)
|
|
43
|
+
if (correctedDate) {
|
|
44
|
+
maskedValue = correctedDate
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
input.value = maskedValue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private parseDate(value: string, format: string) {
|
|
53
|
+
return dayjs(value, format.toLowerCase())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private correctDateValues(value: string, format: string): string {
|
|
57
|
+
if (value.length < 2) return value
|
|
58
|
+
|
|
59
|
+
const formatLower = format.toLowerCase()
|
|
60
|
+
const dayIndex = formatLower.indexOf('dd')
|
|
61
|
+
const monthIndex = formatLower.indexOf('mm')
|
|
62
|
+
|
|
63
|
+
let correctedValue = value
|
|
64
|
+
|
|
65
|
+
// Корректировка дня (01-31)
|
|
66
|
+
if (dayIndex !== -1 && value.length >= 2) {
|
|
67
|
+
const dayValue = parseInt(value.substring(0, 2))
|
|
68
|
+
if (dayValue > 31) {
|
|
69
|
+
correctedValue = '31' + value.substring(2)
|
|
70
|
+
} else if (dayValue === 0) {
|
|
71
|
+
correctedValue = '01' + value.substring(2)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Корректировка месяца (01-12)
|
|
76
|
+
if (monthIndex !== -1 && correctedValue.length >= 4) {
|
|
77
|
+
const monthStart = monthIndex === 0 ? 0 : 2
|
|
78
|
+
const monthValue = parseInt(correctedValue.substring(monthStart, monthStart + 2))
|
|
79
|
+
if (monthValue > 12) {
|
|
80
|
+
correctedValue = correctedValue.substring(0, monthStart) + '12' + correctedValue.substring(monthStart + 2)
|
|
81
|
+
} else if (monthValue === 0) {
|
|
82
|
+
correctedValue = correctedValue.substring(0, monthStart) + '01' + correctedValue.substring(monthStart + 2)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Корректировка даты через dayjs (минимум 4 символа для дд.мм)
|
|
87
|
+
if (correctedValue.length >= 4 && dayIndex !== -1 && monthIndex !== -1) {
|
|
88
|
+
correctedValue = this.correctDateThroughDayjs(correctedValue, format)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return correctedValue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private correctDateThroughDayjs(value: string, format: string): string {
|
|
95
|
+
const formatLower = format.toLowerCase()
|
|
96
|
+
// const dayIndex = formatLower.indexOf('dd')
|
|
97
|
+
// const monthIndex = formatLower.indexOf('mm')
|
|
98
|
+
const yearIndex = formatLower.indexOf('yyyy')
|
|
99
|
+
|
|
100
|
+
// const dayStart = dayIndex === 0 ? 0 : dayIndex === 3 ? 3 : 6
|
|
101
|
+
// const monthStart = monthIndex === 0 ? 0 : monthIndex === 3 ? 3 : 6
|
|
102
|
+
const yearStart = yearIndex === 0 ? 0 : yearIndex === 3 ? 3 : 6
|
|
103
|
+
|
|
104
|
+
// Проверяем, есть ли минимум день и месяц (4 символа для dd.mm)
|
|
105
|
+
if (value.length < 4) {
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Проверяем, начал ли пользователь вводить год
|
|
110
|
+
const hasStartedYear = value.length > yearStart
|
|
111
|
+
if (!hasStartedYear) {
|
|
112
|
+
// Если год не начал вводить, только проверяем день/месяц без автокоррекции через dayjs
|
|
113
|
+
return this.correctDayAndMonthOnly(value, format)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let maskedForParsing = ''
|
|
117
|
+
let valueIndex = 0
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < format.length && valueIndex < value.length; i++) {
|
|
120
|
+
const formatChar = format[i]
|
|
121
|
+
|
|
122
|
+
if (formatChar === 'd' || formatChar === 'm' || formatChar === 'y') {
|
|
123
|
+
maskedForParsing += value[valueIndex] || '0'
|
|
124
|
+
valueIndex++
|
|
125
|
+
} else {
|
|
126
|
+
maskedForParsing += formatChar
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Если год не полный, дополняем текущим годом для валидации
|
|
131
|
+
if (maskedForParsing.length < format.length) {
|
|
132
|
+
const currentYear = new Date().getFullYear().toString()
|
|
133
|
+
const missingLength = format.length - maskedForParsing.length
|
|
134
|
+
maskedForParsing += currentYear.slice(-missingLength)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const parsedDate = dayjs(maskedForParsing, format.toUpperCase())
|
|
139
|
+
if (parsedDate.isValid()) {
|
|
140
|
+
const correctedDateString = parsedDate.format(format.toUpperCase())
|
|
141
|
+
const correctedDigits = correctedDateString.replace(/\D/g, '')
|
|
142
|
+
// Возвращаем только то количество цифр, которое пользователь ввел
|
|
143
|
+
return correctedDigits.substring(0, value.length)
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
// Если парсинг не удался, возвращаем исходное значение
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private correctDayAndMonthOnly(value: string, format: string): string {
|
|
153
|
+
const formatLower = format.toLowerCase()
|
|
154
|
+
const dayIndex = formatLower.indexOf('dd')
|
|
155
|
+
const monthIndex = formatLower.indexOf('mm')
|
|
156
|
+
|
|
157
|
+
const dayStart = dayIndex === 0 ? 0 : dayIndex === 3 ? 3 : 6
|
|
158
|
+
const monthStart = monthIndex === 0 ? 0 : monthIndex === 3 ? 3 : 6
|
|
159
|
+
|
|
160
|
+
// Минимум нужно 4 символа для dd.mm
|
|
161
|
+
if (value.length < 4) return value
|
|
162
|
+
|
|
163
|
+
const day = parseInt(value.substring(dayStart, dayStart + 2))
|
|
164
|
+
const month = parseInt(value.substring(monthStart, monthStart + 2))
|
|
165
|
+
|
|
166
|
+
if (!day || !month) return value
|
|
167
|
+
|
|
168
|
+
// Определяем количество дней в месяце
|
|
169
|
+
const daysInMonth = this.getDaysInMonth(month)
|
|
170
|
+
|
|
171
|
+
if (day > daysInMonth) {
|
|
172
|
+
const correctedDay = daysInMonth.toString().padStart(2, '0')
|
|
173
|
+
return value.substring(0, dayStart) + correctedDay + value.substring(dayStart + 2)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return value
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private getDaysInMonth(month: number): number {
|
|
180
|
+
const daysInMonths = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
181
|
+
if (month < 1 || month > 12) return 31
|
|
182
|
+
return daysInMonths[month - 1]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private applyDateLimits(dateString: string, format: string, minDate?: string, maxDate?: string): string | null {
|
|
186
|
+
if (!minDate && !maxDate) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parsedDate = this.parseDate(dateString, format)
|
|
191
|
+
|
|
192
|
+
if (!parsedDate || !parsedDate.isValid()) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let correctedDate = parsedDate
|
|
197
|
+
let hasChanges = false
|
|
198
|
+
|
|
199
|
+
if (minDate) {
|
|
200
|
+
const minDateObj = dayjs(minDate)
|
|
201
|
+
if (correctedDate.isBefore(minDateObj)) {
|
|
202
|
+
correctedDate = minDateObj
|
|
203
|
+
hasChanges = true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (maxDate) {
|
|
208
|
+
const maxDateObj = dayjs(maxDate)
|
|
209
|
+
if (correctedDate.isAfter(maxDateObj)) {
|
|
210
|
+
correctedDate = maxDateObj
|
|
211
|
+
hasChanges = true
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return hasChanges ? correctedDate.format(format.toUpperCase()) : null
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from 'vue';
|
|
2
|
+
import { unref, toValue } from 'vue';
|
|
3
|
+
|
|
4
|
+
export type VueInstance = ComponentPublicInstance;
|
|
5
|
+
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
|
|
6
|
+
export type MaybeElementRef<T extends MaybeElement = MaybeElement> = MaybeRef<T>;
|
|
7
|
+
export type MaybeElementTarget = MaybeElementRef<MaybeElement> | string;
|
|
8
|
+
|
|
9
|
+
export interface OnClickOutsideOptions {
|
|
10
|
+
ignore?: MaybeRefOrGetter<(MaybeElementRef<MaybeElement> | string)[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const activeListeners = new WeakMap<HTMLElement, () => void>();
|
|
14
|
+
|
|
15
|
+
function unrefElement(elRef: MaybeElementRef<MaybeElement>): HTMLElement | null {
|
|
16
|
+
const plain = unref(elRef);
|
|
17
|
+
return (plain as VueInstance)?.$el ?? (plain as HTMLElement | null);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveTarget(target: MaybeElementTarget): HTMLElement | null {
|
|
21
|
+
if (typeof target === 'string') {
|
|
22
|
+
return document.querySelector(target) as HTMLElement | null;
|
|
23
|
+
}
|
|
24
|
+
return unrefElement(target);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function onClickOutside(
|
|
28
|
+
target: MaybeElementTarget,
|
|
29
|
+
handler: (event: MouseEvent | TouchEvent) => void,
|
|
30
|
+
options: OnClickOutsideOptions = {}
|
|
31
|
+
): () => void {
|
|
32
|
+
const targetElement = resolveTarget(target);
|
|
33
|
+
if (!targetElement) return () => {};
|
|
34
|
+
|
|
35
|
+
// Remove existing listener if present
|
|
36
|
+
const existingCleanup = activeListeners.get(targetElement);
|
|
37
|
+
if (existingCleanup) {
|
|
38
|
+
existingCleanup();
|
|
39
|
+
activeListeners.delete(targetElement);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const listener = (event: MouseEvent | TouchEvent) => {
|
|
43
|
+
const targetNode = event.target instanceof Node ? event.target : null;
|
|
44
|
+
if (!targetNode) return;
|
|
45
|
+
|
|
46
|
+
const rawIgnores = toValue(options.ignore) ?? [];
|
|
47
|
+
const ignoreElements = rawIgnores
|
|
48
|
+
.map(el => {
|
|
49
|
+
if (typeof el === 'string') {
|
|
50
|
+
return document.querySelector(el) as HTMLElement | null;
|
|
51
|
+
}
|
|
52
|
+
return unrefElement(el);
|
|
53
|
+
})
|
|
54
|
+
.filter((el): el is HTMLElement => el !== null && el !== undefined);
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
targetElement.contains(targetNode) ||
|
|
58
|
+
ignoreElements.some(ignoreEl => ignoreEl.contains(targetNode))
|
|
59
|
+
) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
handler(event);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
document.addEventListener('mousedown', listener, { capture: true });
|
|
67
|
+
document.addEventListener('touchstart', listener, { capture: true });
|
|
68
|
+
|
|
69
|
+
// Store cleanup function and return it
|
|
70
|
+
const cleanup = () => {
|
|
71
|
+
document.removeEventListener('mousedown', listener, { capture: true });
|
|
72
|
+
document.removeEventListener('touchstart', listener, { capture: true });
|
|
73
|
+
activeListeners.delete(targetElement);
|
|
74
|
+
};
|
|
75
|
+
activeListeners.set(targetElement, cleanup);
|
|
76
|
+
|
|
77
|
+
return cleanup;
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function parseDurationToSeconds(duration: string): number {
|
|
2
|
+
const regex = /^(\d+)(s|m|h|d|day|month|year)$/;
|
|
3
|
+
const match = duration.match(regex);
|
|
4
|
+
|
|
5
|
+
if (!match) {
|
|
6
|
+
throw new Error('Invalid duration format. Use formats like 30d, 1m, 1h, 1day, 1month, 1year');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const value = parseInt(match[1], 10);
|
|
10
|
+
const unit = match[2];
|
|
11
|
+
|
|
12
|
+
switch (unit) {
|
|
13
|
+
case 's':
|
|
14
|
+
return value; // seconds
|
|
15
|
+
case 'm':
|
|
16
|
+
return value * 60; // minutes
|
|
17
|
+
case 'h':
|
|
18
|
+
return value * 3600; // hours
|
|
19
|
+
case 'd':
|
|
20
|
+
case 'day':
|
|
21
|
+
return value * 86400; // days
|
|
22
|
+
case 'month':
|
|
23
|
+
return value * 2592000; // months (30 days)
|
|
24
|
+
case 'year':
|
|
25
|
+
return value * 31536000; // years (365 days)
|
|
26
|
+
default:
|
|
27
|
+
throw new Error('Unsupported time unit');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default parseDurationToSeconds;
|
|
32
|
+
|
|
33
|
+
/** Примеры использования */
|
|
34
|
+
/** example
|
|
35
|
+
console.log(parseDurationToSeconds('30d')); // 2592000
|
|
36
|
+
console.log(parseDurationToSeconds('1m')); // 60
|
|
37
|
+
console.log(parseDurationToSeconds('1h')); // 3600
|
|
38
|
+
console.log(parseDurationToSeconds('1day')); // 86400
|
|
39
|
+
console.log(parseDurationToSeconds('1month')); // 2592000
|
|
40
|
+
console.log(parseDurationToSeconds('1year')); // 31536000
|
|
41
|
+
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Преобразует строку с размером файла (например, "5mb", "10kb", "1.5gb") в число байт.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} size - Строка, содержащая числовое значение и единицу измерения (b, kb, mb, gb, tb).
|
|
5
|
+
* Пример: "5mb", "10kb", "1.5gb".
|
|
6
|
+
* @returns {number} - Размер в байтах.
|
|
7
|
+
*
|
|
8
|
+
* Поддерживаемые единицы: b, kb, mb, gb, tb (регистр не имеет значения).
|
|
9
|
+
* Если единица не указана, по умолчанию считается "b" (байты).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const parseFileSize = (size: string): number => {
|
|
13
|
+
const units = ['b', 'kb', 'mb', 'gb', 'tb'];
|
|
14
|
+
const unit = size.match(/[a-zA-Z]+/)?.[0] || 'b';
|
|
15
|
+
const value = parseFloat(size.replace(unit, ''));
|
|
16
|
+
const index = units.indexOf(unit.toLowerCase());
|
|
17
|
+
return value * Math.pow(1024, index);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Преобразует размер файла в байтах в человекочитаемый текст (например, "1.5 МБ").
|
|
22
|
+
*
|
|
23
|
+
* @param {number} bytes - Размер файла в байтах.
|
|
24
|
+
* @returns {string} - Размер файла в виде строки с подходящей единицей (Б, КБ, МБ, ГБ, ТБ).
|
|
25
|
+
*/
|
|
26
|
+
export function formatFileSize(bytes: number): string {
|
|
27
|
+
if (isNaN(bytes) || bytes < 0) return '0 Б';
|
|
28
|
+
const units = ['Б', 'КБ', 'МБ', 'ГБ', 'ТБ'];
|
|
29
|
+
let index = 0;
|
|
30
|
+
let value = bytes;
|
|
31
|
+
while (value >= 1024 && index < units.length - 1) {
|
|
32
|
+
value = value / 1024;
|
|
33
|
+
index++;
|
|
34
|
+
}
|
|
35
|
+
// Округляем до одного знака после запятой, если нужно
|
|
36
|
+
const rounded = value % 1 === 0 ? value : value.toFixed(0);
|
|
37
|
+
return `${rounded} ${units[index]}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Преобразует число в форматированную строку с валютой (по умолчанию: ₽)
|
|
3
|
+
*/
|
|
4
|
+
export function formatPrice(value: number, currency = "₽"): string {
|
|
5
|
+
// Используем en-US локаль для точки как разделителя дробной части
|
|
6
|
+
// и заставляем всегда показывать ровно 2 знака после точки
|
|
7
|
+
const formatted = value.toLocaleString("en-US", {
|
|
8
|
+
minimumFractionDigits: 2,
|
|
9
|
+
maximumFractionDigits: 2
|
|
10
|
+
}).replace(/,/g, " ");
|
|
11
|
+
|
|
12
|
+
return `${formatted} ${currency}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Возвращает сумму всех значений (например, для корзины)
|
|
17
|
+
*/
|
|
18
|
+
export function sumPrices(items: number[]): number {
|
|
19
|
+
return items.reduce((total, price) => total + price, 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Делает безопасное преобразование строки в число (цены из инпута)
|
|
24
|
+
*/
|
|
25
|
+
export function parsePrice(input: string): number {
|
|
26
|
+
const normalized = input.replace(/[^\d.,]/g, "").replace(",", ".");
|
|
27
|
+
return parseFloat(normalized) || 0;
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Определяет тип файла по его MIME-типу.
|
|
3
|
+
* Возвращает 'image' для изображений, 'pdf' для PDF-файлов,
|
|
4
|
+
* 'word' для документов Word, 'excel' для файлов Excel,
|
|
5
|
+
* иначе 'other'.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} mimeType - MIME-тип файла
|
|
8
|
+
* @returns {'image' | 'pdf' | 'word' | 'excel' | 'other'}
|
|
9
|
+
*/
|
|
10
|
+
export function getFileTypeByMime(
|
|
11
|
+
mimeType: string
|
|
12
|
+
): 'image' | 'pdf' | 'word' | 'excel' | 'other' {
|
|
13
|
+
if (mimeType.startsWith('image/')) {
|
|
14
|
+
return 'image';
|
|
15
|
+
}
|
|
16
|
+
if (mimeType === 'application/pdf') {
|
|
17
|
+
return 'pdf';
|
|
18
|
+
}
|
|
19
|
+
if (
|
|
20
|
+
mimeType === 'application/msword' ||
|
|
21
|
+
mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
22
|
+
) {
|
|
23
|
+
return 'word';
|
|
24
|
+
}
|
|
25
|
+
if (
|
|
26
|
+
mimeType === 'application/vnd.ms-excel' ||
|
|
27
|
+
mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
28
|
+
) {
|
|
29
|
+
return 'excel';
|
|
30
|
+
}
|
|
31
|
+
return 'other';
|
|
32
|
+
}
|