• 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

0.0
/src/lib/plugins/addResizedColumns.ts
NEW
1
import type { HeaderCell } from '../headerCells.js'
×
NEW
2
import type { NewTableAttributeSet, NewTablePropSet, TablePlugin } from '../types/TablePlugin.js'
×
NEW
3
import { sum } from '../utils/math.js'
×
NEW
4
import { keyed } from '@humanspeak/svelte-keyed'
×
NEW
5
import { derived, writable, type Writable } from 'svelte/store'
×
6

×
7
export interface AddResizedColumnsConfig {
×
NEW
8
    onResizeEnd?: (ev: Event) => void
×
9
}
×
10

×
11
export type ResizedColumnsState = {
×
NEW
12
    columnWidths: Writable<Record<string, number>>
×
NEW
13
}
×
14

×
15
export type ResizedColumnsColumnOptions = {
×
NEW
16
    initialWidth?: number
×
NEW
17
    minWidth?: number
×
NEW
18
    maxWidth?: number
×
NEW
19
    disable?: boolean
×
NEW
20
}
×
21

×
22
export type ResizedColumnsPropSet = NewTablePropSet<{
×
NEW
23
    'thead.tr.th': {
×
NEW
24
        (node: Element): void
×
NEW
25
        drag: (node: Element) => void
×
NEW
26
        reset: (node: Element) => void
×
NEW
27
        disabled: boolean
×
NEW
28
    }
×
NEW
29
}>
×
30

×
31
export type ResizedColumnsAttributeSet = NewTableAttributeSet<{
×
NEW
32
    'thead.tr.th': {
×
NEW
33
        style?: {
×
NEW
34
            width: string
×
NEW
35
            'min-width': string
×
NEW
36
            'max-width': string
×
NEW
37
            'box-sizing': 'border-box'
×
NEW
38
        }
×
NEW
39
    }
×
NEW
40
    'tbody.tr.td': {
×
NEW
41
        style?: {
×
NEW
42
            width: string
×
NEW
43
            'min-width': string
×
NEW
44
            'max-width': string
×
NEW
45
            'box-sizing': 'border-box'
×
NEW
46
        }
×
NEW
47
    }
×
NEW
48
}>
×
49

×
50
const getDragXPos = (event: Event): number => {
×
NEW
51
    if (event instanceof MouseEvent) return event.clientX
×
NEW
52
    if (event instanceof TouchEvent) return event.targetTouches[0].pageX
×
NEW
53
    return 0
×
NEW
54
}
×
55

×
56
// eslint-disable-next-line @typescript-eslint/no-explicit-any
×
57
const isCellDisabled = (cell: HeaderCell<any>, disabledIds: string[]) => {
×
NEW
58
    if (disabledIds.includes(cell.id)) return true
×
NEW
59
    if (cell.isGroup() && cell.ids.every((id) => disabledIds.includes(id))) {
×
NEW
60
        return true
×
NEW
61
    }
×
NEW
62
    return false
×
NEW
63
}
×
64

×
65
type ColumnsWidthState = {
×
NEW
66
    current: Record<string, number>
×
NEW
67
    start: Record<string, number>
×
NEW
68
}
×
69

×
70
export const addResizedColumns =
×
NEW
71
    <Item>({ onResizeEnd }: AddResizedColumnsConfig = {}): TablePlugin<
×
NEW
72
        Item,
×
NEW
73
        ResizedColumnsState,
×
NEW
74
        ResizedColumnsColumnOptions,
×
NEW
75
        ResizedColumnsPropSet,
×
NEW
76
        ResizedColumnsAttributeSet
×
NEW
77
    > =>
×
NEW
78
    ({ columnOptions }) => {
×
NEW
79
        const disabledResizeIds = Object.entries(columnOptions)
×
NEW
80
            .filter(([, option]) => option.disable === true)
×
NEW
81
            .map(([columnId]) => columnId)
×
82

×
NEW
83
        const initialWidths = Object.fromEntries(
×
NEW
84
            Object.entries(columnOptions)
×
NEW
85
                .filter(([, option]) => option.initialWidth !== undefined)
×
NEW
86
                .map(([columnId, { initialWidth }]) => [columnId, initialWidth as number])
×
NEW
87
        )
×
88

×
NEW
89
        const columnsWidthState = writable<ColumnsWidthState>({
×
NEW
90
            current: initialWidths,
×
NEW
91
            start: {}
×
NEW
92
        })
×
NEW
93
        const columnWidths = keyed(columnsWidthState, 'current')
×
94

×
NEW
95
        const pluginState = { columnWidths }
×
96

×
NEW
97
        const dragStartXPosForId: Record<string, number> = {}
×
NEW
98
        const nodeForId: Record<string, Element> = {}
×
99

×
NEW
100
        return {
×
NEW
101
            pluginState,
×
NEW
102
            hooks: {
×
NEW
103
                'thead.tr.th': (cell) => {
×
NEW
104
                    const dblClick = (event: Event) => {
×
NEW
105
                        if (isCellDisabled(cell, disabledResizeIds)) return
×
NEW
106
                        const { target } = event
×
NEW
107
                        if (target === null) return
×
NEW
108
                        event.stopPropagation()
×
NEW
109
                        event.preventDefault()
×
NEW
110
                        if (cell.isGroup()) {
×
NEW
111
                            cell.ids.forEach((id) => {
×
NEW
112
                                const node = nodeForId[id]
×
NEW
113
                                if (node !== undefined) {
×
NEW
114
                                    columnWidths.update(($columnWidths) => ({
×
NEW
115
                                        ...$columnWidths,
×
NEW
116
                                        [id]: initialWidths[id]
×
NEW
117
                                    }))
×
NEW
118
                                }
×
NEW
119
                            })
×
NEW
120
                        } else {
×
NEW
121
                            const node = nodeForId[cell.id]
×
NEW
122
                            if (node !== undefined) {
×
NEW
123
                                columnWidths.update(($columnWidths) => ({
×
NEW
124
                                    ...$columnWidths,
×
NEW
125
                                    [cell.id]: initialWidths[cell.id]
×
NEW
126
                                }))
×
NEW
127
                            }
×
NEW
128
                        }
×
NEW
129
                    }
×
NEW
130
                    let tapedTwice = false
×
NEW
131
                    const checkDoubleTap = (event: Event) => {
×
NEW
132
                        if (!tapedTwice) {
×
NEW
133
                            tapedTwice = true
×
NEW
134
                            setTimeout(function () {
×
NEW
135
                                tapedTwice = false
×
NEW
136
                            }, 300)
×
NEW
137
                            return false
×
NEW
138
                        }
×
NEW
139
                        event.preventDefault()
×
NEW
140
                        dblClick(event)
×
NEW
141
                    }
×
NEW
142
                    const dragStart = (event: Event) => {
×
NEW
143
                        if (isCellDisabled(cell, disabledResizeIds)) return
×
NEW
144
                        const { target } = event
×
NEW
145
                        if (target === null) return
×
NEW
146
                        event.stopPropagation()
×
NEW
147
                        event.preventDefault()
×
NEW
148
                        dragStartXPosForId[cell.id] = getDragXPos(event)
×
NEW
149
                        columnsWidthState.update(($columnsWidthState) => {
×
NEW
150
                            const $updatedState = {
×
NEW
151
                                ...$columnsWidthState,
×
NEW
152
                                start: { ...$columnsWidthState.start }
×
NEW
153
                            }
×
NEW
154
                            if (cell.isGroup()) {
×
NEW
155
                                cell.ids.forEach((id) => {
×
NEW
156
                                    $updatedState.start[id] = $columnsWidthState.current[id]
×
NEW
157
                                })
×
NEW
158
                            } else {
×
NEW
159
                                $updatedState.start[cell.id] = $columnsWidthState.current[cell.id]
×
NEW
160
                            }
×
NEW
161
                            return $updatedState
×
NEW
162
                        })
×
NEW
163
                        if (event instanceof MouseEvent) {
×
NEW
164
                            window.addEventListener('mousemove', dragMove)
×
NEW
165
                            window.addEventListener('mouseup', dragEnd)
×
NEW
166
                        } else {
×
NEW
167
                            window.addEventListener('touchmove', dragMove)
×
NEW
168
                            window.addEventListener('touchend', dragEnd)
×
NEW
169
                        }
×
NEW
170
                    }
×
NEW
171
                    const dragMove = (event: Event) => {
×
NEW
172
                        event.stopPropagation()
×
NEW
173
                        event.preventDefault()
×
NEW
174
                        const deltaWidth = getDragXPos(event) - dragStartXPosForId[cell.id]
×
NEW
175
                        columnsWidthState.update(($columnsWidthState) => {
×
NEW
176
                            const $updatedState = {
×
NEW
177
                                ...$columnsWidthState,
×
NEW
178
                                current: { ...$columnsWidthState.current }
×
NEW
179
                            }
×
NEW
180
                            if (cell.isGroup()) {
×
NEW
181
                                const enabledIds = cell.ids.filter(
×
NEW
182
                                    (id) => !disabledResizeIds.includes(id)
×
NEW
183
                                )
×
NEW
184
                                const totalStartWidth = sum(
×
NEW
185
                                    enabledIds.map((id) => $columnsWidthState.start[id])
×
NEW
186
                                )
×
NEW
187
                                enabledIds.forEach((id) => {
×
NEW
188
                                    const startWidth = $columnsWidthState.start[id]
×
NEW
189
                                    if (startWidth !== undefined) {
×
NEW
190
                                        $updatedState.current[id] = Math.max(
×
NEW
191
                                            0,
×
NEW
192
                                            startWidth + deltaWidth * (startWidth / totalStartWidth)
×
NEW
193
                                        )
×
NEW
194
                                    }
×
NEW
195
                                })
×
NEW
196
                            } else {
×
NEW
197
                                const startWidth = $columnsWidthState.start[cell.id]
×
NEW
198
                                const { minWidth = 0, maxWidth } = columnOptions[cell.id] ?? {}
×
NEW
199
                                if (startWidth !== undefined) {
×
NEW
200
                                    $updatedState.current[cell.id] = Math.min(
×
NEW
201
                                        Math.max(minWidth, startWidth + deltaWidth),
×
NEW
202
                                        ...(maxWidth === undefined ? [] : [maxWidth])
×
NEW
203
                                    )
×
NEW
204
                                }
×
NEW
205
                            }
×
NEW
206
                            return $updatedState
×
NEW
207
                        })
×
NEW
208
                    }
×
NEW
209
                    const dragEnd = (event: Event) => {
×
NEW
210
                        event.stopPropagation()
×
NEW
211
                        event.preventDefault()
×
NEW
212
                        if (cell.isGroup()) {
×
NEW
213
                            cell.ids.forEach((id) => {
×
NEW
214
                                const node = nodeForId[id]
×
NEW
215
                                if (node !== undefined) {
×
NEW
216
                                    columnWidths.update(($columnWidths) => ({
×
NEW
217
                                        ...$columnWidths,
×
NEW
218
                                        [id]: node.getBoundingClientRect().width
×
NEW
219
                                    }))
×
NEW
220
                                }
×
NEW
221
                            })
×
NEW
222
                        } else {
×
NEW
223
                            const node = nodeForId[cell.id]
×
NEW
224
                            if (node !== undefined) {
×
NEW
225
                                columnWidths.update(($columnWidths) => ({
×
NEW
226
                                    ...$columnWidths,
×
NEW
227
                                    [cell.id]: node.getBoundingClientRect().width
×
NEW
228
                                }))
×
NEW
229
                            }
×
NEW
230
                        }
×
NEW
231
                        onResizeEnd?.(event)
×
NEW
232
                        if (event instanceof MouseEvent) {
×
NEW
233
                            window.removeEventListener('mousemove', dragMove)
×
NEW
234
                            window.removeEventListener('mouseup', dragEnd)
×
NEW
235
                        } else {
×
NEW
236
                            window.removeEventListener('touchmove', dragMove)
×
NEW
237
                            window.removeEventListener('touchend', dragEnd)
×
NEW
238
                        }
×
NEW
239
                    }
×
NEW
240
                    const $props = (node: Element) => {
×
NEW
241
                        nodeForId[cell.id] = node
×
NEW
242
                        if (cell.isFlat()) {
×
NEW
243
                            columnWidths.update(($columnWidths) => ({
×
NEW
244
                                ...$columnWidths,
×
NEW
245
                                [cell.id]: node.getBoundingClientRect().width
×
NEW
246
                            }))
×
NEW
247
                        }
×
NEW
248
                        return {
×
NEW
249
                            destroy() {
×
NEW
250
                                delete nodeForId[cell.id]
×
NEW
251
                            }
×
NEW
252
                        }
×
NEW
253
                    }
×
NEW
254
                    $props.drag = (node: Element) => {
×
NEW
255
                        node.addEventListener('mousedown', dragStart)
×
NEW
256
                        node.addEventListener('touchstart', dragStart)
×
NEW
257
                        return {
×
NEW
258
                            destroy() {
×
NEW
259
                                node.removeEventListener('mousedown', dragStart)
×
NEW
260
                                node.removeEventListener('touchstart', dragStart)
×
NEW
261
                            }
×
NEW
262
                        }
×
NEW
263
                    }
×
NEW
264
                    $props.reset = (node: Element) => {
×
NEW
265
                        node.addEventListener('dblclick', dblClick)
×
NEW
266
                        node.addEventListener('touchend', checkDoubleTap)
×
NEW
267
                        return {
×
NEW
268
                            destroy() {
×
NEW
269
                                node.removeEventListener('dblckick', dblClick)
×
NEW
270
                                node.removeEventListener('touchend', checkDoubleTap)
×
NEW
271
                            }
×
NEW
272
                        }
×
NEW
273
                    }
×
NEW
274
                    $props.disabled = isCellDisabled(cell, disabledResizeIds)
×
NEW
275
                    const props = derived([], () => {
×
NEW
276
                        return $props
×
NEW
277
                    })
×
NEW
278
                    const attrs = derived(columnWidths, ($columnWidths) => {
×
NEW
279
                        const width = cell.isGroup()
×
NEW
280
                            ? sum(cell.ids.map((id) => $columnWidths[id]))
×
NEW
281
                            : $columnWidths[cell.id]
×
NEW
282
                        if (width === undefined) {
×
NEW
283
                            return {}
×
NEW
284
                        }
×
NEW
285
                        const widthPx = `${width}px`
×
NEW
286
                        return {
×
NEW
287
                            style: {
×
NEW
288
                                width: widthPx,
×
NEW
289
                                'min-width': widthPx,
×
NEW
290
                                'max-width': widthPx,
×
NEW
291
                                'box-sizing': 'border-box' as const
×
NEW
292
                            }
×
NEW
293
                        }
×
NEW
294
                    })
×
NEW
295
                    return { props, attrs }
×
NEW
296
                },
×
NEW
297
                'tbody.tr.td': (cell) => {
×
NEW
298
                    const attrs = derived(columnWidths, ($columnWidths) => {
×
NEW
299
                        const width = $columnWidths[cell.id]
×
NEW
300
                        if (width === undefined) {
×
NEW
301
                            return {}
×
NEW
302
                        }
×
NEW
303
                        const widthPx = `${width}px`
×
NEW
304
                        return {
×
NEW
305
                            style: {
×
NEW
306
                                width: widthPx,
×
NEW
307
                                'min-width': widthPx,
×
NEW
308
                                'max-width': widthPx,
×
NEW
309
                                'box-sizing': 'border-box' as const
×
NEW
310
                            }
×
NEW
311
                        }
×
NEW
312
                    })
×
NEW
313
                    return { attrs }
×
NEW
314
                }
×
NEW
315
            }
×
NEW
316
        }
×
NEW
317
    }
×
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