@minht11/solid-virtual-container
Version:
Virtual list/grid for Solid-js.
138 lines (137 loc) • 4.94 kB
JavaScript
import { createSignal, createMemo, useContext, createEffect, batch, onCleanup, createComputed, } from 'solid-js';
import { createStore } from 'solid-js/store';
import { ScrollTargetContext } from '../scroll-target-context';
import { getOffsetBetweenElements } from './utils';
const getSizeFromResizeEntry = (entry) => {
let width = 0;
let height = 0;
if (entry.borderBoxSize) {
const { borderBoxSize } = entry;
const size = Array.isArray(borderBoxSize) ? borderBoxSize[0] : borderBoxSize;
width = size.inlineSize;
height = size.blockSize;
}
else {
// TODO. As of yet Safari 14 still doesn't support borderBoxSize.
// getBoundingClientRect changes in respect of css transform
// so this is partial solution, I have no way to test if this
// is working correctly.
const rect = entry.target.getBoundingClientRect();
width = rect.width;
height = rect.height;
}
return { width, height };
};
const createAxis = (axisA, axisB, flip) => {
const [main, cross] = flip ? [axisA, axisB] : [axisB, axisA];
return {
main,
cross,
};
};
const DEFAULT_SIZE = {
main: 0,
cross: 0,
};
const doCrossAxisSizeMatch = (a, b) => a.cross === b.cross;
export const createMeasurementsObserver = (props) => {
const scrollTargetContext = useContext(ScrollTargetContext);
const [containerEl, setContainerRefEl] = createSignal(undefined);
const targetEl = () => props.scrollTarget || scrollTargetContext?.scrollTarget;
const isDirectionHorizontal = createMemo(() => (props.direction || 'vertical') === 'horizontal');
const [measurements, setMeasurements] = createStore({
isMeasured: false,
mainAxisScrollValue: 0,
target: { ...DEFAULT_SIZE },
container: {
...DEFAULT_SIZE,
offsetMain: 0,
offsetCross: 0,
},
itemSize: { ...DEFAULT_SIZE },
});
const onEntry = (entry) => {
const entryTarget = entry.target;
const target = targetEl();
const container = containerEl();
const isHorizontal = isDirectionHorizontal();
const size = getSizeFromResizeEntry(entry);
const axisSize = createAxis(size.width, size.height, isHorizontal);
if (entryTarget === target) {
setMeasurements('target', axisSize);
}
else if (entryTarget === container) {
if (!doCrossAxisSizeMatch(measurements.container, axisSize) ||
!measurements.isMeasured) {
const offset = getOffsetBetweenElements(target, container);
const offsetAxis = createAxis(offset.offsetLeft, offset.offsetTop, isHorizontal);
setMeasurements('container', {
...axisSize,
offsetMain: offsetAxis.main,
offsetCross: offsetAxis.cross,
});
}
}
};
const getLiveScrollValue = () => {
const target = targetEl();
if (target) {
const value = isDirectionHorizontal()
? target.scrollLeft
: target.scrollTop;
// We are not interested in subpixel values.
return Math.floor(value);
}
return 0;
};
const ro = new ResizeObserver((entries) => {
batch(() => {
entries.forEach((entry) => onEntry(entry));
setMeasurements({
isMeasured: true,
mainAxisScrollValue: getLiveScrollValue(),
});
});
});
createComputed(() => {
if (!measurements.isMeasured) {
return;
}
const isHorizontal = isDirectionHorizontal();
const size = props.itemSize;
let itemSizeResolved;
if (typeof size === 'function') {
itemSizeResolved = size(measurements.container.cross, isHorizontal);
}
else {
itemSizeResolved = size;
}
const itemAxis = createAxis(itemSizeResolved.width || 0, itemSizeResolved.height || 0, isHorizontal);
setMeasurements('itemSize', itemAxis);
});
const onScrollHandle = () => {
setMeasurements('mainAxisScrollValue', getLiveScrollValue());
};
createEffect(() => {
const target = targetEl();
const container = containerEl();
if (!target || !container) {
return;
}
target.addEventListener('scroll', onScrollHandle);
ro.observe(target);
ro.observe(container);
onCleanup(() => {
setMeasurements('isMeasured', false);
target.removeEventListener('scroll', onScrollHandle);
ro.unobserve(target);
ro.unobserve(container);
});
});
return {
containerEl,
setContainerRefEl,
isDirectionHorizontal,
measurements,
};
};