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 class="thread">
|
||||
<VirtualList id={channelId + cardId}
|
||||
items={state.topics} itemRenderer={topicRenderer} onMore={actions.more} />
|
||||
<VirtualList id={`${cardId}:${channelId}`}
|
||||
items={state.topics} itemRenderer={topicRenderer} loadMore={actions.more} />
|
||||
</div>
|
||||
<div class="divider">
|
||||
<div class="line" />
|
||||
|
@ -21,17 +21,17 @@ export function TopicItem({ host, topic }) {
|
||||
nameClass = "unknown"
|
||||
}
|
||||
|
||||
const renderAsset = (asset) => {
|
||||
const renderAsset = (asset, idx, topicId) => {
|
||||
if (asset.image) {
|
||||
return <ImageAsset thumbUrl={actions.getAssetUrl(asset.image.thumb)}
|
||||
fullUrl={actions.getAssetUrl(asset.image.full)} />
|
||||
return <ImageAsset thumbUrl={actions.getAssetUrl(asset.image.thumb, topicId)}
|
||||
fullUrl={actions.getAssetUrl(asset.image.full, topicId)} />
|
||||
}
|
||||
if (asset.video) {
|
||||
return <VideoAsset thumbUrl={actions.getAssetUrl(asset.video.thumb)}
|
||||
lqUrl={actions.getAssetUrl(asset.video.lq)} hdUrl={actions.getAssetUrl(asset.video.hd)} />
|
||||
return <VideoAsset thumbUrl={actions.getAssetUrl(asset.video.thumb, topicId)}
|
||||
lqUrl={actions.getAssetUrl(asset.video.lq, topicId)} hdUrl={actions.getAssetUrl(asset.video.hd, topicId)} />
|
||||
}
|
||||
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 <></>
|
||||
}
|
||||
@ -95,46 +95,50 @@ export function TopicItem({ host, topic }) {
|
||||
|
||||
return (
|
||||
<TopicItemWrapper>
|
||||
<div class="topic-header">
|
||||
<div class="avatar">
|
||||
<Logo width={32} height={32} radius={4} url={state.imageUrl} />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class={nameClass}>{ name }</div>
|
||||
<div>{ state.created }</div>
|
||||
</div>
|
||||
<div class="topic-options">
|
||||
<Options />
|
||||
{ state.init && (
|
||||
<>
|
||||
<div class="topic-header">
|
||||
<div class="avatar">
|
||||
<Logo width={32} height={32} radius={4} url={state.imageUrl} />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class={nameClass}>{ name }</div>
|
||||
<div>{ state.created }</div>
|
||||
</div>
|
||||
<div class="topic-options">
|
||||
<Options />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ !state.confirmed && (
|
||||
<div>
|
||||
<div class="message">
|
||||
<Skeleton size={'small'} active={true} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ state.confirmed && (
|
||||
<div>
|
||||
{ state.error && (
|
||||
<div class="asset-placeholder">
|
||||
<FireOutlined style={{ fontSize: 32, color: '#ff8888' }} />
|
||||
{ !state.confirmed && (
|
||||
<div>
|
||||
<div class="message">
|
||||
<Skeleton size={'small'} active={true} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ !state.error && !state.ready && (
|
||||
<div class="asset-placeholder">
|
||||
<PictureOutlined style={{ fontSize: 32 }} />
|
||||
{ state.confirmed && (
|
||||
<div>
|
||||
{ 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>
|
||||
)}
|
||||
{ !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>
|
||||
)
|
||||
|
@ -23,6 +23,11 @@ export function ImageAsset({ thumbUrl, fullUrl }) {
|
||||
}
|
||||
}
|
||||
|
||||
const clearPopout = (e) => {
|
||||
e.stopPropagation();
|
||||
actions.clearPopout();
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageAssetWrapper>
|
||||
<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} />
|
||||
)}
|
||||
{ state.display === 'small' && state.popout && (
|
||||
<div class="fullscreen" onClick={actions.clearPopout}>
|
||||
<div class="fullscreen" onClick={clearPopout} onTouchEnd={clearPopout}>
|
||||
<img class="image" src={fullUrl} />
|
||||
</div>
|
||||
)}
|
||||
|
@ -8,6 +8,7 @@ export function useTopicItem(topic) {
|
||||
const [guid, setGuid] = useState(null);
|
||||
|
||||
const [state, setState] = useState({
|
||||
init: false,
|
||||
name: null,
|
||||
handle: null,
|
||||
imageUrl: null,
|
||||
@ -18,6 +19,7 @@ export function useTopicItem(topic) {
|
||||
error: false,
|
||||
owner: false,
|
||||
assets: [],
|
||||
topicId: null,
|
||||
editing: false,
|
||||
busy: false,
|
||||
textColor: '#444444',
|
||||
@ -98,18 +100,18 @@ export function useTopicItem(topic) {
|
||||
|
||||
if (profile.state.profile.guid == guid) {
|
||||
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 {
|
||||
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]);
|
||||
|
||||
const actions = {
|
||||
getAssetUrl: (assetId) => {
|
||||
return conversation.actions.getAssetUrl(topic?.id, assetId);
|
||||
getAssetUrl: (assetId, topicId) => {
|
||||
return conversation.actions.getAssetUrl(state.topicId, assetId);
|
||||
},
|
||||
removeTopic: async () => {
|
||||
return await conversation.actions.removeTopic(topic.id);
|
||||
|
@ -1,338 +1,371 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { VirtualListWrapper, VirtualItem } from './VirtualList.styled';
|
||||
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 HOLDZONE = 2048; // drop slots outside of holdzone of view
|
||||
const OVERSCAN = 1024; // add slots in overscan of view
|
||||
const DEFAULT_ITEM_HEIGHT = 256;
|
||||
const DEFAULT_LIST_HEIGHT = 4096;
|
||||
const GUTTER = 1;
|
||||
const redZone = 1024;
|
||||
const holdZone = 2048;
|
||||
const fillZone = 1024;
|
||||
|
||||
const [ msg, setMsg ] = useState("YO");
|
||||
const pushDelay = 250;
|
||||
const moreDelay = 2000;
|
||||
const latchDelay = 500;
|
||||
|
||||
const [ canvasHeight, setCanvasHeight ] = useState(16384);
|
||||
const [ slots, setSlots ] = useState(new Map());
|
||||
const [ scroll, setScroll ] = useState('hidden');
|
||||
const defaultHeight = 32;
|
||||
|
||||
let update = useRef(0);
|
||||
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 rollHeight = 16384;
|
||||
|
||||
const addSlot = (id, slot) => {
|
||||
setSlots((m) => { m.set(id, slot); return new Map(m); })
|
||||
}
|
||||
const { state, actions } = useVirtualList();
|
||||
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) => {
|
||||
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() })
|
||||
}
|
||||
const [scrollPos, setScrollPos] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
updateCanvas();
|
||||
}, [canvasHeight]);
|
||||
scrollTop.current = 8192;
|
||||
list.current.scrollTo({ top: 8192, left: 0 });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (key.current != id) {
|
||||
key.current = id;
|
||||
latch.current = true;
|
||||
containers.current = [];
|
||||
clearSlots();
|
||||
}
|
||||
|
||||
// reference copy
|
||||
itemView.current = items;
|
||||
setItems();
|
||||
}, [items, id]);
|
||||
|
||||
const onScrollWheel = (e) => {
|
||||
latch.current = false;
|
||||
}
|
||||
// genearte set of active ids
|
||||
let ids = new Map();
|
||||
for (let i = 0; i < itemView.current.length; i++) {
|
||||
ids.set(getItemKey(itemView.current[i]), i);
|
||||
}
|
||||
|
||||
const onScrollView = (e) => {
|
||||
scrollTop.current = e.target.scrollTop;
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
}
|
||||
// remove any deleted items
|
||||
let slots = [];
|
||||
containers.current.forEach((container) => {
|
||||
if (!ids.has(container.key)) {
|
||||
actions.removeSlot(container.key);
|
||||
}
|
||||
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();
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
latchScroll();
|
||||
}
|
||||
loadSlots();
|
||||
releaseSlots();
|
||||
centerSlots();
|
||||
latchSlots();
|
||||
pushSlots();
|
||||
};
|
||||
|
||||
const limitScroll = () => {
|
||||
let view = getPlacement();
|
||||
if (view && containers.current[containers.current.length - 1].index == itemView.current.length - 1) {
|
||||
if (view?.overscan?.bottom <= 0) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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();
|
||||
const latchSlots = () => {
|
||||
if (containers.current.length > 0 && state.latched) {
|
||||
if (!nolatch.current) {
|
||||
const last = containers.current[containers.current.length - 1];
|
||||
const bottom = last.top + last.height;
|
||||
if (last.heightSet && scrollTop.current < bottom - state.listHeight) {
|
||||
list.current.scrollTo({ top: bottom - state.listHeight, left: 0, behavior: 'smooth' });
|
||||
nolatch.current = true;
|
||||
setTimeout(() => {
|
||||
nomore.current = false;
|
||||
}, 2500);
|
||||
nolatch.current = false;
|
||||
latchSlots();
|
||||
}, latchDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadNextSlot = () => {
|
||||
let view = getPlacement();
|
||||
if (view) {
|
||||
if (view.overscan.top < OVERSCAN) {
|
||||
if (containers.current[0].index > 0 && containers.current[0].index < itemView.current.length) {
|
||||
let below = containers.current[0];
|
||||
let container = {
|
||||
top: below.top - (DEFAULT_ITEM_HEIGHT + 2 * GUTTER),
|
||||
height: DEFAULT_ITEM_HEIGHT,
|
||||
index: containers.current[0].index - 1,
|
||||
id: itemView.current[containers.current[0].index - 1].id,
|
||||
revision: itemView.current[containers.current[0].index - 1].revision,
|
||||
const pushSlots = () => {
|
||||
if (debounce.current != null) {
|
||||
clearTimeout(debounce.current);
|
||||
};
|
||||
debounce.current = setTimeout(() => {
|
||||
if (containers.current.length > 0) {
|
||||
const range = getContainerRange();
|
||||
if (range.bottom - range.top < state.listHeight) {
|
||||
if (scrollTop.current + state.listHeight != range.bottom) {
|
||||
list.current.scrollTo({ top: range.bottom - state.listHeight, left: 0, behavior: 'smooth' });
|
||||
actions.latch();
|
||||
}
|
||||
containers.current.unshift(container);
|
||||
addSlot(container.id, getSlot(container))
|
||||
loadNextSlot();
|
||||
}
|
||||
else if (scrollTop.current + state.listHeight > range.bottom) {
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}, pushDelay);
|
||||
};
|
||||
|
||||
const alignSlots = () => {
|
||||
if (containers.current.length > 1) {
|
||||
let mid = Math.floor(containers.current.length / 2);
|
||||
|
||||
let alignTop = containers.current[mid].top + containers.current[mid].height + 2 * GUTTER;
|
||||
for (let i = mid + 1; i < containers.current.length; i++) {
|
||||
if (containers.current[i].top != alignTop) {
|
||||
containers.current[i].top = alignTop;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
if (state.latched) {
|
||||
const index = containers.current.length - 1;
|
||||
const last = containers.current[index];
|
||||
let bottom = last.top + last.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;
|
||||
}
|
||||
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;
|
||||
for (let i = mid - 1; i >= 0; i--) {
|
||||
alignBottom -= (containers.current[i].height + 2 * GUTTER);
|
||||
if (containers.current[i].top != alignBottom) {
|
||||
containers.current[i].top = alignBottom;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
const bottom = containers.current[containers.current.length - 1];
|
||||
if (bottom.top + bottom.height > rollHeight - 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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const centerCanvas = () => {
|
||||
let view = getPlacement();
|
||||
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 getItemKey = (item) => {
|
||||
return `${id}.${item.id}.${item.revision}`
|
||||
}
|
||||
|
||||
const latchScroll = () => {
|
||||
if (latch.current) {
|
||||
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) => {
|
||||
const getSlot = (item) => {
|
||||
const container = item;
|
||||
return (
|
||||
<VirtualItem style={{ top: container.top, paddingTop: GUTTER, paddingBottom: GUTTER }}>
|
||||
<VirtualItem style={{ top: container.top, key: container.key }}>
|
||||
<ReactResizeDetector handleHeight={true}>
|
||||
{({ height }) => {
|
||||
if (typeof height !== 'undefined') {
|
||||
onItemHeight(container, height);
|
||||
if (typeof height !== 'undefined' && container.height != height) {
|
||||
container.height = height;
|
||||
container.heightSet = true;
|
||||
layoutItems();
|
||||
}
|
||||
return itemRenderer(itemView.current[container.index]);
|
||||
return itemRenderer(container.item);
|
||||
}}
|
||||
</ReactResizeDetector>
|
||||
</ReactResizeDetector>
|
||||
</VirtualItem>
|
||||
)
|
||||
}
|
||||
|
||||
const getPlacement = () => {
|
||||
if (containers.current.length == 0) {
|
||||
return null;
|
||||
const getContainerRange = () => {
|
||||
let top = rollHeight;
|
||||
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;
|
||||
let bottom = containers.current[containers.current.length-1].top + containers.current[containers.current.length-1].height + 2 * GUTTER;
|
||||
let overTop = scrollTop.current - top;
|
||||
let overBottom = bottom - (scrollTop.current + viewHeight.current);
|
||||
return {
|
||||
position: { top, bottom, height: bottom - top },
|
||||
overscan: { top: overTop, bottom: overBottom }
|
||||
};
|
||||
scrollTop.current = e.target.scrollTop;
|
||||
setScrollPos(e.target.scrollTop);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100%' }}>
|
||||
<ReactResizeDetector handleHeight={true} handleWidth={true}>
|
||||
<ReactResizeDetector handleHeight={true} handleWidth={false}>
|
||||
{({ height }) => {
|
||||
if (height) {
|
||||
viewHeight.current = height;
|
||||
updateCanvas();
|
||||
if (height && state.listHeight != height) {
|
||||
actions.setListHeight(height);
|
||||
}
|
||||
return (
|
||||
<VirtualListWrapper onScroll={onScrollView} onWheel={onScrollWheel} onTouchStart={onScrollWheel} >
|
||||
<div class="rollview" style={{ overflowY: 'auto' }} ref={listRef} onScroll={onScrollView}>
|
||||
<div class="roll" style={{ height: canvasHeight }}>
|
||||
{ slots.values() }
|
||||
<VirtualListWrapper onScroll={scrollView}
|
||||
onWheel={actions.unlatch} onTouchStart={actions.unlatch} >
|
||||
<div class="rollview" ref={list} onScroll={scrollView}>
|
||||
<div class="roll" style={{ height: rollHeight }}>
|
||||
{ state.slots }
|
||||
</div>
|
||||
</div>
|
||||
</VirtualListWrapper>
|
||||
|
@ -8,6 +8,7 @@ export const VirtualListWrapper = styled.div`
|
||||
.rollview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
/* hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none;
|
||||
@ -28,4 +29,5 @@ export const VirtualItem = styled.div`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
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