@it-enterprise/forcebpm-ui-kit 1.0.2-beta.21 → 1.0.2-beta.22

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.
@@ -0,0 +1,498 @@
1
+ <script setup>
2
+ // FTextFieldDate
3
+ import { ref, computed, watch, useTemplateRef, nextTick } from 'vue'
4
+ import { useFocus } from '@vueuse/core'
5
+ import { useI18n } from 'vue-i18n'
6
+ import moment from 'moment'
7
+
8
+ const { t, locale } = useI18n()
9
+
10
+ const props = defineProps({
11
+ color: {
12
+ type: String,
13
+ default: 'secondary'
14
+ },
15
+ disabled: {
16
+ type: Boolean,
17
+ default: false
18
+ },
19
+ allowedDates: {
20
+ type: Function,
21
+ default: () => true
22
+ },
23
+ type: {
24
+ type: String,
25
+ default: 'date',
26
+ validator: value => ['date', 'datetime'].includes(value)
27
+ }
28
+ })
29
+
30
+ const modelValue = defineModel({
31
+ type: String,
32
+ default: ''
33
+ })
34
+
35
+ const emit = defineEmits(['blur', 'focus'])
36
+
37
+ const fields = {
38
+ day: {
39
+ min: 1,
40
+ max: () => moment(month.value + '-' + year.value, 'MM-YYYY').daysInMonth() || 31,
41
+ placeholder: t('pickers.dFormat'),
42
+ next: 'month',
43
+ prev: null,
44
+ value: ref(''),
45
+ temp: ref(''),
46
+ ref: useTemplateRef('day-input')
47
+ },
48
+ month: {
49
+ min: 1,
50
+ max: 12,
51
+ placeholder: t('pickers.mFormat'),
52
+ next: 'year',
53
+ prev: 'day',
54
+ value: ref(''),
55
+ temp: ref(''),
56
+ ref: useTemplateRef('month-input')
57
+ },
58
+ year: {
59
+ min: 1,
60
+ max: 9999,
61
+ placeholder: t('pickers.yFormat'),
62
+ next: props.type === 'datetime' ? 'hour' : null,
63
+ prev: 'month',
64
+ value: ref(''),
65
+ temp: ref(''),
66
+ ref: useTemplateRef('year-input')
67
+ },
68
+ hour: {
69
+ min: 0,
70
+ max: 23,
71
+ placeholder: t('pickers.hFormat'),
72
+ next: 'minute',
73
+ prev: 'year',
74
+ value: ref(''),
75
+ temp: ref(''),
76
+ ref: useTemplateRef('hour-input')
77
+ },
78
+ minute: {
79
+ min: 0,
80
+ max: 59,
81
+ placeholder: t('pickers.minFormat'),
82
+ next: null,
83
+ prev: 'hour',
84
+ value: ref(''),
85
+ temp: ref(''),
86
+ ref: useTemplateRef('minute-input')
87
+ }
88
+ }
89
+
90
+ const isAllowedDates = ref(true)
91
+
92
+ const classList = computed(() => {
93
+ const list = [`text-${props.color}`]
94
+ if (props.disabled) list.push('is-disabled')
95
+ if (!isAllowedDates.value) list.push('is-error')
96
+ return list
97
+ })
98
+
99
+ const isValidDate = computed(() => moment(modelValue.value).isValid())
100
+
101
+ // Computed shortcuts for field values
102
+ const day = computed({
103
+ get: () => fields.day.value.value,
104
+ set: val => {
105
+ fields.day.value.value = val
106
+ }
107
+ })
108
+ const month = computed({
109
+ get: () => fields.month.value.value,
110
+ set: val => {
111
+ fields.month.value.value = val
112
+ }
113
+ })
114
+ const year = computed({
115
+ get: () => fields.year.value.value,
116
+ set: val => {
117
+ fields.year.value.value = val
118
+ }
119
+ })
120
+ const hour = computed({
121
+ get: () => fields.hour.value.value,
122
+ set: val => {
123
+ fields.hour.value.value = val
124
+ }
125
+ })
126
+ const minute = computed({
127
+ get: () => fields.minute.value.value,
128
+ set: val => {
129
+ fields.minute.value.value = val
130
+ }
131
+ })
132
+
133
+ // Track focus for each field
134
+ const { focused: dayFocused } = useFocus(fields.day.ref)
135
+ const { focused: monthFocused } = useFocus(fields.month.ref)
136
+ const { focused: yearFocused } = useFocus(fields.year.ref)
137
+ const { focused: hourFocused } = useFocus(fields.hour.ref)
138
+ const { focused: minuteFocused } = useFocus(fields.minute.ref)
139
+
140
+ const isFocused = computed(() => dayFocused.value || monthFocused.value || yearFocused.value || hourFocused.value || minuteFocused.value)
141
+
142
+ watch([day, month, year, hour, minute], () => {
143
+ if (props.type === 'date' && !isNaN(day.value) && !isNaN(month.value) && !isNaN(year.value)) {
144
+ modelValue.value = `${year.value}-${month.value}-${day.value}`
145
+ return
146
+ }
147
+
148
+ if (props.type === 'datetime' && !isNaN(day.value) && !isNaN(month.value) && !isNaN(year.value) && !isNaN(hour.value) && !isNaN(minute.value)) {
149
+ modelValue.value = `${year.value}-${month.value}-${day.value} ${hour.value}:${minute.value}`
150
+ return
151
+ }
152
+
153
+ modelValue.value = ''
154
+ })
155
+
156
+ // If the day is greater than the number of days in the month, set the last day of the month
157
+ watch([month, year], () => {
158
+ const maxDay = moment(month.value + '-' + year.value, 'MM-YYYY').daysInMonth() || 31
159
+ const currentDay = parseInt(day.value, 10)
160
+
161
+ if (currentDay > maxDay) {
162
+ day.value = maxDay.toString().padStart(2, '0')
163
+ fields.day.value.value = maxDay.toString().padStart(2, '0')
164
+ fields.day.temp.value = ''
165
+ }
166
+ })
167
+
168
+ watch(
169
+ modelValue,
170
+ val => {
171
+ const format = props.type === 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm'
172
+ if (val && moment(val, format).isValid()) {
173
+ isAllowedDates.value = props.allowedDates(val)
174
+ }
175
+
176
+ if (typeof val === 'string' || val instanceof Date) {
177
+ if (isValidDate.value) {
178
+ day.value = moment(val, format).format('DD')
179
+ month.value = moment(val, format).format('MM')
180
+ year.value = moment(val, format).format('YYYY')
181
+ if (props.type === 'datetime') {
182
+ hour.value = moment(val, format).format('HH')
183
+ minute.value = moment(val, format).format('mm')
184
+ }
185
+ }
186
+ } else {
187
+ day.value = fields.day.placeholder
188
+ month.value = fields.month.placeholder
189
+ year.value = fields.year.placeholder
190
+ hour.value = fields.hour.placeholder
191
+ minute.value = fields.minute.placeholder
192
+ }
193
+ },
194
+ { immediate: true }
195
+ )
196
+
197
+ // Generic input handler factory
198
+ const createInputHandler = fieldName => {
199
+ return e => {
200
+ const field = fields[fieldName]
201
+ const max = typeof field.max === 'function' ? field.max() : field.max
202
+ const inputValue = parseInt(e.target.value, 10)
203
+
204
+ // Arrow key handling
205
+ if (e.type === 'arrow') {
206
+ const clamped = Math.max(field.min, Math.min(inputValue, max)) // Limit values from min to max
207
+ const padLength = fieldName === 'year' ? 4 : 2 // Format: year - 4 characters, the rest - 2 characters
208
+
209
+ field.value.value = clamped.toString().padStart(padLength, '0')
210
+ nextTick(() => {
211
+ if (field.ref.value[0]) {
212
+ field.ref.value[0].setSelectionRange(0, 0)
213
+ field.ref.value[0].select()
214
+ }
215
+ })
216
+ return
217
+ }
218
+
219
+ // Handle non-numeric input
220
+ if (isNaN(inputValue)) {
221
+ field.temp.value = ''
222
+ field.value.value = field.placeholder
223
+ nextTick(() => field.ref.value[0].select())
224
+ return
225
+ }
226
+
227
+ // Handle numeric input
228
+ handleNumericInput({ field, fieldName, inputValue, max, target: e.target })
229
+ }
230
+ }
231
+
232
+ const handleNumericInput = ({ field, fieldName, inputValue, max, target }) => {
233
+ const firstDigitMax = parseInt(max.toString()[0])
234
+ const nextRef = field.next ? fields[field.next].ref.value[0] : null
235
+ const tempValue = parseInt(field.temp.value, 10)
236
+
237
+ // First digit exceeds first digit of max
238
+ if (inputValue > firstDigitMax && isNaN(tempValue)) {
239
+ field.temp.value = '0' + inputValue
240
+ if (nextRef) {
241
+ nextRef.focus()
242
+ } else if (fieldName === 'minute') {
243
+ target.blur()
244
+ }
245
+ return
246
+ }
247
+
248
+ // First digit input
249
+ if (isNaN(tempValue)) {
250
+ if (fieldName === 'year') {
251
+ field.temp.value = inputValue.toString()
252
+ field.value.value = inputValue.toString().padStart(4, '0')
253
+ } else {
254
+ field.temp.value = '0' + inputValue
255
+ }
256
+ nextTick(() => target.select())
257
+ return
258
+ }
259
+
260
+ // Second+ digit input
261
+ if (fieldName === 'year') {
262
+ // For year: add digit to temp
263
+ const newTempValue = field.temp.value + inputValue.toString()
264
+
265
+ if (newTempValue.length <= 4 && parseInt(newTempValue) <= max) {
266
+ field.temp.value = newTempValue
267
+ field.value.value = newTempValue.padStart(4, '0')
268
+
269
+ if (field.temp.value.length === 4) {
270
+ // Year fully entered
271
+ if (props.type === 'datetime' && nextRef) {
272
+ nextRef.focus()
273
+ } else {
274
+ target.blur()
275
+ }
276
+ } else {
277
+ nextTick(() => target.select())
278
+ }
279
+ }
280
+ } else {
281
+ // For day/month/hour/minute: second digit + new digit
282
+ const newValue = field.temp.value.charAt(1) + inputValue.toString()
283
+
284
+ if (parseInt(newValue) <= max) {
285
+ field.temp.value = newValue
286
+ if (nextRef) {
287
+ nextRef.focus()
288
+ } else if (fieldName === 'minute') {
289
+ target.blur()
290
+ }
291
+ } else {
292
+ // Exceeded max - set max and move on
293
+ field.temp.value = max.toString().padStart(2, '0')
294
+ if (nextRef) {
295
+ nextRef.focus()
296
+ } else if (fieldName === 'minute') {
297
+ target.blur()
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ const focusHandler = e => {
304
+ e.target.setSelectionRange(0, 0)
305
+ e.target.select()
306
+ }
307
+
308
+ const blurHandler = fieldName => {
309
+ const field = fields[fieldName]
310
+
311
+ if (field.temp.value) {
312
+ field.value.value = field.temp.value
313
+ field.temp.value = ''
314
+ }
315
+
316
+ // Timeout 0 to allow focus to move to another field before emitting blur
317
+ setTimeout(() => {
318
+ if (!isFocused.value) {
319
+ if (props.type === 'date') {
320
+ emit('blur', `${year.value}-${month.value}-${day.value}`)
321
+ } else {
322
+ emit('blur', `${year.value}-${month.value}-${day.value} ${hour.value}:${minute.value}`)
323
+ }
324
+ }
325
+ }, 0)
326
+ }
327
+
328
+ const inputHandlers = {
329
+ day: createInputHandler('day'),
330
+ month: createInputHandler('month'),
331
+ year: createInputHandler('year'),
332
+ hour: createInputHandler('hour'),
333
+ minute: createInputHandler('minute')
334
+ }
335
+
336
+ const keydownHandler = (e, fieldName) => {
337
+ const preventKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']
338
+ if (preventKeys.includes(e.key)) e.preventDefault()
339
+
340
+ const field = fields[fieldName]
341
+ const currentValue = !isNaN(field.value.value) ? parseInt(field.value.value) : 0
342
+ const handler = inputHandlers[fieldName]
343
+
344
+ const actions = {
345
+ ArrowUp: () => handler({ target: { value: currentValue + 1 }, type: 'arrow' }),
346
+ ArrowDown: () => handler({ target: { value: currentValue - 1 }, type: 'arrow' }),
347
+ ArrowLeft: () => {
348
+ const prevField = field.prev
349
+ if (prevField && fields[prevField].ref.value[0]) {
350
+ fields[prevField].ref.value[0].focus()
351
+ }
352
+ },
353
+ ArrowRight: () => {
354
+ const nextField = field.next
355
+ if (nextField && fields[nextField].ref.value[0]) {
356
+ fields[nextField].ref.value[0].focus()
357
+ }
358
+ }
359
+ }
360
+
361
+ actions[e.key]?.()
362
+ }
363
+ </script>
364
+
365
+ <template>
366
+ <div class="d-flex align-center f-text-field-date" :class="classList">
367
+ <div v-if="disabled && isValidDate" :class="`text-${color}`">
368
+ {{ type === 'date' ? `${day}.${month}.${year}` : `${day}.${month}.${year} ${hour}:${minute}` }}
369
+ </div>
370
+
371
+ <template v-else>
372
+ <template v-for="(fieldName, index) in ['day', 'month', 'year']" :key="fieldName">
373
+ <!-- Separator before month and year -->
374
+ <span v-if="index > 0" class="f-text-field-date-separator" :class="`text-${color}`">.</span>
375
+
376
+ <!-- Field input -->
377
+ <input
378
+ :ref="fieldName + '-input'"
379
+ v-model="fields[fieldName].value.value"
380
+ type="text"
381
+ :name="fieldName"
382
+ :class="[
383
+ 'f-text-field-date-input',
384
+ `text-${color}`,
385
+ fieldName,
386
+ locale,
387
+ { 'is-empty': fields[fieldName].value.value === fields[fieldName].placeholder }
388
+ ]"
389
+ :placeholder="fields[fieldName].placeholder"
390
+ :disabled="disabled"
391
+ @click="focusHandler"
392
+ @focus="focusHandler"
393
+ @blur="blurHandler(fieldName)"
394
+ @input="inputHandlers[fieldName]"
395
+ @keydown.stop="keydownHandler($event, fieldName)"
396
+ />
397
+ </template>
398
+
399
+ <template v-if="type === 'datetime'">
400
+ <template v-for="fieldName in ['hour', 'minute']" :key="fieldName">
401
+ <!-- Separator before hour (space) and minute (colon) -->
402
+ <span class="f-text-field-date-separator" :class="[`text-${color}`, { 'ml-1': fieldName === 'hour' }]">
403
+ {{ fieldName === 'hour' ? '' : ':' }}
404
+ </span>
405
+
406
+ <!-- Field input -->
407
+ <input
408
+ :ref="fieldName + '-input'"
409
+ v-model="fields[fieldName].value.value"
410
+ type="text"
411
+ :name="fieldName"
412
+ :class="[
413
+ 'f-text-field-date-input',
414
+ `text-${color}`,
415
+ fieldName,
416
+ locale,
417
+ { 'is-empty': fields[fieldName].value.value === fields[fieldName].placeholder }
418
+ ]"
419
+ :placeholder="fields[fieldName].placeholder"
420
+ :disabled="disabled"
421
+ @click="focusHandler"
422
+ @focus="focusHandler"
423
+ @blur="blurHandler(fieldName)"
424
+ @input="inputHandlers[fieldName]"
425
+ @keydown.stop="keydownHandler($event, fieldName)"
426
+ />
427
+ </template>
428
+ </template>
429
+ </template>
430
+ </div>
431
+ </template>
432
+
433
+ <style lang="scss" scoped>
434
+ .f-text-field-date {
435
+ background: transparent;
436
+ width: max-content;
437
+ position: relative;
438
+ font-size: 1.1em;
439
+ &::after {
440
+ content: '';
441
+ display: block;
442
+ position: absolute;
443
+ bottom: 0;
444
+ min-height: 0.5px;
445
+ height: 0.5px;
446
+ width: 100%;
447
+ background: currentColor;
448
+ }
449
+ &.is-disabled::after {
450
+ content: none;
451
+ }
452
+ &.is-error {
453
+ &::after {
454
+ background: rgb(var(--v-theme-error));
455
+ }
456
+ .f-text-field-date-input,
457
+ .f-text-field-date-separator {
458
+ color: rgb(var(--v-theme-error)) !important;
459
+ }
460
+ }
461
+ .f-text-field-date-input {
462
+ border: none;
463
+ outline: none;
464
+ width: 2ch;
465
+ text-align: center;
466
+ appearance: textfield;
467
+ -moz-appearance: textfield;
468
+ color: inherit;
469
+ &.is-empty {
470
+ &.ru.year {
471
+ width: 3ch;
472
+ }
473
+ &.en {
474
+ &.month {
475
+ width: calc(2ch + 6px);
476
+ }
477
+ &.year {
478
+ width: calc(3ch + 3px);
479
+ }
480
+ }
481
+ }
482
+ &.year {
483
+ width: 4ch;
484
+ }
485
+ &:focus {
486
+ outline: none;
487
+ }
488
+ &::placeholder {
489
+ color: rgb(var(--v-theme-disabled));
490
+ }
491
+ &::-webkit-outer-spin-button,
492
+ &::-webkit-inner-spin-button {
493
+ -webkit-appearance: none;
494
+ margin: 0;
495
+ }
496
+ }
497
+ }
498
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ // FFilterPanel
3
+ import { mergeProps } from 'vue'
4
+
5
+ defineProps({
6
+ badge: {
7
+ type: [Number, String, Boolean],
8
+ default: false
9
+ }
10
+ })
11
+
12
+ const modelValue = defineModel({
13
+ type: Boolean,
14
+ default: false
15
+ })
16
+ </script>
17
+ <template>
18
+ <v-menu v-model="modelValue" :close-on-content-click="false" content-class="f-filter-panel">
19
+ <template #activator="{ props: menuProps }">
20
+ <v-tooltip :text="$t('tooltip.filter')">
21
+ <template #activator="{ props: tooltipProps }">
22
+ <v-btn class="f-filter-panel-activator" variant="text" size="30" v-bind="mergeProps(menuProps, tooltipProps)">
23
+ <v-badge dot :model-value="badge" offset-x="-2" offset-y="-2" color="primary">
24
+ <FIcon icon="filter" color="text" size="18" />
25
+ </v-badge>
26
+ </v-btn>
27
+ </template>
28
+ </v-tooltip>
29
+ </template>
30
+
31
+ <!-- Filter content -->
32
+ <div>
33
+ <!-- Filter header -->
34
+ <div class="f-filter-panel-header d-flex align-center pa-3 text-uppercase text-subTitle">
35
+ <FIcon icon="filter" color="text" size="14" class="mr-2" style="margin-top: -2px" />
36
+ {{ $t('filterPanel.Filter.Name') }}
37
+
38
+ <v-spacer />
39
+
40
+ <!-- Counter -->
41
+ <v-badge
42
+ v-if="typeof badge === 'number' || typeof badge === 'string'"
43
+ :content="badge"
44
+ offset-x="4"
45
+ offset-y="-6"
46
+ min-width="16"
47
+ width="16"
48
+ height="16"
49
+ rounded="xs"
50
+ color="secondary"
51
+ />
52
+ </div>
53
+
54
+ <v-divider />
55
+
56
+ <!-- Filter body -->
57
+ <div class="f-filter-panel-body">
58
+ <slot></slot>
59
+ </div>
60
+ </div>
61
+ </v-menu>
62
+ </template>
@@ -0,0 +1,47 @@
1
+ <script setup>
2
+ // FSortPanel
3
+ import { mergeProps } from 'vue'
4
+
5
+ defineProps({
6
+ badge: {
7
+ type: Boolean,
8
+ default: false
9
+ }
10
+ })
11
+
12
+ const modelValue = defineModel({
13
+ type: Boolean,
14
+ default: false
15
+ })
16
+ </script>
17
+ <template>
18
+ <v-menu v-model="modelValue" :close-on-content-click="false" content-class="f-sort-panel">
19
+ <template #activator="{ props: menuProps }">
20
+ <v-tooltip :text="$t('tooltip.sort')">
21
+ <template #activator="{ props: tooltipProps }">
22
+ <v-btn class="f-sort-panel-activator" variant="text" size="30" v-bind="mergeProps(menuProps, tooltipProps)">
23
+ <v-badge dot color="primary" :model-value="badge" offset-x="-4" offset-y="-2">
24
+ <FIcon icon="sort" color="text" size="18" />
25
+ </v-badge>
26
+ </v-btn>
27
+ </template>
28
+ </v-tooltip>
29
+ </template>
30
+
31
+ <!-- Sort content -->
32
+ <div>
33
+ <!-- Sort header -->
34
+ <div class="f-sort-panel-header d-flex align-end pa-3 text-uppercase text-subTitle">
35
+ <FIcon icon="sort" color="text" size="14" class="mr-2" />
36
+ <span class="lh-1">{{ $t('filterPanel.Sort.Name') }}</span>
37
+ </div>
38
+
39
+ <v-divider />
40
+
41
+ <!-- Sort body -->
42
+ <div class="f-sort-panel-body">
43
+ <slot></slot>
44
+ </div>
45
+ </div>
46
+ </v-menu>
47
+ </template>