@pgcorp/ui-kit 0.4.0 → 0.5.0
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/README.md
CHANGED
|
@@ -149,6 +149,27 @@ Utility columns support `headerPresentation: 'assistive'`, `width: '2xs'`, and
|
|
|
149
149
|
`allow-custom-value`; `SPageHeader` combines `appearance="bare"` with
|
|
150
150
|
`title-presentation="assistive"` for an accessible zero-chrome heading.
|
|
151
151
|
|
|
152
|
+
Составная сортировка использует controlled `v-model:sorts`: consumer задаёт
|
|
153
|
+
`sort-mode="multiple"` и передаёт ordered `STableSort[]`. Клик по
|
|
154
|
+
заголовку добавляет критерий, меняет направление или удаляет его; номер возле
|
|
155
|
+
заголовка показывает приоритет. Таблица не переставляет строки самостоятельно.
|
|
156
|
+
|
|
157
|
+
Multi-column sorting uses controlled `v-model:sorts`: the consumer sets
|
|
158
|
+
`sort-mode="multiple"` and provides an ordered `STableSort[]`. Header activation
|
|
159
|
+
adds a descriptor, changes its direction, or removes it; the visible number is
|
|
160
|
+
the priority. The table never reorders rows on its own.
|
|
161
|
+
|
|
162
|
+
```vue
|
|
163
|
+
<STable
|
|
164
|
+
v-model:sorts="sorts"
|
|
165
|
+
:columns="columns"
|
|
166
|
+
:data="sortedRows"
|
|
167
|
+
:get-row-key="(row) => row.id"
|
|
168
|
+
sort-mode="multiple"
|
|
169
|
+
aria-label="Позиции прайс-листа"
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
172
|
+
|
|
152
173
|
## Темы и стили / Themes and styles
|
|
153
174
|
|
|
154
175
|
Для обычного приложения подключайте полный style entrypoint один раз:
|
package/package.json
CHANGED
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
width="full"
|
|
59
59
|
:content-align="column.align === 'right' ? 'end' : column.align === 'center' ? 'center' : 'start'"
|
|
60
60
|
:trailing-icon="sortIcon(column)"
|
|
61
|
+
:badge="sortPriority(column) ?? undefined"
|
|
62
|
+
badge-severity="primary"
|
|
61
63
|
@click="requestSort(column)"
|
|
62
64
|
>
|
|
63
65
|
<span
|
|
@@ -193,6 +195,7 @@ import type {
|
|
|
193
195
|
STableRowState,
|
|
194
196
|
STableSelectionMode,
|
|
195
197
|
STableSort,
|
|
198
|
+
STableSortMode,
|
|
196
199
|
STableSurface,
|
|
197
200
|
} from './table'
|
|
198
201
|
|
|
@@ -209,6 +212,7 @@ export type {
|
|
|
209
212
|
STableSelectionMode,
|
|
210
213
|
STableSort,
|
|
211
214
|
STableSortDirection,
|
|
215
|
+
STableSortMode,
|
|
212
216
|
STableSurface,
|
|
213
217
|
} from './table'
|
|
214
218
|
|
|
@@ -232,7 +236,9 @@ export interface STableProps<
|
|
|
232
236
|
expandedRowKeys?: readonly RowKey[]
|
|
233
237
|
isRowExpandable?: (row: Row, index: number) => boolean
|
|
234
238
|
getRowLabel?: (row: Row, index: number) => string
|
|
239
|
+
sortMode?: STableSortMode
|
|
235
240
|
sort?: STableSort<Field> | null
|
|
241
|
+
sorts?: readonly STableSort<Field>[]
|
|
236
242
|
maxHeight?: ScrollableViewportMaxBlockSize
|
|
237
243
|
density?: STableDensity
|
|
238
244
|
surface?: STableSurface
|
|
@@ -282,7 +288,9 @@ const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
|
|
|
282
288
|
expandedRowKeys: () => [],
|
|
283
289
|
isRowExpandable: undefined,
|
|
284
290
|
getRowLabel: undefined,
|
|
291
|
+
sortMode: 'single',
|
|
285
292
|
sort: null,
|
|
293
|
+
sorts: () => [],
|
|
286
294
|
maxHeight: 'none',
|
|
287
295
|
density: 'default',
|
|
288
296
|
surface: 'bordered',
|
|
@@ -301,6 +309,8 @@ const emit = defineEmits<{
|
|
|
301
309
|
'row-expand': [row: Row, index: number, expanded: boolean]
|
|
302
310
|
'update:sort': [value: STableSort<Field> | null]
|
|
303
311
|
sort: [value: STableSort<Field> | null]
|
|
312
|
+
'update:sorts': [value: readonly STableSort<Field>[]]
|
|
313
|
+
'multi-sort': [value: readonly STableSort<Field>[]]
|
|
304
314
|
}>()
|
|
305
315
|
const slots = useSlots()
|
|
306
316
|
|
|
@@ -313,6 +323,8 @@ const TABLE_CELL_STATES = ['default', 'pending'] as const
|
|
|
313
323
|
const TABLE_COLUMN_CONTENT = ['field', 'value', 'slot'] as const
|
|
314
324
|
const TABLE_HEADER_PRESENTATIONS = ['visible', 'assistive'] as const
|
|
315
325
|
const TABLE_SELECTION_MODES = ['none', 'multiple'] as const
|
|
326
|
+
const TABLE_SORT_MODES = ['single', 'multiple'] as const
|
|
327
|
+
const TABLE_SORT_DIRECTIONS = ['ascending', 'descending'] as const
|
|
316
328
|
|
|
317
329
|
const ownedAttrs = useOwnedAttrs({ component: 'STable', owner: 'table scroll surface' })
|
|
318
330
|
useInteractiveLeafRegistration({ owner: 'STable' })
|
|
@@ -389,6 +401,12 @@ const validatedSelectionMode = computed(() => validateExactString(
|
|
|
389
401
|
props.selectionMode,
|
|
390
402
|
TABLE_SELECTION_MODES,
|
|
391
403
|
))
|
|
404
|
+
const validatedSortMode = computed(() => validateExactString(
|
|
405
|
+
'STable',
|
|
406
|
+
'sortMode',
|
|
407
|
+
props.sortMode,
|
|
408
|
+
TABLE_SORT_MODES,
|
|
409
|
+
))
|
|
392
410
|
|
|
393
411
|
const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
394
412
|
const fields = new Set<string>()
|
|
@@ -454,9 +472,15 @@ const validatedColumns = computed<readonly STableColumn<Row, Field>[]>(() => {
|
|
|
454
472
|
return props.columns
|
|
455
473
|
})
|
|
456
474
|
|
|
457
|
-
|
|
458
|
-
if (
|
|
459
|
-
|
|
475
|
+
function validateSortEntry(value: unknown, coordinate: string): STableSort<Field> {
|
|
476
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
477
|
+
throw new TypeError(
|
|
478
|
+
`STable: ${coordinate} должен быть STableSort object. `
|
|
479
|
+
+ `/ STable: ${coordinate} must be an STableSort object.`,
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
const entry = value as Record<string, unknown>
|
|
483
|
+
const key = validateNonEmptyString('STable', `${coordinate}.key`, entry.key) as Field
|
|
460
484
|
const column = validatedColumns.value.find((candidate) => candidate.field === key)
|
|
461
485
|
if (!column?.sortable) {
|
|
462
486
|
throw new TypeError(`STable: sort key ${JSON.stringify(key)} должен ссылаться на sortable column. / STable: sort key ${JSON.stringify(key)} must reference a sortable column.`)
|
|
@@ -465,25 +489,100 @@ const validatedSort = computed<STableSort<Field> | null>(() => {
|
|
|
465
489
|
key,
|
|
466
490
|
direction: validateExactString(
|
|
467
491
|
'STable',
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
492
|
+
`${coordinate}.direction`,
|
|
493
|
+
entry.direction,
|
|
494
|
+
TABLE_SORT_DIRECTIONS,
|
|
471
495
|
),
|
|
472
496
|
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const validatedSingleSort = computed<STableSort<Field> | null>(() => {
|
|
500
|
+
if (validatedSortMode.value === 'multiple') {
|
|
501
|
+
if (props.sort !== null && props.sort !== undefined) {
|
|
502
|
+
throw new TypeError(
|
|
503
|
+
'STable: sortMode="multiple" несовместим с sort; используйте v-model:sorts. '
|
|
504
|
+
+ '/ STable: sortMode="multiple" is incompatible with sort; use v-model:sorts.',
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
return null
|
|
508
|
+
}
|
|
509
|
+
if (props.sort === null || props.sort === undefined) return null
|
|
510
|
+
return validateSortEntry(props.sort, 'sort')
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const validatedMultipleSort = computed<readonly STableSort<Field>[]>(() => {
|
|
514
|
+
const sorts: unknown = props.sorts
|
|
515
|
+
if (!Array.isArray(sorts)) {
|
|
516
|
+
throw new TypeError(
|
|
517
|
+
'STable: sorts должен быть ordered STableSort array. '
|
|
518
|
+
+ '/ STable: sorts must be an ordered STableSort array.',
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
if (validatedSortMode.value === 'single') {
|
|
522
|
+
if (sorts.length > 0) {
|
|
523
|
+
throw new TypeError(
|
|
524
|
+
'STable: sortMode="single" несовместим с непустым sorts; используйте v-model:sort. '
|
|
525
|
+
+ '/ STable: sortMode="single" is incompatible with non-empty sorts; use v-model:sort.',
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
return []
|
|
529
|
+
}
|
|
530
|
+
const keys = new Set<Field>()
|
|
531
|
+
return sorts.map((entry, index) => {
|
|
532
|
+
const validated = validateSortEntry(entry, `sorts[${index}]`)
|
|
533
|
+
if (keys.has(validated.key)) {
|
|
534
|
+
throw new TypeError(
|
|
535
|
+
`STable: sorts содержит duplicate key ${JSON.stringify(validated.key)}. `
|
|
536
|
+
+ `/ STable: sorts contains duplicate key ${JSON.stringify(validated.key)}.`,
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
keys.add(validated.key)
|
|
540
|
+
return validated
|
|
541
|
+
})
|
|
473
542
|
})
|
|
474
543
|
|
|
544
|
+
const activeSorts = computed<readonly STableSort<Field>[]>(() => {
|
|
545
|
+
const singleSort = validatedSingleSort.value
|
|
546
|
+
const multipleSort = validatedMultipleSort.value
|
|
547
|
+
if (validatedSortMode.value === 'multiple') return multipleSort
|
|
548
|
+
return singleSort === null ? [] : [singleSort]
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
function sortIndex(column: STableColumn<Row, Field>): number {
|
|
552
|
+
return activeSorts.value.findIndex((candidate) => candidate.key === column.field)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function sortPriority(column: STableColumn<Row, Field>): number | null {
|
|
556
|
+
if (validatedSortMode.value !== 'multiple') return null
|
|
557
|
+
const index = sortIndex(column)
|
|
558
|
+
return index === -1 ? null : index + 1
|
|
559
|
+
}
|
|
560
|
+
|
|
475
561
|
const columnAriaSort = (column: STableColumn<Row, Field>): 'none' | 'ascending' | 'descending' | undefined => {
|
|
476
562
|
if (!column.sortable) return undefined
|
|
477
|
-
|
|
563
|
+
if (validatedSortMode.value === 'multiple') {
|
|
564
|
+
const primary = activeSorts.value[0]
|
|
565
|
+
return primary?.key === column.field ? primary.direction : undefined
|
|
566
|
+
}
|
|
567
|
+
const current = activeSorts.value[0]
|
|
568
|
+
return current?.key === column.field ? current.direction : 'none'
|
|
478
569
|
}
|
|
479
570
|
|
|
480
571
|
const sortIcon = (column: STableColumn<Row, Field>) => {
|
|
481
|
-
const direction =
|
|
572
|
+
const direction = activeSorts.value.find((candidate) => candidate.key === column.field)?.direction
|
|
482
573
|
return direction === 'ascending' ? ChevronUp : direction === 'descending' ? ChevronDown : ChevronsUpDown
|
|
483
574
|
}
|
|
484
575
|
|
|
485
576
|
const sortActionLabel = (column: STableColumn<Row, Field>): string => {
|
|
486
|
-
const
|
|
577
|
+
const index = sortIndex(column)
|
|
578
|
+
const current = index === -1 ? undefined : activeSorts.value[index]
|
|
579
|
+
if (validatedSortMode.value === 'multiple') {
|
|
580
|
+
if (!current) return `${column.header}: добавить в сортировку по возрастанию`
|
|
581
|
+
const state = current.direction === 'ascending' ? 'по возрастанию' : 'по убыванию'
|
|
582
|
+
const action = current.direction === 'ascending' ? 'сортировать по убыванию' : 'убрать из сортировки'
|
|
583
|
+
return `${column.header}: приоритет ${index + 1} из ${activeSorts.value.length}, ${state}; ${action}`
|
|
584
|
+
}
|
|
585
|
+
const direction = current?.direction
|
|
487
586
|
if (direction === 'ascending') return `${column.header}: сортировать по убыванию`
|
|
488
587
|
if (direction === 'descending') return `${column.header}: сбросить сортировку`
|
|
489
588
|
return `${column.header}: сортировать по возрастанию`
|
|
@@ -491,15 +590,29 @@ const sortActionLabel = (column: STableColumn<Row, Field>): string => {
|
|
|
491
590
|
|
|
492
591
|
function requestSort(column: STableColumn<Row, Field>): void {
|
|
493
592
|
if (!column.sortable) return
|
|
494
|
-
const current = validatedSort.value
|
|
495
593
|
const key = column.field as Field
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
594
|
+
if (validatedSortMode.value === 'multiple') {
|
|
595
|
+
const current = activeSorts.value
|
|
596
|
+
const index = sortIndex(column)
|
|
597
|
+
let next: readonly STableSort<Field>[]
|
|
598
|
+
if (index === -1) next = [...current, { key, direction: 'ascending' }]
|
|
599
|
+
else if (current[index]!.direction === 'ascending') {
|
|
600
|
+
next = current.map((entry, candidateIndex) => candidateIndex === index
|
|
601
|
+
? { key: entry.key, direction: 'descending' }
|
|
602
|
+
: entry)
|
|
603
|
+
} else next = current.filter((_entry, candidateIndex) => candidateIndex !== index)
|
|
604
|
+
emit('update:sorts', next)
|
|
605
|
+
emit('multi-sort', next)
|
|
606
|
+
} else {
|
|
607
|
+
const current = activeSorts.value[0]
|
|
608
|
+
const next: STableSort<Field> | null = current?.key !== key
|
|
609
|
+
? { key, direction: 'ascending' }
|
|
610
|
+
: current.direction === 'ascending'
|
|
611
|
+
? { key, direction: 'descending' }
|
|
612
|
+
: null
|
|
613
|
+
emit('update:sort', next)
|
|
614
|
+
emit('sort', next)
|
|
615
|
+
}
|
|
503
616
|
}
|
|
504
617
|
|
|
505
618
|
function describeRowKey(key: STableRowKey): string {
|
|
@@ -671,7 +784,7 @@ const rowExpandableByRow = computed<ReadonlyMap<Row, boolean>>(() => {
|
|
|
671
784
|
|
|
672
785
|
const validatedRows = computed<readonly Row[]>(() => {
|
|
673
786
|
const columns = validatedColumns.value
|
|
674
|
-
void
|
|
787
|
+
void activeSorts.value
|
|
675
788
|
void resolvedRowKeys.value
|
|
676
789
|
void validatedSelectedRowKey.value
|
|
677
790
|
void validatedSelectedRowKeys.value
|
|
@@ -14,6 +14,7 @@ export type STableColumnContent = 'field' | 'value' | 'slot'
|
|
|
14
14
|
export type STableColumnHeaderPresentation = 'visible' | 'assistive'
|
|
15
15
|
export type STableSelectionMode = 'none' | 'multiple'
|
|
16
16
|
export type STableSortDirection = 'ascending' | 'descending'
|
|
17
|
+
export type STableSortMode = 'single' | 'multiple'
|
|
17
18
|
|
|
18
19
|
/** Controlled sort state; consumer owns data ordering. / Управляемое состояние сортировки; порядок данных принадлежит consumer. */
|
|
19
20
|
export interface STableSort<Field extends string = string> {
|