@teleporthq/teleport-plugin-next-data-source 0.43.59 → 0.43.60

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.
Files changed (127) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +230 -0
  2. package/__tests__/ecommerce-product-discounts.test.ts +196 -0
  3. package/__tests__/infinite-scroll.test.ts +143 -0
  4. package/__tests__/numbered-pagination.test.ts +113 -0
  5. package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
  6. package/__tests__/page-url-sync.test.ts +208 -0
  7. package/__tests__/product-price-sort.ts +313 -0
  8. package/__tests__/search-url-sync.test.ts +7 -3
  9. package/dist/cjs/array-mapper-pagination.d.ts +9 -0
  10. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
  11. package/dist/cjs/array-mapper-pagination.js.map +1 -1
  12. package/dist/cjs/count-effect.d.ts +42 -0
  13. package/dist/cjs/count-effect.d.ts.map +1 -0
  14. package/dist/cjs/count-effect.js +81 -0
  15. package/dist/cjs/count-effect.js.map +1 -0
  16. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  17. package/dist/cjs/fetchers/postgresql.js +2 -1
  18. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  19. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/teleport.js +3 -2
  21. package/dist/cjs/fetchers/teleport.js.map +1 -1
  22. package/dist/cjs/infinite-scroll.d.ts +65 -0
  23. package/dist/cjs/infinite-scroll.d.ts.map +1 -0
  24. package/dist/cjs/infinite-scroll.js +190 -0
  25. package/dist/cjs/infinite-scroll.js.map +1 -0
  26. package/dist/cjs/numbered-pagination.d.ts +42 -0
  27. package/dist/cjs/numbered-pagination.d.ts.map +1 -0
  28. package/dist/cjs/numbered-pagination.js +167 -0
  29. package/dist/cjs/numbered-pagination.js.map +1 -0
  30. package/dist/cjs/page-reset.d.ts +42 -0
  31. package/dist/cjs/page-reset.d.ts.map +1 -0
  32. package/dist/cjs/page-reset.js +116 -0
  33. package/dist/cjs/page-reset.js.map +1 -0
  34. package/dist/cjs/page-url-sync.d.ts +42 -0
  35. package/dist/cjs/page-url-sync.d.ts.map +1 -0
  36. package/dist/cjs/page-url-sync.js +180 -0
  37. package/dist/cjs/page-url-sync.js.map +1 -0
  38. package/dist/cjs/pagination-controls.d.ts +55 -0
  39. package/dist/cjs/pagination-controls.d.ts.map +1 -0
  40. package/dist/cjs/pagination-controls.js +159 -0
  41. package/dist/cjs/pagination-controls.js.map +1 -0
  42. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  43. package/dist/cjs/pagination-plugin.js +506 -160
  44. package/dist/cjs/pagination-plugin.js.map +1 -1
  45. package/dist/cjs/product-price-sort.d.ts +116 -0
  46. package/dist/cjs/product-price-sort.d.ts.map +1 -0
  47. package/dist/cjs/product-price-sort.js +169 -0
  48. package/dist/cjs/product-price-sort.js.map +1 -0
  49. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  50. package/dist/cjs/transformations/ecommerce-product.js +1 -1
  51. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  52. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  53. package/dist/esm/array-mapper-pagination.d.ts +9 -0
  54. package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
  55. package/dist/esm/array-mapper-pagination.js.map +1 -1
  56. package/dist/esm/count-effect.d.ts +42 -0
  57. package/dist/esm/count-effect.d.ts.map +1 -0
  58. package/dist/esm/count-effect.js +53 -0
  59. package/dist/esm/count-effect.js.map +1 -0
  60. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  61. package/dist/esm/fetchers/postgresql.js +2 -1
  62. package/dist/esm/fetchers/postgresql.js.map +1 -1
  63. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  64. package/dist/esm/fetchers/teleport.js +3 -2
  65. package/dist/esm/fetchers/teleport.js.map +1 -1
  66. package/dist/esm/infinite-scroll.d.ts +65 -0
  67. package/dist/esm/infinite-scroll.d.ts.map +1 -0
  68. package/dist/esm/infinite-scroll.js +159 -0
  69. package/dist/esm/infinite-scroll.js.map +1 -0
  70. package/dist/esm/numbered-pagination.d.ts +42 -0
  71. package/dist/esm/numbered-pagination.d.ts.map +1 -0
  72. package/dist/esm/numbered-pagination.js +138 -0
  73. package/dist/esm/numbered-pagination.js.map +1 -0
  74. package/dist/esm/page-reset.d.ts +42 -0
  75. package/dist/esm/page-reset.d.ts.map +1 -0
  76. package/dist/esm/page-reset.js +87 -0
  77. package/dist/esm/page-reset.js.map +1 -0
  78. package/dist/esm/page-url-sync.d.ts +42 -0
  79. package/dist/esm/page-url-sync.d.ts.map +1 -0
  80. package/dist/esm/page-url-sync.js +152 -0
  81. package/dist/esm/page-url-sync.js.map +1 -0
  82. package/dist/esm/pagination-controls.d.ts +55 -0
  83. package/dist/esm/pagination-controls.d.ts.map +1 -0
  84. package/dist/esm/pagination-controls.js +126 -0
  85. package/dist/esm/pagination-controls.js.map +1 -0
  86. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  87. package/dist/esm/pagination-plugin.js +507 -161
  88. package/dist/esm/pagination-plugin.js.map +1 -1
  89. package/dist/esm/product-price-sort.d.ts +116 -0
  90. package/dist/esm/product-price-sort.d.ts.map +1 -0
  91. package/dist/esm/product-price-sort.js +160 -0
  92. package/dist/esm/product-price-sort.js.map +1 -0
  93. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  94. package/dist/esm/transformations/ecommerce-product.js +2 -2
  95. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  96. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +5 -5
  98. package/src/array-mapper-pagination.ts +10 -0
  99. package/src/count-effect.ts +137 -0
  100. package/src/fetchers/postgresql.ts +52 -13
  101. package/src/fetchers/teleport.ts +53 -13
  102. package/src/infinite-scroll.ts +401 -0
  103. package/src/numbered-pagination.ts +292 -0
  104. package/src/page-reset.ts +162 -0
  105. package/src/page-url-sync.ts +241 -0
  106. package/src/pagination-controls.ts +176 -0
  107. package/src/pagination-plugin.ts +755 -346
  108. package/src/product-price-sort.ts +204 -0
  109. package/src/transformations/ecommerce-product.ts +33 -7
  110. package/dist/cjs/array-mapper-registry.d.ts +0 -85
  111. package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
  112. package/dist/cjs/array-mapper-registry.js +0 -296
  113. package/dist/cjs/array-mapper-registry.js.map +0 -1
  114. package/dist/cjs/pagination-with-count.d.ts +0 -6
  115. package/dist/cjs/pagination-with-count.d.ts.map +0 -1
  116. package/dist/cjs/pagination-with-count.js +0 -63
  117. package/dist/cjs/pagination-with-count.js.map +0 -1
  118. package/dist/esm/array-mapper-registry.d.ts +0 -85
  119. package/dist/esm/array-mapper-registry.d.ts.map +0 -1
  120. package/dist/esm/array-mapper-registry.js +0 -292
  121. package/dist/esm/array-mapper-registry.js.map +0 -1
  122. package/dist/esm/pagination-with-count.d.ts +0 -6
  123. package/dist/esm/pagination-with-count.d.ts.map +0 -1
  124. package/dist/esm/pagination-with-count.js +0 -34
  125. package/dist/esm/pagination-with-count.js.map +0 -1
  126. package/src/array-mapper-registry.ts +0 -416
  127. package/src/pagination-with-count.ts +0 -89
@@ -0,0 +1,401 @@
1
+ import * as types from '@babel/types'
2
+
3
+ /**
4
+ * Infinite scroll: the list APPENDS pages instead of replacing them.
5
+ *
6
+ * The accumulation lives inside the generated `fetchData` closure rather than
7
+ * around the provider. `DataProvider` renders whatever `fetchData` resolves
8
+ * with, so returning "every page fetched so far, in order" is enough to make it
9
+ * append — no change to the runtime component library, and the existing
10
+ * `persistDataDuringLoading` keeps the rows already on screen while the next
11
+ * page is in flight.
12
+ *
13
+ * A `useRef` holds the pages rather than a state, because writing accumulated
14
+ * rows into state from inside `fetchData` would re-render the provider mid-fetch
15
+ * and re-enter it. The ref is keyed by a SIGNATURE of every fetch param except
16
+ * the page, so a filter, sort or search change starts a fresh list instead of
17
+ * interleaving rows from two different queries.
18
+ */
19
+
20
+ // tslint:disable:no-any
21
+
22
+ export interface InfiniteScrollVars {
23
+ accumRefVar: string
24
+ hasMoreFlagVar: string
25
+ setHasMoreFlagVar: string
26
+ hasMoreVar: string
27
+ sentinelRefVar: string
28
+ }
29
+
30
+ export const getInfiniteScrollVars = (index: number): InfiniteScrollVars => ({
31
+ accumRefVar: `ds_${index}_accumRef`,
32
+ hasMoreFlagVar: `ds_${index}_hasMoreFlag`,
33
+ setHasMoreFlagVar: `setDs_${index}_hasMoreFlag`,
34
+ hasMoreVar: `ds_${index}_hasMore`,
35
+ sentinelRefVar: `ds_${index}_sentinelRef`,
36
+ })
37
+
38
+ /**
39
+ * The three declarations behind an appending list:
40
+ *
41
+ * const ds_0_accumRef = useRef({ sig: '', pages: {} })
42
+ * const [ds_0_hasMoreFlag, setDs_0_hasMoreFlag] = useState(true)
43
+ * const ds_0_hasMore = ds_0_hasMoreFlag && (ds_0_maxPages === 0 || <page> < ds_0_maxPages)
44
+ *
45
+ * Two independent signals for "is there more", because either can be missing.
46
+ * `hasMoreFlag` comes from the last response being a SHORT page, which is the
47
+ * only end-of-data signal available from a data source that cannot count.
48
+ * `maxPages` comes from the count endpoint and is the one that stops a request
49
+ * being made at all; it is `0` while unknown, hence the explicit escape.
50
+ */
51
+ export const buildInfiniteScrollDeclarations = (
52
+ vars: InfiniteScrollVars,
53
+ maxPagesVar: string,
54
+ currentPageExpr: types.Expression,
55
+ includeSentinel: boolean
56
+ ): types.Statement[] => {
57
+ const declarations: types.Statement[] = [
58
+ types.variableDeclaration('const', [
59
+ types.variableDeclarator(
60
+ types.identifier(vars.accumRefVar),
61
+ types.callExpression(types.identifier('useRef'), [
62
+ types.objectExpression([
63
+ types.objectProperty(types.identifier('sig'), types.stringLiteral('')),
64
+ types.objectProperty(types.identifier('pages'), types.objectExpression([])),
65
+ ]),
66
+ ])
67
+ ),
68
+ ]),
69
+ types.variableDeclaration('const', [
70
+ types.variableDeclarator(
71
+ types.arrayPattern([
72
+ types.identifier(vars.hasMoreFlagVar),
73
+ types.identifier(vars.setHasMoreFlagVar),
74
+ ]),
75
+ types.callExpression(types.identifier('useState'), [types.booleanLiteral(true)])
76
+ ),
77
+ ]),
78
+ types.variableDeclaration('const', [
79
+ types.variableDeclarator(
80
+ types.identifier(vars.hasMoreVar),
81
+ types.logicalExpression(
82
+ '&&',
83
+ types.identifier(vars.hasMoreFlagVar),
84
+ types.logicalExpression(
85
+ '||',
86
+ types.binaryExpression('===', types.identifier(maxPagesVar), types.numericLiteral(0)),
87
+ types.binaryExpression(
88
+ '<',
89
+ types.cloneNode(currentPageExpr, true) as types.Expression,
90
+ types.identifier(maxPagesVar)
91
+ )
92
+ )
93
+ )
94
+ ),
95
+ ]),
96
+ ]
97
+
98
+ if (includeSentinel) {
99
+ declarations.push(
100
+ types.variableDeclaration('const', [
101
+ types.variableDeclarator(
102
+ types.identifier(vars.sentinelRefVar),
103
+ types.callExpression(types.identifier('useRef'), [types.nullLiteral()])
104
+ ),
105
+ ])
106
+ )
107
+ }
108
+
109
+ return declarations
110
+ }
111
+
112
+ /**
113
+ * The `.then(...)` tail that accumulates a page and reports whether more exist.
114
+ *
115
+ * Returned as the resolved value of `fetchData`, so `DataProvider` stores the
116
+ * whole accumulated list as its data.
117
+ */
118
+ export const buildAccumulatingResponseHandler = (
119
+ vars: InfiniteScrollVars,
120
+ perPage: number
121
+ ): types.ArrowFunctionExpression => {
122
+ const accumCurrent = (): types.MemberExpression =>
123
+ types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
124
+ const accumPages = (): types.MemberExpression =>
125
+ types.memberExpression(accumCurrent(), types.identifier('pages'))
126
+
127
+ return types.arrowFunctionExpression(
128
+ [types.identifier('response')],
129
+ types.blockStatement([
130
+ types.variableDeclaration('const', [
131
+ types.variableDeclarator(
132
+ types.identifier('rows'),
133
+ types.logicalExpression(
134
+ '||',
135
+ types.optionalMemberExpression(
136
+ types.identifier('response'),
137
+ types.identifier('data'),
138
+ false,
139
+ true
140
+ ),
141
+ types.arrayExpression([])
142
+ )
143
+ ),
144
+ ]),
145
+ types.variableDeclaration('const', [
146
+ types.variableDeclarator(
147
+ types.identifier('page'),
148
+ types.logicalExpression(
149
+ '||',
150
+ types.callExpression(types.identifier('Number'), [
151
+ types.memberExpression(types.identifier('params'), types.identifier('page')),
152
+ ]),
153
+ types.numericLiteral(1)
154
+ )
155
+ ),
156
+ ]),
157
+ // Everything that identifies the QUERY, with the page zeroed out: two
158
+ // fetches that differ only by page belong to the same accumulated list.
159
+ types.variableDeclaration('const', [
160
+ types.variableDeclarator(
161
+ types.identifier('sig'),
162
+ types.callExpression(
163
+ types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
164
+ [
165
+ types.objectExpression([
166
+ types.spreadElement(types.identifier('params')),
167
+ types.objectProperty(types.identifier('page'), types.numericLiteral(0)),
168
+ ]),
169
+ ]
170
+ )
171
+ ),
172
+ ]),
173
+ types.ifStatement(
174
+ types.binaryExpression(
175
+ '!==',
176
+ types.memberExpression(accumCurrent(), types.identifier('sig')),
177
+ types.identifier('sig')
178
+ ),
179
+ types.blockStatement([
180
+ types.expressionStatement(
181
+ types.assignmentExpression(
182
+ '=',
183
+ accumCurrent(),
184
+ types.objectExpression([
185
+ types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
186
+ types.objectProperty(types.identifier('pages'), types.objectExpression([])),
187
+ ])
188
+ )
189
+ ),
190
+ ])
191
+ ),
192
+ types.expressionStatement(
193
+ types.assignmentExpression(
194
+ '=',
195
+ types.memberExpression(accumPages(), types.identifier('page'), true),
196
+ types.identifier('rows')
197
+ )
198
+ ),
199
+ // A page that came back short is the last one. This is the only
200
+ // end-of-data signal a data source that cannot count ever gives.
201
+ types.expressionStatement(
202
+ types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
203
+ types.binaryExpression(
204
+ '>=',
205
+ types.memberExpression(types.identifier('rows'), types.identifier('length')),
206
+ types.numericLiteral(perPage)
207
+ ),
208
+ ])
209
+ ),
210
+ // Every page held so far, flattened in page order — a numeric sort, so
211
+ // page 10 does not land between 1 and 2. This is what DataProvider stores
212
+ // as its data, and so what the list renders.
213
+ types.returnStatement(
214
+ types.callExpression(
215
+ types.memberExpression(
216
+ types.callExpression(
217
+ types.memberExpression(
218
+ types.callExpression(
219
+ types.memberExpression(
220
+ types.callExpression(
221
+ types.memberExpression(types.identifier('Object'), types.identifier('keys')),
222
+ [accumPages()]
223
+ ),
224
+ types.identifier('map')
225
+ ),
226
+ [types.identifier('Number')]
227
+ ),
228
+ types.identifier('sort')
229
+ ),
230
+ [
231
+ types.arrowFunctionExpression(
232
+ [types.identifier('a'), types.identifier('b')],
233
+ types.binaryExpression('-', types.identifier('a'), types.identifier('b'))
234
+ ),
235
+ ]
236
+ ),
237
+ types.identifier('reduce')
238
+ ),
239
+ [
240
+ types.arrowFunctionExpression(
241
+ [types.identifier('acc'), types.identifier('p')],
242
+ types.callExpression(
243
+ types.memberExpression(types.identifier('acc'), types.identifier('concat')),
244
+ [types.memberExpression(accumPages(), types.identifier('p'), true)]
245
+ )
246
+ ),
247
+ types.arrayExpression([]),
248
+ ]
249
+ )
250
+ ),
251
+ ])
252
+ )
253
+ }
254
+
255
+ /**
256
+ * The IntersectionObserver that loads the next page when the sentinel scrolls
257
+ * into view. Emitted only in auto mode (no Load More button).
258
+ *
259
+ * `rootMargin` starts the fetch a screenful early so the list rarely shows a
260
+ * gap. The observer is rebuilt whenever `hasMore` or `maxPages` change and
261
+ * disconnected on cleanup; when there is nothing more to load the effect exits
262
+ * before observing, so a finished list costs nothing.
263
+ */
264
+ export const buildSentinelObserverEffect = (
265
+ vars: InfiniteScrollVars,
266
+ maxPagesVar: string,
267
+ buildAdvancePageStatement: () => types.Statement
268
+ ): types.ExpressionStatement =>
269
+ types.expressionStatement(
270
+ types.callExpression(types.identifier('useEffect'), [
271
+ types.arrowFunctionExpression(
272
+ [],
273
+ types.blockStatement([
274
+ types.variableDeclaration('const', [
275
+ types.variableDeclarator(
276
+ types.identifier('el'),
277
+ types.memberExpression(
278
+ types.identifier(vars.sentinelRefVar),
279
+ types.identifier('current')
280
+ )
281
+ ),
282
+ ]),
283
+ // No element yet, no observer support (older browsers, jsdom), or
284
+ // nothing left to load.
285
+ types.ifStatement(
286
+ types.logicalExpression(
287
+ '||',
288
+ types.logicalExpression(
289
+ '||',
290
+ types.unaryExpression('!', types.identifier('el'), true),
291
+ types.binaryExpression(
292
+ '===',
293
+ types.unaryExpression('typeof', types.identifier('IntersectionObserver')),
294
+ types.stringLiteral('undefined')
295
+ )
296
+ ),
297
+ types.unaryExpression('!', types.identifier(vars.hasMoreVar), true)
298
+ ),
299
+ types.returnStatement()
300
+ ),
301
+ types.variableDeclaration('const', [
302
+ types.variableDeclarator(
303
+ types.identifier('observer'),
304
+ types.newExpression(types.identifier('IntersectionObserver'), [
305
+ types.arrowFunctionExpression(
306
+ [types.identifier('entries')],
307
+ types.blockStatement([
308
+ types.expressionStatement(
309
+ types.callExpression(
310
+ types.memberExpression(
311
+ types.identifier('entries'),
312
+ types.identifier('forEach')
313
+ ),
314
+ [
315
+ types.arrowFunctionExpression(
316
+ [types.identifier('entry')],
317
+ types.blockStatement([
318
+ types.ifStatement(
319
+ types.unaryExpression(
320
+ '!',
321
+ types.memberExpression(
322
+ types.identifier('entry'),
323
+ types.identifier('isIntersecting')
324
+ ),
325
+ true
326
+ ),
327
+ types.returnStatement()
328
+ ),
329
+ buildAdvancePageStatement(),
330
+ ])
331
+ ),
332
+ ]
333
+ )
334
+ ),
335
+ ])
336
+ ),
337
+ types.objectExpression([
338
+ types.objectProperty(
339
+ types.identifier('rootMargin'),
340
+ types.stringLiteral('200px')
341
+ ),
342
+ ]),
343
+ ])
344
+ ),
345
+ ]),
346
+ types.expressionStatement(
347
+ types.callExpression(
348
+ types.memberExpression(types.identifier('observer'), types.identifier('observe')),
349
+ [types.identifier('el')]
350
+ )
351
+ ),
352
+ types.returnStatement(
353
+ types.arrowFunctionExpression(
354
+ [],
355
+ types.callExpression(
356
+ types.memberExpression(
357
+ types.identifier('observer'),
358
+ types.identifier('disconnect')
359
+ ),
360
+ []
361
+ )
362
+ )
363
+ ),
364
+ ])
365
+ ),
366
+ types.arrayExpression([types.identifier(vars.hasMoreVar), types.identifier(maxPagesVar)]),
367
+ ])
368
+ )
369
+
370
+ /**
371
+ * `<div ref={ds_N_sentinelRef} aria-hidden="true" style={{ height: '1px' }} />`
372
+ *
373
+ * An invisible one-pixel row after the list. It is generated rather than
374
+ * authored because it is not a control — there is nothing to style or click,
375
+ * only a position in the document for the observer to watch.
376
+ */
377
+ export const buildSentinelElement = (vars: InfiniteScrollVars): types.JSXElement =>
378
+ types.jsxElement(
379
+ types.jsxOpeningElement(
380
+ types.jsxIdentifier('div'),
381
+ [
382
+ types.jsxAttribute(
383
+ types.jsxIdentifier('ref'),
384
+ types.jsxExpressionContainer(types.identifier(vars.sentinelRefVar))
385
+ ),
386
+ types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
387
+ types.jsxAttribute(
388
+ types.jsxIdentifier('style'),
389
+ types.jsxExpressionContainer(
390
+ types.objectExpression([
391
+ types.objectProperty(types.identifier('height'), types.stringLiteral('1px')),
392
+ ])
393
+ )
394
+ ),
395
+ ],
396
+ true
397
+ ),
398
+ null,
399
+ [],
400
+ true
401
+ )
@@ -0,0 +1,292 @@
1
+ import * as types from '@babel/types'
2
+ import {
3
+ convertControlToButton,
4
+ findPaginationControl,
5
+ setJSXExpressionAttribute,
6
+ } from './pagination-controls'
7
+
8
+ /**
9
+ * Numbered pagination: First / Prev / 1 … 4 5 6 … 12 / Next / Last.
10
+ *
11
+ * The builder authors ONE page-number button and a container to repeat it into;
12
+ * this module turns that template into a `.map` over a windowed token list, so
13
+ * every number the visitor sees is the author's own styled node rather than
14
+ * something the generator invented.
15
+ */
16
+
17
+ // tslint:disable:no-any
18
+
19
+ /** `ds_N_pageTokens` — the windowed list of page numbers and ellipsis markers. */
20
+ export const getPageTokensVar = (index: number): string => `ds_${index}_pageTokens`
21
+
22
+ /**
23
+ * Builds the `useMemo` that decides which page numbers are visible.
24
+ *
25
+ * The window is deliberately small and fixed (first, last, and one neighbour
26
+ * either side of the current page, so at most seven entries): an unbounded
27
+ * strip is unusable at 300 pages and wraps the layout the author styled. The
28
+ * two ellipsis markers are distinct strings because React needs unique keys and
29
+ * a list can legitimately need a gap on both sides.
30
+ *
31
+ * ⛔ Keep this in step with `computePageWindow` in the editor's renderer — the
32
+ * canvas and the published page must agree on which numbers are shown, or the
33
+ * author styles a strip they will never see.
34
+ */
35
+ export const buildPageTokensDeclaration = (
36
+ index: number,
37
+ maxPagesVar: string,
38
+ currentPageExpr: types.Expression
39
+ ): types.VariableDeclaration => {
40
+ const total = types.identifier('total')
41
+ const current = types.identifier('current')
42
+ const tokens = types.identifier('tokens')
43
+ const start = types.identifier('start')
44
+ const end = types.identifier('end')
45
+ const pageVar = types.identifier('p')
46
+
47
+ const body = types.blockStatement([
48
+ types.variableDeclaration('const', [
49
+ types.variableDeclarator(total, types.identifier(maxPagesVar)),
50
+ ]),
51
+ types.variableDeclaration('const', [
52
+ types.variableDeclarator(current, types.cloneNode(currentPageExpr, true) as types.Expression),
53
+ ]),
54
+ // Nothing matched at all — the empty branch of the list is what renders.
55
+ types.ifStatement(
56
+ types.binaryExpression('<=', types.cloneNode(total, true), types.numericLiteral(0)),
57
+ types.returnStatement(types.arrayExpression([]))
58
+ ),
59
+ types.ifStatement(
60
+ types.binaryExpression('===', types.cloneNode(total, true), types.numericLiteral(1)),
61
+ types.returnStatement(types.arrayExpression([types.numericLiteral(1)]))
62
+ ),
63
+ types.variableDeclaration('const', [
64
+ types.variableDeclarator(tokens, types.arrayExpression([types.numericLiteral(1)])),
65
+ ]),
66
+ types.variableDeclaration('const', [
67
+ types.variableDeclarator(
68
+ start,
69
+ types.callExpression(
70
+ types.memberExpression(types.identifier('Math'), types.identifier('max')),
71
+ [
72
+ types.numericLiteral(2),
73
+ types.binaryExpression('-', types.cloneNode(current, true), types.numericLiteral(1)),
74
+ ]
75
+ )
76
+ ),
77
+ ]),
78
+ types.variableDeclaration('const', [
79
+ types.variableDeclarator(
80
+ end,
81
+ types.callExpression(
82
+ types.memberExpression(types.identifier('Math'), types.identifier('min')),
83
+ [
84
+ types.binaryExpression('-', types.cloneNode(total, true), types.numericLiteral(1)),
85
+ types.binaryExpression('+', types.cloneNode(current, true), types.numericLiteral(1)),
86
+ ]
87
+ )
88
+ ),
89
+ ]),
90
+ types.ifStatement(
91
+ types.binaryExpression('>', types.cloneNode(start, true), types.numericLiteral(2)),
92
+ types.expressionStatement(
93
+ types.callExpression(
94
+ types.memberExpression(types.cloneNode(tokens, true), types.identifier('push')),
95
+ [types.stringLiteral('start-ellipsis')]
96
+ )
97
+ )
98
+ ),
99
+ types.forStatement(
100
+ types.variableDeclaration('let', [
101
+ types.variableDeclarator(pageVar, types.cloneNode(start, true)),
102
+ ]),
103
+ types.binaryExpression('<=', types.cloneNode(pageVar, true), types.cloneNode(end, true)),
104
+ types.assignmentExpression('+=', types.cloneNode(pageVar, true), types.numericLiteral(1)),
105
+ types.expressionStatement(
106
+ types.callExpression(
107
+ types.memberExpression(types.cloneNode(tokens, true), types.identifier('push')),
108
+ [types.cloneNode(pageVar, true)]
109
+ )
110
+ )
111
+ ),
112
+ types.ifStatement(
113
+ types.binaryExpression(
114
+ '<',
115
+ types.cloneNode(end, true),
116
+ types.binaryExpression('-', types.cloneNode(total, true), types.numericLiteral(1))
117
+ ),
118
+ types.expressionStatement(
119
+ types.callExpression(
120
+ types.memberExpression(types.cloneNode(tokens, true), types.identifier('push')),
121
+ [types.stringLiteral('end-ellipsis')]
122
+ )
123
+ )
124
+ ),
125
+ types.expressionStatement(
126
+ types.callExpression(
127
+ types.memberExpression(types.cloneNode(tokens, true), types.identifier('push')),
128
+ [types.cloneNode(total, true)]
129
+ )
130
+ ),
131
+ types.returnStatement(types.cloneNode(tokens, true)),
132
+ ])
133
+
134
+ return types.variableDeclaration('const', [
135
+ types.variableDeclarator(
136
+ types.identifier(getPageTokensVar(index)),
137
+ types.callExpression(types.identifier('useMemo'), [
138
+ types.arrowFunctionExpression([], body),
139
+ types.arrayExpression([
140
+ types.identifier(maxPagesVar),
141
+ types.cloneNode(currentPageExpr, true) as types.Expression,
142
+ ]),
143
+ ])
144
+ ),
145
+ ])
146
+ }
147
+
148
+ export interface NumberedPaginationVars {
149
+ index: number
150
+ maxPagesVar: string
151
+ /** How this usage reads its current page (`ds_N_state.page` or `ds_N_page`). */
152
+ currentPageExpr: () => types.Expression
153
+ /** How it writes a page number back. */
154
+ buildSetPageStatement: (pageExpr: types.Expression) => types.Expression
155
+ }
156
+
157
+ /** `<setter>(<page>)` wrapped so an already-current page does not re-render. */
158
+ const buildJumpHandler = (
159
+ vars: NumberedPaginationVars,
160
+ targetExpr: types.Expression
161
+ ): types.Expression => types.arrowFunctionExpression([], vars.buildSetPageStatement(targetExpr))
162
+
163
+ /**
164
+ * Wires the First / Last buttons and expands the page-number template.
165
+ *
166
+ * Prev / Next are left to `wirePaginationButtons`, which already emits them and
167
+ * whose output must stay byte-identical for every project that never switches
168
+ * mode.
169
+ */
170
+ export const wireNumberedPagination = (paginationNode: any, vars: NumberedPaginationVars): void => {
171
+ const firstButton = findPaginationControl(paginationNode, 'first')
172
+ const lastButton = findPaginationControl(paginationNode, 'last')
173
+ const pagesContainer = findPaginationControl(paginationNode, 'pages')
174
+
175
+ if (firstButton) {
176
+ convertControlToButton(firstButton)
177
+ setJSXExpressionAttribute(
178
+ firstButton,
179
+ 'onClick',
180
+ buildJumpHandler(vars, types.numericLiteral(1))
181
+ )
182
+ setJSXExpressionAttribute(
183
+ firstButton,
184
+ 'disabled',
185
+ types.binaryExpression('<=', vars.currentPageExpr(), types.numericLiteral(1))
186
+ )
187
+ }
188
+
189
+ if (lastButton) {
190
+ convertControlToButton(lastButton)
191
+ setJSXExpressionAttribute(
192
+ lastButton,
193
+ 'onClick',
194
+ buildJumpHandler(vars, types.identifier(vars.maxPagesVar))
195
+ )
196
+ // `maxPages === 0` is "no results", where jumping to the last page would
197
+ // mean jumping to page 0.
198
+ setJSXExpressionAttribute(
199
+ lastButton,
200
+ 'disabled',
201
+ types.logicalExpression(
202
+ '||',
203
+ types.binaryExpression('===', types.identifier(vars.maxPagesVar), types.numericLiteral(0)),
204
+ types.binaryExpression('>=', vars.currentPageExpr(), types.identifier(vars.maxPagesVar))
205
+ )
206
+ )
207
+ }
208
+
209
+ if (!pagesContainer) {
210
+ return
211
+ }
212
+
213
+ const template = findPaginationControl(pagesContainer, 'page')
214
+ if (!template) {
215
+ // A container with no template to repeat: leave whatever the author put
216
+ // there alone rather than inventing an unstyled strip.
217
+ return
218
+ }
219
+
220
+ const ellipsisTemplate = findPaginationControl(pagesContainer, 'ellipsis')
221
+ const tokenId = types.identifier('token')
222
+
223
+ // The author's own button, cloned per visible page: their class name (and so
224
+ // every style they applied) rides along.
225
+ const numberButton = types.cloneNode(template, true) as any
226
+ convertControlToButton(numberButton)
227
+ setJSXExpressionAttribute(
228
+ numberButton,
229
+ 'onClick',
230
+ buildJumpHandler(vars, types.cloneNode(tokenId, true))
231
+ )
232
+ // The current page is marked, not disabled: `[aria-current='page']` is
233
+ // styleable and keeps the button reachable for screen readers.
234
+ setJSXExpressionAttribute(
235
+ numberButton,
236
+ 'aria-current',
237
+ types.conditionalExpression(
238
+ types.binaryExpression('===', vars.currentPageExpr(), types.cloneNode(tokenId, true)),
239
+ types.stringLiteral('page'),
240
+ types.identifier('undefined')
241
+ )
242
+ )
243
+ setJSXExpressionAttribute(
244
+ numberButton,
245
+ 'key',
246
+ types.templateLiteral(
247
+ [
248
+ types.templateElement({ raw: 'page-', cooked: 'page-' }),
249
+ types.templateElement({ raw: '', cooked: '' }, true),
250
+ ],
251
+ [types.cloneNode(tokenId, true)]
252
+ )
253
+ )
254
+ // Whatever label the template carried ('1') is replaced by the real number.
255
+ numberButton.children = [types.jsxExpressionContainer(types.cloneNode(tokenId, true))]
256
+
257
+ const gapNode = ellipsisTemplate
258
+ ? (types.cloneNode(ellipsisTemplate, true) as any)
259
+ : types.jsxElement(
260
+ types.jsxOpeningElement(types.jsxIdentifier('span'), [], false),
261
+ types.jsxClosingElement(types.jsxIdentifier('span')),
262
+ [types.jsxText('…')],
263
+ false
264
+ )
265
+ setJSXExpressionAttribute(gapNode, 'key', types.cloneNode(tokenId, true))
266
+
267
+ // {ds_N_pageTokens.map(token => typeof token === 'number' ? <button…> : <span…>)}
268
+ pagesContainer.children = [
269
+ types.jsxExpressionContainer(
270
+ types.callExpression(
271
+ types.memberExpression(
272
+ types.identifier(getPageTokensVar(vars.index)),
273
+ types.identifier('map')
274
+ ),
275
+ [
276
+ types.arrowFunctionExpression(
277
+ [types.cloneNode(tokenId, true)],
278
+ types.conditionalExpression(
279
+ types.binaryExpression(
280
+ '===',
281
+ types.unaryExpression('typeof', types.cloneNode(tokenId, true)),
282
+ types.stringLiteral('number')
283
+ ),
284
+ numberButton,
285
+ gapNode
286
+ )
287
+ ),
288
+ ]
289
+ )
290
+ ),
291
+ ]
292
+ }