@rafaelaugusto/jolt 0.1.0-beta.1 → 0.1.0-beta.10

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 (302) hide show
  1. package/README.md +367 -18
  2. package/dist/animate.d.ts +19 -3
  3. package/dist/animate.d.ts.map +1 -1
  4. package/dist/animate.js +186 -14
  5. package/dist/animate.js.map +1 -1
  6. package/dist/color.d.ts +11 -0
  7. package/dist/color.d.ts.map +1 -0
  8. package/dist/color.js +73 -0
  9. package/dist/color.js.map +1 -0
  10. package/dist/config.d.ts +31 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +73 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/decay.d.ts +11 -0
  15. package/dist/decay.d.ts.map +1 -0
  16. package/dist/decay.js +22 -0
  17. package/dist/decay.js.map +1 -0
  18. package/dist/decoration.d.ts +18 -0
  19. package/dist/decoration.d.ts.map +1 -0
  20. package/dist/decoration.js +155 -0
  21. package/dist/decoration.js.map +1 -0
  22. package/dist/drag-input.d.ts +15 -0
  23. package/dist/drag-input.d.ts.map +1 -0
  24. package/dist/drag-input.js +66 -0
  25. package/dist/drag-input.js.map +1 -0
  26. package/dist/drag-physics.d.ts +20 -0
  27. package/dist/drag-physics.d.ts.map +1 -0
  28. package/dist/drag-physics.js +75 -0
  29. package/dist/drag-physics.js.map +1 -0
  30. package/dist/drag.d.ts +58 -0
  31. package/dist/drag.d.ts.map +1 -0
  32. package/dist/drag.js +324 -0
  33. package/dist/drag.js.map +1 -0
  34. package/dist/driver/android-color.d.ts +20 -0
  35. package/dist/driver/android-color.d.ts.map +1 -0
  36. package/dist/driver/android-color.js +136 -0
  37. package/dist/driver/android-color.js.map +1 -0
  38. package/dist/driver/android-decoration.d.ts +22 -0
  39. package/dist/driver/android-decoration.d.ts.map +1 -0
  40. package/dist/driver/android-decoration.js +139 -0
  41. package/dist/driver/android-decoration.js.map +1 -0
  42. package/dist/driver/android-playback.d.ts +13 -0
  43. package/dist/driver/android-playback.d.ts.map +1 -0
  44. package/dist/driver/android-playback.js +141 -0
  45. package/dist/driver/android-playback.js.map +1 -0
  46. package/dist/driver/android.d.ts +1 -1
  47. package/dist/driver/android.d.ts.map +1 -1
  48. package/dist/driver/android.js +165 -13
  49. package/dist/driver/android.js.map +1 -1
  50. package/dist/driver/index.d.ts +25 -2
  51. package/dist/driver/index.d.ts.map +1 -1
  52. package/dist/driver/index.js.map +1 -1
  53. package/dist/driver/ios-color.d.ts +16 -0
  54. package/dist/driver/ios-color.d.ts.map +1 -0
  55. package/dist/driver/ios-color.js +392 -0
  56. package/dist/driver/ios-color.js.map +1 -0
  57. package/dist/driver/ios-decoration.d.ts +17 -0
  58. package/dist/driver/ios-decoration.d.ts.map +1 -0
  59. package/dist/driver/ios-decoration.js +271 -0
  60. package/dist/driver/ios-decoration.js.map +1 -0
  61. package/dist/driver/ios-layout.d.ts +9 -0
  62. package/dist/driver/ios-layout.d.ts.map +1 -0
  63. package/dist/driver/ios-layout.js +52 -0
  64. package/dist/driver/ios-layout.js.map +1 -0
  65. package/dist/driver/ios-playback.d.ts +10 -0
  66. package/dist/driver/ios-playback.d.ts.map +1 -0
  67. package/dist/driver/ios-playback.js +47 -0
  68. package/dist/driver/ios-playback.js.map +1 -0
  69. package/dist/driver/ios-shadow.d.ts +6 -0
  70. package/dist/driver/ios-shadow.d.ts.map +1 -0
  71. package/dist/driver/ios-shadow.js +52 -0
  72. package/dist/driver/ios-shadow.js.map +1 -0
  73. package/dist/driver/ios.d.ts.map +1 -1
  74. package/dist/driver/ios.js +163 -28
  75. package/dist/driver/ios.js.map +1 -1
  76. package/dist/engine.d.ts +75 -5
  77. package/dist/engine.d.ts.map +1 -1
  78. package/dist/engine.js +293 -23
  79. package/dist/engine.js.map +1 -1
  80. package/dist/haptics.d.ts +21 -0
  81. package/dist/haptics.d.ts.map +1 -0
  82. package/dist/haptics.js +120 -0
  83. package/dist/haptics.js.map +1 -0
  84. package/dist/image-effect-native.d.ts +26 -0
  85. package/dist/image-effect-native.d.ts.map +1 -0
  86. package/dist/image-effect-native.js +73 -0
  87. package/dist/image-effect-native.js.map +1 -0
  88. package/dist/image-effect.d.ts +50 -0
  89. package/dist/image-effect.d.ts.map +1 -0
  90. package/dist/image-effect.js +290 -0
  91. package/dist/image-effect.js.map +1 -0
  92. package/dist/image-owner.d.ts +3 -0
  93. package/dist/image-owner.d.ts.map +1 -0
  94. package/dist/image-owner.js +11 -0
  95. package/dist/image-owner.js.map +1 -0
  96. package/dist/index.d.ts +43 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +20 -1
  99. package/dist/index.js.map +1 -1
  100. package/dist/interaction-surface.d.ts +12 -0
  101. package/dist/interaction-surface.d.ts.map +1 -0
  102. package/dist/interaction-surface.js +36 -0
  103. package/dist/interaction-surface.js.map +1 -0
  104. package/dist/items.d.ts +20 -0
  105. package/dist/items.d.ts.map +1 -0
  106. package/dist/items.js +109 -0
  107. package/dist/items.js.map +1 -0
  108. package/dist/keyboard.d.ts +41 -0
  109. package/dist/keyboard.d.ts.map +1 -0
  110. package/dist/keyboard.js +256 -0
  111. package/dist/keyboard.js.map +1 -0
  112. package/dist/label.d.ts +7 -0
  113. package/dist/label.d.ts.map +1 -0
  114. package/dist/label.js +60 -0
  115. package/dist/label.js.map +1 -0
  116. package/dist/layout.d.ts +13 -0
  117. package/dist/layout.d.ts.map +1 -0
  118. package/dist/layout.js +348 -0
  119. package/dist/layout.js.map +1 -0
  120. package/dist/liquid-grid.d.ts +3 -0
  121. package/dist/liquid-grid.d.ts.map +1 -0
  122. package/dist/liquid-grid.js +24 -0
  123. package/dist/liquid-grid.js.map +1 -0
  124. package/dist/liquid.d.ts +18 -0
  125. package/dist/liquid.d.ts.map +1 -0
  126. package/dist/liquid.js +234 -0
  127. package/dist/liquid.js.map +1 -0
  128. package/dist/loader.d.ts +19 -0
  129. package/dist/loader.d.ts.map +1 -0
  130. package/dist/loader.js +173 -0
  131. package/dist/loader.js.map +1 -0
  132. package/dist/motion-scope.d.ts +39 -0
  133. package/dist/motion-scope.d.ts.map +1 -0
  134. package/dist/motion-scope.js +238 -0
  135. package/dist/motion-scope.js.map +1 -0
  136. package/dist/motion-states.d.ts +36 -0
  137. package/dist/motion-states.d.ts.map +1 -0
  138. package/dist/motion-states.js +165 -0
  139. package/dist/motion-states.js.map +1 -0
  140. package/dist/particles.d.ts +39 -0
  141. package/dist/particles.d.ts.map +1 -0
  142. package/dist/particles.js +265 -0
  143. package/dist/particles.js.map +1 -0
  144. package/dist/path.d.ts +12 -0
  145. package/dist/path.d.ts.map +1 -0
  146. package/dist/path.js +90 -0
  147. package/dist/path.js.map +1 -0
  148. package/dist/playback-group.d.ts +6 -0
  149. package/dist/playback-group.d.ts.map +1 -0
  150. package/dist/playback-group.js +141 -0
  151. package/dist/playback-group.js.map +1 -0
  152. package/dist/playback.d.ts +25 -0
  153. package/dist/playback.d.ts.map +1 -0
  154. package/dist/playback.js +35 -0
  155. package/dist/playback.js.map +1 -0
  156. package/dist/press.d.ts.map +1 -1
  157. package/dist/press.js +23 -7
  158. package/dist/press.js.map +1 -1
  159. package/dist/properties.d.ts +6 -3
  160. package/dist/properties.d.ts.map +1 -1
  161. package/dist/properties.js +26 -2
  162. package/dist/properties.js.map +1 -1
  163. package/dist/reveal.d.ts +25 -0
  164. package/dist/reveal.d.ts.map +1 -0
  165. package/dist/reveal.js +294 -0
  166. package/dist/reveal.js.map +1 -0
  167. package/dist/roll-diff.d.ts +22 -0
  168. package/dist/roll-diff.d.ts.map +1 -0
  169. package/dist/roll-diff.js +36 -0
  170. package/dist/roll-diff.js.map +1 -0
  171. package/dist/roll.d.ts +29 -0
  172. package/dist/roll.d.ts.map +1 -0
  173. package/dist/roll.js +442 -0
  174. package/dist/roll.js.map +1 -0
  175. package/dist/scroll-linked.d.ts +10 -0
  176. package/dist/scroll-linked.d.ts.map +1 -0
  177. package/dist/scroll-linked.js +58 -0
  178. package/dist/scroll-linked.js.map +1 -0
  179. package/dist/sequence.d.ts +10 -5
  180. package/dist/sequence.d.ts.map +1 -1
  181. package/dist/sequence.js +208 -14
  182. package/dist/sequence.js.map +1 -1
  183. package/dist/shimmer.d.ts +12 -0
  184. package/dist/shimmer.d.ts.map +1 -0
  185. package/dist/shimmer.js +161 -0
  186. package/dist/shimmer.js.map +1 -0
  187. package/dist/svg-path.d.ts +14 -0
  188. package/dist/svg-path.d.ts.map +1 -0
  189. package/dist/svg-path.js +281 -0
  190. package/dist/svg-path.js.map +1 -0
  191. package/dist/symbol-effect.d.ts +23 -0
  192. package/dist/symbol-effect.d.ts.map +1 -0
  193. package/dist/symbol-effect.js +187 -0
  194. package/dist/symbol-effect.js.map +1 -0
  195. package/dist/text-layout.d.ts +30 -0
  196. package/dist/text-layout.d.ts.map +1 -0
  197. package/dist/text-layout.js +185 -0
  198. package/dist/text-layout.js.map +1 -0
  199. package/dist/text.d.ts +17 -0
  200. package/dist/text.d.ts.map +1 -0
  201. package/dist/text.js +263 -0
  202. package/dist/text.js.map +1 -0
  203. package/dist/track.d.ts +3 -1
  204. package/dist/track.d.ts.map +1 -1
  205. package/dist/track.js +266 -13
  206. package/dist/track.js.map +1 -1
  207. package/dist/transition.d.ts +17 -1
  208. package/dist/transition.d.ts.map +1 -1
  209. package/dist/transition.js +41 -6
  210. package/dist/transition.js.map +1 -1
  211. package/dist/types.d.ts +48 -1
  212. package/dist/types.d.ts.map +1 -1
  213. package/dist/vue/index.d.ts +95 -0
  214. package/dist/vue/index.d.ts.map +1 -0
  215. package/dist/vue/index.js +163 -0
  216. package/dist/vue/index.js.map +1 -0
  217. package/dist/zoom-native.d.ts +30 -0
  218. package/dist/zoom-native.d.ts.map +1 -0
  219. package/dist/zoom-native.js +56 -0
  220. package/dist/zoom-native.js.map +1 -0
  221. package/dist/zoomable.d.ts +48 -0
  222. package/dist/zoomable.d.ts.map +1 -0
  223. package/dist/zoomable.js +249 -0
  224. package/dist/zoomable.js.map +1 -0
  225. package/package.json +29 -5
  226. package/platforms/android/include.gradle +7 -0
  227. package/platforms/android/java/org/rafaelaugusto/jolt/ColorTarget.java +102 -0
  228. package/platforms/android/java/org/rafaelaugusto/jolt/DecorationTarget.java +154 -0
  229. package/platforms/android/java/org/rafaelaugusto/jolt/ImageEffect.java +288 -0
  230. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardFollower.java +91 -0
  231. package/platforms/android/java/org/rafaelaugusto/jolt/KeyboardObserver.java +97 -0
  232. package/platforms/android/java/org/rafaelaugusto/jolt/NumberRoll.java +190 -0
  233. package/platforms/android/java/org/rafaelaugusto/jolt/PhotoZoom.java +250 -0
  234. package/platforms/android/java/org/rafaelaugusto/jolt/TextReveal.java +180 -0
  235. package/platforms/android/proguard-rules.pro +19 -0
  236. package/platforms/ios/build.xcconfig +1 -0
  237. package/platforms/ios/src/JoltImageEffect.h +25 -0
  238. package/platforms/ios/src/JoltImageEffect.mm +242 -0
  239. package/platforms/ios/src/JoltInteractiveAnimation.h +18 -0
  240. package/platforms/ios/src/JoltInteractiveAnimation.m +123 -0
  241. package/platforms/ios/src/JoltPhotoZoom.h +23 -0
  242. package/platforms/ios/src/JoltPhotoZoom.m +229 -0
  243. package/platforms/ios/src/module.modulemap +6 -0
  244. package/src/android-plugin.d.ts +155 -0
  245. package/src/animate.ts +249 -36
  246. package/src/color.ts +101 -0
  247. package/src/config.ts +84 -0
  248. package/src/decay.ts +45 -0
  249. package/src/decoration.ts +180 -0
  250. package/src/drag-input.ts +74 -0
  251. package/src/drag-physics.ts +110 -0
  252. package/src/drag.ts +395 -0
  253. package/src/driver/android-color.ts +170 -0
  254. package/src/driver/android-decoration.ts +182 -0
  255. package/src/driver/android-playback.ts +157 -0
  256. package/src/driver/android.ts +208 -20
  257. package/src/driver/index.ts +37 -2
  258. package/src/driver/ios-color.ts +441 -0
  259. package/src/driver/ios-decoration.ts +332 -0
  260. package/src/driver/ios-layout.ts +63 -0
  261. package/src/driver/ios-playback.ts +51 -0
  262. package/src/driver/ios-shadow.ts +50 -0
  263. package/src/driver/ios.ts +193 -33
  264. package/src/engine.ts +321 -27
  265. package/src/haptics.ts +148 -0
  266. package/src/image-effect-native.ts +129 -0
  267. package/src/image-effect.ts +336 -0
  268. package/src/image-owner.ts +12 -0
  269. package/src/index.ts +59 -2
  270. package/src/interaction-surface.ts +41 -0
  271. package/src/ios-plugin.d.ts +81 -0
  272. package/src/items.ts +140 -0
  273. package/src/keyboard.ts +291 -0
  274. package/src/label.ts +76 -0
  275. package/src/layout.ts +408 -0
  276. package/src/liquid-grid.ts +28 -0
  277. package/src/liquid.ts +287 -0
  278. package/src/loader.ts +202 -0
  279. package/src/motion-scope.ts +279 -0
  280. package/src/motion-states.ts +215 -0
  281. package/src/particles.ts +323 -0
  282. package/src/path.ts +124 -0
  283. package/src/playback-group.ts +143 -0
  284. package/src/playback.ts +62 -0
  285. package/src/press.ts +26 -10
  286. package/src/properties.ts +32 -6
  287. package/src/reveal.ts +374 -0
  288. package/src/roll-diff.ts +57 -0
  289. package/src/roll.ts +515 -0
  290. package/src/scroll-linked.ts +73 -0
  291. package/src/sequence.ts +204 -14
  292. package/src/shimmer.ts +180 -0
  293. package/src/svg-path.ts +355 -0
  294. package/src/symbol-effect.ts +226 -0
  295. package/src/text-layout.ts +228 -0
  296. package/src/text.ts +279 -0
  297. package/src/track.ts +347 -12
  298. package/src/transition.ts +63 -6
  299. package/src/types.ts +49 -1
  300. package/src/vue/index.ts +231 -0
  301. package/src/zoom-native.ts +124 -0
  302. package/src/zoomable.ts +330 -0
package/src/items.ts ADDED
@@ -0,0 +1,140 @@
1
+ import { ListView, View, type EventData, type ItemEventData } from '@nativescript/core'
2
+ import { animate, endingValues, startingValues, toView } from './animate.js'
3
+ import { getDriver } from './driver/index.js'
4
+ import { stopView } from './engine.js'
5
+ import type { PresetName } from './presets.js'
6
+ import { writeValue } from './properties.js'
7
+ import type { SpringName } from './transition.js'
8
+ import type { AnimateOptions, Keyframes, Target } from './types.js'
9
+
10
+ export type ItemsOptions = AnimateOptions & {
11
+ /** Animate each data item only the first time it shows up. Defaults to false. */
12
+ once?: boolean
13
+ }
14
+
15
+ interface Pending {
16
+ index: number
17
+ section: number | undefined
18
+ item: unknown
19
+ key: string
20
+ }
21
+
22
+ type Row = View & {
23
+ bindingContext?: unknown
24
+ _listViewItemIndex?: number
25
+ _listViewSectionIndex?: number
26
+ }
27
+
28
+ interface Source extends View {
29
+ items?: unknown
30
+ _getDataItem?(index: number): unknown
31
+ _getDataItemInSection?(section: number, index: number): unknown
32
+ }
33
+
34
+ /**
35
+ * Plays an animation on each row of a list as it comes on screen. Rows are
36
+ * recycled, so a row that is reused for another item starts over, or settles
37
+ * at the end values when that item was already shown and `once` is set.
38
+ * Returns a function that removes the effect.
39
+ *
40
+ * ```ts
41
+ * animateItems(listView, 'fadeInUp')
42
+ * animateItems(listView, 'fadeInUp', { once: true })
43
+ * ```
44
+ */
45
+ export function animateItems(
46
+ list: Target,
47
+ animation: Keyframes | PresetName,
48
+ options?: ItemsOptions | SpringName,
49
+ ): () => void {
50
+ const source = toView(list) as Source
51
+ const { once = false, ...rest } = typeof options === 'object' ? options : {}
52
+ if (rest.stagger !== undefined)
53
+ throw new TypeError('animateItems animates one row at a time; stagger has no effect.')
54
+ const transition: AnimateOptions | SpringName | undefined =
55
+ typeof options === 'string' ? options : rest
56
+ const properties = [...startingValues(animation, transition).keys()]
57
+ const ends = endingValues(animation, transition)
58
+
59
+ const rows = new Set<Row>()
60
+ const seenItems = new WeakSet<object>()
61
+ const seenKeys = new Set<string>()
62
+ const pending: Pending[] = []
63
+ let queued = false
64
+ let disposed = false
65
+
66
+ const settle = (row: Row) => {
67
+ stopView(row, properties)
68
+ for (const [property, value] of ends)
69
+ if (value !== null) writeValue(row, property, value)
70
+ getDriver().sync?.(row)
71
+ }
72
+ const show = (row: Row, { item, key }: Pending) => {
73
+ rows.add(row)
74
+ const tracked = typeof item === 'object' && item !== null
75
+ const seen = tracked ? seenItems.has(item) : seenKeys.has(key)
76
+ if (tracked) seenItems.add(item)
77
+ else seenKeys.add(key)
78
+ if (once && seen) settle(row)
79
+ else {
80
+ stopView(row, properties)
81
+ animate(row, animation, transition)
82
+ }
83
+ }
84
+ const flush = () => {
85
+ queued = false
86
+ if (disposed) return
87
+ const current = new Set<Row>()
88
+ const fresh: Row[] = []
89
+ source.eachChildView((child: Row) => {
90
+ current.add(child)
91
+ if (!rows.has(child)) fresh.push(child)
92
+ return true
93
+ })
94
+ for (const row of rows) if (!current.has(row)) rows.delete(row)
95
+ for (const entry of pending.splice(0)) {
96
+ const row =
97
+ fresh.find(
98
+ (candidate) =>
99
+ candidate._listViewItemIndex === entry.index &&
100
+ candidate._listViewSectionIndex === entry.section,
101
+ ) ??
102
+ fresh.find(
103
+ (candidate) =>
104
+ entry.item !== undefined && candidate.bindingContext === entry.item,
105
+ )
106
+ if (!row) continue
107
+ fresh.splice(fresh.indexOf(row), 1)
108
+ show(row, entry)
109
+ }
110
+ }
111
+ const onItemLoading = (event: EventData) => {
112
+ const { index, section, view } = event as ItemEventData
113
+ const item =
114
+ section === undefined
115
+ ? source._getDataItem?.(index)
116
+ : source._getDataItemInSection?.(section, index)
117
+ const entry: Pending = { index, section, item, key: `${section ?? ''}:${index}` }
118
+ if (view instanceof View) {
119
+ show(view, entry)
120
+ return
121
+ }
122
+ pending.push(entry)
123
+ if (queued) return
124
+ queued = true
125
+ void Promise.resolve().then(flush)
126
+ }
127
+ const cleanup = () => {
128
+ if (disposed) return
129
+ disposed = true
130
+ source.off(ListView.itemLoadingEvent, onItemLoading)
131
+ source.off('unloaded', cleanup)
132
+ pending.length = 0
133
+ for (const row of rows) settle(row)
134
+ rows.clear()
135
+ }
136
+
137
+ source.on(ListView.itemLoadingEvent, onItemLoading)
138
+ source.on('unloaded', cleanup)
139
+ return cleanup
140
+ }
@@ -0,0 +1,291 @@
1
+ import { Application, Utils, isAndroid, isIOS, type View } from '@nativescript/core'
2
+ import { stop, toView } from './animate.js'
3
+ import type { EasingFunction, EasingName } from './easing.js'
4
+ import type { Spring, Transition } from './transition.js'
5
+ import type { Target } from './types.js'
6
+
7
+ export interface KeyboardOptions {
8
+ /** DIPs kept between the view's bottom edge and the keyboard. Defaults to zero. */
9
+ offset?: number
10
+ }
11
+
12
+ export interface KeyboardState {
13
+ /** Whether a docked software keyboard covers part of the window. */
14
+ readonly visible: boolean
15
+ /** DIPs of the window the keyboard covers, from its bottom edge. Zero when hidden or floating. */
16
+ readonly height: number
17
+ /** Milliseconds the keyboard takes to get there. Zero when it changes at once. */
18
+ readonly duration: number
19
+ /** The keyboard's own timing, to pass to `animate()` and move in step with it. */
20
+ readonly transition: Transition
21
+ }
22
+
23
+ const BEGIN_FROM_CURRENT_STATE = 4
24
+ const IOS_KEYBOARD_CURVE = 7
25
+ // Measured: Core Animation plays the keyboard's spring (stiffness 1000, mass 3) critically damped in 500 ms.
26
+ const iosKeyboard: Spring = {
27
+ type: 'spring',
28
+ stiffness: 1000,
29
+ mass: 3,
30
+ damping: 2 * Math.sqrt(3000),
31
+ }
32
+ const iosCurves: readonly EasingName[] = ['easeInOut', 'easeIn', 'easeOut', 'linear']
33
+ const hidden: KeyboardState = Object.freeze({
34
+ visible: false,
35
+ height: 0,
36
+ duration: 0,
37
+ transition: { duration: 0 },
38
+ })
39
+
40
+ const listeners = new Set<(keyboard: KeyboardState) => void>()
41
+ let state = hidden
42
+ let unwatch: (() => void) | undefined
43
+
44
+ /**
45
+ * Calls back whenever the keyboard shows, hides or changes height, with how much
46
+ * of the window it covers and its timing. Returns a cleanup function.
47
+ *
48
+ * ```ts
49
+ * const off = onKeyboard(({ visible, transition }) => {
50
+ * animate(header, { scale: visible ? 0.8 : 1 }, transition)
51
+ * })
52
+ * ```
53
+ */
54
+ export function onKeyboard(callback: (keyboard: KeyboardState) => void): () => void {
55
+ if (typeof callback !== 'function')
56
+ throw new TypeError('onKeyboard() takes a callback.')
57
+ const listener = (keyboard: KeyboardState) => callback(keyboard)
58
+ if (listeners.size === 0) {
59
+ state = hidden
60
+ unwatch = isIOS ? watchIOS() : isAndroid ? watchAndroid() : undefined
61
+ }
62
+ listeners.add(listener)
63
+ return () => {
64
+ if (!listeners.delete(listener) || listeners.size > 0) return
65
+ unwatch?.()
66
+ unwatch = undefined
67
+ }
68
+ }
69
+
70
+ /** The last state reported while someone listens. */
71
+ export function currentKeyboard(): KeyboardState {
72
+ return state
73
+ }
74
+
75
+ function update(height: number, duration: number, transition: Transition) {
76
+ if (height === state.height) return
77
+ state = Object.freeze({ visible: height > 0, height, duration, transition })
78
+ for (const listener of [...listeners]) listener(state)
79
+ }
80
+
81
+ function watchIOS() {
82
+ const name = UIKeyboardWillChangeFrameNotification
83
+ const observer = Application.ios.addNotificationObserver(
84
+ name,
85
+ (notification: NSNotification) => {
86
+ const info = notification.userInfo
87
+ const window = Application.ios.window
88
+ const end = info?.objectForKey(UIKeyboardFrameEndUserInfoKey)?.CGRectValue as
89
+ CGRect | undefined
90
+ if (!info || !window || !end) return
91
+ const frame = window.convertRectFromWindow(end, null)
92
+ const bottom = window.bounds.size.height
93
+ const docked =
94
+ frame.size.height > 0 &&
95
+ frame.origin.y < bottom &&
96
+ frame.origin.y + frame.size.height >= bottom - 0.5
97
+ const reported = numberIn(info, UIKeyboardAnimationDurationUserInfoKey) * 1000
98
+ const curve = numberIn(info, UIKeyboardAnimationCurveUserInfoKey)
99
+ const height = docked ? bottom - frame.origin.y : 0
100
+ if (!reported) return update(height, 0, { duration: 0 })
101
+ if (curve === IOS_KEYBOARD_CURVE) return update(height, 500, iosKeyboard)
102
+ update(height, reported, {
103
+ duration: reported,
104
+ ease: iosCurves[curve] ?? 'easeInOut',
105
+ })
106
+ },
107
+ )
108
+ return () => Application.ios.removeNotificationObserver(observer, name)
109
+ }
110
+
111
+ function watchAndroid() {
112
+ if (android.os.Build.VERSION.SDK_INT < 30) return undefined
113
+ const density = Utils.layout.getDisplayDensity()
114
+ let observer: org.rafaelaugusto.jolt.KeyboardObserver | undefined
115
+ let stopped = false
116
+ const attach = () => {
117
+ Application.off(Application.launchEvent, attach)
118
+ if (stopped || observer) return
119
+ const activity =
120
+ Application.android.foregroundActivity ?? Application.android.startActivity
121
+ const root = activity?.getWindow()?.getDecorView()
122
+ if (!root) return
123
+ try {
124
+ observer = new org.rafaelaugusto.jolt.KeyboardObserver(
125
+ root,
126
+ new org.rafaelaugusto.jolt.KeyboardObserver.Listener({
127
+ onChange: (height, duration, curve) =>
128
+ update(
129
+ height / density,
130
+ duration,
131
+ duration
132
+ ? { duration, ease: curve ? sampled(curve) : 'linear' }
133
+ : { duration: 0 },
134
+ ),
135
+ }),
136
+ )
137
+ } catch {
138
+ // A JS-only update cannot install the native plugin.
139
+ return
140
+ }
141
+ observer.attach()
142
+ const height = observer.height() / density
143
+ if (height > 0) state = Object.freeze({ ...hidden, visible: true, height })
144
+ }
145
+ if (Application.android.foregroundActivity ?? Application.android.startActivity)
146
+ attach()
147
+ else Application.on(Application.launchEvent, attach)
148
+ return () => {
149
+ stopped = true
150
+ Application.off(Application.launchEvent, attach)
151
+ observer?.detach()
152
+ observer = undefined
153
+ }
154
+ }
155
+
156
+ function sampled(curve: ArrayLike<number>): EasingFunction {
157
+ const values = Array.from({ length: curve.length }, (_, i) => curve[i]!)
158
+ const last = values.length - 1
159
+ if (last < 1) return (progress) => progress
160
+ return (progress) => {
161
+ const position = Math.min(Math.max(progress, 0), 1) * last
162
+ const index = Math.min(Math.floor(position), last - 1)
163
+ return values[index]! + (values[index + 1]! - values[index]!) * (position - index)
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Lifts the view by its overlap with the software keyboard, at the keyboard's
169
+ * own speed and curve, and lets it back down when the keyboard hides.
170
+ * Returns a function that removes the effect.
171
+ *
172
+ * ```ts
173
+ * const release = followKeyboard(composer)
174
+ * followKeyboard(sendButton, { offset: 12 })
175
+ * ```
176
+ */
177
+ export function followKeyboard(
178
+ target: Target,
179
+ options: KeyboardOptions = {},
180
+ ): () => void {
181
+ const view = toView(target)
182
+ const offset = options.offset ?? 0
183
+ if (!Number.isFinite(offset)) throw new RangeError('Keyboard offset must be finite.')
184
+ if (isIOS) return followIOS(view, offset)
185
+ if (isAndroid) return followAndroid(view, offset)
186
+ return () => {}
187
+ }
188
+
189
+ function followIOS(view: View, offset: number) {
190
+ let disposed = false
191
+ let current = 0
192
+ const onKeyboard = (notification: NSNotification) => {
193
+ if (disposed || !view.isLoaded) return
194
+ const native = view.nativeViewProtected as UIView | undefined
195
+ const window = native?.window
196
+ const info = notification.userInfo
197
+ if (!native || !window || !info) return
198
+ const end = info.objectForKey(UIKeyboardFrameEndUserInfoKey)?.CGRectValue as
199
+ CGRect | undefined
200
+ if (!end) return
201
+ const keyboard = window.convertRectFromWindow(end, null)
202
+ const hidden =
203
+ keyboard.size.height <= 0 || keyboard.origin.y >= window.bounds.size.height
204
+ const frame = native.convertRectToView(native.bounds, null)
205
+ const bottom = frame.origin.y + frame.size.height - view.translateY
206
+ const overlap = hidden ? 0 : Math.max(0, bottom + offset - keyboard.origin.y)
207
+ if (overlap === current) return
208
+ current = overlap
209
+ stop(view, ['y'])
210
+ const apply = () => {
211
+ view.translateY = overlap ? -overlap : 0
212
+ }
213
+ const duration = numberIn(info, UIKeyboardAnimationDurationUserInfoKey)
214
+ if (!duration) return apply()
215
+ const curve = numberIn(info, UIKeyboardAnimationCurveUserInfoKey)
216
+ // The keyboard's curve is private; shifted into the options it still applies.
217
+ UIView.animateWithDurationDelayOptionsAnimationsCompletion(
218
+ duration,
219
+ 0,
220
+ ((curve << 16) | BEGIN_FROM_CURRENT_STATE) as UIViewAnimationOptions,
221
+ apply,
222
+ () => {},
223
+ )
224
+ }
225
+ const observer = Application.ios.addNotificationObserver(
226
+ UIKeyboardWillChangeFrameNotification,
227
+ onKeyboard,
228
+ )
229
+ const cleanup = () => {
230
+ if (disposed) return
231
+ disposed = true
232
+ Application.ios.removeNotificationObserver(
233
+ observer,
234
+ UIKeyboardWillChangeFrameNotification,
235
+ )
236
+ view.off('unloaded', cleanup)
237
+ if (current) view.translateY = 0
238
+ }
239
+ view.on('unloaded', cleanup)
240
+ return cleanup
241
+ }
242
+
243
+ function followAndroid(view: View, offset: number) {
244
+ if (android.os.Build.VERSION.SDK_INT < 30) return () => {}
245
+ const density = Utils.layout.getDisplayDensity()
246
+ let disposed = false
247
+ let follower: org.rafaelaugusto.jolt.KeyboardFollower | undefined
248
+ const attach = () => {
249
+ if (disposed || follower) return
250
+ const native = view.nativeViewProtected as android.view.View | undefined
251
+ if (!native) return
252
+ try {
253
+ follower = new org.rafaelaugusto.jolt.KeyboardFollower(
254
+ native,
255
+ offset * density,
256
+ new org.rafaelaugusto.jolt.KeyboardFollower.Listener({
257
+ onStart: () => stop(view, ['y']),
258
+ onSettled: (translation) => {
259
+ view.translateY = translation / density
260
+ },
261
+ }),
262
+ )
263
+ } catch {
264
+ // A JS-only update cannot install the native plugin. Leave the platform's resize in place.
265
+ return
266
+ }
267
+ follower.attach()
268
+ }
269
+ const cleanup = () => {
270
+ if (disposed) return
271
+ disposed = true
272
+ view.off('loaded', attach)
273
+ view.off('unloaded', cleanup)
274
+ if (follower) {
275
+ follower.detach()
276
+ follower = undefined
277
+ view.translateY = 0
278
+ }
279
+ }
280
+ view.on('loaded', attach)
281
+ view.on('unloaded', cleanup)
282
+ if (view.isLoaded) attach()
283
+ return cleanup
284
+ }
285
+
286
+ /** The runtime hands NSNumber values over as plain numbers. */
287
+ function numberIn(info: NSDictionary<unknown, unknown>, key: string): number {
288
+ const value = info.objectForKey(key) as number | NSNumber | null | undefined
289
+ if (typeof value === 'number') return value
290
+ return value?.doubleValue ?? 0
291
+ }
package/src/label.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type { Label } from '@nativescript/core'
2
+
3
+ const callbacks = new WeakMap<NSObject, (finished: boolean) => void>()
4
+ let Delegate: typeof NSObject | undefined
5
+ const styleEvents = [
6
+ 'fontInternalChange',
7
+ 'colorChange',
8
+ 'textAlignmentChange',
9
+ 'textDecorationChange',
10
+ 'textTransformChange',
11
+ 'letterSpacingChange',
12
+ 'lineHeightChange',
13
+ 'textShadowChange',
14
+ 'textStrokeChange',
15
+ 'whiteSpaceChange',
16
+ 'textOverflowChange',
17
+ 'maxLinesChange',
18
+ 'paddingLeftChange',
19
+ 'paddingRightChange',
20
+ 'paddingTopChange',
21
+ 'paddingBottomChange',
22
+ 'visibilityChange',
23
+ 'directionChange',
24
+ 'borderLeftWidthChange',
25
+ 'borderRightWidthChange',
26
+ 'borderTopWidthChange',
27
+ 'borderBottomWidthChange',
28
+ ]
29
+
30
+ /** Reports text, formatted text and style changes of a Label until the returned function is called. */
31
+ export function watchLabel(view: Label, onChange: () => void): () => void {
32
+ const formatted = view.formattedText
33
+ view.on('textChange', onChange)
34
+ view.on('formattedTextChange', onChange)
35
+ formatted?.on('propertyChange', onChange)
36
+ for (const event of styleEvents) view.style.on(event, onChange)
37
+ return () => {
38
+ view.off('textChange', onChange)
39
+ view.off('formattedTextChange', onChange)
40
+ formatted?.off('propertyChange', onChange)
41
+ for (const event of styleEvents) view.style.off(event, onChange)
42
+ }
43
+ }
44
+
45
+ /** A CAAnimationDelegate that reports when its animation stops. Release it with `forgetCompletion`. */
46
+ export function completion(
47
+ callback: (finished: boolean) => void,
48
+ ): NSObject & CAAnimationDelegate {
49
+ if (!Delegate) {
50
+ const base = NSObject as typeof NSObject & {
51
+ extend(
52
+ methods: object,
53
+ options: { protocols: unknown[]; name: string },
54
+ ): typeof NSObject
55
+ }
56
+ Delegate = base.extend(
57
+ {
58
+ animationDidStopFinished(
59
+ this: NSObject,
60
+ _animation: CAAnimation,
61
+ finished: boolean,
62
+ ) {
63
+ callbacks.get(this)?.(finished)
64
+ },
65
+ },
66
+ { protocols: [CAAnimationDelegate], name: 'JoltLabelAnimationDelegate' },
67
+ )
68
+ }
69
+ const delegate = Delegate.new()
70
+ callbacks.set(delegate, callback)
71
+ return delegate
72
+ }
73
+
74
+ export function forgetCompletion(delegate: NSObject) {
75
+ callbacks.delete(delegate)
76
+ }