picocrank 1.32.0 → 1.34.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/docs/filter-query-v1.md +5 -1
- package/docs/table.md +99 -0
- package/package.json +1 -1
- package/router.js +33 -0
- package/styles/picocrank-extensions.css +23 -2
- package/vue/components/Accordion.spec.js +48 -0
- package/vue/components/Accordion.vue +46 -0
- package/vue/components/AccordionItem.vue +103 -0
- package/vue/components/FieldValue.spec.js +88 -0
- package/vue/components/FieldValue.vue +222 -0
- package/vue/components/FormField.spec.js +1 -0
- package/vue/components/FormField.vue +38 -10
- package/vue/components/Header.vue +24 -2
- package/vue/components/NotificationBlock.vue +25 -0
- package/vue/components/ReorderList.spec.js +318 -0
- package/vue/components/ReorderList.vue +614 -0
- package/vue/components/Table.spec.js +490 -1
- package/vue/components/Table.vue +1038 -111
- package/vue/components/TableColumnFilterPopover.vue +63 -19
- package/vue/components/TableColumnOptionsPopover.vue +248 -273
- package/vue/components/TopBar.vue +6 -2
- package/vue/components/accordionContext.js +1 -0
- package/vue/composables/tableColumnPresets.js +33 -0
- package/vue/composables/tableFilters.js +6 -1
- package/vue/composables/tableGroupBy.js +173 -0
- package/vue/composables/tableGroupBy.spec.js +69 -0
- package/vue/composables/tableViewSettings.js +34 -0
- package/vue/composables/tableViewSettings.spec.js +21 -0
- package/vue/composables/useHeaderTopBarFit.js +91 -0
- package/vue/composables/useResponsiveNav.js +5 -3
- package/vue/data/navSectionHubs.js +16 -1
- package/vue/examples/AccordionExample.vue +33 -0
- package/vue/examples/FieldValueExample.vue +143 -0
- package/vue/examples/FormExample.vue +11 -6
- package/vue/examples/ReorderListExample.vue +236 -0
- package/vue/examples/TableExample.vue +5 -1
- package/vue/examples/TableRemoteExample.vue +28 -4
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<p v-if="items.length === 0" class="reorder-list-empty">
|
|
3
|
+
<slot name="empty">Nothing to reorder.</slot>
|
|
4
|
+
</p>
|
|
5
|
+
<ul
|
|
6
|
+
v-else
|
|
7
|
+
ref="rootRef"
|
|
8
|
+
class="reorder-list"
|
|
9
|
+
data-reorder-list-root
|
|
10
|
+
:class="{ 'is-disabled': disabled }"
|
|
11
|
+
:aria-label="ariaLabel || undefined"
|
|
12
|
+
:aria-labelledby="ariaLabelledby || undefined"
|
|
13
|
+
:aria-disabled="disabled ? 'true' : undefined"
|
|
14
|
+
@dragover.prevent="onListDragOver"
|
|
15
|
+
@drop.prevent="onListDrop"
|
|
16
|
+
@dragleave="onListDragLeave"
|
|
17
|
+
>
|
|
18
|
+
<li
|
|
19
|
+
v-if="dropGap !== null"
|
|
20
|
+
class="reorder-list-separator"
|
|
21
|
+
:style="{ top: `${separatorTop}px` }"
|
|
22
|
+
:data-drop-gap="dropGap"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
/>
|
|
25
|
+
<li
|
|
26
|
+
v-for="(item, index) in items"
|
|
27
|
+
:key="keyOf(item, index)"
|
|
28
|
+
class="reorder-list-row"
|
|
29
|
+
:class="{
|
|
30
|
+
'is-dragging': draggingKey === keyOf(item, index),
|
|
31
|
+
'is-drag-whole-row': dragWholeRow && !disabled && !isItemReorderDisabled(item, index),
|
|
32
|
+
'is-reorder-disabled': isItemReorderDisabled(item, index),
|
|
33
|
+
}"
|
|
34
|
+
:data-reorder-key="keyOf(item, index)"
|
|
35
|
+
:draggable="dragWholeRow && !disabled && !isItemReorderDisabled(item, index) ? 'true' : 'false'"
|
|
36
|
+
:tabindex="rowTabIndex"
|
|
37
|
+
:aria-label="rowDragAriaLabel(item, index)"
|
|
38
|
+
@dragstart="onRowDragStart(item, index, $event)"
|
|
39
|
+
@dragend="onDragEnd"
|
|
40
|
+
@keydown="onRowKeydown(index, $event)"
|
|
41
|
+
>
|
|
42
|
+
<div v-if="showMoveButtons || showHandleGrip" class="reorder-list-controls">
|
|
43
|
+
<div
|
|
44
|
+
v-if="showMoveButtons"
|
|
45
|
+
class="reorder-list-move"
|
|
46
|
+
role="group"
|
|
47
|
+
:aria-label="`Move ${labelOf(item, index)}`"
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="neutral reorder-list-move-button"
|
|
52
|
+
:disabled="!canMove(index, -1)"
|
|
53
|
+
:aria-label="`Move ${labelOf(item, index)} up`"
|
|
54
|
+
@click="moveBy(index, -1, true)"
|
|
55
|
+
>
|
|
56
|
+
<HugeiconsIcon
|
|
57
|
+
:icon="ArrowUp01Icon"
|
|
58
|
+
width="0.9em"
|
|
59
|
+
height="0.9em"
|
|
60
|
+
:strokeWidth="2"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
class="neutral reorder-list-move-button"
|
|
67
|
+
:disabled="!canMove(index, 1)"
|
|
68
|
+
:aria-label="`Move ${labelOf(item, index)} down`"
|
|
69
|
+
@click="moveBy(index, 1, true)"
|
|
70
|
+
>
|
|
71
|
+
<HugeiconsIcon
|
|
72
|
+
:icon="ArrowDown01Icon"
|
|
73
|
+
width="0.9em"
|
|
74
|
+
height="0.9em"
|
|
75
|
+
:strokeWidth="2"
|
|
76
|
+
aria-hidden="true"
|
|
77
|
+
/>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<span
|
|
82
|
+
v-if="showHandleGrip"
|
|
83
|
+
class="reorder-list-handle"
|
|
84
|
+
:class="{ 'is-disabled': disabled || isItemReorderDisabled(item, index) }"
|
|
85
|
+
:draggable="disabled || isItemReorderDisabled(item, index) ? 'false' : 'true'"
|
|
86
|
+
:aria-hidden="showMoveButtons ? 'true' : undefined"
|
|
87
|
+
:tabindex="handleTabIndex"
|
|
88
|
+
:aria-label="showMoveButtons ? undefined : `Reorder ${labelOf(item, index)}. Arrow up or arrow down to move.`"
|
|
89
|
+
@dragstart.stop="onHandleDragStart(item, index, $event)"
|
|
90
|
+
@dragend="onDragEnd"
|
|
91
|
+
@keydown="onHandleKeydown(index, $event)"
|
|
92
|
+
>
|
|
93
|
+
<HugeiconsIcon
|
|
94
|
+
:icon="DragDropVerticalIcon"
|
|
95
|
+
width="0.95em"
|
|
96
|
+
height="0.95em"
|
|
97
|
+
:strokeWidth="2"
|
|
98
|
+
/>
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div class="reorder-list-body">
|
|
103
|
+
<slot name="item" :item="item" :index="index">
|
|
104
|
+
{{ labelOf(item, index) }}
|
|
105
|
+
</slot>
|
|
106
|
+
</div>
|
|
107
|
+
</li>
|
|
108
|
+
</ul>
|
|
109
|
+
</template>
|
|
110
|
+
|
|
111
|
+
<script setup>
|
|
112
|
+
/**
|
|
113
|
+
* Reordering uses native HTML5 drag-and-drop (`draggable`, dragstart/dragover/drop).
|
|
114
|
+
* Drop position is derived from pointer Y during dragover; handle and whole-row modes
|
|
115
|
+
* both use the same DnD pipeline (not a custom pointer-drag implementation).
|
|
116
|
+
*/
|
|
117
|
+
import { computed, nextTick, ref, useId, watch } from 'vue'
|
|
118
|
+
import { HugeiconsIcon } from '@hugeicons/vue'
|
|
119
|
+
import {
|
|
120
|
+
ArrowDown01Icon,
|
|
121
|
+
ArrowUp01Icon,
|
|
122
|
+
DragDropVerticalIcon,
|
|
123
|
+
} from '@hugeicons/core-free-icons'
|
|
124
|
+
|
|
125
|
+
const props = defineProps({
|
|
126
|
+
/**
|
|
127
|
+
* Ordered rows. Objects or primitive values. Reorder emits a new array.
|
|
128
|
+
*/
|
|
129
|
+
modelValue: {
|
|
130
|
+
type: Array,
|
|
131
|
+
default: () => [],
|
|
132
|
+
},
|
|
133
|
+
/**
|
|
134
|
+
* Property name used as the row identity, or a function (item, index) => key.
|
|
135
|
+
* Primitives use their own value when the property is missing.
|
|
136
|
+
*/
|
|
137
|
+
itemKey: {
|
|
138
|
+
type: [String, Function],
|
|
139
|
+
default: 'id',
|
|
140
|
+
},
|
|
141
|
+
/**
|
|
142
|
+
* Accessible name for move buttons. Defaults to label, name, title, or "row N".
|
|
143
|
+
*/
|
|
144
|
+
itemLabel: {
|
|
145
|
+
type: Function,
|
|
146
|
+
default: null,
|
|
147
|
+
},
|
|
148
|
+
/**
|
|
149
|
+
* Show up/down buttons. Off by default; drag handles are the primary control.
|
|
150
|
+
* When hidden, arrow keys on the handle reorder rows.
|
|
151
|
+
*/
|
|
152
|
+
showMoveButtons: {
|
|
153
|
+
type: Boolean,
|
|
154
|
+
default: false,
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Show the grip used to drag a row. Arrow keys on the handle reorder when move buttons are hidden.
|
|
158
|
+
*/
|
|
159
|
+
showDragHandles: {
|
|
160
|
+
type: Boolean,
|
|
161
|
+
default: true,
|
|
162
|
+
},
|
|
163
|
+
/**
|
|
164
|
+
* HTML5 drag from anywhere on the row (except buttons and form controls in the slot).
|
|
165
|
+
* Hides the grip handle; move buttons still apply when enabled.
|
|
166
|
+
*/
|
|
167
|
+
dragWholeRow: {
|
|
168
|
+
type: Boolean,
|
|
169
|
+
default: false,
|
|
170
|
+
},
|
|
171
|
+
disabled: {
|
|
172
|
+
type: Boolean,
|
|
173
|
+
default: false,
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* When true for a row, that row cannot be dragged and keeps its handle disabled.
|
|
177
|
+
*/
|
|
178
|
+
isItemReorderDisabled: {
|
|
179
|
+
type: Function,
|
|
180
|
+
default: null,
|
|
181
|
+
},
|
|
182
|
+
ariaLabel: {
|
|
183
|
+
type: String,
|
|
184
|
+
default: '',
|
|
185
|
+
},
|
|
186
|
+
ariaLabelledby: {
|
|
187
|
+
type: String,
|
|
188
|
+
default: '',
|
|
189
|
+
},
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const emit = defineEmits(['update:modelValue', 'reorder'])
|
|
193
|
+
|
|
194
|
+
const rootRef = ref(null)
|
|
195
|
+
const listInstanceId = useId()
|
|
196
|
+
const draggingKey = ref(null)
|
|
197
|
+
/** Visual gap index: 0 is before the first row, length is after the last. */
|
|
198
|
+
const dropGap = ref(null)
|
|
199
|
+
const separatorTop = ref(0)
|
|
200
|
+
|
|
201
|
+
const items = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []))
|
|
202
|
+
|
|
203
|
+
const showHandleGrip = computed(() => props.showDragHandles && !props.dragWholeRow)
|
|
204
|
+
|
|
205
|
+
const handleTabIndex = computed(() => {
|
|
206
|
+
if (!showHandleGrip.value || props.showMoveButtons || props.disabled) {
|
|
207
|
+
return undefined
|
|
208
|
+
}
|
|
209
|
+
return 0
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const rowTabIndex = computed(() => {
|
|
213
|
+
if (!props.dragWholeRow || props.showMoveButtons || props.disabled) {
|
|
214
|
+
return undefined
|
|
215
|
+
}
|
|
216
|
+
return 0
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
function rowDragAriaLabel(item, index) {
|
|
220
|
+
if (!props.dragWholeRow || props.showMoveButtons) {
|
|
221
|
+
return undefined
|
|
222
|
+
}
|
|
223
|
+
return `Reorder ${labelOf(item, index)}. Arrow up or arrow down to move.`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function keyOf(item, index) {
|
|
227
|
+
if (typeof props.itemKey === 'function') {
|
|
228
|
+
return String(props.itemKey(item, index))
|
|
229
|
+
}
|
|
230
|
+
if (item != null && typeof item === 'object' && props.itemKey in item && item[props.itemKey] != null) {
|
|
231
|
+
return String(item[props.itemKey])
|
|
232
|
+
}
|
|
233
|
+
return String(item)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function labelOf(item, index) {
|
|
237
|
+
if (typeof props.itemLabel === 'function') {
|
|
238
|
+
const custom = props.itemLabel(item, index)
|
|
239
|
+
if (custom != null && custom !== '') {
|
|
240
|
+
return String(custom)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (item != null && typeof item === 'object') {
|
|
244
|
+
const text = item.label ?? item.name ?? item.title
|
|
245
|
+
if (text != null && text !== '') {
|
|
246
|
+
return String(text)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (item != null && typeof item !== 'object') {
|
|
250
|
+
return String(item)
|
|
251
|
+
}
|
|
252
|
+
return `row ${index + 1}`
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function indexOfKey(key) {
|
|
256
|
+
return items.value.findIndex((item, index) => keyOf(item, index) === key)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function isItemReorderDisabled(item, index) {
|
|
260
|
+
return typeof props.isItemReorderDisabled === 'function'
|
|
261
|
+
&& props.isItemReorderDisabled(item, index)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function canMove(index, direction) {
|
|
265
|
+
if (props.disabled || isItemReorderDisabled(items.value[index], index)) {
|
|
266
|
+
return false
|
|
267
|
+
}
|
|
268
|
+
const target = index + direction
|
|
269
|
+
return target >= 0 && target < items.value.length
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function reorder(fromIndex, toIndex) {
|
|
273
|
+
if (props.disabled) {
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (fromIndex === toIndex) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
if (fromIndex < 0 || toIndex < 0 || fromIndex >= items.value.length || toIndex >= items.value.length) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const next = items.value.slice()
|
|
284
|
+
const [moved] = next.splice(fromIndex, 1)
|
|
285
|
+
next.splice(toIndex, 0, moved)
|
|
286
|
+
emit('update:modelValue', next)
|
|
287
|
+
emit('reorder', {
|
|
288
|
+
items: next,
|
|
289
|
+
item: moved,
|
|
290
|
+
fromIndex,
|
|
291
|
+
toIndex,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function moveBy(index, direction, restoreFocus = false) {
|
|
296
|
+
if (!canMove(index, direction)) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
const key = keyOf(items.value[index], index)
|
|
300
|
+
reorder(index, index + direction)
|
|
301
|
+
if (!restoreFocus) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
await nextTick()
|
|
305
|
+
const selectorKey = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
|
|
306
|
+
? CSS.escape(key)
|
|
307
|
+
: key.replace(/"/g, '\\"')
|
|
308
|
+
const row = rootRef.value?.querySelector(`[data-reorder-key="${selectorKey}"]`)
|
|
309
|
+
const preferredLabel = direction < 0 ? ' up' : ' down'
|
|
310
|
+
const alternateLabel = direction < 0 ? ' down' : ' up'
|
|
311
|
+
const buttons = [...(row?.querySelectorAll('button') ?? [])]
|
|
312
|
+
const preferred = buttons.find((button) => button.getAttribute('aria-label')?.endsWith(preferredLabel) && !button.disabled)
|
|
313
|
+
const alternate = buttons.find((button) => button.getAttribute('aria-label')?.endsWith(alternateLabel) && !button.disabled)
|
|
314
|
+
const handle = row?.querySelector('.reorder-list-handle')
|
|
315
|
+
;(preferred ?? alternate ?? handle)?.focus()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function onHandleKeydown(index, event) {
|
|
319
|
+
if (!showHandleGrip.value || props.showMoveButtons || props.disabled) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
onReorderKeydown(index, event)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function onRowKeydown(index, event) {
|
|
326
|
+
if (!props.dragWholeRow || props.showMoveButtons || props.disabled) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
onReorderKeydown(index, event)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function onReorderKeydown(index, event) {
|
|
333
|
+
if (event.key === 'ArrowUp') {
|
|
334
|
+
event.preventDefault()
|
|
335
|
+
moveBy(index, -1, true)
|
|
336
|
+
} else if (event.key === 'ArrowDown') {
|
|
337
|
+
event.preventDefault()
|
|
338
|
+
moveBy(index, 1, true)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const DRAG_CANCEL_SELECTOR = 'button, a, input, select, textarea, label'
|
|
343
|
+
|
|
344
|
+
function dragStartBlocked(event) {
|
|
345
|
+
const target = event.target
|
|
346
|
+
if (!(target instanceof Element)) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
return Boolean(target.closest(DRAG_CANCEL_SELECTOR))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function beginDrag(item, index, event) {
|
|
353
|
+
if (props.disabled || isItemReorderDisabled(item, index)) {
|
|
354
|
+
event.preventDefault()
|
|
355
|
+
return
|
|
356
|
+
}
|
|
357
|
+
draggingKey.value = keyOf(item, index)
|
|
358
|
+
dropGap.value = null
|
|
359
|
+
if (event.dataTransfer) {
|
|
360
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
361
|
+
event.dataTransfer.setData('text/plain', draggingKey.value)
|
|
362
|
+
event.dataTransfer.setData('application/x-picocrank-reorder-list', listInstanceId)
|
|
363
|
+
}
|
|
364
|
+
event.stopPropagation()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function onRowDragStart(item, index, event) {
|
|
368
|
+
if (!props.dragWholeRow) {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
if (dragStartBlocked(event)) {
|
|
372
|
+
event.preventDefault()
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
beginDrag(item, index, event)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function onHandleDragStart(item, index, event) {
|
|
379
|
+
if (!showHandleGrip.value) {
|
|
380
|
+
event.preventDefault()
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
beginDrag(item, index, event)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function clearDrag() {
|
|
387
|
+
draggingKey.value = null
|
|
388
|
+
dropGap.value = null
|
|
389
|
+
separatorTop.value = 0
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function directRows(list) {
|
|
393
|
+
return [...list.querySelectorAll(':scope > li.reorder-list-row')]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function separatorTopForGap(gap, rows, list) {
|
|
397
|
+
const listRect = list.getBoundingClientRect()
|
|
398
|
+
let y
|
|
399
|
+
if (gap <= 0) {
|
|
400
|
+
y = rows[0].getBoundingClientRect().top
|
|
401
|
+
} else if (gap >= rows.length) {
|
|
402
|
+
y = rows[rows.length - 1].getBoundingClientRect().bottom
|
|
403
|
+
} else {
|
|
404
|
+
const previous = rows[gap - 1].getBoundingClientRect()
|
|
405
|
+
const next = rows[gap].getBoundingClientRect()
|
|
406
|
+
y = (previous.bottom + next.top) / 2
|
|
407
|
+
}
|
|
408
|
+
return y - listRect.top + list.scrollTop
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Gap in the current list where a drop would insert the dragged row.
|
|
413
|
+
* The slots immediately before and after the dragged row are null: dropping
|
|
414
|
+
* there would not move it.
|
|
415
|
+
*/
|
|
416
|
+
function dropGapFromDragEvent(event) {
|
|
417
|
+
const list = rootRef.value
|
|
418
|
+
if (!list || draggingKey.value == null) {
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
const rows = directRows(list)
|
|
422
|
+
const fromIndex = indexOfKey(draggingKey.value)
|
|
423
|
+
if (fromIndex < 0 || rows.length === 0) {
|
|
424
|
+
return null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let gap = rows.length
|
|
428
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
429
|
+
const rect = rows[index].getBoundingClientRect()
|
|
430
|
+
const midpoint = rect.top + (rect.height / 2)
|
|
431
|
+
if (event.clientY < midpoint) {
|
|
432
|
+
gap = index
|
|
433
|
+
break
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (gap === fromIndex || gap === fromIndex + 1) {
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
gap,
|
|
441
|
+
fromIndex,
|
|
442
|
+
top: separatorTopForGap(gap, rows, list),
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function onListDragOver(event) {
|
|
447
|
+
if (props.disabled || draggingKey.value == null) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
event.preventDefault()
|
|
451
|
+
event.stopPropagation()
|
|
452
|
+
const next = dropGapFromDragEvent(event)
|
|
453
|
+
dropGap.value = next ? next.gap : null
|
|
454
|
+
separatorTop.value = next ? next.top : 0
|
|
455
|
+
if (event.dataTransfer) {
|
|
456
|
+
event.dataTransfer.dropEffect = 'move'
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function onListDragLeave(event) {
|
|
461
|
+
const list = rootRef.value
|
|
462
|
+
const next = event.relatedTarget
|
|
463
|
+
if (list && next instanceof Node && list.contains(next)) {
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
dropGap.value = null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function onListDrop(event) {
|
|
470
|
+
if (props.disabled || draggingKey.value == null) {
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
event.preventDefault()
|
|
474
|
+
event.stopPropagation()
|
|
475
|
+
const next = dropGapFromDragEvent(event)
|
|
476
|
+
const sourceKey = draggingKey.value
|
|
477
|
+
clearDrag()
|
|
478
|
+
if (!sourceKey || !next) {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
const toIndex = next.gap > next.fromIndex ? next.gap - 1 : next.gap
|
|
482
|
+
reorder(next.fromIndex, toIndex)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function onDragEnd() {
|
|
486
|
+
clearDrag()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
watch(() => [props.showDragHandles, props.dragWholeRow], () => {
|
|
490
|
+
if (!props.showDragHandles && !props.dragWholeRow) {
|
|
491
|
+
clearDrag()
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
</script>
|
|
495
|
+
|
|
496
|
+
<style scoped>
|
|
497
|
+
.reorder-list {
|
|
498
|
+
position: relative;
|
|
499
|
+
list-style: none;
|
|
500
|
+
margin: 0;
|
|
501
|
+
padding: 0;
|
|
502
|
+
display: flex;
|
|
503
|
+
flex-direction: column;
|
|
504
|
+
gap: 0.5rem;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.reorder-list-empty {
|
|
508
|
+
margin: 0;
|
|
509
|
+
color: var(--muted-text-color);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.reorder-list-row {
|
|
513
|
+
display: flex;
|
|
514
|
+
align-items: center;
|
|
515
|
+
gap: 0.75rem;
|
|
516
|
+
min-width: 0;
|
|
517
|
+
padding: 0.55rem 0.75rem;
|
|
518
|
+
border: 1px solid var(--border-color);
|
|
519
|
+
border-radius: 0.35rem;
|
|
520
|
+
background: var(--standout-bg-color);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.reorder-list-row.is-dragging {
|
|
524
|
+
opacity: 0.55;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.reorder-list-row.is-drag-whole-row {
|
|
528
|
+
cursor: grab;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.reorder-list-row.is-drag-whole-row:hover:not(.is-dragging) {
|
|
532
|
+
background-color: var(--control-hover-bg-color);
|
|
533
|
+
color: var(--hover-text-color);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.reorder-list-row.is-drag-whole-row:active {
|
|
537
|
+
cursor: grabbing;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.reorder-list-row.is-reorder-disabled {
|
|
541
|
+
opacity: 0.85;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.reorder-list-row.is-drag-whole-row:focus-visible {
|
|
545
|
+
outline: 2px solid var(--focus-outline-color, var(--text-color));
|
|
546
|
+
outline-offset: 2px;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.reorder-list-separator {
|
|
550
|
+
position: absolute;
|
|
551
|
+
z-index: 1;
|
|
552
|
+
left: 0;
|
|
553
|
+
right: 0;
|
|
554
|
+
height: 3px;
|
|
555
|
+
margin: 0;
|
|
556
|
+
padding: 0;
|
|
557
|
+
border: 0;
|
|
558
|
+
border-radius: 999px;
|
|
559
|
+
background: var(--text-color);
|
|
560
|
+
pointer-events: none;
|
|
561
|
+
transform: translateY(-50%);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.reorder-list-controls {
|
|
565
|
+
display: inline-flex;
|
|
566
|
+
align-items: center;
|
|
567
|
+
gap: 0.35rem;
|
|
568
|
+
flex: 0 0 auto;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.reorder-list-move {
|
|
572
|
+
display: inline-flex;
|
|
573
|
+
gap: 0.2rem;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.reorder-list-move-button {
|
|
577
|
+
display: inline-flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
justify-content: center;
|
|
580
|
+
min-width: 1.75rem;
|
|
581
|
+
padding: 0.2rem 0.35rem;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.reorder-list-handle {
|
|
585
|
+
display: inline-flex;
|
|
586
|
+
align-items: center;
|
|
587
|
+
justify-content: center;
|
|
588
|
+
padding: 0.15rem;
|
|
589
|
+
color: inherit;
|
|
590
|
+
opacity: 0.65;
|
|
591
|
+
cursor: grab;
|
|
592
|
+
border-radius: 0.25rem;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.reorder-list-handle:active {
|
|
596
|
+
cursor: grabbing;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
.reorder-list-handle.is-disabled,
|
|
600
|
+
.reorder-list.is-disabled .reorder-list-handle {
|
|
601
|
+
opacity: 0.35;
|
|
602
|
+
cursor: default;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.reorder-list-handle:focus-visible {
|
|
606
|
+
outline: 2px solid var(--focus-outline-color, var(--text-color));
|
|
607
|
+
outline-offset: 2px;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.reorder-list-body {
|
|
611
|
+
flex: 1 1 auto;
|
|
612
|
+
min-width: 0;
|
|
613
|
+
}
|
|
614
|
+
</style>
|