@octane-xplat/pager 0.15.0 → 0.16.1
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/package.json +4 -4
- package/src/Pager.tsrx +161 -133
- package/src/layout-child.ts +71 -33
- package/src/style-merge.ts +59 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/pager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Paged swipe container for Octane xplat — web scroll-snap row + @nativescript-community/ui-pager (ViewPager2 / paging UICollectionView)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"android",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@nativescript-community/octane": "0.2.5",
|
|
53
53
|
"@nativescript/core": "9.1.3",
|
|
54
|
-
"@octane-xplat/macos-renderer": "0.
|
|
55
|
-
"@octane-xplat/ui": "0.
|
|
54
|
+
"@octane-xplat/macos-renderer": "0.16.1",
|
|
55
|
+
"@octane-xplat/ui": "0.16.1",
|
|
56
56
|
"@octanejs/vite-plugin": "0.1.61",
|
|
57
57
|
"octane": "0.11.0",
|
|
58
|
-
"tsrx-typegen": "0.3.
|
|
58
|
+
"tsrx-typegen": "0.3.1",
|
|
59
59
|
"typescript": "5.9.3",
|
|
60
60
|
"vite": "8.3.0"
|
|
61
61
|
},
|
package/src/Pager.tsrx
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { memo, useCallback, useEffect, useRef, useState } from 'octane'
|
|
2
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'octane'
|
|
3
|
+
import { registerElement } from '@nativescript-community/octane'
|
|
4
|
+
|
|
5
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
6
|
+
import {
|
|
7
|
+
createPortal as createUniversalPortal,
|
|
8
|
+
type UniversalRenderable,
|
|
9
|
+
} from 'octane/universal/native'
|
|
10
|
+
|
|
11
|
+
import { Pager as NSPager } from '@nativescript-community/ui-pager'
|
|
12
|
+
import { GridLayout } from '@nativescript/core'
|
|
13
|
+
import type { PagerProps } from './props'
|
|
3
14
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
15
|
+
applyEscapeProps,
|
|
16
|
+
nativeAccessibilityRole,
|
|
17
|
+
nativeAccessibilityState,
|
|
18
|
+
} from './escape-props.mobile'
|
|
6
19
|
|
|
7
|
-
import
|
|
8
|
-
import { createPortal as createUniversalPortal, type UniversalRenderable } from 'octane/universal/native';
|
|
9
|
-
import { Pager as NSPager } from '@nativescript-community/ui-pager';
|
|
10
|
-
import { GridLayout } from '@nativescript/core';
|
|
11
|
-
import type { PagerProps } from './props';
|
|
12
|
-
import { applyEscapeProps, nativeAccessibilityRole, nativeAccessibilityState } from './escape-props.mobile';
|
|
13
|
-
import { layoutChildProps } from './layout-child';
|
|
20
|
+
import { layoutChildProps } from './layout-child'
|
|
14
21
|
|
|
15
22
|
declare module '@nativescript-community/octane/intrinsics' {
|
|
16
23
|
interface NativeScriptElements {
|
|
17
|
-
pager: Attributes<typeof NSPager
|
|
24
|
+
pager: Attributes<typeof NSPager>
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
@@ -22,168 +29,188 @@ declare module '@nativescript-community/octane/intrinsics' {
|
|
|
22
29
|
// UICollectionView on iOS. It is a required peer: this leaf sits in the
|
|
23
30
|
// shared native barrel, so every native app must declare the plugin for
|
|
24
31
|
// the /ns/m dev bridge to resolve it under the app root.
|
|
25
|
-
registerElement('pager', NSPager)
|
|
32
|
+
registerElement('pager', NSPager)
|
|
26
33
|
|
|
27
34
|
function createPortal(children: unknown, target: unknown) {
|
|
28
|
-
return createUniversalPortal(children as UniversalRenderable, target)
|
|
35
|
+
return createUniversalPortal(children as UniversalRenderable, target)
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** One recycled page: the adapter-owned host plus the current render data.
|
|
32
|
-
|
|
39
|
+
* Its content is a portal from Pager's declaring tree. */
|
|
33
40
|
interface PagerCell {
|
|
34
|
-
host: any
|
|
35
|
-
pager: any
|
|
36
|
-
renderItem: any
|
|
37
|
-
item: any
|
|
38
|
-
index: number
|
|
41
|
+
host: any
|
|
42
|
+
pager: any
|
|
43
|
+
renderItem: any
|
|
44
|
+
item: any
|
|
45
|
+
index: number
|
|
39
46
|
}
|
|
40
47
|
|
|
41
|
-
const cellsByHost = new WeakMap<object, PagerCell>()
|
|
42
|
-
const cellsByPager = new WeakMap<object, Set<PagerCell>>()
|
|
48
|
+
const cellsByHost = new WeakMap<object, PagerCell>()
|
|
49
|
+
const cellsByPager = new WeakMap<object, Set<PagerCell>>()
|
|
43
50
|
|
|
44
51
|
function cellFor(host: any): PagerCell {
|
|
45
|
-
let cell = cellsByHost.get(host)
|
|
52
|
+
let cell = cellsByHost.get(host)
|
|
46
53
|
if (!cell) {
|
|
47
|
-
cell = { host, pager: null, renderItem: null, item: undefined, index: -1 }
|
|
48
|
-
cellsByHost.set(host, cell)
|
|
54
|
+
cell = { host, pager: null, renderItem: null, item: undefined, index: -1 }
|
|
55
|
+
cellsByHost.set(host, cell)
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
return cell
|
|
58
|
+
return cell
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
/** `createView` runs inside the plugin's adapter — no pager instance in
|
|
55
62
|
* scope, so the cell is only linked to its pager at `itemLoading` (where
|
|
56
63
|
* `args.object` is the owner). */
|
|
57
64
|
function createPageHost() {
|
|
58
|
-
const host = new GridLayout()
|
|
59
|
-
cellFor(host)
|
|
60
|
-
return host
|
|
65
|
+
const host = new GridLayout()
|
|
66
|
+
cellFor(host)
|
|
67
|
+
return host
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }]
|
|
70
|
+
const PAGER_TEMPLATES = [{ key: 'xplat-page', createView: createPageHost }]
|
|
64
71
|
// The built-in 'default' template stays first in the plugin's internal
|
|
65
72
|
// list, so the selector must name our key — a 'default' entry would match
|
|
66
73
|
// the built-in (an XML string parse, which yields nothing here).
|
|
67
|
-
const selectPageTemplate = () => 'xplat-page'
|
|
74
|
+
const selectPageTemplate = () => 'xplat-page'
|
|
68
75
|
|
|
69
76
|
function releasePagerCells(pager: any) {
|
|
70
|
-
const cells = cellsByPager.get(pager)
|
|
71
|
-
if (!cells)
|
|
77
|
+
const cells = cellsByPager.get(pager)
|
|
78
|
+
if (!cells) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
for (const cell of cells) {
|
|
73
|
-
cellsByHost.delete(cell.host)
|
|
83
|
+
cellsByHost.delete(cell.host)
|
|
74
84
|
}
|
|
75
85
|
|
|
76
|
-
cellsByPager.delete(pager)
|
|
86
|
+
cellsByPager.delete(pager)
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
function PagerInner(props: PagerProps) @{
|
|
80
|
-
const host = useRef<any>(null)
|
|
81
|
-
const [portalCells, setPortalCells] = useState<PagerCell[]>([])
|
|
90
|
+
const host = useRef<any>(null)
|
|
91
|
+
const [portalCells, setPortalCells] = useState<PagerCell[]>([])
|
|
82
92
|
// Track selectedIndex writes across synchronous and delayed echoes.
|
|
83
93
|
// The plugin raises
|
|
84
94
|
// 'selectedIndexChange' for script writes exactly as for swipes, and
|
|
85
95
|
// the driver's auto-mute only covers JSX prop writes, not ours.
|
|
86
|
-
const current = useRef(props.page ?? props.defaultPage ?? 0)
|
|
87
|
-
const pageSeeded = useRef(false)
|
|
96
|
+
const current = useRef(props.page ?? props.defaultPage ?? 0)
|
|
97
|
+
const pageSeeded = useRef(false)
|
|
88
98
|
|
|
89
99
|
// Stable ref identity — an inline closure re-fires ref(null)/ref(v) per
|
|
90
100
|
// render, which would bounce host.current around the effects below.
|
|
91
101
|
const bindHost = useCallback((v: any) => {
|
|
92
|
-
host.current = v
|
|
93
|
-
}, [])
|
|
102
|
+
host.current = v
|
|
103
|
+
}, [])
|
|
94
104
|
|
|
95
105
|
const bindCell = (args: any) => {
|
|
96
|
-
const cell = cellFor(args.view)
|
|
97
|
-
cell.pager = args.object
|
|
98
|
-
let owned = cellsByPager.get(cell.pager)
|
|
106
|
+
const cell = cellFor(args.view)
|
|
107
|
+
cell.pager = args.object
|
|
108
|
+
let owned = cellsByPager.get(cell.pager)
|
|
99
109
|
if (!owned) {
|
|
100
|
-
owned = new Set()
|
|
101
|
-
cellsByPager.set(cell.pager, owned)
|
|
110
|
+
owned = new Set()
|
|
111
|
+
cellsByPager.set(cell.pager, owned)
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
owned.add(cell)
|
|
105
|
-
const item = args.bindingContext ?? props.items?.[args.index]
|
|
106
|
-
if (
|
|
107
|
-
|
|
114
|
+
owned.add(cell)
|
|
115
|
+
const item = args.bindingContext ?? props.items?.[args.index]
|
|
116
|
+
if (
|
|
117
|
+
cell.renderItem === props.renderItem &&
|
|
118
|
+
cell.index === args.index &&
|
|
119
|
+
Object.is(cell.item, item)
|
|
120
|
+
) {
|
|
121
|
+
return
|
|
108
122
|
}
|
|
109
123
|
|
|
110
|
-
cell.renderItem = props.renderItem
|
|
111
|
-
cell.item = item
|
|
112
|
-
cell.index = args.index
|
|
124
|
+
cell.renderItem = props.renderItem
|
|
125
|
+
cell.item = item
|
|
126
|
+
cell.index = args.index
|
|
113
127
|
setPortalCells((current) => {
|
|
114
|
-
const existing = current.some((entry) => entry === cell)
|
|
115
|
-
return existing ? [...current] : [...current, cell]
|
|
116
|
-
})
|
|
117
|
-
}
|
|
128
|
+
const existing = current.some((entry) => entry === cell)
|
|
129
|
+
return existing ? [...current] : [...current, cell]
|
|
130
|
+
})
|
|
131
|
+
}
|
|
118
132
|
|
|
119
133
|
const disposeCell = (args: any) => {
|
|
120
|
-
const cell = args.view && cellsByHost.get(args.view)
|
|
134
|
+
const cell = args.view && cellsByHost.get(args.view)
|
|
121
135
|
if (cell) {
|
|
122
|
-
cellsByHost.delete(args.view)
|
|
123
|
-
if (cell.pager)
|
|
124
|
-
|
|
136
|
+
cellsByHost.delete(args.view)
|
|
137
|
+
if (cell.pager) {
|
|
138
|
+
cellsByPager.get(cell.pager)?.delete(cell)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setPortalCells((current) => current.filter((entry) => entry !== cell))
|
|
125
142
|
}
|
|
126
|
-
}
|
|
143
|
+
}
|
|
127
144
|
|
|
128
145
|
// One-time wiring (mount): the template set + selector are imperative —
|
|
129
146
|
// as JSX props they would be re-applied on every render. The cleanup
|
|
130
147
|
// captures the pager so cell roots release after the ref detaches.
|
|
131
148
|
useEffect(() => {
|
|
132
|
-
const pager = host.current
|
|
133
|
-
if (!pager)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
const pager = host.current
|
|
150
|
+
if (!pager) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pager.itemTemplates = PAGER_TEMPLATES
|
|
155
|
+
pager.itemTemplateSelector = selectPageTemplate
|
|
156
|
+
return () => releasePagerCells(pager)
|
|
157
|
+
}, [])
|
|
138
158
|
|
|
139
159
|
// Items and the index are imperative writes, not JSX props: `items` must
|
|
140
160
|
// land before `selectedIndex` (the plugin coerces the index against the
|
|
141
161
|
// page count — a write ahead of items clamps to -1), and a re-assert
|
|
142
162
|
// after an items change repairs the plugin's own re-coercion.
|
|
143
163
|
useEffect(() => {
|
|
144
|
-
const pager = host.current
|
|
145
|
-
if (!pager)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
const pager = host.current
|
|
165
|
+
if (!pager) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
applyEscapeProps(pager, props)
|
|
170
|
+
if (pager.items !== props.items) {
|
|
171
|
+
pager.items = props.items
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const target = props.page ?? (pageSeeded.current ? undefined : props.defaultPage)
|
|
149
175
|
if (target !== undefined && pager.selectedIndex !== target) {
|
|
150
176
|
// Track the coerced target before writing: Android may echo later.
|
|
151
|
-
current.current = Math.max(0, Math.min(props.items.length - 1, target))
|
|
152
|
-
pager.selectedIndex = target
|
|
177
|
+
current.current = Math.max(0, Math.min(props.items.length - 1, target))
|
|
178
|
+
pager.selectedIndex = target
|
|
153
179
|
}
|
|
154
180
|
|
|
155
|
-
pageSeeded.current = true
|
|
156
|
-
})
|
|
181
|
+
pageSeeded.current = true
|
|
182
|
+
})
|
|
157
183
|
|
|
158
184
|
<>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
<pager
|
|
186
|
+
ref={bindHost}
|
|
187
|
+
id={props.id}
|
|
188
|
+
{...layoutChildProps(props)}
|
|
189
|
+
className={['vx-pager', props.className]}
|
|
190
|
+
onItemLoading={bindCell}
|
|
191
|
+
onItemDisposing={disposeCell}
|
|
192
|
+
onSelectedIndexChange={(args: any) => {
|
|
193
|
+
if (args.value !== current.current) {
|
|
194
|
+
current.current = args.value
|
|
195
|
+
props.onPageChange?.(args.value)
|
|
196
|
+
}
|
|
197
|
+
}}
|
|
198
|
+
accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
|
|
199
|
+
accessibilityLabel={props.accessibilityLabel}
|
|
200
|
+
accessibilityHint={props.accessibilityHint}
|
|
201
|
+
accessibilityValue={props.accessibilityValue}
|
|
202
|
+
accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
|
|
203
|
+
accessible={props.accessible}
|
|
204
|
+
accessibilityLiveRegion={props.accessibilityLiveRegion as any}
|
|
205
|
+
/>
|
|
206
|
+
{portalCells.map((cell) =>
|
|
207
|
+
createPortal(
|
|
208
|
+
<masonview style={{ display: 'grid' }}>
|
|
209
|
+
{cell.renderItem?.(cell.item, cell.index)}
|
|
210
|
+
</masonview>,
|
|
211
|
+
cell.host,
|
|
212
|
+
),
|
|
213
|
+
)}
|
|
187
214
|
</>
|
|
188
215
|
}
|
|
189
216
|
|
|
@@ -193,34 +220,36 @@ function PagerInner(props: PagerProps) @{
|
|
|
193
220
|
// UITableView: `renderItem` should be identity-stable — a fresh closure per
|
|
194
221
|
// render is deliberately not part of the compare. (`PagerInner` must precede
|
|
195
222
|
// this — compiled components are consts, not hoisted functions.)
|
|
196
|
-
const MemoPager = memo(
|
|
197
|
-
|
|
198
|
-
a
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
const MemoPager = memo(
|
|
224
|
+
PagerInner,
|
|
225
|
+
(a, b) =>
|
|
226
|
+
a.items === b.items &&
|
|
227
|
+
a.className === b.className &&
|
|
228
|
+
a.style === b.style &&
|
|
229
|
+
a.id === b.id &&
|
|
230
|
+
a.page === b.page &&
|
|
231
|
+
a.onPageChange === b.onPageChange &&
|
|
232
|
+
a.ios === b.ios &&
|
|
233
|
+
a.android === b.android &&
|
|
234
|
+
a.row === b.row &&
|
|
235
|
+
a.col === b.col &&
|
|
236
|
+
a.rowSpan === b.rowSpan &&
|
|
237
|
+
a.colSpan === b.colSpan &&
|
|
238
|
+
a.dock === b.dock &&
|
|
239
|
+
a.left === b.left &&
|
|
240
|
+
a.top === b.top &&
|
|
241
|
+
a.flexGrow === b.flexGrow &&
|
|
242
|
+
a.flexShrink === b.flexShrink &&
|
|
243
|
+
a.alignSelf === b.alignSelf &&
|
|
244
|
+
a.order === b.order &&
|
|
245
|
+
a.accessible === b.accessible &&
|
|
246
|
+
a.accessibilityRole === b.accessibilityRole &&
|
|
247
|
+
a.accessibilityLabel === b.accessibilityLabel &&
|
|
248
|
+
a.accessibilityHint === b.accessibilityHint &&
|
|
249
|
+
a.accessibilityValue === b.accessibilityValue &&
|
|
250
|
+
a.accessibilityState === b.accessibilityState &&
|
|
251
|
+
a.accessibilityLiveRegion === b.accessibilityLiveRegion,
|
|
252
|
+
)
|
|
224
253
|
|
|
225
254
|
/** Pager — paged horizontal swiping via @nativescript-community/ui-pager
|
|
226
255
|
* (ViewPager2 / paging UICollectionView), chrome-reset: one full-size cell
|
|
@@ -239,7 +268,6 @@ export function Pager(props: PagerProps) @{
|
|
|
239
268
|
accessibilityLiveRegion={props.accessibilityLiveRegion}
|
|
240
269
|
id={props.id}
|
|
241
270
|
className={props.className}
|
|
242
|
-
style={props.style}
|
|
243
271
|
items={props.items}
|
|
244
272
|
renderItem={props.renderItem}
|
|
245
273
|
page={props.page}
|
package/src/layout-child.ts
CHANGED
|
@@ -1,57 +1,95 @@
|
|
|
1
1
|
import type { LayoutChildProps, FlexContainerProps } from './props'
|
|
2
|
+
import { mergeStyle } from './style-merge'
|
|
2
3
|
|
|
3
4
|
// start/end shorthand → flex-start/flex-end.
|
|
4
5
|
const FLEX_JUSTIFY: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
|
|
5
6
|
const FLEX_ALIGN: Record<string, string> = { start: 'flex-start', end: 'flex-end' }
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
/** Convert parent-layout metadata + the flex-container contract to the CSS
|
|
9
|
+
* style bag on the host masonview — mirrors layout-child.web.ts so the same
|
|
10
|
+
* component props compile to the same CSS on both targets. `dock` has no
|
|
11
|
+
* CSS equivalent on web (ignored there); on native it becomes absolute
|
|
12
|
+
* insets. Native adds `position: absolute` for edge-placed children since
|
|
13
|
+
* there is no `.vx-absolute > *` stylesheet rule on the mason engine. */
|
|
14
|
+
export function layoutChildProps(
|
|
15
|
+
props: LayoutChildProps & Partial<FlexContainerProps> & { style?: any },
|
|
16
|
+
baseStyle = props.style,
|
|
17
|
+
) {
|
|
18
|
+
const style: Record<string, any> = { ...(typeof baseStyle === 'object' ? baseStyle : {}) }
|
|
19
|
+
if (props.row !== undefined) {
|
|
20
|
+
style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`
|
|
21
|
+
} else if (props.rowSpan !== undefined) {
|
|
22
|
+
style.gridRow = `span ${props.rowSpan}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (props.col !== undefined) {
|
|
26
|
+
style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`
|
|
27
|
+
} else if (props.colSpan !== undefined) {
|
|
28
|
+
style.gridColumn = `span ${props.colSpan}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (props.dock !== undefined) {
|
|
32
|
+
style.position = style.position ?? 'absolute'
|
|
33
|
+
if (props.dock === 'left' || props.dock === 'top' || props.dock === 'bottom') {
|
|
34
|
+
style.left = style.left ?? 0
|
|
35
|
+
}
|
|
36
|
+
if (props.dock === 'right' || props.dock === 'top' || props.dock === 'bottom') {
|
|
37
|
+
style.right = style.right ?? 0
|
|
38
|
+
}
|
|
39
|
+
if (props.dock === 'top' || props.dock === 'left' || props.dock === 'right') {
|
|
40
|
+
style.top = style.top ?? 0
|
|
41
|
+
}
|
|
42
|
+
if (props.dock === 'bottom' || props.dock === 'left' || props.dock === 'right') {
|
|
43
|
+
style.bottom = style.bottom ?? 0
|
|
28
44
|
}
|
|
29
45
|
}
|
|
30
46
|
|
|
31
|
-
if (props.
|
|
32
|
-
|
|
47
|
+
if (props.left !== undefined) {
|
|
48
|
+
style.left = props.left
|
|
49
|
+
}
|
|
50
|
+
if (props.top !== undefined) {
|
|
51
|
+
style.top = props.top
|
|
52
|
+
}
|
|
53
|
+
if ((props.left !== undefined || props.top !== undefined) && props.dock === undefined) {
|
|
54
|
+
style.position = style.position ?? 'absolute'
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
if (props.
|
|
36
|
-
|
|
57
|
+
if (props.flexGrow !== undefined) {
|
|
58
|
+
style.flexGrow = props.flexGrow
|
|
59
|
+
}
|
|
60
|
+
if (props.flexShrink !== undefined) {
|
|
61
|
+
style.flexShrink = props.flexShrink
|
|
62
|
+
}
|
|
63
|
+
if (props.alignSelf !== undefined) {
|
|
64
|
+
style.alignSelf = props.alignSelf
|
|
65
|
+
}
|
|
66
|
+
if (props.order !== undefined) {
|
|
67
|
+
style.order = props.order
|
|
37
68
|
}
|
|
38
69
|
|
|
70
|
+
// Flex-container props (View/Row/Pressable hosts) — RN vocabulary on the
|
|
71
|
+
// element style.
|
|
72
|
+
if (props.justifyContent !== undefined) {
|
|
73
|
+
style.justifyContent = FLEX_JUSTIFY[props.justifyContent] ?? props.justifyContent
|
|
74
|
+
}
|
|
75
|
+
if (props.alignItems !== undefined) {
|
|
76
|
+
style.alignItems = FLEX_ALIGN[props.alignItems] ?? props.alignItems
|
|
77
|
+
}
|
|
39
78
|
if (props.flexWrap !== undefined) {
|
|
40
|
-
|
|
79
|
+
style.flexWrap =
|
|
41
80
|
props.flexWrap === true ? 'wrap' : props.flexWrap === false ? 'nowrap' : props.flexWrap
|
|
42
81
|
}
|
|
43
|
-
|
|
44
82
|
if (props.gap !== undefined) {
|
|
45
|
-
|
|
83
|
+
style.gap = props.gap
|
|
46
84
|
}
|
|
47
|
-
|
|
48
85
|
if (props.rowGap !== undefined) {
|
|
49
|
-
|
|
86
|
+
style.rowGap = props.rowGap
|
|
50
87
|
}
|
|
51
|
-
|
|
52
88
|
if (props.columnGap !== undefined) {
|
|
53
|
-
|
|
89
|
+
style.columnGap = props.columnGap
|
|
54
90
|
}
|
|
55
91
|
|
|
56
|
-
|
|
92
|
+
// A string base is inline CSS — serialize the emitted keys under it so the
|
|
93
|
+
// authored declarations still win.
|
|
94
|
+
return { style: typeof baseStyle === 'string' ? mergeStyle(style, baseStyle) : style }
|
|
57
95
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const kebab = (key: string) => key.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())
|
|
2
|
+
|
|
3
|
+
/** Anything accepted as a style value, including nested arrays. */
|
|
4
|
+
export type StyleInput =
|
|
5
|
+
| string
|
|
6
|
+
| Record<string, any>
|
|
7
|
+
| null
|
|
8
|
+
| undefined
|
|
9
|
+
| readonly StyleInput[]
|
|
10
|
+
|
|
11
|
+
/** Merge component-emitted style defaults under the authored `style` prop.
|
|
12
|
+
* Authored keys win; array styles fold in order; a string style is appended
|
|
13
|
+
* after the serialized defaults so its declarations override them too. */
|
|
14
|
+
export function mergeStyle(
|
|
15
|
+
base: Record<string, any>,
|
|
16
|
+
style?: Record<string, any> | readonly (Record<string, any> | null | undefined)[] | null,
|
|
17
|
+
): Record<string, any>
|
|
18
|
+
export function mergeStyle(
|
|
19
|
+
base: Record<string, any>,
|
|
20
|
+
style?: StyleInput,
|
|
21
|
+
): string | Record<string, any>
|
|
22
|
+
export function mergeStyle(
|
|
23
|
+
base: Record<string, any>,
|
|
24
|
+
style?: StyleInput,
|
|
25
|
+
): string | Record<string, any> {
|
|
26
|
+
if (style == null) {
|
|
27
|
+
return base
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let obj: Record<string, any> = { ...base }
|
|
31
|
+
const strings: string[] = []
|
|
32
|
+
const add = (s: StyleInput) => {
|
|
33
|
+
if (s == null) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (typeof s === 'string') {
|
|
37
|
+
strings.push(s)
|
|
38
|
+
} else if (Array.isArray(s)) {
|
|
39
|
+
for (const part of s) {
|
|
40
|
+
add(part)
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
obj = { ...obj, ...s }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
add(style)
|
|
47
|
+
|
|
48
|
+
if (strings.length === 0) {
|
|
49
|
+
return obj
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const prefix = Object.entries(obj)
|
|
53
|
+
.filter(([, v]) => v != null)
|
|
54
|
+
.map(([k, v]) => `${kebab(k)}:${v}`)
|
|
55
|
+
.join(';')
|
|
56
|
+
return [prefix, ...strings].filter(Boolean).join(';')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type StyleProp = string | Record<string, any> | readonly StyleProp[] | null | undefined
|