lazy-thumbnails
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
|
import { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
|
||||||
import { getTagColorStyle } from '../utils/colors';
|
import { getTagColorStyle } from '../utils/colors';
|
||||||
import { DownloadIcon, EditIcon, ViewListIcon, ViewGridIcon, FolderIcon, TrashIcon } from '../ui/icons';
|
import { DownloadIcon, EditIcon, ViewListIcon, ViewGridIcon, FolderIcon, TrashIcon } from '../ui/icons';
|
||||||
@@ -13,13 +13,62 @@ const getPageCount = (doc) =>
|
|||||||
? doc.current_version.metadata.page_count
|
? doc.current_version.metadata.page_count
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Detects when an element becomes visible within a scroll container.
|
||||||
|
const useLazyVisibility = (rootRef, resetKey) => {
|
||||||
|
const targetRef = useRef(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, [resetKey]);
|
||||||
|
|
||||||
|
const rootNode = rootRef?.current || null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const element = targetRef.current;
|
||||||
|
if (!element) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (typeof window === 'undefined' || typeof IntersectionObserver === 'undefined') {
|
||||||
|
setIsVisible(true);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setIsVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
root: rootNode,
|
||||||
|
rootMargin: '200px 0px',
|
||||||
|
threshold: 0.01,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(element);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [isVisible, rootNode, resetKey]);
|
||||||
|
|
||||||
|
return { ref: targetRef, isVisible };
|
||||||
|
};
|
||||||
|
|
||||||
const DocumentThumbnailImage = ({
|
const DocumentThumbnailImage = ({
|
||||||
document,
|
document,
|
||||||
ensureAssetUrl,
|
ensureAssetUrl,
|
||||||
getDocumentAsset,
|
getDocumentAsset,
|
||||||
alt,
|
alt,
|
||||||
maxSize = LIST_ICON_SIZE,
|
maxSize = LIST_ICON_SIZE,
|
||||||
|
scrollRootRef = null,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { ref: visibilityRef, isVisible } = useLazyVisibility(scrollRootRef, document?.id);
|
||||||
const resolvedMaxSize = Math.max(1, Math.round(maxSize || 1));
|
const resolvedMaxSize = Math.max(1, Math.round(maxSize || 1));
|
||||||
const thumbnailAsset = useMemo(
|
const thumbnailAsset = useMemo(
|
||||||
() => getAssetFromVersion(document?.current_version, 'thumbnail'),
|
() => getAssetFromVersion(document?.current_version, 'thumbnail'),
|
||||||
@@ -45,14 +94,15 @@ const DocumentThumbnailImage = ({
|
|||||||
() => ({ width: `${dimensions.width}px`, height: `${dimensions.height}px` }),
|
() => ({ width: `${dimensions.width}px`, height: `${dimensions.height}px` }),
|
||||||
[dimensions.height, dimensions.width],
|
[dimensions.height, dimensions.width],
|
||||||
);
|
);
|
||||||
const url = useMemo(
|
const url = useMemo(() => {
|
||||||
() =>
|
if (!isVisible) {
|
||||||
resolveDocumentAssetUrl(document, 'thumbnail', {
|
return null;
|
||||||
ensureAssetUrl,
|
}
|
||||||
getAsset: getDocumentAsset,
|
return resolveDocumentAssetUrl(document, 'thumbnail', {
|
||||||
}),
|
ensureAssetUrl,
|
||||||
[document, ensureAssetUrl, getDocumentAsset],
|
getAsset: getDocumentAsset,
|
||||||
);
|
});
|
||||||
|
}, [document, ensureAssetUrl, getDocumentAsset, isVisible]);
|
||||||
|
|
||||||
const pageCount = getPageCount(document);
|
const pageCount = getPageCount(document);
|
||||||
const showMultiPageBadge = Number.isFinite(pageCount) && pageCount > 1;
|
const showMultiPageBadge = Number.isFinite(pageCount) && pageCount > 1;
|
||||||
@@ -62,13 +112,15 @@ const DocumentThumbnailImage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="document-thumbnail-wrapper">
|
<div className="document-thumbnail-wrapper" ref={visibilityRef}>
|
||||||
<div className={innerClasses.join(' ')} style={innerStyle}>
|
<div className={innerClasses.join(' ')} style={innerStyle}>
|
||||||
{url ? (
|
{url ? (
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
alt={alt || ''}
|
alt={alt || ''}
|
||||||
className="document-thumbnail"
|
className="document-thumbnail"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
draggable={false}
|
draggable={false}
|
||||||
onDragStart={(event) => event.preventDefault()}
|
onDragStart={(event) => event.preventDefault()}
|
||||||
/>
|
/>
|
||||||
@@ -142,6 +194,18 @@ const DocumentsTable = ({
|
|||||||
[draggingDocumentIds],
|
[draggingDocumentIds],
|
||||||
);
|
);
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
|
const [, forceVisibilityTick] = useState(0);
|
||||||
|
const lastScrollNodeRef = useRef(null);
|
||||||
|
const assignScrollRef = useCallback((node) => {
|
||||||
|
if (lastScrollNodeRef.current === node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastScrollNodeRef.current = node;
|
||||||
|
scrollRef.current = node;
|
||||||
|
if (node) {
|
||||||
|
forceVisibilityTick((value) => value + 1);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const isGridView = viewMode === 'grid';
|
const isGridView = viewMode === 'grid';
|
||||||
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
||||||
const handleSetViewMode = useCallback(
|
const handleSetViewMode = useCallback(
|
||||||
@@ -329,7 +393,7 @@ const DocumentsTable = ({
|
|||||||
)}
|
)}
|
||||||
<div className="column-body">
|
<div className="column-body">
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={assignScrollRef}
|
||||||
className="documents-scroll"
|
className="documents-scroll"
|
||||||
onFocus={(event) => {
|
onFocus={(event) => {
|
||||||
if (event.target === scrollRef.current) {
|
if (event.target === scrollRef.current) {
|
||||||
@@ -454,6 +518,7 @@ const DocumentsTable = ({
|
|||||||
getDocumentAsset={getDocumentAsset}
|
getDocumentAsset={getDocumentAsset}
|
||||||
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
||||||
maxSize={gridIconSize}
|
maxSize={gridIconSize}
|
||||||
|
scrollRootRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
<div className="document-card__meta">
|
<div className="document-card__meta">
|
||||||
<div
|
<div
|
||||||
@@ -670,6 +735,7 @@ const DocumentsTable = ({
|
|||||||
ensureAssetUrl={ensureAssetUrl}
|
ensureAssetUrl={ensureAssetUrl}
|
||||||
getDocumentAsset={getDocumentAsset}
|
getDocumentAsset={getDocumentAsset}
|
||||||
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
alt={`Thumbnail for ${doc.title || doc.original_name}`}
|
||||||
|
scrollRootRef={scrollRef}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="doc-list__name">
|
<td className="doc-list__name">
|
||||||
|
|||||||
Reference in New Issue
Block a user