• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

humanspeak / svelte-headless-table / 12913869200

22 Jan 2025 05:36PM UTC coverage: 34.206% (-25.6%) from 59.766%
12913869200

push

github

web-flow
Merge pull request #9 from humanspeak/feature-version

Feature: 5.x

404 of 538 branches covered (75.09%)

Branch coverage included in aggregate %.

2129 of 3542 new or added lines in 41 files covered. (60.11%)

1 existing line in 1 file now uncovered.

2639 of 8358 relevant lines covered (31.57%)

17.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.51
/src/lib/plugins/addSortBy.ts
1
import type { DataBodyCell } from '../bodyCells.js'
1✔
2
import type { BodyRow } from '../bodyRows.js'
1✔
3
import type { TablePlugin, NewTablePropSet, DeriveRowsFn } from '../types/TablePlugin.js'
1✔
4
import { compare } from '../utils/compare.js'
1✔
5
import { isShiftClick } from '../utils/event.js'
1✔
6
import { derived, writable, type Readable, type Writable } from 'svelte/store'
1✔
7

1✔
8
export interface SortByConfig {
1✔
9
    initialSortKeys?: SortKey[]
1✔
10
    disableMultiSort?: boolean
1✔
11
    isMultiSortEvent?: (event: Event) => boolean
1✔
12
    toggleOrder?: ('asc' | 'desc' | undefined)[]
1✔
13
    serverSide?: boolean
1✔
14
}
1✔
15

1✔
16
const DEFAULT_TOGGLE_ORDER: ('asc' | 'desc' | undefined)[] = ['asc', 'desc', undefined]
1✔
17

1✔
18
export interface SortByState<Item> {
1✔
19
    sortKeys: WritableSortKeys
1✔
20
    preSortedRows: Readable<BodyRow<Item>[]>
1✔
21
}
1✔
22

1✔
23
export interface SortByColumnOptions {
1✔
24
    disable?: boolean
1✔
25
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
1✔
26
    getSortValue?: (value: any) => string | number | (string | number)[]
1✔
27
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
1✔
28
    compareFn?: (left: any, right: any) => number
1✔
29
    invert?: boolean
1✔
30
}
1✔
31

1✔
32
export type SortByPropSet = NewTablePropSet<{
1✔
33
    'thead.tr.th': {
1✔
34
        order: 'asc' | 'desc' | undefined
1✔
35
        toggle: (event: Event) => void
1✔
36
        clear: () => void
1✔
37
        disabled: boolean
1✔
38
    }
1✔
39
    'tbody.tr.td': {
1✔
40
        order: 'asc' | 'desc' | undefined
1✔
41
    }
1✔
42
}>
1✔
43

1✔
44
export interface SortKey {
1✔
45
    id: string
1✔
46
    order: 'asc' | 'desc'
1✔
47
}
1✔
48

1✔
49
export const createSortKeysStore = (initKeys: SortKey[]): WritableSortKeys => {
1✔
50
    const { subscribe, update, set } = writable(initKeys)
3✔
51
    const toggleId = (
3✔
NEW
52
        id: string,
×
NEW
53
        { multiSort = true, toggleOrder = DEFAULT_TOGGLE_ORDER }: ToggleOptions = {}
×
NEW
54
    ) => {
×
NEW
55
        update(($sortKeys) => {
×
NEW
56
            const keyIdx = $sortKeys.findIndex((key) => key.id === id)
×
NEW
57
            const key = $sortKeys[keyIdx]
×
NEW
58
            const order = key?.order
×
NEW
59
            const orderIdx = toggleOrder.findIndex((o) => o === order)
×
NEW
60
            const nextOrderIdx = (orderIdx + 1) % toggleOrder.length
×
NEW
61
            const nextOrder = toggleOrder[nextOrderIdx]
×
NEW
62
            if (!multiSort) {
×
NEW
63
                if (nextOrder === undefined) {
×
NEW
64
                    return []
×
NEW
65
                }
×
NEW
66
                return [{ id, order: nextOrder }]
×
NEW
67
            }
×
NEW
68
            if (keyIdx === -1 && nextOrder !== undefined) {
×
NEW
69
                return [...$sortKeys, { id, order: nextOrder }]
×
NEW
70
            }
×
NEW
71
            if (nextOrder === undefined) {
×
NEW
72
                return [...$sortKeys.slice(0, keyIdx), ...$sortKeys.slice(keyIdx + 1)]
×
NEW
73
            }
×
NEW
74
            return [
×
NEW
75
                ...$sortKeys.slice(0, keyIdx),
×
NEW
76
                { id, order: nextOrder },
×
NEW
77
                ...$sortKeys.slice(keyIdx + 1)
×
NEW
78
            ]
×
NEW
79
        })
×
NEW
80
    }
×
81
    const clearId = (id: string) => {
3✔
NEW
82
        update(($sortKeys) => {
×
NEW
83
            const keyIdx = $sortKeys.findIndex((key) => key.id === id)
×
NEW
84
            if (keyIdx === -1) {
×
NEW
85
                return $sortKeys
×
NEW
86
            }
×
NEW
87
            return [...$sortKeys.slice(0, keyIdx), ...$sortKeys.slice(keyIdx + 1)]
×
NEW
88
        })
×
NEW
89
    }
×
90
    return {
3✔
91
        subscribe,
3✔
92
        update,
3✔
93
        set,
3✔
94
        toggleId,
3✔
95
        clearId
3✔
96
    }
3✔
97
}
3✔
98

1✔
99
interface ToggleOptions {
1✔
100
    multiSort?: boolean
1✔
101
    toggleOrder?: ('asc' | 'desc' | undefined)[]
1✔
102
}
1✔
103

1✔
104
export type WritableSortKeys = Writable<SortKey[]> & {
1✔
105
    toggleId: (id: string, options: ToggleOptions) => void
1✔
106
    clearId: (id: string) => void
1✔
107
}
1✔
108

1✔
109
const getSortedRows = <Item, Row extends BodyRow<Item>>(
1✔
110
    rows: Row[],
3✔
111
    sortKeys: SortKey[],
3✔
112
    columnOptions: Record<string, SortByColumnOptions>
3✔
113
): Row[] => {
3✔
114
    // Shallow clone to prevent sort affecting `preSortedRows`.
3✔
115
    const $sortedRows = [...rows] as typeof rows
3✔
116
    $sortedRows.sort((a, b) => {
3✔
117
        for (const key of sortKeys) {
16✔
118
            const invert = columnOptions[key.id]?.invert ?? false
16✔
119
            // TODO check why cellForId returns `undefined`.
16✔
120
            const cellA = a.cellForId[key.id]
16✔
121
            const cellB = b.cellForId[key.id]
16✔
122
            let order = 0
16✔
123
            const compareFn = columnOptions[key.id]?.compareFn
16✔
124
            const getSortValue = columnOptions[key.id]?.getSortValue
16✔
125
            // Only need to check properties of `cellA` as both should have the same
16✔
126
            // properties.
16✔
127
            if (!cellA.isData()) {
16!
NEW
128
                return 0
×
NEW
129
            }
×
130
            const valueA = cellA.value
16✔
131
            const valueB = (cellB as DataBodyCell<Item>).value
16✔
132
            if (compareFn !== undefined) {
16✔
133
                order = compareFn(valueA, valueB)
6✔
134
            } else if (getSortValue !== undefined) {
16!
NEW
135
                const sortValueA = getSortValue(valueA)
×
NEW
136
                const sortValueB = getSortValue(valueB)
×
NEW
137
                order = compare(sortValueA, sortValueB)
×
138
            } else if (typeof valueA === 'string' || typeof valueA === 'number') {
10!
NEW
139
                // typeof `cellB.value` is logically equal to `cellA.value`.
×
NEW
140
                order = compare(valueA, valueB as string | number)
×
141
            } else if (valueA instanceof Date || valueB instanceof Date) {
10✔
142
                const sortValueA = valueA instanceof Date ? valueA.getTime() : 0
10✔
143
                const sortValueB = valueB instanceof Date ? valueB.getTime() : 0
10✔
144
                order = compare(sortValueA, sortValueB)
10✔
145
            }
10✔
146
            if (order !== 0) {
16✔
147
                let orderFactor = 1
16✔
148
                // If the current key order is `'desc'`, reverse the order.
16✔
149
                if (key.order === 'desc') {
16✔
150
                    orderFactor *= -1
5✔
151
                }
5✔
152
                // If `invert` is `true`, we want to invert the sort without
16✔
153
                // affecting the view model's indication.
16✔
154
                if (invert) {
16!
NEW
155
                    orderFactor *= -1
×
NEW
156
                }
×
157
                return order * orderFactor
16✔
158
            }
16✔
159
        }
16!
NEW
160
        return 0
×
161
    })
3✔
162
    for (let i = 0; i < $sortedRows.length; i++) {
3✔
163
        const { subRows } = $sortedRows[i]
12✔
164
        if (subRows === undefined) {
12✔
165
            continue
12✔
166
        }
12!
NEW
167
        const sortedSubRows = getSortedRows<Item, Row>(subRows as Row[], sortKeys, columnOptions)
×
NEW
168
        const clonedRow = $sortedRows[i].clone() as Row
×
NEW
169
        clonedRow.subRows = sortedSubRows
×
NEW
170
        $sortedRows[i] = clonedRow
×
NEW
171
    }
×
172
    return $sortedRows
3✔
173
}
3✔
174

1✔
175
export const addSortBy =
1✔
176
    <Item>({
1✔
177
        initialSortKeys = [],
3✔
178
        disableMultiSort = false,
3✔
179
        isMultiSortEvent = isShiftClick,
3✔
180
        toggleOrder,
3✔
181
        serverSide = false
3✔
182
    }: SortByConfig = {}): TablePlugin<
3✔
183
        Item,
3✔
184
        SortByState<Item>,
3✔
185
        SortByColumnOptions,
3✔
186
        SortByPropSet
3✔
187
    > =>
3✔
188
    ({ columnOptions }) => {
3✔
189
        const disabledSortIds = Object.entries(columnOptions)
3✔
190
            .filter(([, option]) => option.disable === true)
3✔
191
            .map(([columnId]) => columnId)
3✔
192

3✔
193
        const sortKeys = createSortKeysStore(initialSortKeys)
3✔
194
        const preSortedRows = writable<BodyRow<Item>[]>([])
3✔
195

3✔
196
        const deriveRows: DeriveRowsFn<Item> = (rows) => {
3✔
197
            return derived([rows, sortKeys], ([$rows, $sortKeys]) => {
3✔
198
                preSortedRows.set($rows)
3✔
199
                if (serverSide) {
3!
NEW
200
                    return $rows
×
NEW
201
                }
×
202
                return getSortedRows<Item, (typeof $rows)[number]>($rows, $sortKeys, columnOptions)
3✔
203
            })
3✔
204
        }
3✔
205

3✔
206
        const pluginState: SortByState<Item> = { sortKeys, preSortedRows }
3✔
207

3✔
208
        return {
3✔
209
            pluginState,
3✔
210
            deriveRows,
3✔
211
            hooks: {
3✔
212
                'thead.tr.th': (cell) => {
3✔
NEW
213
                    const disabled = disabledSortIds.includes(cell.id)
×
NEW
214
                    const props = derived(sortKeys, ($sortKeys) => {
×
NEW
215
                        const key = $sortKeys.find((k) => k.id === cell.id)
×
NEW
216
                        const toggle = (event: Event) => {
×
NEW
217
                            if (!cell.isData()) return
×
NEW
218
                            if (disabled) return
×
NEW
219
                            sortKeys.toggleId(cell.id, {
×
NEW
220
                                multiSort: disableMultiSort ? false : isMultiSortEvent(event),
×
NEW
221
                                toggleOrder
×
NEW
222
                            })
×
NEW
223
                        }
×
NEW
224
                        const clear = () => {
×
NEW
225
                            if (!cell.isData()) return
×
NEW
226
                            if (disabledSortIds.includes(cell.id)) return
×
NEW
227
                            sortKeys.clearId(cell.id)
×
NEW
228
                        }
×
NEW
229
                        return {
×
NEW
230
                            order: key?.order,
×
NEW
231
                            toggle,
×
NEW
232
                            clear,
×
NEW
233
                            disabled
×
NEW
234
                        }
×
NEW
235
                    })
×
NEW
236
                    return { props }
×
NEW
237
                },
×
238
                'tbody.tr.td': (cell) => {
3✔
239
                    const props = derived(sortKeys, ($sortKeys) => {
12✔
NEW
240
                        const key = $sortKeys.find((k) => k.id === cell.id)
×
NEW
241
                        return {
×
NEW
242
                            order: key?.order
×
NEW
243
                        }
×
244
                    })
12✔
245
                    return { props }
12✔
246
                }
12✔
247
            }
3✔
248
        }
3✔
249
    }
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc