@ceriousdevtech/vue-cerious-scroll 1.0.6 → 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 CHANGED
@@ -5,6 +5,26 @@ All notable changes to vue-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 through the existing `#item` slot or `render-item` prop.
12
+ - Dynamic-height probe rendering with short-lived Vue trees that are disposed after synchronous measurement.
13
+ - `jumpToItem(index, screenOffset?)` on the composable result and exposed component API.
14
+ - `CeriousScrollOptions` support for wrapper-owned Masonry rendering, canonical/dynamic demo routes, and regression coverage for both modes.
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
+
20
+ ## [1.0.7] - 2026-06-24
21
+
22
+ ### Fixed
23
+ - **`provide()` values from the component using `<CeriousScroll>` now reach `inject()` inside rows** ([#1](https://github.com/ceriousdevtech/vue-cerious-scroll/issues/1)). Rows (and the table header) are rendered as detached vnode trees via Vue's `render()`, so injection resolved only against the app context's `provides` — app-level/plugin provides worked, but local `provide()` on the owning component did not. The wrapper now renders rows with the owning component instance's `provides` (which prototype-chains to the app provides), so component-level, plugin, and app-level provide/inject all work inside virtualized rows. When the owner provides nothing, behavior is unchanged.
24
+
25
+ ### Changed
26
+ - Updated the core engine dependency to `@ceriousdevtech/cerious-scroll@^1.0.8` (native-scrollbar drag rendering is now coalesced to one render per frame, with no per-row layout thrash on fast drags).
27
+
8
28
  ## [1.0.6] - 2026-06-11
9
29
 
10
30
  ### Changed
package/README.md CHANGED
@@ -109,7 +109,7 @@ const { containerRef } = useCeriousScroll({
109
109
  | `totalElements` | `number` | Total item count. Required if `items` is omitted. |
110
110
  | `getItem` | `(index) => TItem` | Lazy item getter for large/sparse datasets. |
111
111
  | `renderItem` | `(item, index) => VNodeChild` | Render prop alternative to the `#item` scoped slot. |
112
- | `options` | `CeriousScrollOptions` | Engine options (keyboard/touch/wheel/scrollbar/etc.). Read once at creation. |
112
+ | `options` | `CeriousScrollOptions` | Engine options. Masonry's DOM callback is supplied by the wrapper. Read once at creation. |
113
113
  | `autoRender` | `boolean` | Re-render on scroll/resize/data changes. Default `true`. |
114
114
 
115
115
  The row is provided by the **`#item` scoped slot** (`{ item, index }`) or the
@@ -130,6 +130,7 @@ component — they fall through onto the scroll container (set a height!).
130
130
  ```ts
131
131
  const scroll = ref<InstanceType<typeof CeriousScroll> | null>(null);
132
132
  // scroll.value?.jumpToElement(500);
133
+ // scroll.value?.jumpToItem(500); // Masonry cards
133
134
  // scroll.value?.scrollToPercentage(50);
134
135
  // scroll.value?.reset();
135
136
  // scroll.value?.render();
@@ -139,6 +140,40 @@ const scroll = ref<InstanceType<typeof CeriousScroll> | null>(null);
139
140
 
140
141
  ---
141
142
 
143
+ ## Masonry layout
144
+
145
+ Set `layout: 'masonry'`; the wrapper renders the `#item` slot into each core
146
+ Masonry card, so no imperative DOM callback is exposed:
147
+
148
+ ```vue
149
+ <CeriousScroll
150
+ ref="scroll"
151
+ class="gallery"
152
+ :total-elements="photos.length"
153
+ :get-item="(index) => photos[index]"
154
+ :options="{
155
+ layout: 'masonry',
156
+ masonry: {
157
+ getItemHeight: (_index, width) => width * 0.75 + 48,
158
+ targetColumnWidth: 280,
159
+ gap: 16
160
+ }
161
+ }"
162
+ >
163
+ <template #item="{ item: photo, index }">
164
+ <PhotoCard :photo="photo" :index="index" />
165
+ </template>
166
+ </CeriousScroll>
167
+ ```
168
+
169
+ Omit `getItemHeight` for dynamic DOM measurement. Vue creates a short-lived
170
+ offscreen render tree for measurement and disposes it after the synchronous
171
+ height read; visible cards remain reactive Vue trees with provide/inject and
172
+ event handling.
173
+
174
+ Use `scroll.value?.jumpToItem(index, screenOffset?)` for card navigation. The
175
+ demo gallery includes canonical and dynamic Masonry pages.
176
+
142
177
  ## Table layout
143
178
 
144
179
  Pass `:options="{ layout: 'table' }"` to render real `<table>` / `<tr>` / `<td>` rows with a frozen header and native column alignment. The `#item` slot returns the row's `<td>` cells; a `#header` slot provides the (declarative, reactive) `<thead>` row:
@@ -176,8 +211,8 @@ Pass `:options="{ layout: 'table' }"` to render real `<table>` / `<tr>` / `<td>`
176
211
  the engine's built-in `ResizeObserver`.
177
212
  - **`options` are read at creation.** Changing `options` after mount has no
178
213
  effect; remount (e.g. with a `:key`) to apply new engine options.
179
- - **Changing the item count** recreates the engine internally (scroll position
180
- is preserved). Mutating items without changing the count just re-renders the
214
+ - **Changing the item count** updates lists/tables in place. Masonry recreates
215
+ its card-count-derived segment layout. Mutating items without changing the count just re-renders the
181
216
  content in place (cheap; Vue patches each row, so focus/selection survive) — it
182
217
  does **not** discard cached heights, so editable grids that produce a new
183
218
  `items` array on every edit don't trigger a full viewport re-measure.
@@ -1,5 +1,6 @@
1
1
  import { PropType, VNodeChild } from 'vue';
2
- import { CeriousScrollOptions, MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
2
+ import { MeasuredViewportRange } from '@ceriousdevtech/cerious-scroll';
3
+ import { CeriousScrollOptions } from './use-cerious-scroll';
3
4
  import { CeriousViewportChangeDetail } from './viewport-change';
4
5
  /**
5
6
  * High-performance virtual scroll list. Provide `items` (or `total-elements` +
@@ -93,10 +94,10 @@ export declare const CeriousScroll: import('vue').DefineComponent<import('vue').
93
94
  "onMeasured-viewport"?: ((_range: MeasuredViewportRange) => any) | undefined;
94
95
  onReady?: ((_scroller: unknown) => any) | undefined;
95
96
  }>, {
97
+ renderItem: (item: unknown, index: number) => VNodeChild;
96
98
  totalElements: number | null;
97
99
  items: readonly unknown[] | null;
98
100
  getItem: (index: number) => unknown;
99
- renderItem: (item: unknown, index: number) => VNodeChild;
100
101
  options: CeriousScrollOptions;
101
102
  autoRender: boolean;
102
103
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const r=require("vue"),N=require("@ceriousdevtech/cerious-scroll"),M="data-cerious-scroll-content",q="data-cerious-scroll-row";function L(e){const u=e.querySelector(`[${M}]`);if(u)return u;const o=document.createElement("div");return o.setAttribute(M,""),o.style.position="relative",o.style.width="100%",o.style.height="100%",o.style.overflowY="clip",o.style.overflowX="auto",e.appendChild(o),o}function B(e,u){const o=l=>{const n=l.detail;n&&u({percentage:n.percentage,currentElement:n.currentElement,scrollOffset:n.scrollOffset,result:{element:n.result.element,offset:n.result.offset}})},g=l=>{const n=l.detail;n&&u({percentage:n.percentage,currentElement:n.element,scrollOffset:n.scrollOffset,result:{element:n.element,offset:n.scrollOffset}})};return e.addEventListener("cerious-viewport-change",o),e.addEventListener("viewport-change",g),()=>{e.removeEventListener("cerious-viewport-change",o),e.removeEventListener("viewport-change",g)}}function j(e,u){const o=typeof e=="number"?e:typeof u=="number"?u:void 0;if(o===void 0||Number.isNaN(o))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(o))}function P(e){var I,A;const u=(I=r.getCurrentInstance())==null?void 0:I.appContext,o=r.ref(null),g=r.shallowRef(null);let l=null;const n=new Map;let s=null;const b=((A=e.options)==null?void 0:A.layout)==="table";let d=null;const H=t=>{if(!e.renderHeader)return;d==null||d();const c=u??null,a=r.effectScope(!0);a.run(()=>r.watchEffect(()=>{const f=e.renderHeader(),m=Array.isArray(f)?f:[f];for(const i of m)r.isVNode(i)&&(i.appContext=c);const h=r.h(r.Fragment,m);h.appContext=c,r.render(h,t)},{flush:"sync"})),d=()=>a.stop()},R=()=>r.toValue(e.autoRender)??!0,x=t=>{if(e.getItem)return e.getItem(t);const c=r.toValue(e.items);return c?c[t]:void 0},F=(t,c)=>{const a=u??null,f=r.effectScope(!0);return f.run(()=>r.watchEffect(()=>{const m=e.renderItem(x(t),t),h=Array.isArray(m)?m:[m];for(const p of h)r.isVNode(p)&&(p.appContext=a);const i=r.h(r.Fragment,h);i.appContext=a,r.render(i,c)},{flush:"sync"})),()=>f.stop()},T=t=>{t.stop(),r.render(null,t.mount),t.mount.remove()},y=()=>{var p;if(!l)return null;const{scroller:t,contentEl:c,container:a}=l,f=a.clientHeight||a.offsetHeight||0,m=(v,E)=>{const w=document.createElement("div");b&&(w.style.display="contents"),w.setAttribute(q,String(v)),E.appendChild(w);const C=F(v,w);n.set(v,{el:E,mount:w,stop:C})},h=t.renderViewport(f,c,m),i=new Set(t.getRenderedIndices());return n.forEach((v,E)=>{i.has(E)||(T(v),n.delete(E))}),(p=e.onMeasuredViewport)==null||p.call(e,h),h},V=t=>{if(!l)return;const c=l;l=null,c.unsubscribe(),t&&(s={currentElement:c.scroller.currentElement,scrollOffset:c.scroller.scrollOffset}),n.forEach(a=>T(a)),n.clear(),d==null||d(),d=null,c.contentEl.textContent="",c.scroller.detachScrollbar(c.container),c.scroller.dispose(),g.value=null},O=t=>{var v,E,w;const c=L(t),a=e.options??{},f=a.onScroll,m={...a,onScroll:()=>{f==null||f(),R()&&y()}};if(b&&e.renderHeader){const C=(v=a.table)==null?void 0:v.header;m.table={...a.table,header:S=>{C==null||C(S),H(S)}}}const h=j(r.toValue(e.totalElements),((E=r.toValue(e.items))==null?void 0:E.length)??null),i=new N.CeriousScroll(t,h,m);s&&(i.currentElement=Math.min(s.currentElement,h-1),i.scrollOffset=s.scrollOffset,s=null);const p=B(t,C=>{var S;(S=e.onViewportChange)==null||S.call(e,C)});l={scroller:i,contentEl:c,container:t,unsubscribe:p},g.value=i,(w=e.onReady)==null||w.call(e,i),R()&&requestAnimationFrame(()=>y())},_=()=>{V(!0);const t=o.value;t&&O(t)};return r.onMounted(()=>{const t=o.value;t&&O(t)}),r.onBeforeUnmount(()=>{V(!1)}),r.watch(()=>[r.toValue(e.totalElements),r.toValue(e.items)],()=>{var c;if(!l)return;const t=j(r.toValue(e.totalElements),((c=r.toValue(e.items))==null?void 0:c.length)??null);l.scroller.totalElements!==t&&_()}),{containerRef:o,scroller:g,render:y,jumpToElement:t=>l?(l.scroller.jumpToElement(t),y()):null,scrollToPercentage:t=>l?(l.scroller.handleScrollPercentage(t),y()):null,reset:()=>l?(l.scroller.reset(),y()):null,recalculate:()=>l?(l.scroller.clearAllCaches(),y()):null}}const U=r.defineComponent({name:"CeriousScroll",props:{totalElements:{type:Number,default:null},items:{type:Array,default:null},getItem:{type:Function,default:void 0},renderItem:{type:Function,default:void 0},options:{type:Object,default:void 0},autoRender:{type:Boolean,default:!0}},emits:{"viewport-change":e=>!0,"measured-viewport":e=>!0,ready:e=>!0},setup(e,{slots:u,emit:o,expose:g}){const l=(s,b)=>{var d;return e.renderItem?e.renderItem(s,b):(d=u.item)==null?void 0:d.call(u,{item:s,index:b})},n=P({totalElements:()=>e.totalElements,items:()=>e.items,getItem:e.getItem?s=>e.getItem(s):void 0,renderItem:l,renderHeader:u.header?()=>u.header():void 0,options:e.options,autoRender:()=>e.autoRender,onViewportChange:s=>o("viewport-change",s),onMeasuredViewport:s=>o("measured-viewport",s),onReady:s=>o("ready",s)});return g({scroller:n.scroller,render:n.render,jumpToElement:n.jumpToElement,scrollToPercentage:n.scrollToPercentage,reset:n.reset,recalculate:n.recalculate}),()=>{var s;return r.h("div",{ref:n.containerRef,style:{position:"relative",overflow:"hidden"}},(s=u.default)==null?void 0:s.call(u))}}});Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>N.CeriousScroll});exports.CeriousScroll=U;exports.useCeriousScroll=P;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("vue"),L=require("@ceriousdevtech/cerious-scroll"),F="data-cerious-scroll-content",A="data-cerious-scroll-row";function X(e){const s=e.querySelector(`[${F}]`);if(s)return s;const u=document.createElement("div");return u.setAttribute(F,""),u.style.position="relative",u.style.width="100%",u.style.height="100%",u.style.overflowY="clip",u.style.overflowX="auto",e.appendChild(u),u}function Y(e,s){const u=p=>{const l=p.detail;l&&s({percentage:l.percentage,currentElement:l.currentElement,scrollOffset:l.scrollOffset,result:{element:l.result.element,offset:l.result.offset}})},w=p=>{const l=p.detail;l&&s({percentage:l.percentage,currentElement:l.element,scrollOffset:l.scrollOffset,result:{element:l.element,offset:l.scrollOffset}})};return e.addEventListener("cerious-viewport-change",u),e.addEventListener("viewport-change",w),()=>{e.removeEventListener("cerious-viewport-change",u),e.removeEventListener("viewport-change",w)}}function _(e,s){const u=typeof e=="number"?e:typeof s=="number"?s:void 0;if(u===void 0||Number.isNaN(u))throw new Error("useCeriousScroll: provide `totalElements` or `items`.");return Math.max(1,Math.floor(u))}function B(e){var N,q;const s=n.getCurrentInstance(),u=s==null?void 0:s.provides,w=s?{...s.appContext,provides:u??s.appContext.provides}:void 0,p=n.ref(null),l=n.shallowRef(null);let r=null;const f=new Map,T=((N=e.options)==null?void 0:N.layout)==="table",b=((q=e.options)==null?void 0:q.layout)==="masonry";let y=null;const $=t=>{if(!e.renderHeader)return;y==null||y();const o=w??null,c=n.effectScope(!0);c.run(()=>n.watchEffect(()=>{const a=e.renderHeader(),m=Array.isArray(a)?a:[a];for(const d of m)n.isVNode(d)&&(d.appContext=o);const i=n.h(n.Fragment,m);i.appContext=o,n.render(i,t)},{flush:"sync"})),y=()=>c.stop()},V=()=>n.toValue(e.autoRender)??!0,U=t=>{if(e.getItem)return e.getItem(t);const o=n.toValue(e.items);return o?o[t]:void 0},j=(t,o)=>{const c=w??null,a=n.effectScope(!0);return a.run(()=>n.watchEffect(()=>{const m=e.renderItem(U(t),t),i=Array.isArray(m)?m:[m];for(const C of i)n.isVNode(C)&&(C.appContext=c);const d=n.h(n.Fragment,i);d.appContext=c,n.render(d,o)},{flush:"sync"})),()=>a.stop()},M=t=>{t.stop(),n.render(null,t.mount),t.mount.remove()},W=(t,o)=>{let c=o.querySelector(`[${A}]`);if(c||(c=document.createElement("div"),o.appendChild(c)),c.setAttribute(A,String(t)),o.dataset.ceriousMasonry==="probe"){const i=j(t,c);queueMicrotask(()=>{i(),n.render(null,c)});return}f.forEach((i,d)=>{i.el===o&&d!==t&&(i.stop(),f.delete(d))});const a=f.get(t);if((a==null?void 0:a.mount)===c)return;const m=j(t,c);f.set(t,{el:o,mount:c,stop:m})},v=()=>{var C;if(!r)return null;const{scroller:t,contentEl:o,container:c}=r,a=c.clientHeight||c.offsetHeight||0,m=(h,E)=>{const g=document.createElement("div");T&&(g.style.display="contents"),g.setAttribute(A,String(h)),E.appendChild(g);const R=j(h,g);f.set(h,{el:E,mount:g,stop:R})},i=t.renderViewport(a,o,m),d=b?null:new Set(t.getRenderedIndices());return f.forEach((h,E)=>{(b?!h.el.isConnected||h.el.dataset.elementIndex!==String(E):!d.has(E))&&(M(h),f.delete(E))}),(C=e.onMeasuredViewport)==null||C.call(e,i),i},O=t=>{if(!r)return;const o=r;r=null,o.unsubscribe(),f.forEach(c=>M(c)),f.clear(),y==null||y(),y=null,o.scroller.detachScrollbar(o.container),o.scroller.dispose(),o.contentEl.textContent="",l.value=null},P=t=>{var g,R,H;const o=e.options??{},c=b?t:X(t),a=o.onScroll,{masonry:m,...i}=o,d={...i,onScroll:()=>{a==null||a(),V()&&v()}};if(b){if(!m)throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");d.masonry={...m,renderItem:W}}if(T&&e.renderHeader){const S=(g=o.table)==null?void 0:g.header;d.table={...o.table,header:I=>{S==null||S(I),$(I)}}}const C=_(n.toValue(e.totalElements),((R=n.toValue(e.items))==null?void 0:R.length)??null),h=new L.CeriousScroll(t,C,d),E=Y(t,S=>{var I;(I=e.onViewportChange)==null||I.call(e,S)});r={scroller:h,contentEl:c,container:t,unsubscribe:E},l.value=h,(H=e.onReady)==null||H.call(e,h),V()&&requestAnimationFrame(()=>v())};return n.onMounted(()=>{const t=p.value;t&&P(t)}),n.onBeforeUnmount(()=>{O()}),n.watch(()=>[n.toValue(e.totalElements),n.toValue(e.items)],()=>{var c;if(!r)return;const t=_(n.toValue(e.totalElements),((c=n.toValue(e.items))==null?void 0:c.length)??null);if((b?r.scroller.itemCount:r.scroller.totalElements)!==t){if(b){const a=p.value;O(),a&&P(a);return}r.scroller.updateTotalElements(t),r.scroller.currentElement>t-1&&r.scroller.jumpToElement(t-1),V()&&v(),r.scroller.syncScrollbar()}}),{containerRef:p,scroller:l,render:v,jumpToElement:t=>r?(r.scroller.jumpToElement(t),v()):null,jumpToItem:(t,o=0)=>r?(r.scroller.jumpToItem(t,o),v()):null,scrollToPercentage:t=>r?(r.scroller.handleScrollPercentage(t),v()):null,reset:()=>r?(r.scroller.reset(),v()):null,recalculate:()=>r?(r.scroller.clearAllCaches(),v()):null}}const k=n.defineComponent({name:"CeriousScroll",props:{totalElements:{type:Number,default:null},items:{type:Array,default:null},getItem:{type:Function,default:void 0},renderItem:{type:Function,default:void 0},options:{type:Object,default:void 0},autoRender:{type:Boolean,default:!0}},emits:{"viewport-change":e=>!0,"measured-viewport":e=>!0,ready:e=>!0},setup(e,{slots:s,emit:u,expose:w}){const p=(r,f)=>{var T;return e.renderItem?e.renderItem(r,f):(T=s.item)==null?void 0:T.call(s,{item:r,index:f})},l=B({totalElements:()=>e.totalElements,items:()=>e.items,getItem:e.getItem?r=>e.getItem(r):void 0,renderItem:p,renderHeader:s.header?()=>s.header():void 0,options:e.options,autoRender:()=>e.autoRender,onViewportChange:r=>u("viewport-change",r),onMeasuredViewport:r=>u("measured-viewport",r),onReady:r=>u("ready",r)});return w({scroller:l.scroller,render:l.render,jumpToElement:l.jumpToElement,jumpToItem:l.jumpToItem,scrollToPercentage:l.scrollToPercentage,reset:l.reset,recalculate:l.recalculate}),()=>{var r;return n.h("div",{ref:l.containerRef,style:{position:"relative",overflow:"hidden"}},(r=s.default)==null?void 0:r.call(s))}}});Object.defineProperty(exports,"CeriousScrollEngine",{enumerable:!0,get:()=>L.CeriousScroll});exports.CeriousScroll=k;exports.useCeriousScroll=B;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { CeriousScroll } from './cerious-scroll';
9
9
  export { useCeriousScroll } from './use-cerious-scroll';
10
- export type { UseCeriousScrollOptions, UseCeriousScrollResult, } from './use-cerious-scroll';
10
+ export type { UseCeriousScrollOptions, UseCeriousScrollResult, CeriousScrollOptions, } from './use-cerious-scroll';
11
11
  export type { CeriousViewportChangeDetail } from './viewport-change';
12
12
  export { CeriousScroll as CeriousScrollEngine } from '@ceriousdevtech/cerious-scroll';
13
- export type { CeriousScrollOptions, KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, } from '@ceriousdevtech/cerious-scroll';
13
+ export type { KeyboardNavigationOptions, TouchNavigationOptions, WheelNavigationOptions, ElementRenderer, ElementHeightCalculator, ScrollResult, MeasuredViewportRange, HeightProvider, MasonryDeterminism, MasonryOptions, } from '@ceriousdevtech/cerious-scroll';
package/dist/index.js CHANGED
@@ -1,156 +1,177 @@
1
- import { getCurrentInstance as U, ref as W, shallowRef as X, onMounted as Y, onBeforeUnmount as z, watch as D, toValue as w, effectScope as N, watchEffect as j, isVNode as H, h as I, Fragment as P, render as T, defineComponent as G } from "vue";
2
- import { CeriousScroll as J } from "@ceriousdevtech/cerious-scroll";
3
- import { CeriousScroll as ie } from "@ceriousdevtech/cerious-scroll";
4
- const _ = "data-cerious-scroll-content", K = "data-cerious-scroll-row";
5
- function Q(e) {
6
- const c = e.querySelector(`[${_}]`);
1
+ import { getCurrentInstance as z, ref as D, shallowRef as G, onMounted as J, onBeforeUnmount as K, watch as Q, toValue as y, render as A, effectScope as L, watchEffect as $, isVNode as B, h as V, Fragment as x, defineComponent as Z } from "vue";
2
+ import { CeriousScroll as ee } from "@ceriousdevtech/cerious-scroll";
3
+ import { CeriousScroll as pe } from "@ceriousdevtech/cerious-scroll";
4
+ const U = "data-cerious-scroll-content", O = "data-cerious-scroll-row";
5
+ function te(e) {
6
+ const c = e.querySelector(`[${U}]`);
7
7
  if (c) return c;
8
- const n = document.createElement("div");
9
- return n.setAttribute(_, ""), n.style.position = "relative", n.style.width = "100%", n.style.height = "100%", n.style.overflowY = "clip", n.style.overflowX = "auto", e.appendChild(n), n;
8
+ const s = document.createElement("div");
9
+ return s.setAttribute(U, ""), s.style.position = "relative", s.style.width = "100%", s.style.height = "100%", s.style.overflowY = "clip", s.style.overflowX = "auto", e.appendChild(s), s;
10
10
  }
11
- function Z(e, c) {
12
- const n = (o) => {
13
- const r = o.detail;
14
- r && c({
15
- percentage: r.percentage,
16
- currentElement: r.currentElement,
17
- scrollOffset: r.scrollOffset,
18
- result: { element: r.result.element, offset: r.result.offset }
11
+ function re(e, c) {
12
+ const s = (p) => {
13
+ const o = p.detail;
14
+ o && c({
15
+ percentage: o.percentage,
16
+ currentElement: o.currentElement,
17
+ scrollOffset: o.scrollOffset,
18
+ result: { element: o.result.element, offset: o.result.offset }
19
19
  });
20
- }, h = (o) => {
21
- const r = o.detail;
22
- r && c({
23
- percentage: r.percentage,
24
- currentElement: r.element,
25
- scrollOffset: r.scrollOffset,
26
- result: { element: r.element, offset: r.scrollOffset }
20
+ }, w = (p) => {
21
+ const o = p.detail;
22
+ o && c({
23
+ percentage: o.percentage,
24
+ currentElement: o.element,
25
+ scrollOffset: o.scrollOffset,
26
+ result: { element: o.element, offset: o.scrollOffset }
27
27
  });
28
28
  };
29
- return e.addEventListener("cerious-viewport-change", n), e.addEventListener("viewport-change", h), () => {
30
- e.removeEventListener("cerious-viewport-change", n), e.removeEventListener("viewport-change", h);
29
+ return e.addEventListener("cerious-viewport-change", s), e.addEventListener("viewport-change", w), () => {
30
+ e.removeEventListener("cerious-viewport-change", s), e.removeEventListener("viewport-change", w);
31
31
  };
32
32
  }
33
- function F(e, c) {
34
- const n = typeof e == "number" ? e : typeof c == "number" ? c : void 0;
35
- if (n === void 0 || Number.isNaN(n))
33
+ function W(e, c) {
34
+ const s = typeof e == "number" ? e : typeof c == "number" ? c : void 0;
35
+ if (s === void 0 || Number.isNaN(s))
36
36
  throw new Error("useCeriousScroll: provide `totalElements` or `items`.");
37
- return Math.max(1, Math.floor(n));
37
+ return Math.max(1, Math.floor(s));
38
38
  }
39
- function k(e) {
40
- var V, x;
41
- const c = (V = U()) == null ? void 0 : V.appContext, n = W(null), h = X(null);
42
- let o = null;
43
- const r = /* @__PURE__ */ new Map();
44
- let s = null;
45
- const b = ((x = e.options) == null ? void 0 : x.layout) === "table";
46
- let f = null;
47
- const L = (t) => {
39
+ function ne(e) {
40
+ var q, _;
41
+ const c = z(), s = c == null ? void 0 : c.provides, w = c ? { ...c.appContext, provides: s ?? c.appContext.provides } : void 0, p = D(null), o = G(null);
42
+ let r = null;
43
+ const d = /* @__PURE__ */ new Map(), T = ((q = e.options) == null ? void 0 : q.layout) === "table", b = ((_ = e.options) == null ? void 0 : _.layout) === "masonry";
44
+ let g = null;
45
+ const X = (t) => {
48
46
  if (!e.renderHeader) return;
49
- f == null || f();
50
- const l = c ?? null, u = N(!0);
51
- u.run(
52
- () => j(
47
+ g == null || g();
48
+ const n = w ?? null, l = L(!0);
49
+ l.run(
50
+ () => $(
53
51
  () => {
54
- const a = e.renderHeader(), d = Array.isArray(a) ? a : [a];
55
- for (const i of d) H(i) && (i.appContext = l);
56
- const m = I(P, d);
57
- m.appContext = l, T(m, t);
52
+ const u = e.renderHeader(), m = Array.isArray(u) ? u : [u];
53
+ for (const i of m) B(i) && (i.appContext = n);
54
+ const a = V(x, m);
55
+ a.appContext = n, A(a, t);
58
56
  },
59
57
  { flush: "sync" }
60
58
  )
61
- ), f = () => u.stop();
62
- }, O = () => w(e.autoRender) ?? !0, $ = (t) => {
59
+ ), g = () => l.stop();
60
+ }, j = () => y(e.autoRender) ?? !0, Y = (t) => {
63
61
  if (e.getItem) return e.getItem(t);
64
- const l = w(e.items);
65
- return l ? l[t] : void 0;
66
- }, q = (t, l) => {
67
- const u = c ?? null, a = N(!0);
68
- return a.run(
69
- () => j(
62
+ const n = y(e.items);
63
+ return n ? n[t] : void 0;
64
+ }, M = (t, n) => {
65
+ const l = w ?? null, u = L(!0);
66
+ return u.run(
67
+ () => $(
70
68
  () => {
71
- const d = e.renderItem($(t), t), m = Array.isArray(d) ? d : [d];
72
- for (const y of m)
73
- H(y) && (y.appContext = u);
74
- const i = I(P, m);
75
- i.appContext = u, T(i, l);
69
+ const m = e.renderItem(Y(t), t), a = Array.isArray(m) ? m : [m];
70
+ for (const C of a)
71
+ B(C) && (C.appContext = l);
72
+ const i = V(x, a);
73
+ i.appContext = l, A(i, n);
76
74
  },
77
75
  { flush: "sync" }
78
76
  )
79
- ), () => a.stop();
80
- }, S = (t) => {
81
- t.stop(), T(null, t.mount), t.mount.remove();
82
- }, p = () => {
83
- var y;
84
- if (!o) return null;
85
- const { scroller: t, contentEl: l, container: u } = o, a = u.clientHeight || u.offsetHeight || 0, d = (v, g) => {
86
- const E = document.createElement("div");
87
- b && (E.style.display = "contents"), E.setAttribute(K, String(v)), g.appendChild(E);
88
- const C = q(v, E);
89
- r.set(v, { el: g, mount: E, stop: C });
90
- }, m = t.renderViewport(a, l, d), i = new Set(t.getRenderedIndices());
91
- return r.forEach((v, g) => {
92
- i.has(g) || (S(v), r.delete(g));
93
- }), (y = e.onMeasuredViewport) == null || y.call(e, m), m;
94
- }, A = (t) => {
95
- if (!o) return;
96
- const l = o;
97
- o = null, l.unsubscribe(), t && (s = {
98
- currentElement: l.scroller.currentElement,
99
- scrollOffset: l.scroller.scrollOffset
100
- }), r.forEach((u) => S(u)), r.clear(), f == null || f(), f = null, l.contentEl.textContent = "", l.scroller.detachScrollbar(l.container), l.scroller.dispose(), h.value = null;
101
- }, M = (t) => {
102
- var v, g, E;
103
- const l = Q(t), u = e.options ?? {}, a = u.onScroll, d = {
104
- ...u,
77
+ ), () => u.stop();
78
+ }, N = (t) => {
79
+ t.stop(), A(null, t.mount), t.mount.remove();
80
+ }, k = (t, n) => {
81
+ let l = n.querySelector(`[${O}]`);
82
+ if (l || (l = document.createElement("div"), n.appendChild(l)), l.setAttribute(O, String(t)), n.dataset.ceriousMasonry === "probe") {
83
+ const a = M(t, l);
84
+ queueMicrotask(() => {
85
+ a(), A(null, l);
86
+ });
87
+ return;
88
+ }
89
+ d.forEach((a, i) => {
90
+ a.el === n && i !== t && (a.stop(), d.delete(i));
91
+ });
92
+ const u = d.get(t);
93
+ if ((u == null ? void 0 : u.mount) === l) return;
94
+ const m = M(t, l);
95
+ d.set(t, { el: n, mount: l, stop: m });
96
+ }, v = () => {
97
+ var C;
98
+ if (!r) return null;
99
+ const { scroller: t, contentEl: n, container: l } = r, u = l.clientHeight || l.offsetHeight || 0, m = (f, E) => {
100
+ const h = document.createElement("div");
101
+ T && (h.style.display = "contents"), h.setAttribute(O, String(f)), E.appendChild(h);
102
+ const R = M(f, h);
103
+ d.set(f, { el: E, mount: h, stop: R });
104
+ }, a = t.renderViewport(u, n, m), i = b ? null : new Set(t.getRenderedIndices());
105
+ return d.forEach((f, E) => {
106
+ (b ? !f.el.isConnected || f.el.dataset.elementIndex !== String(E) : !i.has(E)) && (N(f), d.delete(E));
107
+ }), (C = e.onMeasuredViewport) == null || C.call(e, a), a;
108
+ }, P = (t) => {
109
+ if (!r) return;
110
+ const n = r;
111
+ r = null, n.unsubscribe(), d.forEach((l) => N(l)), d.clear(), g == null || g(), g = null, n.scroller.detachScrollbar(n.container), n.scroller.dispose(), n.contentEl.textContent = "", o.value = null;
112
+ }, H = (t) => {
113
+ var h, R, F;
114
+ const n = e.options ?? {}, l = b ? t : te(t), u = n.onScroll, { masonry: m, ...a } = n, i = {
115
+ ...a,
105
116
  onScroll: () => {
106
- a == null || a(), O() && p();
117
+ u == null || u(), j() && v();
107
118
  }
108
119
  };
109
- if (b && e.renderHeader) {
110
- const C = (v = u.table) == null ? void 0 : v.header;
111
- d.table = {
112
- ...u.table,
113
- header: (R) => {
114
- C == null || C(R), L(R);
120
+ if (b) {
121
+ if (!m)
122
+ throw new Error("useCeriousScroll: layout 'masonry' requires `options.masonry`.");
123
+ i.masonry = {
124
+ ...m,
125
+ renderItem: k
126
+ };
127
+ }
128
+ if (T && e.renderHeader) {
129
+ const I = (h = n.table) == null ? void 0 : h.header;
130
+ i.table = {
131
+ ...n.table,
132
+ header: (S) => {
133
+ I == null || I(S), X(S);
115
134
  }
116
135
  };
117
136
  }
118
- const m = F(w(e.totalElements), ((g = w(e.items)) == null ? void 0 : g.length) ?? null), i = new J(t, m, d);
119
- s && (i.currentElement = Math.min(s.currentElement, m - 1), i.scrollOffset = s.scrollOffset, s = null);
120
- const y = Z(t, (C) => {
121
- var R;
122
- (R = e.onViewportChange) == null || R.call(e, C);
137
+ const C = W(y(e.totalElements), ((R = y(e.items)) == null ? void 0 : R.length) ?? null), f = new ee(t, C, i), E = re(t, (I) => {
138
+ var S;
139
+ (S = e.onViewportChange) == null || S.call(e, I);
123
140
  });
124
- o = { scroller: i, contentEl: l, container: t, unsubscribe: y }, h.value = i, (E = e.onReady) == null || E.call(e, i), O() && requestAnimationFrame(() => p());
125
- }, B = () => {
126
- A(!0);
127
- const t = n.value;
128
- t && M(t);
141
+ r = { scroller: f, contentEl: l, container: t, unsubscribe: E }, o.value = f, (F = e.onReady) == null || F.call(e, f), j() && requestAnimationFrame(() => v());
129
142
  };
130
- return Y(() => {
131
- const t = n.value;
132
- t && M(t);
133
- }), z(() => {
134
- A(!1);
135
- }), D(
136
- () => [w(e.totalElements), w(e.items)],
143
+ return J(() => {
144
+ const t = p.value;
145
+ t && H(t);
146
+ }), K(() => {
147
+ P();
148
+ }), Q(
149
+ () => [y(e.totalElements), y(e.items)],
137
150
  () => {
138
151
  var l;
139
- if (!o) return;
140
- const t = F(w(e.totalElements), ((l = w(e.items)) == null ? void 0 : l.length) ?? null);
141
- o.scroller.totalElements !== t && B();
152
+ if (!r) return;
153
+ const t = W(y(e.totalElements), ((l = y(e.items)) == null ? void 0 : l.length) ?? null);
154
+ if ((b ? r.scroller.itemCount : r.scroller.totalElements) !== t) {
155
+ if (b) {
156
+ const u = p.value;
157
+ P(), u && H(u);
158
+ return;
159
+ }
160
+ r.scroller.updateTotalElements(t), r.scroller.currentElement > t - 1 && r.scroller.jumpToElement(t - 1), j() && v(), r.scroller.syncScrollbar();
161
+ }
142
162
  }
143
163
  ), {
144
- containerRef: n,
145
- scroller: h,
146
- render: p,
147
- jumpToElement: (t) => o ? (o.scroller.jumpToElement(t), p()) : null,
148
- scrollToPercentage: (t) => o ? (o.scroller.handleScrollPercentage(t), p()) : null,
149
- reset: () => o ? (o.scroller.reset(), p()) : null,
150
- recalculate: () => o ? (o.scroller.clearAllCaches(), p()) : null
164
+ containerRef: p,
165
+ scroller: o,
166
+ render: v,
167
+ jumpToElement: (t) => r ? (r.scroller.jumpToElement(t), v()) : null,
168
+ jumpToItem: (t, n = 0) => r ? (r.scroller.jumpToItem(t, n), v()) : null,
169
+ scrollToPercentage: (t) => r ? (r.scroller.handleScrollPercentage(t), v()) : null,
170
+ reset: () => r ? (r.scroller.reset(), v()) : null,
171
+ recalculate: () => r ? (r.scroller.clearAllCaches(), v()) : null
151
172
  };
152
173
  }
153
- const ce = G({
174
+ const me = Z({
154
175
  name: "CeriousScroll",
155
176
  props: {
156
177
  /** Total item count. Falls back to `items.length` when omitted. */
@@ -177,15 +198,15 @@ const ce = G({
177
198
  /** Emitted once the underlying engine instance is ready (and after recreation). */
178
199
  ready: (e) => !0
179
200
  },
180
- setup(e, { slots: c, emit: n, expose: h }) {
181
- const o = (s, b) => {
182
- var f;
183
- return e.renderItem ? e.renderItem(s, b) : (f = c.item) == null ? void 0 : f.call(c, { item: s, index: b });
184
- }, r = k({
201
+ setup(e, { slots: c, emit: s, expose: w }) {
202
+ const p = (r, d) => {
203
+ var T;
204
+ return e.renderItem ? e.renderItem(r, d) : (T = c.item) == null ? void 0 : T.call(c, { item: r, index: d });
205
+ }, o = ne({
185
206
  totalElements: () => e.totalElements,
186
207
  items: () => e.items,
187
- getItem: e.getItem ? (s) => e.getItem(s) : void 0,
188
- renderItem: o,
208
+ getItem: e.getItem ? (r) => e.getItem(r) : void 0,
209
+ renderItem: p,
189
210
  // Table mode: declarative header. Render the `#header` slot (a <tr> of
190
211
  // <th>s) into the engine's <thead>. `undefined` when no slot is provided.
191
212
  renderHeader: c.header ? () => c.header() : void 0,
@@ -193,34 +214,35 @@ const ce = G({
193
214
  // that contract (remount via `:key` to apply new engine options).
194
215
  options: e.options,
195
216
  autoRender: () => e.autoRender,
196
- onViewportChange: (s) => n("viewport-change", s),
197
- onMeasuredViewport: (s) => n("measured-viewport", s),
198
- onReady: (s) => n("ready", s)
217
+ onViewportChange: (r) => s("viewport-change", r),
218
+ onMeasuredViewport: (r) => s("measured-viewport", r),
219
+ onReady: (r) => s("ready", r)
199
220
  });
200
- return h({
221
+ return w({
201
222
  /** The underlying engine instance (`null` before mount). */
202
- scroller: r.scroller,
203
- render: r.render,
204
- jumpToElement: r.jumpToElement,
205
- scrollToPercentage: r.scrollToPercentage,
206
- reset: r.reset,
207
- recalculate: r.recalculate
223
+ scroller: o.scroller,
224
+ render: o.render,
225
+ jumpToElement: o.jumpToElement,
226
+ jumpToItem: o.jumpToItem,
227
+ scrollToPercentage: o.scrollToPercentage,
228
+ reset: o.reset,
229
+ recalculate: o.recalculate
208
230
  }), () => {
209
- var s;
210
- return I(
231
+ var r;
232
+ return V(
211
233
  "div",
212
234
  {
213
- ref: r.containerRef,
235
+ ref: o.containerRef,
214
236
  // User-supplied class/style/attrs fall through and merge onto this root.
215
237
  style: { position: "relative", overflow: "hidden" }
216
238
  },
217
- (s = c.default) == null ? void 0 : s.call(c)
239
+ (r = c.default) == null ? void 0 : r.call(c)
218
240
  );
219
241
  };
220
242
  }
221
243
  });
222
244
  export {
223
- ce as CeriousScroll,
224
- ie as CeriousScrollEngine,
225
- k as useCeriousScroll
245
+ me as CeriousScroll,
246
+ pe as CeriousScrollEngine,
247
+ ne as useCeriousScroll
226
248
  };
@@ -1,6 +1,13 @@
1
1
  import { MaybeRefOrGetter, Ref, VNodeChild } from 'vue';
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 Vue-owned Masonry rendering. */
5
+ export type CeriousScrollOptions = Omit<CoreCeriousScrollOptions, 'masonry'> & {
6
+ masonry?: Omit<MasonryOptions, 'renderItem'> & {
7
+ /** Accepted for backward compatibility; Vue 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?: MaybeRefOrGetter<number | null | undefined>;
@@ -35,6 +42,8 @@ export interface UseCeriousScrollResult {
35
42
  render: () => MeasuredViewportRange | null;
36
43
  /** Jump to an element index, then render. */
37
44
  jumpToElement: (index: number) => MeasuredViewportRange | null;
45
+ /** Masonry mode only: jump to a card index, then render. */
46
+ jumpToItem: (index: number, screenOffset?: number) => MeasuredViewportRange | null;
38
47
  /** Scroll to a percentage (0..100), then render. */
39
48
  scrollToPercentage: (percentage: number) => MeasuredViewportRange | null;
40
49
  /** Reset to the top, then render. */
@@ -61,6 +70,7 @@ export interface UseCeriousScrollResult {
61
70
  *
62
71
  * Rows are rendered with the host component's `appContext`, so globally
63
72
  * registered components, directives, and installed plugins are available inside
64
- * each row.
73
+ * each row — and with the owning component instance's `provides`, so local
74
+ * `provide()` values reach `inject()` inside rows too (see below).
65
75
  */
66
76
  export declare function useCeriousScroll<TItem = unknown>(opts: UseCeriousScrollOptions<TItem>): UseCeriousScrollResult;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ceriousdevtech/vue-cerious-scroll",
3
- "version": "1.0.6",
4
- "description": "Vue 3 bindings for CeriousScroll — high-performance virtual scrolling with O(1) memory and no height estimation",
3
+ "version": "1.1.0",
4
+ "description": "Vue 3 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",
@@ -45,6 +45,7 @@
45
45
  "large-lists",
46
46
  "data-grid",
47
47
  "variable-height",
48
+ "masonry",
48
49
  "o1-memory",
49
50
  "cerious-scroll"
50
51
  ],
@@ -57,7 +58,7 @@
57
58
  "demo:build": "vite build --config demo/vite.config.ts"
58
59
  },
59
60
  "dependencies": {
60
- "@ceriousdevtech/cerious-scroll": "^1.0.7"
61
+ "@ceriousdevtech/cerious-scroll": "^1.1.0"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "vue": "^3.3.0"