block-selector

This commit is contained in:
StyleZhang
2024-02-22 15:01:41 +08:00
parent 0759b29ca2
commit ea76f46223
10 changed files with 123 additions and 404 deletions

View File

@ -1,137 +0,0 @@
'use client'
import { useCallback, useRef, useState } from 'react'
import { createContext, useContext } from 'use-context-selector'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import {
FloatingPortal,
flip,
offset,
shift,
useDismiss,
useFloating,
useInteractions,
} from '@floating-ui/react'
import type { OnSelect } from './types'
import BlockSelector from './index'
type UpdateParams = {
from?: string
placement?: Placement
offset?: OffsetOptions
open?: boolean
className?: string
callback?: OnSelect
}
export type BlockSelectorContextValue = {
from: string
open: boolean
setOpen: (open: boolean) => void
referenceRef: any
floatingRef: any
floatingStyles: React.CSSProperties
getFloatingProps: any
handleToggle: (v: UpdateParams) => void
}
export const BlockSelectorContext = createContext<BlockSelectorContextValue>({
from: '',
open: false,
setOpen: () => {},
referenceRef: null,
floatingRef: null,
floatingStyles: {},
getFloatingProps: () => {},
handleToggle: () => {},
})
export const useBlockSelectorContext = () => useContext(BlockSelectorContext)
type BlockSelectorContextProviderProps = {
children: React.ReactNode
}
export const BlockSelectorContextProvider = ({
children,
}: BlockSelectorContextProviderProps) => {
const [from, setFrom] = useState('node')
const [open, setOpen] = useState(false)
const [placement, setPlacement] = useState<Placement>('top')
const [offsetValue, setOffsetValue] = useState<OffsetOptions>(0)
const [className, setClassName] = useState<string>('')
const callbackRef = useRef<OnSelect | undefined>(undefined)
const { refs, floatingStyles, context } = useFloating({
placement,
strategy: 'fixed',
open,
onOpenChange: setOpen,
middleware: [
flip(),
shift(),
offset(offsetValue),
],
})
const dismiss = useDismiss(context)
const { getFloatingProps } = useInteractions([
dismiss,
])
const handleToggle = useCallback(({
from,
open,
placement,
offset,
className,
callback,
}: UpdateParams) => {
setFrom(from || 'node')
if (open !== undefined)
setOpen(open)
else
setOpen(v => !v)
setPlacement(placement || 'top')
setOffsetValue(offset || 0)
setClassName(className || '')
callbackRef.current = callback
}, [])
const handleSelect = useCallback<OnSelect>((type) => {
if (callbackRef.current)
callbackRef.current(type)
setOpen(v => !v)
}, [])
return (
<BlockSelectorContext.Provider value={{
from,
open,
setOpen,
handleToggle,
referenceRef: refs.setReference,
floatingRef: refs.setFloating,
floatingStyles,
getFloatingProps,
}}>
{children}
{
open && (from === 'node' || from === 'panel') && (
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className='z-[1000]'
>
<BlockSelector
className={className}
onSelect={handleSelect}
/>
</div>
</FloatingPortal>
)
}
</BlockSelectorContext.Provider>
)
}

View File

@ -1,30 +1,80 @@
import type { FC } from 'react'
import { memo } from 'react'
import {
memo,
useState,
} from 'react'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type { BlockEnum } from '../types'
import Tabs from './tabs'
import type { OnSelect } from './types'
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import {
Plus02,
SearchLg,
} from '@/app/components/base/icons/src/vender/line/general'
type NodeSelectorProps = {
onSelect: OnSelect
className?: string
onSelect: (type: BlockEnum) => void
trigger?: (open: boolean) => React.ReactNode
placement?: Placement
offset?: OffsetOptions
popupClassName?: string
asChild?: boolean
}
const NodeSelector: FC<NodeSelectorProps> = ({
onSelect,
className,
trigger,
placement = 'right',
offset = 6,
popupClassName,
asChild,
}) => {
const [open, setOpen] = useState(false)
return (
<div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${className}`}>
<div className='px-2 pt-2'>
<div className='flex items-center px-2 rounded-lg bg-gray-100'>
<SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
<input
className='grow px-0.5 py-[7px] text-[13px] bg-transparent appearance-none outline-none'
placeholder='Search block'
/>
<PortalToFollowElem
placement={placement}
offset={offset}
open={open}
onOpenChange={setOpen}
>
<PortalToFollowElemTrigger asChild={asChild} onClick={() => setOpen(v => !v)}>
{
trigger
? trigger(open)
: (
<div
className={`
hidden absolute -right-2 top-4 items-center justify-center
w-4 h-4 rounded-full bg-primary-600 cursor-pointer z-10 group-hover:flex
${open && '!flex'}
`}
>
<Plus02 className='w-2.5 h-2.5 text-white' />
</div>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`w-[256px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2'>
<div className='flex items-center px-2 rounded-lg bg-gray-100'>
<SearchLg className='shrink-0 ml-[1px] mr-[5px] w-3.5 h-3.5 text-gray-400' />
<input
className='grow px-0.5 py-[7px] text-[13px] bg-transparent appearance-none outline-none'
placeholder='Search block'
/>
</div>
</div>
<Tabs onSelect={onSelect} />
</div>
</div>
<Tabs onSelect={onSelect} />
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}

View File

@ -4,7 +4,7 @@ import {
useState,
} from 'react'
import BlockIcon from '../block-icon'
import type { OnSelect } from './types'
import type { BlockEnum } from '../types'
import {
BLOCK_CLASSIFICATIONS,
BLOCK_GROUP_BY_CLASSIFICATION,
@ -12,7 +12,7 @@ import {
} from './constants'
export type TabsProps = {
onSelect: OnSelect
onSelect: (type: BlockEnum) => void
}
const Tabs: FC<TabsProps> = ({
onSelect,

View File

@ -1,3 +0,0 @@
import type { BlockEnum } from '../types'
export type OnSelect = (type: BlockEnum) => void