有如下组件:
index.js:(src/components/FileTree)
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import './index.less'
import { Icon, Input, Popconfirm } from 'antd'
import classnames from 'classnames'
import { Iconfont } from 'components'
export default class FileTree extends Component {
constructor (props) {
super(props)
this.state = {
count: 0,
selectedKey: props.selectedKey || null,
expandedKeysMap: this.arrayToMap(props.expandedKeys),
hoverNode: null,
confirmVisible: false
}
this.tempExpandedMap = {}
this.placeHolder = document.createElement('li')
this.placeHolder.className = 'tree-placeholder'
}
arrayToMap (list) {
const object = {}
list && list.map(l => {
object[l] = true
})
return object
}
componentWillReceiveProps (nextProps) {
if (nextProps.selectedKey !== this.props.selectedKey) {
this.setState({
selectedKey: nextProps.selectedKey
})
}
if (nextProps.expandedKeys && nextProps.expandedKeys.length !== this.props.expandedKeys.length) {
this.setState({
expandedKeysMap: this.arrayToMap(nextProps.expandedKeys)
})
}
if (nextProps.search !== this.props.search) {
// 保留搜索前的展开状态
if (this.props.search === '') {
this.preMap = { ...this.state.expandedKeysMap }
}
// 恢复搜索前的展开状态
if (nextProps.search === '') {
this.setState({
expandedKeysMap: { ...this.preMap }
})
return
}
this.tempExpandedMap = {}
// 查找需要展开的节点
this.filterTree(nextProps.data, nextProps.search, false)
this.setState({
expandedKeysMap: { ...this.tempExpandedMap }
})
}
}
onClick (el) {
const { onClick, onSelectedKeyChange } = this.props
const { expandedKeysMap } = this.state
expandedKeysMap[el.id] = !expandedKeysMap[el.id]
this.setState({
selectedKey: el.id,
expandedKeysMap
})
onSelectedKeyChange && onSelectedKeyChange(el.id, el)
onClick && onClick(el)
}
onDoubleClick (e, el) {
this.props.onDoubleClick && this.props.onDoubleClick(e, el, this.props.type)
}
onContextMenu (el, e) {
e.stopPropagation()
this.props.onContextMenu && this.props.onContextMenu(e, el)
}
onDragStart (it, e) {
e.stopPropagation()
this.dragged = e.currentTarget.parentNode
e.dataTransfer.effectAllowed = 'move'
this.props.onDrag && this.props.onDrag(it)
}
onDragEnd (it, e) {
const { data, onMove } = this.props
const from = this.dragged.dataset.id
const to = this.over.dataset.id
const fromPath = from.split('_')
const toPath = to.split('_')
const isAfter = this.nodePlacement === 'after'
this.dragged.style.display = 'block'
this.over.parentNode.removeChild(this.placeHolder)
// 寻找需要更新的两个文件夹并进行移动
if (fromPath.length === toPath.length) {
let fromSearch = data
let toSearch = data
fromPath.map((f, i) => {
const fromStep = parseInt(f)
let toStep = parseInt(toPath[i])
if (i === fromPath.length - 1) {
if (isAfter) toStep++
if (fromSearch === toSearch && fromStep < toStep) toStep--
// 移动源数据
onMove(data, fromSearch, fromStep, toSearch, toStep)
// toSearch.splice(toStep, 0, fromSearch.splice(fromStep, 1)[0])
} else {
fromSearch = fromSearch[fromStep].subTree
toSearch = toSearch[toStep].subTree
}
})
// 关闭拖拽
this.triggerDraggable(it, false)()
}
}
onDrop (it, e) {
e.stopPropagation()
e.preventDefault()
this.props.onDrop && this.props.onDrop(it)
}
onDragEnter (it, e) {
const { expandedKeysMap } = this.state
const targetLi = e.currentTarget.parentNode
// 同级拖动,折叠文件夹,子级拖动到父级,展开文件夹
expandedKeysMap[it.id] = targetLi.parentNode !== this.dragged.parentNode
this.setState({
expandedKeysMap
})
}
onDragOver (e) {
e.preventDefault()
const targetLi = e.currentTarget.parentNode
this.dragged.style.display = 'none'
// 子文件夹不能添加到和父文件夹同一级,拖动到父文件夹上时默认不处理,现在只支持两级
if (targetLi.parentNode === this.dragged.parentNode.parentNode.parentNode) {
e.dataTransfer.dropEffect = 'none'
return
}
// 父文件夹不能添加到和子文件夹同一级,拖动到子文件夹上时默认不处理,现在只支持两级
if (targetLi.parentNode.parentNode.parentNode === this.dragged.parentNode) {
e.dataTransfer.dropEffect = 'none'
return
}
this.over = targetLi
const relY = e.clientY - this.over.offsetTop
const height = this.over.offsetHeight / 2
const parent = targetLi.parentNode
if (relY > height) {
this.nodePlacement = 'after'
parent.insertBefore(this.placeHolder, targetLi.nextElementSibling)
} else if (relY < height) {
this.nodePlacement = 'before'
parent.insertBefore(this.placeHolder, targetLi)
}
}
onDragLeave () {
}
// 拖拽开关
triggerDraggable = (el, can) => () => {
el.draggable = can
const { expandedKeysMap } = this.state
expandedKeysMap[el.id] = false
this.setState({
expandedKeysMap
})
}
// 添加Hover样式
addHover = (el) => () => {
if (this.state.confirmVisible) {
return
}
this.setState({
hoverNode: el.id
})
}
// 移除当前Hover样式
removeHover = () => {
if (this.state.confirmVisible) {
return
}
this.setState({
hoverNode: null
})
}
onConfirmPopover = (visible) => {
this.setState({
confirmVisible: visible
})
}
renderAContent (data, el) {
const { expandedKeysMap } = this.state
const { search, readOnly, name } = this.props
const { content, isEdit, id, subTree, count } = el
// 高亮关键字
let showContent = content
if (search.length) {
const index = content.toLowerCase().search(search.toLowerCase())
if (index > -1) {
const beforeStr = content.substr(0, index)
const afterStr = content.substr(index + search.length)
const matchStr = content.substr(index, search.length)
showContent = (
<span>{beforeStr}<span style={{ color: '#f50', backgroundColor: '#ffff00' }}>{matchStr}</span>{afterStr}</span>
)
}
}
return (
<div className="tree_detail"
onMouseEnter={this.addHover(el)}>
<div className="tree_main">
{!isEdit ? (
<div className="tree_hd">
<Icon type={ subTree != null ? (!expandedKeysMap[id] ? 'plus-square-o' : 'minus-square-o') : ''}/>
<span className="tree_title" title={content}>
{showContent}({count || 0})
</span>
</div>
) : (
<div className="tree_hd">
<Input
autoFocus
defaultValue={content}
onPressEnter={this.hideEdit(el)}
onBlur={this.hideEdit(el)}
className="tree_input"
/>
</div>
)}
</div>
{isEdit || readOnly ? null : (
<div className="tree_addon">
<span>
<Iconfont type="edit" onClick={this.showEdit(el)}/>
<Popconfirm
title={
<div style={{ width: 300 }}>
<p className="tree-confirm-title">删除{name}</p>
<p className="tree-confirm-content">{name === '标签' && count ?
`该${name}将从系统中删除,您无法再通过“${content}”找到对应的${count}项任务,确定删除吗?` :
`确定删除“${content}”吗?`}</p>
</div>
}
okType="danger"
okText="删除"
cancelText="取消"
overlayClassName="tree-popover-confirm"
onConfirm={this.deleteItem(data, el)}
onVisibleChange={this.onConfirmPopover}
onClick={e => e.stopPropagation()}
>
<Icon type="close-circle-o" />
</Popconfirm>
{search.length || data.length < 2 ? null : <Iconfont type="sider-collaspe"
onMouseDown={this.triggerDraggable(el, true)}
onMouseUp={this.triggerDraggable(el, false)}/>}
</span>
</div>
)}
</div>
)
}
renderDropDir (data, liClass, el, subTree, positionId) {
const { readOnly } = this.props
return (
<li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
key={el.id}
data-id={positionId}
onContextMenu={this.onContextMenu.bind(this, el)}>
<div className="tree_wp"
onDrop={readOnly ? null : this.onDrop.bind(this, el)}
onDragOver={readOnly ? null : this.onDragOver.bind(this)}
onDragEnter={readOnly ? null : this.onDragEnter.bind(this, el)}
onDragLeave={readOnly ? null : this.onDragLeave.bind(this)}
onClick={this.onClick.bind(this, el)}
onDoubleClick={this.onDoubleClick.bind(this, el)}>
{this.renderAContent(data, el)}
</div>
{subTree}
</li>
)
}
renderDragItem (data, liClass, el, subTree, positionId) {
const { readOnly } = this.props
return <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
key={el.id}
data-id={positionId}
onContextMenu={this.onContextMenu.bind(this, el)}>
<div className="tree_wp"
draggable={!readOnly && el.draggable}
onDragStart={readOnly ? null : this.onDragStart.bind(this, el)}
onDragEnd={readOnly ? null : this.onDragEnd.bind(this, el)}
onClick={this.onClick.bind(this, el)}
onDoubleClick={this.onDoubleClick.bind(this, el)}>
{this.renderAContent(data, el)}
</div>
{subTree}
</li>
}
renderLi (data, liClass, el, subTree, positionId) {
return <li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
key={el.id}
data-id={positionId}
onContextMenu={this.onContextMenu.bind(this, el)}>
<div className="tree_wp"
onClick={this.onClick.bind(this, el)}
onDoubleClick={this.onDoubleClick.bind(this, el)}>
{this.renderAContent(data, el)}
</div>
{subTree}
</li>
}
renderDragAndDropItem (data, liClass, el, subTree, positionId) {
const { readOnly } = this.props
return (
<li className={classnames('tree_item', liClass, { 'actived': this.state.selectedKey === el.id }, { 'hover': this.state.hoverNode === el.id })}
key={el.id}
data-id={positionId}
onContextMenu={this.onContextMenu.bind(this, el)}>
<div className="tree_wp"
draggable={!readOnly && el.draggable}
onDragStart={readOnly ? null : this.onDragStart.bind(this, el)}
onDragEnd={readOnly ? null : this.onDragEnd.bind(this, el)}
onDrop={readOnly ? null : this.onDrop.bind(this, el)}
onDragOver={readOnly ? null : this.onDragOver.bind(this)}
onDragEnter={readOnly ? null : this.onDragEnter.bind(this, el)}
onDragLeave={readOnly ? null : this.onDragLeave.bind(this)}
onClick={this.onClick.bind(this, el)}
onDoubleClick={this.onDoubleClick.bind(this, el)}>
{this.renderAContent(data, el)}
</div>
{subTree}
</li>
)
}
renderTree (parentId, displayData, originalData, isRoot = false, parentPosition = '') {
const { readOnly } = this.props
const { expandedKeysMap } = this.state
return (
<ul className={classnames('tree', { 'tree-root': isRoot })}
onMouseLeave={isRoot ? this.removeHover : null}>
{displayData && displayData.map((it, index) => {
const { id, subTree, filteredSubTree, liClass, isDrop, isDrag } = it
const positionId = parentPosition ? parentPosition + '_' + index : `${index}`
const subTreeRender = filteredSubTree ? this.renderTree(id, filteredSubTree, subTree, false, positionId) : ''
const isOpened = !!expandedKeysMap[id]
const liClassName = (isOpened ? 'tree-open' : '') + ' ' + (liClass || '')
if (isDrop && !isDrag) {
return this.renderDropDir(originalData, liClassName, it, subTreeRender, positionId)
} else if (isDrag && !isDrop) {
return this.renderDragItem(originalData, liClassName, it, subTreeRender, positionId)
} else if (isDrop && isDrag) {
return this.renderDragAndDropItem(originalData, liClassName, it, subTreeRender, positionId)
} else {
return this.renderLi(originalData, liClassName, it, subTreeRender, positionId)
}
})}
{isRoot || readOnly ? null : (
<li className="tree_item-add">
<a href="javascript:void(0)" onClick={this.addItem(originalData, parentId)}><Icon type="plus"/>新建</a>
</li>
)}
</ul>
)
}
showEdit = (el) => (e) => {
e.stopPropagation()
el.isEdit = true
this.forceUpdate()
}
hideEdit = (el) => (e) => {
const { onRename, onCreate, data } = this.props
el.isEdit = false
if (e.target.value || el.isNew) {
const newName = e.target.value.trim()
if (el.isNew) {
if (newName) {
onCreate(data, el, newName)
} else {
el.parent.pop()
}
} else {
if (newName !== el.content) {
onRename(data, el, newName)
}
}
el.content = newName
}
this.forceUpdate()
}
addItem = (parent, parentId) => () => {
const { count } = this.state
const newItem = {
'content': '',
'id': `new${count}`,
'isDir': false,
'isDrag': true,
'isDrop': true,
'isEdit': true,
'isNew': true,
'parentId': parentId,
'parent': parent
}
parent.push(newItem)
this.setState({
count: count + 1
})
}
deleteItem = (parent, el) => (e) => {
const { onDelete, data } = this.props
e.stopPropagation()
let index = -1
parent.find((p, i) => {
if (p.id === el.id) {
index = i
return true
}
})
if (index > -1) {
onDelete(data, el, parent, index)
// parent.splice(index, 1)
this.forceUpdate()
}
}
filterTree (data, search, stop) {
return data.filter(d => {
const { subTree, content } = d
let exist = content.toLowerCase().search(search.toLowerCase()) > -1 || stop
if (subTree) {
// 如果父级匹配到了,那么子级就不用再过滤了
const filteredSubTree = this.filterTree(subTree, search, exist)
d.filteredSubTree = filteredSubTree
if (filteredSubTree.length) {
exist = true
}
// 记录需要展开子树的节点
if (search && exist) {
this.tempExpandedMap[d.id] = true
}
}
return exist
})
}
render () {
const { data, noDataTips, search } = this.props
const filteredTree = this.filterTree(data, search, false)
return filteredTree.length ? this.renderTree(0, filteredTree, data, true) : <span>{noDataTips}</span>
}
}
FileTree.defaultProps = {
name: '文件夹',
search: '',
noDataTips: '没有找到相关内容',
readOnly: false,
expandedKeys: []
}
FileTree.propTypes = {
type: PropTypes.string,
name: PropTypes.string,
readOnly: PropTypes.bool,
data: PropTypes.array,
onChange: PropTypes.func,
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
onContextMenu: PropTypes.func,
onDrag: PropTypes.func,
onDrop: PropTypes.func,
search: PropTypes.string,
noDataTips: PropTypes.string,
selectedKey: PropTypes.any,
expandedKeys: PropTypes.array,
onSelectedKeyChange: PropTypes.func,
onRename: PropTypes.func,
onDelete: PropTypes.func,
onCreate: PropTypes.func,
onMove: PropTypes.func,
}
在(src/Pages/Favorite/Favorite.js)使用如下语句引入:
import FileTree from '@/components/FileTree'
出现下图错误:
检查到已经export了这个组件,请各位大神看看还有哪里出错了呢?
会被认为是引用全局module,如果要引用当前目录下的某个组件,应该这么写:
https://segmentfault.com/a/11...
前段时间的一篇文章这里有介绍到module的路径问题,虽说是原生ES-Module,但是在处理非全局module下,规则基本通用(除了后缀)