@redseed/redseed-ui-vue3 8.20.0 → 8.21.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.20.0",
3
+ "version": "8.21.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 { ref, computed } from 'vue'
2
+ import { ref, computed, onMounted, onUpdated, onBeforeUnmount } from 'vue'
3
3
  import Empty from '../Empty/Empty.vue'
4
4
  import Pagination from '../Pagination/Pagination.vue'
5
5
  import { useResponsiveWidth } from '../../helpers'
@@ -29,10 +29,20 @@ const props = defineProps({
29
29
  pagination: {
30
30
  type: Boolean,
31
31
  default: true
32
+ },
33
+ reorderable: {
34
+ type: Boolean,
35
+ default: false
36
+ },
37
+ handleAlignment: {
38
+ type: String,
39
+ default: 'center',
40
+ validator: (value) => ['top', 'center', 'bottom'].includes(value)
32
41
  }
33
42
  })
34
43
 
35
44
  const cardGroupElement = ref(null)
45
+ const cardsContainerElement = ref(null)
36
46
 
37
47
  const { responsiveWidth } = useResponsiveWidth(cardGroupElement)
38
48
 
@@ -41,6 +51,7 @@ const cardGroupClasses = computed(() => [
41
51
  {
42
52
  'rsui-card-group--compact': props.variant === 'compact',
43
53
  'rsui-card-group--list': props.variant === 'list',
54
+ 'rsui-card-group--reorderable': props.reorderable,
44
55
  'rsui-card-group--2xs': responsiveWidth.value['2xs'],
45
56
  'rsui-card-group--xs': responsiveWidth.value['xs'],
46
57
  'rsui-card-group--sm': responsiveWidth.value['sm'],
@@ -58,9 +69,164 @@ const emit = defineEmits([
58
69
  'lastPageUpdated',
59
70
  'clickEmptyAction',
60
71
  'clickSecondaryEmptyAction',
61
- 'clickTertiaryEmptyAction'
72
+ 'clickTertiaryEmptyAction',
73
+ 'reorder'
62
74
  ])
63
75
 
76
+ // Drag and drop
77
+ const dragFromIndex = ref(null)
78
+
79
+ function createHandleSvg() {
80
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
81
+ svg.setAttribute('width', '16')
82
+ svg.setAttribute('height', '24')
83
+ svg.setAttribute('viewBox', '0 0 16 24')
84
+ svg.setAttribute('fill', 'currentColor')
85
+ svg.setAttribute('aria-hidden', 'true')
86
+ svg.classList.add('rsui-card-group__drag-handle-icon')
87
+ // 6 dots: 2 columns x 3 rows
88
+ const positions = [
89
+ [4, 5], [12, 5],
90
+ [4, 12], [12, 12],
91
+ [4, 19], [12, 19],
92
+ ]
93
+ positions.forEach(([cx, cy]) => {
94
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
95
+ circle.setAttribute('cx', cx)
96
+ circle.setAttribute('cy', cy)
97
+ circle.setAttribute('r', '2')
98
+ svg.appendChild(circle)
99
+ })
100
+ return svg
101
+ }
102
+
103
+ function getCardChild(el) {
104
+ const container = cardsContainerElement.value
105
+ if (!container || !el) return null
106
+ let node = el
107
+ while (node && node.parentElement !== container) {
108
+ node = node.parentElement
109
+ }
110
+ return node
111
+ }
112
+
113
+ function getChildIndex(el) {
114
+ const child = getCardChild(el)
115
+ if (!child) return -1
116
+ return Array.from(cardsContainerElement.value.children).indexOf(child)
117
+ }
118
+
119
+ function onHandleMouseDown(e) {
120
+ const child = getCardChild(e.target)
121
+ if (child) {
122
+ child.setAttribute('draggable', 'true')
123
+ }
124
+ }
125
+
126
+ function onHandleMouseUp(e) {
127
+ const child = getCardChild(e.target)
128
+ if (child) {
129
+ child.removeAttribute('draggable')
130
+ }
131
+ }
132
+
133
+ function onDragStart(e) {
134
+ if (!props.reorderable) return
135
+ const index = getChildIndex(e.target)
136
+ if (index === -1) return
137
+ dragFromIndex.value = index
138
+ const child = getCardChild(e.target)
139
+ child.classList.add('rsui-card-group__card--dragging')
140
+ e.dataTransfer.effectAllowed = 'move'
141
+ }
142
+
143
+ function onDragOver(e) {
144
+ if (!props.reorderable || dragFromIndex.value === null) return
145
+ e.preventDefault()
146
+ e.dataTransfer.dropEffect = 'move'
147
+ const index = getChildIndex(e.target)
148
+ if (index === -1 || index === dragFromIndex.value) return
149
+
150
+ const container = cardsContainerElement.value
151
+ const children = Array.from(container.children)
152
+ children.forEach((child, i) => {
153
+ child.classList.toggle('rsui-card-group__card--drag-over', i === index)
154
+ })
155
+ }
156
+
157
+ function onDragLeave(e) {
158
+ if (!props.reorderable) return
159
+ const child = getCardChild(e.target)
160
+ if (child && !child.contains(e.relatedTarget)) {
161
+ child.classList.remove('rsui-card-group__card--drag-over')
162
+ }
163
+ }
164
+
165
+ function onDrop(e) {
166
+ if (!props.reorderable) return
167
+ e.preventDefault()
168
+ const toIndex = getChildIndex(e.target)
169
+ if (toIndex !== -1 && dragFromIndex.value !== null && dragFromIndex.value !== toIndex) {
170
+ emit('reorder', { fromIndex: dragFromIndex.value, toIndex })
171
+ }
172
+ cleanupDragState()
173
+ }
174
+
175
+ function onDragEnd() {
176
+ cleanupDragState()
177
+ }
178
+
179
+ function cleanupDragState() {
180
+ const container = cardsContainerElement.value
181
+ if (container) {
182
+ Array.from(container.children).forEach(child => {
183
+ child.classList.remove('rsui-card-group__card--drag-over', 'rsui-card-group__card--dragging')
184
+ child.removeAttribute('draggable')
185
+ })
186
+ }
187
+ dragFromIndex.value = null
188
+ }
189
+
190
+ function syncHandles() {
191
+ const container = cardsContainerElement.value
192
+ if (!container) return
193
+
194
+ Array.from(container.children).forEach(child => {
195
+ const hasHandle = child.querySelector('.rsui-card-group__drag-handle')
196
+ if (props.reorderable && !hasHandle) {
197
+ const handle = document.createElement('div')
198
+ handle.className = `rsui-card-group__drag-handle rsui-card-group__drag-handle--${props.handleAlignment}`
199
+ handle.addEventListener('mousedown', onHandleMouseDown)
200
+ handle.addEventListener('mouseup', onHandleMouseUp)
201
+ handle.appendChild(createHandleSvg())
202
+ child.style.position = 'relative'
203
+ child.appendChild(handle)
204
+ } else if (!props.reorderable && hasHandle) {
205
+ hasHandle.removeEventListener('mousedown', onHandleMouseDown)
206
+ hasHandle.removeEventListener('mouseup', onHandleMouseUp)
207
+ hasHandle.remove()
208
+ }
209
+ })
210
+ }
211
+
212
+ function cleanupHandles() {
213
+ const container = cardsContainerElement.value
214
+ if (!container) return
215
+ Array.from(container.children).forEach(child => {
216
+ const handle = child.querySelector('.rsui-card-group__drag-handle')
217
+ if (handle) {
218
+ handle.removeEventListener('mousedown', onHandleMouseDown)
219
+ handle.removeEventListener('mouseup', onHandleMouseUp)
220
+ handle.remove()
221
+ }
222
+ child.removeAttribute('draggable')
223
+ })
224
+ }
225
+
226
+ onMounted(syncHandles)
227
+ onUpdated(syncHandles)
228
+ onBeforeUnmount(cleanupHandles)
229
+
64
230
  const pageUpdated = (event) => {
65
231
  emit('pageUpdated', event)
66
232
  }
@@ -89,7 +255,15 @@ const showNotFoundMessage = computed(() => !props.totalItems && props.controlApp
89
255
  @last="lastPageUpdated"
90
256
  ></Pagination>
91
257
  </div>
92
- <div v-if="totalItems" class="rsui-card-group__cards">
258
+ <div v-if="totalItems"
259
+ ref="cardsContainerElement"
260
+ class="rsui-card-group__cards"
261
+ @dragstart="onDragStart"
262
+ @dragover="onDragOver"
263
+ @dragleave="onDragLeave"
264
+ @drop="onDrop"
265
+ @dragend="onDragEnd"
266
+ >
93
267
  <slot></slot>
94
268
  </div>
95
269
  <div v-if="showEmptyMessage" class="rsui-card-group__empty">