forked from enviPath/enviPy
102 lines
2.8 KiB
JavaScript
102 lines
2.8 KiB
JavaScript
/****************************************************************************
|
|
* Copyright 2017 EPAM Systems
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
***************************************************************************/
|
|
|
|
import { h, Component } from 'preact';
|
|
/** @jsx h */
|
|
|
|
const STYLE_INNER = 'position:relative; overflow:hidden; width:100%; min-height:100%;';
|
|
|
|
const STYLE_CONTENT = 'position:absolute; top:0; left:0; height:100%; width:100%; overflow:visible;';
|
|
|
|
export default class VirtualList extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.state = {
|
|
offset: 0,
|
|
height: 0
|
|
};
|
|
}
|
|
|
|
resize = (ev, reset) => {
|
|
const height = this.base.offsetHeight;
|
|
|
|
if (this.state.height !== height) {
|
|
this.setState({ height });
|
|
}
|
|
|
|
if (reset) {
|
|
this.setState({offset: 0});
|
|
this.base.scrollTop = 0;
|
|
}
|
|
};
|
|
|
|
handleScroll = () => {
|
|
this.setState({ offset: this.base.scrollTop });
|
|
if (this.props.sync) this.forceUpdate();
|
|
};
|
|
|
|
componentDidUpdate({data}) {
|
|
const equal = (data.length === this.props.data.length &&
|
|
this.props.data.every((v, i)=> v === data[i]));
|
|
|
|
this.resize(null, !equal);
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.resize();
|
|
addEventListener('resize', this.resize);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
removeEventListener('resize', this.resize);
|
|
}
|
|
|
|
render() {
|
|
const { data, rowHeight, children, Tag="div", overscanCount=1, sync, ...props } = this.props;
|
|
const { offset, height } = this.state;
|
|
|
|
// first visible row index
|
|
let start = (offset / rowHeight) || 0;
|
|
const renderRow = children[0];
|
|
|
|
// actual number of visible rows (without overscan)
|
|
let visibleRowCount = (height / rowHeight) || 0;
|
|
|
|
// Overscan: render blocks of rows modulo an overscan row count
|
|
// This dramatically reduces DOM writes during scrolling
|
|
if (overscanCount) {
|
|
start = Math.max(0, start - (start % overscanCount));
|
|
visibleRowCount += overscanCount;
|
|
}
|
|
|
|
// last visible + overscan row index
|
|
const end = start + 1 + visibleRowCount;
|
|
|
|
// data slice currently in viewport plus overscan items
|
|
let selection = data.slice(start, end);
|
|
|
|
return (
|
|
<div onScroll={this.handleScroll} {...props}>
|
|
<div style={`${STYLE_INNER} height:${data.length*rowHeight}px;`}>
|
|
<Tag style={`${STYLE_CONTENT} top:${start*rowHeight}px;`}>
|
|
{ selection.map((d, i) => renderRow(d, start + i)) }
|
|
</Tag>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|