mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
refactor of reverse virtual list
This commit is contained in:
parent
4cceaab423
commit
296bed18d5
@ -36,8 +36,8 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div class="thread">
|
<div class="thread">
|
||||||
<VirtualList id={channelId + cardId}
|
<VirtualList id={`${cardId}:${channelId}`}
|
||||||
items={state.topics} itemRenderer={topicRenderer} onMore={actions.more} />
|
items={state.topics} itemRenderer={topicRenderer} loadMore={actions.more} />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider">
|
<div class="divider">
|
||||||
<div class="line" />
|
<div class="line" />
|
||||||
|
@ -21,17 +21,17 @@ export function TopicItem({ host, topic }) {
|
|||||||
nameClass = "unknown"
|
nameClass = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAsset = (asset) => {
|
const renderAsset = (asset, idx, topicId) => {
|
||||||
if (asset.image) {
|
if (asset.image) {
|
||||||
return <ImageAsset thumbUrl={actions.getAssetUrl(asset.image.thumb)}
|
return <ImageAsset thumbUrl={actions.getAssetUrl(asset.image.thumb, topicId)}
|
||||||
fullUrl={actions.getAssetUrl(asset.image.full)} />
|
fullUrl={actions.getAssetUrl(asset.image.full, topicId)} />
|
||||||
}
|
}
|
||||||
if (asset.video) {
|
if (asset.video) {
|
||||||
return <VideoAsset thumbUrl={actions.getAssetUrl(asset.video.thumb)}
|
return <VideoAsset thumbUrl={actions.getAssetUrl(asset.video.thumb, topicId)}
|
||||||
lqUrl={actions.getAssetUrl(asset.video.lq)} hdUrl={actions.getAssetUrl(asset.video.hd)} />
|
lqUrl={actions.getAssetUrl(asset.video.lq, topicId)} hdUrl={actions.getAssetUrl(asset.video.hd, topicId)} />
|
||||||
}
|
}
|
||||||
if (asset.audio) {
|
if (asset.audio) {
|
||||||
return <AudioAsset label={asset.audio.label} audioUrl={actions.getAssetUrl(asset.audio.full)} />
|
return <AudioAsset label={asset.audio.label} audioUrl={actions.getAssetUrl(asset.audio.full, topicId)} />
|
||||||
}
|
}
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
@ -95,46 +95,50 @@ export function TopicItem({ host, topic }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TopicItemWrapper>
|
<TopicItemWrapper>
|
||||||
<div class="topic-header">
|
{ state.init && (
|
||||||
<div class="avatar">
|
<>
|
||||||
<Logo width={32} height={32} radius={4} url={state.imageUrl} />
|
<div class="topic-header">
|
||||||
</div>
|
<div class="avatar">
|
||||||
<div class="info">
|
<Logo width={32} height={32} radius={4} url={state.imageUrl} />
|
||||||
<div class={nameClass}>{ name }</div>
|
</div>
|
||||||
<div>{ state.created }</div>
|
<div class="info">
|
||||||
</div>
|
<div class={nameClass}>{ name }</div>
|
||||||
<div class="topic-options">
|
<div>{ state.created }</div>
|
||||||
<Options />
|
</div>
|
||||||
|
<div class="topic-options">
|
||||||
|
<Options />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{ !state.confirmed && (
|
||||||
{ !state.confirmed && (
|
<div>
|
||||||
<div>
|
<div class="message">
|
||||||
<div class="message">
|
<Skeleton size={'small'} active={true} />
|
||||||
<Skeleton size={'small'} active={true} />
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{ state.confirmed && (
|
|
||||||
<div>
|
|
||||||
{ state.error && (
|
|
||||||
<div class="asset-placeholder">
|
|
||||||
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !state.error && !state.ready && (
|
{ state.confirmed && (
|
||||||
<div class="asset-placeholder">
|
<div>
|
||||||
<PictureOutlined style={{ fontSize: 32 }} />
|
{ state.error && (
|
||||||
|
<div class="asset-placeholder">
|
||||||
|
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.error && !state.ready && (
|
||||||
|
<div class="asset-placeholder">
|
||||||
|
<PictureOutlined style={{ fontSize: 32 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{ !state.error && state.ready && state.assets.length > 0 && (
|
||||||
|
<div class="topic-assets">
|
||||||
|
<Carousel pad={40} items={state.assets} itemRenderer={renderAsset} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="message">
|
||||||
|
<Message />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{ !state.error && state.ready && state.assets.length > 0 && (
|
</>
|
||||||
<div class="topic-assets">
|
|
||||||
<Carousel pad={40} items={state.assets} itemRenderer={renderAsset} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class="message">
|
|
||||||
<Message />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</TopicItemWrapper>
|
</TopicItemWrapper>
|
||||||
)
|
)
|
||||||
|
@ -23,6 +23,11 @@ export function ImageAsset({ thumbUrl, fullUrl }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearPopout = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
actions.clearPopout();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageAssetWrapper>
|
<ImageAssetWrapper>
|
||||||
<ReactResizeDetector handleWidth={true} handleHeight={true}>
|
<ReactResizeDetector handleWidth={true} handleHeight={true}>
|
||||||
@ -46,7 +51,7 @@ export function ImageAsset({ thumbUrl, fullUrl }) {
|
|||||||
<div class="viewer" style={{ width: dimension.width, height: dimension.height }} onClick={popout} />
|
<div class="viewer" style={{ width: dimension.width, height: dimension.height }} onClick={popout} />
|
||||||
)}
|
)}
|
||||||
{ state.display === 'small' && state.popout && (
|
{ state.display === 'small' && state.popout && (
|
||||||
<div class="fullscreen" onClick={actions.clearPopout}>
|
<div class="fullscreen" onClick={clearPopout} onTouchEnd={clearPopout}>
|
||||||
<img class="image" src={fullUrl} />
|
<img class="image" src={fullUrl} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -8,6 +8,7 @@ export function useTopicItem(topic) {
|
|||||||
const [guid, setGuid] = useState(null);
|
const [guid, setGuid] = useState(null);
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
|
init: false,
|
||||||
name: null,
|
name: null,
|
||||||
handle: null,
|
handle: null,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
@ -18,6 +19,7 @@ export function useTopicItem(topic) {
|
|||||||
error: false,
|
error: false,
|
||||||
owner: false,
|
owner: false,
|
||||||
assets: [],
|
assets: [],
|
||||||
|
topicId: null,
|
||||||
editing: false,
|
editing: false,
|
||||||
busy: false,
|
busy: false,
|
||||||
textColor: '#444444',
|
textColor: '#444444',
|
||||||
@ -98,18 +100,18 @@ export function useTopicItem(topic) {
|
|||||||
|
|
||||||
if (profile.state.profile.guid == guid) {
|
if (profile.state.profile.guid == guid) {
|
||||||
const { name, handle, imageUrl } = profile.actions.getProfile();
|
const { name, handle, imageUrl } = profile.actions.getProfile();
|
||||||
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize });
|
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const { name, handle, imageUrl } = card.actions.getCardProfileByGuid(guid);
|
const { name, handle, imageUrl } = card.actions.getCardProfileByGuid(guid);
|
||||||
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize });
|
updateState({ name, handle, imageUrl, status, message, transform, assets, confirmed, error, ready, created: createdStr, owner, textColor, textSize, topicId: topic.id, init: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [profile, card, conversation, topic]);
|
}, [profile, card, conversation, topic]);
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getAssetUrl: (assetId) => {
|
getAssetUrl: (assetId, topicId) => {
|
||||||
return conversation.actions.getAssetUrl(topic?.id, assetId);
|
return conversation.actions.getAssetUrl(state.topicId, assetId);
|
||||||
},
|
},
|
||||||
removeTopic: async () => {
|
removeTopic: async () => {
|
||||||
return await conversation.actions.removeTopic(topic.id);
|
return await conversation.actions.removeTopic(topic.id);
|
||||||
|
@ -1,338 +1,371 @@
|
|||||||
import React, { useRef, useState, useEffect } from 'react';
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
import { VirtualListWrapper, VirtualItem } from './VirtualList.styled';
|
import { VirtualListWrapper, VirtualItem } from './VirtualList.styled';
|
||||||
import ReactResizeDetector from 'react-resize-detector';
|
import ReactResizeDetector from 'react-resize-detector';
|
||||||
|
import { useVirtualList } from './useVirtualList.hook';
|
||||||
|
|
||||||
export function VirtualList({ id, items, itemRenderer, onMore }) {
|
export function VirtualList({ id, items, itemRenderer, loadMore }) {
|
||||||
|
|
||||||
const REDZONE = 1024; // recenter on canvas if in canvas edge redzone
|
const redZone = 1024;
|
||||||
const HOLDZONE = 2048; // drop slots outside of holdzone of view
|
const holdZone = 2048;
|
||||||
const OVERSCAN = 1024; // add slots in overscan of view
|
const fillZone = 1024;
|
||||||
const DEFAULT_ITEM_HEIGHT = 256;
|
|
||||||
const DEFAULT_LIST_HEIGHT = 4096;
|
|
||||||
const GUTTER = 1;
|
|
||||||
|
|
||||||
const [ msg, setMsg ] = useState("YO");
|
const pushDelay = 250;
|
||||||
|
const moreDelay = 2000;
|
||||||
|
const latchDelay = 500;
|
||||||
|
|
||||||
const [ canvasHeight, setCanvasHeight ] = useState(16384);
|
const defaultHeight = 32;
|
||||||
const [ slots, setSlots ] = useState(new Map());
|
|
||||||
const [ scroll, setScroll ] = useState('hidden');
|
|
||||||
|
|
||||||
let update = useRef(0);
|
const rollHeight = 16384;
|
||||||
let viewHeight = useRef(DEFAULT_LIST_HEIGHT);
|
|
||||||
let latch = useRef(true);
|
|
||||||
let scrollTop = useRef(0);
|
|
||||||
let containers = useRef([]);
|
|
||||||
let listRef = useRef();
|
|
||||||
let key = useRef(null);
|
|
||||||
let itemView = useRef([]);
|
|
||||||
let nomore = useRef(false);
|
|
||||||
|
|
||||||
const addSlot = (id, slot) => {
|
const { state, actions } = useVirtualList();
|
||||||
setSlots((m) => { m.set(id, slot); return new Map(m); })
|
const containers = useRef([]);
|
||||||
}
|
const itemView = useRef([]);
|
||||||
|
const debounce = useRef([]);
|
||||||
|
const nomore = useRef(false);
|
||||||
|
const nolatch = useRef(false);
|
||||||
|
const list = useRef(null);
|
||||||
|
const scrollTop = useRef(0);
|
||||||
|
|
||||||
const updateSlot = (id, slot) => {
|
const [scrollPos, setScrollPos] = useState(0);
|
||||||
setSlots((m) => { m.set(id, slot); return new Map(m); })
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeSlot = (id) => {
|
|
||||||
setSlots((m) => { m.delete(id); return new Map(m); });
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearSlots = () => {
|
|
||||||
setSlots((m) => { return new Map() })
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCanvas();
|
scrollTop.current = 8192;
|
||||||
}, [canvasHeight]);
|
list.current.scrollTo({ top: 8192, left: 0 });
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (key.current != id) {
|
|
||||||
key.current = id;
|
// reference copy
|
||||||
latch.current = true;
|
|
||||||
containers.current = [];
|
|
||||||
clearSlots();
|
|
||||||
}
|
|
||||||
itemView.current = items;
|
itemView.current = items;
|
||||||
setItems();
|
|
||||||
}, [items, id]);
|
|
||||||
|
|
||||||
const onScrollWheel = (e) => {
|
// genearte set of active ids
|
||||||
latch.current = false;
|
let ids = new Map();
|
||||||
}
|
for (let i = 0; i < itemView.current.length; i++) {
|
||||||
|
ids.set(getItemKey(itemView.current[i]), i);
|
||||||
|
}
|
||||||
|
|
||||||
const onScrollView = (e) => {
|
// remove any deleted items
|
||||||
scrollTop.current = e.target.scrollTop;
|
let slots = [];
|
||||||
loadNextSlot();
|
containers.current.forEach((container) => {
|
||||||
dropSlots();
|
if (!ids.has(container.key)) {
|
||||||
centerCanvas();
|
actions.removeSlot(container.key);
|
||||||
limitScroll();
|
}
|
||||||
}
|
else {
|
||||||
|
container.index = ids.get(container.key);
|
||||||
|
container.item = itemView.current[container.index];
|
||||||
|
slots.push(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
containers.current = slots;
|
||||||
|
|
||||||
const updateCanvas = () => {
|
// sort by index
|
||||||
|
containers.current.sort((a, b) => {
|
||||||
|
if (a.index < b.index) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// rerender list
|
||||||
|
layoutItems();
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
layoutItems();
|
||||||
|
}, [scrollPos, state.listHeight, state.view]);
|
||||||
|
|
||||||
|
const layoutItems = () => {
|
||||||
alignSlots();
|
alignSlots();
|
||||||
loadNextSlot();
|
loadSlots();
|
||||||
dropSlots();
|
releaseSlots();
|
||||||
centerCanvas();
|
centerSlots();
|
||||||
limitScroll();
|
latchSlots();
|
||||||
latchScroll();
|
pushSlots();
|
||||||
}
|
};
|
||||||
|
|
||||||
const limitScroll = () => {
|
const latchSlots = () => {
|
||||||
let view = getPlacement();
|
if (containers.current.length > 0 && state.latched) {
|
||||||
if (view && containers.current[containers.current.length - 1].index == itemView.current.length - 1) {
|
if (!nolatch.current) {
|
||||||
if (view?.overscan?.bottom <= 0) {
|
const last = containers.current[containers.current.length - 1];
|
||||||
if (view.position.height < viewHeight.current) {
|
const bottom = last.top + last.height;
|
||||||
if (scrollTop.current != view.position.top) {
|
if (last.heightSet && scrollTop.current < bottom - state.listHeight) {
|
||||||
scrollTop.current = view.position.top;
|
list.current.scrollTo({ top: bottom - state.listHeight, left: 0, behavior: 'smooth' });
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
nolatch.current = true;
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (scrollTop.current != view.position.bottom - viewHeight.current) {
|
|
||||||
scrollTop.current = view.position.bottom - viewHeight.current;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
latch.current = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (view && containers.current[0].index == 0) {
|
|
||||||
if (view?.overscan?.top <= 0) {
|
|
||||||
scrollTop.current = containers.current[0].top;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
|
||||||
if (!nomore.current) {
|
|
||||||
nomore.current = true;
|
|
||||||
onMore();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
nomore.current = false;
|
nolatch.current = false;
|
||||||
}, 2500);
|
latchSlots();
|
||||||
|
}, latchDelay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNextSlot = () => {
|
const pushSlots = () => {
|
||||||
let view = getPlacement();
|
if (debounce.current != null) {
|
||||||
if (view) {
|
clearTimeout(debounce.current);
|
||||||
if (view.overscan.top < OVERSCAN) {
|
};
|
||||||
if (containers.current[0].index > 0 && containers.current[0].index < itemView.current.length) {
|
debounce.current = setTimeout(() => {
|
||||||
let below = containers.current[0];
|
if (containers.current.length > 0) {
|
||||||
let container = {
|
const range = getContainerRange();
|
||||||
top: below.top - (DEFAULT_ITEM_HEIGHT + 2 * GUTTER),
|
if (range.bottom - range.top < state.listHeight) {
|
||||||
height: DEFAULT_ITEM_HEIGHT,
|
if (scrollTop.current + state.listHeight != range.bottom) {
|
||||||
index: containers.current[0].index - 1,
|
list.current.scrollTo({ top: range.bottom - state.listHeight, left: 0, behavior: 'smooth' });
|
||||||
id: itemView.current[containers.current[0].index - 1].id,
|
actions.latch();
|
||||||
revision: itemView.current[containers.current[0].index - 1].revision,
|
|
||||||
}
|
}
|
||||||
containers.current.unshift(container);
|
}
|
||||||
addSlot(container.id, getSlot(container))
|
else if (scrollTop.current + state.listHeight > range.bottom) {
|
||||||
loadNextSlot();
|
list.current.scrollTo({ top: range.bottom - state.listHeight, left: 0, behavior: 'smooth' });
|
||||||
|
actions.latch();
|
||||||
|
}
|
||||||
|
else if (scrollTop.current < range.top) {
|
||||||
|
list.current.scrollTo({ top: range.top, left: 0, behavior: 'smooth' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (view.overscan.bottom < OVERSCAN) {
|
}, pushDelay);
|
||||||
if (containers.current[containers.current.length - 1].index + 1 < itemView.current.length) {
|
};
|
||||||
let above = containers.current[containers.current.length - 1];
|
|
||||||
let container = {
|
|
||||||
top: above.top + above.height + 2 * GUTTER,
|
|
||||||
height: DEFAULT_ITEM_HEIGHT,
|
|
||||||
index: containers.current[containers.current.length - 1].index + 1,
|
|
||||||
id: itemView.current[containers.current[containers.current.length - 1].index + 1].id,
|
|
||||||
revision: itemView.current[containers.current[containers.current.length - 1].index + 1].revision,
|
|
||||||
}
|
|
||||||
containers.current.push(container);
|
|
||||||
addSlot(container.id, getSlot(container))
|
|
||||||
loadNextSlot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropSlots = () => {
|
|
||||||
while (containers.current.length > 0 &&
|
|
||||||
containers.current[0].top + containers.current[0].height + 2 * GUTTER + HOLDZONE < scrollTop.current) {
|
|
||||||
removeSlot(containers.current[0].id);
|
|
||||||
containers.current.shift();
|
|
||||||
}
|
|
||||||
while (containers.current.length > 0 &&
|
|
||||||
containers.current[containers.current.length - 1].top > scrollTop.current + viewHeight.current + HOLDZONE) {
|
|
||||||
removeSlot(containers.current[containers.current.length - 1].id);
|
|
||||||
containers.current.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const alignSlots = () => {
|
const alignSlots = () => {
|
||||||
if (containers.current.length > 1) {
|
if (containers.current.length > 1) {
|
||||||
let mid = Math.floor(containers.current.length / 2);
|
if (state.latched) {
|
||||||
|
const index = containers.current.length - 1;
|
||||||
let alignTop = containers.current[mid].top + containers.current[mid].height + 2 * GUTTER;
|
const last = containers.current[index];
|
||||||
for (let i = mid + 1; i < containers.current.length; i++) {
|
let bottom = last.top + last.height;
|
||||||
if (containers.current[i].top != alignTop) {
|
for (let i = index; i >= 0; i--) {
|
||||||
containers.current[i].top = alignTop;
|
let container = containers.current[i];
|
||||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
if (container.top + container.height != bottom) {
|
||||||
|
container.top = bottom - container.height;
|
||||||
|
actions.updateSlot(container.key, getSlot(container));
|
||||||
|
}
|
||||||
|
bottom -= container.height;
|
||||||
}
|
}
|
||||||
alignTop += containers.current[i].height + 2 * GUTTER;
|
}
|
||||||
|
else {
|
||||||
|
const index = Math.floor(containers.current.length / 2);
|
||||||
|
const mid = containers.current[index];
|
||||||
|
let top = mid.top;
|
||||||
|
for (let i = index; i < containers.current.length; i++) {
|
||||||
|
let container = containers.current[i];
|
||||||
|
if (container.top != top) {
|
||||||
|
container.top = top;
|
||||||
|
actions.updateSlot(container.key, getSlot(container));
|
||||||
|
}
|
||||||
|
top += container.height;
|
||||||
|
}
|
||||||
|
let bottom = mid.top + mid.height;
|
||||||
|
for (let i = index; i >= 0; i--) {
|
||||||
|
let container = containers.current[i];
|
||||||
|
if (container.top + container.height != bottom) {
|
||||||
|
container.top = bottom - container.height;
|
||||||
|
actions.updateSlot(container.key, getSlot(container));
|
||||||
|
}
|
||||||
|
bottom -= container.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlots = () => {
|
||||||
|
|
||||||
|
if (containers.current.length == 0) {
|
||||||
|
// add the first slot
|
||||||
|
if (itemView.current.length > 0) {
|
||||||
|
let item = itemView.current[itemView.current.length - 1];
|
||||||
|
let slot = {
|
||||||
|
top: rollHeight / 2 + state.listHeight,
|
||||||
|
height: defaultHeight,
|
||||||
|
heightSet: false,
|
||||||
|
key: getItemKey(item),
|
||||||
|
index: itemView.current.length - 1,
|
||||||
|
item: item,
|
||||||
|
}
|
||||||
|
containers.current.push(slot);
|
||||||
|
actions.addSlot(slot.key, getSlot(slot));
|
||||||
|
list.current.scrollTo({ top: rollHeight / 2, left: 0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// fill in any missing slots
|
||||||
|
let index = containers.current[0].index;
|
||||||
|
for (let i = 1; i < containers.current.length; i++) {
|
||||||
|
let container = containers.current[i];
|
||||||
|
if (container.index != index + i) {
|
||||||
|
const item = itemView.current[index + 1];
|
||||||
|
let slot = {
|
||||||
|
top: container.top - defaultHeight,
|
||||||
|
height: defaultHeight,
|
||||||
|
heightSet: false,
|
||||||
|
index: index + i,
|
||||||
|
item: item,
|
||||||
|
key: getItemKey(item),
|
||||||
|
}
|
||||||
|
containers.current.splice(i, 0, slot);
|
||||||
|
actions.addSlot(slot.key, getSlot(slot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSlotAbove();
|
||||||
|
loadSlotBelow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSlotAbove = () => {
|
||||||
|
if (containers.current.length > 0) {
|
||||||
|
const range = getContainerRange();
|
||||||
|
if (scrollTop.current - fillZone < range.top) {
|
||||||
|
const container = containers.current[0];
|
||||||
|
if (container.index > 0) {
|
||||||
|
const index = container.index - 1;
|
||||||
|
const item = itemView.current[index];
|
||||||
|
let slot = {
|
||||||
|
top: container.top - defaultHeight,
|
||||||
|
height: defaultHeight,
|
||||||
|
heightSet: false,
|
||||||
|
index: index,
|
||||||
|
item: item,
|
||||||
|
key: getItemKey(item),
|
||||||
|
}
|
||||||
|
containers.current.unshift(slot);
|
||||||
|
actions.addSlot(slot.key, getSlot(slot));
|
||||||
|
loadSlotAbove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadSlotBelow = () => {
|
||||||
|
if (containers.current.length > 0) {
|
||||||
|
const container = containers.current[containers.current.length - 1];
|
||||||
|
if (container.index + 1 < itemView.current.length) {
|
||||||
|
const range = getContainerRange();
|
||||||
|
if (scrollTop.current + state.listHeight + fillZone > range.bottom) {
|
||||||
|
const index = container.index + 1;
|
||||||
|
const item = itemView.current[index];
|
||||||
|
let slot = {
|
||||||
|
top: container.top + container.height,
|
||||||
|
height: defaultHeight,
|
||||||
|
heightSet: false,
|
||||||
|
index: index,
|
||||||
|
item: item,
|
||||||
|
key: getItemKey(item),
|
||||||
|
}
|
||||||
|
containers.current.push(slot);
|
||||||
|
actions.addSlot(slot.key, getSlot(slot));
|
||||||
|
loadSlotBelow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseSlots = () => {
|
||||||
|
releaseSlotAbove();
|
||||||
|
releaseSlotBelow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const releaseSlotAbove = () => {
|
||||||
|
if (containers.current.length > 1) {
|
||||||
|
const container = containers.current[0];
|
||||||
|
if (container.top + container.height < scrollTop.current - holdZone) {
|
||||||
|
actions.removeSlot(container.key);
|
||||||
|
containers.current.shift();
|
||||||
|
releaseSlotAbove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseSlotBelow = () => {
|
||||||
|
if (containers.current.length > 1) {
|
||||||
|
const container = containers.current[containers.current.length - 1];
|
||||||
|
if (container.top > scrollTop.current + state.listHeight + holdZone) {
|
||||||
|
actions.removeSlot(container.key);
|
||||||
|
containers.current.pop();
|
||||||
|
releaseSlotBelow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerSlots = () => {
|
||||||
|
if (containers.current.length > 0) {
|
||||||
|
const top = containers.current[0];
|
||||||
|
if (top.top < redZone) {
|
||||||
|
containers.current.forEach(container => {
|
||||||
|
container.top += rollHeight / 2;
|
||||||
|
actions.updateSlot(container.key, getSlot(container));
|
||||||
|
});
|
||||||
|
list.current.scrollTo({ top: scrollTop.current + rollHeight / 2, left: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
let alignBottom = containers.current[mid].top;
|
const bottom = containers.current[containers.current.length - 1];
|
||||||
for (let i = mid - 1; i >= 0; i--) {
|
if (bottom.top + bottom.height > rollHeight - redZone) {
|
||||||
alignBottom -= (containers.current[i].height + 2 * GUTTER);
|
containers.current.forEach(container => {
|
||||||
if (containers.current[i].top != alignBottom) {
|
container.top -= rollHeight / 2;
|
||||||
containers.current[i].top = alignBottom;
|
actions.updateSlot(container.key, getSlot(container));
|
||||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
});
|
||||||
}
|
list.current.scrollTo({ top: scrollTop.current - rollHeight / 2, left: 0 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const centerCanvas = () => {
|
const getItemKey = (item) => {
|
||||||
let view = getPlacement();
|
return `${id}.${item.id}.${item.revision}`
|
||||||
if (view) {
|
|
||||||
let height = canvasHeight;
|
|
||||||
if (view.position.top < REDZONE) {
|
|
||||||
let shift = height / 2;
|
|
||||||
for (let i = 0; i < containers.current.length; i++) {
|
|
||||||
containers.current[i].top += shift;
|
|
||||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
|
||||||
}
|
|
||||||
scrollTop.current += shift;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
|
||||||
}
|
|
||||||
else if (view.position.bottom + REDZONE > height) {
|
|
||||||
let shift = height / 2;
|
|
||||||
for (let i = 0; i < containers.current.length; i++) {
|
|
||||||
containers.current[i].top -= shift;
|
|
||||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
|
||||||
}
|
|
||||||
scrollTop.current -= shift;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const latchScroll = () => {
|
const getSlot = (item) => {
|
||||||
if (latch.current) {
|
const container = item;
|
||||||
let view = getPlacement();
|
|
||||||
if (view) {
|
|
||||||
if (view.position.height < viewHeight.current) {
|
|
||||||
if (scrollTop.current != view.position.top) {
|
|
||||||
scrollTop.current = view.position.top;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (scrollTop.current != view.position.bottom - viewHeight.current) {
|
|
||||||
scrollTop.current = view.position.bottom - viewHeight.current;
|
|
||||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setItems = () => {
|
|
||||||
|
|
||||||
// align containers in case history was loaded
|
|
||||||
if (containers.current.length > 0) {
|
|
||||||
let container = containers.current[0];
|
|
||||||
for (let j = 0; j < itemView.current.length; j++) {
|
|
||||||
if (itemView.current[j].id == container.id) {
|
|
||||||
for(let i = 0; i < containers.current.length; i++) {
|
|
||||||
containers.current[i].index = i + j;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove containers following any removed item
|
|
||||||
for (let i = 0; i < containers.current.length; i++) {
|
|
||||||
let container = containers.current[i];
|
|
||||||
if (itemView.current.length <= container.index || itemView.current[container.index].id != container.id) {
|
|
||||||
while (containers.current.length > i) {
|
|
||||||
let popped = containers.current.pop();
|
|
||||||
removeSlot(popped.id);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else if (itemView.current[container.index].revision != container.revision) {
|
|
||||||
updateSlot(container.id, getSlot(containers.current[i]));
|
|
||||||
containers.revision = itemView.current[container.index].revision;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// place first slot
|
|
||||||
if (itemView.current.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: itemView.current.length - 1,
|
|
||||||
id: itemView.current[itemView.current.length - 1].id,
|
|
||||||
revision: itemView.current[itemView.current.length - 1].revision,
|
|
||||||
}
|
|
||||||
|
|
||||||
containers.current.push(container);
|
|
||||||
addSlot(container.id, getSlot(container));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onItemHeight = (container, height) => {
|
|
||||||
container.height = height;
|
|
||||||
updateCanvas();
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSlot = (container) => {
|
|
||||||
return (
|
return (
|
||||||
<VirtualItem style={{ top: container.top, paddingTop: GUTTER, paddingBottom: GUTTER }}>
|
<VirtualItem style={{ top: container.top, key: container.key }}>
|
||||||
<ReactResizeDetector handleHeight={true}>
|
<ReactResizeDetector handleHeight={true}>
|
||||||
{({ height }) => {
|
{({ height }) => {
|
||||||
if (typeof height !== 'undefined') {
|
if (typeof height !== 'undefined' && container.height != height) {
|
||||||
onItemHeight(container, height);
|
container.height = height;
|
||||||
|
container.heightSet = true;
|
||||||
|
layoutItems();
|
||||||
}
|
}
|
||||||
return itemRenderer(itemView.current[container.index]);
|
return itemRenderer(container.item);
|
||||||
}}
|
}}
|
||||||
</ReactResizeDetector>
|
</ReactResizeDetector>
|
||||||
</VirtualItem>
|
</VirtualItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPlacement = () => {
|
const getContainerRange = () => {
|
||||||
if (containers.current.length == 0) {
|
let top = rollHeight;
|
||||||
return null;
|
let bottom = 0;
|
||||||
|
containers.current.forEach((c) => {
|
||||||
|
if (c.top < top) {
|
||||||
|
top = c.top;
|
||||||
|
}
|
||||||
|
if (c.top + c.height > bottom) {
|
||||||
|
bottom = c.top + c.height;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { top, bottom };
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollView = (e) => {
|
||||||
|
if (containers.current.length > 0 && containers.current[0].index == 0 && !nomore.current) {
|
||||||
|
loadMore();
|
||||||
|
nomore.current = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
nomore.current = false;
|
||||||
|
}, moreDelay);
|
||||||
}
|
}
|
||||||
let top = containers.current[0].top;
|
scrollTop.current = e.target.scrollTop;
|
||||||
let bottom = containers.current[containers.current.length-1].top + containers.current[containers.current.length-1].height + 2 * GUTTER;
|
setScrollPos(e.target.scrollTop);
|
||||||
let overTop = scrollTop.current - top;
|
|
||||||
let overBottom = bottom - (scrollTop.current + viewHeight.current);
|
|
||||||
return {
|
|
||||||
position: { top, bottom, height: bottom - top },
|
|
||||||
overscan: { top: overTop, bottom: overBottom }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', height: '100%' }}>
|
<div style={{ position: 'relative', height: '100%' }}>
|
||||||
<ReactResizeDetector handleHeight={true} handleWidth={true}>
|
<ReactResizeDetector handleHeight={true} handleWidth={false}>
|
||||||
{({ height }) => {
|
{({ height }) => {
|
||||||
if (height) {
|
if (height && state.listHeight != height) {
|
||||||
viewHeight.current = height;
|
actions.setListHeight(height);
|
||||||
updateCanvas();
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VirtualListWrapper onScroll={onScrollView} onWheel={onScrollWheel} onTouchStart={onScrollWheel} >
|
<VirtualListWrapper onScroll={scrollView}
|
||||||
<div class="rollview" style={{ overflowY: 'auto' }} ref={listRef} onScroll={onScrollView}>
|
onWheel={actions.unlatch} onTouchStart={actions.unlatch} >
|
||||||
<div class="roll" style={{ height: canvasHeight }}>
|
<div class="rollview" ref={list} onScroll={scrollView}>
|
||||||
{ slots.values() }
|
<div class="roll" style={{ height: rollHeight }}>
|
||||||
|
{ state.slots }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VirtualListWrapper>
|
</VirtualListWrapper>
|
||||||
|
@ -8,6 +8,7 @@ export const VirtualListWrapper = styled.div`
|
|||||||
.rollview {
|
.rollview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
/* hide scrollbar for IE, Edge and Firefox */
|
/* hide scrollbar for IE, Edge and Firefox */
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
@ -28,4 +29,5 @@ export const VirtualItem = styled.div`
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
max-height: 1024px;
|
||||||
`;
|
`;
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { useContext, useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export function useVirtualList(id) {
|
||||||
|
|
||||||
|
const [state, setState] = useState({
|
||||||
|
view: null,
|
||||||
|
listHeight: 128,
|
||||||
|
slots: [],
|
||||||
|
latched: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const slots = useRef(new Map());
|
||||||
|
|
||||||
|
const updateState = (value) => {
|
||||||
|
setState((s) => ({ ...s, ...value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
setView: (view) => {
|
||||||
|
updateState({ view });
|
||||||
|
},
|
||||||
|
latch: () => {
|
||||||
|
updateState({ latched: true });
|
||||||
|
},
|
||||||
|
unlatch: () => {
|
||||||
|
updateState({ latched: false });
|
||||||
|
},
|
||||||
|
setListHeight: (listHeight) => {
|
||||||
|
updateState({ listHeight });
|
||||||
|
},
|
||||||
|
addSlot: (id, slot) => {
|
||||||
|
slots.current.set(id, slot);
|
||||||
|
let items = Array.from(slots.current.values());
|
||||||
|
updateState({ slots: items });
|
||||||
|
},
|
||||||
|
updateSlot: (id, slot) => {
|
||||||
|
slots.current.set(id, slot);
|
||||||
|
let items = Array.from(slots.current.values());
|
||||||
|
updateState({ slots: items });
|
||||||
|
},
|
||||||
|
removeSlot: (id) => {
|
||||||
|
slots.current.set(id, (<></>));
|
||||||
|
let items = Array.from(slots.current.values());
|
||||||
|
updateState({ slots: items });
|
||||||
|
},
|
||||||
|
clearSlots: () => {
|
||||||
|
slots.current = new Map();
|
||||||
|
let items = Array.from(slots.current.values());
|
||||||
|
updateState({ slots: items });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return { state, actions };
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user