@rafaelaugusto/jolt 0.1.0-beta.16 → 0.1.0-beta.17
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/README.md +7 -2
- package/dist/bottom-sheet/geometry.d.ts +18 -0
- package/dist/bottom-sheet/geometry.d.ts.map +1 -0
- package/dist/bottom-sheet/geometry.js +51 -0
- package/dist/bottom-sheet/geometry.js.map +1 -0
- package/dist/bottom-sheet/index.d.ts +56 -0
- package/dist/bottom-sheet/index.d.ts.map +1 -0
- package/dist/bottom-sheet/index.js +462 -0
- package/dist/bottom-sheet/index.js.map +1 -0
- package/dist/bottom-sheet/native.d.ts +11 -0
- package/dist/bottom-sheet/native.d.ts.map +1 -0
- package/dist/bottom-sheet/native.js +173 -0
- package/dist/bottom-sheet/native.js.map +1 -0
- package/dist/gesture-native.d.ts +1 -0
- package/dist/gesture-native.d.ts.map +1 -1
- package/dist/gesture-native.js +6 -0
- package/dist/gesture-native.js.map +1 -1
- package/dist/gestures/index.d.ts +4 -0
- package/dist/gestures/index.d.ts.map +1 -1
- package/dist/gestures/index.js +12 -1
- package/dist/gestures/index.js.map +1 -1
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -1
- package/dist/native-property.d.ts +2 -0
- package/dist/native-property.d.ts.map +1 -1
- package/dist/native-property.js +12 -4
- package/dist/native-property.js.map +1 -1
- package/dist/reorder/handle.d.ts +6 -0
- package/dist/reorder/handle.d.ts.map +1 -0
- package/dist/reorder/handle.js +10 -0
- package/dist/reorder/handle.js.map +1 -0
- package/dist/reorder/index.d.ts +10 -0
- package/dist/reorder/index.d.ts.map +1 -0
- package/dist/reorder/index.js +233 -0
- package/dist/reorder/index.js.map +1 -0
- package/dist/reorder/list-view.d.ts +5 -0
- package/dist/reorder/list-view.d.ts.map +1 -0
- package/dist/reorder/list-view.js +238 -0
- package/dist/reorder/list-view.js.map +1 -0
- package/dist/reorder/model.d.ts +6 -0
- package/dist/reorder/model.d.ts.map +1 -0
- package/dist/reorder/model.js +30 -0
- package/dist/reorder/model.js.map +1 -0
- package/dist/reorder/surface.d.ts +6 -0
- package/dist/reorder/surface.d.ts.map +1 -0
- package/dist/reorder/surface.js +10 -0
- package/dist/reorder/surface.js.map +1 -0
- package/dist/reorder/types.d.ts +92 -0
- package/dist/reorder/types.d.ts.map +1 -0
- package/dist/reorder/types.js +2 -0
- package/dist/reorder/types.js.map +1 -0
- package/dist/scene-shader.d.ts +18 -0
- package/dist/scene-shader.d.ts.map +1 -0
- package/dist/scene-shader.js +76 -0
- package/dist/scene-shader.js.map +1 -0
- package/package.json +9 -1
- package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +7 -2
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
- package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
- package/platforms/android/proguard-rules.pro +5 -0
- package/platforms/ios/src/JoltGestureSession.h +1 -0
- package/platforms/ios/src/JoltGestureSession.m +7 -2
- package/platforms/ios/src/JoltReorderSession.h +15 -0
- package/platforms/ios/src/JoltReorderSession.m +205 -0
- package/platforms/ios/src/module.modulemap +1 -0
- package/src/android-plugin.d.ts +34 -0
- package/src/bottom-sheet/geometry.ts +84 -0
- package/src/bottom-sheet/index.ts +536 -0
- package/src/bottom-sheet/native.ts +214 -0
- package/src/gesture-native.ts +15 -0
- package/src/gestures/index.ts +16 -1
- package/src/ios-plugin.d.ts +32 -0
- package/src/native/index.ts +6 -0
- package/src/native-property.ts +17 -4
- package/src/reorder/handle.ts +9 -0
- package/src/reorder/index.ts +243 -0
- package/src/reorder/list-view.ts +262 -0
- package/src/reorder/model.ts +47 -0
- package/src/reorder/surface.ts +10 -0
- package/src/reorder/types.ts +87 -0
- package/src/scene-shader.ts +139 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Application,
|
|
3
|
+
Screen,
|
|
4
|
+
Utils,
|
|
5
|
+
isIOS,
|
|
6
|
+
type ScrollView,
|
|
7
|
+
type View,
|
|
8
|
+
} from '@nativescript/core'
|
|
9
|
+
|
|
10
|
+
function firstResponder(view: UIView): UIView | undefined {
|
|
11
|
+
if (view.isFirstResponder) return view
|
|
12
|
+
for (let i = 0; i < view.subviews.count; i++) {
|
|
13
|
+
const found = firstResponder(view.subviews.objectAtIndex(i))
|
|
14
|
+
if (found) return found
|
|
15
|
+
}
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function windowHeight(view: View): number {
|
|
20
|
+
if (isIOS)
|
|
21
|
+
return (
|
|
22
|
+
(view.nativeViewProtected as UIView)?.window?.bounds.size.height ??
|
|
23
|
+
Screen.mainScreen.heightDIPs
|
|
24
|
+
)
|
|
25
|
+
const activity =
|
|
26
|
+
Application.android.foregroundActivity ?? Application.android.startActivity
|
|
27
|
+
if (android.os.Build.VERSION.SDK_INT >= 30 && activity)
|
|
28
|
+
return (
|
|
29
|
+
activity.getWindowManager().getCurrentWindowMetrics().getBounds().height() /
|
|
30
|
+
Utils.layout.getDisplayDensity()
|
|
31
|
+
)
|
|
32
|
+
return Screen.mainScreen.heightDIPs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function captureFocus(panel: View, returnFocus?: View): () => void {
|
|
36
|
+
if (returnFocus)
|
|
37
|
+
return () => {
|
|
38
|
+
if (!returnFocus.isLoaded) return
|
|
39
|
+
returnFocus.focus()
|
|
40
|
+
returnFocus.accessibilityScreenChanged()
|
|
41
|
+
}
|
|
42
|
+
if (isIOS) {
|
|
43
|
+
const window = (panel.nativeViewProtected as UIView)?.window
|
|
44
|
+
const responder = window ? firstResponder(window) : undefined
|
|
45
|
+
const accessible = UIAccessibilityFocusedElement(null)
|
|
46
|
+
return () => {
|
|
47
|
+
if (responder?.window) responder.becomeFirstResponder()
|
|
48
|
+
UIAccessibilityPostNotification(
|
|
49
|
+
UIAccessibilityLayoutChangedNotification,
|
|
50
|
+
accessible ?? responder ?? null,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const activity =
|
|
55
|
+
Application.android.foregroundActivity ?? Application.android.startActivity
|
|
56
|
+
const previous = activity?.getCurrentFocus()
|
|
57
|
+
return () => {
|
|
58
|
+
if (previous?.isAttachedToWindow()) {
|
|
59
|
+
previous.requestFocus()
|
|
60
|
+
previous.sendAccessibilityEvent(
|
|
61
|
+
android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function revealFocusedField(panel: View, scroll?: ScrollView): void {
|
|
68
|
+
if (!scroll?.nativeViewProtected || !panel.nativeViewProtected) return
|
|
69
|
+
if (isIOS) {
|
|
70
|
+
const focused = firstResponder(panel.nativeViewProtected as UIView)
|
|
71
|
+
const native = scroll.nativeViewProtected as UIScrollView
|
|
72
|
+
if (focused?.isDescendantOfView(native))
|
|
73
|
+
native.scrollRectToVisibleAnimated(
|
|
74
|
+
focused.convertRectToView(focused.bounds, native),
|
|
75
|
+
false,
|
|
76
|
+
)
|
|
77
|
+
} else {
|
|
78
|
+
const focused = (panel.nativeViewProtected as android.view.View).findFocus()
|
|
79
|
+
if (focused)
|
|
80
|
+
focused.requestRectangleOnScreen(
|
|
81
|
+
new android.graphics.Rect(0, 0, focused.getWidth(), focused.getHeight()),
|
|
82
|
+
true,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function dismissKeyboard(panel: View): void {
|
|
88
|
+
if (!panel.nativeViewProtected) return
|
|
89
|
+
if (isIOS) (panel.nativeViewProtected as UIView).endEditing(true)
|
|
90
|
+
else {
|
|
91
|
+
const focused = (panel.nativeViewProtected as android.view.View).findFocus()
|
|
92
|
+
if (focused) {
|
|
93
|
+
const manager = focused
|
|
94
|
+
.getContext()
|
|
95
|
+
.getSystemService(
|
|
96
|
+
android.content.Context.INPUT_METHOD_SERVICE,
|
|
97
|
+
) as android.view.inputmethod.InputMethodManager
|
|
98
|
+
manager.hideSoftInputFromWindow(focused.getWindowToken(), 0)
|
|
99
|
+
focused.clearFocus()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Add only our actions; preserve an application's existing native accessibility actions. */
|
|
105
|
+
export function sheetActions(
|
|
106
|
+
handle: View,
|
|
107
|
+
actions: readonly { label: string; run(): void }[],
|
|
108
|
+
): () => void {
|
|
109
|
+
if (isIOS) {
|
|
110
|
+
const native = handle.nativeViewProtected as UIView
|
|
111
|
+
const previous = native.accessibilityCustomActions
|
|
112
|
+
const own = actions.map(({ label, run }) =>
|
|
113
|
+
UIAccessibilityCustomAction.alloc().initWithNameActionHandler(label, () => {
|
|
114
|
+
run()
|
|
115
|
+
return true
|
|
116
|
+
}),
|
|
117
|
+
)
|
|
118
|
+
const existing: UIAccessibilityCustomAction[] = []
|
|
119
|
+
if (previous)
|
|
120
|
+
for (let i = 0; i < previous.count; i++) existing.push(previous.objectAtIndex(i))
|
|
121
|
+
native.accessibilityCustomActions = NSArray.arrayWithArray([...existing, ...own])
|
|
122
|
+
return () => {
|
|
123
|
+
const current = native.accessibilityCustomActions
|
|
124
|
+
const retained: UIAccessibilityCustomAction[] = []
|
|
125
|
+
if (current)
|
|
126
|
+
for (let i = 0; i < current.count; i++) {
|
|
127
|
+
const action = current.objectAtIndex(i)
|
|
128
|
+
if (!own.includes(action)) retained.push(action)
|
|
129
|
+
}
|
|
130
|
+
native.accessibilityCustomActions = retained.length
|
|
131
|
+
? NSArray.arrayWithArray(retained)
|
|
132
|
+
: null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const native = handle.nativeViewProtected as android.view.View
|
|
136
|
+
const compat = androidx.core.view.ViewCompat
|
|
137
|
+
const ids: number[] = []
|
|
138
|
+
let ended = false,
|
|
139
|
+
timer: ReturnType<typeof setTimeout> | undefined
|
|
140
|
+
// Core batches accessibility-style updates in a microtask and installs its delegate.
|
|
141
|
+
// Wrap that delegate afterwards, retaining its semantics and all existing actions.
|
|
142
|
+
const repair = () => {
|
|
143
|
+
if (ended || handle.nativeViewProtected !== native) return
|
|
144
|
+
compat.setAccessibilityDelegate(
|
|
145
|
+
native,
|
|
146
|
+
compat.getAccessibilityDelegate(native) ??
|
|
147
|
+
new androidx.core.view.AccessibilityDelegateCompat(),
|
|
148
|
+
)
|
|
149
|
+
if (ids.length) return
|
|
150
|
+
const node = native.createAccessibilityNodeInfo(),
|
|
151
|
+
existing = node.getActionList()
|
|
152
|
+
const used = new Set<number>()
|
|
153
|
+
for (let index = 0; index < existing.size(); index++)
|
|
154
|
+
used.add(existing.get(index).getId())
|
|
155
|
+
node.recycle()
|
|
156
|
+
const resources = native.getResources(),
|
|
157
|
+
packageName = native.getContext().getPackageName()
|
|
158
|
+
const available: number[] = []
|
|
159
|
+
for (let index = 0; index < 32; index++) {
|
|
160
|
+
const id = resources.getIdentifier(
|
|
161
|
+
`accessibility_custom_action_${index}`,
|
|
162
|
+
'id',
|
|
163
|
+
packageName,
|
|
164
|
+
)
|
|
165
|
+
if (id && !used.has(id)) available.push(id)
|
|
166
|
+
}
|
|
167
|
+
if (available.length < actions.length)
|
|
168
|
+
throw new Error(
|
|
169
|
+
'Not enough free Android accessibility actions for the sheet handle.',
|
|
170
|
+
)
|
|
171
|
+
for (let index = 0; index < actions.length; index++) {
|
|
172
|
+
const { label, run } = actions[index]!,
|
|
173
|
+
id = available[index]!
|
|
174
|
+
const action =
|
|
175
|
+
new androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat(
|
|
176
|
+
id,
|
|
177
|
+
label,
|
|
178
|
+
)
|
|
179
|
+
compat.replaceAccessibilityAction(
|
|
180
|
+
native,
|
|
181
|
+
action,
|
|
182
|
+
label,
|
|
183
|
+
new androidx.core.view.accessibility.AccessibilityViewCommand({
|
|
184
|
+
perform: () => {
|
|
185
|
+
run()
|
|
186
|
+
return true
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
)
|
|
190
|
+
ids.push(id)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const schedule = () => {
|
|
194
|
+
if (ended || timer !== undefined) return
|
|
195
|
+
timer = setTimeout(() => {
|
|
196
|
+
timer = undefined
|
|
197
|
+
repair()
|
|
198
|
+
}, 0)
|
|
199
|
+
}
|
|
200
|
+
const changes = [
|
|
201
|
+
'accessibleChange',
|
|
202
|
+
'accessibilityRoleChange',
|
|
203
|
+
'accessibilityStateChange',
|
|
204
|
+
'accessibilityMediaSessionChange',
|
|
205
|
+
]
|
|
206
|
+
for (const event of changes) handle.style.on(event, schedule)
|
|
207
|
+
schedule()
|
|
208
|
+
return () => {
|
|
209
|
+
ended = true
|
|
210
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
211
|
+
for (const event of changes) handle.style.off(event, schedule)
|
|
212
|
+
ids.forEach((id) => compat.removeAccessibilityAction(native, id))
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/gesture-native.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface GestureSettings {
|
|
|
11
11
|
rotate: boolean
|
|
12
12
|
tap: boolean
|
|
13
13
|
doubleTap: boolean
|
|
14
|
+
contactEvents: boolean
|
|
14
15
|
minX: number
|
|
15
16
|
maxX: number
|
|
16
17
|
minY: number
|
|
@@ -43,6 +44,13 @@ export function createGestureDriver(
|
|
|
43
44
|
if (isIOS) {
|
|
44
45
|
if (typeof JoltGestureSession === 'undefined')
|
|
45
46
|
throw new Error('Rebuild the native app to use gestures().')
|
|
47
|
+
if (
|
|
48
|
+
options.contactEvents &&
|
|
49
|
+
typeof JoltGestureSession.supportsContactEvents !== 'function'
|
|
50
|
+
)
|
|
51
|
+
throw new Error(
|
|
52
|
+
'Rebuild the native app to use gesture contact events and bottomSheet().',
|
|
53
|
+
)
|
|
46
54
|
const [x, y, scale, rotation] = nodes as [
|
|
47
55
|
JoltLiveSpring,
|
|
48
56
|
JoltLiveSpring,
|
|
@@ -91,6 +99,13 @@ export function createGestureDriver(
|
|
|
91
99
|
},
|
|
92
100
|
}
|
|
93
101
|
}
|
|
102
|
+
if (
|
|
103
|
+
options.contactEvents &&
|
|
104
|
+
typeof org.rafaelaugusto.jolt.GestureSession.supportsContactEvents !== 'function'
|
|
105
|
+
)
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Rebuild the native app to use gesture contact events and bottomSheet().',
|
|
108
|
+
)
|
|
94
109
|
const [x, y, scale, rotation] = nodes as [
|
|
95
110
|
org.rafaelaugusto.jolt.LiveSpring,
|
|
96
111
|
org.rafaelaugusto.jolt.LiveSpring,
|
package/src/gestures/index.ts
CHANGED
|
@@ -53,6 +53,10 @@ export interface GesturesOptions {
|
|
|
53
53
|
spring?: Pick<SpringValueOptions, 'stiffness' | 'damping' | 'mass'>
|
|
54
54
|
/** A descendant, vertical NativeScript ScrollView. Other list plugins are not supported. */
|
|
55
55
|
scrollView?: ScrollView
|
|
56
|
+
/** First contact, before pan recognition; useful when a touch interrupts a spring. */
|
|
57
|
+
onContactStart?: (event: GestureEvent) => void
|
|
58
|
+
/** Last contact released or cancelled, including a touch that never becomes a pan. */
|
|
59
|
+
onContactEnd?: (event: GestureEvent) => void
|
|
56
60
|
onStart?: (event: GestureEvent) => void
|
|
57
61
|
onEnd?: (event: GestureEvent) => void
|
|
58
62
|
onCancel?: (event: GestureEvent) => void
|
|
@@ -106,7 +110,15 @@ function settings(options: GesturesOptions): GestureSettings {
|
|
|
106
110
|
throw new TypeError(`Gesture ${key} must be boolean or an options object.`)
|
|
107
111
|
if (options.rotate !== undefined && typeof options.rotate !== 'boolean')
|
|
108
112
|
throw new TypeError('Gesture rotate must be boolean.')
|
|
109
|
-
for (const key of [
|
|
113
|
+
for (const key of [
|
|
114
|
+
'onStart',
|
|
115
|
+
'onEnd',
|
|
116
|
+
'onCancel',
|
|
117
|
+
'onTap',
|
|
118
|
+
'onDoubleTap',
|
|
119
|
+
'onContactStart',
|
|
120
|
+
'onContactEnd',
|
|
121
|
+
] as const)
|
|
110
122
|
if (options[key] !== undefined && typeof options[key] !== 'function')
|
|
111
123
|
throw new TypeError(`Gesture ${key} must be a function.`)
|
|
112
124
|
const pan = typeof options.pan === 'object' ? options.pan : {}
|
|
@@ -123,6 +135,7 @@ function settings(options: GesturesOptions): GestureSettings {
|
|
|
123
135
|
rotate: !!options.rotate,
|
|
124
136
|
tap: !!options.onTap,
|
|
125
137
|
doubleTap: !!options.onDoubleTap,
|
|
138
|
+
contactEvents: !!(options.onContactStart || options.onContactEnd),
|
|
126
139
|
minScale: pinch.min ?? 0.25,
|
|
127
140
|
maxScale: pinch.max ?? 4,
|
|
128
141
|
}
|
|
@@ -201,6 +214,8 @@ export function gestures(
|
|
|
201
214
|
options.onCancel,
|
|
202
215
|
options.onTap,
|
|
203
216
|
options.onDoubleTap,
|
|
217
|
+
options.onContactStart,
|
|
218
|
+
options.onContactEnd,
|
|
204
219
|
][state]
|
|
205
220
|
try {
|
|
206
221
|
handler?.(event)
|
package/src/ios-plugin.d.ts
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
+
declare class JoltReorderHandle extends UIButton {
|
|
2
|
+
onMove: ((direction: number) => void) | null
|
|
3
|
+
}
|
|
4
|
+
declare class JoltReorderSession extends NSObject {
|
|
5
|
+
static attachOptionsOnState(
|
|
6
|
+
list: UITableView,
|
|
7
|
+
options: string,
|
|
8
|
+
callback: (state: number, index: number, toIndex: number) => void,
|
|
9
|
+
): JoltReorderSession
|
|
10
|
+
readonly active: boolean
|
|
11
|
+
registerHandle(view: UIView): void
|
|
12
|
+
setEnabled(enabled: boolean): void
|
|
13
|
+
setReducedMotion(reduced: boolean): void
|
|
14
|
+
cancel(): void
|
|
15
|
+
dispose(): void
|
|
16
|
+
}
|
|
1
17
|
declare class JoltGestureSession extends NSObject {
|
|
18
|
+
static supportsContactEvents(): boolean
|
|
2
19
|
static attachScrollXYScaleRotationOptionsOnState(
|
|
3
20
|
surface: UIView,
|
|
4
21
|
scroll: UIScrollView | null,
|
|
@@ -180,3 +197,18 @@ declare class JoltReferenceProperties extends NSObject {
|
|
|
180
197
|
static sceneAppearanceSinkCount(): number
|
|
181
198
|
static sceneAppearanceWriteCount(): number
|
|
182
199
|
}
|
|
200
|
+
|
|
201
|
+
declare class JoltSceneShaderProperties extends NSObject {
|
|
202
|
+
static environmentError(
|
|
203
|
+
scene: NSObject,
|
|
204
|
+
error: interop.Reference<NSError>,
|
|
205
|
+
): JoltPropertySink | null
|
|
206
|
+
static uniformNameConfigurationError(
|
|
207
|
+
target: NSObject,
|
|
208
|
+
name: string,
|
|
209
|
+
configuration: string,
|
|
210
|
+
error: interop.Reference<NSError>,
|
|
211
|
+
): JoltPropertySink | null
|
|
212
|
+
static sinkCount(): number
|
|
213
|
+
static writeCount(): number
|
|
214
|
+
}
|
package/src/native/index.ts
CHANGED
|
@@ -22,3 +22,9 @@ export {
|
|
|
22
22
|
sceneKitMaterialDiffuse,
|
|
23
23
|
sceneKitMaterialEmission,
|
|
24
24
|
} from '../scene-appearance.js'
|
|
25
|
+
export { sceneKitEnvironmentIntensity, sceneKitUniform } from '../scene-shader.js'
|
|
26
|
+
export type {
|
|
27
|
+
SceneUniformType,
|
|
28
|
+
SceneUniformValue,
|
|
29
|
+
SceneUniformOptions,
|
|
30
|
+
} from '../scene-shader.js'
|
package/src/native-property.ts
CHANGED
|
@@ -56,10 +56,16 @@ const owners = new WeakMap<object, Map<string, Owner>>()
|
|
|
56
56
|
export function registerNativeProperty<T>(
|
|
57
57
|
definition: NativePropertyDefinition<T>,
|
|
58
58
|
): (target: object) => NativeProperty<T> {
|
|
59
|
-
|
|
60
|
-
throw new TypeError('Native property names must be namespaced: plugin/property.')
|
|
59
|
+
validateDefinition(definition)
|
|
61
60
|
if (definitions.has(definition.name))
|
|
62
61
|
throw new Error(`Native property "${definition.name}" is already registered.`)
|
|
62
|
+
definitions.add(definition.name)
|
|
63
|
+
return (target) => new PropertyTarget(target, definition)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateDefinition<T>(definition: NativePropertyDefinition<T>) {
|
|
67
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(definition.name))
|
|
68
|
+
throw new TypeError('Native property names must be namespaced: plugin/property.')
|
|
63
69
|
if (
|
|
64
70
|
!definition.units.length ||
|
|
65
71
|
definition.units.some((unit) => typeof unit !== 'string' || !unit)
|
|
@@ -76,8 +82,15 @@ export function registerNativeProperty<T>(
|
|
|
76
82
|
typeof definition.decode !== 'function'
|
|
77
83
|
)
|
|
78
84
|
throw new TypeError('A native property needs create(), encode() and decode().')
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Internal factory for per-target schemas; identity still shares the registry ownership map. */
|
|
88
|
+
export function nativePropertyTarget<T>(
|
|
89
|
+
target: object,
|
|
90
|
+
definition: NativePropertyDefinition<T>,
|
|
91
|
+
): NativeProperty<T> {
|
|
92
|
+
validateDefinition(definition)
|
|
93
|
+
return new PropertyTarget(target, definition)
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
class PropertyTarget<T> implements NativeProperty<T> {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Button, isIOS } from '@nativescript/core'
|
|
2
|
+
|
|
3
|
+
/** Focusable native handle: Alt/Option + ↑/↓ moves its current item. */
|
|
4
|
+
export class ReorderHandle extends Button {
|
|
5
|
+
override createNativeView(): ReturnType<Button['createNativeView']> {
|
|
6
|
+
if (isIOS) return JoltReorderHandle.new()
|
|
7
|
+
return new org.rafaelaugusto.jolt.ReorderHandle(this._context)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { Application, ListView, Trace, type View } from '@nativescript/core'
|
|
2
|
+
import { onReducedMotion, reducedMotion } from '../config.js'
|
|
3
|
+
import { coreListAdapter } from './list-view.js'
|
|
4
|
+
import { itemKeys, moveAt, sameKeys } from './model.js'
|
|
5
|
+
import type {
|
|
6
|
+
Reorder,
|
|
7
|
+
ReorderAdapter,
|
|
8
|
+
ReorderAdapterDriver,
|
|
9
|
+
ReorderCancelReason,
|
|
10
|
+
ReorderKey,
|
|
11
|
+
ReorderNativeEvent,
|
|
12
|
+
ReorderOptions,
|
|
13
|
+
} from './types.js'
|
|
14
|
+
|
|
15
|
+
export { coreListAdapter }
|
|
16
|
+
export { ReorderHandle } from './handle.js'
|
|
17
|
+
export { ReorderSurface } from './surface.js'
|
|
18
|
+
export type * from './types.js'
|
|
19
|
+
|
|
20
|
+
const owners = new WeakMap<View, { dispose(): void }>()
|
|
21
|
+
function report(callback: () => void) {
|
|
22
|
+
try {
|
|
23
|
+
callback()
|
|
24
|
+
} catch (error) {
|
|
25
|
+
Trace.write(
|
|
26
|
+
`Jolt reorder callback failed: ${String(error)}`,
|
|
27
|
+
Trace.categories.Error,
|
|
28
|
+
Trace.messageType.error,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reorder a flat, vertical Core ListView, or an explicitly supported adapter. */
|
|
34
|
+
export function reorder<T, K extends ReorderKey = ReorderKey>(
|
|
35
|
+
source: ListView | ReorderAdapter<T>,
|
|
36
|
+
options: ReorderOptions<T, K>,
|
|
37
|
+
): Reorder<K> {
|
|
38
|
+
if (
|
|
39
|
+
!options ||
|
|
40
|
+
typeof options.key !== 'function' ||
|
|
41
|
+
typeof options.onCommit !== 'function'
|
|
42
|
+
)
|
|
43
|
+
throw new TypeError('reorder() requires key(item) and onCommit(move).')
|
|
44
|
+
const adapter = source instanceof ListView ? coreListAdapter<T>(source) : source
|
|
45
|
+
if (
|
|
46
|
+
!adapter?.view ||
|
|
47
|
+
typeof adapter.items !== 'function' ||
|
|
48
|
+
typeof adapter.subscribe !== 'function' ||
|
|
49
|
+
typeof adapter.attach !== 'function'
|
|
50
|
+
)
|
|
51
|
+
throw new TypeError('Provide a Core ListView or a ReorderAdapter.')
|
|
52
|
+
for (const callback of [options.onStart, options.onCancel])
|
|
53
|
+
if (callback !== undefined && typeof callback !== 'function')
|
|
54
|
+
throw new TypeError('Reorder callbacks must be functions.')
|
|
55
|
+
if (
|
|
56
|
+
options.handle !== undefined &&
|
|
57
|
+
typeof options.handle !== 'string' &&
|
|
58
|
+
typeof options.handle !== 'function'
|
|
59
|
+
)
|
|
60
|
+
throw new TypeError('The reorder handle must be a view id or resolver.')
|
|
61
|
+
const longPress = options.longPress ?? 350,
|
|
62
|
+
edgeSize = options.edge?.size ?? 72,
|
|
63
|
+
edgeSpeed = options.edge?.speed ?? 640
|
|
64
|
+
if (!Number.isFinite(longPress) || longPress < 150 || longPress > 2000)
|
|
65
|
+
throw new RangeError('Reorder longPress must be between 150 and 2000 ms.')
|
|
66
|
+
if (![edgeSize, edgeSpeed].every((value) => Number.isFinite(value) && value > 0))
|
|
67
|
+
throw new RangeError('Reorder edge size and speed must be positive finite numbers.')
|
|
68
|
+
|
|
69
|
+
const view = adapter.view
|
|
70
|
+
let keys = itemKeys(adapter.items(), options.key)
|
|
71
|
+
owners.get(view)?.dispose()
|
|
72
|
+
let driver: ReorderAdapterDriver | undefined,
|
|
73
|
+
active: { key: K; index: number; keys: K[] } | undefined,
|
|
74
|
+
disposed = false,
|
|
75
|
+
paused = false
|
|
76
|
+
const read = () => itemKeys(adapter.items(), options.key)
|
|
77
|
+
|
|
78
|
+
function cancel(reason: ReorderCancelReason) {
|
|
79
|
+
const previous = active
|
|
80
|
+
active = undefined
|
|
81
|
+
driver?.cancel()
|
|
82
|
+
if (previous) report(() => options.onCancel?.({ key: previous.key, reason }))
|
|
83
|
+
}
|
|
84
|
+
function refresh() {
|
|
85
|
+
if (disposed) return
|
|
86
|
+
let next: K[]
|
|
87
|
+
try {
|
|
88
|
+
next = read()
|
|
89
|
+
} catch (error) {
|
|
90
|
+
cancel('data-changed')
|
|
91
|
+
throw error
|
|
92
|
+
}
|
|
93
|
+
if (active) cancel(next.includes(active.key) ? 'data-changed' : 'removed')
|
|
94
|
+
keys = next
|
|
95
|
+
}
|
|
96
|
+
function event(event: ReorderNativeEvent) {
|
|
97
|
+
if (disposed || paused) return
|
|
98
|
+
if (event.type === 'start') {
|
|
99
|
+
keys = read()
|
|
100
|
+
const key = keys[event.index]
|
|
101
|
+
if (key === undefined) {
|
|
102
|
+
driver?.cancel()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
active = { key, index: event.index, keys: [...keys] }
|
|
106
|
+
report(() => options.onStart?.(key))
|
|
107
|
+
} else if (event.type === 'cancel') {
|
|
108
|
+
cancel(event.geometryChanged ? 'geometry-changed' : 'cancelled')
|
|
109
|
+
} else {
|
|
110
|
+
const previous = active
|
|
111
|
+
if (!previous) return
|
|
112
|
+
const next = read()
|
|
113
|
+
if (!sameKeys(previous.keys, next) || event.index !== previous.index) {
|
|
114
|
+
cancel(next.includes(previous.key) ? 'data-changed' : 'removed')
|
|
115
|
+
keys = next
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
const move = moveAt(previous.keys, previous.index, event.toIndex)
|
|
119
|
+
active = undefined
|
|
120
|
+
if (move) report(() => options.onCommit(move))
|
|
121
|
+
else report(() => options.onCancel?.({ key: previous.key, reason: 'unchanged' }))
|
|
122
|
+
keys = read()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function moveTo(key: K, toIndex: number): boolean {
|
|
126
|
+
if (disposed || paused) return false
|
|
127
|
+
cancel('cancelled')
|
|
128
|
+
keys = read()
|
|
129
|
+
const move = moveAt(keys, keys.indexOf(key), toIndex)
|
|
130
|
+
if (!move) return false
|
|
131
|
+
report(() => options.onCommit(move))
|
|
132
|
+
keys = read()
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
function attach() {
|
|
136
|
+
if (disposed || driver || !view.isLoaded) return
|
|
137
|
+
keys = read()
|
|
138
|
+
driver = adapter.attach({
|
|
139
|
+
handle: options.handle,
|
|
140
|
+
longPress,
|
|
141
|
+
edgeSize,
|
|
142
|
+
edgeSpeed,
|
|
143
|
+
reducedMotion: reducedMotion(),
|
|
144
|
+
accessibility: {
|
|
145
|
+
moveUp: options.accessibility?.moveUp ?? 'Move up',
|
|
146
|
+
moveDown: options.accessibility?.moveDown ?? 'Move down',
|
|
147
|
+
},
|
|
148
|
+
event: (input) => {
|
|
149
|
+
try {
|
|
150
|
+
event(input)
|
|
151
|
+
} catch (error) {
|
|
152
|
+
cancel('data-changed')
|
|
153
|
+
report(() => {
|
|
154
|
+
throw error
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
move: (index, direction) => {
|
|
159
|
+
keys = read()
|
|
160
|
+
const key = keys[index]
|
|
161
|
+
return key !== undefined && moveTo(key, index + direction) ? key : undefined
|
|
162
|
+
},
|
|
163
|
+
keyAt: (index) => keys[index],
|
|
164
|
+
indexOf: (key) => {
|
|
165
|
+
try {
|
|
166
|
+
return read().indexOf(key as K)
|
|
167
|
+
} catch {
|
|
168
|
+
return -1
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
driver.setEnabled(!paused)
|
|
173
|
+
}
|
|
174
|
+
function unload() {
|
|
175
|
+
cancel('unloaded')
|
|
176
|
+
driver?.dispose()
|
|
177
|
+
driver = undefined
|
|
178
|
+
}
|
|
179
|
+
const suspend = () => cancel('cancelled')
|
|
180
|
+
const releaseItems = adapter.subscribe(refresh)
|
|
181
|
+
const releaseReduced = onReducedMotion((value) => driver?.setReducedMotion(value))
|
|
182
|
+
const result: Reorder<K> = {
|
|
183
|
+
get activeKey() {
|
|
184
|
+
return active?.key
|
|
185
|
+
},
|
|
186
|
+
get disposed() {
|
|
187
|
+
return disposed
|
|
188
|
+
},
|
|
189
|
+
move(key, beforeKey) {
|
|
190
|
+
if (disposed || paused) return false
|
|
191
|
+
const current = read(),
|
|
192
|
+
from = current.indexOf(key)
|
|
193
|
+
if (from < 0 || beforeKey === key) return false
|
|
194
|
+
const before = beforeKey === null ? current.length : current.indexOf(beforeKey)
|
|
195
|
+
return before < 0 ? false : moveTo(key, before > from ? before - 1 : before)
|
|
196
|
+
},
|
|
197
|
+
moveUp(key) {
|
|
198
|
+
if (disposed || paused) return false
|
|
199
|
+
return moveTo(key, read().indexOf(key) - 1)
|
|
200
|
+
},
|
|
201
|
+
moveDown(key) {
|
|
202
|
+
if (disposed || paused) return false
|
|
203
|
+
return moveTo(key, read().indexOf(key) + 1)
|
|
204
|
+
},
|
|
205
|
+
cancel: () => cancel('cancelled'),
|
|
206
|
+
refresh,
|
|
207
|
+
pause() {
|
|
208
|
+
paused = true
|
|
209
|
+
cancel('disabled')
|
|
210
|
+
driver?.setEnabled(false)
|
|
211
|
+
},
|
|
212
|
+
play() {
|
|
213
|
+
if (!disposed) {
|
|
214
|
+
paused = false
|
|
215
|
+
driver?.setEnabled(true)
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
dispose() {
|
|
219
|
+
if (disposed) return
|
|
220
|
+
cancel('disposed')
|
|
221
|
+
disposed = true
|
|
222
|
+
driver?.dispose()
|
|
223
|
+
driver = undefined
|
|
224
|
+
releaseItems()
|
|
225
|
+
releaseReduced()
|
|
226
|
+
view.off('loaded', attach)
|
|
227
|
+
view.off('unloaded', unload)
|
|
228
|
+
Application.off(Application.suspendEvent, suspend)
|
|
229
|
+
if (owners.get(view) === result) owners.delete(view)
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
owners.set(view, result)
|
|
233
|
+
view.on('loaded', attach)
|
|
234
|
+
view.on('unloaded', unload)
|
|
235
|
+
Application.on(Application.suspendEvent, suspend)
|
|
236
|
+
try {
|
|
237
|
+
attach()
|
|
238
|
+
} catch (error) {
|
|
239
|
+
result.dispose()
|
|
240
|
+
throw error
|
|
241
|
+
}
|
|
242
|
+
return result
|
|
243
|
+
}
|