Commit ccfc025a by Яков

add func renderHeaderColumn

parents
node_modules/
.idea/
lib/
es/
.sass-cache
build
coverage/
\ No newline at end of file
node_modules/
.idea/
.sass-cache
build/
coverage/
\ No newline at end of file
# 0.2.0
- use `componentDidUpdate` to replace `componentWillReceiveProps`
\ No newline at end of file
MIT License
Copyright (c) 2017 Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
.react-finder {
&-container {
display: flex;
background-color: white;
border: solid 1px lightgray;
min-height: 400px;
overflow: auto;
}
&-column {
border-right: solid 1px lightgray;
max-height: 600px;
min-height: inherit;
min-width: 200px;
overflow-y: auto;
}
&-item {
padding: 6px;
cursor: pointer;
font-size: 13px;
&:hover {
background-color: #ecf6fd;
}
&.opened {
background-color: #DEDEDE;
}
&.selected {
background-color: dodgerblue;
color: white;
}
}
&-detail {
align-items: center;
border-right: 0;
display: flex;
flex: 2;
justify-content: center;
padding: 0 1.5em;
}
}
import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom';
import ReactFinder from '../src';
import {mockTree, findInTree, atTreePath} from '../src/utils';
const mockData = mockTree(3);
class Test extends PureComponent {
state = {
dataSource: mockData,
selectedKey: mockData[0].children[0].key,
selectedData: null
}
render() {
const {dataSource} = this.state;
return (
<div>
<ReactFinder
renderHeaderColumn={({parentOpenedKey, openedKey, selectedKey}) => (<div>123</div>)}
dataSource={dataSource}
renderItem={({data, isLeaf}) => (
<span>{data.key} {!isLeaf && <span style={{float: 'right'}}>></span>}</span>
)}
selectedKey={this.state.selectedKey}
onSelect={(selectedKey, {data}) => {
this.setState({selectedKey, selectedData: data});
const nodeData = findInTree(dataSource, (node) => node.key === selectedKey, {withAppendData: true});
if (nodeData) {
console.log('selected node', atTreePath(dataSource, nodeData.loc));
}
}}
/>
selectedKey: {this.state.selectedKey}
</div>
);
}
}
ReactDOM.render(<Test/>, document.getElementById('__react-content'));
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import ReactFinder from '../src';
import { mockTree } from '../src/utils';
class Test extends PureComponent {
state = {
dataSource: mockTree(3)
}
render() {
return (
<div>
<ReactFinder
dataSource={this.state.dataSource}
renderItem={({ data, isLeaf }) => (
<span>{data.key} {!isLeaf && <span style={{ float: 'right' }}>></span>}</span>
)}
draggable
onDragEnd={sortedData => this.setState({ dataSource: sortedData })}
/>
</div>
);
}
}
ReactDOM.render(<Test />, document.getElementById('__react-content'));
{
"name": "react-finder",
"version": "0.2.1",
"description": "",
"main": "./lib/index",
"module": "./es/index",
"homepage": "https://github.com/Frezc/react-finder",
"scripts": {
"build": "rclib-tools run build",
"compile": "rclib-tools run compile --babel-runtime",
"gh-pages": "rclib-tools run gh-pages",
"start": "rclib-tools run server",
"pub": "npm run lint:fix && rclib-tools run pub --babel-runtime",
"lint": "rclib-tools run lint",
"lint:fix": "rclib-tools run lint --fix",
"test": "jest",
"coverage": "jest --coverage",
"pub-d": "npm run lint:fix && rclib-tools run publish --babel-runtime",
"precommit": "npm run lint:fix",
"prepublishOnly": "rclib-tools run guard",
"coverage:upload": "jest --coverage && cat ./coverage/lcov.info | coveralls",
"clean": "rclib-tools run clean",
"init": "rclib-tools run init",
"update-snapshot": "jest -u",
"preinstall": "npx npm-force-resolutions"
},
"keywords": [],
"repository": {
"type": "git",
"url": "git@github.com:Frezc/react-finder.git"
},
"bugs": {
"url": "http://github.com/Frezc/react-finder/issues"
},
"files": [
"assets/*.css",
"assets/*.png",
"assets/*.gif",
"dist",
"es",
"lib"
],
"author": "frezc",
"license": "MIT",
"config": {
"port": 8000
},
"devDependencies": {
"enzyme": "^3.1.0",
"enzyme-adapter-react-15": "^1.0.2",
"enzyme-to-json": "^3.1.4",
"jest": "^21.2.0",
"pre-commit": "1.x",
"rclib-tools": "^0.1.13",
"react-dom": "^15.6.1"
},
"dependencies": {
"lodash": "^4.17.4",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-sortable-hoc": "^0.6.7"
},
"pre-commit": [
"lint"
],
"jest": {
"setupFiles": [
"./tests/setup.js"
],
"collectCoverageFrom": [
"src/**/*"
],
"transform": {
"\\.tsx?$": "./node_modules/rclib-tools/scripts/jestPreprocessor.js",
"\\.jsx?$": "./node_modules/rclib-tools/scripts/jestPreprocessor.js"
},
"snapshotSerializers": [
"enzyme-to-json/serializer"
],
"moduleNameMapper": {
"^.+\\.(css|less|scss)$": "babel-jest"
}
},
"lib-tools-config": {
"disable_update": true
},
"resolutions": {
"graceful-fs": "^4.2.4"
}
}
Implement finder in react.
Base on react, react-sortable-hoc and some lodash helper.
## example
http://frezc.github.io/react-finder/examples/
## TODO
- [ ] complete doc.
- [ ] support function data source like finder.js
- [ ] support drag across level
- [ ] add simple finder Component like finder.js
- [ ] write test
/**
* Created by wanli on 2017/9/15.
*/
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Item from './FinderItem';
import HeaderColumn from './FinderColumnHeader';
import {SortableContainer as sortableContainer} from 'react-sortable-hoc';
class FinderColumn extends PureComponent {
static propTypes = {
nodeKey: PropTypes.string,
dataSource: PropTypes.arrayOf(PropTypes.object),
selectedKey: PropTypes.any,
openedKey: PropTypes.any,
parentOpenedKey: PropTypes.any,
renderItem: PropTypes.func,
renderHeaderColumn: PropTypes.func,
onSelect: PropTypes.func,
checkIsLeaf: PropTypes.func.isRequired,
shouldDragDisabled: PropTypes.func
}
static defaultProps = {
dataSource: [],
selectedKey: null,
isLast: false
}
render() {
const {
dataSource,
selectedKey,
renderItem,
nodeKey,
openedKey,
onSelect,
checkIsLeaf,
parentOpenedKey,
renderHeaderColumn,
shouldDragDisabled
} = this.props;
return (
<div className="react-finder-column">
{renderHeaderColumn ?
<HeaderColumn
renderHeaderColumn={renderHeaderColumn}
parentOpenedKey={parentOpenedKey}
openedKey={openedKey}
selectedKey={selectedKey}
/> : ''}
{dataSource && dataSource.map((data, index) => (
<Item
disabled={shouldDragDisabled && shouldDragDisabled({data})}
key={data[nodeKey]}
index={index}
data={data}
render={renderItem}
selected={selectedKey === data[nodeKey]}
opened={openedKey === data[nodeKey]}
onSelect={onSelect}
checkIsLeaf={checkIsLeaf}
/>
))}
</div>
);
}
}
export default sortableContainer(FinderColumn);
/**
* Created by wanli on 2017/9/15.
*/
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Item from './FinderItem';
import {SortableContainer as sortableContainer} from 'react-sortable-hoc';
class FinderColumnHeader extends PureComponent {
static propTypes = {
nodeKey: PropTypes.string,
dataSource: PropTypes.arrayOf(PropTypes.object),
selectedKey: PropTypes.any,
openedKey: PropTypes.any,
parentOpenedKey: PropTypes.any,
}
static defaultProps = {
dataSource: [],
selectedKey: null,
isLast: false
}
render() {
const {
selectedKey,
openedKey,
renderHeaderColumn,
parentOpenedKey
} = this.props;
return (
<div className="react-finder-column-header">
{renderHeaderColumn({parentOpenedKey, openedKey, selectedKey})}
</div>
);
}
}
export default sortableContainer(FinderColumnHeader);
/**
* Created by wanli on 2017/9/16.
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { SortableElement as sortableElement } from 'react-sortable-hoc';
class FinderItem extends PureComponent {
static propTypes = {
render: PropTypes.func,
data: PropTypes.object,
// 0: false, 1: remaining, 2: true
selected: PropTypes.bool,
opened: PropTypes.bool,
onSelect: PropTypes.func,
checkIsLeaf: PropTypes.func.isRequired
}
static defaultProps = {
data: {},
selected: false,
opened: false
}
renderDefault() {
return (
<span>{this.props.data.name} <span>{'>'}</span></span>
);
}
render() {
const { data, render, onSelect, selected, opened, checkIsLeaf } = this.props;
const classList = ['react-finder-item'];
if (selected) classList.push('selected');
else if (opened) classList.push('opened');
return (
<div className={classList.join(' ')} onClick={() => onSelect && onSelect({ data })}>
{render ? render({ data, isLeaf: checkIsLeaf(data) }) : this.renderDefault()}
</div>
);
}
}
export default sortableElement(FinderItem);
/* eslint no-loop-func:0 */
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Column from './FinderColumn';
import {findInTree, atTreePath} from './utils';
import {arrayMove} from 'react-sortable-hoc';
import find from 'lodash/find';
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
class ReactFinder extends PureComponent {
static propTypes = {
nodeKey: PropTypes.string, // 某一个节点的唯一key的标识符
dataSource: PropTypes.arrayOf(PropTypes.object), // 数据源
childrenPropName: PropTypes.string, // 子节点参数的名称
renderItem: PropTypes.func, // ({ data, isLeaf }) => ReactElement
renderHeaderColumn: PropTypes.func,
renderDetail: PropTypes.func, // ({ data, isLeaf }) => ReactElement
isLeaf: PropTypes.func, // check if is leaf node. (data) => bool
selectedKey: PropTypes.any,
defaultSelectedKey: PropTypes.any,
shouldDragDisabled: PropTypes.func, // (data) => bool
style: PropTypes.object,
draggable: PropTypes.bool,
onDragEnd: PropTypes.func,
onSelect: PropTypes.func, // (selectedKey, { data }) => {}
// ({ column, index }) => Object
sortableContainerProps: PropTypes.oneOfType([PropTypes.object, PropTypes.func])
}
static defaultProps = {
nodeKey: 'key',
dataSource: [],
childrenPropName: 'children',
isLeaf: null,
style: {},
sortableContainerProps: {},
draggable: false
}
state = {
selectedKey: null,
openedKeys: []
}
constructor(props) {
super(props);
this.state.selectedKey = props.selectedKey || props.defaultSelectedKey;
this.state.openedKeys = this.calcOpenedKeysBySelectedKey(this.state.selectedKey);
}
/**
* compatible react@15 & react@16
* todo: write in [getDerivedStateFromProps](https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops)
*/
componentDidUpdate() {
const {selectedKey} = this.props;
if ('selectedKey' in this.props && selectedKey !== this.state.selectedKey) {
/* eslint-disable react/no-did-update-set-state */
this.setState({
selectedKey,
openedKeys: this.calcOpenedKeysBySelectedKey(selectedKey)
});
/* eslint-enable react/no-did-update-set-state */
}
}
getRenderColumns() {
const {dataSource, nodeKey, childrenPropName} = this.props;
const {openedKeys} = this.state;
const result = [dataSource];
openedKeys.forEach((key, i) => {
const openedNode = find(result[i], node => node[nodeKey] === key);
if (openedNode) result.push(openedNode[childrenPropName]);
});
return result;
}
checkIsLeaf = (data) => {
const {isLeaf, childrenPropName} = this.props;
if (isLeaf) {
return isLeaf(data);
}
return !data[childrenPropName];
}
calcOpenedKeysBySelectedKey(selectedKey) {
const {dataSource, childrenPropName, nodeKey} = this.props;
if (!selectedKey) return [];
const loc = findInTree(
dataSource,
node => node[nodeKey] === selectedKey,
{childrenName: childrenPropName, withAppendData: true}
).loc;
const nodeList = atTreePath(dataSource, loc, {childrenName: childrenPropName});
if (nodeList.length > 0 && this.checkIsLeaf(nodeList[nodeList.length - 1])) nodeList.pop();
return nodeList.map(node => node[nodeKey]);
}
shouldDragDisabled = (...params) => {
const {draggable, shouldDragDisabled} = this.props;
if (!draggable) return true;
return shouldDragDisabled && shouldDragDisabled(...params);
}
handleSelect(data) {
const {onSelect, nodeKey} = this.props;
if (!('selectedKey' in this.props)) {
this.setState({
selectedKey: data[nodeKey]
});
}
if (onSelect) onSelect(data[nodeKey], {data});
}
renderDetail() {
const {renderDetail, dataSource, nodeKey, childrenPropName} = this.props;
const {selectedKey} = this.state;
const el = renderDetail && renderDetail({
selectedKey,
selectedData: findInTree(
dataSource,
n => n[nodeKey] === selectedKey, {childrenName: childrenPropName}
)
});
if (el) {
return (
<div className="react-finder-detail">
{el}
</div>
);
}
return null;
}
render() {
const {
renderItem, nodeKey, style, sortableContainerProps, onDragEnd, dataSource, childrenPropName, renderHeaderColumn
} = this.props;
const {selectedKey, openedKeys} = this.state;
const columns = this.getRenderColumns();
return (
<div className="react-finder react-finder-container" style={style}>
{columns.map((col, i) => {
const appendProps = typeof sortableContainerProps === 'function' ?
sortableContainerProps({column: col, index: i}) : sortableContainerProps;
return (
<Column
renderHeaderColumn={renderHeaderColumn}
key={i}
dataSource={col}
distance={8}
selectedKey={selectedKey}
parentOpenedKey={openedKeys[i - 1]}
openedKey={openedKeys[i]}
renderItem={renderItem}
nodeKey={nodeKey}
onSelect={({data}) => {
const newOpenKeys = openedKeys.slice(0, i);
// if not leaf, add to openedKeys
if (!this.checkIsLeaf(data)) newOpenKeys.push(data[nodeKey]);
this.setState({openedKeys: newOpenKeys});
this.handleSelect(data);
}}
checkIsLeaf={this.checkIsLeaf}
shouldDragDisabled={this.shouldDragDisabled}
onSortEnd={({oldIndex, newIndex}) => {
let sortedData;
if (i <= 0) sortedData = arrayMove(col, oldIndex, newIndex);
else {
const keys = openedKeys.slice(0, i - 1);
sortedData = cloneDeep(dataSource);
let acc = sortedData;
for (let j = 0; j < keys.length; j++) {
const key = keys[j];
const node = find(acc, data => data[nodeKey] === key);
if (!node) {
console.error(`can not find key ${key} in`, acc);
return;
}
acc = node[childrenPropName];
}
set(
find(acc, data => data[nodeKey] === openedKeys[i - 1]),
childrenPropName,
arrayMove(col, oldIndex, newIndex)
);
}
if (onDragEnd) onDragEnd(sortedData, {oldIndex, newIndex, column: col, index: i});
}}
{...appendProps}
/>
);
})}
{this.renderDetail()}
</div>
);
}
}
export default ReactFinder;
/**
* Created by wanli on 2017/9/15.
*/
import ReactFinder from './ReactFinder';
import '../assets/styles.scss';
export { SortableElement, SortableContainer, SortableHandle, arrayMove } from 'react-sortable-hoc';
export default ReactFinder;
import isEmpty from 'lodash/isEmpty';
function mockArray(maxLength) {
return Array.apply(null, new Array(Math.floor(Math.random() * maxLength)));
}
const noop = () => {
};
export function mockTree(deep = 3) {
if (deep > 0) {
return mockArray(100).map((_, i) => ({
key: `${deep}-${i}-${Math.random()}`,
children: mockTree(deep - 1)
}));
}
}
/**
* 遍历树,DFS
* @param tree array e.g. [{ a: 1, children: [{ a: 2, children: [] }, ...] }, ...]
* @param callback function (node, loc, parentNode) => {} return false explicitly will end travel.
* @param options object { childrenName, parentLoc }
* @return boolean if travel to end
*/
export function travelTree(tree, callback = noop, options = {}) {
const {childrenName = 'children', parentLoc = [], parentNode = null} = options;
if (!tree) return false;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
const curLoc = parentLoc.concat(i);
if (callback(node, curLoc, parentNode) === false) {
return false;
}
if (!isEmpty(node[childrenName])) {
if (
travelTree(
node[childrenName], callback, {...options, parentLoc: curLoc, parentNode: node}
) === false
) {
return false;
}
}
}
return true;
}
/**
* 在树中找到某个节点
* @param tree
* @param predicate function (node, loc, parentNode) => bool
* @param options see travelTree
*/
export function findInTree(tree, predicate = noop, options = {}) {
const {withAppendData = false} = options;
let result = {loc: []};
travelTree(tree, (node, loc, parentNode) => {
if (predicate(node, loc, parentNode)) {
result = {node, loc, parentNode};
return false;
}
return true;
}, options);
if (withAppendData) return result;
return result.node;
}
/**
* 根据位置信息得到路径上的所有节点
* @param tree
* @param loc
* @param options
*/
export function atTreePath(tree, loc = [], options = {}) {
const {childrenName = 'children'} = options;
const result = [];
for (let i = 0; i < loc.length; i++) {
const index = loc[i];
let targetList = tree;
if (result.length > 0) targetList = result[result.length - 1][childrenName];
result.push(targetList[index]);
}
return result;
}
import React from 'react';
import { render } from 'enzyme';
import ReactFinder from '../src';
describe('ReactFinder', () => {
const dataSource = [
{
key: '1', children: [
{ key: '1-1' },
{ key: '1-2' },
{ key: '1-3', children: [{ key: '1-3-1' }] },
{ key: '1-4' },
{ key: '1-5' }
]
},
{ key: '2' },
{ key: '3' },
{ key: '4', children: [{ key: '4-1' }] },
{ key: '5' }
];
function createFinder(props = {}) {
return (
<ReactFinder
dataSource={dataSource}
renderItem={({ data, isLeaf }) => (
<span>{data.key} {!isLeaf && <span style={{ float: 'right' }}>></span>}</span>
)}
{...props}
/>
);
}
it('renders correctly', () => {
expect(render(createFinder({}))).toMatchSnapshot();
expect(render(createFinder({ defaultSelectedKey: '1-3-1' }))).toMatchSnapshot();
expect(render(createFinder({ selectedKey: '4-1' }))).toMatchSnapshot();
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReactFinder renders correctly 1`] = `
<div
class="react-finder react-finder-container"
>
<div
class="react-finder-column"
>
<div
class="react-finder-item"
>
<span>
1
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
2
</span>
</div>
<div
class="react-finder-item"
>
<span>
3
</span>
</div>
<div
class="react-finder-item"
>
<span>
4
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
5
</span>
</div>
</div>
</div>
`;
exports[`ReactFinder renders correctly 2`] = `
<div
class="react-finder react-finder-container"
>
<div
class="react-finder-column"
>
<div
class="react-finder-item opened"
>
<span>
1
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
2
</span>
</div>
<div
class="react-finder-item"
>
<span>
3
</span>
</div>
<div
class="react-finder-item"
>
<span>
4
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
5
</span>
</div>
</div>
<div
class="react-finder-column"
>
<div
class="react-finder-item"
>
<span>
1-1
</span>
</div>
<div
class="react-finder-item"
>
<span>
1-2
</span>
</div>
<div
class="react-finder-item opened"
>
<span>
1-3
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
1-4
</span>
</div>
<div
class="react-finder-item"
>
<span>
1-5
</span>
</div>
</div>
<div
class="react-finder-column"
>
<div
class="react-finder-item selected"
>
<span>
1-3-1
</span>
</div>
</div>
</div>
`;
exports[`ReactFinder renders correctly 3`] = `
<div
class="react-finder react-finder-container"
>
<div
class="react-finder-column"
>
<div
class="react-finder-item"
>
<span>
1
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
2
</span>
</div>
<div
class="react-finder-item"
>
<span>
3
</span>
</div>
<div
class="react-finder-item opened"
>
<span>
4
<span
style="float:right;"
>
&gt;
</span>
</span>
</div>
<div
class="react-finder-item"
>
<span>
5
</span>
</div>
</div>
<div
class="react-finder-column"
>
<div
class="react-finder-item selected"
>
<span>
4-1
</span>
</div>
</div>
</div>
`;
const Adapter = require('enzyme-adapter-react-15');
require('enzyme').configure({ adapter: new Adapter() });
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment