UNPKG

@ustack/uskin

Version:

A graceful framework which provides developers another chance to build an amazing site.

678 lines (584 loc) 19 kB
import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; import styles from '../../mixins/styles'; import createClassName from '../../mixins/createClassName'; const DOC = document; const FILTER_ID = 'uskin-filter-container'; function noop() {} class Table extends React.Component { constructor(props) { super(props); this.state = { data: this.props.data, loading: this.props.loading, // shift键多选 start: '', shift: false, checkedKey: {}, sortCol: undefined, sortDirection: undefined, filterColKey: {}, filterBy: undefined, colWidth: new Array(props.column.length) }; ['resizeScrollCol', 'onCheck', 'check', 'filter', 'sort', 'onResizeColumn'].forEach((func) => { this[func] = this[func].bind(this); }); } componentWillMount() { const props = this.props; let initChecked = this.props.checkboxInitialize; if (props.checkbox && initChecked) { let key = props.dataKey; let checkedKey = {}; props.data.forEach((item) => { if (initChecked(item)) { checkedKey[item[key]] = true; } }); this.setState({ checkedKey: checkedKey }); } } componentDidMount() { window.addEventListener('resize', this.resizeScrollCol); this.resizeScrollCol(); } componentDidUpdate() { this.resizeScrollCol(); } componentWillUnmount() { window.removeEventListener('resize', this.resizeScrollCol); } componentWillReceiveProps(nextProps) { let data = this.getProcessedData(nextProps.data, nextProps.dataKey); this.setState({ data: data, loading: nextProps.loading }); } resizeScrollCol() { const theadDOM = this.refs.thead; const tbodyDOM = this.refs.tbody; let colLength = this.props.column.length + (this.props.checkbox ? 1 : 0); if (tbodyDOM.scrollHeight > tbodyDOM.clientHeight) { if (colLength === theadDOM.childNodes.length) { let scrollCol = DOC.createElement('div'); let width = (tbodyDOM.offsetWidth - tbodyDOM.clientWidth) + 'px'; let colStyle = scrollCol.style; colStyle.flex = 1; colStyle.width = width; colStyle.maxWidth = width; colStyle.minWidth = width; colStyle.padding = 0; theadDOM.appendChild(scrollCol); } } else if (colLength < this.refs.thead.childNodes.length) { theadDOM.removeChild(theadDOM.lastChild); } } onShiftKey(index, e) { const state = this.state; const shift = state.shift; this.setState({ shift: e.shiftKey }); if(e.shiftKey && typeof index === 'number') { this.setState({ // 如果shift为true, 说明上次点击shift被按下,start不用更新, 反之更新start. start: shift ? state.start : index }); } else { // 点击的是all, 所以清空start. this.setState({ start: '' }); } } //checkbox onChange onCheck(index, e) { const state = this.state; let key = e.target.value; let isChecked = e.target.checked; let checkedKeys = this.state.checkedKey; let newCheckedKeys = checkedKeys; if (key === 'null') { newCheckedKeys = {}; if (isChecked) { let dataKey = this.props.dataKey; state.data.forEach((item) => { newCheckedKeys[item[dataKey]] = true; }); } } else if (isChecked) { newCheckedKeys[key] = true; // 点击的同时shift键被按下 if(state.shift && e.target.checked) { // 按大小排列start和end let start, end; if(state.start > index) { start = index; end = state.start; } else { start = state.start; end = index; } const dataKey = this.props.dataKey; state.data.forEach((item, i) => { // 在start和end中的全部选中 if(i >= start && i <= end) { newCheckedKeys[item[dataKey]] = true; } }); } } else { delete newCheckedKeys[key]; } //set state this.check(newCheckedKeys); //deliver selected data to outside table let checkedItem; let dataKey = this.props.dataKey; let arr = []; this.state.data.forEach((item) => { if ('' + key === '' + item[dataKey]) { checkedItem = item; } if (newCheckedKeys[item[dataKey]]) { arr.push(item); } }); this.props.checkboxOnChange(isChecked, checkedItem, arr); //deliver data when all checkbox is clicked if (key === 'null') { this.props.checkboxOnChangeAll(isChecked, isChecked ? this.state.data : []); } } //filter onClick onFilter(column, e) { e.stopPropagation(); e.nativeEvent.stopImmediatePropagation(); //create a filter, if it exists, destroy it const prevFilter = DOC.getElementById(FILTER_ID); if (prevFilter) { this.destroyFilter(); } this.createFilter(column, e); //destroy filter listener DOC.addEventListener('click', this.destroyFilter, false); } destroyFilter() { const filter = DOC.getElementById(FILTER_ID); if (filter) { let root = filter.parentNode; ReactDOM.unmountComponentAtNode(filter); root.removeChild(filter); } DOC.removeEventListener('click', this.destroyFilter, false); } createFilter(column, e) { let root = e.target.parentNode; const state = this.state; let filterColKey = state.filterColKey; let filterBy = state.filterBy; const filter = ( <div className="filter" data-key={column.key}> <div className={!filterBy ? 'selected' : null} key="null" onClick={this.filter.bind(this, {}, undefined)}> <span>{column.filterAll ? column.filterAll : 'All'}</span> { !filterBy ? <i className="glyphicon icon-active-yes" /> : null } </div> { column.filter.map((item) => { var selected = filterColKey[column.key] && (filterBy === item.filterBy), newFilterCol = {}; newFilterCol[column.key] = true; return ( <div className={selected ? 'selected' : null} key={item.key} onClick={this.filter.bind(this, newFilterCol, item.filterBy)}> <span>{item.name}</span> { selected ? <i className="glyphicon icon-active-yes" data-value={item.key} /> : null } </div> ); }) } </div> ); let container = document.createElement('div'); container.id = FILTER_ID; root.appendChild(container); ReactDOM.render(filter, container); } getFilteredData(columnKeys, filterBy, data) { let newData = data; if (filterBy) { const props = this.props; let columns = props.column.filter((column) => columnKeys[column.key]); //filterBy can be function or string if (typeof filterBy === 'function') { newData = data.filter((item) => filterBy(item, columns)); } else if (typeof filterBy === 'string') { newData = data.filter((item) => columns.some((column) => item[column.dataIndex] === filterBy) ); } } return newData; } //sort onClick onSort(column, direction, e) { e.stopPropagation(); let filterNode = e.target.parentNode.parentNode; if (filterNode.parentNode.getAttribute('id') !== FILTER_ID && filterNode.getAttribute('id') !== FILTER_ID) { if (this.shouldClearSort(column, direction)) { this.state.data = null; let state = this.state; let data = this.getFilteredData(state.filterColKey, state.filterBy, this.props.data); this.setState({ sortCol: undefined, sortDirection: direction, data: data }); } else { this.sort(column, direction); } } } //sort helper shouldClearSort(column, direction) { const state = this.state; let prevCol = state.sortCol; let prevDir = state.sortDirection; return (column === prevCol && direction === prevDir); } getSortedData(column, direction, data) { const props = this.props; let propsData = props.data; let dataKey = props.dataKey; if (data === propsData) { data = Object.assign([], propsData); } data.sort((item1, item2) => { let ret = this.sortData(item1, item2, column.sortBy, column.dataIndex); return ret !== 0 ? ret * direction : this.sortByKey(item1, item2, dataKey); }); return data; } sortData(item1, item2, sortBy, key) { if (typeof sortBy === 'function') { return sortBy(item1, item2); } else { return this.sortByDefaultType(item1[key], item2[key], sortBy); } } sortByDefaultType(a, b, sortType) { if (typeof a === 'undefined') { return 1; } else if (typeof b === 'undefined') { return -1; } switch (sortType) { case 'number': return a - b; case 'boolean': return (a === b) ? 0 : a; case 'date': return new Date(a) - new Date(b); default: return a.localeCompare(b); } } sortByKey(a, b, key) { return a[key] > b[key] ? 1 : -1; } //resizable column onResizeColumn(colIndex, e) { let getCol = (i) => (this.refs['col-' + i]); let table = this.refs.table; let thead = this.refs.thead; let tbody = this.refs.tbody; let line = document.createElement('div'); let col = getCol(colIndex); let nextCol = getCol(colIndex + 1); let startX = col.getBoundingClientRect().left + col.offsetWidth; let startXMouse = e.clientX; let startY = col.getBoundingClientRect().top; let minWidth = 32; let minX = col.getBoundingClientRect().left + minWidth; let maxX = nextCol.getBoundingClientRect().left + nextCol.offsetWidth - minWidth; let tableHeight = thead.clientHeight + tbody.clientHeight; line.className = 'resize-column-line'; let lineStyle = line.style; lineStyle.height = tableHeight + 'px'; lineStyle.left = (startX - 1) + 'px'; lineStyle.top = startY + 'px'; let bodyStyle = document.body.style; bodyStyle['user-select'] = 'none'; bodyStyle['-moz-user-select'] = 'none'; bodyStyle['-webkit-user-select'] = 'none'; bodyStyle['-ms-user-select'] = 'none'; table.appendChild(line); function getValidX (value) { if (value < minX) { return minX; } else if (value > maxX) { return maxX; } return value; } let onDrag = (evt) => { let offset = startXMouse - evt.clientX; lineStyle.left = getValidX(startX - offset) + 'px'; }; let endDrag = (evt) => { let endXMouse = getValidX(evt.clientX); let offset = startXMouse - endXMouse; let colWidth = this.state.colWidth; colWidth[colIndex] = col.offsetWidth - offset; colWidth[colIndex + 1] = nextCol.offsetWidth + offset; this.setState({ colWidth: colWidth }); bodyStyle['user-select'] = ''; table.removeChild(line); document.removeEventListener('mousemove', onDrag, true); document.removeEventListener('mouseup', endDrag, true); }; document.addEventListener('mouseup', endDrag, true); document.addEventListener('mousemove', onDrag, true); } //public function check(checkedKey) { this.setState({ checkedKey: checkedKey }); } filter(columnKeys, filterBy, e) { let data = this.props.data, state = this.state; if (!this.props.getData) { data = this.getFilteredData(columnKeys, filterBy, data); } else { this.props.getData(columnKeys, filterBy); } //check sort if (this.state.sortCol) { data = this.getSortedData(state.sortCol, state.sortDirection, data); } this.setState({ filterColKey: columnKeys, filterBy: filterBy, data: data }); //check checkbox this.check(this.state.checkedKey); } sort(column, direction) { let data = this.state.data; if (!column) { data = this.props.data; } data = this.getSortedData(column, direction, data); this.setState({ sortCol: column, sortDirection: direction, data: data }); } setData(data) { this.setState({ data: this.getProcessedData(data, this.props.dataKey) }); } getProcessedData(oldData, key) { let data = oldData; const state = this.state; //check filter and sort if (!state.filterBy && state.sortCol) { data = Object.assign([], oldData); } if (state.filterBy) { data = this.getFilteredData(state.filterColKey, state.filterBy, data); } if (state.sortCol) { data = this.getSortedData(state.sortCol, state.sortDirection, data); } return data; } loading(status) { this.setState({ loading: status }); } //clear all the state except loading clearState() { this.setState({ data: this.props.data, checkedKey: {}, sortCol: undefined, sortDirection: undefined, filterColKey: {}, filterBy: undefined }); } //render helper getFixedWidth(width) { return { width: width, minWidth: width, maxWidth: width }; } checkedAll(data, key, checked) { return data.length > 0 && !data.some((item) => !checked[item[key]]); } render() { const props = this.props; const state = this.state; let dataKey = props.dataKey; let loading = state.loading; let style = styles.getWidth(props.width); let className = createClassName({ default: 'table', prefix: 'table-', props: { mini: props.mini, hover: props.hover, striped: props.striped } }); let checkedAll = this.checkedAll(state.data, dataKey, state.checkedKey); return ( <div ref="table" style={style} className={className}> <div ref="thead" className="table-header" onClick={this.tableHeadOnClick}> { props.checkbox ? <div key="checkbox" className="checkbox"> <input value="null" onMouseDown={this.onShiftKey.bind(this, null)} onChange={this.onCheck.bind(this, null)} type="checkbox" checked={checkedAll} /> </div> : null } { props.column.map((col, index) => { let isSorted = (col === state.sortCol); let nextDir = (isSorted && state.sortDirection) ? state.sortDirection * -1 : 1; let ref = 'col-' + index; let colStyle = {}; if (state.colWidth[index]) { colStyle = this.getFixedWidth(state.colWidth[index]); } else if (col.width) { colStyle = this.getFixedWidth(col.width); } return ( <div key={col.key} ref={ref} style={colStyle} className={col.sortBy ? 'sortable' : null}> { index < props.column.length - 1 ? <div className="drag-line" onMouseDown={this.onResizeColumn.bind(this, index)} /> : null } { col.sortBy ? <div className="sort-box"> <span className={'sort-up' + (isSorted && (state.sortDirection === 1) ? ' selected' : '')} onClick={this.onSort.bind(this, col, 1)} > <span className="arrow-up" data-value={col.key} data-direction="up" /> </span> <span className={'sort-down' + (isSorted && (state.sortDirection === -1) ? ' selected' : '')} onClick={this.onSort.bind(this, col, -1)} > <span className="arrow-down" data-value={col.key} data-direction="down" /> </span> </div> : null } <span className="title" onClick={col.sortBy && this.onSort.bind(this, col, nextDir)}> {col.title} </span> { col.filter ? <div className="filter-box"> <div className="filter-icon" onClick={this.onFilter.bind(this, col)} /> </div> : null } </div> ); }) } </div> { loading ? <div className="loading-data"> <i className="glyphicon icon-loading" /> </div> : null } <div ref="tbody" style={{display: loading ? 'none' : 'block'}} className="table-body"> { state.data.map((item, index) => { let key = item[dataKey]; let checked = !!state.checkedKey[key]; return ( <div key={key} className={'row' + (checked ? ' selected' : '')}> { props.checkbox ? <div className="checkbox"> <input value={key} onMouseDown={this.onShiftKey.bind(this, index)} onChange={this.onCheck.bind(this, index)} type="checkbox" checked={checked} /> </div> : null } { props.column.map((col, colIndex) => { let cellStyle = {}; if (state.colWidth[colIndex]) { cellStyle = this.getFixedWidth(state.colWidth[colIndex]); } else if (col.width) { cellStyle = this.getFixedWidth(col.width); } return ( <div key={col.key} ref={'row-' + index + '-col-' + colIndex} style={cellStyle}> {col.render ? col.render(col, item, index) : item[col.dataIndex]} </div> ); }) } </div> ); }) } </div> </div> ); } } Table.propTypes = { columns: PropTypes.arrayOf(PropTypes.object), data: PropTypes.arrayOf(PropTypes.object), checkboxOnChange: PropTypes.func, checkboxOnChangeAll: PropTypes.func }; Table.defaultProps = { columns: [], data: [], checkboxOnChange: noop, checkboxOnChangeAll: noop }; export default Table;