2023-01-19 19:19:06 +02:00
|
|
|
import * as React from 'react';
|
2024-04-25 16:31:18 +02:00
|
|
|
import { DragEventHandler, KeyboardEventHandler, UIEventHandler } from 'react';
|
2023-01-19 19:19:06 +02:00
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
interface Props<ItemType> {
|
|
|
|
style: React.CSSProperties & { height: number };
|
2023-01-19 19:19:06 +02:00
|
|
|
itemHeight: number;
|
2024-04-25 16:31:18 +02:00
|
|
|
items: ItemType[];
|
2023-01-29 15:11:23 +02:00
|
|
|
disabled?: boolean;
|
2024-04-25 16:31:18 +02:00
|
|
|
onKeyDown?: KeyboardEventHandler<HTMLElement>;
|
|
|
|
itemRenderer: (item: ItemType, index: number)=> React.JSX.Element;
|
2023-01-29 15:11:23 +02:00
|
|
|
className?: string;
|
2024-04-25 16:31:18 +02:00
|
|
|
onItemDrop?: DragEventHandler<HTMLElement>;
|
2024-08-05 20:37:23 +02:00
|
|
|
|
|
|
|
id?: string;
|
|
|
|
role?: string;
|
|
|
|
'aria-label'?: string;
|
2023-01-19 19:19:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
topItemIndex: number;
|
|
|
|
bottomItemIndex: number;
|
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
class ItemList<ItemType> extends React.Component<Props<ItemType>, State> {
|
2023-01-19 19:19:06 +02:00
|
|
|
|
|
|
|
private scrollTop_: number;
|
2024-04-25 16:31:18 +02:00
|
|
|
private listRef: React.MutableRefObject<HTMLDivElement>;
|
2017-11-05 02:17:48 +02:00
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public constructor(props: Props<ItemType>) {
|
2023-01-19 19:19:06 +02:00
|
|
|
super(props);
|
2017-11-04 21:46:37 +02:00
|
|
|
|
|
|
|
this.scrollTop_ = 0;
|
2019-01-25 21:59:36 +02:00
|
|
|
|
|
|
|
this.listRef = React.createRef();
|
|
|
|
|
|
|
|
this.onScroll = this.onScroll.bind(this);
|
|
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
2023-02-26 17:40:13 +02:00
|
|
|
this.onDrop = this.onDrop.bind(this);
|
2019-01-25 21:59:36 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public visibleItemCount(props: Props<ItemType> = undefined) {
|
2019-01-25 21:59:36 +02:00
|
|
|
if (typeof props === 'undefined') props = this.props;
|
|
|
|
return Math.ceil(props.style.height / props.itemHeight);
|
2017-11-04 21:46:37 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public updateStateItemIndexes(props: Props<ItemType> = undefined) {
|
2017-11-05 02:17:48 +02:00
|
|
|
if (typeof props === 'undefined') props = this.props;
|
2017-11-04 21:46:37 +02:00
|
|
|
|
2017-11-05 02:17:48 +02:00
|
|
|
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
|
2019-02-10 17:59:24 +02:00
|
|
|
const visibleItemCount = this.visibleItemCount(props);
|
2017-11-05 02:17:48 +02:00
|
|
|
|
2019-12-28 23:53:21 +02:00
|
|
|
let bottomItemIndex = topItemIndex + (visibleItemCount - 1);
|
2017-11-05 02:17:48 +02:00
|
|
|
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
2017-11-04 21:46:37 +02:00
|
|
|
|
|
|
|
this.setState({
|
2017-11-05 02:17:48 +02:00
|
|
|
topItemIndex: topItemIndex,
|
|
|
|
bottomItemIndex: bottomItemIndex,
|
2017-11-04 21:46:37 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public offsetTop() {
|
2020-05-27 18:21:46 +02:00
|
|
|
return this.listRef.current ? this.listRef.current.offsetTop : 0;
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public offsetScroll() {
|
2020-09-15 15:01:07 +02:00
|
|
|
return this.scrollTop_;
|
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public get container() {
|
|
|
|
return this.listRef.current;
|
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public UNSAFE_componentWillMount() {
|
2017-11-05 02:17:48 +02:00
|
|
|
this.updateStateItemIndexes();
|
2017-11-04 21:46:37 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public UNSAFE_componentWillReceiveProps(newProps: Props<ItemType>) {
|
2017-11-05 02:17:48 +02:00
|
|
|
this.updateStateItemIndexes(newProps);
|
2017-11-04 21:46:37 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public onScroll: UIEventHandler<HTMLDivElement> = event => {
|
|
|
|
this.scrollTop_ = (event.target as HTMLElement).scrollTop;
|
2019-01-25 21:59:36 +02:00
|
|
|
this.updateStateItemIndexes();
|
2024-04-25 16:31:18 +02:00
|
|
|
};
|
2019-01-25 21:59:36 +02:00
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public onKeyDown: KeyboardEventHandler<HTMLElement> = event => {
|
2019-01-25 21:59:36 +02:00
|
|
|
if (this.props.onKeyDown) this.props.onKeyDown(event);
|
2024-04-25 16:31:18 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
public onDrop: DragEventHandler<HTMLElement> = event => {
|
|
|
|
if (this.props.onItemDrop) this.props.onItemDrop(event);
|
|
|
|
};
|
|
|
|
|
|
|
|
public get firstVisibleIndex() {
|
|
|
|
return Math.min(this.props.items.length - 1, this.state.topItemIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
public get lastVisibleIndex() {
|
|
|
|
return Math.max(0, this.state.bottomItemIndex);
|
2019-01-25 21:59:36 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
public isIndexVisible(itemIndex: number) {
|
|
|
|
return itemIndex >= this.firstVisibleIndex && itemIndex <= this.lastVisibleIndex;
|
2023-02-26 17:40:13 +02:00
|
|
|
}
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public makeItemIndexVisible(itemIndex: number) {
|
2024-04-25 16:31:18 +02:00
|
|
|
if (this.isIndexVisible(itemIndex)) return;
|
2019-01-25 21:59:36 +02:00
|
|
|
|
2024-04-25 16:31:18 +02:00
|
|
|
const top = this.firstVisibleIndex;
|
2019-01-25 21:59:36 +02:00
|
|
|
|
|
|
|
let scrollTop = 0;
|
|
|
|
if (itemIndex < top) {
|
|
|
|
scrollTop = this.props.itemHeight * itemIndex;
|
|
|
|
} else {
|
2019-01-30 19:35:41 +02:00
|
|
|
scrollTop = this.props.itemHeight * itemIndex - (this.visibleItemCount() - 1) * this.props.itemHeight;
|
2019-01-25 21:59:36 +02:00
|
|
|
}
|
|
|
|
|
2019-01-30 19:35:41 +02:00
|
|
|
if (scrollTop < 0) scrollTop = 0;
|
|
|
|
|
2017-11-05 02:17:48 +02:00
|
|
|
this.scrollTop_ = scrollTop;
|
2019-01-25 21:59:36 +02:00
|
|
|
this.listRef.current.scrollTop = scrollTop;
|
|
|
|
|
2017-11-05 02:17:48 +02:00
|
|
|
this.updateStateItemIndexes();
|
2017-11-04 21:46:37 +02:00
|
|
|
}
|
|
|
|
|
2020-05-27 18:21:46 +02:00
|
|
|
// shouldComponentUpdate(nextProps, nextState) {
|
|
|
|
// for (const n in this.props) {
|
|
|
|
// if (this.props[n] !== nextProps[n]) {
|
|
|
|
// console.info('Props', n, nextProps[n]);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// for (const n in this.state) {
|
|
|
|
// if (this.state[n] !== nextState[n]) {
|
|
|
|
// console.info('State', n, nextState[n]);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
|
|
|
// return true;
|
|
|
|
// }
|
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public render() {
|
2017-11-04 18:40:34 +02:00
|
|
|
const items = this.props.items;
|
2024-04-25 16:31:18 +02:00
|
|
|
const style: React.CSSProperties = {
|
|
|
|
...this.props.style,
|
|
|
|
overflowX: 'hidden',
|
|
|
|
overflowY: 'auto',
|
|
|
|
};
|
2017-11-04 18:40:34 +02:00
|
|
|
|
2020-05-27 18:21:46 +02:00
|
|
|
// if (this.props.disabled) style.opacity = 0.5;
|
|
|
|
|
2017-11-04 21:46:37 +02:00
|
|
|
if (!this.props.itemHeight) throw new Error('itemHeight is required');
|
|
|
|
|
2023-01-19 19:19:06 +02:00
|
|
|
const blankItem = function(key: string, height: number) {
|
2019-07-29 14:13:23 +02:00
|
|
|
return <div key={key} style={{ height: height }}></div>;
|
|
|
|
};
|
2017-11-04 21:46:37 +02:00
|
|
|
|
2020-03-14 01:46:14 +02:00
|
|
|
const itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
|
2017-11-04 21:46:37 +02:00
|
|
|
|
|
|
|
for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) {
|
2020-05-27 18:21:46 +02:00
|
|
|
const itemComp = this.props.itemRenderer(items[i], i);
|
2017-11-04 18:40:34 +02:00
|
|
|
itemComps.push(itemComp);
|
|
|
|
}
|
|
|
|
|
2017-11-04 21:46:37 +02:00
|
|
|
itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight));
|
|
|
|
|
2020-03-14 01:46:14 +02:00
|
|
|
const classes = ['item-list'];
|
2017-11-04 21:46:37 +02:00
|
|
|
if (this.props.className) classes.push(this.props.className);
|
|
|
|
|
2017-11-04 18:40:34 +02:00
|
|
|
return (
|
2024-08-05 20:37:23 +02:00
|
|
|
<div
|
|
|
|
ref={this.listRef}
|
|
|
|
className={classes.join(' ')}
|
|
|
|
style={style}
|
|
|
|
|
|
|
|
id={this.props.id}
|
|
|
|
role={this.props.role}
|
|
|
|
aria-label={this.props['aria-label']}
|
|
|
|
aria-setsize={items.length}
|
|
|
|
|
|
|
|
onScroll={this.onScroll}
|
|
|
|
onKeyDown={this.onKeyDown}
|
|
|
|
onDrop={this.onDrop}
|
|
|
|
>
|
2019-07-29 14:13:23 +02:00
|
|
|
{itemComps}
|
2017-11-04 18:40:34 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-19 19:19:06 +02:00
|
|
|
export default ItemList;
|