@webority-technologies/mobile-ui 0.0.8 → 0.0.9

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.
@@ -14,6 +14,8 @@ var _jsxRuntime = require("react/jsx-runtime");
14
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
15
  const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
16
16
  const MAX_RENDER_SCALE = 3;
17
+ /** How many pages beyond the current one stay mounted, each direction. */
18
+ const RENDER_WINDOW = 1;
17
19
  const toError = value => value instanceof Error ? value : new Error(String(value));
18
20
 
19
21
  /** The renderPage scale is capped so a very wide view never asks the native
@@ -38,11 +40,19 @@ const localPathFromSource = async source => {
38
40
  });
39
41
  return result.path;
40
42
  };
43
+
41
44
  /**
42
45
  * Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
43
- * Android) — no third-party PDF dependency. The WebView fallback consumer
44
- * apps use for non-PDF office documents is a distinct rendering strategy and
45
- * stays app-side.
46
+ * Android) — no third-party PDF dependency. Pages are paged through a plain
47
+ * `ScrollView` with manual windowed mounting (render the visible page ±
48
+ * `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
49
+ * would trip React Native's "VirtualizedLists should never be nested inside
50
+ * plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
51
+ * scrolling screen, which is a completely ordinary way to use it. Avoiding the
52
+ * VirtualizedList primitive entirely removes that whole class of warning
53
+ * rather than working around one instance of it. The WebView fallback
54
+ * consumer apps use for non-PDF office documents is a distinct rendering
55
+ * strategy and stays app-side.
46
56
  */
47
57
  const DocumentViewer = ({
48
58
  source,
@@ -64,11 +74,14 @@ const DocumentViewer = ({
64
74
  }); // US Letter fallback
65
75
  const pageAspect = pageSizePt.height / pageSizePt.width;
66
76
  const [renderedPages, setRenderedPages] = (0, _react.useState)({});
77
+ const [currentPage, setCurrentPage] = (0, _react.useState)(0);
67
78
  const handleRef = (0, _react.useRef)(null);
68
79
  const onErrorRef = (0, _react.useRef)(onError);
69
80
  onErrorRef.current = onError;
70
81
  const onLoadCompleteRef = (0, _react.useRef)(onLoadComplete);
71
82
  onLoadCompleteRef.current = onLoadComplete;
83
+ const onPageChangedRef = (0, _react.useRef)(onPageChanged);
84
+ onPageChangedRef.current = onPageChanged;
72
85
  const documentSource = (0, _react.useMemo)(() => ({
73
86
  uri: source.uri,
74
87
  headers: source.headers,
@@ -93,6 +106,8 @@ const DocumentViewer = ({
93
106
  }
94
107
  handleRef.current = opened.handle;
95
108
  setPageCount(opened.pageCount);
109
+ setCurrentPage(0);
110
+ setRenderedPages({});
96
111
  if (opened.pageWidth > 0 && opened.pageHeight > 0) {
97
112
  setPageSizePt({
98
113
  width: opened.pageWidth,
@@ -135,56 +150,37 @@ const DocumentViewer = ({
135
150
  _mobileCore.Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
136
151
  }
137
152
  }, [pageSizePt.width, windowWidth]);
138
- const data = (0, _react.useMemo)(() => Array.from({
139
- length: pageCount
140
- }, (_unused, index) => ({
141
- index
142
- })), [pageCount]);
143
- const onPageChangedRef = (0, _react.useRef)(onPageChanged);
144
- onPageChangedRef.current = onPageChanged;
145
- const pageCountRef = (0, _react.useRef)(pageCount);
146
- pageCountRef.current = pageCount;
147
- const renderPageImageRef = (0, _react.useRef)(renderPageImage);
148
- renderPageImageRef.current = renderPageImage;
153
+ const pageHeight = windowWidth * pageAspect;
149
154
 
150
- // A stable function identity: FlatList warns if onViewableItemsChanged changes
151
- // identity across renders, so the current values it needs are read from refs
152
- // kept up to date every render instead of being closed over here.
153
- const onViewableItemsChanged = (0, _react.useRef)(({
154
- viewableItems
155
- }) => {
156
- const first = viewableItems[0]?.item;
157
- if (first) {
158
- onPageChangedRef.current?.(first.index + 1, pageCountRef.current);
159
- viewableItems.forEach(v => void renderPageImageRef.current(v.item.index));
155
+ // Mount and rasterize the current page plus a small window around it;
156
+ // everything else stays an empty placeholder slot until scrolled near.
157
+ (0, _react.useEffect)(() => {
158
+ if (pageCount === 0) {
159
+ return;
160
160
  }
161
- });
162
- const renderItem = (0, _react.useCallback)(({
163
- item
164
- }) => {
165
- const uri = renderedPages[item.index];
166
- const pageWidth = windowWidth;
167
- const pageHeight = pageWidth * pageAspect;
168
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
169
- style: [styles.page, {
170
- width: pageWidth,
171
- height: pageHeight
172
- }],
173
- children: uri ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
174
- source: {
175
- uri
176
- },
177
- style: styles.pageImage,
178
- resizeMode: "contain",
179
- accessible: false
180
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
181
- style: styles.pagePlaceholder,
182
- children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
183
- size: "lg"
184
- })
185
- })
161
+ const start = Math.max(0, currentPage - RENDER_WINDOW);
162
+ const end = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
163
+ for (let index = start; index <= end; index += 1) {
164
+ void renderPageImage(index);
165
+ }
166
+ }, [currentPage, pageCount, renderPageImage]);
167
+ const handleScroll = (0, _react.useCallback)(event => {
168
+ if (pageHeight <= 0) {
169
+ return;
170
+ }
171
+ const page = Math.min(pageCount - 1, Math.max(0, Math.round(event.nativeEvent.contentOffset.y / pageHeight)));
172
+ setCurrentPage(prev => {
173
+ if (prev !== page) {
174
+ onPageChangedRef.current?.(page + 1, pageCount);
175
+ }
176
+ return page;
186
177
  });
187
- }, [loadingIndicator, pageAspect, renderedPages, windowWidth]);
178
+ }, [pageCount, pageHeight]);
179
+ const pages = (0, _react.useMemo)(() => Array.from({
180
+ length: pageCount
181
+ }, (_unused, i) => i), [pageCount]);
182
+ const renderWindowStart = Math.max(0, currentPage - RENDER_WINDOW);
183
+ const renderWindowEnd = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
188
184
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
189
185
  style: [styles.root, style],
190
186
  testID: testID,
@@ -196,16 +192,34 @@ const DocumentViewer = ({
196
192
  children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
197
193
  size: "lg"
198
194
  })
199
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
200
- data: data,
201
- keyExtractor: item => String(item.index),
202
- renderItem: renderItem,
203
- onViewableItemsChanged: onViewableItemsChanged.current,
204
- viewabilityConfig: {
205
- itemVisiblePercentThreshold: 50
206
- },
207
- initialNumToRender: 2,
208
- windowSize: 3
195
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
196
+ onScroll: handleScroll,
197
+ scrollEventThrottle: 32,
198
+ showsVerticalScrollIndicator: true,
199
+ removeClippedSubviews: true,
200
+ children: pages.map(index => {
201
+ const withinWindow = index >= renderWindowStart && index <= renderWindowEnd;
202
+ const uri = withinWindow ? renderedPages[index] : undefined;
203
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
204
+ style: [styles.page, {
205
+ width: windowWidth,
206
+ height: pageHeight
207
+ }],
208
+ children: uri ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
209
+ source: {
210
+ uri
211
+ },
212
+ style: styles.pageImage,
213
+ resizeMode: "contain",
214
+ accessible: false
215
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
216
+ style: styles.pagePlaceholder,
217
+ children: withinWindow ? loadingIndicator?.() ?? /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Spinner, {
218
+ size: "lg"
219
+ }) : null
220
+ })
221
+ }, index);
222
+ })
209
223
  })
210
224
  });
211
225
  };
@@ -3,12 +3,14 @@
3
3
  import { Logger } from '@webority-technologies/mobile-core';
4
4
  import { downloadFile } from '@webority-technologies/mobile-core/download';
5
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
- import { FlatList, Image, PixelRatio, StyleSheet, useWindowDimensions, View } from 'react-native';
6
+ import { Image, PixelRatio, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
7
7
  import PdfRasterizer from "../../specs/NativePdfRasterizer.js";
8
8
  import { Spinner } from "../Spinner/index.js";
9
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
10
  const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
11
11
  const MAX_RENDER_SCALE = 3;
12
+ /** How many pages beyond the current one stay mounted, each direction. */
13
+ const RENDER_WINDOW = 1;
12
14
  const toError = value => value instanceof Error ? value : new Error(String(value));
13
15
 
14
16
  /** The renderPage scale is capped so a very wide view never asks the native
@@ -33,11 +35,19 @@ const localPathFromSource = async source => {
33
35
  });
34
36
  return result.path;
35
37
  };
38
+
36
39
  /**
37
40
  * Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
38
- * Android) — no third-party PDF dependency. The WebView fallback consumer
39
- * apps use for non-PDF office documents is a distinct rendering strategy and
40
- * stays app-side.
41
+ * Android) — no third-party PDF dependency. Pages are paged through a plain
42
+ * `ScrollView` with manual windowed mounting (render the visible page ±
43
+ * `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
44
+ * would trip React Native's "VirtualizedLists should never be nested inside
45
+ * plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
46
+ * scrolling screen, which is a completely ordinary way to use it. Avoiding the
47
+ * VirtualizedList primitive entirely removes that whole class of warning
48
+ * rather than working around one instance of it. The WebView fallback
49
+ * consumer apps use for non-PDF office documents is a distinct rendering
50
+ * strategy and stays app-side.
41
51
  */
42
52
  export const DocumentViewer = ({
43
53
  source,
@@ -59,11 +69,14 @@ export const DocumentViewer = ({
59
69
  }); // US Letter fallback
60
70
  const pageAspect = pageSizePt.height / pageSizePt.width;
61
71
  const [renderedPages, setRenderedPages] = useState({});
72
+ const [currentPage, setCurrentPage] = useState(0);
62
73
  const handleRef = useRef(null);
63
74
  const onErrorRef = useRef(onError);
64
75
  onErrorRef.current = onError;
65
76
  const onLoadCompleteRef = useRef(onLoadComplete);
66
77
  onLoadCompleteRef.current = onLoadComplete;
78
+ const onPageChangedRef = useRef(onPageChanged);
79
+ onPageChangedRef.current = onPageChanged;
67
80
  const documentSource = useMemo(() => ({
68
81
  uri: source.uri,
69
82
  headers: source.headers,
@@ -88,6 +101,8 @@ export const DocumentViewer = ({
88
101
  }
89
102
  handleRef.current = opened.handle;
90
103
  setPageCount(opened.pageCount);
104
+ setCurrentPage(0);
105
+ setRenderedPages({});
91
106
  if (opened.pageWidth > 0 && opened.pageHeight > 0) {
92
107
  setPageSizePt({
93
108
  width: opened.pageWidth,
@@ -130,56 +145,37 @@ export const DocumentViewer = ({
130
145
  Logger.error(`[@webority-technologies/mobile-ui] DocumentViewer failed to render page ${pageIndex}.`, toError(error));
131
146
  }
132
147
  }, [pageSizePt.width, windowWidth]);
133
- const data = useMemo(() => Array.from({
134
- length: pageCount
135
- }, (_unused, index) => ({
136
- index
137
- })), [pageCount]);
138
- const onPageChangedRef = useRef(onPageChanged);
139
- onPageChangedRef.current = onPageChanged;
140
- const pageCountRef = useRef(pageCount);
141
- pageCountRef.current = pageCount;
142
- const renderPageImageRef = useRef(renderPageImage);
143
- renderPageImageRef.current = renderPageImage;
148
+ const pageHeight = windowWidth * pageAspect;
144
149
 
145
- // A stable function identity: FlatList warns if onViewableItemsChanged changes
146
- // identity across renders, so the current values it needs are read from refs
147
- // kept up to date every render instead of being closed over here.
148
- const onViewableItemsChanged = useRef(({
149
- viewableItems
150
- }) => {
151
- const first = viewableItems[0]?.item;
152
- if (first) {
153
- onPageChangedRef.current?.(first.index + 1, pageCountRef.current);
154
- viewableItems.forEach(v => void renderPageImageRef.current(v.item.index));
150
+ // Mount and rasterize the current page plus a small window around it;
151
+ // everything else stays an empty placeholder slot until scrolled near.
152
+ useEffect(() => {
153
+ if (pageCount === 0) {
154
+ return;
155
155
  }
156
- });
157
- const renderItem = useCallback(({
158
- item
159
- }) => {
160
- const uri = renderedPages[item.index];
161
- const pageWidth = windowWidth;
162
- const pageHeight = pageWidth * pageAspect;
163
- return /*#__PURE__*/_jsx(View, {
164
- style: [styles.page, {
165
- width: pageWidth,
166
- height: pageHeight
167
- }],
168
- children: uri ? /*#__PURE__*/_jsx(Image, {
169
- source: {
170
- uri
171
- },
172
- style: styles.pageImage,
173
- resizeMode: "contain",
174
- accessible: false
175
- }) : /*#__PURE__*/_jsx(View, {
176
- style: styles.pagePlaceholder,
177
- children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
178
- size: "lg"
179
- })
180
- })
156
+ const start = Math.max(0, currentPage - RENDER_WINDOW);
157
+ const end = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
158
+ for (let index = start; index <= end; index += 1) {
159
+ void renderPageImage(index);
160
+ }
161
+ }, [currentPage, pageCount, renderPageImage]);
162
+ const handleScroll = useCallback(event => {
163
+ if (pageHeight <= 0) {
164
+ return;
165
+ }
166
+ const page = Math.min(pageCount - 1, Math.max(0, Math.round(event.nativeEvent.contentOffset.y / pageHeight)));
167
+ setCurrentPage(prev => {
168
+ if (prev !== page) {
169
+ onPageChangedRef.current?.(page + 1, pageCount);
170
+ }
171
+ return page;
181
172
  });
182
- }, [loadingIndicator, pageAspect, renderedPages, windowWidth]);
173
+ }, [pageCount, pageHeight]);
174
+ const pages = useMemo(() => Array.from({
175
+ length: pageCount
176
+ }, (_unused, i) => i), [pageCount]);
177
+ const renderWindowStart = Math.max(0, currentPage - RENDER_WINDOW);
178
+ const renderWindowEnd = Math.min(pageCount - 1, currentPage + RENDER_WINDOW);
183
179
  return /*#__PURE__*/_jsx(View, {
184
180
  style: [styles.root, style],
185
181
  testID: testID,
@@ -191,16 +187,34 @@ export const DocumentViewer = ({
191
187
  children: loadingIndicator ? loadingIndicator() : /*#__PURE__*/_jsx(Spinner, {
192
188
  size: "lg"
193
189
  })
194
- }) : /*#__PURE__*/_jsx(FlatList, {
195
- data: data,
196
- keyExtractor: item => String(item.index),
197
- renderItem: renderItem,
198
- onViewableItemsChanged: onViewableItemsChanged.current,
199
- viewabilityConfig: {
200
- itemVisiblePercentThreshold: 50
201
- },
202
- initialNumToRender: 2,
203
- windowSize: 3
190
+ }) : /*#__PURE__*/_jsx(ScrollView, {
191
+ onScroll: handleScroll,
192
+ scrollEventThrottle: 32,
193
+ showsVerticalScrollIndicator: true,
194
+ removeClippedSubviews: true,
195
+ children: pages.map(index => {
196
+ const withinWindow = index >= renderWindowStart && index <= renderWindowEnd;
197
+ const uri = withinWindow ? renderedPages[index] : undefined;
198
+ return /*#__PURE__*/_jsx(View, {
199
+ style: [styles.page, {
200
+ width: windowWidth,
201
+ height: pageHeight
202
+ }],
203
+ children: uri ? /*#__PURE__*/_jsx(Image, {
204
+ source: {
205
+ uri
206
+ },
207
+ style: styles.pageImage,
208
+ resizeMode: "contain",
209
+ accessible: false
210
+ }) : /*#__PURE__*/_jsx(View, {
211
+ style: styles.pagePlaceholder,
212
+ children: withinWindow ? loadingIndicator?.() ?? /*#__PURE__*/_jsx(Spinner, {
213
+ size: "lg"
214
+ }) : null
215
+ })
216
+ }, index);
217
+ })
204
218
  })
205
219
  });
206
220
  };
@@ -19,9 +19,16 @@ export interface DocumentViewerProps {
19
19
  }
20
20
  /**
21
21
  * Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
22
- * Android) — no third-party PDF dependency. The WebView fallback consumer
23
- * apps use for non-PDF office documents is a distinct rendering strategy and
24
- * stays app-side.
22
+ * Android) — no third-party PDF dependency. Pages are paged through a plain
23
+ * `ScrollView` with manual windowed mounting (render the visible page ±
24
+ * `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
25
+ * would trip React Native's "VirtualizedLists should never be nested inside
26
+ * plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
27
+ * scrolling screen, which is a completely ordinary way to use it. Avoiding the
28
+ * VirtualizedList primitive entirely removes that whole class of warning
29
+ * rather than working around one instance of it. The WebView fallback
30
+ * consumer apps use for non-PDF office documents is a distinct rendering
31
+ * strategy and stays app-side.
25
32
  */
26
33
  export declare const DocumentViewer: React.FC<DocumentViewerProps>;
27
34
  export default DocumentViewer;
@@ -19,9 +19,16 @@ export interface DocumentViewerProps {
19
19
  }
20
20
  /**
21
21
  * Renders a PDF via our own native rasterizer (PDFKit on iOS, PdfRenderer on
22
- * Android) — no third-party PDF dependency. The WebView fallback consumer
23
- * apps use for non-PDF office documents is a distinct rendering strategy and
24
- * stays app-side.
22
+ * Android) — no third-party PDF dependency. Pages are paged through a plain
23
+ * `ScrollView` with manual windowed mounting (render the visible page ±
24
+ * `RENDER_WINDOW`), not a `FlatList` — a `VirtualizedList`-based component
25
+ * would trip React Native's "VirtualizedLists should never be nested inside
26
+ * plain ScrollViews" the moment a consumer embeds DocumentViewer in their own
27
+ * scrolling screen, which is a completely ordinary way to use it. Avoiding the
28
+ * VirtualizedList primitive entirely removes that whole class of warning
29
+ * rather than working around one instance of it. The WebView fallback
30
+ * consumer apps use for non-PDF office documents is a distinct rendering
31
+ * strategy and stays app-side.
25
32
  */
26
33
  export declare const DocumentViewer: React.FC<DocumentViewerProps>;
27
34
  export default DocumentViewer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority-technologies/mobile-ui",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Beautiful, animated, accessible React Native components, theme and form engine for Webority projects.",
5
5
  "keywords": [
6
6
  "react-native",