@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.
Files changed (84) hide show
  1. package/README.md +7 -2
  2. package/dist/bottom-sheet/geometry.d.ts +18 -0
  3. package/dist/bottom-sheet/geometry.d.ts.map +1 -0
  4. package/dist/bottom-sheet/geometry.js +51 -0
  5. package/dist/bottom-sheet/geometry.js.map +1 -0
  6. package/dist/bottom-sheet/index.d.ts +56 -0
  7. package/dist/bottom-sheet/index.d.ts.map +1 -0
  8. package/dist/bottom-sheet/index.js +462 -0
  9. package/dist/bottom-sheet/index.js.map +1 -0
  10. package/dist/bottom-sheet/native.d.ts +11 -0
  11. package/dist/bottom-sheet/native.d.ts.map +1 -0
  12. package/dist/bottom-sheet/native.js +173 -0
  13. package/dist/bottom-sheet/native.js.map +1 -0
  14. package/dist/gesture-native.d.ts +1 -0
  15. package/dist/gesture-native.d.ts.map +1 -1
  16. package/dist/gesture-native.js +6 -0
  17. package/dist/gesture-native.js.map +1 -1
  18. package/dist/gestures/index.d.ts +4 -0
  19. package/dist/gestures/index.d.ts.map +1 -1
  20. package/dist/gestures/index.js +12 -1
  21. package/dist/gestures/index.js.map +1 -1
  22. package/dist/native/index.d.ts +2 -0
  23. package/dist/native/index.d.ts.map +1 -1
  24. package/dist/native/index.js +1 -0
  25. package/dist/native/index.js.map +1 -1
  26. package/dist/native-property.d.ts +2 -0
  27. package/dist/native-property.d.ts.map +1 -1
  28. package/dist/native-property.js +12 -4
  29. package/dist/native-property.js.map +1 -1
  30. package/dist/reorder/handle.d.ts +6 -0
  31. package/dist/reorder/handle.d.ts.map +1 -0
  32. package/dist/reorder/handle.js +10 -0
  33. package/dist/reorder/handle.js.map +1 -0
  34. package/dist/reorder/index.d.ts +10 -0
  35. package/dist/reorder/index.d.ts.map +1 -0
  36. package/dist/reorder/index.js +233 -0
  37. package/dist/reorder/index.js.map +1 -0
  38. package/dist/reorder/list-view.d.ts +5 -0
  39. package/dist/reorder/list-view.d.ts.map +1 -0
  40. package/dist/reorder/list-view.js +238 -0
  41. package/dist/reorder/list-view.js.map +1 -0
  42. package/dist/reorder/model.d.ts +6 -0
  43. package/dist/reorder/model.d.ts.map +1 -0
  44. package/dist/reorder/model.js +30 -0
  45. package/dist/reorder/model.js.map +1 -0
  46. package/dist/reorder/surface.d.ts +6 -0
  47. package/dist/reorder/surface.d.ts.map +1 -0
  48. package/dist/reorder/surface.js +10 -0
  49. package/dist/reorder/surface.js.map +1 -0
  50. package/dist/reorder/types.d.ts +92 -0
  51. package/dist/reorder/types.d.ts.map +1 -0
  52. package/dist/reorder/types.js +2 -0
  53. package/dist/reorder/types.js.map +1 -0
  54. package/dist/scene-shader.d.ts +18 -0
  55. package/dist/scene-shader.d.ts.map +1 -0
  56. package/dist/scene-shader.js +76 -0
  57. package/dist/scene-shader.js.map +1 -0
  58. package/package.json +9 -1
  59. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +7 -2
  60. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
  61. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
  62. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
  63. package/platforms/android/proguard-rules.pro +5 -0
  64. package/platforms/ios/src/JoltGestureSession.h +1 -0
  65. package/platforms/ios/src/JoltGestureSession.m +7 -2
  66. package/platforms/ios/src/JoltReorderSession.h +15 -0
  67. package/platforms/ios/src/JoltReorderSession.m +205 -0
  68. package/platforms/ios/src/module.modulemap +1 -0
  69. package/src/android-plugin.d.ts +34 -0
  70. package/src/bottom-sheet/geometry.ts +84 -0
  71. package/src/bottom-sheet/index.ts +536 -0
  72. package/src/bottom-sheet/native.ts +214 -0
  73. package/src/gesture-native.ts +15 -0
  74. package/src/gestures/index.ts +16 -1
  75. package/src/ios-plugin.d.ts +32 -0
  76. package/src/native/index.ts +6 -0
  77. package/src/native-property.ts +17 -4
  78. package/src/reorder/handle.ts +9 -0
  79. package/src/reorder/index.ts +243 -0
  80. package/src/reorder/list-view.ts +262 -0
  81. package/src/reorder/model.ts +47 -0
  82. package/src/reorder/surface.ts +10 -0
  83. package/src/reorder/types.ts +87 -0
  84. 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
+ }
@@ -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,
@@ -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 ['onStart', 'onEnd', 'onCancel', 'onTap', 'onDoubleTap'] as const)
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)
@@ -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
+ }
@@ -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'
@@ -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
- if (!/^[\w.-]+\/[\w.-]+$/.test(definition.name))
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
- definitions.add(definition.name)
80
- return (target) => new PropertyTarget(target, definition)
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
+ }