tarojs-plugin-chucker 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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * useVirtualList — Taro React Hook
3
+ *
4
+ * Provides the same high-performance chunk-based virtualization as
5
+ * the native virtualListBehavior, but as a React hook for Taro projects.
6
+ *
7
+ * Usage:
8
+ * import { useVirtualList } from 'miniprogram-virtual-list/taro';
9
+ *
10
+ * function MyPage() {
11
+ * const [items, setItems] = useState(generateItems(0, 1000));
12
+ * const { vlChunks, vlVisible, vlStyles, vlActiveIndex, vlScrollTo, vlScrollToItem } = useVirtualList({
13
+ * items,
14
+ * itemHeight: 80,
15
+ * chunkSize: 10,
16
+ * overScan: 1,
17
+ * columns: 1
18
+ * });
19
+ *
20
+ * return (
21
+ * <View>
22
+ * <Button onClick={() => vlScrollToItem('item-500')}>Scroll to #500</Button>
23
+ * <Text>Active Chunk: {vlActiveIndex}</Text>
24
+ * {vlChunks.map((chunk, ci) => (
25
+ * <View key={ci} id={`vl-chunk-${ci}`} className="vl-chunk" style={vlStyles[ci]}>
26
+ * {vlVisible[ci] && chunk.map((item) => (
27
+ * <MyItem key={item.id} item={item} index={item.__vlIdx} />
28
+ * ))}
29
+ * </View>
30
+ * ))}
31
+ * </View>
32
+ * );
33
+ * }
34
+ */
35
+ export interface UseVirtualListOptions<T> {
36
+ items: T[];
37
+ /** Estimated height (px) of a single item — used for initial placeholder sizing */
38
+ itemHeight?: number;
39
+ /** Number of items per chunk. If not configured, auto-scales based on dataset length */
40
+ chunkSize?: number;
41
+ /** Number of extra chunks to render above and below the viewport bounds */
42
+ overScan?: number;
43
+ /** Number of layout columns (1 for list, >1 for grid) */
44
+ columns?: number;
45
+ /** Disable virtualization (e.g., when UI is minimized) to avoid unnecessary processing */
46
+ disabled?: boolean;
47
+ }
48
+ export interface VirtualListResult<T> {
49
+ /** Chunked item arrays, each item augmented with `__vlIdx` (absolute index) */
50
+ vlChunks: Array<T & {
51
+ __vlIdx: number;
52
+ }>[];
53
+ /** Per-chunk visibility flags */
54
+ vlVisible: boolean[];
55
+ /** Per-chunk inline style strings (height placeholders) */
56
+ vlStyles: string[];
57
+ /** Index of the first visible chunk currently in viewport */
58
+ vlActiveIndex: number;
59
+ /** Array of all currently visible chunk indices */
60
+ vlVisibleIndices: number[];
61
+ /** Scroll programmatically to target item index */
62
+ vlScrollTo: (index: number, options?: {
63
+ duration?: number;
64
+ }) => number;
65
+ /** Scroll programmatically to target item object reference or ID key */
66
+ vlScrollToItem: (target: any, options?: {
67
+ duration?: number;
68
+ keyProperty?: string;
69
+ }) => number;
70
+ }
71
+ export declare function useVirtualList<T>(options: UseVirtualListOptions<T>): VirtualListResult<T>;
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ /**
3
+ * useVirtualList — Taro React Hook
4
+ *
5
+ * Provides the same high-performance chunk-based virtualization as
6
+ * the native virtualListBehavior, but as a React hook for Taro projects.
7
+ *
8
+ * Usage:
9
+ * import { useVirtualList } from 'miniprogram-virtual-list/taro';
10
+ *
11
+ * function MyPage() {
12
+ * const [items, setItems] = useState(generateItems(0, 1000));
13
+ * const { vlChunks, vlVisible, vlStyles, vlActiveIndex, vlScrollTo, vlScrollToItem } = useVirtualList({
14
+ * items,
15
+ * itemHeight: 80,
16
+ * chunkSize: 10,
17
+ * overScan: 1,
18
+ * columns: 1
19
+ * });
20
+ *
21
+ * return (
22
+ * <View>
23
+ * <Button onClick={() => vlScrollToItem('item-500')}>Scroll to #500</Button>
24
+ * <Text>Active Chunk: {vlActiveIndex}</Text>
25
+ * {vlChunks.map((chunk, ci) => (
26
+ * <View key={ci} id={`vl-chunk-${ci}`} className="vl-chunk" style={vlStyles[ci]}>
27
+ * {vlVisible[ci] && chunk.map((item) => (
28
+ * <MyItem key={item.id} item={item} index={item.__vlIdx} />
29
+ * ))}
30
+ * </View>
31
+ * ))}
32
+ * </View>
33
+ * );
34
+ * }
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.useVirtualList = useVirtualList;
38
+ const react_1 = require("react");
39
+ function useVirtualList(options) {
40
+ const { items, itemHeight = 80, chunkSize, overScan = 1, columns = 1, disabled = false } = options;
41
+ const [vlChunks, setVlChunks] = (0, react_1.useState)([]);
42
+ const [vlVisible, setVlVisible] = (0, react_1.useState)([]);
43
+ const [vlStyles, setVlStyles] = (0, react_1.useState)([]);
44
+ const observerRef = (0, react_1.useRef)(null);
45
+ const visibleRef = (0, react_1.useRef)([]);
46
+ const stylesRef = (0, react_1.useRef)([]);
47
+ const chunksRef = (0, react_1.useRef)([]);
48
+ const prevItemsRef = (0, react_1.useRef)([]);
49
+ const prevChunkSizeRef = (0, react_1.useRef)(10);
50
+ const prevItemHeightRef = (0, react_1.useRef)(itemHeight);
51
+ const prevChunksCountRef = (0, react_1.useRef)(0);
52
+ const isAppendRef = (0, react_1.useRef)(false);
53
+ const isUnmountingRef = (0, react_1.useRef)(false);
54
+ const prevChunksLengthRef = (0, react_1.useRef)(0);
55
+ const disconnectAll = (0, react_1.useCallback)(() => {
56
+ if (observerRef.current) {
57
+ try {
58
+ observerRef.current.disconnect();
59
+ }
60
+ catch (_) {
61
+ /* noop */
62
+ }
63
+ }
64
+ observerRef.current = null;
65
+ }, []);
66
+ // Track unmount lifecycle specifically
67
+ (0, react_1.useEffect)(() => {
68
+ return () => {
69
+ isUnmountingRef.current = true;
70
+ };
71
+ }, []);
72
+ // Chunk the items whenever the input array or sizing changes
73
+ (0, react_1.useEffect)(() => {
74
+ if (disabled) {
75
+ setVlChunks([]);
76
+ setVlVisible([]);
77
+ setVlStyles([]);
78
+ chunksRef.current = [];
79
+ visibleRef.current = [];
80
+ stylesRef.current = [];
81
+ prevItemsRef.current = [];
82
+ return;
83
+ }
84
+ const prevItems = prevItemsRef.current;
85
+ prevItemsRef.current = items;
86
+ // Determine current chunkSize (property if configured, otherwise dynamically scaled)
87
+ let currentChunkSize = chunkSize;
88
+ if (!currentChunkSize) {
89
+ if (prevChunksCountRef.current > 0) {
90
+ currentChunkSize = prevChunkSizeRef.current || 10;
91
+ }
92
+ else {
93
+ currentChunkSize = Math.max(10, Math.min(100, Math.ceil(items.length / 300)));
94
+ }
95
+ }
96
+ const prevChunkSize = prevChunkSizeRef.current;
97
+ const prevItemHeight = prevItemHeightRef.current;
98
+ prevChunkSizeRef.current = currentChunkSize;
99
+ prevItemHeightRef.current = itemHeight;
100
+ // Check if it is an append operation (same start item, length >= old length, same sizing configuration)
101
+ const isAppend = prevItems.length > 0 &&
102
+ items.length >= prevItems.length &&
103
+ items[0] === prevItems[0] &&
104
+ currentChunkSize === prevChunkSize &&
105
+ itemHeight === prevItemHeight;
106
+ isAppendRef.current = isAppend;
107
+ let chunks = [];
108
+ if (isAppend) {
109
+ const oldChunks = vlChunks;
110
+ prevChunksCountRef.current = oldChunks.length;
111
+ // Shallow clone existing chunks to avoid mutation issues
112
+ chunks = oldChunks.map((c) => [...c]);
113
+ const oldItemsCount = prevItems.length;
114
+ const newRawItems = items.slice(oldItemsCount);
115
+ // Map only new items with absolute index
116
+ const mappedNew = newRawItems.map((item, idx) => {
117
+ const absoluteIdx = oldItemsCount + idx;
118
+ if (typeof item === "object" && item !== null) {
119
+ return { ...item, __vlIdx: absoluteIdx };
120
+ }
121
+ return item;
122
+ });
123
+ let remainingNew = mappedNew;
124
+ if (chunks.length > 0) {
125
+ const lastChunkIndex = chunks.length - 1;
126
+ const lastChunk = chunks[lastChunkIndex];
127
+ if (lastChunk.length < currentChunkSize) {
128
+ const slotsOpen = currentChunkSize - lastChunk.length;
129
+ const itemsToFill = mappedNew.slice(0, slotsOpen);
130
+ chunks[lastChunkIndex] = lastChunk.concat(itemsToFill);
131
+ // If the last chunk is off-screen, recalculate height placeholder
132
+ if (!visibleRef.current[lastChunkIndex]) {
133
+ stylesRef.current[lastChunkIndex] =
134
+ "height: " + Math.ceil(chunks[lastChunkIndex].length / columns) * itemHeight + "px;";
135
+ }
136
+ remainingNew = mappedNew.slice(slotsOpen);
137
+ }
138
+ }
139
+ const newChunks = [];
140
+ for (let i = 0; i < remainingNew.length; i += currentChunkSize) {
141
+ newChunks.push(remainingNew.slice(i, i + currentChunkSize));
142
+ }
143
+ newChunks.forEach((chunk) => {
144
+ chunks.push(chunk);
145
+ visibleRef.current.push(false);
146
+ stylesRef.current.push("height: " + Math.ceil(chunk.length / columns) * itemHeight + "px;");
147
+ });
148
+ chunksRef.current = chunks;
149
+ setVlChunks(chunks);
150
+ setVlVisible([...visibleRef.current]);
151
+ setVlStyles([...stylesRef.current]);
152
+ }
153
+ else {
154
+ prevChunksCountRef.current = 0;
155
+ const mapped = items.map((item, idx) => {
156
+ if (typeof item === "object" && item !== null) {
157
+ return { ...item, __vlIdx: idx };
158
+ }
159
+ return item;
160
+ });
161
+ for (let i = 0; i < mapped.length; i += currentChunkSize) {
162
+ chunks.push(mapped.slice(i, i + currentChunkSize));
163
+ }
164
+ const visibleList = [];
165
+ const stylesList = [];
166
+ for (let i = 0; i < chunks.length; i++) {
167
+ const isFirst = i === 0;
168
+ visibleList.push(isFirst);
169
+ stylesList.push(isFirst
170
+ ? "height: auto;"
171
+ : "height: " + Math.ceil(chunks[i].length / columns) * itemHeight + "px;");
172
+ }
173
+ visibleRef.current = visibleList;
174
+ stylesRef.current = stylesList;
175
+ chunksRef.current = chunks;
176
+ setVlChunks(chunks);
177
+ setVlVisible(visibleList);
178
+ setVlStyles(stylesList);
179
+ }
180
+ }, [items, itemHeight, chunkSize, columns, disabled]);
181
+ (0, react_1.useEffect)(() => {
182
+ if (disabled || vlChunks.length === 0) {
183
+ disconnectAll();
184
+ prevChunksLengthRef.current = 0;
185
+ return;
186
+ }
187
+ // Only recreate observer when number of chunks changes to avoid lag
188
+ if (observerRef.current && vlChunks.length === prevChunksLengthRef.current) {
189
+ return;
190
+ }
191
+ prevChunksLengthRef.current = vlChunks.length;
192
+ disconnectAll();
193
+ const timer = setTimeout(() => {
194
+ var _a;
195
+ const runtimeTaro = require("@tarojs/taro");
196
+ const page = (_a = runtimeTaro.getCurrentInstance()) === null || _a === void 0 ? void 0 : _a.page;
197
+ if (!page)
198
+ return;
199
+ const currentChunkSize = prevChunkSizeRef.current || 10;
200
+ const buffer = overScan * Math.ceil(currentChunkSize / columns) * itemHeight;
201
+ observerRef.current = runtimeTaro.createIntersectionObserver(page, { observeAll: true });
202
+ observerRef.current
203
+ .relativeToViewport({ top: buffer, bottom: buffer })
204
+ .observe(".vl-chunk", (res) => {
205
+ const indexStr = res.id.replace("vl-chunk-", "");
206
+ const targetIndex = parseInt(indexStr, 10);
207
+ if (isNaN(targetIndex))
208
+ return;
209
+ const isIn = res.intersectionRatio > 0;
210
+ const wasIn = visibleRef.current[targetIndex];
211
+ if (isIn && !wasIn) {
212
+ visibleRef.current[targetIndex] = true;
213
+ stylesRef.current[targetIndex] = "height: auto;";
214
+ setVlVisible([...visibleRef.current]);
215
+ setVlStyles([...stylesRef.current]);
216
+ }
217
+ else if (!isIn && wasIn) {
218
+ const query = runtimeTaro.createSelectorQuery();
219
+ query
220
+ .select("#vl-chunk-" + targetIndex)
221
+ .boundingClientRect((rect) => {
222
+ if (rect) {
223
+ const measuredHeight = rect.height || 0;
224
+ visibleRef.current[targetIndex] = false;
225
+ stylesRef.current[targetIndex] = "height: " + measuredHeight + "px;";
226
+ setVlVisible([...visibleRef.current]);
227
+ setVlStyles([...stylesRef.current]);
228
+ }
229
+ })
230
+ .exec();
231
+ }
232
+ });
233
+ }, 100);
234
+ return () => {
235
+ clearTimeout(timer);
236
+ if (isUnmountingRef.current) {
237
+ disconnectAll();
238
+ }
239
+ };
240
+ }, [vlChunks.length, overScan, itemHeight, columns, disconnectAll, disabled]);
241
+ (0, react_1.useEffect)(() => {
242
+ return () => disconnectAll();
243
+ }, [disconnectAll]);
244
+ const vlScrollTo = (0, react_1.useCallback)((index, options) => {
245
+ if (index < 0 || index >= items.length)
246
+ return 0;
247
+ const currentChunkSize = prevChunkSizeRef.current || 10;
248
+ const chunkIndex = Math.floor(index / currentChunkSize);
249
+ let scrollTop = 0;
250
+ for (let i = 0; i < chunkIndex; i++) {
251
+ const style = stylesRef.current[i] || "";
252
+ const match = style.match(/height:\s*([\d.]+)/);
253
+ if (match) {
254
+ scrollTop += parseFloat(match[1]);
255
+ }
256
+ else {
257
+ const chunkLength = chunksRef.current[i] ? chunksRef.current[i].length : currentChunkSize;
258
+ scrollTop += Math.ceil(chunkLength / columns) * itemHeight;
259
+ }
260
+ }
261
+ scrollTop += Math.floor((index % currentChunkSize) / columns) * itemHeight;
262
+ const duration = options && typeof options.duration === "number" ? options.duration : 0;
263
+ const runtimeTaro = require("@tarojs/taro");
264
+ runtimeTaro.pageScrollTo({
265
+ scrollTop,
266
+ duration,
267
+ });
268
+ return scrollTop;
269
+ }, [items.length, itemHeight, columns]);
270
+ const vlScrollToItem = (0, react_1.useCallback)((target, options) => {
271
+ const keyProperty = (options === null || options === void 0 ? void 0 : options.keyProperty) || "id";
272
+ const index = items.findIndex((item) => {
273
+ if (typeof target === "object" && target !== null) {
274
+ return item === target;
275
+ }
276
+ return item && item[keyProperty] === target;
277
+ });
278
+ if (index !== -1) {
279
+ return vlScrollTo(index, options);
280
+ }
281
+ return 0;
282
+ }, [items, vlScrollTo]);
283
+ const vlVisibleIndices = vlVisible.map((v, i) => (v ? i : -1)).filter((i) => i !== -1);
284
+ const vlActiveIndex = vlVisibleIndices.length > 0 ? vlVisibleIndices[0] : -1;
285
+ // Filter chunks through Data Windowing before yielding to render tree
286
+ const renderedChunks = vlChunks.map((chunk, idx) => {
287
+ return vlVisible[idx] ? chunk : [];
288
+ });
289
+ return {
290
+ vlChunks: renderedChunks,
291
+ vlVisible,
292
+ vlStyles,
293
+ vlActiveIndex,
294
+ vlVisibleIndices,
295
+ vlScrollTo,
296
+ vlScrollToItem,
297
+ };
298
+ }
@@ -0,0 +1,9 @@
1
+ import { chuckerStore } from "./store";
2
+ import { Chucker } from "./components/Chucker";
3
+ export interface ChuckerInitOptions {
4
+ maxLogs?: number;
5
+ }
6
+ export declare function initChucker(options?: ChuckerInitOptions): void;
7
+ export { Chucker, chuckerStore };
8
+ export type { ChuckerLog } from "./interceptor";
9
+ export type { CustomLogInput, TrackingCompleteInput } from "./store";
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.chuckerStore = exports.Chucker = void 0;
7
+ exports.initChucker = initChucker;
8
+ const taro_1 = __importDefault(require("@tarojs/taro"));
9
+ const interceptor_1 = require("./interceptor");
10
+ const store_1 = require("./store");
11
+ Object.defineProperty(exports, "chuckerStore", { enumerable: true, get: function () { return store_1.chuckerStore; } });
12
+ const Chucker_1 = require("./components/Chucker");
13
+ Object.defineProperty(exports, "Chucker", { enumerable: true, get: function () { return Chucker_1.Chucker; } });
14
+ function initChucker(options) {
15
+ var _a;
16
+ const maxLogs = (_a = options === null || options === void 0 ? void 0 : options.maxLogs) !== null && _a !== void 0 ? _a : 100;
17
+ // Initialize state store
18
+ store_1.chuckerStore.init(maxLogs);
19
+ // Initialize API interceptors
20
+ (0, interceptor_1.initInterceptors)();
21
+ // Monkey-patch native Page and Component constructors if they exist (for injected WXML floating button triggers)
22
+ const globalObj = typeof globalThis !== "undefined"
23
+ ? globalThis
24
+ : typeof global !== "undefined"
25
+ ? global
26
+ : typeof window !== "undefined"
27
+ ? window
28
+ : {};
29
+ // @ts-ignore
30
+ if (typeof Page === "function") {
31
+ // @ts-ignore
32
+ const originalPage = Page;
33
+ // @ts-ignore
34
+ globalObj.Page = function (pageOpts) {
35
+ if (pageOpts) {
36
+ pageOpts.chuckerTap = function () {
37
+ taro_1.default.navigateTo({ url: "/pages/chucker/index" });
38
+ };
39
+ }
40
+ return originalPage(pageOpts);
41
+ };
42
+ }
43
+ // @ts-ignore
44
+ if (typeof Component === "function") {
45
+ // @ts-ignore
46
+ const originalComponent = Component;
47
+ // @ts-ignore
48
+ globalObj.Component = function (compOpts) {
49
+ if (compOpts) {
50
+ if (!compOpts.methods) {
51
+ compOpts.methods = {};
52
+ }
53
+ compOpts.methods.chuckerTap = function () {
54
+ taro_1.default.navigateTo({ url: "/pages/chucker/index" });
55
+ };
56
+ compOpts.chuckerTap = function () {
57
+ taro_1.default.navigateTo({ url: "/pages/chucker/index" });
58
+ };
59
+ }
60
+ return originalComponent(compOpts);
61
+ };
62
+ }
63
+ console.log("Taro Chucker Interceptors initialized");
64
+ }
@@ -0,0 +1,15 @@
1
+ export interface ChuckerLog {
2
+ id: string;
3
+ type: "network" | "native" | (string & {});
4
+ method: string;
5
+ url: string;
6
+ requestHeaders?: Record<string, string>;
7
+ requestData?: any;
8
+ status?: string | number;
9
+ responseHeaders?: Record<string, string>;
10
+ responseData?: any;
11
+ error?: string;
12
+ startTime: number;
13
+ duration?: number;
14
+ }
15
+ export declare function initInterceptors(): void;