mirror of
https://github.com/balzack/databag.git
synced 2025-02-14 12:39:17 +00:00
rendering customized virtual list
This commit is contained in:
parent
43d88b5a64
commit
b845404048
@ -5,41 +5,12 @@ import { Button, Checkbox, Modal } from 'antd'
|
|||||||
import { ConversationWrapper, CloseButton, ListItem } from './Conversation.styled';
|
import { ConversationWrapper, CloseButton, ListItem } from './Conversation.styled';
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
|
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized';
|
||||||
import { AddTopic } from './AddTopic/AddTopic';
|
import { AddTopic } from './AddTopic/AddTopic';
|
||||||
|
import { VirtualList } from './VirtualList/VirtualList';
|
||||||
|
|
||||||
export function Conversation() {
|
export function Conversation() {
|
||||||
|
|
||||||
const [ scrollIndex, setScrollIndex ] = useState(null);
|
|
||||||
const { state, actions } = useConversation();
|
const { state, actions } = useConversation();
|
||||||
|
|
||||||
const cache = new CellMeasurerCache({
|
|
||||||
defaultHeight: 256,
|
|
||||||
fixedWidth: true
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setScrollIndex(state.topics.length);
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
const renderRow = ({ index, isScrolling, key, parent, style }) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CellMeasurer
|
|
||||||
cache={cache}
|
|
||||||
columnIndex={0}
|
|
||||||
key={key}
|
|
||||||
parent={parent}
|
|
||||||
rowIndex={index}
|
|
||||||
>
|
|
||||||
{({ measure, registerChild }) => (
|
|
||||||
// 'style' attribute required to position cell (within parent List)
|
|
||||||
<div class="noselect" ref={registerChild} style={style}>
|
|
||||||
{ state.topics[index].data.topicDetail.data }
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConversationWrapper>
|
<ConversationWrapper>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
@ -47,22 +18,14 @@ export function Conversation() {
|
|||||||
<CloseButton type="text" class="close" size={'large'} onClick={() => actions.close()} icon={<CloseOutlined />} />
|
<CloseButton type="text" class="close" size={'large'} onClick={() => actions.close()} icon={<CloseOutlined />} />
|
||||||
</div>
|
</div>
|
||||||
<div class="thread">
|
<div class="thread">
|
||||||
<div style={{ flex: '1 1 auto' }}>
|
<VirtualList topics={[
|
||||||
<AutoSizer>
|
'OLDEST TOPIC',
|
||||||
{({height, width}) => (
|
'NEXT TOPIC',
|
||||||
<List
|
'NEXT TOPIC',
|
||||||
width={width}
|
'NEXT TOPIC',
|
||||||
height={height}
|
'NEXT TOPIC',
|
||||||
deferredMeasurementCache={cache}
|
'NEWEST TOPIC',
|
||||||
rowHeight={cache.rowHeight}
|
]}/>
|
||||||
rowRenderer={renderRow}
|
|
||||||
rowCount={state.topics.length}
|
|
||||||
overscanRowCount={16}
|
|
||||||
scrollToIndex={scrollIndex}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<AddTopic />
|
<AddTopic />
|
||||||
</ConversationWrapper>
|
</ConversationWrapper>
|
||||||
|
@ -43,7 +43,6 @@ export const ConversationWrapper = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-left: 16px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { TopicItemWrapper } from './TopicItem.styled';
|
||||||
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
|
||||||
|
export function TopicItem({ topic, onHeight }) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactResizeDetector handleHeight={true}>
|
||||||
|
{({ height }) => {
|
||||||
|
if (typeof height !== 'undefined' && height > 0) {
|
||||||
|
onHeight(height);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TopicItemWrapper>
|
||||||
|
<div>topic</div>
|
||||||
|
</TopicItemWrapper>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</ReactResizeDetector>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const TopicItemWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
205
net/web/src/User/Conversation/VirtualList/VirtualList.jsx
Normal file
205
net/web/src/User/Conversation/VirtualList/VirtualList.jsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { VirtualListWrapper } from './VirtualList.styled';
|
||||||
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import { TopicItem } from './TopicItem/TopicItem';
|
||||||
|
|
||||||
|
export function VirtualList({ topics }) {
|
||||||
|
|
||||||
|
const OVERSCAN = 300
|
||||||
|
const DEFAULT_ITEM_HEIGHT = 64;
|
||||||
|
const DEFAULT_LIST_HEIGHT = 1024;
|
||||||
|
|
||||||
|
const [ viewHeight, setViewHeight ] = useState(DEFAULT_LIST_HEIGHT);
|
||||||
|
const [ canvasHeight, setCanvasHeight ] = useState(DEFAULT_LIST_HEIGHT*3);
|
||||||
|
const [ scrolling, setScrolling ] = useState(false);
|
||||||
|
const [ items, setItems ] = useState([]);
|
||||||
|
|
||||||
|
let scrollTop = useRef(0);
|
||||||
|
let containers = useRef([]);
|
||||||
|
let anchor = useRef(null);
|
||||||
|
let listRef = useRef();
|
||||||
|
|
||||||
|
const addItemTop = (item) => {
|
||||||
|
setItems((i) => { i.unshift(item); return [...i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const addItemBottom = (item) => {
|
||||||
|
setItems((i) => { i.push(item); return [...i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItem = (idx, item) => {
|
||||||
|
setItems((i) => { i[idx] = item; return [...i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewHeight * 3 > canvasHeight) {
|
||||||
|
setCanvasHeight(viewHeight*3);
|
||||||
|
}
|
||||||
|
setTopics();
|
||||||
|
}, [viewHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTopics();
|
||||||
|
}, [topics]);
|
||||||
|
|
||||||
|
const onScrollView = (e) => {
|
||||||
|
|
||||||
|
// add or remove from overscan
|
||||||
|
|
||||||
|
// clip to top or bottom
|
||||||
|
|
||||||
|
// set or clear latch
|
||||||
|
|
||||||
|
scrollTop.current = e.target.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadNextItem = () => {
|
||||||
|
let view = getPlacement();
|
||||||
|
if (view) {
|
||||||
|
if (view.overscan.top < OVERSCAN) {
|
||||||
|
if (containers.current[0].index > 0) {
|
||||||
|
let container = {
|
||||||
|
top: containers.current[0].top - DEFAULT_ITEM_HEIGHT,
|
||||||
|
height: DEFAULT_ITEM_HEIGHT,
|
||||||
|
index: containers.current[0].index - 1,
|
||||||
|
}
|
||||||
|
anchor.current += 1;
|
||||||
|
containers.current.unshift(container);
|
||||||
|
console.log("ADD ITEM BEFORE", container);
|
||||||
|
addItemTop(getItem(container))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (view.overscan.bottom < OVERSCAN) {
|
||||||
|
if (containers.current[containers.current.length - 1].index + 1 < topics.length) {
|
||||||
|
console.log("ADD ITEM AFTER");
|
||||||
|
let container = {
|
||||||
|
top: containers.current[containers.current.length - 1].top + containers.current[containers.current.length - 1].height,
|
||||||
|
height: DEFAULT_ITEM_HEIGHT,
|
||||||
|
index: containers.current[containers.current.length - 1].index + 1,
|
||||||
|
}
|
||||||
|
containers.current.push(container);
|
||||||
|
addItemBottom(getItem(container))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const alignItems = () => {
|
||||||
|
if (containers.current.length > 0) {
|
||||||
|
let pos = null;
|
||||||
|
|
||||||
|
pos = containers.current[anchor.current].top;
|
||||||
|
for (let i = anchor.current - 1; i >= 0; i--) {
|
||||||
|
pos -= containers.current[i].height;
|
||||||
|
if (containers.current[i].top != pos) {
|
||||||
|
containers.current[i].top = pos;
|
||||||
|
updateItem(i, getItem(containers.current[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos < 0) {
|
||||||
|
// TODO reset canvas
|
||||||
|
console.log("ALERT: reset convas");
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = containers.current[anchor.current].top + containers.current[anchor.current].height;
|
||||||
|
for (let i = anchor.current + 1; i < containers.current.length; i++) {
|
||||||
|
if (containers.current[i].top != pos) {
|
||||||
|
containers.current[i].top = pos;
|
||||||
|
updateItem(i, getItem(containers.current[i]));
|
||||||
|
}
|
||||||
|
pos += containers.current[i].height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos > canvasHeight) {
|
||||||
|
// TODO reset canvas
|
||||||
|
console.log("ALERT: reset canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
let view = getPlacement();
|
||||||
|
if (!scrolling) {
|
||||||
|
if (view.position.height < viewHeight) {
|
||||||
|
listRef.current.scrollTo({ top: view.position.top, left: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
listRef.current.scrollTo({ top: view.position.bottom - viewHeight, left: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadNextItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTopics = () => {
|
||||||
|
// validate items
|
||||||
|
|
||||||
|
if (topics.length > 0 && canvasHeight > 0) {
|
||||||
|
let view = getPlacement();
|
||||||
|
if (!view) {
|
||||||
|
let pos = canvasHeight / 2;
|
||||||
|
listRef.current.scrollTo({ top: pos, left: 0 });
|
||||||
|
scrollTop.current = pos;
|
||||||
|
|
||||||
|
let container = {
|
||||||
|
top: pos - DEFAULT_ITEM_HEIGHT,
|
||||||
|
height: DEFAULT_ITEM_HEIGHT,
|
||||||
|
index: topics.length - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
anchor.current = 0;
|
||||||
|
containers.current.push(container);
|
||||||
|
addItemBottom(getItem(container));
|
||||||
|
|
||||||
|
listRef.current.scrollTo({ top: container.top, left: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
loadNextItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTopicHeight = (container, height) => {
|
||||||
|
container.height = height;
|
||||||
|
alignItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
const getItem = (container) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'absolute', top: container.top }}>
|
||||||
|
<TopicItem topic={topics[container.index]} onHeight={(height) => onTopicHeight(container, height)} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlacement = () => {
|
||||||
|
if (containers.current.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let top = containers.current[0].top;
|
||||||
|
let bottom = containers.current[containers.current.length-1].top + containers.current[containers.current.length-1].height;
|
||||||
|
let overTop = scrollTop.current - top;
|
||||||
|
let overBottom = bottom - (scrollTop.current + viewHeight);
|
||||||
|
return {
|
||||||
|
position: { top, bottom, height: bottom - top },
|
||||||
|
overscan: { top: overTop, bottom: overBottom }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactResizeDetector handleHeight={true}>
|
||||||
|
{({ height }) => {
|
||||||
|
setViewHeight(height);
|
||||||
|
return (
|
||||||
|
<VirtualListWrapper onScroll={onScrollView}>
|
||||||
|
<div class="rollview" ref={listRef} onScroll={onScrollView}>
|
||||||
|
<div class="roll" style={{ height: canvasHeight }}>
|
||||||
|
{ items }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VirtualListWrapper>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</ReactResizeDetector>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const VirtualListWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f6f5ed;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.rollview {
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roll {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user