| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- import {
- createContext,
- useCallback,
- useContext,
- useReducer,
- useEffect,
- useMemo,
- useState,
- } from 'react'
- import PropTypes from 'prop-types'
- import classNames from 'classnames'
- import _ from 'lodash'
- import { findInTree } from '../util/find-in-tree'
- import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
- import { useProjectContext } from '../../../shared/context/project-context'
- import { useEditorContext } from '../../../shared/context/editor-context'
- import { useLayoutContext } from '../../../shared/context/layout-context'
- import usePersistedState from '../../../shared/hooks/use-persisted-state'
- import usePreviousValue from '../../../shared/hooks/use-previous-value'
- const FileTreeSelectableContext = createContext()
- const ACTION_TYPES = {
- SELECT: 'SELECT',
- MULTI_SELECT: 'MULTI_SELECT',
- UNSELECT: 'UNSELECT',
- }
- function fileTreeSelectableReadWriteReducer(selectedEntityIds, action) {
- switch (action.type) {
- case ACTION_TYPES.SELECT: {
- // reset selection
- return new Set([action.id])
- }
- case ACTION_TYPES.MULTI_SELECT: {
- const selectedEntityIdsCopy = new Set(selectedEntityIds)
- if (selectedEntityIdsCopy.has(action.id)) {
- // entity already selected
- if (selectedEntityIdsCopy.size > 1) {
- // entity already multi-selected; remove from set
- selectedEntityIdsCopy.delete(action.id)
- }
- } else {
- // entity not selected: add to set
- selectedEntityIdsCopy.add(action.id)
- }
- return selectedEntityIdsCopy
- }
- case ACTION_TYPES.UNSELECT: {
- const selectedEntityIdsCopy = new Set(selectedEntityIds)
- selectedEntityIdsCopy.delete(action.id)
- return selectedEntityIdsCopy
- }
- default:
- throw new Error(`Unknown selectable action type: ${action.type}`)
- }
- }
- function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) {
- switch (action.type) {
- case ACTION_TYPES.SELECT:
- return new Set([action.id])
- case ACTION_TYPES.MULTI_SELECT:
- case ACTION_TYPES.UNSELECT:
- return selectedEntityIds
- default:
- throw new Error(`Unknown selectable action type: ${action.type}`)
- }
- }
- export function FileTreeSelectableProvider({ onSelect, children }) {
- const { _id: projectId, rootDocId } = useProjectContext(
- projectContextPropTypes
- )
- const { permissionsLevel } = useEditorContext(editorContextPropTypes)
- const [initialSelectedEntityId] = usePersistedState(
- `doc.open_id.${projectId}`,
- rootDocId
- )
- const { fileTreeData, setSelectedEntities } = useFileTreeData()
- const [isRootFolderSelected, setIsRootFolderSelected] = useState(false)
- const [selectedEntityIds, dispatch] = useReducer(
- permissionsLevel === 'readOnly'
- ? fileTreeSelectableReadOnlyReducer
- : fileTreeSelectableReadWriteReducer,
- null,
- () => {
- if (!initialSelectedEntityId) return new Set()
- // the entity with id=initialSelectedEntityId might not exist in the tree
- // anymore. This checks that it exists before initialising the reducer
- // with the id.
- if (findInTree(fileTreeData, initialSelectedEntityId))
- return new Set([initialSelectedEntityId])
- // the entity doesn't exist anymore; don't select any files
- return new Set()
- }
- )
- const [selectedEntityParentIds, setSelectedEntityParentIds] = useState(
- new Set()
- )
- // fills `selectedEntityParentIds` set
- useEffect(() => {
- const ids = new Set()
- selectedEntityIds.forEach(id => {
- const found = findInTree(fileTreeData, id)
- if (found) {
- found.path.forEach(pathItem => ids.add(pathItem))
- }
- })
- setSelectedEntityParentIds(ids)
- }, [fileTreeData, selectedEntityIds])
- // calls `onSelect` on entities selection
- const previousSelectedEntityIds = usePreviousValue(selectedEntityIds)
- useEffect(() => {
- if (_.isEqual(selectedEntityIds, previousSelectedEntityIds)) {
- return
- }
- const _selectedEntities = Array.from(selectedEntityIds)
- .map(id => findInTree(fileTreeData, id))
- .filter(Boolean)
- onSelect(_selectedEntities)
- setSelectedEntities(_selectedEntities)
- }, [
- fileTreeData,
- selectedEntityIds,
- previousSelectedEntityIds,
- onSelect,
- setSelectedEntities,
- ])
- useEffect(() => {
- // listen for `editor.openDoc` and selected that doc
- function handleOpenDoc(ev) {
- const found = findInTree(fileTreeData, ev.detail)
- if (!found) return
- dispatch({ type: ACTION_TYPES.SELECT, id: found.entity._id })
- }
- window.addEventListener('editor.openDoc', handleOpenDoc)
- return () => window.removeEventListener('editor.openDoc', handleOpenDoc)
- }, [fileTreeData])
- const select = useCallback(id => {
- dispatch({ type: ACTION_TYPES.SELECT, id })
- }, [])
- const unselect = useCallback(id => {
- dispatch({ type: ACTION_TYPES.UNSELECT, id })
- }, [])
- const selectOrMultiSelectEntity = useCallback((id, isMultiSelect) => {
- const actionType = isMultiSelect
- ? ACTION_TYPES.MULTI_SELECT
- : ACTION_TYPES.SELECT
- dispatch({ type: actionType, id })
- }, [])
- const value = {
- selectedEntityIds,
- selectedEntityParentIds,
- select,
- unselect,
- selectOrMultiSelectEntity,
- isRootFolderSelected,
- setIsRootFolderSelected,
- }
- return (
- <FileTreeSelectableContext.Provider value={value}>
- {children}
- </FileTreeSelectableContext.Provider>
- )
- }
- FileTreeSelectableProvider.propTypes = {
- onSelect: PropTypes.func.isRequired,
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.node),
- PropTypes.node,
- ]).isRequired,
- }
- const projectContextPropTypes = {
- _id: PropTypes.string.isRequired,
- rootDocId: PropTypes.string,
- }
- const editorContextPropTypes = {
- permissionsLevel: PropTypes.oneOf(['readOnly', 'readAndWrite', 'owner']),
- }
- export function useSelectableEntity(id, isFile) {
- const { view, setView } = useLayoutContext(layoutContextPropTypes)
- const {
- selectedEntityIds,
- selectOrMultiSelectEntity,
- isRootFolderSelected,
- setIsRootFolderSelected,
- } = useContext(FileTreeSelectableContext)
- const isSelected = selectedEntityIds.has(id)
- const handleEvent = useCallback(
- ev => {
- ev.stopPropagation()
- setIsRootFolderSelected(false)
- selectOrMultiSelectEntity(id, ev.ctrlKey || ev.metaKey)
- setView(isFile ? 'file' : 'editor')
- },
- [id, setIsRootFolderSelected, selectOrMultiSelectEntity, setView, isFile]
- )
- const handleClick = useCallback(
- ev => {
- handleEvent(ev)
- },
- [handleEvent]
- )
- const handleKeyPress = useCallback(
- ev => {
- if (ev.key === 'Enter' || ev.key === ' ') {
- handleEvent(ev)
- }
- },
- [handleEvent]
- )
- const handleContextMenu = useCallback(
- ev => {
- // make sure the right-clicked entity gets selected
- if (!selectedEntityIds.has(id)) {
- handleEvent(ev)
- }
- },
- [id, handleEvent, selectedEntityIds]
- )
- const isVisuallySelected =
- !isRootFolderSelected && isSelected && view !== 'pdf'
- const props = useMemo(
- () => ({
- className: classNames({ selected: isVisuallySelected }),
- 'aria-selected': isVisuallySelected,
- onClick: handleClick,
- onContextMenu: handleContextMenu,
- onKeyPress: handleKeyPress,
- }),
- [handleClick, handleContextMenu, handleKeyPress, isVisuallySelected]
- )
- return { isSelected, props }
- }
- const layoutContextPropTypes = {
- view: PropTypes.string,
- setView: PropTypes.func.isRequired,
- }
- export function useFileTreeSelectable() {
- const context = useContext(FileTreeSelectableContext)
- if (!context) {
- throw new Error(
- `useFileTreeSelectable is only available inside FileTreeSelectableProvider`
- )
- }
- return context
- }
|