632 lines
20 KiB
Vue
632 lines
20 KiB
Vue
import PropTypes from '../../_util/vue-types'
|
|
import classNames from 'classnames'
|
|
import warning from 'warning'
|
|
import { initDefaultProps, getOptionProps, getSlots } from '../../_util/props-util'
|
|
import { cloneElement } from '../../_util/vnode'
|
|
import BaseMixin from '../../_util/BaseMixin'
|
|
import proxyComponent from '../../_util/proxyComponent'
|
|
import {
|
|
convertTreeToEntities, convertDataToTree,
|
|
getPosition, getDragNodesKeys,
|
|
parseCheckedKeys,
|
|
conductExpandParent, calcSelectedKeys,
|
|
calcDropPosition,
|
|
arrAdd, arrDel, posToArr,
|
|
mapChildren, conductCheck,
|
|
warnOnlyTreeNode,
|
|
} from './util'
|
|
|
|
/**
|
|
* Thought we still use `cloneElement` to pass `key`,
|
|
* other props can pass with context for future refactor.
|
|
*/
|
|
|
|
const Tree = {
|
|
name: 'Tree',
|
|
mixins: [BaseMixin],
|
|
props: initDefaultProps({
|
|
prefixCls: PropTypes.string,
|
|
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
children: PropTypes.any,
|
|
treeData: PropTypes.array, // Generate treeNode by children
|
|
showLine: PropTypes.bool,
|
|
showIcon: PropTypes.bool,
|
|
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
|
focusable: PropTypes.bool,
|
|
selectable: PropTypes.bool,
|
|
disabled: PropTypes.bool,
|
|
multiple: PropTypes.bool,
|
|
checkable: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
|
|
checkStrictly: PropTypes.bool,
|
|
draggable: PropTypes.bool,
|
|
defaultExpandParent: PropTypes.bool,
|
|
autoExpandParent: PropTypes.bool,
|
|
defaultExpandAll: PropTypes.bool,
|
|
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
expandedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
checkedKeys: PropTypes.oneOfType([
|
|
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
|
PropTypes.object,
|
|
]),
|
|
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
selectedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
// onClick: PropTypes.func,
|
|
// onDoubleClick: PropTypes.func,
|
|
// onExpand: PropTypes.func,
|
|
// onCheck: PropTypes.func,
|
|
// onSelect: PropTypes.func,
|
|
loadData: PropTypes.func,
|
|
loadedKeys: PropTypes.arrayOf(PropTypes.string),
|
|
// onMouseEnter: PropTypes.func,
|
|
// onMouseLeave: PropTypes.func,
|
|
// onRightClick: PropTypes.func,
|
|
// onDragStart: PropTypes.func,
|
|
// onDragEnter: PropTypes.func,
|
|
// onDragOver: PropTypes.func,
|
|
// onDragLeave: PropTypes.func,
|
|
// onDragEnd: PropTypes.func,
|
|
// onDrop: PropTypes.func,
|
|
filterTreeNode: PropTypes.func,
|
|
openTransitionName: PropTypes.string,
|
|
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
switcherIcon: PropTypes.any,
|
|
_propsSymbol: PropTypes.any,
|
|
}, {
|
|
prefixCls: 'rc-tree',
|
|
showLine: false,
|
|
showIcon: true,
|
|
selectable: true,
|
|
multiple: false,
|
|
checkable: false,
|
|
disabled: false,
|
|
checkStrictly: false,
|
|
draggable: false,
|
|
defaultExpandParent: true,
|
|
autoExpandParent: false,
|
|
defaultExpandAll: false,
|
|
defaultExpandedKeys: [],
|
|
defaultCheckedKeys: [],
|
|
defaultSelectedKeys: [],
|
|
}),
|
|
|
|
data () {
|
|
const state = {
|
|
_posEntities: {},
|
|
_keyEntities: {},
|
|
_expandedKeys: [],
|
|
_selectedKeys: [],
|
|
_checkedKeys: [],
|
|
_halfCheckedKeys: [],
|
|
_loadedKeys: [],
|
|
_loadingKeys: [],
|
|
_treeNode: [],
|
|
_prevProps: null,
|
|
_dragOverNodeKey: '',
|
|
_dropPosition: null,
|
|
_dragNodesKeys: [],
|
|
}
|
|
return {
|
|
...state,
|
|
// ...this.getSyncProps(props),
|
|
// dragOverNodeKey: '',
|
|
// dropPosition: null,
|
|
// dragNodesKeys: [],
|
|
// sLoadedKeys: [],
|
|
// sLoadingKeys: [],
|
|
...this.getDerivedStateFromProps(getOptionProps(this), state),
|
|
}
|
|
},
|
|
provide () {
|
|
return {
|
|
vcTree: this,
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
__propsSymbol__ () {
|
|
this.setState(this.getDerivedStateFromProps(getOptionProps(this), this.$data))
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
getDerivedStateFromProps (props, prevState) {
|
|
const { _prevProps } = prevState
|
|
const newState = {
|
|
_prevProps: { ...props },
|
|
}
|
|
|
|
function needSync (name) {
|
|
return (!_prevProps && name in props) || (_prevProps && _prevProps[name] !== props[name])
|
|
}
|
|
|
|
// ================== Tree Node ==================
|
|
let treeNode = null
|
|
|
|
// Check if `treeData` or `children` changed and save into the state.
|
|
if (needSync('treeData')) {
|
|
treeNode = convertDataToTree(this.$createElement, props.treeData)
|
|
} else if (needSync('children')) {
|
|
treeNode = props.children
|
|
}
|
|
|
|
// Tree support filter function which will break the tree structure in the vdm.
|
|
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
|
|
if (treeNode) {
|
|
newState._treeNode = treeNode
|
|
|
|
// Calculate the entities data for quick match
|
|
const entitiesMap = convertTreeToEntities(treeNode)
|
|
newState._posEntities = entitiesMap.posEntities
|
|
newState._keyEntities = entitiesMap.keyEntities
|
|
}
|
|
|
|
const keyEntities = newState._keyEntities || prevState._keyEntities
|
|
|
|
// ================ expandedKeys =================
|
|
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
|
|
newState._expandedKeys = (props.autoExpandParent || (!_prevProps && props.defaultExpandParent))
|
|
? conductExpandParent(props.expandedKeys, keyEntities) : props.expandedKeys
|
|
} else if (!_prevProps && props.defaultExpandAll) {
|
|
newState._expandedKeys = Object.keys(keyEntities)
|
|
} else if (!_prevProps && props.defaultExpandedKeys) {
|
|
newState._expandedKeys = (props.autoExpandParent || props.defaultExpandParent)
|
|
? conductExpandParent(props.defaultExpandedKeys, keyEntities) : props.defaultExpandedKeys
|
|
}
|
|
|
|
// ================ selectedKeys =================
|
|
if (props.selectable) {
|
|
if (needSync('selectedKeys')) {
|
|
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props)
|
|
} else if (!_prevProps && props.defaultSelectedKeys) {
|
|
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props)
|
|
}
|
|
}
|
|
|
|
// ================= checkedKeys =================
|
|
if (props.checkable) {
|
|
let checkedKeyEntity
|
|
|
|
if (needSync('checkedKeys')) {
|
|
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}
|
|
} else if (!_prevProps && props.defaultCheckedKeys) {
|
|
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}
|
|
} else if (treeNode) {
|
|
// If treeNode changed, we also need check it
|
|
checkedKeyEntity = {
|
|
checkedKeys: prevState._checkedKeys,
|
|
halfCheckedKeys: prevState._halfCheckedKeys,
|
|
}
|
|
}
|
|
|
|
if (checkedKeyEntity) {
|
|
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity
|
|
|
|
if (!props.checkStrictly) {
|
|
const conductKeys = conductCheck(checkedKeys, true, keyEntities)
|
|
checkedKeys = conductKeys.checkedKeys
|
|
halfCheckedKeys = conductKeys.halfCheckedKeys
|
|
}
|
|
|
|
newState._checkedKeys = checkedKeys
|
|
newState._halfCheckedKeys = halfCheckedKeys
|
|
}
|
|
}
|
|
// ================= loadedKeys ==================
|
|
if (needSync('loadedKeys')) {
|
|
newState._loadedKeys = props.loadedKeys
|
|
}
|
|
|
|
return newState
|
|
},
|
|
onNodeDragStart (event, node) {
|
|
const { _expandedKeys } = this.$data
|
|
const { eventKey } = node
|
|
const children = getSlots(node).default
|
|
this.dragNode = node
|
|
|
|
this.setState({
|
|
_dragNodesKeys: getDragNodesKeys(children, node),
|
|
_expandedKeys: arrDel(_expandedKeys, eventKey),
|
|
})
|
|
this.__emit('dragstart', { event, node })
|
|
},
|
|
|
|
/**
|
|
* [Legacy] Select handler is less small than node,
|
|
* so that this will trigger when drag enter node or select handler.
|
|
* This is a little tricky if customize css without padding.
|
|
* Better for use mouse move event to refresh drag state.
|
|
* But let's just keep it to avoid event trigger logic change.
|
|
*/
|
|
onNodeDragEnter (event, node) {
|
|
const { _expandedKeys: expandedKeys } = this.$data
|
|
const { pos, eventKey } = node
|
|
|
|
if (!this.dragNode || !node.$refs.selectHandle) return
|
|
|
|
const dropPosition = calcDropPosition(event, node)
|
|
|
|
// Skip if drag node is self
|
|
if (
|
|
this.dragNode.eventKey === eventKey &&
|
|
dropPosition === 0
|
|
) {
|
|
this.setState({
|
|
_dragOverNodeKey: '',
|
|
_dropPosition: null,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Ref: https://github.com/react-component/tree/issues/132
|
|
// Add timeout to let onDragLevel fire before onDragEnter,
|
|
// so that we can clean drag props for onDragLeave node.
|
|
// Macro task for this:
|
|
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
|
|
setTimeout(() => {
|
|
// Update drag over node
|
|
this.setState({
|
|
_dragOverNodeKey: eventKey,
|
|
_dropPosition: dropPosition,
|
|
})
|
|
|
|
// Side effect for delay drag
|
|
if (!this.delayedDragEnterLogic) {
|
|
this.delayedDragEnterLogic = {}
|
|
}
|
|
Object.keys(this.delayedDragEnterLogic).forEach((key) => {
|
|
clearTimeout(this.delayedDragEnterLogic[key])
|
|
})
|
|
this.delayedDragEnterLogic[pos] = setTimeout(() => {
|
|
const newExpandedKeys = arrAdd(expandedKeys, eventKey)
|
|
this.setState({
|
|
_expandedKeys: newExpandedKeys,
|
|
})
|
|
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys })
|
|
}, 400)
|
|
}, 0)
|
|
},
|
|
onNodeDragOver (event, node) {
|
|
const { eventKey } = node
|
|
const { _dragOverNodeKey, _dropPosition } = this.$data
|
|
// Update drag position
|
|
if (this.dragNode && eventKey === _dragOverNodeKey && node.$refs.selectHandle) {
|
|
const dropPosition = calcDropPosition(event, node)
|
|
|
|
if (dropPosition === _dropPosition) return
|
|
|
|
this.setState({
|
|
_dropPosition,
|
|
})
|
|
}
|
|
this.__emit('dragover', { event, node })
|
|
},
|
|
onNodeDragLeave (event, node) {
|
|
this.setState({
|
|
_dragOverNodeKey: '',
|
|
})
|
|
this.__emit('dragleave', { event, node })
|
|
},
|
|
onNodeDragEnd (event, node) {
|
|
this.setState({
|
|
_dragOverNodeKey: '',
|
|
})
|
|
this.__emit('dragend', { event, node })
|
|
this.dragNode = null
|
|
},
|
|
onNodeDrop (event, node) {
|
|
const { _dragNodesKeys = [], _dropPosition } = this.$data
|
|
|
|
const { eventKey, pos } = node
|
|
|
|
this.setState({
|
|
_dragOverNodeKey: '',
|
|
})
|
|
|
|
if (_dragNodesKeys.indexOf(eventKey) !== -1) {
|
|
warning(false, 'Can not drop to dragNode(include it\'s children node)')
|
|
return
|
|
}
|
|
|
|
const posArr = posToArr(pos)
|
|
|
|
const dropResult = {
|
|
event,
|
|
node,
|
|
dragNode: this.dragNode,
|
|
dragNodesKeys: _dragNodesKeys.slice(),
|
|
dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
|
|
}
|
|
|
|
if (_dropPosition !== 0) {
|
|
dropResult.dropToGap = true
|
|
}
|
|
this.__emit('drop', dropResult)
|
|
this.dragNode = null
|
|
},
|
|
|
|
onNodeClick (e, treeNode) {
|
|
this.__emit('click', e, treeNode)
|
|
},
|
|
|
|
onNodeDoubleClick (e, treeNode) {
|
|
this.__emit('doubleclick', e, treeNode)
|
|
},
|
|
|
|
onNodeSelect (e, treeNode) {
|
|
let { _selectedKeys: selectedKeys } = this.$data
|
|
const { _keyEntities: keyEntities } = this.$data
|
|
const { multiple } = this.$props
|
|
const { selected, eventKey } = getOptionProps(treeNode)
|
|
const targetSelected = !selected
|
|
// Update selected keys
|
|
if (!targetSelected) {
|
|
selectedKeys = arrDel(selectedKeys, eventKey)
|
|
} else if (!multiple) {
|
|
selectedKeys = [eventKey]
|
|
} else {
|
|
selectedKeys = arrAdd(selectedKeys, eventKey)
|
|
}
|
|
|
|
// [Legacy] Not found related usage in doc or upper libs
|
|
const selectedNodes = selectedKeys.map(key => {
|
|
const entity = keyEntities[key]
|
|
if (!entity) return null
|
|
|
|
return entity.node
|
|
}).filter(node => node)
|
|
|
|
this.setUncontrolledState({ _selectedKeys: selectedKeys })
|
|
|
|
const eventObj = {
|
|
event: 'select',
|
|
selected: targetSelected,
|
|
node: treeNode,
|
|
selectedNodes,
|
|
nativeEvent: e,
|
|
}
|
|
this.__emit('select', selectedKeys, eventObj)
|
|
},
|
|
onNodeCheck (e, treeNode, checked) {
|
|
const { _keyEntities: keyEntities, _checkedKeys: oriCheckedKeys, _halfCheckedKeys: oriHalfCheckedKeys } = this.$data
|
|
const { checkStrictly } = this.$props
|
|
const { eventKey } = getOptionProps(treeNode)
|
|
|
|
// Prepare trigger arguments
|
|
let checkedObj
|
|
const eventObj = {
|
|
event: 'check',
|
|
node: treeNode,
|
|
checked,
|
|
nativeEvent: e,
|
|
}
|
|
|
|
if (checkStrictly) {
|
|
const checkedKeys = checked ? arrAdd(oriCheckedKeys, eventKey) : arrDel(oriCheckedKeys, eventKey)
|
|
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey)
|
|
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }
|
|
|
|
eventObj.checkedNodes = checkedKeys
|
|
.map(key => keyEntities[key])
|
|
.filter(entity => entity)
|
|
.map(entity => entity.node)
|
|
|
|
this.setUncontrolledState({ _checkedKeys: checkedKeys })
|
|
} else {
|
|
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
|
|
checkedKeys: oriCheckedKeys, halfCheckedKeys: oriHalfCheckedKeys,
|
|
})
|
|
|
|
checkedObj = checkedKeys
|
|
|
|
// [Legacy] This is used for `rc-tree-select`
|
|
eventObj.checkedNodes = []
|
|
eventObj.checkedNodesPositions = []
|
|
eventObj.halfCheckedKeys = halfCheckedKeys
|
|
|
|
checkedKeys.forEach((key) => {
|
|
const entity = keyEntities[key]
|
|
if (!entity) return
|
|
|
|
const { node, pos } = entity
|
|
|
|
eventObj.checkedNodes.push(node)
|
|
eventObj.checkedNodesPositions.push({ node, pos })
|
|
})
|
|
|
|
this.setUncontrolledState({
|
|
_checkedKeys: checkedKeys,
|
|
_halfCheckedKeys: halfCheckedKeys,
|
|
})
|
|
}
|
|
this.__emit('check', checkedObj, eventObj)
|
|
},
|
|
onNodeLoad (treeNode) {
|
|
return new Promise((resolve) => {
|
|
// We need to get the latest state of loading/loaded keys
|
|
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
|
|
const { loadData } = this.$props
|
|
const { eventKey } = getOptionProps(treeNode)
|
|
|
|
if (!loadData || loadedKeys.indexOf(eventKey) !== -1 || loadingKeys.indexOf(eventKey) !== -1) {
|
|
return {}
|
|
}
|
|
|
|
// Process load data
|
|
const promise = loadData(treeNode)
|
|
promise.then(() => {
|
|
const newLoadedKeys = arrAdd(this.$data._loadedKeys, eventKey)
|
|
const newLoadingKeys = arrDel(this.$data._loadingKeys, eventKey)
|
|
|
|
// onLoad should trigger before internal setState to avoid `loadData` trigger twice.
|
|
// https://github.com/ant-design/ant-design/issues/12464
|
|
const eventObj = {
|
|
event: 'load',
|
|
node: treeNode,
|
|
}
|
|
this.__emit('load', eventObj)
|
|
this.setUncontrolledState({
|
|
_loadedKeys: newLoadedKeys,
|
|
})
|
|
this.setState({
|
|
_loadingKeys: newLoadingKeys,
|
|
})
|
|
resolve()
|
|
})
|
|
|
|
return {
|
|
_loadingKeys: arrAdd(loadingKeys, eventKey),
|
|
}
|
|
})
|
|
})
|
|
},
|
|
|
|
onNodeExpand (e, treeNode) {
|
|
let { _expandedKeys: expandedKeys } = this.$data
|
|
const { loadData } = this.$props
|
|
const { eventKey, expanded } = getOptionProps(treeNode)
|
|
|
|
// Update selected keys
|
|
const index = expandedKeys.indexOf(eventKey)
|
|
const targetExpanded = !expanded
|
|
|
|
warning(
|
|
(expanded && index !== -1) || (!expanded && index === -1)
|
|
, 'Expand state not sync with index check')
|
|
|
|
if (targetExpanded) {
|
|
expandedKeys = arrAdd(expandedKeys, eventKey)
|
|
} else {
|
|
expandedKeys = arrDel(expandedKeys, eventKey)
|
|
}
|
|
|
|
this.setUncontrolledState({ _expandedKeys: expandedKeys })
|
|
this.__emit('expand', expandedKeys, {
|
|
node: treeNode,
|
|
expanded: targetExpanded,
|
|
nativeEvent: e,
|
|
})
|
|
|
|
// Async Load data
|
|
if (targetExpanded && loadData) {
|
|
const loadPromise = this.onNodeLoad(treeNode)
|
|
return loadPromise ? loadPromise.then(() => {
|
|
// [Legacy] Refresh logic
|
|
this.setUncontrolledState({ _expandedKeys: expandedKeys })
|
|
}) : null
|
|
}
|
|
|
|
return null
|
|
},
|
|
|
|
onNodeMouseEnter (event, node) {
|
|
this.__emit('mouseenter', { event, node })
|
|
},
|
|
|
|
onNodeMouseLeave (event, node) {
|
|
this.__emit('mouseleave', { event, node })
|
|
},
|
|
|
|
onNodeContextMenu (event, node) {
|
|
event.preventDefault()
|
|
this.__emit('rightClick', { event, node })
|
|
},
|
|
|
|
/**
|
|
* Only update the value which is not in props
|
|
*/
|
|
setUncontrolledState (state) {
|
|
let needSync = false
|
|
const newState = {}
|
|
const props = getOptionProps(this)
|
|
Object.keys(state).forEach(name => {
|
|
if (name.replace('_', '') in props) return
|
|
needSync = true
|
|
newState[name] = state[name]
|
|
})
|
|
|
|
if (needSync) {
|
|
this.setState(newState)
|
|
}
|
|
},
|
|
|
|
isKeyChecked (key) {
|
|
const { _checkedKeys: checkedKeys = [] } = this.$data
|
|
return checkedKeys.indexOf(key) !== -1
|
|
},
|
|
|
|
/**
|
|
* [Legacy] Original logic use `key` as tracking clue.
|
|
* We have to use `cloneElement` to pass `key`.
|
|
*/
|
|
renderTreeNode (child, index, level = 0) {
|
|
const {
|
|
_keyEntities: keyEntities,
|
|
_expandedKeys: expandedKeys = [],
|
|
_selectedKeys: selectedKeys = [],
|
|
_halfCheckedKeys: halfCheckedKeys = [],
|
|
_loadedKeys: loadedKeys = [],
|
|
_loadingKeys: loadingKeys = [],
|
|
_dragOverNodeKey: dragOverNodeKey,
|
|
_dropPosition: dropPosition,
|
|
} = this.$data
|
|
const pos = getPosition(level, index)
|
|
const key = child.key || pos
|
|
if (!keyEntities[key]) {
|
|
warnOnlyTreeNode()
|
|
return null
|
|
}
|
|
|
|
return cloneElement(child, {
|
|
props: {
|
|
key,
|
|
eventKey: key,
|
|
expanded: expandedKeys.indexOf(key) !== -1,
|
|
selected: selectedKeys.indexOf(key) !== -1,
|
|
loaded: loadedKeys.indexOf(key) !== -1,
|
|
loading: loadingKeys.indexOf(key) !== -1,
|
|
checked: this.isKeyChecked(key),
|
|
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
|
|
pos,
|
|
|
|
// [Legacy] Drag props
|
|
dragOver: dragOverNodeKey === key && dropPosition === 0,
|
|
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
|
|
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
|
|
},
|
|
})
|
|
},
|
|
},
|
|
|
|
render () {
|
|
const { _treeNode: treeNode } = this.$data
|
|
const {
|
|
prefixCls, focusable,
|
|
showLine, tabIndex = 0,
|
|
} = this.$props
|
|
const domProps = {}
|
|
|
|
return (
|
|
<ul
|
|
{...domProps}
|
|
class={classNames(prefixCls, {
|
|
[`${prefixCls}-show-line`]: showLine,
|
|
})}
|
|
role='tree'
|
|
unselectable='on'
|
|
tabIndex={focusable ? tabIndex : null}
|
|
onKeydown={focusable ? this.onKeydown : () => {}}
|
|
>
|
|
{mapChildren(treeNode, (node, index) => (
|
|
this.renderTreeNode(node, index)
|
|
))}
|
|
</ul>
|
|
)
|
|
},
|
|
}
|
|
|
|
export { Tree }
|
|
|
|
export default proxyComponent(Tree)
|