@ceriousdevtech/react-cerious-scroll 1.0.7 → 1.1.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/CHANGELOG.md +12 -0
- package/README.md +36 -3
- package/dist/cerious-scroll.d.ts +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +179 -147
- package/dist/use-cerious-scroll.d.ts +10 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,18 @@ All notable changes to react-cerious-scroll will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Declarative canonical and dynamic Masonry support. The wrapper supplies the core DOM `renderItem` callback while consumers continue returning React nodes from the existing `renderItem` prop.
|
|
12
|
+
- `jumpToItem(index, screenOffset?)` on the hook result and component ref.
|
|
13
|
+
- `CeriousScrollOptions` now accepts Masonry geometry without requiring the engine's imperative DOM callback.
|
|
14
|
+
- Canonical-height and dynamic-height Masonry demo routes and regression tests for both rendering paths.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Updated `@ceriousdevtech/cerious-scroll` to `^1.1.0` and re-exported the new Masonry and height-provider types.
|
|
18
|
+
- Masonry card-count changes recreate card-derived segment state; list and table count changes continue updating in place.
|
|
19
|
+
|
|
8
20
|
## [1.0.7] - 2026-06-24
|
|
9
21
|
|
|
10
22
|
### Changed
|
package/README.md
CHANGED
|
@@ -110,7 +110,7 @@ function List() {
|
|
|
110
110
|
| `totalElements` | `number` | Total item count. Required if `items` is omitted. |
|
|
111
111
|
| `getItem` | `(index) => TItem` | Lazy item getter for large/sparse datasets. |
|
|
112
112
|
| `tableHeader` | `ReactNode` | Table mode only. A `<tr>` of `<th>`s rendered into the engine's `<thead>` (see [Table layout](#table-layout)). |
|
|
113
|
-
| `options` | `CeriousScrollOptions` | Engine options
|
|
113
|
+
| `options` | `CeriousScrollOptions` | Engine options. Masonry's DOM callback is supplied by the wrapper. Read once at creation. |
|
|
114
114
|
| `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
|
|
115
115
|
| `onViewportChange` | `(detail) => void` | Normalized viewport-change callback. |
|
|
116
116
|
| `onMeasuredViewport` | `(range) => void` | Measured range after each render pass. |
|
|
@@ -122,6 +122,7 @@ function List() {
|
|
|
122
122
|
```tsx
|
|
123
123
|
const ref = useRef<CeriousScrollHandle>(null);
|
|
124
124
|
// ref.current?.jumpToElement(500);
|
|
125
|
+
// ref.current?.jumpToItem(500); // Masonry cards
|
|
125
126
|
// ref.current?.scrollToPercentage(50);
|
|
126
127
|
// ref.current?.reset();
|
|
127
128
|
// ref.current?.render();
|
|
@@ -131,6 +132,38 @@ const ref = useRef<CeriousScrollHandle>(null);
|
|
|
131
132
|
|
|
132
133
|
---
|
|
133
134
|
|
|
135
|
+
## Masonry layout
|
|
136
|
+
|
|
137
|
+
Set `layout: 'masonry'` and provide Masonry geometry without a DOM
|
|
138
|
+
`renderItem`; the wrapper connects your existing React render prop to the core
|
|
139
|
+
renderer. Supplying `getItemHeight` selects canonical placement:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<CeriousScroll
|
|
143
|
+
ref={ref}
|
|
144
|
+
className="gallery"
|
|
145
|
+
totalElements={photos.length}
|
|
146
|
+
getItem={(index) => photos[index]}
|
|
147
|
+
options={{
|
|
148
|
+
layout: 'masonry',
|
|
149
|
+
masonry: {
|
|
150
|
+
getItemHeight: (_index, width) => width * 0.75 + 48,
|
|
151
|
+
targetColumnWidth: 280,
|
|
152
|
+
gap: 16,
|
|
153
|
+
},
|
|
154
|
+
}}
|
|
155
|
+
renderItem={(photo, index) => <PhotoCard photo={photo} index={index} />}
|
|
156
|
+
/>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Omit `getItemHeight` for dynamic DOM-measured cards. React static markup is
|
|
160
|
+
used for the offscreen measurement probe; visible cards remain normal live
|
|
161
|
+
portals with Context, refs, state, and events.
|
|
162
|
+
|
|
163
|
+
Use `ref.current?.jumpToItem(index, screenOffset?)` for card navigation and
|
|
164
|
+
`ref.current?.scroller?.masonryDeterminism` to read `'canonical'` or `'local'`.
|
|
165
|
+
The demo gallery includes matching canonical and dynamic Masonry pages.
|
|
166
|
+
|
|
134
167
|
## Table layout
|
|
135
168
|
|
|
136
169
|
Pass `options={{ layout: 'table' }}` to render real `<table>` / `<tr>` / `<td>` rows with a frozen header and native column alignment. Your `renderItem` returns the row's `<td>` cells, and `tableHeader` provides the (declarative, reactive) `<thead>` row:
|
|
@@ -173,8 +206,8 @@ import { TABLE_COLUMNS } from './data';
|
|
|
173
206
|
built-in `ResizeObserver`.
|
|
174
207
|
- **`options` are read at creation.** Changing `options` after mount has no
|
|
175
208
|
effect; remount (e.g. with a `key`) to apply new engine options.
|
|
176
|
-
- **Changing the item count**
|
|
177
|
-
|
|
209
|
+
- **Changing the item count** updates lists/tables in place. Masonry recreates
|
|
210
|
+
its card-count-derived segment layout. Mutating items without changing the count just re-renders the
|
|
178
211
|
content (cheap, and your row state is preserved) — it does **not** discard
|
|
179
212
|
cached heights, so editable grids that produce a new `items` array on every
|
|
180
213
|
edit don't trigger a full viewport re-measure.
|
package/dist/cerious-scroll.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface CeriousScrollHandle {
|
|
|
24
24
|
scroller: CeriousScrollEngine | null;
|
|
25
25
|
render(): MeasuredViewportRange | null;
|
|
26
26
|
jumpToElement(index: number): MeasuredViewportRange | null;
|
|
27
|
+
jumpToItem(index: number, screenOffset?: number): MeasuredViewportRange | null;
|
|
27
28
|
scrollToPercentage(percentage: number): MeasuredViewportRange | null;
|
|
28
29
|
reset(): MeasuredViewportRange | null;
|
|
29
30
|
/**
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const ae=require("react/jsx-runtime"),n=require("react"),G=require("react-dom"),x=require("react-dom/server"),re=require("@ceriousdevtech/cerious-scroll"),ee="data-cerious-scroll-content",_="data-cerious-scroll-row";function ie(r){const m=r.querySelector(`[${ee}]`);if(m)return m;const c=document.createElement("div");return c.setAttribute(ee,""),c.style.position="relative",c.style.width="100%",c.style.height="100%",c.style.overflowY="clip",c.style.overflowX="auto",r.appendChild(c),c}function fe(r,m){const c=E=>{const l=E.detail;l&&m({percentage:l.percentage,currentElement:l.currentElement,scrollOffset:l.scrollOffset,result:{element:l.result.element,offset:l.result.offset}})},a=E=>{const l=E.detail;l&&m({percentage:l.percentage,currentElement:l.element,scrollOffset:l.scrollOffset,result:{element:l.element,offset:l.scrollOffset}})};return r.addEventListener("cerious-viewport-change",c),r.addEventListener("viewport-change",a),()=>{r.removeEventListener("cerious-viewport-change",c),r.removeEventListener("viewport-change",a)}}function te(r,m){const c=typeof r=="number"?r:typeof m=="number"?m:void 0;if(c===void 0||Number.isNaN(c))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(c))}function ne(r){var Z;const m=n.useRef(null),c=n.useRef(null),a=n.useRef(new Map),E=n.useRef(new Map),l=n.useRef(null),y=n.useRef(null),w=n.useRef(null),q=n.useRef(r.tableHeader),[,k]=n.useReducer(e=>e+1,0),[H,T]=n.useState(null),[F,P]=n.useReducer(e=>e+1,0),p=n.useRef(r.renderItem),b=n.useRef(r.items??null),$=n.useRef(r.getItem),S=n.useRef(r.options),I=n.useRef(r.autoRender??!0),B=n.useRef(r.totalElements??null),L=n.useRef(r.onViewportChange),N=n.useRef(r.onMeasuredViewport),V=n.useRef(r.onReady);p.current=r.renderItem,b.current=r.items??null,$.current=r.getItem,q.current=r.tableHeader,S.current=r.options,I.current=r.autoRender??!0,B.current=r.totalElements??null,L.current=r.onViewportChange,N.current=r.onMeasuredViewport,V.current=r.onReady;const z=n.useCallback(e=>{const t=$.current;if(t)return t(e);const s=b.current;return s?s[e]:void 0},[]),J=n.useCallback((e,t)=>{const s=z(e);if(t.dataset.ceriousMasonry==="probe"){const f=document.createElement("div");f.setAttribute(_,String(e)),f.innerHTML=x.renderToStaticMarkup(n.createElement(n.Fragment,null,p.current(s,e))),t.appendChild(f);return}let o=t.querySelector(`[${_}]`);o||(o=document.createElement("div"),t.appendChild(o)),o.setAttribute(_,String(e)),a.current.forEach((f,R)=>{f.el===t&&R!==e&&a.current.delete(R)}),a.current.set(e,{el:t,mount:o})},[z]),u=n.useCallback(()=>{var O,A,j;const e=c.current;if(!e)return null;const{scroller:t,contentEl:s,container:o}=e,f=o.clientHeight||o.offsetHeight||0,R=[],M=((O=S.current)==null?void 0:O.layout)==="table",v=((A=S.current)==null?void 0:A.layout)==="masonry",D=(h,i)=>{const g=document.createElement("div");M&&(g.style.display="contents"),g.setAttribute(_,String(h)),g.innerHTML=x.renderToStaticMarkup(n.createElement(n.Fragment,null,p.current(z(h),h))),i.appendChild(g),a.current.set(h,{el:i,mount:g}),R.push(g)},d=t.renderViewport(f,s,D),Y=v?null:new Set(t.getRenderedIndices());return a.current.forEach((h,i)=>{v?(!h.el.isConnected||h.el.dataset.elementIndex!==String(i))&&a.current.delete(i):Y.has(i)||a.current.delete(i)}),R.forEach(h=>{h.textContent=""}),G.flushSync(k),(j=N.current)==null||j.call(N,d),d},[]),K=r.totalElements??((Z=r.items)==null?void 0:Z.length)??null;n.useEffect(()=>{var A,j,h;const e=m.current;if(!e)return;const t=S.current??{},s=t.layout==="masonry",o=s?e:ie(e),f=t.onScroll,{masonry:R,...M}=t,v={...M,onScroll:()=>{f==null||f(),I.current&&u()}};if(s){if(!R)throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");v.masonry={...R,renderItem:J}}if(t.layout==="table"&&q.current!=null){const i=(A=t.table)==null?void 0:A.header;v.table={...t.table,header:g=>{w.current=g,i==null||i(g)}}}const D=te(B.current,((j=b.current)==null?void 0:j.length)??null),d=new re.CeriousScroll(e,D,v);y.current&&(d.currentElement=Math.min(y.current.currentElement,D-1),d.scrollOffset=y.current.scrollOffset,y.current=null),c.current={scroller:d,contentEl:o,container:e},T(d);const Y=fe(e,i=>{var g;(g=L.current)==null||g.call(L,i)});(h=V.current)==null||h.call(V,d);let O=0;return I.current&&(O=requestAnimationFrame(()=>u())),()=>{var i;cancelAnimationFrame(O),Y(),y.current=((i=S.current)==null?void 0:i.layout)==="masonry"?null:{currentElement:d.currentElement,scrollOffset:d.scrollOffset},a.current.clear(),E.current.clear(),l.current=null,d.detachScrollbar(e),d.dispose(),o.textContent="",c.current=null,T(null)}},[u,J,F]),n.useEffect(()=>{var f,R;const e=c.current;if(!e||K==null)return;const t=te(B.current,((f=b.current)==null?void 0:f.length)??null),s=((R=S.current)==null?void 0:R.layout)==="masonry",o=s?e.scroller.itemCount:e.scroller.totalElements;if(t!==o){if(s){P();return}if(e.scroller.updateTotalElements(t),e.scroller.currentElement>t-1&&e.scroller.jumpToElement(t-1),I.current){const M=e.scroller;requestAnimationFrame(()=>{u(),M.syncScrollbar()})}else e.scroller.syncScrollbar()}},[K,u]),n.useEffect(()=>{if(!c.current||!I.current)return;const e=requestAnimationFrame(()=>u());return()=>cancelAnimationFrame(e)},[r.items,r.getItem]);const ce=n.useCallback(e=>{const t=c.current;return t?(t.scroller.jumpToElement(e),u()):null},[u]),oe=n.useCallback((e,t=0)=>{const s=c.current;return s?(s.scroller.jumpToItem(e,t),u()):null},[u]),le=n.useCallback(e=>{const t=c.current;return t?(t.scroller.handleScrollPercentage(e),u()):null},[u]),se=n.useCallback(()=>{const e=c.current;return e?(e.scroller.reset(),u()):null},[u]),ue=n.useCallback(()=>{const e=c.current;return e?(e.scroller.clearAllCaches(),E.current.clear(),u()):null},[u]),Q=b.current,U=!$.current&&Q!=null?Q.length:null,W=p.current,C=E.current;l.current!==W&&(C.clear(),l.current=W);const X=[];return a.current.forEach((e,t)=>{if(U!==null&&t>=U){C.delete(t);return}const s=z(t);let o=C.get(t);(!o||o.item!==s||o.mount!==e.mount)&&(o={item:s,mount:e.mount,node:G.createPortal(W(s,t),e.mount,String(t))},C.set(t,o)),X.push(o.node)}),C.size>a.current.size&&C.forEach((e,t)=>{a.current.has(t)||C.delete(t)}),r.tableHeader!=null&&w.current&&X.push(G.createPortal(r.tableHeader,w.current,"cerious-thead")),{containerRef:m,portals:X,scroller:H,render:u,jumpToElement:ce,jumpToItem:oe,scrollToPercentage:le,reset:se,recalculate:ue}}function me(r,m){const{className:c,style:a,"data-testid":E,children:l,...y}=r,{containerRef:w,portals:q,scroller:k,render:H,jumpToElement:T,jumpToItem:F,scrollToPercentage:P,reset:p,recalculate:b}=ne(y);return n.useImperativeHandle(m,()=>({scroller:k,render:H,jumpToElement:T,jumpToItem:F,scrollToPercentage:P,reset:p,recalculate:b}),[k,H,T,F,P,p,b]),ae.jsxs("div",{ref:w,className:c,"data-testid":E,style:{position:"relative",overflow:"hidden",...a},children:[l,q]})}const de=n.forwardRef(me);Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>re.CeriousScroll});exports.CeriousScroll=de;exports.useCeriousScroll=ne;
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { CeriousScroll } from './cerious-scroll';
|
|
9
9
|
export type { CeriousScrollProps, CeriousScrollHandle } from './cerious-scroll';
|
|
10
10
|
export { useCeriousScroll } from './use-cerious-scroll';
|
|
11
|
-
export type { UseCeriousScrollOptions, UseCeriousScrollResult, } from './use-cerious-scroll';
|
|
11
|
+
export type { UseCeriousScrollOptions, UseCeriousScrollResult, CeriousScrollOptions, } from './use-cerious-scroll';
|
|
12
12
|
export type { CeriousViewportChangeDetail } from './viewport-change';
|
|
13
13
|
export { CeriousScroll as CeriousScrollEngine } from '@ceriousdevtech/cerious-scroll';
|
|
14
|
-
export type {
|
|
14
|
+
export type { KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, HeightProvider, MasonryDeterminism, MasonryOptions, } from '@ceriousdevtech/cerious-scroll';
|
package/dist/index.js
CHANGED
|
@@ -1,198 +1,230 @@
|
|
|
1
|
-
import { jsxs as
|
|
2
|
-
import { useRef as
|
|
3
|
-
import { flushSync as
|
|
4
|
-
import { renderToStaticMarkup as
|
|
5
|
-
import { CeriousScroll as
|
|
6
|
-
import { CeriousScroll as
|
|
7
|
-
const
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
1
|
+
import { jsxs as fe } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as u, useReducer as ee, useState as de, useCallback as b, createElement as te, Fragment as re, useEffect as J, forwardRef as he, useImperativeHandle as ge } from "react";
|
|
3
|
+
import { flushSync as Ee, createPortal as ne } from "react-dom";
|
|
4
|
+
import { renderToStaticMarkup as oe } from "react-dom/server";
|
|
5
|
+
import { CeriousScroll as pe } from "@ceriousdevtech/cerious-scroll";
|
|
6
|
+
import { CeriousScroll as He } from "@ceriousdevtech/cerious-scroll";
|
|
7
|
+
const ce = "data-cerious-scroll-content", B = "data-cerious-scroll-row";
|
|
8
|
+
function ye(r) {
|
|
9
|
+
const f = r.querySelector(`[${ce}]`);
|
|
10
|
+
if (f) return f;
|
|
11
11
|
const n = document.createElement("div");
|
|
12
|
-
return n.setAttribute(
|
|
12
|
+
return n.setAttribute(ce, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflowY = "clip", n.style.overflowX = "auto", r.appendChild(n), n;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
15
|
-
const n = (
|
|
16
|
-
const c =
|
|
17
|
-
c &&
|
|
14
|
+
function be(r, f) {
|
|
15
|
+
const n = (p) => {
|
|
16
|
+
const c = p.detail;
|
|
17
|
+
c && f({
|
|
18
18
|
percentage: c.percentage,
|
|
19
19
|
currentElement: c.currentElement,
|
|
20
20
|
scrollOffset: c.scrollOffset,
|
|
21
21
|
result: { element: c.result.element, offset: c.result.offset }
|
|
22
22
|
});
|
|
23
|
-
},
|
|
24
|
-
const c =
|
|
25
|
-
c &&
|
|
23
|
+
}, a = (p) => {
|
|
24
|
+
const c = p.detail;
|
|
25
|
+
c && f({
|
|
26
26
|
percentage: c.percentage,
|
|
27
27
|
currentElement: c.element,
|
|
28
28
|
scrollOffset: c.scrollOffset,
|
|
29
29
|
result: { element: c.element, offset: c.scrollOffset }
|
|
30
30
|
});
|
|
31
31
|
};
|
|
32
|
-
return
|
|
33
|
-
|
|
32
|
+
return r.addEventListener("cerious-viewport-change", n), r.addEventListener("viewport-change", a), () => {
|
|
33
|
+
r.removeEventListener("cerious-viewport-change", n), r.removeEventListener("viewport-change", a);
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
function
|
|
37
|
-
const n = typeof
|
|
36
|
+
function le(r, f) {
|
|
37
|
+
const n = typeof r == "number" ? r : typeof f == "number" ? f : void 0;
|
|
38
38
|
if (n === void 0 || Number.isNaN(n))
|
|
39
39
|
throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
|
|
40
40
|
return Math.max(1, Math.floor(n));
|
|
41
41
|
}
|
|
42
|
-
function
|
|
43
|
-
var
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
const l =
|
|
50
|
-
return l ? l[
|
|
51
|
-
}, []),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
42
|
+
function ve(r) {
|
|
43
|
+
var x;
|
|
44
|
+
const f = u(null), n = u(null), a = u(/* @__PURE__ */ new Map()), p = u(/* @__PURE__ */ new Map()), c = u(null), v = u(null), I = u(null), F = u(r.tableHeader), [, L] = ee((e) => e + 1, 0), [q, T] = de(null), [N, P] = ee((e) => e + 1, 0), S = u(r.renderItem), y = u(r.items ?? null), D = u(r.getItem), R = u(r.options), M = u(r.autoRender ?? !0), W = u(r.totalElements ?? null), V = u(r.onViewportChange), $ = u(r.onMeasuredViewport), k = u(r.onReady);
|
|
45
|
+
S.current = r.renderItem, y.current = r.items ?? null, D.current = r.getItem, F.current = r.tableHeader, R.current = r.options, M.current = r.autoRender ?? !0, W.current = r.totalElements ?? null, V.current = r.onViewportChange, $.current = r.onMeasuredViewport, k.current = r.onReady;
|
|
46
|
+
const z = b((e) => {
|
|
47
|
+
const t = D.current;
|
|
48
|
+
if (t) return t(e);
|
|
49
|
+
const l = y.current;
|
|
50
|
+
return l ? l[e] : void 0;
|
|
51
|
+
}, []), K = b((e, t) => {
|
|
52
|
+
const l = z(e);
|
|
53
|
+
if (t.dataset.ceriousMasonry === "probe") {
|
|
54
|
+
const m = document.createElement("div");
|
|
55
|
+
m.setAttribute(B, String(e)), m.innerHTML = oe(
|
|
56
|
+
te(re, null, S.current(l, e))
|
|
57
|
+
), t.appendChild(m);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let o = t.querySelector(`[${B}]`);
|
|
61
|
+
o || (o = document.createElement("div"), t.appendChild(o)), o.setAttribute(B, String(e)), a.current.forEach((m, E) => {
|
|
62
|
+
m.el === t && E !== e && a.current.delete(E);
|
|
63
|
+
}), a.current.set(e, { el: t, mount: o });
|
|
64
|
+
}, [z]), s = b(() => {
|
|
65
|
+
var A, H, j;
|
|
66
|
+
const e = n.current;
|
|
67
|
+
if (!e) return null;
|
|
68
|
+
const { scroller: t, contentEl: l, container: o } = e, m = o.clientHeight || o.offsetHeight || 0, E = [], O = ((A = R.current) == null ? void 0 : A.layout) === "table", w = ((H = R.current) == null ? void 0 : H.layout) === "masonry", _ = (h, i) => {
|
|
69
|
+
const g = document.createElement("div");
|
|
70
|
+
O && (g.style.display = "contents"), g.setAttribute(B, String(h)), g.innerHTML = oe(
|
|
71
|
+
te(re, null, S.current(z(h), h))
|
|
72
|
+
), i.appendChild(g), a.current.set(h, { el: i, mount: g }), E.push(g);
|
|
73
|
+
}, d = t.renderViewport(m, l, _), G = w ? null : new Set(t.getRenderedIndices());
|
|
74
|
+
return a.current.forEach((h, i) => {
|
|
75
|
+
w ? (!h.el.isConnected || h.el.dataset.elementIndex !== String(i)) && a.current.delete(i) : G.has(i) || a.current.delete(i);
|
|
76
|
+
}), E.forEach((h) => {
|
|
77
|
+
h.textContent = "";
|
|
78
|
+
}), Ee(L), (j = $.current) == null || j.call($, d), d;
|
|
79
|
+
}, []), Q = r.totalElements ?? ((x = r.items) == null ? void 0 : x.length) ?? null;
|
|
80
|
+
J(() => {
|
|
81
|
+
var H, j, h;
|
|
82
|
+
const e = f.current;
|
|
83
|
+
if (!e) return;
|
|
84
|
+
const t = R.current ?? {}, l = t.layout === "masonry", o = l ? e : ye(e), m = t.onScroll, { masonry: E, ...O } = t, w = {
|
|
85
|
+
...O,
|
|
73
86
|
onScroll: () => {
|
|
74
|
-
|
|
87
|
+
m == null || m(), M.current && s();
|
|
75
88
|
}
|
|
76
89
|
};
|
|
77
|
-
if (l
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
if (l) {
|
|
91
|
+
if (!E)
|
|
92
|
+
throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");
|
|
93
|
+
w.masonry = {
|
|
94
|
+
...E,
|
|
95
|
+
renderItem: K
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (t.layout === "table" && F.current != null) {
|
|
99
|
+
const i = (H = t.table) == null ? void 0 : H.header;
|
|
100
|
+
w.table = {
|
|
101
|
+
...t.table,
|
|
102
|
+
header: (g) => {
|
|
103
|
+
I.current = g, i == null || i(g);
|
|
83
104
|
}
|
|
84
105
|
};
|
|
85
106
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
var
|
|
90
|
-
(
|
|
107
|
+
const _ = le(W.current, ((j = y.current) == null ? void 0 : j.length) ?? null), d = new pe(e, _, w);
|
|
108
|
+
v.current && (d.currentElement = Math.min(v.current.currentElement, _ - 1), d.scrollOffset = v.current.scrollOffset, v.current = null), n.current = { scroller: d, contentEl: o, container: e }, T(d);
|
|
109
|
+
const G = be(e, (i) => {
|
|
110
|
+
var g;
|
|
111
|
+
(g = V.current) == null || g.call(V, i);
|
|
91
112
|
});
|
|
92
|
-
(
|
|
93
|
-
let
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
scrollOffset: m.scrollOffset
|
|
98
|
-
}, f.current.clear(), h.current.clear(), c.current = null, r.textContent = "", m.detachScrollbar(t), m.dispose(), n.current = null, C(null);
|
|
113
|
+
(h = k.current) == null || h.call(k, d);
|
|
114
|
+
let A = 0;
|
|
115
|
+
return M.current && (A = requestAnimationFrame(() => s())), () => {
|
|
116
|
+
var i;
|
|
117
|
+
cancelAnimationFrame(A), G(), v.current = ((i = R.current) == null ? void 0 : i.layout) === "masonry" ? null : { currentElement: d.currentElement, scrollOffset: d.scrollOffset }, a.current.clear(), p.current.clear(), c.current = null, d.detachScrollbar(e), d.dispose(), o.textContent = "", n.current = null, T(null);
|
|
99
118
|
};
|
|
100
|
-
}, [s]),
|
|
101
|
-
var
|
|
102
|
-
const
|
|
103
|
-
if (!
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
if (
|
|
107
|
-
|
|
119
|
+
}, [s, K, N]), J(() => {
|
|
120
|
+
var m, E;
|
|
121
|
+
const e = n.current;
|
|
122
|
+
if (!e || Q == null) return;
|
|
123
|
+
const t = le(W.current, ((m = y.current) == null ? void 0 : m.length) ?? null), l = ((E = R.current) == null ? void 0 : E.layout) === "masonry", o = l ? e.scroller.itemCount : e.scroller.totalElements;
|
|
124
|
+
if (t !== o) {
|
|
125
|
+
if (l) {
|
|
126
|
+
P();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (e.scroller.updateTotalElements(t), e.scroller.currentElement > t - 1 && e.scroller.jumpToElement(t - 1), M.current) {
|
|
130
|
+
const O = e.scroller;
|
|
108
131
|
requestAnimationFrame(() => {
|
|
109
|
-
s(),
|
|
132
|
+
s(), O.syncScrollbar();
|
|
110
133
|
});
|
|
111
134
|
} else
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
e.scroller.syncScrollbar();
|
|
136
|
+
}
|
|
137
|
+
}, [Q, s]), J(() => {
|
|
138
|
+
if (!n.current || !M.current) return;
|
|
139
|
+
const e = requestAnimationFrame(() => s());
|
|
140
|
+
return () => cancelAnimationFrame(e);
|
|
141
|
+
}, [r.items, r.getItem]);
|
|
142
|
+
const se = b(
|
|
143
|
+
(e) => {
|
|
144
|
+
const t = n.current;
|
|
145
|
+
return t ? (t.scroller.jumpToElement(e), s()) : null;
|
|
146
|
+
},
|
|
147
|
+
[s]
|
|
148
|
+
), ue = b(
|
|
149
|
+
(e, t = 0) => {
|
|
150
|
+
const l = n.current;
|
|
151
|
+
return l ? (l.scroller.jumpToItem(e, t), s()) : null;
|
|
122
152
|
},
|
|
123
153
|
[s]
|
|
124
|
-
),
|
|
125
|
-
(
|
|
126
|
-
const
|
|
127
|
-
return
|
|
154
|
+
), ae = b(
|
|
155
|
+
(e) => {
|
|
156
|
+
const t = n.current;
|
|
157
|
+
return t ? (t.scroller.handleScrollPercentage(e), s()) : null;
|
|
128
158
|
},
|
|
129
159
|
[s]
|
|
130
|
-
),
|
|
131
|
-
const
|
|
132
|
-
return
|
|
133
|
-
}, [s]),
|
|
134
|
-
const
|
|
135
|
-
return
|
|
136
|
-
}, [s]),
|
|
137
|
-
c.current !==
|
|
138
|
-
const
|
|
139
|
-
return
|
|
140
|
-
if (
|
|
141
|
-
|
|
160
|
+
), ie = b(() => {
|
|
161
|
+
const e = n.current;
|
|
162
|
+
return e ? (e.scroller.reset(), s()) : null;
|
|
163
|
+
}, [s]), me = b(() => {
|
|
164
|
+
const e = n.current;
|
|
165
|
+
return e ? (e.scroller.clearAllCaches(), p.current.clear(), s()) : null;
|
|
166
|
+
}, [s]), U = y.current, Z = !D.current && U != null ? U.length : null, X = S.current, C = p.current;
|
|
167
|
+
c.current !== X && (C.clear(), c.current = X);
|
|
168
|
+
const Y = [];
|
|
169
|
+
return a.current.forEach((e, t) => {
|
|
170
|
+
if (Z !== null && t >= Z) {
|
|
171
|
+
C.delete(t);
|
|
142
172
|
return;
|
|
143
173
|
}
|
|
144
|
-
const l =
|
|
145
|
-
let
|
|
146
|
-
(!
|
|
174
|
+
const l = z(t);
|
|
175
|
+
let o = C.get(t);
|
|
176
|
+
(!o || o.item !== l || o.mount !== e.mount) && (o = {
|
|
147
177
|
item: l,
|
|
148
|
-
mount:
|
|
149
|
-
node:
|
|
150
|
-
},
|
|
151
|
-
}),
|
|
152
|
-
|
|
153
|
-
}),
|
|
154
|
-
containerRef:
|
|
155
|
-
portals:
|
|
156
|
-
scroller:
|
|
178
|
+
mount: e.mount,
|
|
179
|
+
node: ne(X(l, t), e.mount, String(t))
|
|
180
|
+
}, C.set(t, o)), Y.push(o.node);
|
|
181
|
+
}), C.size > a.current.size && C.forEach((e, t) => {
|
|
182
|
+
a.current.has(t) || C.delete(t);
|
|
183
|
+
}), r.tableHeader != null && I.current && Y.push(ne(r.tableHeader, I.current, "cerious-thead")), {
|
|
184
|
+
containerRef: f,
|
|
185
|
+
portals: Y,
|
|
186
|
+
scroller: q,
|
|
157
187
|
render: s,
|
|
158
|
-
jumpToElement:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
jumpToElement: se,
|
|
189
|
+
jumpToItem: ue,
|
|
190
|
+
scrollToPercentage: ae,
|
|
191
|
+
reset: ie,
|
|
192
|
+
recalculate: me
|
|
162
193
|
};
|
|
163
194
|
}
|
|
164
|
-
function
|
|
165
|
-
const { className: n, style:
|
|
166
|
-
containerRef:
|
|
167
|
-
portals:
|
|
168
|
-
scroller:
|
|
169
|
-
render:
|
|
170
|
-
jumpToElement:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
195
|
+
function Se(r, f) {
|
|
196
|
+
const { className: n, style: a, "data-testid": p, children: c, ...v } = r, {
|
|
197
|
+
containerRef: I,
|
|
198
|
+
portals: F,
|
|
199
|
+
scroller: L,
|
|
200
|
+
render: q,
|
|
201
|
+
jumpToElement: T,
|
|
202
|
+
jumpToItem: N,
|
|
203
|
+
scrollToPercentage: P,
|
|
204
|
+
reset: S,
|
|
205
|
+
recalculate: y
|
|
206
|
+
} = ve(v);
|
|
207
|
+
return ge(
|
|
208
|
+
f,
|
|
209
|
+
() => ({ scroller: L, render: q, jumpToElement: T, jumpToItem: N, scrollToPercentage: P, reset: S, recalculate: y }),
|
|
210
|
+
[L, q, T, N, P, S, y]
|
|
211
|
+
), /* @__PURE__ */ fe(
|
|
180
212
|
"div",
|
|
181
213
|
{
|
|
182
|
-
ref:
|
|
214
|
+
ref: I,
|
|
183
215
|
className: n,
|
|
184
|
-
"data-testid":
|
|
185
|
-
style: { position: "relative", overflow: "hidden", ...
|
|
216
|
+
"data-testid": p,
|
|
217
|
+
style: { position: "relative", overflow: "hidden", ...a },
|
|
186
218
|
children: [
|
|
187
219
|
c,
|
|
188
|
-
|
|
220
|
+
F
|
|
189
221
|
]
|
|
190
222
|
}
|
|
191
223
|
);
|
|
192
224
|
}
|
|
193
|
-
const
|
|
225
|
+
const Me = he(Se);
|
|
194
226
|
export {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
Me as CeriousScroll,
|
|
228
|
+
He as CeriousScrollEngine,
|
|
229
|
+
ve as useCeriousScroll
|
|
198
230
|
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { ReactNode, RefObject } from 'react';
|
|
2
|
-
import { CeriousScroll as CeriousScrollEngine, CeriousScrollOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
|
|
2
|
+
import { CeriousScroll as CeriousScrollEngine, CeriousScrollOptions as CoreCeriousScrollOptions, MasonryOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
|
|
3
3
|
import { CeriousViewportChangeDetail } from './viewport-change';
|
|
4
|
+
/** Engine options adapted for React-owned Masonry rendering. */
|
|
5
|
+
export type CeriousScrollOptions = Omit<CoreCeriousScrollOptions, 'masonry'> & {
|
|
6
|
+
masonry?: Omit<MasonryOptions, 'renderItem'> & {
|
|
7
|
+
/** Accepted for backward compatibility; React supplies the active renderer. */
|
|
8
|
+
renderItem?: MasonryOptions['renderItem'];
|
|
9
|
+
};
|
|
10
|
+
};
|
|
4
11
|
export interface UseCeriousScrollOptions<TItem = unknown> {
|
|
5
12
|
/** Total number of items. Falls back to `items.length` when omitted. */
|
|
6
13
|
totalElements?: number | null;
|
|
@@ -38,6 +45,8 @@ export interface UseCeriousScrollResult {
|
|
|
38
45
|
render: () => MeasuredViewportRange | null;
|
|
39
46
|
/** Jump to an element index, then render. */
|
|
40
47
|
jumpToElement: (index: number) => MeasuredViewportRange | null;
|
|
48
|
+
/** Masonry mode only: jump to a card index, then render. */
|
|
49
|
+
jumpToItem: (index: number, screenOffset?: number) => MeasuredViewportRange | null;
|
|
41
50
|
/** Scroll to a percentage (0..100), then render. */
|
|
42
51
|
scrollToPercentage: (percentage: number) => MeasuredViewportRange | null;
|
|
43
52
|
/** Reset to the top, then render. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ceriousdevtech/react-cerious-scroll",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "React bindings for CeriousScroll —
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "React bindings for CeriousScroll — virtualized lists, native tables, and Masonry grids",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"large-lists",
|
|
45
45
|
"data-grid",
|
|
46
46
|
"variable-height",
|
|
47
|
+
"masonry",
|
|
47
48
|
"o1-memory",
|
|
48
49
|
"cerious-scroll"
|
|
49
50
|
],
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"demo:build": "vite build --config demo/vite.config.ts"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
|
-
"@ceriousdevtech/cerious-scroll": "^1.0
|
|
60
|
+
"@ceriousdevtech/cerious-scroll": "^1.1.0"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
63
|
"react": ">=18 <20",
|