Yay lazy loading

At long last.
This commit is contained in:
Simon Cambier
2025-02-15 15:54:43 +01:00
parent 89786b712e
commit 7925e63106
3 changed files with 253 additions and 19 deletions

View File

@@ -28,6 +28,7 @@
import { cancelable, CancelablePromise } from 'cancelable-promise'
import { debounce } from 'lodash-es'
import type OmnisearchPlugin from '../main'
import LazyLoader from './lazy-loader/LazyLoader.svelte'
export let modal: OmnisearchVaultModal
export let previousQuery: string | undefined
@@ -111,9 +112,7 @@
async function prevSearchHistory() {
// Filter out the empty string, if it's there
const history = (await plugin.searchHistory.getHistory()).filter(
s => s
)
const history = (await plugin.searchHistory.getHistory()).filter(s => s)
if (++historySearchIndex >= history.length) {
historySearchIndex = 0
}
@@ -122,9 +121,7 @@
}
async function nextSearchHistory() {
const history = (await plugin.searchHistory.getHistory()).filter(
s => s
)
const history = (await plugin.searchHistory.getHistory()).filter(s => s)
if (--historySearchIndex < 0) {
historySearchIndex = history.length ? history.length - 1 : 0
}
@@ -240,10 +237,18 @@
// Generate link
let link: string
if (file && active) {
link = plugin.app.fileManager.generateMarkdownLink(file, active.path, "", selectedNote.displayTitle)
link = plugin.app.fileManager.generateMarkdownLink(
file,
active.path,
'',
selectedNote.displayTitle
)
} else {
const maybeDisplayTitle = selectedNote.displayTitle === "" ? "" : `|${selectedNote.displayTitle}`
link = `[[${selectedNote.basename}.${getExtension(selectedNote.path)}${maybeDisplayTitle}]]`
const maybeDisplayTitle =
selectedNote.displayTitle === '' ? '' : `|${selectedNote.displayTitle}`
link = `[[${selectedNote.basename}.${getExtension(
selectedNote.path
)}${maybeDisplayTitle}]]`
}
// Inject link
@@ -323,8 +328,13 @@
<ModalContainer>
{#each resultNotes as result, i}
<LazyLoader
height="{100}"
offset="{500}"
keep="{true}"
fadeOption="{{ delay: 0, duration: 0 }}">
<ResultItemVault
{plugin}
plugin="{plugin}"
selected="{i === selectedIndex}"
note="{result}"
on:mousemove="{_ => (selectedIndex = i)}"
@@ -332,6 +342,7 @@
on:auxclick="{evt => {
if (evt.button == 1) openNoteInNewPane()
}}" />
</LazyLoader>
{/each}
<div style="text-align: center;">
{#if !resultNotes.length && searchQuery && !searching}

View File

@@ -0,0 +1,208 @@
<div use:load class={rootClass} style="height: {rootInitialHeight}">
{#if loaded}
<div
in:fade={fadeOption || {}}
class={contentClass}
style={contentStyle}
>
<slot>Lazy load content</slot>
</div>
{#if !contentShow && placeholder}
<Placeholder {placeholder} {placeholderProps} />
{/if}
{:else if placeholder}
<Placeholder {placeholder} {placeholderProps} />
{/if}
</div>
<script>
// https://github.com/leafOfTree/svelte-lazy
import { fade } from 'svelte/transition';
import Placeholder from './Placeholder.svelte';
export let keep = false;
export let height = 0;
export let offset = 150;
export let fadeOption = {
delay: 0,
duration: 400,
};
export let resetHeightDelay = 0;
export let onload = null;
export let placeholder = null;
export let placeholderProps = null;
let className = '';
export { className as class };
const rootClass = 'svelte-lazy'
+ (className ? ' ' + className : '');
const contentClass = 'svelte-lazy-content';
const rootInitialHeight = getStyleHeight();
let loaded = false;
let contentShow = true;
$: contentStyle = !contentShow ? 'display: none' : '';
function load(node) {
setHeight(node);
const handler = createHandler(node);
addListeners(handler);
setTimeout(() => {
handler();
});
const observer = observeNode(node, handler);
return {
destroy: () => {
removeListeners(handler);
observer.unobserve(node);
},
};
}
function createHandler(node) {
const handler = throttle(e => {
const nodeTop = node.getBoundingClientRect().top;
const nodeBottom = node.getBoundingClientRect().bottom;
const expectedTop = getContainerHeight(e) + offset;
if (nodeTop <= expectedTop && nodeBottom > 0) {
loadNode(node);
} else if (!keep) {
unload(node)
}
}, 200);
return handler;
}
function observeNode(node, handler) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadNode(node);
}
})
observer.observe(node);
return observer;
}
function unload(node) {
setHeight(node);
loaded = false
}
function loadNode(node, handler) {
if (loaded) {
return;
}
loaded = true;
resetHeight(node);
if (onload) {
onload(node);
}
}
function addListeners(handler) {
document.addEventListener('scroll', handler, true);
window.addEventListener('resize', handler);
}
function removeListeners(handler) {
document.removeEventListener('scroll', handler, true);
window.removeEventListener('resize', handler);
}
function getStyleHeight() {
return (typeof height === 'number')
? height + 'px'
: height;
}
function setHeight(node) {
if (height) {
node.style.height = getStyleHeight();
}
}
function resetHeight(node) {
setTimeout(() => {
const isLoading = checkImgLoadingStatus(node);
if (!isLoading) {
node.style.height = 'auto';
}
// Add a delay to wait for remote resources like images to load
}, resetHeightDelay);
}
function checkImgLoadingStatus(node) {
const img = node.querySelector('img');
if (!img) {
return false
}
if (!img.complete) {
contentShow = false;
node.addEventListener('load', () => {
// Use auto height if loading successfully
contentShow = true;
node.style.height = 'auto';
}, { capture: true, once: true });
node.addEventListener('error', () => {
// Show content with fixed height if there is error
contentShow = true;
}, { capture: true, once: true });
return true;
}
if (img.naturalHeight === 0) {
// Use fixed height if img has zero height
return true;
}
return false;
}
function getContainerHeight(e) {
if (e?.target?.getBoundingClientRect) {
return e.target.getBoundingClientRect().bottom;
} else {
return window.innerHeight;
}
}
// From underscore souce code
function throttle(func, wait, options) {
let context, args, result;
let timeout = null;
let previous = 0;
if (!options) options = {};
const later = function() {
previous = options.leading === false ? 0 : new Date();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function(event) {
const now = new Date();
if (!previous && options.leading === false) previous = now;
const remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
}
</script>

View File

@@ -0,0 +1,15 @@
{#if placeholder}
<div class={placeholderClass}>
{#if typeof placeholder === 'string'}
<div>{placeholder}</div>
{:else if ['function', 'object'].includes(typeof placeholder)}
<svelte:component this={placeholder} {...placeholderProps} />
{/if}
</div>
{/if}
<script>
export let placeholder = null;
export let placeholderProps = null;
const placeholderClass = 'svelte-lazy-placeholder';
</script>