@octane-xplat/keyboard 0.0.1 → 0.17.0
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 +153 -1
- package/UPSTREAM.md +12 -0
- package/package.json +77 -9
- package/src/KeyboardAwareScrollView.mobile.tsrx +54 -0
- package/src/KeyboardAwareScrollView.tsrx +53 -0
- package/src/KeyboardStickyView.mobile.tsrx +66 -0
- package/src/KeyboardStickyView.tsrx +9 -0
- package/src/Provider.tsrx +11 -0
- package/src/binding-types.ts +16 -0
- package/src/controller.ts +243 -0
- package/src/focus-clock.macos.ts +13 -0
- package/src/focus-geometry.ts +34 -0
- package/src/focus-publish.ts +14 -0
- package/src/focus-runtime-types.ts +9 -0
- package/src/focus-runtime.macos.ts +62 -0
- package/src/focus-runtime.mobile.ts +57 -0
- package/src/focus-runtime.ts +14 -0
- package/src/focus-runtime.web.ts +139 -0
- package/src/focus-source.android.ts +173 -0
- package/src/focus-source.ios.ts +202 -0
- package/src/focus-source.macos.ts +113 -0
- package/src/focus-source.ts +12 -0
- package/src/focus-source.web.ts +157 -0
- package/src/focus-types.ts +19 -0
- package/src/focused-input.ts +43 -0
- package/src/index.ts +12 -0
- package/src/motion.ts +82 -0
- package/src/native-scope.ts +28 -0
- package/src/prediction.ts +32 -0
- package/src/props.ts +37 -0
- package/src/runtime-types.ts +10 -0
- package/src/runtime.mobile.ts +62 -0
- package/src/runtime.ts +16 -0
- package/src/scroll-controller.ts +136 -0
- package/src/scroll-geometry.ts +72 -0
- package/src/scroll-runtime-types.ts +6 -0
- package/src/scroll-runtime.macos.ts +41 -0
- package/src/scroll-runtime.mobile.ts +46 -0
- package/src/scroll-runtime.ts +7 -0
- package/src/scroll-runtime.web.ts +25 -0
- package/src/scroll-types.ts +24 -0
- package/src/source.android.ts +179 -0
- package/src/source.ios.ts +283 -0
- package/src/source.ts +5 -0
- package/src/tsconfig.json +1 -0
- package/src/types.ts +45 -0
- package/src/viewport-types.ts +7 -0
- package/src/viewport.android.ts +238 -0
- package/src/viewport.ios.ts +6 -0
- package/src/viewport.ts +6 -0
- package/types/focus-types.d.ts +17 -0
- package/types/index.d.ts +20 -0
- package/types/props.d.ts +35 -0
- package/types/scroll-types.d.ts +22 -0
- package/types/types.d.ts +40 -0
- package/index.js +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { keyboardBottomOverlap } from '@octane-xplat/platform/keyboard-geometry'
|
|
2
|
+
import { leaseKeyboardProperty } from '@octane-xplat/platform/keyboard-property'
|
|
3
|
+
import type { KeyboardPropertyLease } from '@octane-xplat/platform/keyboard-property'
|
|
4
|
+
import { observeKeyboardViewport } from '@octane-xplat/platform/keyboard-viewport'
|
|
5
|
+
import type { KeyboardViewportLease } from '@octane-xplat/platform/keyboard-viewport'
|
|
6
|
+
import type { KeyboardWindowState } from '@octane-xplat/platform/keyboard-state'
|
|
7
|
+
import { keyboardRuntime } from './runtime'
|
|
8
|
+
import type { KeyboardBindingOptions, KeyboardHostBinding } from './binding-types'
|
|
9
|
+
import type { KeyboardSnapshot, SoftwareKeyboardController } from './types'
|
|
10
|
+
|
|
11
|
+
const snapshots = new WeakMap<KeyboardWindowState, KeyboardSnapshot>()
|
|
12
|
+
const snapshot = (state: KeyboardWindowState): KeyboardSnapshot => {
|
|
13
|
+
let value = snapshots.get(state)
|
|
14
|
+
if (!value) {
|
|
15
|
+
value = {
|
|
16
|
+
inset: state.inset,
|
|
17
|
+
windowRect: state.windowRect,
|
|
18
|
+
currentFrame: state.currentFrame,
|
|
19
|
+
targetFrame: state.targetFrame,
|
|
20
|
+
source: state.source,
|
|
21
|
+
capabilities: {
|
|
22
|
+
geometry: !!state.windowRect && state.source !== 'unknown',
|
|
23
|
+
nativeFrames: state.motion?.nativeFrames ?? state.source === 'native-frames',
|
|
24
|
+
interactive: state.motion?.interactive ?? false,
|
|
25
|
+
floating: state.motion?.floating ?? false,
|
|
26
|
+
},
|
|
27
|
+
phase: state.motion?.phase ?? 'idle',
|
|
28
|
+
transitionId: state.motion?.transitionId ?? 0,
|
|
29
|
+
timestampMs: state.motion?.timestampMs ?? 0,
|
|
30
|
+
durationMs: state.motion?.durationMs ?? null,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
snapshots.set(state, value)
|
|
34
|
+
}
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const observeState = (host: object, listener: (state: KeyboardWindowState) => void) => {
|
|
39
|
+
const deactivate = keyboardRuntime.activate()
|
|
40
|
+
let active = true
|
|
41
|
+
let off: () => void
|
|
42
|
+
try {
|
|
43
|
+
off = keyboardRuntime.subscribe(host, (state) => {
|
|
44
|
+
if (active) {
|
|
45
|
+
listener(state)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
} catch (error) {
|
|
49
|
+
active = false
|
|
50
|
+
deactivate()
|
|
51
|
+
throw error
|
|
52
|
+
}
|
|
53
|
+
return () => {
|
|
54
|
+
if (!active) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
active = false
|
|
58
|
+
try {
|
|
59
|
+
off()
|
|
60
|
+
} finally {
|
|
61
|
+
deactivate()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const observe = (host: object, listener: (state: KeyboardSnapshot) => void) =>
|
|
67
|
+
observeState(host, (state) => listener(snapshot(state)))
|
|
68
|
+
|
|
69
|
+
const validateBindingOptions = (options: KeyboardBindingOptions) => {
|
|
70
|
+
for (const value of [options.openOffset, options.closedOffset, options.consumedBottom]) {
|
|
71
|
+
if (value !== undefined && !Number.isFinite(value)) {
|
|
72
|
+
throw new RangeError('Keyboard offsets must be finite')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type Binding = {
|
|
78
|
+
lease: KeyboardPropertyLease
|
|
79
|
+
viewport: KeyboardViewportLease
|
|
80
|
+
owners: { read(): KeyboardBindingOptions }[]
|
|
81
|
+
state?: KeyboardWindowState
|
|
82
|
+
openExtent: number
|
|
83
|
+
off: () => void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bindings = new WeakMap<object, Binding>()
|
|
87
|
+
|
|
88
|
+
const apply = (host: object, binding: Binding) => {
|
|
89
|
+
const measured = binding.state,
|
|
90
|
+
options = binding.owners.at(-1)?.read()
|
|
91
|
+
if (!measured || !options) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
validateBindingOptions(options)
|
|
95
|
+
const state = measured.bindingFrame
|
|
96
|
+
? { ...measured, currentFrame: measured.bindingFrame }
|
|
97
|
+
: measured
|
|
98
|
+
binding.viewport.update()
|
|
99
|
+
const frame = state.currentFrame ?? state.targetFrame
|
|
100
|
+
const current = keyboardBottomOverlap(state.windowRect, frame)
|
|
101
|
+
const target = keyboardBottomOverlap(state.windowRect, state.targetFrame)
|
|
102
|
+
if (target > 0) {
|
|
103
|
+
binding.openExtent = target
|
|
104
|
+
} else if (!binding.openExtent && current > 0) {
|
|
105
|
+
binding.openExtent = current
|
|
106
|
+
}
|
|
107
|
+
const progress =
|
|
108
|
+
state.source === 'native-frames'
|
|
109
|
+
? Math.min(1, Math.max(0, binding.openExtent ? current / binding.openExtent : 0))
|
|
110
|
+
: state.inset > 0
|
|
111
|
+
? 1
|
|
112
|
+
: 0
|
|
113
|
+
const extra =
|
|
114
|
+
(options.closedOffset ?? 0) +
|
|
115
|
+
((options.openOffset ?? 0) - (options.closedOffset ?? 0)) * progress
|
|
116
|
+
const overlap = keyboardRuntime.overlap(host, state, binding.lease.offset)
|
|
117
|
+
const budget = Math.max(
|
|
118
|
+
0,
|
|
119
|
+
Math.max(state.inset, current, overlap) - Math.max(0, options.consumedBottom ?? 0),
|
|
120
|
+
)
|
|
121
|
+
binding.lease.set(-Math.min(overlap, budget) - extra)
|
|
122
|
+
if (current === 0 && target === 0) {
|
|
123
|
+
binding.openExtent = 0
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Software-keyboard observation and movement; separate from hardware useKeyboard.
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import { KeyboardController } from '@octane-xplat/keyboard'
|
|
131
|
+
* function observe(host: object) {
|
|
132
|
+
* return KeyboardController.subscribe(host, state => console.log(state.phase))
|
|
133
|
+
* }
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export const KeyboardController: SoftwareKeyboardController = {
|
|
137
|
+
activate: () => keyboardRuntime.activate(),
|
|
138
|
+
getSnapshot: (host) => snapshot(keyboardRuntime.getSnapshot(host)),
|
|
139
|
+
onMove: observe,
|
|
140
|
+
subscribe(host, listener) {
|
|
141
|
+
let previous: KeyboardSnapshot | undefined
|
|
142
|
+
return observe(host, (state) => {
|
|
143
|
+
const rect = state.targetFrame,
|
|
144
|
+
before = previous?.targetFrame
|
|
145
|
+
const changed =
|
|
146
|
+
!previous ||
|
|
147
|
+
state.source !== previous.source ||
|
|
148
|
+
state.inset !== previous.inset ||
|
|
149
|
+
rect?.x !== before?.x ||
|
|
150
|
+
rect?.y !== before?.y ||
|
|
151
|
+
rect?.width !== before?.width ||
|
|
152
|
+
rect?.height !== before?.height ||
|
|
153
|
+
state.windowRect?.width !== previous.windowRect?.width ||
|
|
154
|
+
state.windowRect?.height !== previous.windowRect?.height ||
|
|
155
|
+
state.capabilities.nativeFrames !== previous.capabilities.nativeFrames ||
|
|
156
|
+
state.capabilities.interactive !== previous.capabilities.interactive ||
|
|
157
|
+
state.capabilities.floating !== previous.capabilities.floating ||
|
|
158
|
+
(state.phase !== 'move' &&
|
|
159
|
+
(state.phase !== previous.phase || state.transitionId !== previous.transitionId))
|
|
160
|
+
previous = state
|
|
161
|
+
if (changed) {
|
|
162
|
+
listener(state)
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** @internal Bind only framework-owned native movement hosts. */
|
|
169
|
+
export function bindKeyboardHost(
|
|
170
|
+
host: object,
|
|
171
|
+
options: KeyboardBindingOptions | (() => KeyboardBindingOptions) = {},
|
|
172
|
+
): KeyboardHostBinding {
|
|
173
|
+
const read = typeof options === 'function' ? options : () => options
|
|
174
|
+
const initial = read()
|
|
175
|
+
const inert = () => Object.assign(() => {}, { update() {} })
|
|
176
|
+
if (initial.enabled === false) {
|
|
177
|
+
return inert()
|
|
178
|
+
}
|
|
179
|
+
validateBindingOptions(initial)
|
|
180
|
+
let binding = bindings.get(host)
|
|
181
|
+
const owned = { read }
|
|
182
|
+
if (!binding) {
|
|
183
|
+
const property = keyboardRuntime.property(host)
|
|
184
|
+
if (!property) {
|
|
185
|
+
return inert()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
binding = {
|
|
189
|
+
lease: leaseKeyboardProperty(property.read, property.write),
|
|
190
|
+
viewport: observeKeyboardViewport(host),
|
|
191
|
+
owners: [owned],
|
|
192
|
+
openExtent: 0,
|
|
193
|
+
off: () => {},
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
bindings.set(host, binding)
|
|
197
|
+
const entry = binding
|
|
198
|
+
try {
|
|
199
|
+
entry.off = observeState(host, (state) => {
|
|
200
|
+
entry.state = state
|
|
201
|
+
apply(host, entry)
|
|
202
|
+
})
|
|
203
|
+
} catch (error) {
|
|
204
|
+
bindings.delete(host)
|
|
205
|
+
entry.lease.release()
|
|
206
|
+
entry.viewport.release()
|
|
207
|
+
throw error
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
binding.owners.push(owned)
|
|
211
|
+
apply(host, binding)
|
|
212
|
+
}
|
|
213
|
+
const entry = binding
|
|
214
|
+
let active = true
|
|
215
|
+
const release = () => {
|
|
216
|
+
if (!active) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
active = false
|
|
220
|
+
entry.owners.splice(entry.owners.indexOf(owned), 1)
|
|
221
|
+
if (entry.owners.length) {
|
|
222
|
+
apply(host, entry)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
bindings.delete(host)
|
|
226
|
+
try {
|
|
227
|
+
entry.off()
|
|
228
|
+
} finally {
|
|
229
|
+
try {
|
|
230
|
+
entry.lease.release()
|
|
231
|
+
} finally {
|
|
232
|
+
entry.viewport.release()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return Object.assign(release, {
|
|
237
|
+
update() {
|
|
238
|
+
if (active) {
|
|
239
|
+
apply(host, entry)
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NSTimer, NSRunLoop } from '@octane-xplat/platform/macos'
|
|
2
|
+
|
|
3
|
+
/** @internal Owned metadata polling, including during AppKit tracking modes. */
|
|
4
|
+
export function startFocusClock(read: () => void): () => void {
|
|
5
|
+
const timer = NSTimer.timerWithTimeIntervalRepeatsBlock(1 / 60, true, read)
|
|
6
|
+
try {
|
|
7
|
+
NSRunLoop.mainRunLoop.addTimerForMode(timer, 'kCFRunLoopCommonModes')
|
|
8
|
+
} catch (error) {
|
|
9
|
+
timer.invalidate()
|
|
10
|
+
throw error
|
|
11
|
+
}
|
|
12
|
+
return () => timer.invalidate()
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { isKeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
|
|
2
|
+
import type { KeyboardRect } from '@octane-xplat/platform/keyboard-geometry'
|
|
3
|
+
import { unknownFocusedInputState } from '@octane-xplat/platform/focused-input-state'
|
|
4
|
+
import type { FocusedInputState } from '@octane-xplat/platform/focused-input-state'
|
|
5
|
+
|
|
6
|
+
/** @internal Convert a plain CGRect/NSRect value, never an SDK object in shared policy. */
|
|
7
|
+
export function focusRect(value: any): KeyboardRect | null {
|
|
8
|
+
const rect = value && {
|
|
9
|
+
x: value.origin?.x,
|
|
10
|
+
y: value.origin?.y,
|
|
11
|
+
width: value.size?.width,
|
|
12
|
+
height: value.size?.height,
|
|
13
|
+
}
|
|
14
|
+
return isKeyboardRect(rect) ? rect : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** @internal Selection metadata only; invalid native ranges are unavailable. */
|
|
18
|
+
export function focusSelection(start: number, end: number): FocusedInputState['selection'] {
|
|
19
|
+
return Number.isSafeInteger(start) && Number.isSafeInteger(end) && start >= 0 && end >= 0
|
|
20
|
+
? { start, end }
|
|
21
|
+
: null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @internal A supported window without an editing input. */
|
|
25
|
+
export function emptyFocus(
|
|
26
|
+
windowRect: KeyboardRect | null,
|
|
27
|
+
caretBounds: FocusedInputState['capabilities']['caretBounds'],
|
|
28
|
+
): FocusedInputState {
|
|
29
|
+
return {
|
|
30
|
+
...unknownFocusedInputState(),
|
|
31
|
+
windowRect,
|
|
32
|
+
capabilities: { inputBounds: true, caretBounds, selection: true },
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { sameFocusedInput } from '@octane-xplat/platform/focused-input-state'
|
|
2
|
+
import type { FocusedInputState } from '@octane-xplat/platform/focused-input-state'
|
|
3
|
+
|
|
4
|
+
/** @internal Stable snapshots across idle polling frames. */
|
|
5
|
+
export function focusPublisher(publish: (state: FocusedInputState) => void) {
|
|
6
|
+
let previous: FocusedInputState | undefined
|
|
7
|
+
return (next: FocusedInputState) => {
|
|
8
|
+
if (previous && sameFocusedInput(previous, next)) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
previous = { ...next, timestampMs: Date.now() }
|
|
12
|
+
publish(previous)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FocusedInputState } from '@octane-xplat/platform/focused-input-state'
|
|
2
|
+
|
|
3
|
+
/** @internal Platform-owned window resolution and observation. */
|
|
4
|
+
export interface FocusRuntime {
|
|
5
|
+
activate(): () => void
|
|
6
|
+
getSnapshot(host: object): FocusedInputState
|
|
7
|
+
subscribe(host: object, listener: (state: FocusedInputState) => void): () => void
|
|
8
|
+
recheck(host: object): void
|
|
9
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
focusedInputWindows,
|
|
3
|
+
unknownFocusedInputState,
|
|
4
|
+
} from '@octane-xplat/platform/focused-input-state'
|
|
5
|
+
|
|
6
|
+
import { observeFocusedInput, focusRechecks } from './focus-source'
|
|
7
|
+
import { startFocusClock } from './focus-clock'
|
|
8
|
+
import type { FocusRuntime } from './focus-runtime-types'
|
|
9
|
+
|
|
10
|
+
const inactive = unknownFocusedInputState()
|
|
11
|
+
const owner = (host: any): object | undefined => (host?.native ?? host)?.window ?? undefined
|
|
12
|
+
|
|
13
|
+
export const focusRuntime: FocusRuntime = {
|
|
14
|
+
activate: () => focusedInputWindows.install(observeFocusedInput),
|
|
15
|
+
getSnapshot(host) {
|
|
16
|
+
const key = owner(host)
|
|
17
|
+
return key ? focusedInputWindows.getSnapshot(key) : inactive
|
|
18
|
+
},
|
|
19
|
+
recheck(host) {
|
|
20
|
+
const key = owner(host)
|
|
21
|
+
if (key) {
|
|
22
|
+
focusRechecks.get(key)?.()
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
subscribe(host, listener) {
|
|
26
|
+
let active = true
|
|
27
|
+
let key: object | undefined,
|
|
28
|
+
initialized = false
|
|
29
|
+
let off = () => {}
|
|
30
|
+
const rebind = () => {
|
|
31
|
+
if (!active) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
const next = owner(host)
|
|
35
|
+
if (initialized && key === next) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
initialized = true
|
|
39
|
+
key = next
|
|
40
|
+
off()
|
|
41
|
+
off = () => {}
|
|
42
|
+
if (next) {
|
|
43
|
+
off = focusedInputWindows.subscribe(next, listener)
|
|
44
|
+
} else {
|
|
45
|
+
listener(inactive)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
rebind()
|
|
49
|
+
let stop: () => void
|
|
50
|
+
try {
|
|
51
|
+
stop = startFocusClock(rebind)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
off()
|
|
54
|
+
throw error
|
|
55
|
+
}
|
|
56
|
+
return () => {
|
|
57
|
+
active = false
|
|
58
|
+
stop()
|
|
59
|
+
off()
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
focusedInputWindows,
|
|
3
|
+
unknownFocusedInputState,
|
|
4
|
+
} from '@octane-xplat/platform/focused-input-state'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
keyboardWindowForHost,
|
|
8
|
+
subscribeKeyboardState,
|
|
9
|
+
} from '@octane-xplat/ui/nativescript/keyboard'
|
|
10
|
+
|
|
11
|
+
import { observeFocusedInput, focusRechecks } from './focus-source'
|
|
12
|
+
import type { FocusRuntime } from './focus-runtime-types'
|
|
13
|
+
|
|
14
|
+
const inactive = unknownFocusedInputState()
|
|
15
|
+
const unwrap = (host: any) => host?.native ?? host
|
|
16
|
+
|
|
17
|
+
export const focusRuntime: FocusRuntime = {
|
|
18
|
+
activate: () => focusedInputWindows.install(observeFocusedInput),
|
|
19
|
+
getSnapshot(host) {
|
|
20
|
+
const key = keyboardWindowForHost(unwrap(host))
|
|
21
|
+
return key ? focusedInputWindows.getSnapshot(key) : inactive
|
|
22
|
+
},
|
|
23
|
+
recheck(host) {
|
|
24
|
+
const key = keyboardWindowForHost(unwrap(host))
|
|
25
|
+
if (key) {
|
|
26
|
+
focusRechecks.get(key)?.()
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
subscribe(host, listener) {
|
|
30
|
+
let key: object | undefined
|
|
31
|
+
let offFocus = () => {}
|
|
32
|
+
let initialized = false
|
|
33
|
+
const offWindow = subscribeKeyboardState(
|
|
34
|
+
(_state, next) => {
|
|
35
|
+
if (initialized && key === next) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
initialized = true
|
|
39
|
+
offFocus()
|
|
40
|
+
key = next
|
|
41
|
+
offFocus = next ? focusedInputWindows.subscribe(next, listener) : () => {}
|
|
42
|
+
if (!next) {
|
|
43
|
+
listener(inactive)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
unwrap(host),
|
|
47
|
+
false,
|
|
48
|
+
)
|
|
49
|
+
return () => {
|
|
50
|
+
try {
|
|
51
|
+
offWindow()
|
|
52
|
+
} finally {
|
|
53
|
+
offFocus()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { unknownFocusedInputState } from '@octane-xplat/platform/focused-input-state'
|
|
2
|
+
import type { FocusRuntime } from './focus-runtime-types'
|
|
3
|
+
|
|
4
|
+
const inactive = unknownFocusedInputState()
|
|
5
|
+
/** Neutral fallback for hosts without qualified input geometry adapters. */
|
|
6
|
+
export const focusRuntime: FocusRuntime = {
|
|
7
|
+
activate: () => () => {},
|
|
8
|
+
getSnapshot: () => inactive,
|
|
9
|
+
subscribe(_host, listener) {
|
|
10
|
+
listener(inactive)
|
|
11
|
+
return () => {}
|
|
12
|
+
},
|
|
13
|
+
recheck() {},
|
|
14
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
focusedInputWindows,
|
|
3
|
+
unknownFocusedInputState,
|
|
4
|
+
} from '@octane-xplat/platform/focused-input-state'
|
|
5
|
+
|
|
6
|
+
import { observeFocusedInput, focusRechecks } from './focus-source'
|
|
7
|
+
import type { FocusRuntime } from './focus-runtime-types'
|
|
8
|
+
|
|
9
|
+
const inactive = unknownFocusedInputState()
|
|
10
|
+
const lifecycles = new WeakMap<Document, { readers: number; paused: boolean }>()
|
|
11
|
+
const view = (host: any): HTMLElement | undefined => host?.native ?? host
|
|
12
|
+
// A WindowProxy survives navigation; the document is its observation generation.
|
|
13
|
+
const owner = (host: object): Document | undefined => {
|
|
14
|
+
const element = view(host),
|
|
15
|
+
document = element?.ownerDocument,
|
|
16
|
+
window = document?.defaultView
|
|
17
|
+
try {
|
|
18
|
+
return element?.isConnected &&
|
|
19
|
+
window &&
|
|
20
|
+
!window.closed &&
|
|
21
|
+
window.document === document &&
|
|
22
|
+
!lifecycles.get(document!)?.paused
|
|
23
|
+
? document
|
|
24
|
+
: undefined
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const watchWindow = (document: Document, rebind: () => void) => {
|
|
31
|
+
const window = document.defaultView!
|
|
32
|
+
let state = lifecycles.get(document)
|
|
33
|
+
if (!state) {
|
|
34
|
+
state = { readers: 0, paused: false }
|
|
35
|
+
lifecycles.set(document, state)
|
|
36
|
+
}
|
|
37
|
+
const owned = state
|
|
38
|
+
owned.readers++
|
|
39
|
+
let active = true
|
|
40
|
+
const hide = () => {
|
|
41
|
+
if (active) {
|
|
42
|
+
owned.paused = true
|
|
43
|
+
rebind()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const show = () => {
|
|
47
|
+
if (active) {
|
|
48
|
+
owned.paused = false
|
|
49
|
+
rebind()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
window.addEventListener('pagehide', hide)
|
|
53
|
+
window.addEventListener('pageshow', show)
|
|
54
|
+
return () => {
|
|
55
|
+
if (!active) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
active = false
|
|
59
|
+
window.removeEventListener('pagehide', hide)
|
|
60
|
+
window.removeEventListener('pageshow', show)
|
|
61
|
+
if (!--owned.readers) {
|
|
62
|
+
lifecycles.delete(document)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const focusRuntime: FocusRuntime = {
|
|
68
|
+
activate: () => focusedInputWindows.install(observeFocusedInput),
|
|
69
|
+
getSnapshot(host) {
|
|
70
|
+
const key = owner(host)
|
|
71
|
+
return key ? focusedInputWindows.getSnapshot(key) : inactive
|
|
72
|
+
},
|
|
73
|
+
recheck(host) {
|
|
74
|
+
const key = owner(host)
|
|
75
|
+
if (key) {
|
|
76
|
+
focusRechecks.get(key)?.()
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
subscribe(host, listener) {
|
|
80
|
+
let active = true
|
|
81
|
+
let key: Document | undefined,
|
|
82
|
+
document: Document | undefined,
|
|
83
|
+
initialized = false
|
|
84
|
+
let observer: MutationObserver | undefined
|
|
85
|
+
let offWindow = () => {}
|
|
86
|
+
let off = () => {}
|
|
87
|
+
const rebind = () => {
|
|
88
|
+
if (!active) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const nextDocument = view(host)?.ownerDocument
|
|
92
|
+
if (nextDocument !== document) {
|
|
93
|
+
observer?.disconnect()
|
|
94
|
+
offWindow()
|
|
95
|
+
offWindow = () => {}
|
|
96
|
+
document = nextDocument
|
|
97
|
+
const Observer = document?.defaultView?.MutationObserver
|
|
98
|
+
observer = Observer ? new Observer(rebind) : undefined
|
|
99
|
+
if (document) {
|
|
100
|
+
observer?.observe(document, { childList: true, subtree: true })
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (document?.defaultView) {
|
|
104
|
+
offWindow = watchWindow(document, rebind)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const next = owner(host)
|
|
108
|
+
if (initialized && key === next) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
initialized = true
|
|
112
|
+
off()
|
|
113
|
+
off = () => {}
|
|
114
|
+
key = next
|
|
115
|
+
if (next) {
|
|
116
|
+
off = focusedInputWindows.subscribe(next, listener)
|
|
117
|
+
} else {
|
|
118
|
+
listener(inactive)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
rebind()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
observer?.disconnect()
|
|
125
|
+
offWindow()
|
|
126
|
+
off()
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
return () => {
|
|
130
|
+
if (!active) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
active = false
|
|
134
|
+
observer?.disconnect()
|
|
135
|
+
offWindow()
|
|
136
|
+
off()
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
}
|