file-tree-draggable.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { useRef, useEffect, useState } 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. import { useFileTreeMainContext } from './file-tree-main'
  14. // HACK ALERT
  15. // DnD binds drag and drop events on window and stop propagation if the dragged
  16. // item is not a DnD element. This break other drag and drop interfaces; in
  17. // particular in rich text.
  18. // This is a hacky workaround to avoid calling the DnD listeners when the
  19. // draggable or droppable element is not within a `dnd-container` element.
  20. const ModifiedBackend = (...args) => {
  21. function isDndChild(elt) {
  22. if (elt.getAttribute && elt.getAttribute('dnd-container')) return true
  23. if (!elt.parentNode) return false
  24. return isDndChild(elt.parentNode)
  25. }
  26. const instance = new HTML5Backend(...args)
  27. const dragDropListeners = [
  28. 'handleTopDragStart',
  29. 'handleTopDragStartCapture',
  30. 'handleTopDragEndCapture',
  31. 'handleTopDragEnter',
  32. 'handleTopDragEnterCapture',
  33. 'handleTopDragLeaveCapture',
  34. 'handleTopDragOver',
  35. 'handleTopDragOverCapture',
  36. 'handleTopDrop',
  37. 'handleTopDropCapture',
  38. ]
  39. dragDropListeners.forEach(dragDropListener => {
  40. const originalListener = instance[dragDropListener]
  41. instance[dragDropListener] = (ev, ...extraArgs) => {
  42. if (isDndChild(ev.target)) originalListener(ev, ...extraArgs)
  43. }
  44. })
  45. return instance
  46. }
  47. const DndContext = createDndContext(ModifiedBackend)
  48. const DRAGGABLE_TYPE = 'ENTITY'
  49. export function FileTreeDraggableProvider({ children }) {
  50. const DndManager = useRef(DndContext)
  51. return (
  52. <DndProvider manager={DndManager.current.dragDropManager}>
  53. {children}
  54. </DndProvider>
  55. )
  56. }
  57. FileTreeDraggableProvider.propTypes = {
  58. children: PropTypes.oneOfType([
  59. PropTypes.arrayOf(PropTypes.node),
  60. PropTypes.node,
  61. ]).isRequired,
  62. }
  63. export function useDraggable(draggedEntityId) {
  64. const { t } = useTranslation()
  65. const { hasWritePermissions } = useFileTreeMainContext()
  66. const { fileTreeData } = useFileTreeMutable()
  67. const { selectedEntityIds } = useFileTreeSelectable()
  68. const [isDraggable, setIsDraggable] = useState(hasWritePermissions)
  69. const item = { type: DRAGGABLE_TYPE }
  70. const [{ isDragging }, dragRef, preview] = useDrag({
  71. item, // required, but overwritten by the return value of `begin`
  72. begin: () => {
  73. const draggedEntityIds = getDraggedEntityIds(
  74. selectedEntityIds,
  75. draggedEntityId
  76. )
  77. const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
  78. const title = getDraggedTitle(draggedItems, t)
  79. const forbiddenFolderIds = getForbiddenFolderIds(draggedItems)
  80. return { ...item, title, forbiddenFolderIds, draggedEntityIds }
  81. },
  82. collect: monitor => ({
  83. isDragging: !!monitor.isDragging(),
  84. }),
  85. canDrag: () => isDraggable,
  86. })
  87. // remove the automatic preview as we're using a custom preview via
  88. // FileTreeDraggablePreviewLayer
  89. useEffect(() => {
  90. preview(getEmptyImage())
  91. }, [preview])
  92. return {
  93. dragRef,
  94. isDragging,
  95. setIsDraggable,
  96. }
  97. }
  98. export function useDroppable(droppedEntityId) {
  99. const { finishMoving } = useFileTreeActionable()
  100. const [{ isOver }, dropRef] = useDrop({
  101. accept: DRAGGABLE_TYPE,
  102. canDrop: (item, monitor) => {
  103. const isOver = monitor.isOver({ shallow: true })
  104. if (!isOver) return false
  105. if (item.forbiddenFolderIds.has(droppedEntityId)) return false
  106. return true
  107. },
  108. drop: (item, monitor) => {
  109. const didDropInChild = monitor.didDrop()
  110. if (didDropInChild) return
  111. finishMoving(droppedEntityId, item.draggedEntityIds)
  112. },
  113. collect: monitor => ({
  114. isOver: monitor.canDrop(),
  115. }),
  116. })
  117. return {
  118. dropRef,
  119. isOver,
  120. }
  121. }
  122. // Get the list of dragged entity ids. If the dragged entity is one of the
  123. // selected entities then all the selected entites are dragged entities,
  124. // otherwise it's the dragged entity only.
  125. function getDraggedEntityIds(selectedEntityIds, draggedEntityId) {
  126. if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
  127. // dragging the multi-selected entities
  128. return new Set(selectedEntityIds)
  129. } else {
  130. // not dragging the selection; only the current item
  131. return new Set([draggedEntityId])
  132. }
  133. }
  134. // Get the draggable title. This is the name of the dragged entities if there's
  135. // only one, otherwise it's the number of dragged entities.
  136. function getDraggedTitle(draggedItems, t) {
  137. if (draggedItems.size === 1) {
  138. const draggedItem = Array.from(draggedItems)[0]
  139. return draggedItem.entity.name
  140. }
  141. return t('n_items', { count: draggedItems.size })
  142. }
  143. // Get all children folder ids of any of the dragged items.
  144. function getForbiddenFolderIds(draggedItems) {
  145. const draggedFoldersArray = Array.from(draggedItems)
  146. .filter(draggedItem => {
  147. return draggedItem.type === 'folder'
  148. })
  149. .map(draggedItem => draggedItem.entity)
  150. const draggedFolders = new Set(draggedFoldersArray)
  151. return findAllFolderIdsInFolders(draggedFolders)
  152. }