@redseed/redseed-ui-vue3 8.38.0 → 8.39.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/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, ref, watch } from 'vue'
|
|
2
|
+
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
|
3
3
|
import { Card, CardHeader } from '../Card'
|
|
4
4
|
import Tr from './Tr.vue'
|
|
5
5
|
import Th from './Th.vue'
|
|
@@ -49,6 +49,15 @@ const props = defineProps({
|
|
|
49
49
|
type: Boolean,
|
|
50
50
|
default: false,
|
|
51
51
|
},
|
|
52
|
+
// Opt-in click-and-drag horizontal scrolling. When true, the user can grab
|
|
53
|
+
// the scrollable columns and drag left/right to pan the table from anywhere,
|
|
54
|
+
// not just the bottom scrollbar. A plain click still emits `click:row`; only a
|
|
55
|
+
// drag past a small threshold scrolls (and suppresses the trailing click).
|
|
56
|
+
// Mouse only — native touch panning is left untouched.
|
|
57
|
+
dragScroll: {
|
|
58
|
+
type: Boolean,
|
|
59
|
+
default: false,
|
|
60
|
+
},
|
|
52
61
|
})
|
|
53
62
|
|
|
54
63
|
// v-model:visibleKeys — undefined means "uncontrolled" / use internal default (all visible).
|
|
@@ -87,6 +96,80 @@ const visibleColumns = computed(() => {
|
|
|
87
96
|
return effectiveVisibleKeys.value.includes(column.key)
|
|
88
97
|
})
|
|
89
98
|
})
|
|
99
|
+
|
|
100
|
+
// --- Drag-to-scroll (opt-in via `dragScroll`) --------------------------------
|
|
101
|
+
const scrollContainer = ref(null)
|
|
102
|
+
const isScrollable = ref(false) // does the container actually overflow horizontally?
|
|
103
|
+
const isDragging = ref(false) // true once a press crosses the drag threshold
|
|
104
|
+
|
|
105
|
+
const DRAG_THRESHOLD = 5 // px of movement before a press becomes a drag, not a click
|
|
106
|
+
|
|
107
|
+
let pressX = 0
|
|
108
|
+
let pressScrollLeft = 0
|
|
109
|
+
let pressing = false
|
|
110
|
+
let dragged = false
|
|
111
|
+
|
|
112
|
+
function updateScrollable() {
|
|
113
|
+
const el = scrollContainer.value
|
|
114
|
+
isScrollable.value = !!el && el.scrollWidth > el.clientWidth
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function onPointerDown(event) {
|
|
118
|
+
if (!props.dragScroll) return
|
|
119
|
+
// Mouse only — leave native touch panning alone — and primary button only.
|
|
120
|
+
if (event.pointerType !== 'mouse' || event.button !== 0) return
|
|
121
|
+
const el = scrollContainer.value
|
|
122
|
+
if (!el || el.scrollWidth <= el.clientWidth) return
|
|
123
|
+
// The pinned column is the anchor: grab the scrollable columns, not it.
|
|
124
|
+
if (event.target.closest?.('.rsui-td--pinned, .rsui-th--pinned')) return
|
|
125
|
+
pressing = true
|
|
126
|
+
dragged = false
|
|
127
|
+
pressX = event.clientX
|
|
128
|
+
pressScrollLeft = el.scrollLeft
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function onPointerMove(event) {
|
|
132
|
+
if (!pressing) return
|
|
133
|
+
const dx = event.clientX - pressX
|
|
134
|
+
if (!dragged) {
|
|
135
|
+
if (Math.abs(dx) < DRAG_THRESHOLD) return
|
|
136
|
+
dragged = true
|
|
137
|
+
isDragging.value = true
|
|
138
|
+
scrollContainer.value?.setPointerCapture?.(event.pointerId)
|
|
139
|
+
}
|
|
140
|
+
event.preventDefault() // suppress text selection while dragging
|
|
141
|
+
scrollContainer.value.scrollLeft = pressScrollLeft - dx
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function endPress(event) {
|
|
145
|
+
if (!pressing) return
|
|
146
|
+
pressing = false
|
|
147
|
+
isDragging.value = false
|
|
148
|
+
const el = scrollContainer.value
|
|
149
|
+
if (el?.hasPointerCapture?.(event.pointerId)) el.releasePointerCapture(event.pointerId)
|
|
150
|
+
// `dragged` stays true so the click-capture handler swallows the click the
|
|
151
|
+
// browser fires after a drag; it resets on the next press.
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function onClickCapture(event) {
|
|
155
|
+
// Swallow the click that trails a drag so a row isn't activated by scrolling.
|
|
156
|
+
if (dragged) {
|
|
157
|
+
event.stopPropagation()
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
dragged = false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let resizeObserver = null
|
|
164
|
+
onMounted(() => {
|
|
165
|
+
updateScrollable()
|
|
166
|
+
if (window.ResizeObserver && scrollContainer.value) {
|
|
167
|
+
resizeObserver = new ResizeObserver(updateScrollable)
|
|
168
|
+
resizeObserver.observe(scrollContainer.value)
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
onBeforeUnmount(() => resizeObserver?.disconnect())
|
|
172
|
+
watch([() => props.rows, visibleColumns], () => nextTick(updateScrollable), { deep: true })
|
|
90
173
|
</script>
|
|
91
174
|
|
|
92
175
|
<template>
|
|
@@ -138,7 +221,20 @@ const visibleColumns = computed(() => {
|
|
|
138
221
|
},
|
|
139
222
|
]"
|
|
140
223
|
>
|
|
141
|
-
<div
|
|
224
|
+
<div ref="scrollContainer"
|
|
225
|
+
:class="[
|
|
226
|
+
'rsui-table__container',
|
|
227
|
+
{
|
|
228
|
+
'rsui-table__container--draggable': dragScroll && isScrollable,
|
|
229
|
+
'rsui-table__container--dragging': isDragging,
|
|
230
|
+
},
|
|
231
|
+
]"
|
|
232
|
+
@pointerdown="onPointerDown"
|
|
233
|
+
@pointermove="onPointerMove"
|
|
234
|
+
@pointerup="endPress"
|
|
235
|
+
@pointercancel="endPress"
|
|
236
|
+
@click.capture="onClickCapture"
|
|
237
|
+
>
|
|
142
238
|
<table
|
|
143
239
|
:aria-labelledby="showHeader && $slots.title ? titleId : undefined"
|
|
144
240
|
:aria-label="!showHeader || !$slots.title ? ariaLabel : undefined"
|