file-tree-draggable.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import React, { useRef, useEffect } from 'react'
  2. import PropTypes from 'prop-types'
  3. import { useTranslation } from 'react-i18next'
  4. import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
  5. import { HTML5Backend, getEmptyImage } from 'react-dnd-html5-backend'
  6. import {
  7. findAllInTreeOrThrow,
  8. findAllFolderIdsInFolders
  9. } from '../util/find-in-tree'
  10. import { useFileTreeActionable } from './file-tree-actionable'
  11. import { useFileTreeMutable } from './file-tree-mutable'
  12. import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
  13. const DndContext = createDndContext(HTML5Backend)
  14. const DRAGGABLE_TYPE = 'ENTITY'
  15. export function FileTreeDraggableProvider({ children }) {
  16. const DndManager = useRef(DndContext)
  17. return (
  18. <DndProvider manager={DndManager.current.dragDropManager}>
  19. {children}
  20. </DndProvider>
  21. )
  22. }
  23. FileTreeDraggableProvider.propTypes = {
  24. children: PropTypes.oneOfType([
  25. PropTypes.arrayOf(PropTypes.node),
  26. PropTypes.node
  27. ]).isRequired
  28. }
  29. export function useDraggable(draggedEntityId) {
  30. const { t } = useTranslation()
  31. const { fileTreeData } = useFileTreeMutable()
  32. const { selectedEntityIds } = useFileTreeSelectable()
  33. const item = { type: DRAGGABLE_TYPE }
  34. const [{ isDragging }, dragRef, preview] = useDrag({
  35. item, // required, but overwritten by the return value of `begin`
  36. begin: () => {
  37. const draggedEntityIds = getDraggedEntityIds(
  38. selectedEntityIds,
  39. draggedEntityId
  40. )
  41. const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
  42. const title = getDraggedTitle(draggedItems, t)
  43. const forbiddenFolderIds = getForbiddenFolderIds(draggedItems)
  44. return { ...item, title, forbiddenFolderIds, draggedEntityIds }
  45. },
  46. collect: monitor => ({
  47. isDragging: !!monitor.isDragging()
  48. })
  49. })
  50. // remove the automatic preview as we're using a custom preview via
  51. // FileTreeDraggablePreviewLayer
  52. useEffect(
  53. () => {
  54. preview(getEmptyImage())
  55. },
  56. [preview]
  57. )
  58. return {
  59. dragRef,
  60. isDragging
  61. }
  62. }
  63. export function useDroppable(droppedEntityId) {
  64. const { finishMoving } = useFileTreeActionable()
  65. const [{ isOver }, dropRef] = useDrop({
  66. accept: DRAGGABLE_TYPE,
  67. canDrop: (item, monitor) => {
  68. const isOver = monitor.isOver({ shallow: true })
  69. if (!isOver) return false
  70. if (item.forbiddenFolderIds.has(droppedEntityId)) return false
  71. return true
  72. },
  73. drop: (item, monitor) => {
  74. const didDropInChild = monitor.didDrop()
  75. if (didDropInChild) return
  76. finishMoving(droppedEntityId, item.draggedEntityIds)
  77. },
  78. collect: monitor => ({
  79. isOver: monitor.canDrop()
  80. })
  81. })
  82. return {
  83. dropRef,
  84. isOver
  85. }
  86. }
  87. // Get the list of dragged entity ids. If the dragged entity is one of the
  88. // selected entities then all the selected entites are dragged entities,
  89. // otherwise it's the dragged entity only.
  90. function getDraggedEntityIds(selectedEntityIds, draggedEntityId) {
  91. if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
  92. // dragging the multi-selected entities
  93. return new Set(selectedEntityIds)
  94. } else {
  95. // not dragging the selection; only the current item
  96. return new Set([draggedEntityId])
  97. }
  98. }
  99. // Get the draggable title. This is the name of the dragged entities if there's
  100. // only one, otherwise it's the number of dragged entities.
  101. function getDraggedTitle(draggedItems, t) {
  102. if (draggedItems.size === 1) {
  103. const draggedItem = Array.from(draggedItems)[0]
  104. return draggedItem.entity.name
  105. }
  106. return t('n_items', { count: draggedItems.size })
  107. }
  108. // Get all children folder ids of any of the dragged items.
  109. function getForbiddenFolderIds(draggedItems) {
  110. const draggedFoldersArray = Array.from(draggedItems)
  111. .filter(draggedItem => {
  112. return draggedItem.type === 'folder'
  113. })
  114. .map(draggedItem => draggedItem.entity)
  115. const draggedFolders = new Set(draggedFoldersArray)
  116. return findAllFolderIdsInFolders(draggedFolders)
  117. }