theatre/theatre/studio/src/panels/OutlinePanel/ObjectsList/ObjectsList.tsx
Clément Roche 6558181dbb
Make items in the outline menu collapsable (#367)
Co-authored-by: Clement Roche <rchclement@gmail.com>
Co-authored-by: Cole Lawrence <cole@colelawrence.com>
Co-authored-by: Aria <aria.minaei@gmail.com>
2023-01-02 18:43:19 +00:00

181 lines
4.7 KiB
TypeScript

import type Sheet from '@theatre/core/sheets/Sheet'
import {usePrism} from '@theatre/react'
import {val} from '@theatre/dataverse'
import React from 'react'
import styled from 'styled-components'
import {ObjectItem} from './ObjectItem'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import BaseItem from '@theatre/studio/panels/OutlinePanel/BaseItem'
import {useCollapseStateInOutlinePanel} from '@theatre/studio/panels/OutlinePanel/outlinePanelUtils'
export const Li = styled.li<{isSelected: boolean}>`
color: ${(props) => (props.isSelected ? 'white' : 'hsl(1, 1%, 80%)')};
`
const ObjectsList: React.FC<{
depth: number
sheet: Sheet
}> = ({sheet, depth}) => {
return usePrism(() => {
const objectsMap = val(sheet.objectsP)
const objects = Object.values(objectsMap).filter(
(a): a is SheetObject => a != null,
)
const rootObject: NamespacedObjects = new Map()
objects.forEach((object) => {
addToNamespace(rootObject, object)
})
return (
<NamespaceTree
namespace={rootObject}
visualIndentation={depth}
path={[]}
sheet={sheet}
/>
)
}, [sheet, depth])
}
function NamespaceTree(props: {
namespace: NamespacedObjects
visualIndentation: number
path: string[]
sheet: Sheet
}) {
return (
<>
{[...props.namespace.entries()].map(([label, {object, nested}]) => {
return (
<Namespace
key={label}
label={label}
object={object}
nested={nested}
visualIndentation={props.visualIndentation}
path={props.path}
sheet={props.sheet}
/>
)
})}
</>
)
}
function Namespace(props: {
nested?: NamespacedObjects
label: string
object?: SheetObject
visualIndentation: number
path: string[]
sheet: Sheet
}) {
const {nested, label, object, sheet} = props
const {collapsed, setCollapsed} = useCollapseStateInOutlinePanel({
type: 'namespace',
sheet,
path: props.path,
})
const nestedChildrenElt = nested && (
<NamespaceTree
namespace={nested}
path={[...props.path, label]}
// Question: will there be key conflict if two components have the same labels?
key={'namespaceTree(' + label + ')'}
visualIndentation={props.visualIndentation + 1}
sheet={sheet}
/>
)
const sameNameElt = object && (
<ObjectItem
depth={props.visualIndentation}
// key is useful for navigating react dev component tree
key={'objectPath(' + object.address.objectKey + ')'}
// object entries should not allow this to be undefined
sheetObject={object}
overrideLabel={label}
/>
)
return (
<React.Fragment key={`${label} - ${props.visualIndentation}`}>
{sameNameElt}
{nestedChildrenElt && (
<BaseItem
selectionStatus="not-selectable"
label={label}
// key necessary for no duplicate keys (next to other React.Fragments)
key={`baseItem(${label})`}
depth={props.visualIndentation}
children={nestedChildrenElt}
collapsed={collapsed}
setIsCollapsed={setCollapsed}
/>
)}
</React.Fragment>
)
}
export default ObjectsList
/** See {@link addToNamespace} for adding to the namespace, easily. */
type NamespacedObjects = Map<
string,
{
object?: SheetObject
nested?: NamespacedObjects
path: string[]
}
>
function addToNamespace(
mutObjects: NamespacedObjects,
object: SheetObject,
path = getObjectNamespacePath(object),
) {
const [next, ...rest] = path
let existing = mutObjects.get(next)
if (!existing) {
existing = {
nested: undefined,
object: undefined,
path: [...path],
}
mutObjects.set(next, existing)
}
if (rest.length === 0) {
console.assert(
!existing.object,
'expect not to have existing object with same name',
{existing, object},
)
existing.object = object
} else {
if (!existing.nested) {
existing.nested = new Map()
}
addToNamespace(existing.nested, object, rest)
}
}
function getObjectNamespacePath(object: SheetObject): string[] {
let existing = OBJECT_SPLITS_MEMO.get(object)
if (!existing) {
existing = object.address.objectKey.split(
RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE,
)
console.assert(existing.length > 0, 'expected not empty')
OBJECT_SPLITS_MEMO.set(object, existing)
}
return existing
}
/**
* Relying on the fact we try to "sanitize paths" earlier.
* Go look for `sanifySlashedPath` in a `utils/slashedPaths.ts`.
*/
const RE_SPLIT_BY_SLASH_WITHOUT_WHITESPACE = /\s*\/\s*/g
const OBJECT_SPLITS_MEMO = new WeakMap<SheetObject, string[]>()