| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- import { useRef, useEffect, useState } from 'react'
- import PropTypes from 'prop-types'
- import { useTranslation } from 'react-i18next'
- import { DndProvider, createDndContext, useDrag, useDrop } from 'react-dnd'
- import { HTML5Backend, getEmptyImage } from 'react-dnd-html5-backend'
- import {
- findAllInTreeOrThrow,
- findAllFolderIdsInFolders,
- } from '../util/find-in-tree'
- import { useFileTreeActionable } from './file-tree-actionable'
- import { useFileTreeMutable } from './file-tree-mutable'
- import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
- import { useFileTreeMainContext } from './file-tree-main'
- // HACK ALERT
- // DnD binds drag and drop events on window and stop propagation if the dragged
- // item is not a DnD element. This break other drag and drop interfaces; in
- // particular in rich text.
- // This is a hacky workaround to avoid calling the DnD listeners when the
- // draggable or droppable element is not within a `dnd-container` element.
- const ModifiedBackend = (...args) => {
- function isDndChild(elt) {
- if (elt.getAttribute && elt.getAttribute('dnd-container')) return true
- if (!elt.parentNode) return false
- return isDndChild(elt.parentNode)
- }
- const instance = new HTML5Backend(...args)
- const dragDropListeners = [
- 'handleTopDragStart',
- 'handleTopDragStartCapture',
- 'handleTopDragEndCapture',
- 'handleTopDragEnter',
- 'handleTopDragEnterCapture',
- 'handleTopDragLeaveCapture',
- 'handleTopDragOver',
- 'handleTopDragOverCapture',
- 'handleTopDrop',
- 'handleTopDropCapture',
- ]
- dragDropListeners.forEach(dragDropListener => {
- const originalListener = instance[dragDropListener]
- instance[dragDropListener] = (ev, ...extraArgs) => {
- if (isDndChild(ev.target)) originalListener(ev, ...extraArgs)
- }
- })
- return instance
- }
- const DndContext = createDndContext(ModifiedBackend)
- const DRAGGABLE_TYPE = 'ENTITY'
- export function FileTreeDraggableProvider({ children }) {
- const DndManager = useRef(DndContext)
- return (
- <DndProvider manager={DndManager.current.dragDropManager}>
- {children}
- </DndProvider>
- )
- }
- FileTreeDraggableProvider.propTypes = {
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.node),
- PropTypes.node,
- ]).isRequired,
- }
- export function useDraggable(draggedEntityId) {
- const { t } = useTranslation()
- const { hasWritePermissions } = useFileTreeMainContext()
- const { fileTreeData } = useFileTreeMutable()
- const { selectedEntityIds } = useFileTreeSelectable()
- const [isDraggable, setIsDraggable] = useState(hasWritePermissions)
- const item = { type: DRAGGABLE_TYPE }
- const [{ isDragging }, dragRef, preview] = useDrag({
- item, // required, but overwritten by the return value of `begin`
- begin: () => {
- const draggedEntityIds = getDraggedEntityIds(
- selectedEntityIds,
- draggedEntityId
- )
- const draggedItems = findAllInTreeOrThrow(fileTreeData, draggedEntityIds)
- const title = getDraggedTitle(draggedItems, t)
- const forbiddenFolderIds = getForbiddenFolderIds(draggedItems)
- return { ...item, title, forbiddenFolderIds, draggedEntityIds }
- },
- collect: monitor => ({
- isDragging: !!monitor.isDragging(),
- }),
- canDrag: () => isDraggable,
- })
- // remove the automatic preview as we're using a custom preview via
- // FileTreeDraggablePreviewLayer
- useEffect(() => {
- preview(getEmptyImage())
- }, [preview])
- return {
- dragRef,
- isDragging,
- setIsDraggable,
- }
- }
- export function useDroppable(droppedEntityId) {
- const { finishMoving } = useFileTreeActionable()
- const [{ isOver }, dropRef] = useDrop({
- accept: DRAGGABLE_TYPE,
- canDrop: (item, monitor) => {
- const isOver = monitor.isOver({ shallow: true })
- if (!isOver) return false
- if (item.forbiddenFolderIds.has(droppedEntityId)) return false
- return true
- },
- drop: (item, monitor) => {
- const didDropInChild = monitor.didDrop()
- if (didDropInChild) return
- finishMoving(droppedEntityId, item.draggedEntityIds)
- },
- collect: monitor => ({
- isOver: monitor.canDrop(),
- }),
- })
- return {
- dropRef,
- isOver,
- }
- }
- // Get the list of dragged entity ids. If the dragged entity is one of the
- // selected entities then all the selected entites are dragged entities,
- // otherwise it's the dragged entity only.
- function getDraggedEntityIds(selectedEntityIds, draggedEntityId) {
- if (selectedEntityIds.size > 1 && selectedEntityIds.has(draggedEntityId)) {
- // dragging the multi-selected entities
- return new Set(selectedEntityIds)
- } else {
- // not dragging the selection; only the current item
- return new Set([draggedEntityId])
- }
- }
- // Get the draggable title. This is the name of the dragged entities if there's
- // only one, otherwise it's the number of dragged entities.
- function getDraggedTitle(draggedItems, t) {
- if (draggedItems.size === 1) {
- const draggedItem = Array.from(draggedItems)[0]
- return draggedItem.entity.name
- }
- return t('n_items', { count: draggedItems.size })
- }
- // Get all children folder ids of any of the dragged items.
- function getForbiddenFolderIds(draggedItems) {
- const draggedFoldersArray = Array.from(draggedItems)
- .filter(draggedItem => {
- return draggedItem.type === 'folder'
- })
- .map(draggedItem => draggedItem.entity)
- const draggedFolders = new Set(draggedFoldersArray)
- return findAllFolderIdsInFolders(draggedFolders)
- }
|