feat: schema builder's create flow
This commit is contained in:
1 parent
f514146fed
commit
083cc9e23e
71 files changed
+7223
-61
No files matched your search
@@ -0,0 +1,125 @@
|
||||
import { computed, nextTick, ref, shallowRef } from 'vue'
|
||||
import { t } from '@/i18n'
|
||||
import { moveItem } from '@/lib/arrayMove'
|
||||
|
||||
interface KeyboardReorderOptions<T> {
|
||||
items: () => readonly T[]
|
||||
name: (item: T) => string
|
||||
commit: (next: T[]) => void
|
||||
root: () => HTMLElement | null
|
||||
}
|
||||
|
||||
export function useKeyboardReorder<T>(options: KeyboardReorderOptions<T>) {
|
||||
const working = shallowRef<T[] | null>(null)
|
||||
const grabbed = ref<number | null>(null)
|
||||
const origin = ref(0)
|
||||
const announcement = ref('')
|
||||
|
||||
const shown = computed<readonly T[]>(() => working.value ?? options.items())
|
||||
const active = computed(() => grabbed.value !== null)
|
||||
|
||||
function focusHandle(index: number) {
|
||||
nextTick(() =>
|
||||
options.root()?.querySelector<HTMLElement>(`[data-handle="${index}"]`)?.focus(),
|
||||
)
|
||||
}
|
||||
|
||||
function movingText(item: T, position: number, total: number) {
|
||||
return t(
|
||||
'Moving {0}. Position {1} of {2}. Press Up or Down to move, Space to drop, Esc to cancel.',
|
||||
[options.name(item), position, total],
|
||||
)
|
||||
}
|
||||
|
||||
function grab(index: number) {
|
||||
const items = [...options.items()]
|
||||
const item = items[index]
|
||||
if (item === undefined) {
|
||||
return
|
||||
}
|
||||
working.value = items
|
||||
grabbed.value = index
|
||||
origin.value = index
|
||||
announcement.value = movingText(item, index + 1, items.length)
|
||||
}
|
||||
|
||||
function move(delta: number) {
|
||||
const current = grabbed.value
|
||||
const items = working.value
|
||||
if (current === null || !items) {
|
||||
return
|
||||
}
|
||||
const target = current + delta
|
||||
if (target < 0 || target >= items.length) {
|
||||
return
|
||||
}
|
||||
working.value = moveItem(items, current, target)
|
||||
grabbed.value = target
|
||||
announcement.value = movingText(items[current]!, target + 1, items.length)
|
||||
focusHandle(target)
|
||||
}
|
||||
|
||||
function release() {
|
||||
working.value = null
|
||||
grabbed.value = null
|
||||
}
|
||||
|
||||
function drop() {
|
||||
const current = grabbed.value
|
||||
const items = working.value
|
||||
if (current === null || !items) {
|
||||
return
|
||||
}
|
||||
announcement.value = t('{0} dropped. Position {1} of {2}.', [
|
||||
options.name(items[current]!),
|
||||
current + 1,
|
||||
items.length,
|
||||
])
|
||||
release()
|
||||
if (current !== origin.value) {
|
||||
options.commit(items)
|
||||
}
|
||||
focusHandle(current)
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
const current = grabbed.value
|
||||
const items = working.value
|
||||
if (current === null || !items) {
|
||||
return
|
||||
}
|
||||
const index = origin.value
|
||||
announcement.value = t('Reorder cancelled. {0} is back at position {1} of {2}.', [
|
||||
options.name(items[current]!),
|
||||
index + 1,
|
||||
items.length,
|
||||
])
|
||||
release()
|
||||
focusHandle(index)
|
||||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent, index: number) {
|
||||
if (grabbed.value === null) {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
grab(index)
|
||||
}
|
||||
return
|
||||
}
|
||||
const actions: Record<string, () => void> = {
|
||||
ArrowUp: () => move(-1),
|
||||
ArrowDown: () => move(1),
|
||||
' ': drop,
|
||||
Enter: drop,
|
||||
Escape: cancel,
|
||||
}
|
||||
const action = actions[event.key]
|
||||
if (action) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
action()
|
||||
}
|
||||
}
|
||||
|
||||
return { shown, grabbed, active, announcement, onKeydown }
|
||||
}
|
||||
Reference in new issue
Block a user