@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.38.0",
3
+ "version": "8.39.0",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -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 class="rsui-table__container">
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"