internals
Drag and Drop helpers
Общие помощники Pragmatic Drag and Drop (клавиатурный drag, reorder, прозрачный пиксель) — см. ADR-002.
Загрузка превью…
src/lib/dnd/keyboard-drag.ts
/**
* Общая клавиатурная машина состояний для reui dnd-компонентов
* (Sortable, Kanban) — рукописная замена dnd-kit `KeyboardSensor` +
* `sortableKeyboardCoordinates` (см. docs/adr/002-drag-and-drop.md и
* комментарий в шапке Sortable.vue).
*
* Состояния: "не взят" -> Space/Enter -> "взят" -> стрелки двигают на
* шаг (коммитится немедленно, а не при отпускании) -> Space/Enter
* подтверждает, Escape отменяет (восстановлением снимка, снятого в
* момент захвата — снимок и его восстановление остаются на стороне
* вызывающего компонента, здесь только переходы состояний).
*
* Компонент-обёртка (SortableItem.vue / KanbanItemHandle.vue /
* KanbanColumnHandle.vue) отвечает за:
* - решение, можно ли вообще взять элемент (disabled/isOverlay) —
* до вызова возвращённого обработчика;
* - собственно `grabbed`-флаг и снимок для отмены;
* - что означает "подвинуть на шаг" для конкретного контейнера
* (соседний элемент в том же списке, соседняя колонка и т.д.).
*/
export interface KeyboardDragCallbacks {
/** Взят ли сейчас элемент (управляется вызывающим компонентом). */
isGrabbed: () => boolean
onGrab: () => void
onMove: (direction: -1 | 1) => void
onDrop: () => void
onCancel: () => void
}
export function createKeyboardDragHandler(
callbacks: KeyboardDragCallbacks
): (event: KeyboardEvent) => void {
return function handleKeydown(event: KeyboardEvent): void {
if (!callbacks.isGrabbed()) {
if (event.key === " " || event.key === "Enter") {
event.preventDefault()
callbacks.onGrab()
}
return
}
switch (event.key) {
case "ArrowUp":
case "ArrowLeft":
event.preventDefault()
callbacks.onMove(-1)
break
case "ArrowDown":
case "ArrowRight":
event.preventDefault()
callbacks.onMove(1)
break
case " ":
case "Enter":
event.preventDefault()
callbacks.onDrop()
break
case "Escape":
event.preventDefault()
callbacks.onCancel()
break
}
}
}
src/lib/dnd/reorder.ts
/**
* Общая математика переупорядочивания для reui dnd-компонентов
* (Sortable, Kanban), см. docs/adr/002-drag-and-drop.md.
*
* Та же семантика, что `arrayMove` из `@dnd-kit/sortable` /
* `@atlaskit/pragmatic-drag-and-drop/reorder`: элемент вынимается с
* позиции `from` и вставляется на позицию `to` в получившемся (уже
* укороченном на один элемент) массиве.
*/
export function reorderArray<T>(list: T[], from: number, to: number): T[] {
const result = list.slice()
const [moved] = result.splice(from, 1)
result.splice(to, 0, moved as T)
return result
}
/**
* Перемещение элемента между двумя записями `Record<string, T[]>`
* (или в пределах одной и той же записи) — обобщение `reorderArray` на
* kanban-подобные многоконтейнерные структуры. `targetIndex` — индекс
* элемента, оказавшегося под курсором в ЦЕЛЕВОМ контейнере (индекс "over"
* ДО удаления перемещаемого элемента из исходного) — та же величина, что
* `overIndex` в `reorderArray`/dnd-kit `arrayMove`. Если контейнер тот же
* самый, удаление и вставка выполняются как два последовательных `splice`
* (как в `reorderArray`) — раздельный пересчёт индекса не нужен: `splice`
* сам корректно "сдвигает" массив между операциями.
*/
export function moveBetweenContainers<T>(
columns: Record<string, T[]>,
getItemValue: (item: T) => string,
fromContainer: string,
itemValue: string,
toContainer: string,
targetIndex: number
): Record<string, T[]> {
const source = columns[fromContainer]
if (!source) return columns
const activeIndex = source.findIndex((item) => getItemValue(item) === itemValue)
if (activeIndex === -1) return columns
const isSame = fromContainer === toContainer
const newSource = source.slice()
const [moved] = newSource.splice(activeIndex, 1)
if (isSame) {
newSource.splice(targetIndex, 0, moved as T)
return { ...columns, [fromContainer]: newSource }
}
const dest = (columns[toContainer] ?? []).slice()
dest.splice(targetIndex, 0, moved as T)
return { ...columns, [fromContainer]: newSource, [toContainer]: dest }
}
src/lib/dnd/transparent-pixel.ts
/**
* Общий прозрачный пиксель для гашения нативной картинки перетаскивания
* (`nativeSetDragImage`) у `@atlaskit/pragmatic-drag-and-drop` — см.
* комментарий в шапке Sortable.vue про замену `DragOverlay` на
* `position: fixed` Teleport-оверлей. Используется и Sortable, и Kanban.
*/
let cachedTransparentPixel: HTMLImageElement | undefined
export function transparentPixel(): HTMLImageElement {
if (!cachedTransparentPixel) {
const img = new Image()
img.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBTAA7"
cachedTransparentPixel = img
}
return cachedTransparentPixel
}
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/dnd.jsonИсточник: порт из ReUI (Keenthemes, MIT)