@octane-xplat/pager 0.7.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/dist/native/Pager.tsrx.js +282 -0
- package/dist/native/cx.mobile.js +11 -0
- package/dist/native/escape-props.mobile.js +39 -0
- package/dist/native/index.js +2 -0
- package/dist/native/layout-child.js +38 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/Pager.web.tsrx.js +337 -0
- package/dist/web/index.js +2 -0
- package/dist/web/layout-child.web.js +47 -0
- package/dist/web/octane-client-build.json +8 -0
- package/dist/web/platformProps.web.js +9 -0
- package/package.json +58 -0
- package/src/Pager.macos.tsrx +7 -0
- package/src/Pager.tsrx +231 -0
- package/src/Pager.web.test.tsrx +83 -0
- package/src/Pager.web.tsrx +104 -0
- package/src/cx.mobile.ts +8 -0
- package/src/escape-props.mobile.ts +65 -0
- package/src/index.macos.ts +2 -0
- package/src/index.ts +2 -0
- package/src/index.web.ts +2 -0
- package/src/layout-child.ts +57 -0
- package/src/layout-child.web.ts +108 -0
- package/src/platformProps.web.ts +14 -0
- package/src/props.ts +84 -0
- package/types/index.d.ts +5 -0
- package/types/index.macos.d.ts +4 -0
- package/types/props.d.ts +65 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { applyWebProps } from "./platformProps.web.js";
|
|
2
|
+
import { layoutChildProps } from "./layout-child.web.js";
|
|
3
|
+
import { bag4, bagOf, child, clone, drainFrag, forBlock, hookSlots, ifBlock, markDangerouslySetInnerHTMLChildren, snapshotSpread, template, useEffect, useRef } from "octane";
|
|
4
|
+
import { bindSignalChild, bindSignalHostPropSources, enableSignalBindings, setAriaAttributeIfChanged } from "octane/internal/client";
|
|
5
|
+
//#region src/Pager.web.tsrx
|
|
6
|
+
enableSignalBindings(1, true);
|
|
7
|
+
var _t$0 = /* @__PURE__ */ template("<div class=\"vx-pager-page\" role=\"group\" aria-roledescription=\"slide\"></div>");
|
|
8
|
+
var _t$1 = /* @__PURE__ */ template("<div><!></div>");
|
|
9
|
+
var _t$2 = /* @__PURE__ */ template("<!>", 0, 1);
|
|
10
|
+
var _hs$ = /* @__PURE__ */ hookSlots(8);
|
|
11
|
+
var _key$1 = (item, i) => i;
|
|
12
|
+
function __item$2(item, __s, __extra) {
|
|
13
|
+
const __block = __s.block;
|
|
14
|
+
const props = __extra[0];
|
|
15
|
+
const i = __block.itemIndex;
|
|
16
|
+
let _b = __s.slots[0];
|
|
17
|
+
if (_b === void 0) {
|
|
18
|
+
let _m1;
|
|
19
|
+
const _root = clone(_t$0);
|
|
20
|
+
_m1 = _root;
|
|
21
|
+
_b = bag4(__s, _root, void 0, _m1, null, void 0);
|
|
22
|
+
}
|
|
23
|
+
_b.a = setAriaAttributeIfChanged(`${i + 1} of ${props.items.length}`, _b.a, _b.b, "aria-label");
|
|
24
|
+
_b.e = bindSignalChild(__s, _b.e, 1, _b.b, props.renderItem(item, i), "b:a193c343", null, void 0, void 0, void 0, true);
|
|
25
|
+
}
|
|
26
|
+
var _rm$5 = [
|
|
27
|
+
"s",
|
|
28
|
+
"p",
|
|
29
|
+
"o"
|
|
30
|
+
];
|
|
31
|
+
function __then$0(__props, __s, __extra) {
|
|
32
|
+
const host = __extra[0];
|
|
33
|
+
const props = __extra[1];
|
|
34
|
+
let _b = __s.slots[0];
|
|
35
|
+
if (_b === void 0) {
|
|
36
|
+
let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14, _m15, _m16;
|
|
37
|
+
const _root = clone(_t$1);
|
|
38
|
+
_m0 = (el) => {
|
|
39
|
+
host.current = el;
|
|
40
|
+
applyWebProps(el, props.web);
|
|
41
|
+
};
|
|
42
|
+
_m1 = props.id;
|
|
43
|
+
_m2 = ["vx-pager", props.className];
|
|
44
|
+
_m3 = snapshotSpread(layoutChildProps(props));
|
|
45
|
+
_m4 = props.accessibilityRole ?? "group";
|
|
46
|
+
_m5 = "carousel";
|
|
47
|
+
_m6 = props.accessibilityLabel;
|
|
48
|
+
_m7 = props.accessible === false ? true : void 0;
|
|
49
|
+
_m8 = props.accessibilityHint;
|
|
50
|
+
_m9 = props.accessibilityValue;
|
|
51
|
+
_m10 = props.accessibilityState?.disabled;
|
|
52
|
+
_m11 = props.accessibilityState?.selected;
|
|
53
|
+
_m12 = props.accessibilityState?.checked;
|
|
54
|
+
_m13 = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
|
|
55
|
+
_m14 = _root;
|
|
56
|
+
_m15 = bindSignalHostPropSources(__s, void 0, _root, [
|
|
57
|
+
[
|
|
58
|
+
false,
|
|
59
|
+
"ref",
|
|
60
|
+
_m0
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
false,
|
|
64
|
+
"id",
|
|
65
|
+
_m1
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
false,
|
|
69
|
+
"className",
|
|
70
|
+
_m2
|
|
71
|
+
],
|
|
72
|
+
[true, _m3],
|
|
73
|
+
[
|
|
74
|
+
false,
|
|
75
|
+
"role",
|
|
76
|
+
_m4
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
false,
|
|
80
|
+
"aria-roledescription",
|
|
81
|
+
_m5
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
false,
|
|
85
|
+
"aria-label",
|
|
86
|
+
_m6
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
false,
|
|
90
|
+
"aria-hidden",
|
|
91
|
+
_m7
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
false,
|
|
95
|
+
"aria-description",
|
|
96
|
+
_m8
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
false,
|
|
100
|
+
"aria-valuetext",
|
|
101
|
+
_m9
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
false,
|
|
105
|
+
"aria-disabled",
|
|
106
|
+
_m10
|
|
107
|
+
],
|
|
108
|
+
[
|
|
109
|
+
false,
|
|
110
|
+
"aria-selected",
|
|
111
|
+
_m11
|
|
112
|
+
],
|
|
113
|
+
[
|
|
114
|
+
false,
|
|
115
|
+
"aria-checked",
|
|
116
|
+
_m12
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
false,
|
|
120
|
+
"aria-live",
|
|
121
|
+
_m13
|
|
122
|
+
]
|
|
123
|
+
], "b:452923c1", true);
|
|
124
|
+
markDangerouslySetInnerHTMLChildren(_root);
|
|
125
|
+
_m16 = child(_root);
|
|
126
|
+
_b = bagOf(__s, _root, {
|
|
127
|
+
a: _m0,
|
|
128
|
+
b: _m1,
|
|
129
|
+
c: _m2,
|
|
130
|
+
d: _m3,
|
|
131
|
+
e: _m4,
|
|
132
|
+
f: _m5,
|
|
133
|
+
g: _m6,
|
|
134
|
+
h: _m7,
|
|
135
|
+
i: _m8,
|
|
136
|
+
j: _m9,
|
|
137
|
+
k: _m10,
|
|
138
|
+
l: _m11,
|
|
139
|
+
m: _m12,
|
|
140
|
+
n: _m13,
|
|
141
|
+
o: _m14,
|
|
142
|
+
p: _m15,
|
|
143
|
+
q: _m16
|
|
144
|
+
});
|
|
145
|
+
__s.refFields = _rm$5;
|
|
146
|
+
} else {
|
|
147
|
+
{
|
|
148
|
+
const _v = (el) => {
|
|
149
|
+
host.current = el;
|
|
150
|
+
applyWebProps(el, props.web);
|
|
151
|
+
};
|
|
152
|
+
if (_b.a !== _v) _b.a = _v;
|
|
153
|
+
}
|
|
154
|
+
{
|
|
155
|
+
const _v = props.id;
|
|
156
|
+
if (_b.b !== _v) _b.b = _v;
|
|
157
|
+
}
|
|
158
|
+
{
|
|
159
|
+
const _v = ["vx-pager", props.className];
|
|
160
|
+
if (_b.c !== _v) _b.c = _v;
|
|
161
|
+
}
|
|
162
|
+
_b.d = snapshotSpread(layoutChildProps(props));
|
|
163
|
+
{
|
|
164
|
+
const _v = props.accessibilityRole ?? "group";
|
|
165
|
+
if (_b.e !== _v) _b.e = _v;
|
|
166
|
+
}
|
|
167
|
+
{
|
|
168
|
+
const _v = "carousel";
|
|
169
|
+
if (_b.f !== _v) _b.f = _v;
|
|
170
|
+
}
|
|
171
|
+
{
|
|
172
|
+
const _v = props.accessibilityLabel;
|
|
173
|
+
if (_b.g !== _v) _b.g = _v;
|
|
174
|
+
}
|
|
175
|
+
{
|
|
176
|
+
const _v = props.accessible === false ? true : void 0;
|
|
177
|
+
if (_b.h !== _v) _b.h = _v;
|
|
178
|
+
}
|
|
179
|
+
{
|
|
180
|
+
const _v = props.accessibilityHint;
|
|
181
|
+
if (_b.i !== _v) _b.i = _v;
|
|
182
|
+
}
|
|
183
|
+
{
|
|
184
|
+
const _v = props.accessibilityValue;
|
|
185
|
+
if (_b.j !== _v) _b.j = _v;
|
|
186
|
+
}
|
|
187
|
+
{
|
|
188
|
+
const _v = props.accessibilityState?.disabled;
|
|
189
|
+
if (_b.k !== _v) _b.k = _v;
|
|
190
|
+
}
|
|
191
|
+
{
|
|
192
|
+
const _v = props.accessibilityState?.selected;
|
|
193
|
+
if (_b.l !== _v) _b.l = _v;
|
|
194
|
+
}
|
|
195
|
+
{
|
|
196
|
+
const _v = props.accessibilityState?.checked;
|
|
197
|
+
if (_b.m !== _v) _b.m = _v;
|
|
198
|
+
}
|
|
199
|
+
{
|
|
200
|
+
const _v = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
|
|
201
|
+
if (_b.n !== _v) _b.n = _v;
|
|
202
|
+
}
|
|
203
|
+
_b.p = bindSignalHostPropSources(__s, _b.p, _b.o, [
|
|
204
|
+
[
|
|
205
|
+
false,
|
|
206
|
+
"ref",
|
|
207
|
+
_b.a
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
false,
|
|
211
|
+
"id",
|
|
212
|
+
_b.b
|
|
213
|
+
],
|
|
214
|
+
[
|
|
215
|
+
false,
|
|
216
|
+
"className",
|
|
217
|
+
_b.c
|
|
218
|
+
],
|
|
219
|
+
[true, _b.d],
|
|
220
|
+
[
|
|
221
|
+
false,
|
|
222
|
+
"role",
|
|
223
|
+
_b.e
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
false,
|
|
227
|
+
"aria-roledescription",
|
|
228
|
+
_b.f
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
false,
|
|
232
|
+
"aria-label",
|
|
233
|
+
_b.g
|
|
234
|
+
],
|
|
235
|
+
[
|
|
236
|
+
false,
|
|
237
|
+
"aria-hidden",
|
|
238
|
+
_b.h
|
|
239
|
+
],
|
|
240
|
+
[
|
|
241
|
+
false,
|
|
242
|
+
"aria-description",
|
|
243
|
+
_b.i
|
|
244
|
+
],
|
|
245
|
+
[
|
|
246
|
+
false,
|
|
247
|
+
"aria-valuetext",
|
|
248
|
+
_b.j
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
false,
|
|
252
|
+
"aria-disabled",
|
|
253
|
+
_b.k
|
|
254
|
+
],
|
|
255
|
+
[
|
|
256
|
+
false,
|
|
257
|
+
"aria-selected",
|
|
258
|
+
_b.l
|
|
259
|
+
],
|
|
260
|
+
[
|
|
261
|
+
false,
|
|
262
|
+
"aria-checked",
|
|
263
|
+
_b.m
|
|
264
|
+
],
|
|
265
|
+
[
|
|
266
|
+
false,
|
|
267
|
+
"aria-live",
|
|
268
|
+
_b.n
|
|
269
|
+
]
|
|
270
|
+
], "b:452923c1", true);
|
|
271
|
+
}
|
|
272
|
+
forBlock(__s, 1, _b.o, props.items, _key$1, __item$2, 50, [props], null, _b.q, true, "f3hircj");
|
|
273
|
+
}
|
|
274
|
+
function __else$6(__props, __s, __extra) {
|
|
275
|
+
const __block = __s.block;
|
|
276
|
+
const props = __extra[1];
|
|
277
|
+
let _b = __s.slots[0];
|
|
278
|
+
if (_b === void 0) {
|
|
279
|
+
let _m0, _m1;
|
|
280
|
+
const _root = clone(_t$2);
|
|
281
|
+
_m0 = __block.parentNode;
|
|
282
|
+
_m1 = child(_root);
|
|
283
|
+
drainFrag(_root, __block.parentNode, __block.endMarker);
|
|
284
|
+
_b = bag4(__s, null, _m0, _m1, null, void 0);
|
|
285
|
+
}
|
|
286
|
+
_b.e = bindSignalChild(__s, _b.e, 1, _b.a, props.renderEmpty?.(), "b:8f0f2972", _b.b, true, void 0, true, void 0);
|
|
287
|
+
}
|
|
288
|
+
var Pager = function Pager(props, __s, __extra) {
|
|
289
|
+
const __block = __s.block;
|
|
290
|
+
const host = useRef(null, _hs$ + 0);
|
|
291
|
+
const latest = useRef(props, _hs$ + 1);
|
|
292
|
+
const current = useRef(props.page ?? props.defaultPage ?? 0, _hs$ + 2);
|
|
293
|
+
const settle = useRef(null, _hs$ + 3);
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
latest.current = props;
|
|
296
|
+
}, [props], _hs$ + 4);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
const el = host.current;
|
|
299
|
+
if (!el) return;
|
|
300
|
+
const onScroll = () => {
|
|
301
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
302
|
+
settle.current = setTimeout(() => {
|
|
303
|
+
settle.current = null;
|
|
304
|
+
const el2 = host.current;
|
|
305
|
+
if (!el2 || el2.clientWidth === 0) return;
|
|
306
|
+
const max = Math.max(latest.current.items.length - 1, 0);
|
|
307
|
+
const next = Math.max(0, Math.min(max, Math.round(el2.scrollLeft / el2.clientWidth)));
|
|
308
|
+
if (next !== current.current) {
|
|
309
|
+
current.current = next;
|
|
310
|
+
latest.current.onPageChange?.(next);
|
|
311
|
+
}
|
|
312
|
+
}, 90);
|
|
313
|
+
};
|
|
314
|
+
el.addEventListener("scroll", onScroll, { passive: true });
|
|
315
|
+
return () => {
|
|
316
|
+
el.removeEventListener("scroll", onScroll);
|
|
317
|
+
if (settle.current !== null) clearTimeout(settle.current);
|
|
318
|
+
};
|
|
319
|
+
}, [], _hs$ + 5);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
const el = host.current;
|
|
322
|
+
if (!el || current.current <= 0) return;
|
|
323
|
+
el.scrollLeft = current.current * el.clientWidth;
|
|
324
|
+
}, [], _hs$ + 6);
|
|
325
|
+
useEffect(() => {
|
|
326
|
+
const el = host.current;
|
|
327
|
+
if (!el || props.page === void 0 || props.page === current.current) return;
|
|
328
|
+
current.current = props.page;
|
|
329
|
+
el.scrollTo({
|
|
330
|
+
left: props.page * el.clientWidth,
|
|
331
|
+
behavior: "smooth"
|
|
332
|
+
});
|
|
333
|
+
}, [props.page], _hs$ + 7);
|
|
334
|
+
ifBlock(__s, 0, __block.parentNode, props.items.length > 0, __then$0, __else$6, __block.endMarker, [host, props]);
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
337
|
+
export { Pager };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/layout-child.web.ts
|
|
2
|
+
/** The shared contract is "numbers are dips" (RN semantics), but octane's DOM
|
|
3
|
+
* renderer keeps React's unitless list — lineHeight is unitless there, so
|
|
4
|
+
* `style={{lineHeight: 40}}` would render a 40× multiplier instead of 40px.
|
|
5
|
+
* lineHeight is the one unitless-in-React prop that is a length in the
|
|
6
|
+
* shared vocabulary; force the px suffix. */
|
|
7
|
+
function normStyle(style) {
|
|
8
|
+
if (style && typeof style.lineHeight === "number") return {
|
|
9
|
+
...style,
|
|
10
|
+
lineHeight: style.lineHeight + "px"
|
|
11
|
+
};
|
|
12
|
+
return style;
|
|
13
|
+
}
|
|
14
|
+
var FLEX_JUSTIFY = {
|
|
15
|
+
start: "flex-start",
|
|
16
|
+
end: "flex-end"
|
|
17
|
+
};
|
|
18
|
+
var FLEX_ALIGN = {
|
|
19
|
+
start: "flex-start",
|
|
20
|
+
end: "flex-end"
|
|
21
|
+
};
|
|
22
|
+
/** Convert parent-layout metadata to CSS on the web leaf. Native's `dock`
|
|
23
|
+
* attribute has no CSS equivalent and is ignored on web. Grid indices are
|
|
24
|
+
* zero-based in the shared API, matching NativeScript. */
|
|
25
|
+
function layoutChildProps(props, baseStyle = props.style) {
|
|
26
|
+
const style = { ...normStyle(baseStyle) };
|
|
27
|
+
if (props.row !== void 0) style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`;
|
|
28
|
+
else if (props.rowSpan !== void 0) style.gridRow = `span ${props.rowSpan}`;
|
|
29
|
+
if (props.col !== void 0) style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`;
|
|
30
|
+
else if (props.colSpan !== void 0) style.gridColumn = `span ${props.colSpan}`;
|
|
31
|
+
if (props.left !== void 0) style.left = props.left;
|
|
32
|
+
if (props.top !== void 0) style.top = props.top;
|
|
33
|
+
if (props.flexGrow !== void 0) style.flexGrow = props.flexGrow;
|
|
34
|
+
if (props.flexShrink !== void 0) style.flexShrink = props.flexShrink;
|
|
35
|
+
if (props.alignSelf !== void 0) style.alignSelf = props.alignSelf;
|
|
36
|
+
if (props.order !== void 0) style.order = props.order;
|
|
37
|
+
if (props.justifyContent !== void 0) style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent;
|
|
38
|
+
if (props.alignItems !== void 0) style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems;
|
|
39
|
+
if (props.flexWrap !== void 0) style.flexWrap = props.flexWrap === true ? "wrap" : props.flexWrap === false ? "nowrap" : props.flexWrap;
|
|
40
|
+
if (props.gap !== void 0) style.gap = props.gap;
|
|
41
|
+
if (props.rowGap !== void 0) style.rowGap = props.rowGap;
|
|
42
|
+
if (props.columnGap !== void 0) style.columnGap = props.columnGap;
|
|
43
|
+
if (typeof style.lineHeight === "number") style.lineHeight = style.lineHeight + "px";
|
|
44
|
+
return { style };
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { layoutChildProps, normStyle };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/platformProps.web.ts
|
|
2
|
+
/** Apply a web escape-hatch bag after the primitive's standard props. */
|
|
3
|
+
function applyWebProps(el, props) {
|
|
4
|
+
if (!el || !props) return;
|
|
5
|
+
for (const [name, value] of Object.entries(props)) if (name.includes("-")) el.setAttribute(name, String(value));
|
|
6
|
+
else el[name] = value;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { applyWebProps };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octane-xplat/pager",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Paged swipe container leaf for Octane xplat — web scroll-snap row + @nativescript-community/ui-pager (ViewPager2 / paging UICollectionView)",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/octane-xplat/octane-xplat.git",
|
|
8
|
+
"directory": "packages/pager"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"types",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"macos": {
|
|
19
|
+
"types": "./types/index.macos.d.ts",
|
|
20
|
+
"default": "./src/index.macos.ts"
|
|
21
|
+
},
|
|
22
|
+
"types": "./types/index.d.ts",
|
|
23
|
+
"web": "./dist/web/index.js",
|
|
24
|
+
"native": "./dist/native/index.js",
|
|
25
|
+
"default": "./dist/native/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@nativescript-community/ui-pager": "14.1.42"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@nativescript-community/octane": "0.2.1",
|
|
36
|
+
"@nativescript/core": "9.1.2",
|
|
37
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
38
|
+
"octane": "0.6.3",
|
|
39
|
+
"typescript": "5.9.3",
|
|
40
|
+
"vite": "8.3.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
44
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
45
|
+
"octane": ">=0.6.3 <1"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@nativescript-community/octane": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"@nativescript/core": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @jsxImportSource @xplat/macos/renderer */
|
|
2
|
+
import type { PagerProps } from './props'
|
|
3
|
+
|
|
4
|
+
/** Explicit placeholder until the AppKit host has a paging surface. */
|
|
5
|
+
export function Pager(_props: PagerProps) @{
|
|
6
|
+
<label className="vx-unsupported">Pager is unsupported by the AppKit host</label>
|
|
7
|
+
}
|
package/src/Pager.tsrx
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { memo, useCallback, useEffect, useRef } from 'octane';
|
|
3
|
+
import {
|
|
4
|
+
NATIVESCRIPT_RENDERER_ID,
|
|
5
|
+
createNativeScriptRoot,
|
|
6
|
+
defineUniversalComponent,
|
|
7
|
+
registerElement,
|
|
8
|
+
} from '@nativescript-community/octane';
|
|
9
|
+
|
|
10
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics';
|
|
11
|
+
import type { UniversalRoot } from '@nativescript-community/octane';
|
|
12
|
+
import { Pager as NSPager } from '@nativescript-community/ui-pager';
|
|
13
|
+
import { GridLayout } from '@nativescript/core';
|
|
14
|
+
import type { PagerProps } from './props';
|
|
15
|
+
import { cx } from './cx.mobile';
|
|
16
|
+
import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props.mobile';
|
|
17
|
+
import { layoutChildProps } from './layout-child';
|
|
18
|
+
|
|
19
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
20
|
+
interface NativeScriptElements {
|
|
21
|
+
pager: Attributes<typeof NSPager>;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// @nativescript-community/ui-pager — ViewPager2 on Android, a paging
|
|
26
|
+
// UICollectionView on iOS. It is a required peer: this leaf sits in the
|
|
27
|
+
// shared native barrel, so every native app must declare the plugin for
|
|
28
|
+
// the /ns/m dev bridge to resolve it under the app root.
|
|
29
|
+
registerElement('pager', NSPager);
|
|
30
|
+
|
|
31
|
+
/** One recycled page: the host view the plugin's adapter asks for plus the
|
|
32
|
+
* Octane root mounted into it. Mirrors the driver's ListView cell seam —
|
|
33
|
+
* ui-pager fires the same `itemLoading`/`itemDisposing` pair. */
|
|
34
|
+
interface PagerCell {
|
|
35
|
+
host: any;
|
|
36
|
+
root: UniversalRoot;
|
|
37
|
+
pager: any;
|
|
38
|
+
renderItem: any;
|
|
39
|
+
item: any;
|
|
40
|
+
index: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cellsByHost = new WeakMap<object, PagerCell>();
|
|
44
|
+
const cellsByPager = new WeakMap<object, Set<PagerCell>>();
|
|
45
|
+
|
|
46
|
+
/** Every page root renders this one component, so rebinds are diffs of the
|
|
47
|
+
* cell's tree rather than remounts. The renderItem output wraps in a
|
|
48
|
+
* gridlayout — page content must stay single-root. */
|
|
49
|
+
const PagerCell = defineUniversalComponent(NATIVESCRIPT_RENDERER_ID, (props: any) =>
|
|
50
|
+
props.renderItem(props.item, props.index),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function cellFor(host: any): PagerCell {
|
|
54
|
+
let cell = cellsByHost.get(host);
|
|
55
|
+
if (!cell) {
|
|
56
|
+
cell = { host, root: createNativeScriptRoot(host), pager: null, renderItem: null, item: undefined, index: -1 };
|
|
57
|
+
cellsByHost.set(host, cell);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cell;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `createView` runs inside the plugin's adapter — no pager instance in
|
|
64
|
+
* scope, so the cell is only linked to its pager at `itemLoading` (where
|
|
65
|
+
* `args.object` is the owner). */
|
|
66
|
+
function createPageHost() {
|
|
67
|
+
const host = new GridLayout();
|
|
68
|
+
cellFor(host);
|
|
69
|
+
return host;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }];
|
|
73
|
+
// The built-in 'default' template stays first in the plugin's internal
|
|
74
|
+
// list, so the selector must name our key — a 'default' entry would match
|
|
75
|
+
// the built-in (an XML string parse, which yields nothing here).
|
|
76
|
+
const selectPageTemplate = () => 'xplat-page';
|
|
77
|
+
|
|
78
|
+
function releasePagerCells(pager: any) {
|
|
79
|
+
const cells = cellsByPager.get(pager);
|
|
80
|
+
if (!cells) return;
|
|
81
|
+
for (const cell of cells) {
|
|
82
|
+
cellsByHost.delete(cell.host);
|
|
83
|
+
cell.root.unmount();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
cellsByPager.delete(pager);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function PagerInner(props: PagerProps) @{
|
|
90
|
+
const host = useRef<any>(null);
|
|
91
|
+
// selectedIndex writes the leaf makes itself — the plugin raises
|
|
92
|
+
// 'selectedIndexChange' for script writes exactly as for swipes, and
|
|
93
|
+
// the driver's auto-mute only covers JSX prop writes, not ours.
|
|
94
|
+
const suppressEcho = useRef(false);
|
|
95
|
+
const pageSeeded = useRef(false);
|
|
96
|
+
|
|
97
|
+
// Stable ref identity — an inline closure re-fires ref(null)/ref(v) per
|
|
98
|
+
// render, which would bounce host.current around the effects below.
|
|
99
|
+
const bindHost = useCallback((v: any) => {
|
|
100
|
+
host.current = v;
|
|
101
|
+
}, []);
|
|
102
|
+
|
|
103
|
+
const bindCell = (args: any) => {
|
|
104
|
+
const cell = cellFor(args.view);
|
|
105
|
+
cell.pager = args.object;
|
|
106
|
+
let owned = cellsByPager.get(cell.pager);
|
|
107
|
+
if (!owned) {
|
|
108
|
+
owned = new Set();
|
|
109
|
+
cellsByPager.set(cell.pager, owned);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
owned.add(cell);
|
|
113
|
+
const item = args.bindingContext ?? props.items?.[args.index];
|
|
114
|
+
if (cell.renderItem === props.renderItem && cell.index === args.index && Object.is(cell.item, item)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
cell.renderItem = props.renderItem;
|
|
119
|
+
cell.item = item;
|
|
120
|
+
cell.index = args.index;
|
|
121
|
+
cell.root.render(PagerCell, {
|
|
122
|
+
renderItem: (it: any, i: number) => <gridlayout>{props.renderItem(it, i)}</gridlayout>,
|
|
123
|
+
item,
|
|
124
|
+
index: args.index,
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const disposeCell = (args: any) => {
|
|
129
|
+
const cell = args.view && cellsByHost.get(args.view);
|
|
130
|
+
if (cell) {
|
|
131
|
+
cellsByHost.delete(args.view);
|
|
132
|
+
if (cell.pager) cellsByPager.get(cell.pager)?.delete(cell);
|
|
133
|
+
cell.root.unmount();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// One-time wiring (mount): the template set + selector are imperative —
|
|
138
|
+
// as JSX props they would be re-applied on every render. The cleanup
|
|
139
|
+
// captures the pager so cell roots release after the ref detaches.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const pager = host.current;
|
|
142
|
+
if (!pager) return;
|
|
143
|
+
pager.itemTemplates = PAGER_TEMPLATES;
|
|
144
|
+
pager.itemTemplateSelector = selectPageTemplate;
|
|
145
|
+
return () => releasePagerCells(pager);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
// Items and the index are imperative writes, not JSX props: `items` must
|
|
149
|
+
// land before `selectedIndex` (the plugin coerces the index against the
|
|
150
|
+
// page count — a write ahead of items clamps to -1), and a re-assert
|
|
151
|
+
// after an items change repairs the plugin's own re-coercion.
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
const pager = host.current;
|
|
154
|
+
if (!pager) return;
|
|
155
|
+
applyEscapeProps(pager, props);
|
|
156
|
+
if (pager.items !== props.items) pager.items = props.items;
|
|
157
|
+
const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage);
|
|
158
|
+
if (target !== undefined && pager.selectedIndex !== target) {
|
|
159
|
+
suppressEcho.current = true;
|
|
160
|
+
try {
|
|
161
|
+
pager.selectedIndex = target;
|
|
162
|
+
} finally {
|
|
163
|
+
suppressEcho.current = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pageSeeded.current = true;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
<pager
|
|
171
|
+
ref={bindHost}
|
|
172
|
+
id={props.id}
|
|
173
|
+
{...layoutChildProps(props)}
|
|
174
|
+
className={cx('vx-pager', props.className)}
|
|
175
|
+
style={props.style}
|
|
176
|
+
onItemLoading={bindCell}
|
|
177
|
+
onItemDisposing={disposeCell}
|
|
178
|
+
onSelectedIndexChange={(args: any) => {
|
|
179
|
+
if (!suppressEcho.current) props.onPageChange?.(args.value);
|
|
180
|
+
}}
|
|
181
|
+
accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
|
|
182
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
183
|
+
accessibilityHint={props.accessibilityHint}
|
|
184
|
+
accessibilityValue={props.accessibilityValue}
|
|
185
|
+
accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
|
|
186
|
+
accessible={props.accessible}
|
|
187
|
+
accessibilityLiveRegion={props.accessibilityLiveRegion as any}
|
|
188
|
+
/>
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// The pager subtree is memoized on `items` and configuration: the universal
|
|
192
|
+
// driver re-applies props on every render (no value diffing) and a stray
|
|
193
|
+
// `items` write would refresh the whole adapter. Constraint matches
|
|
194
|
+
// UITableView: `renderItem` should be identity-stable — a fresh closure per
|
|
195
|
+
// render is deliberately not part of the compare. (`PagerInner` must precede
|
|
196
|
+
// this — compiled components are consts, not hoisted functions.)
|
|
197
|
+
const MemoPager = memo(PagerInner, (a, b) =>
|
|
198
|
+
a.items === b.items &&
|
|
199
|
+
a.className === b.className &&
|
|
200
|
+
a.style === b.style &&
|
|
201
|
+
a.id === b.id &&
|
|
202
|
+
a.page === b.page &&
|
|
203
|
+
a.onPageChange === b.onPageChange &&
|
|
204
|
+
a.ios === b.ios &&
|
|
205
|
+
a.android === b.android
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
/** Pager — paged horizontal swiping via @nativescript-community/ui-pager
|
|
209
|
+
* (ViewPager2 / paging UICollectionView), chrome-reset: one full-size cell
|
|
210
|
+
* per item, no indicator. Pages recycle through `itemTemplates` +
|
|
211
|
+
* `itemLoading`, each slot hosting its own Octane root — the same cell
|
|
212
|
+
* machinery the driver builds for `listview`, hand-wired here. */
|
|
213
|
+
export function Pager(props: PagerProps) @{
|
|
214
|
+
@if (props.items.length > 0) {
|
|
215
|
+
<MemoPager
|
|
216
|
+
id={props.id}
|
|
217
|
+
className={props.className}
|
|
218
|
+
style={props.style}
|
|
219
|
+
items={props.items}
|
|
220
|
+
renderItem={props.renderItem}
|
|
221
|
+
page={props.page}
|
|
222
|
+
defaultPage={props.defaultPage}
|
|
223
|
+
onPageChange={props.onPageChange}
|
|
224
|
+
ios={props.ios}
|
|
225
|
+
android={props.android}
|
|
226
|
+
{...layoutChildProps(props)}
|
|
227
|
+
/>
|
|
228
|
+
} @else {
|
|
229
|
+
<>{props.renderEmpty?.()}</>
|
|
230
|
+
}
|
|
231
|
+
}
|