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

humanspeak / svelte-headless-table / 20583418766

29 Dec 2025 09:47PM UTC coverage: 57.475% (-5.2%) from 62.664%
20583418766

push

github

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

Enhancement: Package Updates & getRowState optimizations

276 of 556 branches covered (49.64%)

Branch coverage included in aggregate %.

24 of 28 new or added lines in 3 files covered. (85.71%)

5 existing lines in 4 files now uncovered.

835 of 1377 relevant lines covered (60.64%)

39.33 hits per line

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

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

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

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

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

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

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

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

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

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

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

109
const getSortedRows = <Item, Row extends BodyRow<Item>>(
2✔
110
    rows: Row[],
111
    sortKeys: SortKey[],
112
    columnOptions: Record<string, SortByColumnOptions>
113
): Row[] => {
114
    // Shallow clone to prevent sort affecting `preSortedRows`.
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`.
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
126
            // properties.
127
            if (!cellA.isData()) {
16!
128
                return 0
×
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) {
10!
135
                const sortValueA = getSortValue(valueA)
×
136
                const sortValueB = getSortValue(valueB)
×
137
                order = compare(sortValueA, sortValueB)
×
138
            } else if (typeof valueA === 'string' || typeof valueA === 'number') {
10!
139
                // typeof `cellB.value` is logically equal to `cellA.value`.
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
            }
146
            if (order !== 0) {
16!
147
                let orderFactor = 1
16✔
148
                // If the current key order is `'desc'`, reverse the order.
149
                if (key.order === 'desc') {
16✔
150
                    orderFactor *= -1
5✔
151
                }
152
                // If `invert` is `true`, we want to invert the sort without
153
                // affecting the view model's indication.
154
                if (invert) {
16!
155
                    orderFactor *= -1
×
156
                }
157
                return order * orderFactor
16✔
158
            }
159
        }
160
        return 0
×
161
    })
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
        }
167
        const sortedSubRows = getSortedRows<Item, Row>(subRows as Row[], sortKeys, columnOptions)
×
168
        const clonedRow = $sortedRows[i].clone() as Row
×
169
        clonedRow.subRows = sortedSubRows
×
170
        $sortedRows[i] = clonedRow
×
171
    }
172
    return $sortedRows
3✔
173
}
174

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

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

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

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

208
        return {
4✔
209
            pluginState,
210
            deriveRows,
211
            hooks: {
212
                'thead.tr.th': (cell) => {
213
                    const disabled = disabledSortIds.includes(cell.id)
×
214
                    const props = derived(sortKeys, ($sortKeys) => {
×
215
                        const key = $sortKeys.find((k) => k.id === cell.id)
×
216
                        const toggle = (event: Event) => {
×
217
                            if (!cell.isData()) return
×
218
                            if (disabled) return
×
219
                            sortKeys.toggleId(cell.id, {
×
220
                                multiSort: disableMultiSort ? false : isMultiSortEvent(event),
×
221
                                toggleOrder
222
                            })
223
                        }
224
                        const clear = () => {
×
225
                            if (!cell.isData()) return
×
226
                            if (disabledSortIds.includes(cell.id)) return
×
227
                            sortKeys.clearId(cell.id)
×
228
                        }
229
                        return {
×
230
                            order: key?.order,
231
                            toggle,
232
                            clear,
233
                            disabled
234
                        }
235
                    })
236
                    return { props }
×
237
                },
238
                'tbody.tr.td': (cell) => {
239
                    const props = derived(sortKeys, ($sortKeys) => {
12✔
240
                        const key = $sortKeys.find((k) => k.id === cell.id)
×
241
                        return {
×
242
                            order: key?.order
243
                        }
244
                    })
245
                    return { props }
12✔
246
                }
247
            }
248
        }
249
    }
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