jsreport-studio
Version:
jsreport templates editor and designer
476 lines (402 loc) • 14.3 kB
JavaScript
import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
import { useSelector } from 'react-redux'
import isEqual from 'lodash/isEqual'
import useFilteredEntities from './useFilteredEntities'
import useCollapsed from './useCollapsed'
import useContextMenu from './useContextMenu'
import useDropHandler from './useDropHandler'
import useConstructor from '../../hooks/useConstructor'
import HierarchyReplaceEntityModal from '../Modals/HierarchyReplaceEntityModal'
import { selectors as entitiesSelectors } from '../../redux/entities'
import ENTITY_NODE_DRAG_TYPE from './nodeDragType'
import {
checkIsGroupNode,
checkIsGroupEntityNode,
getAllEntitiesInHierarchy,
getNodeDOMId,
getNodeTitleDOMId
} from './utils'
import { registerCollapseEntityHandler, modalHandler, entityTreeDropResolvers } from '../../lib/configuration.js'
const mainEntityDropResolver = {
type: ENTITY_NODE_DRAG_TYPE,
handler: () => {}
}
export default function useEntityTree (main, {
paddingByLevelInTree,
selectable,
selectionMode,
entities,
selected,
activeEntity,
getContextMenuItems,
openTab,
hierarchyMove,
onNewEntity,
onRemove,
onClone,
onRename,
onSelectionChanged,
listRef,
contextMenuRef
}) {
const entitiesFromState = useSelector((state) => state.entities)
const getEntityById = useCallback((id, ...params) => entitiesSelectors.getById({ entities: entitiesFromState }, id, ...params), [entitiesFromState])
const getEntityByShortid = useCallback((shortid, ...params) => entitiesSelectors.getByShortid({ entities: entitiesFromState }, shortid, ...params), [entitiesFromState])
const dragOverContextRef = useRef(null)
const [clipboard, setClipboard] = useState(null)
const [highlightedArea, setHighlightedArea] = useState(null)
const [currentEntities, setFilter] = useFilteredEntities(entities)
const { isNodeCollapsed, toogleNodeCollapse, collapseHandler } = useCollapsed({
listRef,
getEntityById,
getEntityByShortid
})
const { contextMenu, showContextMenu, clearContextMenu } = useContextMenu(contextMenuRef)
useEffect(() => {
if (main) {
registerCollapseEntityHandler(collapseHandler)
}
}, [main, collapseHandler])
const copyOrMoveEntity = useCallback((sourceInfo, targetInfo, shouldCopy = false) => {
hierarchyMove(sourceInfo, targetInfo, shouldCopy, false, true).then((result) => {
if (targetInfo.shortid != null) {
const targetEntity = getEntityByShortid(targetInfo.shortid)
toogleNodeCollapse(listRef.current.entityNodesById[targetEntity._id], false)
}
if (!result || result.duplicatedEntity !== true) {
return
}
modalHandler.open(HierarchyReplaceEntityModal, {
sourceId: sourceInfo.id,
targetShortId: targetInfo.shortid,
targetChildren: targetInfo.children,
existingEntity: result.existingEntity,
existingEntityEntitySet: result.existingEntityEntitySet,
shouldCopy
})
})
}, [hierarchyMove, getEntityByShortid, toogleNodeCollapse, listRef])
const isValidHierarchyTarget = useCallback((sourceNode, targetNode) => {
const containersInHierarchyForTarget = []
let containerSourceEntity
let containerTargetEntity
if (sourceNode.data.__entitySet === 'folders') {
containerSourceEntity = getEntityByShortid(sourceNode.data.shortid)
} else {
return true
}
if (targetNode.data.__entitySet === 'folders') {
containerTargetEntity = getEntityByShortid(targetNode.data.shortid)
} else {
if (targetNode.data.folder == null) {
return true
}
containerTargetEntity = getEntityByShortid(targetNode.data.folder.shortid)
}
let currentContainer = containerTargetEntity
if (containerSourceEntity.shortid === containerTargetEntity.shortid) {
return false
}
containersInHierarchyForTarget.push(containerTargetEntity.shortid)
while (
currentContainer.shortid !== containerSourceEntity.shortid &&
currentContainer.folder != null
) {
const parentContainer = getEntityByShortid(currentContainer.folder.shortid)
containersInHierarchyForTarget.push(parentContainer.shortid)
currentContainer = parentContainer
}
if (
containersInHierarchyForTarget.indexOf(containerSourceEntity.shortid) !== -1
) {
return false
}
return true
}, [getEntityByShortid])
mainEntityDropResolver.handler = async ({ draggedItem, dragOverContext, dropComplete }) => {
const sourceEntitySet = draggedItem.entitySet
const sourceNode = draggedItem.node
const targetNode = dragOverContext ? dragOverContext.targetNode : undefined
let sourceInfo
let targetInfo
if (sourceNode && dragOverContext && !dragOverContext.containerTargetEntity) {
if (!dragOverContext.overRoot) {
if (!isValidHierarchyTarget(sourceNode, targetNode)) {
return
}
}
// drop will be at the same root level, so we stop it
if (sourceNode.data.folder == null) {
return
}
sourceInfo = {
id: sourceNode.data._id,
entitySet: sourceEntitySet
}
targetInfo = {
shortid: null
}
} else if (
sourceNode &&
dragOverContext &&
dragOverContext.containerTargetEntity
) {
if (!isValidHierarchyTarget(sourceNode, targetNode)) {
return
}
// skip drop over same hierarchy
if (
(sourceNode.data.__entitySet === 'folders' &&
sourceNode.data.shortid === dragOverContext.containerTargetEntity.shortid) ||
(sourceNode.data.folder && sourceNode.data.folder.shortid === dragOverContext.containerTargetEntity.shortid)
) {
return
}
sourceInfo = {
id: sourceNode.data._id,
entitySet: sourceEntitySet
}
targetInfo = {
shortid: dragOverContext.containerTargetEntity.shortid,
children: getAllEntitiesInHierarchy(listRef.current.entityNodesById[dragOverContext.containerTargetEntity._id])
}
}
if (sourceInfo && targetInfo) {
dropComplete()
copyOrMoveEntity(sourceInfo, targetInfo)
}
}
useConstructor(() => {
if (main) {
const registered = entityTreeDropResolvers.find((r) => r === mainEntityDropResolver)
if (registered != null) {
return
}
entityTreeDropResolvers.push(mainEntityDropResolver)
}
})
const clearHighlightedArea = useCallback(() => {
setHighlightedArea(null)
}, [])
const showHighlightedArea = useCallback((sourceEntityNode, targetEntityNode) => {
const newHighlightedArea = {}
let containerTargetInContext
if (
!targetEntityNode ||
// support highlight root hierarchy when over entities at root
(targetEntityNode.data.__entitySet !== 'folders' &&
targetEntityNode.data.folder == null)
) {
const hierarchyEntityDimensions = listRef.current.node.getBoundingClientRect()
newHighlightedArea.hierarchy = {
top: hierarchyEntityDimensions.top - 2,
left: hierarchyEntityDimensions.left + 6,
width: `${paddingByLevelInTree}rem`,
height: hierarchyEntityDimensions.height + 4
}
if (dragOverContextRef.current) {
dragOverContextRef.current.overRoot = true
}
} else if (targetEntityNode.data.folder != null || targetEntityNode.data.__entitySet === 'folders') {
let hierarchyEntity
if (dragOverContextRef.current) {
dragOverContextRef.current.overRoot = false
}
if (targetEntityNode.data.__entitySet === 'folders') {
hierarchyEntity = getEntityByShortid(targetEntityNode.data.shortid)
containerTargetInContext = hierarchyEntity
} else {
hierarchyEntity = getEntityByShortid(targetEntityNode.data.folder.shortid)
containerTargetInContext = hierarchyEntity
}
if (sourceEntityNode && sourceEntityNode.data.__entitySet === 'folders') {
if (!isValidHierarchyTarget(sourceEntityNode, targetEntityNode)) {
return clearHighlightedArea()
}
}
const hierarchyEntityNodeId = getNodeDOMId(hierarchyEntity)
const hierarchyEntityTitleNodeId = getNodeTitleDOMId(hierarchyEntity)
if (!hierarchyEntityNodeId || !hierarchyEntityTitleNodeId) {
return
}
const hierarchyEntityDOMNode = document.getElementById(hierarchyEntityNodeId)
const hierarchyEntityTitleDOMNode = document.getElementById(hierarchyEntityTitleNodeId)
if (!hierarchyEntityDOMNode || !hierarchyEntityTitleDOMNode) {
return
}
const hierachyEntityTitleDimensions = hierarchyEntityTitleDOMNode.getBoundingClientRect()
newHighlightedArea.label = {
top: hierachyEntityTitleDimensions.top,
left: hierachyEntityTitleDimensions.left,
width: hierachyEntityTitleDimensions.width,
height: hierachyEntityTitleDimensions.height
}
let containerTargetIsCollapsed = false
let containerTargetHasEntities = false
if (containerTargetInContext && listRef.current.entityNodesById[containerTargetInContext._id]) {
const nodeObj = listRef.current.entityNodesById[containerTargetInContext._id]
if (getAllEntitiesInHierarchy(nodeObj, false, true).length > 0) {
containerTargetHasEntities = true
}
if (isNodeCollapsed(nodeObj)) {
containerTargetIsCollapsed = true
}
}
if (containerTargetInContext && (containerTargetIsCollapsed || !containerTargetHasEntities)) {
newHighlightedArea.hierarchy = null
} else {
const hierarchyEntityDimensions = hierarchyEntityDOMNode.getBoundingClientRect()
newHighlightedArea.hierarchy = {
top: hierachyEntityTitleDimensions.top + (hierachyEntityTitleDimensions.height + 4),
left: hierachyEntityTitleDimensions.left,
width: `${paddingByLevelInTree}rem`,
height: hierarchyEntityDimensions.height - (hierachyEntityTitleDimensions.height + 4)
}
}
}
if (Object.keys(newHighlightedArea).length > 0) {
if (dragOverContextRef.current) {
dragOverContextRef.current.containerTargetEntity = containerTargetInContext
}
setHighlightedArea((prev) => {
if (isEqual(prev, newHighlightedArea)) {
return prev
}
return newHighlightedArea
})
}
}, [paddingByLevelInTree, getEntityByShortid, clearHighlightedArea, isNodeCollapsed, isValidHierarchyTarget, listRef])
const { draggedNode, dragOverNode, connectDropTarget } = useDropHandler({
listRef,
dragOverContextRef,
onDragOverNode: showHighlightedArea,
onDragEnd: clearHighlightedArea,
onDragOut: clearHighlightedArea
})
const connectDropping = !selectable ? connectDropTarget : undefined
const sharedValues = useMemo(() => {
return {
allEntities: entities,
paddingByLevel: paddingByLevelInTree,
selectable,
selectionMode,
contextMenu,
clipboard,
contextMenuRef,
onRemove,
onClone,
onRename,
onNewEntity: (node, ...params) => {
if (node && node.isEntitySet !== true) {
toogleNodeCollapse(node, false)
}
onNewEntity(...params)
},
onOpen: (entity) => {
openTab({ _id: entity._id })
},
onNodeSelect: (node, value, mode) => {
if (!selectable) {
return
}
const isGroupEntityNode = checkIsGroupEntityNode(node)
const toProcess = isGroupEntityNode ? getAllEntitiesInHierarchy(node, true) : [node.data._id]
if (toProcess.length === 0) {
return
}
const updates = {
...(mode !== 'single' ? selected : undefined),
...toProcess.reduce((acu, currentEntityId) => {
acu[currentEntityId] = value != null ? value : !selected[currentEntityId]
return acu
}, {})
}
const newSelected = {}
for (const entityId of Object.keys(updates)) {
if (updates[entityId] === true) {
newSelected[entityId] = true
}
}
onSelectionChanged(newSelected)
},
onNodeClick: (node) => {
const isGroup = checkIsGroupNode(node)
const isGroupEntity = checkIsGroupEntityNode(node)
const isEntity = !isGroupEntity && !checkIsGroupNode(node)
if (isEntity) {
openTab({ _id: node.data._id })
} else if (isGroup || isGroupEntity) {
toogleNodeCollapse(node)
}
clearContextMenu()
},
onNodeDragOver: dragOverNode,
onNodeCollapse: toogleNodeCollapse,
onContextMenu: showContextMenu,
onClearContextMenu: clearContextMenu,
onSetClipboard: (newClipboard) => {
setClipboard(newClipboard)
},
onReleaseClipboardTo: (destination) => {
if (clipboard == null) {
return
}
copyOrMoveEntity({
id: clipboard.entityId,
entitySet: clipboard.entitySet
}, {
shortid: destination.shortid,
children: destination.children
}, clipboard.action === 'copy')
setClipboard(null)
},
isNodeCollapsed,
isNodeSelected: (node) => {
return selected[node.data._id] === true
},
isNodeActive: (node) => {
let active = false
if (
activeEntity != null &&
(checkIsGroupEntityNode(node) || !checkIsGroupNode(node)) &&
node.data != null && node.data._id === activeEntity._id
) {
active = true
}
return active
},
getContextMenuItems
}
}, [
entities,
paddingByLevelInTree,
selectable,
selectionMode,
contextMenu,
clipboard,
selected,
activeEntity,
onNewEntity,
onClone,
onRename,
onRemove,
onSelectionChanged,
openTab,
getContextMenuItems,
isNodeCollapsed,
toogleNodeCollapse,
dragOverNode,
showContextMenu,
clearContextMenu,
copyOrMoveEntity,
contextMenuRef
])
return {
currentEntities,
highlightedArea,
draggedNode,
connectDropping,
setFilter,
context: sharedValues
}
}