@scrolloop/react 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.mjs +1 -0
- package/package.json +76 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 송재욱
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react"),t=require("react-dom"),n=require("scrolloop"),r=require("react/jsx-runtime");const i=(0,e.memo)(({count:i,itemSize:a,renderItem:o,height:s=400,overscan:c=4,className:l,style:u,onRangeChange:d})=>{let f=(0,e.useRef)(null),p=(0,e.useRef)(0),m=(0,e.useRef)(0),h=(0,e.useRef)(d),[,g]=(0,e.useState)(0),_=(0,e.useRef)({start:-1,end:-1});(0,e.useEffect)(()=>{h.current=d},[d]);let v=(0,e.useMemo)(()=>i*a,[i,a]),y=p.current,b=m.current,{renderStart:x,renderEnd:S}=(0,n.calculateVirtualRange)(y,s,a,i,c,b);(0,e.useEffect)(()=>{h.current&&(_.current.start!==x||_.current.end!==S)&&(_.current={start:x,end:S},h.current({startIndex:x,endIndex:S}))},[x,S]);let C=(0,e.useCallback)(()=>{let e=f.current;e&&(m.current=p.current,p.current=e.scrollTop,(0,t.flushSync)(()=>{g(e=>e+1)}))},[]);(0,e.useEffect)(()=>{let e=f.current;if(e)return e.addEventListener(`scroll`,C,{passive:!0}),()=>{e.removeEventListener(`scroll`,C)}},[C]);let w=(0,e.useMemo)(()=>{let t=[];for(let n=x;n<=S;n++){let r={position:`absolute`,top:n*a,left:0,right:0,height:a},i=o(n,r);(0,e.isValidElement)(i)&&t.push((0,e.cloneElement)(i,{key:n,role:`listitem`}))}return t},[x,S,a,o]);return(0,r.jsx)(`div`,{ref:f,role:`list`,className:l,style:(0,e.useMemo)(()=>({overflow:`auto`,height:s,...u}),[s,u]),children:(0,r.jsx)(`div`,{style:(0,e.useMemo)(()=>({position:`relative`,height:v,width:`100%`}),[v]),children:w})})});i.displayName=`VirtualList`;const a=(0,e.forwardRef)((t,n)=>{let{items:i,renderItem:a,itemSize:o,height:s=400,className:c,style:l,"data-ssr-list":u=!0}=t;return(0,r.jsx)(`div`,{ref:n,className:c,style:(0,e.useMemo)(()=>({overflow:`auto`,height:s,...l}),[s,l]),"data-ssr-list":u,role:`list`,children:(0,e.useMemo)(()=>i.map((e,t)=>{let n={height:o,minHeight:o};return(0,r.jsx)(`div`,{"data-item-index":t,"data-ssr-item":!0,style:n,children:a(e,t,n)},t)}),[i,a,o])})}),o=Object.assign((0,e.memo)(a),{displayName:`FullList`});function s(t){let{fetchPage:r,pageSize:i,initialPage:a,onPageLoad:o,onError:s}=t,[c]=(0,e.useState)(()=>new n.InfiniteSource({fetchPage:r,pageSize:i,initialPage:a,onPageLoad:o,onError:s})),[l,u]=(0,e.useState)(()=>c.getState());(0,e.useEffect)(()=>{let e=c.subscribe(u);return()=>{e(),c.destroy()}},[c]),(0,e.useEffect)(()=>{c.updateCallbacks({fetchPage:r,onPageLoad:o,onError:s})},[c,r,o,s]);let d=(0,e.useCallback)(e=>c.loadPage(e),[c]),f=(0,e.useCallback)(()=>c.retry(),[c]),p=(0,e.useCallback)(()=>c.reset(),[c]);return{...l,loadPage:d,retry:f,reset:p}}const c={switchTrigger:`immediate`,transitionStrategy:`replace-offscreen`,pruneStrategy:`idle`,chunkSize:10},l=e=>typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(e):setTimeout(e,1),u=e=>typeof window<`u`&&`cancelIdleCallback`in window?window.cancelIdleCallback(e):clearTimeout(e);function d(e,t,n){let r,i=!1,a=()=>{if(i)return;let o=0;e.querySelectorAll(`[data-item-index]`).forEach(e=>{if(o++>5)return;let r=parseInt(e.getAttribute(`data-item-index`)||`-1`,10);r>=0&&(r<t.start||r>t.end)&&n(r)}),o>0&&!i&&(r=l(a))};return r=l(a),()=>{i=!0,u(r)}}function f(e,t,n,r){let i,a=!1,o=()=>{if(a)return;let s=[...e.querySelectorAll(`[data-item-index]`)].filter(e=>{let n=parseInt(e.getAttribute(`data-item-index`)||`-1`,10);return n>=0&&(n<t.start||n>t.end)});s.slice(0,n).forEach(e=>r(parseInt(e.getAttribute(`data-item-index`),10))),s.length>n&&!a&&(i=setTimeout(o,16))};return i=setTimeout(o,16),()=>{a=!0,clearTimeout(i)}}function p(e,t,n){let r=e.scrollTop,i=e.clientHeight,a=Math.floor(r/t),o=document.activeElement,s=o?.id||o?.getAttribute(`data-item-id`)||null,c=new Map,l=Math.max(0,a-5),u=Math.min(n-1,a+Math.ceil(i/t)+5);for(let t=l;t<=u;t++){let n=e.querySelector(`[data-item-index="${t}"]`);n&&c.set(t,n.offsetHeight)}return{scrollTop:r,viewportHeight:i,firstVisibleIndex:a,focusedElement:o,focusedElementId:s,itemMeasurements:c}}function m(e,t){if(e.scrollTop=t.scrollTop,t.focusedElementId&&t.focusedElement){let e=document.getElementById(t.focusedElementId)||document.querySelector(`[data-item-id="${t.focusedElementId}"]`);e&&e instanceof HTMLElement&&setTimeout(()=>{e.focus()},0)}}function h({enabled:t,containerRef:n,itemSize:r,totalItems:i,visibleRange:a,strategy:o=c,onTransitionStart:s,onTransitionComplete:l,onTransitionError:u}){let[h,g]=(0,e.useState)({type:`SSR_DOM`}),_=(0,e.useRef)(!1),v=(0,e.useRef)(!1),y=(0,e.useRef)(null),b={...c,...o};return(0,e.useEffect)(()=>{if(!t||_.current)return;let e=()=>{n.current&&!_.current&&(_.current=!0,g({type:`HYDRATED`}))};e();let r=setTimeout(e,0);return()=>clearTimeout(r)},[t,n]),(0,e.useEffect)(()=>{if(!t||h.type!==`HYDRATED`)return;let e=n.current;if(!e)return;let o=()=>{try{s?.();let t=p(e,r,i);g({type:`SWITCHING`,snapshot:t}),m(e,t),y.current=b.pruneStrategy===`chunk`?f(e,a,b.chunkSize||10,()=>{}):d(e,a,()=>{}),setTimeout(()=>{g({type:`VIRTUALIZED`}),l?.()},100)}catch(e){u?.(e instanceof Error?e:Error(String(e))),b.transitionStrategy===`replace-offscreen`&&g({type:`VIRTUALIZED`})}};if(b.switchTrigger===`immediate`)o();else if(b.switchTrigger===`first-interaction`){let t=()=>{v.current||(v.current=!0,o(),[`click`,`keydown`,`scroll`].forEach(n=>e.removeEventListener(n,t)))};return[`click`,`keydown`,`scroll`].forEach(n=>e.addEventListener(n,t,{once:!0})),()=>[`click`,`keydown`,`scroll`].forEach(n=>e.removeEventListener(n,t))}else b.switchTrigger===`idle`&&(`requestIdleCallback`in window?window.requestIdleCallback(o):setTimeout(o,1e3))},[t,h.type,n,r,i,a,b,s,l,u]),(0,e.useEffect)(()=>()=>y.current?.(),[]),{state:h,isVirtualized:h.type===`VIRTUALIZED`,isSwitching:h.type===`SWITCHING`,snapshot:h.type===`SWITCHING`?h.snapshot:null}}function g(){return typeof window>`u`}function _(t){let{fetchPage:a,renderItem:c,itemSize:l,pageSize:u=20,initialPage:d=0,prefetchThreshold:f=1,height:p=400,overscan:m,className:_,style:v,renderLoading:y,renderError:b,renderEmpty:x,onPageLoad:S,onError:C,isServerSide:w=!1,transitionStrategy:T,initialData:E,initialTotal:D}=t,O=(0,e.useMemo)(()=>m??Math.max(20,u*2),[m,u]),k=(0,e.useRef)(null),A=(0,e.useRef)(0),{allItems:j,pages:M,loadingPages:N,hasMore:P,error:F,loadPage:I,retry:L}=s({fetchPage:a,pageSize:u,initialPage:d,onPageLoad:S,onError:C}),R=(0,e.useMemo)(()=>{if(!w||!E?.length)return null;let e=new Map,t=Math.ceil(E.length/u);for(let n=0;n<t;n++)e.set(n,E.slice(n*u,(n+1)*u));return{pages:e,total:D??E.length,hasMore:D?E.length<D:!0}},[w,E,D,u]),z=(0,e.useMemo)(()=>{if(R){let e=new Map(M);return R.pages.forEach((t,n)=>!e.has(n)&&e.set(n,t)),e}return M},[M,R]),B=R?Math.max(R.total,j.length):j.length,V=R&&R.hasMore||P,H=(0,e.useMemo)(()=>{if(R&&E){let e=Array(B);return E.forEach((t,n)=>e[n]=t),z.forEach((t,n)=>{let r=n*u;t.forEach((t,n)=>e[r+n]=t)}),e}return j},[R,E,B,z,u,j]);(0,e.useEffect)(()=>{if(!w&&!z.size&&!F){let e=Math.ceil(p/l)+O*2;for(let t=0;t<Math.ceil(e/u)+f;t++)I(t)}},[w,z.size,I,F,p,l,u,f,O]);let U=(0,e.useMemo)(()=>{let e=A.current,{renderStart:t,renderEnd:r}=(0,n.calculateVirtualRange)(e,p,l,H.length,O,e);return{start:t,end:r}},[p,l,H.length,O]),{isVirtualized:W}=h({enabled:w,containerRef:k,itemSize:l,totalItems:H.length,visibleRange:U,strategy:T}),G=(0,e.useCallback)(e=>{if(w&&!W){A.current=k.current?.scrollTop??0;return}let t=(0,n.findMissingPages)(e.startIndex/u|0,(e.endIndex/u|0)+f+Math.ceil(O/u),z,N);for(let e of t)I(e)},[w,W,u,f,O,z,N,I]);(0,e.useEffect)(()=>{if(!w||!k.current)return;let e=()=>{A.current=k.current?.scrollTop??0};return k.current.addEventListener(`scroll`,e,{passive:!0}),()=>k.current?.removeEventListener(`scroll`,e)},[w]);let K=(0,e.useCallback)((e,t)=>c(H[e],e,t),[H,c]),q={height:p,display:`flex`,alignItems:`center`,justifyContent:`center`},J={height:p};return F&&!H.length?b?(0,r.jsx)(`div`,{style:J,children:b(F,L)}):(0,r.jsx)(`div`,{style:q,children:(0,r.jsxs)(`div`,{style:{textAlign:`center`},children:[(0,r.jsx)(`p`,{children:`Error.`}),(0,r.jsx)(`p`,{style:{color:`#666`,fontSize:`0.9em`},children:F.message}),(0,r.jsx)(`button`,{onClick:L,style:{marginTop:8,padding:`4px 12px`,cursor:`pointer`},children:`Retry`})]})}):!H.length&&N.size?y?(0,r.jsx)(`div`,{style:J,children:y()}):(0,r.jsx)(`div`,{style:q,children:(0,r.jsx)(`p`,{children:`Loading...`})}):!H.length&&!V?x?(0,r.jsx)(`div`,{style:J,children:x()}):(0,r.jsx)(`div`,{style:q,children:(0,r.jsx)(`p`,{children:`No data.`})}):g()||w&&!W?(0,r.jsx)(o,{ref:k,items:H,renderItem:c,itemSize:l,height:p,className:_,style:v,"data-ssr-list":!0}):(0,r.jsx)(i,{count:H.length,itemSize:l,height:p,overscan:O,className:_,style:v,onRangeChange:G,renderItem:K})}const v=(0,e.memo)(_);exports.FullList=o,exports.InfiniteList=v,exports.VirtualList=i;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface Range {
|
|
5
|
+
startIndex: number;
|
|
6
|
+
endIndex: number;
|
|
7
|
+
}
|
|
8
|
+
interface PageResponse<T> {
|
|
9
|
+
items: T[];
|
|
10
|
+
total: number;
|
|
11
|
+
hasMore: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface TransitionSnapshot {
|
|
14
|
+
scrollTop: number;
|
|
15
|
+
viewportHeight: number;
|
|
16
|
+
firstVisibleIndex: number;
|
|
17
|
+
focusedElement: HTMLElement | null;
|
|
18
|
+
focusedElementId: string | null;
|
|
19
|
+
itemMeasurements: Map<number, number>;
|
|
20
|
+
}
|
|
21
|
+
interface TransitionStrategy {
|
|
22
|
+
switchTrigger?: "immediate" | "first-interaction" | "idle";
|
|
23
|
+
transitionStrategy?: "abort" | "replace-offscreen";
|
|
24
|
+
pruneStrategy?: "idle" | "chunk";
|
|
25
|
+
chunkSize?: number;
|
|
26
|
+
}
|
|
27
|
+
interface VirtualListProps {
|
|
28
|
+
count: number;
|
|
29
|
+
itemSize: number;
|
|
30
|
+
renderItem: (index: number, style: CSSProperties) => ReactNode;
|
|
31
|
+
height?: number;
|
|
32
|
+
overscan?: number;
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
onRangeChange?: (range: Range) => void;
|
|
36
|
+
}
|
|
37
|
+
interface InfiniteListProps<T> {
|
|
38
|
+
fetchPage: (page: number, size: number) => Promise<PageResponse<T>>;
|
|
39
|
+
renderItem: (item: T | undefined, index: number, style: CSSProperties) => ReactNode;
|
|
40
|
+
itemSize: number;
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
initialPage?: number;
|
|
43
|
+
prefetchThreshold?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
overscan?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
renderLoading?: () => ReactNode;
|
|
49
|
+
renderError?: (error: Error, retry: () => void) => ReactNode;
|
|
50
|
+
renderEmpty?: () => ReactNode;
|
|
51
|
+
onPageLoad?: (page: number, items: T[]) => void;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
53
|
+
isServerSide?: boolean;
|
|
54
|
+
transitionStrategy?: TransitionStrategy;
|
|
55
|
+
initialData?: T[];
|
|
56
|
+
initialTotal?: number;
|
|
57
|
+
}
|
|
58
|
+
interface FullListProps<T> {
|
|
59
|
+
items: (T | undefined)[];
|
|
60
|
+
renderItem: (item: T | undefined, index: number, style: CSSProperties) => ReactNode;
|
|
61
|
+
itemSize: number;
|
|
62
|
+
height?: number;
|
|
63
|
+
className?: string;
|
|
64
|
+
style?: CSSProperties;
|
|
65
|
+
"data-ssr-list"?: boolean;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/components/VirtualList.d.ts
|
|
69
|
+
declare const VirtualList: import("react").NamedExoticComponent<VirtualListProps>;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/components/InfiniteList.d.ts
|
|
72
|
+
declare const InfiniteList: <T>(props: InfiniteListProps<T>) => JSX.Element;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/components/FullList.d.ts
|
|
75
|
+
declare const FullListWithRef: <T>(props: FullListProps<T> & {
|
|
76
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
77
|
+
}) => JSX.Element;
|
|
78
|
+
declare const FullList: typeof FullListWithRef & {
|
|
79
|
+
displayName: string;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { FullList, InfiniteList, type InfiniteListProps, type PageResponse, type Range, type TransitionSnapshot, type TransitionStrategy, VirtualList, type VirtualListProps };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
interface Range {
|
|
5
|
+
startIndex: number;
|
|
6
|
+
endIndex: number;
|
|
7
|
+
}
|
|
8
|
+
interface PageResponse<T> {
|
|
9
|
+
items: T[];
|
|
10
|
+
total: number;
|
|
11
|
+
hasMore: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface TransitionSnapshot {
|
|
14
|
+
scrollTop: number;
|
|
15
|
+
viewportHeight: number;
|
|
16
|
+
firstVisibleIndex: number;
|
|
17
|
+
focusedElement: HTMLElement | null;
|
|
18
|
+
focusedElementId: string | null;
|
|
19
|
+
itemMeasurements: Map<number, number>;
|
|
20
|
+
}
|
|
21
|
+
interface TransitionStrategy {
|
|
22
|
+
switchTrigger?: "immediate" | "first-interaction" | "idle";
|
|
23
|
+
transitionStrategy?: "abort" | "replace-offscreen";
|
|
24
|
+
pruneStrategy?: "idle" | "chunk";
|
|
25
|
+
chunkSize?: number;
|
|
26
|
+
}
|
|
27
|
+
interface VirtualListProps {
|
|
28
|
+
count: number;
|
|
29
|
+
itemSize: number;
|
|
30
|
+
renderItem: (index: number, style: CSSProperties) => ReactNode;
|
|
31
|
+
height?: number;
|
|
32
|
+
overscan?: number;
|
|
33
|
+
className?: string;
|
|
34
|
+
style?: CSSProperties;
|
|
35
|
+
onRangeChange?: (range: Range) => void;
|
|
36
|
+
}
|
|
37
|
+
interface InfiniteListProps<T> {
|
|
38
|
+
fetchPage: (page: number, size: number) => Promise<PageResponse<T>>;
|
|
39
|
+
renderItem: (item: T | undefined, index: number, style: CSSProperties) => ReactNode;
|
|
40
|
+
itemSize: number;
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
initialPage?: number;
|
|
43
|
+
prefetchThreshold?: number;
|
|
44
|
+
height?: number;
|
|
45
|
+
overscan?: number;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
renderLoading?: () => ReactNode;
|
|
49
|
+
renderError?: (error: Error, retry: () => void) => ReactNode;
|
|
50
|
+
renderEmpty?: () => ReactNode;
|
|
51
|
+
onPageLoad?: (page: number, items: T[]) => void;
|
|
52
|
+
onError?: (error: Error) => void;
|
|
53
|
+
isServerSide?: boolean;
|
|
54
|
+
transitionStrategy?: TransitionStrategy;
|
|
55
|
+
initialData?: T[];
|
|
56
|
+
initialTotal?: number;
|
|
57
|
+
}
|
|
58
|
+
interface FullListProps<T> {
|
|
59
|
+
items: (T | undefined)[];
|
|
60
|
+
renderItem: (item: T | undefined, index: number, style: CSSProperties) => ReactNode;
|
|
61
|
+
itemSize: number;
|
|
62
|
+
height?: number;
|
|
63
|
+
className?: string;
|
|
64
|
+
style?: CSSProperties;
|
|
65
|
+
"data-ssr-list"?: boolean;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/components/VirtualList.d.ts
|
|
69
|
+
declare const VirtualList: import("react").NamedExoticComponent<VirtualListProps>;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/components/InfiniteList.d.ts
|
|
72
|
+
declare const InfiniteList: <T>(props: InfiniteListProps<T>) => JSX.Element;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/components/FullList.d.ts
|
|
75
|
+
declare const FullListWithRef: <T>(props: FullListProps<T> & {
|
|
76
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
77
|
+
}) => JSX.Element;
|
|
78
|
+
declare const FullList: typeof FullListWithRef & {
|
|
79
|
+
displayName: string;
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|
|
82
|
+
export { FullList, InfiniteList, type InfiniteListProps, type PageResponse, type Range, type TransitionSnapshot, type TransitionStrategy, VirtualList, type VirtualListProps };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{cloneElement as e,forwardRef as t,isValidElement as n,memo as r,useCallback as i,useEffect as a,useMemo as o,useRef as s,useState as c}from"react";import{flushSync as l}from"react-dom";import{InfiniteSource as u,calculateVirtualRange as d,findMissingPages as f}from"scrolloop";import{jsx as p,jsxs as m}from"react/jsx-runtime";const h=r(({count:t,itemSize:r,renderItem:u,height:f=400,overscan:m=4,className:h,style:g,onRangeChange:_})=>{let v=s(null),y=s(0),b=s(0),x=s(_),[,S]=c(0),C=s({start:-1,end:-1});a(()=>{x.current=_},[_]);let w=o(()=>t*r,[t,r]),T=y.current,E=b.current,{renderStart:D,renderEnd:O}=d(T,f,r,t,m,E);a(()=>{x.current&&(C.current.start!==D||C.current.end!==O)&&(C.current={start:D,end:O},x.current({startIndex:D,endIndex:O}))},[D,O]);let k=i(()=>{let e=v.current;e&&(b.current=y.current,y.current=e.scrollTop,l(()=>{S(e=>e+1)}))},[]);a(()=>{let e=v.current;if(e)return e.addEventListener(`scroll`,k,{passive:!0}),()=>{e.removeEventListener(`scroll`,k)}},[k]);let A=o(()=>{let t=[];for(let i=D;i<=O;i++){let a={position:`absolute`,top:i*r,left:0,right:0,height:r},o=u(i,a);n(o)&&t.push(e(o,{key:i,role:`listitem`}))}return t},[D,O,r,u]);return p(`div`,{ref:v,role:`list`,className:h,style:o(()=>({overflow:`auto`,height:f,...g}),[f,g]),children:p(`div`,{style:o(()=>({position:`relative`,height:w,width:`100%`}),[w]),children:A})})});h.displayName=`VirtualList`;const g=t((e,t)=>{let{items:n,renderItem:r,itemSize:i,height:a=400,className:s,style:c,"data-ssr-list":l=!0}=e;return p(`div`,{ref:t,className:s,style:o(()=>({overflow:`auto`,height:a,...c}),[a,c]),"data-ssr-list":l,role:`list`,children:o(()=>n.map((e,t)=>{let n={height:i,minHeight:i};return p(`div`,{"data-item-index":t,"data-ssr-item":!0,style:n,children:r(e,t,n)},t)}),[n,r,i])})}),_=Object.assign(r(g),{displayName:`FullList`});function v(e){let{fetchPage:t,pageSize:n,initialPage:r,onPageLoad:o,onError:s}=e,[l]=c(()=>new u({fetchPage:t,pageSize:n,initialPage:r,onPageLoad:o,onError:s})),[d,f]=c(()=>l.getState());a(()=>{let e=l.subscribe(f);return()=>{e(),l.destroy()}},[l]),a(()=>{l.updateCallbacks({fetchPage:t,onPageLoad:o,onError:s})},[l,t,o,s]);let p=i(e=>l.loadPage(e),[l]),m=i(()=>l.retry(),[l]),h=i(()=>l.reset(),[l]);return{...d,loadPage:p,retry:m,reset:h}}const y={switchTrigger:`immediate`,transitionStrategy:`replace-offscreen`,pruneStrategy:`idle`,chunkSize:10},b=e=>typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(e):setTimeout(e,1),x=e=>typeof window<`u`&&`cancelIdleCallback`in window?window.cancelIdleCallback(e):clearTimeout(e);function S(e,t,n){let r,i=!1,a=()=>{if(i)return;let o=0;e.querySelectorAll(`[data-item-index]`).forEach(e=>{if(o++>5)return;let r=parseInt(e.getAttribute(`data-item-index`)||`-1`,10);r>=0&&(r<t.start||r>t.end)&&n(r)}),o>0&&!i&&(r=b(a))};return r=b(a),()=>{i=!0,x(r)}}function C(e,t,n,r){let i,a=!1,o=()=>{if(a)return;let s=[...e.querySelectorAll(`[data-item-index]`)].filter(e=>{let n=parseInt(e.getAttribute(`data-item-index`)||`-1`,10);return n>=0&&(n<t.start||n>t.end)});s.slice(0,n).forEach(e=>r(parseInt(e.getAttribute(`data-item-index`),10))),s.length>n&&!a&&(i=setTimeout(o,16))};return i=setTimeout(o,16),()=>{a=!0,clearTimeout(i)}}function w(e,t,n){let r=e.scrollTop,i=e.clientHeight,a=Math.floor(r/t),o=document.activeElement,s=o?.id||o?.getAttribute(`data-item-id`)||null,c=new Map,l=Math.max(0,a-5),u=Math.min(n-1,a+Math.ceil(i/t)+5);for(let t=l;t<=u;t++){let n=e.querySelector(`[data-item-index="${t}"]`);n&&c.set(t,n.offsetHeight)}return{scrollTop:r,viewportHeight:i,firstVisibleIndex:a,focusedElement:o,focusedElementId:s,itemMeasurements:c}}function T(e,t){if(e.scrollTop=t.scrollTop,t.focusedElementId&&t.focusedElement){let e=document.getElementById(t.focusedElementId)||document.querySelector(`[data-item-id="${t.focusedElementId}"]`);e&&e instanceof HTMLElement&&setTimeout(()=>{e.focus()},0)}}function E({enabled:e,containerRef:t,itemSize:n,totalItems:r,visibleRange:i,strategy:o=y,onTransitionStart:l,onTransitionComplete:u,onTransitionError:d}){let[f,p]=c({type:`SSR_DOM`}),m=s(!1),h=s(!1),g=s(null),_={...y,...o};return a(()=>{if(!e||m.current)return;let n=()=>{t.current&&!m.current&&(m.current=!0,p({type:`HYDRATED`}))};n();let r=setTimeout(n,0);return()=>clearTimeout(r)},[e,t]),a(()=>{if(!e||f.type!==`HYDRATED`)return;let a=t.current;if(!a)return;let o=()=>{try{l?.();let e=w(a,n,r);p({type:`SWITCHING`,snapshot:e}),T(a,e),g.current=_.pruneStrategy===`chunk`?C(a,i,_.chunkSize||10,()=>{}):S(a,i,()=>{}),setTimeout(()=>{p({type:`VIRTUALIZED`}),u?.()},100)}catch(e){d?.(e instanceof Error?e:Error(String(e))),_.transitionStrategy===`replace-offscreen`&&p({type:`VIRTUALIZED`})}};if(_.switchTrigger===`immediate`)o();else if(_.switchTrigger===`first-interaction`){let e=()=>{h.current||(h.current=!0,o(),[`click`,`keydown`,`scroll`].forEach(t=>a.removeEventListener(t,e)))};return[`click`,`keydown`,`scroll`].forEach(t=>a.addEventListener(t,e,{once:!0})),()=>[`click`,`keydown`,`scroll`].forEach(t=>a.removeEventListener(t,e))}else _.switchTrigger===`idle`&&(`requestIdleCallback`in window?window.requestIdleCallback(o):setTimeout(o,1e3))},[e,f.type,t,n,r,i,_,l,u,d]),a(()=>()=>g.current?.(),[]),{state:f,isVirtualized:f.type===`VIRTUALIZED`,isSwitching:f.type===`SWITCHING`,snapshot:f.type===`SWITCHING`?f.snapshot:null}}function D(){return typeof window>`u`}function O(e){let{fetchPage:t,renderItem:n,itemSize:r,pageSize:c=20,initialPage:l=0,prefetchThreshold:u=1,height:g=400,overscan:y,className:b,style:x,renderLoading:S,renderError:C,renderEmpty:w,onPageLoad:T,onError:O,isServerSide:k=!1,transitionStrategy:A,initialData:j,initialTotal:M}=e,N=o(()=>y??Math.max(20,c*2),[y,c]),P=s(null),F=s(0),{allItems:I,pages:L,loadingPages:R,hasMore:z,error:B,loadPage:V,retry:H}=v({fetchPage:t,pageSize:c,initialPage:l,onPageLoad:T,onError:O}),U=o(()=>{if(!k||!j?.length)return null;let e=new Map,t=Math.ceil(j.length/c);for(let n=0;n<t;n++)e.set(n,j.slice(n*c,(n+1)*c));return{pages:e,total:M??j.length,hasMore:M?j.length<M:!0}},[k,j,M,c]),W=o(()=>{if(U){let e=new Map(L);return U.pages.forEach((t,n)=>!e.has(n)&&e.set(n,t)),e}return L},[L,U]),G=U?Math.max(U.total,I.length):I.length,K=U&&U.hasMore||z,q=o(()=>{if(U&&j){let e=Array(G);return j.forEach((t,n)=>e[n]=t),W.forEach((t,n)=>{let r=n*c;t.forEach((t,n)=>e[r+n]=t)}),e}return I},[U,j,G,W,c,I]);a(()=>{if(!k&&!W.size&&!B){let e=Math.ceil(g/r)+N*2;for(let t=0;t<Math.ceil(e/c)+u;t++)V(t)}},[k,W.size,V,B,g,r,c,u,N]);let J=o(()=>{let e=F.current,{renderStart:t,renderEnd:n}=d(e,g,r,q.length,N,e);return{start:t,end:n}},[g,r,q.length,N]),{isVirtualized:Y}=E({enabled:k,containerRef:P,itemSize:r,totalItems:q.length,visibleRange:J,strategy:A}),X=i(e=>{if(k&&!Y){F.current=P.current?.scrollTop??0;return}let t=f(e.startIndex/c|0,(e.endIndex/c|0)+u+Math.ceil(N/c),W,R);for(let e of t)V(e)},[k,Y,c,u,N,W,R,V]);a(()=>{if(!k||!P.current)return;let e=()=>{F.current=P.current?.scrollTop??0};return P.current.addEventListener(`scroll`,e,{passive:!0}),()=>P.current?.removeEventListener(`scroll`,e)},[k]);let Z=i((e,t)=>n(q[e],e,t),[q,n]),Q={height:g,display:`flex`,alignItems:`center`,justifyContent:`center`},$={height:g};return B&&!q.length?C?p(`div`,{style:$,children:C(B,H)}):p(`div`,{style:Q,children:m(`div`,{style:{textAlign:`center`},children:[p(`p`,{children:`Error.`}),p(`p`,{style:{color:`#666`,fontSize:`0.9em`},children:B.message}),p(`button`,{onClick:H,style:{marginTop:8,padding:`4px 12px`,cursor:`pointer`},children:`Retry`})]})}):!q.length&&R.size?S?p(`div`,{style:$,children:S()}):p(`div`,{style:Q,children:p(`p`,{children:`Loading...`})}):!q.length&&!K?w?p(`div`,{style:$,children:w()}):p(`div`,{style:Q,children:p(`p`,{children:`No data.`})}):D()||k&&!Y?p(_,{ref:P,items:q,renderItem:n,itemSize:r,height:g,className:b,style:x,"data-ssr-list":!0}):p(h,{count:q.length,itemSize:r,height:g,overscan:N,className:b,style:x,onRangeChange:X,renderItem:Z})}const k=r(O);export{_ as FullList,k as InfiniteList,h as VirtualList};
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scrolloop/react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React adapter for scrolloop",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/zaewc/scrolloop.git",
|
|
8
|
+
"directory": "packages/react"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.cts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./dist/index.d.cts",
|
|
22
|
+
"default": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18.0.0",
|
|
32
|
+
"react-dom": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"scrolloop": "1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@playwright/test": "^1.57.0",
|
|
39
|
+
"@testing-library/jest-dom": "^6.0.0",
|
|
40
|
+
"@testing-library/react": "^14.0.0",
|
|
41
|
+
"@testing-library/user-event": "^14.0.0",
|
|
42
|
+
"@types/express": "^4.17.21",
|
|
43
|
+
"@types/react": "^18.2.0",
|
|
44
|
+
"@types/react-dom": "^18.2.0",
|
|
45
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
46
|
+
"esbuild": "^0.27.2",
|
|
47
|
+
"express": "^4.18.2",
|
|
48
|
+
"jsdom": "^24.0.0",
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"react-dom": "^18.2.0",
|
|
51
|
+
"tsx": "^4.7.0",
|
|
52
|
+
"typescript": "^5.0.0",
|
|
53
|
+
"vitest": "^2.0.0"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20.19.0"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"provenance": true
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsdown",
|
|
65
|
+
"dev": "tsdown --watch",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest",
|
|
68
|
+
"test:ssr": "vitest run src/__tests__/ssr/ssr.test.ts",
|
|
69
|
+
"test:e2e": "playwright test",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"size": "size-limit",
|
|
72
|
+
"lint:publint": "publint --strict",
|
|
73
|
+
"lint:attw": "attw --pack --profile node16",
|
|
74
|
+
"lint:package": "pnpm run lint:publint && pnpm run lint:attw"
|
|
75
|
+
}
|
|
76
|
+
}
|