file-tree-draggable.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import { useRef, useEffect, useState, FC } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import getDroppedFiles from '@uppy/utils/lib/getDroppedFiles'
  4. import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
  5. import {
  6. HTML5Backend,
  7. getEmptyImage,
  8. NativeTypes,
  9. } from 'react-dnd-html5-backend'
  10. import {
  11. findAllInTreeOrThrow,
  12. findAllFolderIdsInFolders,
  13. } from '../util/find-in-tree'
  14. import { useFileTreeActionable } from './file-tree-actionable'
  15. import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
  16. import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
  17. import { useEditorContext } from '../../../shared/context/editor-context'
  18. // HACK ALERT
  19. // DnD binds drag and drop events on window and stop propagation if the dragged
  20. // item is not a DnD element. This break other drag and drop interfaces; in
  21. // particular in rich text.
  22. // This is a hacky workaround to avoid calling the DnD listeners when the
  23. // draggable or droppable element is not within a `dnd-container` element.
  24. const ModifiedBackend = (...args: any[]) => {
  25. function isDndChild(elt: Element): boolean {
  26. if (elt.getAttribute && elt.getAttribute('dnd-container')) return true
  27. if (!elt.parentNode) return false
  28. return isDndChild(elt.parentNode as Element)
  29. }
  30. // @ts-ignore
  31. const instance = new HTML5Backend(...args)
  32. const dragDropListeners = [
  33. 'handleTopDragStart',
  34. 'handleTopDragStartCapture',
  35. 'handleTopDragEndCapture',
  36. 'handleTopDragEnter',
  37. 'handleTopDragEnterCapture',
  38. 'handleTopDragLeaveCapture',
  39. 'handleTopDragOver',
  40. 'handleTopDragOverCapture',
  41. 'handleTopDrop',
  42. 'handleTopDropCapture',
  43. ]
  44. dragDropListeners.forEach(dragDropListener => {
  45. const originalListener = instance[dragDropListener]
  46. instance[dragDropListener] = (ev: Event, ...extraArgs: any[]) => {
  47. if (isDndChild(ev.target as Element)) originalListener(ev, ...extraArgs)
  48. }
  49. })
  50. return instance
  51. }
  52. const DndContext = createDndContext(ModifiedBackend)
  53. const DRAGGABLE_TYPE = 'ENTITY'
  54. export const FileTreeDraggableProvider: FC = ({ children }) => {
  55. const DndManager = useRef(DndContext)
  56. return (
  57. <DndProvider manager={DndManager.current.dragDropManager!}>
  58. {children}
  59. </DndProvider>
  60. )
  61. }
  62. export function useDraggable(draggedEntityId: string) {
  63. const { t } = useTranslation()
  64. const { permissionsLevel } = useEditorContext()
  65. const { fileTreeData } = useFileTreeData()
  66. const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
  67. const [isDraggable, setIsDraggable] = useState(true)
  68. const item = { type: DRAGGABLE_TYPE }
  69. const [{ isDragging, draggedEntityIds }, dragRef, preview] = useDrag({
  70. item, // required, but overwritten by the return value of `begin`
  71. begin: () => {
  72. const draggedEntityIds = getDraggedEntityIds(
  73. isRootFolderSelected ? new Set() : selectedEntityIds,
  74. draggedEntityId
  75. )
  76. const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
  77. const title = getDraggedTitle(draggedItems, t)
  78. const forbiddenFolderIds = getForbiddenFolderIds(draggedItems)
  79. return { ...item, title, forbiddenFolderIds, draggedEntityIds }
  80. },
  81. collect: monitor => ({
  82. isDragging: !!monitor.isDragging(),
  83. draggedEntityIds: monitor.getItem()?.draggedEntityIds,
  84. }),
  85. canDrag: () => permissionsLevel !== 'readOnly' && isDraggable,
  86. end: () => item,
  87. })
  88. // remove the automatic preview as we're using a custom preview via
  89. // FileTreeDraggablePreviewLayer
  90. useEffect(() => {
  91. preview(getEmptyImage())
  92. }, [preview])
  93. return {
  94. dragRef,
  95. isDragging,
  96. setIsDraggable,
  97. draggedEntityIds,
  98. }
  99. }
  100. export function useDroppable(droppedEntityId: string) {
  101. const { finishMoving, setDroppedFiles, startUploadingDocOrFile } =
  102. useFileTreeActionable()
  103. const [{ isOver }, dropRef] = useDrop<any, any, any>({
  104. accept: [DRAGGABLE_TYPE, NativeTypes.FILE],
  105. canDrop: (item, monitor) => {
  106. const isOver = monitor.isOver({ shallow: true })
  107. if (!isOver) return false
  108. if (
  109. item.type === DRAGGABLE_TYPE &&
  110. item.forbiddenFolderIds.has(droppedEntityId)
  111. )
  112. return false
  113. return true
  114. },
  115. drop: (item, monitor) => {
  116. const didDropInChild = monitor.didDrop()
  117. if (didDropInChild) return
  118. if (item.type === DRAGGABLE_TYPE) {
  119. finishMoving(droppedEntityId, item.draggedEntityIds)
  120. } else {
  121. getDroppedFiles(item).then(files => {
  122. setDroppedFiles({ files, targetFolderId: droppedEntityId })
  123. startUploadingDocOrFile()
  124. })
  125. }
  126. },
  127. collect: monitor => ({
  128. isOver: monitor.canDrop(),
  129. }),
  130. })
  131. return {
  132. dropRef,
  133. isOver,
  134. }
  135. }
  136. // Get the list of dragged entity ids. If the dragged entity is one of the
  137. // selected entities then all the selected entites are dragged entities,
  138. // otherwise it's the dragged entity only.
  139. function getDraggedEntityIds(
  140. selectedEntityIds: Set<string>,
  141. draggedEntityId: string
  142. ) {
  143. if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
  144. // dragging the multi-selected entities
  145. return new Set(selectedEntityIds)
  146. } else {
  147. // not dragging the selection; only the current item
  148. return new Set([draggedEntityId])
  149. }
  150. }
  151. // Get the draggable title. This is the name of the dragged entities if there's
  152. // only one, otherwise it's the number of dragged entities.
  153. function getDraggedTitle(
  154. draggedItems: Set<any>,
  155. t: (key: string, options: Record<string, any>) => void
  156. ) {
  157. if (draggedItems.size === 1) {
  158. const draggedItem = Array.from(draggedItems)[0]
  159. return draggedItem.entity.name
  160. }
  161. return t('n_items', { count: draggedItems.size })
  162. }
  163. // Get all children folder ids of any of the dragged items.
  164. function getForbiddenFolderIds(draggedItems: Set<any>) {
  165. const draggedFoldersArray = Array.from(draggedItems)
  166. .filter(draggedItem => {
  167. return draggedItem.type === 'folder'
  168. })
  169. .map(draggedItem => draggedItem.entity)
  170. const draggedFolders = new Set(draggedFoldersArray)
  171. return findAllFolderIdsInFolders(draggedFolders)
  172. }