feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position (#10646)
* Squashed * Change strategy - now pre-measure buckets offscreen, so don't need to worry about sub-bucket scroll preservation * Reduce jank on scroll, delay DOM updates until after scroll * css opt, log measure time * Trickle out queue while scrolling, flush when stopped * yay * Cleanup cleanup... * everybody... * everywhere... * Clean up cleanup! * Everybody do their share * CLEANUP! * package-lock ? * dynamic measure, todo * Fix web test * type lint * fix e2e * e2e test * Better scrollbar * Tuning, and more tunables * Tunable tweaks, more tunables * Scrollbar dots and viewport events * lint * Tweaked tunnables, use requestIdleCallback for garbage tasks, bug fixes * New tunables, and don't update url by default * Bug fixes * Bug fix, with debug * Fix flickr, fix graybox bug, reduced debug * Refactor/cleanup * Fix * naming * Final cleanup * review comment * Forgot to update this after naming change * scrubber works, with debug * cleanup * Rename scrollbar to scrubber * rename to * left over rename and change to previous album bar * bugfix addassets, comments * missing destroy(), cleanup --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
export const autoGrowHeight = (textarea: HTMLTextAreaElement, height = 'auto') => {
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.style.height = height;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
type Config = IntersectionObserverActionProperties & {
|
||||
observer?: IntersectionObserver;
|
||||
};
|
||||
type TrackedProperties = {
|
||||
root?: Element | Document | null;
|
||||
threshold?: number | number[];
|
||||
top?: string;
|
||||
right?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
};
|
||||
type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElement) => unknown;
|
||||
type OnSeperateCallback = (element: HTMLElement) => unknown;
|
||||
type IntersectionObserverActionProperties = {
|
||||
key?: string;
|
||||
onSeparate?: OnSeperateCallback;
|
||||
onIntersect?: OnIntersectCallback;
|
||||
|
||||
root?: Element | Document | null;
|
||||
threshold?: number | number[];
|
||||
top?: string;
|
||||
right?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
};
|
||||
type TaskKey = HTMLElement | string;
|
||||
|
||||
function isEquivalent(a: TrackedProperties, b: TrackedProperties) {
|
||||
return (
|
||||
a?.bottom === b?.bottom &&
|
||||
a?.top === b?.top &&
|
||||
a?.left === b?.left &&
|
||||
a?.right == b?.right &&
|
||||
a?.threshold === b?.threshold &&
|
||||
a?.root === b?.root
|
||||
);
|
||||
}
|
||||
|
||||
const elementToConfig = new Map<TaskKey, Config>();
|
||||
|
||||
const observe = (key: HTMLElement | string, target: HTMLElement, properties: IntersectionObserverActionProperties) => {
|
||||
if (!target.isConnected) {
|
||||
elementToConfig.get(key)?.observer?.unobserve(target);
|
||||
return;
|
||||
}
|
||||
const {
|
||||
root,
|
||||
threshold,
|
||||
top = '0px',
|
||||
right = '0px',
|
||||
bottom = '0px',
|
||||
left = '0px',
|
||||
onSeparate,
|
||||
onIntersect,
|
||||
} = properties;
|
||||
const rootMargin = `${top} ${right} ${bottom} ${left}`;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries: IntersectionObserverEntry[]) => {
|
||||
// This IntersectionObserver is limited to observing a single element, the one the
|
||||
// action is attached to. If there are multiple entries, it means that this
|
||||
// observer is being notified of multiple events that have occured quickly together,
|
||||
// and the latest element is the one we are interested in.
|
||||
|
||||
entries.sort((a, b) => a.time - b.time);
|
||||
|
||||
const latestEntry = entries.pop();
|
||||
if (latestEntry?.isIntersecting) {
|
||||
onIntersect?.(latestEntry);
|
||||
} else {
|
||||
onSeparate?.(target);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin,
|
||||
threshold,
|
||||
root,
|
||||
},
|
||||
);
|
||||
observer.observe(target);
|
||||
elementToConfig.set(key, { ...properties, observer });
|
||||
};
|
||||
|
||||
function configure(key: HTMLElement | string, element: HTMLElement, properties: IntersectionObserverActionProperties) {
|
||||
elementToConfig.set(key, properties);
|
||||
observe(key, element, properties);
|
||||
}
|
||||
|
||||
function _intersectionObserver(
|
||||
key: HTMLElement | string,
|
||||
element: HTMLElement,
|
||||
properties: IntersectionObserverActionProperties,
|
||||
) {
|
||||
if (properties.disabled) {
|
||||
properties.onIntersect?.(element);
|
||||
} else {
|
||||
configure(key, element, properties);
|
||||
}
|
||||
return {
|
||||
update(properties: IntersectionObserverActionProperties) {
|
||||
const config = elementToConfig.get(key);
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
if (isEquivalent(config, properties)) {
|
||||
return;
|
||||
}
|
||||
configure(key, element, properties);
|
||||
},
|
||||
destroy: () => {
|
||||
if (properties.disabled) {
|
||||
properties.onSeparate?.(element);
|
||||
} else {
|
||||
const config = elementToConfig.get(key);
|
||||
const { observer, onSeparate } = config || {};
|
||||
observer?.unobserve(element);
|
||||
elementToConfig.delete(key);
|
||||
if (onSeparate) {
|
||||
onSeparate?.(element);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function intersectionObserver(
|
||||
element: HTMLElement,
|
||||
properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[],
|
||||
) {
|
||||
// svelte doesn't allow multiple use:action directives of the same kind on the same element,
|
||||
// so accept an array when multiple configurations are needed.
|
||||
if (Array.isArray(properties)) {
|
||||
if (!properties.every((p) => p.key)) {
|
||||
throw new Error('Multiple configurations must specify key');
|
||||
}
|
||||
const observers = properties.map((p) => _intersectionObserver(p.key as string, element, p));
|
||||
return {
|
||||
update: (properties: IntersectionObserverActionProperties[]) => {
|
||||
for (const [i, props] of properties.entries()) {
|
||||
observers[i].update(props);
|
||||
}
|
||||
},
|
||||
destroy: () => {
|
||||
for (const observer of observers) {
|
||||
observer.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return _intersectionObserver(element, element, properties);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
type OnResizeCallback = (resizeEvent: { target: HTMLElement; width: number; height: number }) => void;
|
||||
|
||||
let observer: ResizeObserver;
|
||||
let callbacks: WeakMap<HTMLElement, OnResizeCallback>;
|
||||
|
||||
/**
|
||||
* Installs a resizeObserver on the given element - when the element changes
|
||||
* size, invokes a callback function with the width/height. Intended as a
|
||||
* replacement for bind:clientWidth and bind:clientHeight in svelte4 which use
|
||||
* an iframe to measure the size of the element, which can be bad for
|
||||
* performance and memory usage. In svelte5, they adapted bind:clientHeight and
|
||||
* bind:clientWidth to use an internal resize observer.
|
||||
*
|
||||
* TODO: When svelte5 is ready, go back to bind:clientWidth and
|
||||
* bind:clientHeight.
|
||||
*/
|
||||
export function resizeObserver(element: HTMLElement, onResize: OnResizeCallback) {
|
||||
if (!observer) {
|
||||
callbacks = new WeakMap();
|
||||
observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const onResize = callbacks.get(entry.target as HTMLElement);
|
||||
if (onResize) {
|
||||
onResize({
|
||||
target: entry.target as HTMLElement,
|
||||
width: entry.borderBoxSize[0].inlineSize,
|
||||
height: entry.borderBoxSize[0].blockSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
callbacks.set(element, onResize);
|
||||
observer.observe(element);
|
||||
|
||||
return {
|
||||
destroy: () => {
|
||||
callbacks.delete(element);
|
||||
observer.unobserve(element);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { decodeBase64 } from '$lib/utils';
|
||||
import { thumbHashToRGBA } from 'thumbhash';
|
||||
|
||||
export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash));
|
||||
const pixels = ctx.createImageData(w, h);
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
pixels.data.set(rgba);
|
||||
ctx.putImageData(pixels, 0, 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user