@ovine/craft
Version:
Ovine json editor.
220 lines (191 loc) • 6.07 kB
JSX
/**
* 组件选择器
* TODO:
* 1. 修复 当无滚动区域时,每次都nav自动跳到最后一个问题。(这是bootstrap问题,完全理解不了为什么,要选到最后一项)
* 正确逻辑是:默认不做任何操作
* https://github.com/twbs/bootstrap/blob/main/js/src/scrollspy.js#L209
* 2. 完成 选择后 将 组件添加到对应位置
*/
import { Drawer } from 'amis'
import { flattenTree, filterTree } from 'amis/lib/utils/helper'
import _ from 'lodash'
import React, { useEffect, useMemo, useRef } from 'react'
import { app } from '@core/app'
import { useImmer, useSubscriber } from '@core/utils/hooks'
import { publish } from '@core/utils/message'
import { onAddNode } from '@/components/preview/actions'
import { message, domId } from '@/constants'
import nodeConfig from './nodes'
import { StyledSelector } from './styled'
const Selector = (props) => {
const { label: headerLabel = '添加组件' } = props.info
const [state, setState] = useImmer({
searchText: '',
})
const storeRef = useRef({})
const { searchText } = state
const items = useMemo(() => {
// 没有查询时 返回所有数据
if (!searchText) {
return flattenTree(nodeConfig)
}
const filteredItems = filterTree(nodeConfig, (item) => {
const { type = '', label = '', parent } = item
// 父级 默认全部通过匹配,直接判断 子级 的数据
if (!parent) {
return true
}
// 只判断 type 与 label 两个数据
return type.indexOf(searchText) > -1 || label.indexOf(searchText) > -1
})
return flattenTree(
// 过滤掉 没有 子级 的数据
filterTree(filteredItems, (item) => (!item.parent && !!item.children.length) || !!item.parent)
)
}, [searchText])
const onSearchChange = _.throttle((e) => {
const { value } = e.currentTarget
setState((d) => {
d.searchText = value
})
}, 100)
const onItemClick = (item, e) => {
const $item = $(e.currentTarget)
$item
.addClass('active')
.siblings()
.removeClass('active')
onAddNode({
container: props.info,
node: item,
position: {
x: e.pageX,
y: e.pageY,
},
})
}
useEffect(() => {
// 使用了 bootstrap scrollspy 组件 https://getbootstrap.com/docs/4.5/components/scrollspy
const $scrollSpy = $(`#${domId.editorSelector} [data-spy="scroll"]`)
const $scrollNav = $(`#${domId.editorSelectorNav}`)
storeRef.current.$scrollSpy = $scrollSpy
$scrollSpy.scrollspy()
$scrollNav.on('click', '.nav-link', (e) => {
const $item = $(e.target)
if ($item.hasClass('active')) {
return
}
$scrollSpy.scrollspy('dispose')
$item
.addClass('active')
.siblings()
.removeClass('active')
const $targetItem = $($item.attr('href'))
$scrollSpy.animate(
{ scrollTop: $scrollSpy.scrollTop() + $targetItem.offset().top - $scrollSpy.offset().top },
200,
() => {
$scrollSpy.scrollspy('refresh')
}
)
})
return () => {
$scrollSpy.scrollspy('dispose')
}
}, [])
useEffect(() => {
const { $scrollSpy } = storeRef.current
if (!items.length) {
$scrollSpy.scrollspy('dispose')
} else {
$scrollSpy.scrollspy('refresh')
}
}, [items.length])
return (
<StyledSelector id={domId.editorSelector}>
<div className="selector-title">
<p>{headerLabel}</p>
</div>
<div className="selector-input">
<div className="input">
<input placeholder="搜索可用组件" value={searchText} onChange={onSearchChange} />
<i className="fa fa-search" />
</div>
</div>
{items.length === 0 && (
<div className="selector-empty">
<i className="fa fa-chain-broken" />
<span>暂未找到对应组件</span>
</div>
)}
<div className="selector-list">
<div id={domId.editorSelectorNav} className="selector-nav">
{items.map((item, index) => {
if (!item.parent) {
return (
<div key={index} className="title nav-link" href={`#spy-${item.type}`}>
<i className={`fa fa-${item.icon} p-r-xs`} />
<span>{item.label}</span>
</div>
)
}
return (
<div key={index} className="item nav-link" href={`#spy-${item.parent}-${item.type}`}>
{item.label}
</div>
)
})}
</div>
<div
className="selector-content"
data-target={`#${domId.editorSelectorNav}`}
data-spy="scroll"
>
{items.map((item, index) => {
if (!item.parent) {
return (
<div key={index} id={`spy-${item.type}`} className="title">
{item.label}
</div>
)
}
return (
<div
key={index}
id={`spy-${item.parent}-${item.type}`}
className="item"
onClick={(e) => onItemClick(item, e)}
>
<img src={item.img} alt={item.label} />
<div>
<h6>{item.label}</h6>
<p>{item.desc}</p>
</div>
</div>
)
})}
</div>
</div>
</StyledSelector>
)
}
export const toggleSelector = (info = {}) => {
publish(message.toggleSelector, info)
}
export default () => {
const [info, setInfo] = useImmer({})
const { isShow = false, ...resetInfo } = info
const onHide = () => {
setInfo((d) => {
d.isShow = false
})
}
useSubscriber(message.toggleSelector, (data = {}) => {
setInfo(() => data)
})
return (
<Drawer theme={app.theme.getName()} size="md" onHide={onHide} show={isShow} position="right">
<Selector info={resetInfo} />
</Drawer>
)
}