@uno.build/runtime 0.0.0-stage → 0.2.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 +2 -2
- package/base/base.native.ts +7 -0
- package/base/base.web.ts +3 -0
- package/canvas/canvas.native.ts +12 -0
- package/canvas/canvas.web.ts +20 -0
- package/index.ts +5 -0
- package/memory/memory.native.ts +8 -0
- package/memory/memory.web.ts +15 -0
- package/package.json +30 -5
- package/screen/README.md +39 -0
- package/screen/screen.native.ts +33 -0
- package/screen/screen.web.ts +35 -0
- package/screen/types.ts +10 -0
- package/ui/index.ts +1 -0
- package/ui/ui.native.ts +22 -0
- package/ui/ui.web.ts +11 -0
- package/utils.ts +24 -0
- package/virtualkeyboard/README.md +122 -0
- package/virtualkeyboard/types.ts +36 -0
- package/virtualkeyboard/virtualkeyboard.native.ts +10 -0
- package/virtualkeyboard/virtualkeyboard.web.ts +197 -0
- package/yoga/yoga.native.ts +16 -0
- package/yoga/yoga.web.ts +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @uno.build/runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JavaScript APIs of the uno runtime. The documentation is at [uno.build](https://uno.build).
|
package/base/base.web.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getUnoModuleNative } from '../utils'
|
|
2
|
+
|
|
3
|
+
// The native HTMLCanvasElement, which fills the app window.
|
|
4
|
+
const canvas: HTMLCanvasElement = getUnoModuleNative('canvas').element
|
|
5
|
+
|
|
6
|
+
function init(): any {
|
|
7
|
+
return canvas
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Canvas = {
|
|
11
|
+
init,
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function init(canvasElement?: any): any {
|
|
2
|
+
if (canvasElement === undefined) {
|
|
3
|
+
canvasElement = document.querySelector('canvas')
|
|
4
|
+
if (canvasElement === null) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
'No canvas element found. Please provide a canvas element to initialize.',
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const getContext = canvasElement.getContext.bind(canvasElement)
|
|
12
|
+
canvasElement.getContext = (context_type: string, context_attributes?: any) =>
|
|
13
|
+
getContext('webgpu', context_attributes)
|
|
14
|
+
|
|
15
|
+
return canvasElement
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const Canvas = {
|
|
19
|
+
init,
|
|
20
|
+
}
|
package/index.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@uno.build/runtime",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"description": "JavaScript APIs of the uno runtime",
|
|
7
|
+
"homepage": "https://uno.build",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/uno-build/runtime"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "./index.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.ts",
|
|
18
|
+
"./ui": "./ui/index.ts"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"yoga-layout": "^3.2.1"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@uno.build/ui": "^0.4.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@uno.build/ui": {
|
|
28
|
+
"optional": true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/screen/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Screen
|
|
2
|
+
|
|
3
|
+
Read the current screen orientation and subscribe to orientation changes:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Screen } from 'uno'
|
|
7
|
+
|
|
8
|
+
console.log(Screen.getOrientation())
|
|
9
|
+
|
|
10
|
+
function onChange(orientation) {
|
|
11
|
+
console.log(orientation)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
Screen.addEventListener('orientation', onChange)
|
|
15
|
+
|
|
16
|
+
// Stop listening when the consumer is disposed.
|
|
17
|
+
Screen.removeEventListener('orientation', onChange)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Orientation values are `portrait-primary`, `portrait-secondary`,
|
|
21
|
+
`landscape-primary`, and `landscape-secondary`. Listeners receive the orientation
|
|
22
|
+
string directly. Registering the same callback twice does not duplicate it.
|
|
23
|
+
Registration does not emit the initial value; use `getOrientation()` to read it.
|
|
24
|
+
|
|
25
|
+
## Web limitations
|
|
26
|
+
|
|
27
|
+
- `setOrientation()` is a no-op. It does not rotate or lock the screen, and it
|
|
28
|
+
does not throw an error.
|
|
29
|
+
- Reading and observing orientation depend on the browser's
|
|
30
|
+
[Screen Orientation API](https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation).
|
|
31
|
+
The module reads `screen.orientation.type` and listens for its `change` event.
|
|
32
|
+
- If `screen` or `screen.orientation` is unavailable, `getOrientation()` returns
|
|
33
|
+
`undefined`. Listeners can still be added and removed, but receive no events.
|
|
34
|
+
There is no fallback to sensors, legacy orientation events, or viewport dimensions.
|
|
35
|
+
- The reported value describes screen orientation, not physical device movement.
|
|
36
|
+
The module does not use `deviceorientation` or request sensor permissions.
|
|
37
|
+
- Resizing the browser window or canvas is not an orientation change. Use the window's
|
|
38
|
+
`resize` event, `addEventListener('resize', callback)`, to react to size changes.
|
|
39
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getUnoModuleNative } from '../utils'
|
|
2
|
+
import type { ScreenOrientation, ScreenEffectiveOrientation } from './types'
|
|
3
|
+
|
|
4
|
+
export type { ScreenOrientation, ScreenEffectiveOrientation } from './types'
|
|
5
|
+
|
|
6
|
+
const module_ref = getUnoModuleNative<{
|
|
7
|
+
getOrientation: () => ScreenEffectiveOrientation
|
|
8
|
+
onChangeOrientation: (callback: ((orientation: ScreenEffectiveOrientation) => void) | null) => void
|
|
9
|
+
setOrientation: (orientation: ScreenOrientation) => void
|
|
10
|
+
}>('screen')
|
|
11
|
+
|
|
12
|
+
const event_listeners = {
|
|
13
|
+
orientation: new Set<(orientation: ScreenEffectiveOrientation) => void>(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module_ref.onChangeOrientation((orientation) => {
|
|
17
|
+
event_listeners.orientation.forEach((callback) => callback(orientation))
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function addEventListener(name: 'orientation', callback: (orientation: ScreenEffectiveOrientation) => void): void {
|
|
21
|
+
event_listeners[name].add(callback)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removeEventListener(name: 'orientation', callback: (orientation: ScreenEffectiveOrientation) => void): void {
|
|
25
|
+
event_listeners[name].delete(callback)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Screen = {
|
|
29
|
+
getOrientation: module_ref.getOrientation,
|
|
30
|
+
setOrientation: module_ref.setOrientation,
|
|
31
|
+
addEventListener,
|
|
32
|
+
removeEventListener,
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getGlobalScope } from '../utils'
|
|
2
|
+
import type { ScreenOrientation, ScreenEffectiveOrientation } from './types'
|
|
3
|
+
|
|
4
|
+
export type { ScreenOrientation, ScreenEffectiveOrientation } from './types'
|
|
5
|
+
|
|
6
|
+
const scope = getGlobalScope()
|
|
7
|
+
const screen_orientation = scope.screen?.orientation
|
|
8
|
+
const event_listeners = {
|
|
9
|
+
orientation: new Set<(orientation: ScreenEffectiveOrientation) => void>(),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
screen_orientation?.addEventListener('change', () => {
|
|
13
|
+
event_listeners.orientation.forEach((callback) => callback(screen_orientation.type))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
function getOrientation(): ScreenEffectiveOrientation | undefined {
|
|
17
|
+
return screen_orientation?.type
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function addEventListener(name: 'orientation', callback: (orientation: ScreenEffectiveOrientation) => void): void {
|
|
21
|
+
event_listeners[name].add(callback)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removeEventListener(name: 'orientation', callback: (orientation: ScreenEffectiveOrientation) => void): void {
|
|
25
|
+
event_listeners[name].delete(callback)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Screen = {
|
|
29
|
+
getOrientation,
|
|
30
|
+
addEventListener,
|
|
31
|
+
removeEventListener,
|
|
32
|
+
setOrientation(_orientation: ScreenOrientation): void {
|
|
33
|
+
// throw new Error('Screen.setOrientation is only supported on iOS')
|
|
34
|
+
},
|
|
35
|
+
}
|
package/screen/types.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ScreenOrientation =
|
|
2
|
+
| 'any'
|
|
3
|
+
| 'portrait'
|
|
4
|
+
| 'portrait-primary'
|
|
5
|
+
| 'portrait-secondary'
|
|
6
|
+
| 'landscape'
|
|
7
|
+
| 'landscape-primary'
|
|
8
|
+
| 'landscape-secondary'
|
|
9
|
+
|
|
10
|
+
export type ScreenEffectiveOrientation = Exclude<ScreenOrientation, 'any' | 'portrait' | 'landscape'>
|
package/ui/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ui'
|
package/ui/ui.native.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EventOptions, UIWebGPUOptions } from '@uno.build/ui/UIWebGPU'
|
|
2
|
+
import ResourcesWebGPU from '@uno.build/ui/ResourcesWebGPU'
|
|
3
|
+
import UIWebGPU from '@uno.build/ui/UIWebGPU'
|
|
4
|
+
import { Yoga } from '../yoga/yoga'
|
|
5
|
+
|
|
6
|
+
export type { DefinedEvent } from '@uno.build/ui/UIWebGPU'
|
|
7
|
+
export type UIOptions = Omit<UIWebGPUOptions, 'loadYoga' | 'defined_events'> &
|
|
8
|
+
EventOptions<UI> & {
|
|
9
|
+
register_platform_events?: boolean
|
|
10
|
+
yoga?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default class UI extends UIWebGPU {
|
|
14
|
+
static override async create({ register_platform_events = true, yoga = true, ...options }: UIOptions) {
|
|
15
|
+
const ui = new UI({ ...options, loadYoga: yoga ? Yoga.loadYoga : undefined } as UIWebGPUOptions)
|
|
16
|
+
await ui.initialize()
|
|
17
|
+
if (register_platform_events) ui.registerPlatformEvents()
|
|
18
|
+
return { ui }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { ResourcesWebGPU as ResourcesUI, UI }
|
package/ui/ui.web.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EventOptions, UIWebGPUOptions } from '@uno.build/ui/UIWebGPU'
|
|
2
|
+
import ResourcesWebGPU from '@uno.build/ui/ResourcesWebGPU'
|
|
3
|
+
import UI from '@uno.build/ui/UI'
|
|
4
|
+
|
|
5
|
+
export type { DefinedEvent } from '@uno.build/ui/UIWebGPU'
|
|
6
|
+
export type UIOptions = Omit<UIWebGPUOptions, 'loadYoga' | 'defined_events'> &
|
|
7
|
+
EventOptions<UI> & {
|
|
8
|
+
register_platform_events?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { ResourcesWebGPU as ResourcesUI, UI }
|
package/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function getGlobalScope(): any {
|
|
2
|
+
if (typeof globalThis !== 'undefined') {
|
|
3
|
+
return globalThis as any
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof self !== 'undefined') {
|
|
7
|
+
return self as any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof window !== 'undefined') {
|
|
11
|
+
return window as any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof global !== 'undefined') {
|
|
15
|
+
return global as any
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getUnoModuleNative<T = any>(moduleName: string): T {
|
|
22
|
+
const uno = getGlobalScope().__unoNative
|
|
23
|
+
return uno[moduleName] as T
|
|
24
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# VirtualKeyboard
|
|
2
|
+
|
|
3
|
+
`VirtualKeyboard.open(options)` accepts the following input and keyboard options:
|
|
4
|
+
|
|
5
|
+
| Option | Category | Behavior |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| `input_value` | Input | Required initial text. |
|
|
8
|
+
| `appearance` | Input / Keyboard | `dark`, `light`, or `default` (default). On iOS, sets the keyboard and input bar appearance; `default` inherits the app appearance. On web, sets the input's CSS `color-scheme`; `default` inherits the page scheme. Applies to custom inputs too, but does not guarantee the system keyboard appearance or override explicit CSS colors. |
|
|
9
|
+
| `input_visible` | Input | Shows the native iOS input bar. Defaults to `true`. Setting it to `false` keeps keyboard input and callbacks active without displaying the bar. Ignored on web. |
|
|
10
|
+
| `input_placeholder` | Input | Hint text for an empty input. Defaults to an empty string. |
|
|
11
|
+
| `input_secure` | Input | Enables password entry. Defaults to `false`. |
|
|
12
|
+
| `input_max_length` | Input | Optional non-negative integer limiting the input length. Does not truncate the initial value. |
|
|
13
|
+
| `getInputElement` | Input | Optional web-only function returning `{ element, getStatus }`. See below. |
|
|
14
|
+
| `onInputChange` | Input | Receives the updated text when the input value changes. |
|
|
15
|
+
| `onInputSelectionChange` | Input | Receives `{ start, end }` when the input selection changes, including cursor movement without text changes. Offsets are zero-based UTF-16 code units; equal offsets indicate the cursor position. On iOS, also reports the selection when opening a session. Custom web inputs must support the DOM selection APIs. |
|
|
16
|
+
| `onInputSubmit` | Input | Receives the current text when the keyboard's return action confirms the input. |
|
|
17
|
+
| `onInputBlur` | Input | Receives the current text when the input loses focus. Submitting, calling `VirtualKeyboard.close()`, or replacing a session does not emit this callback. |
|
|
18
|
+
| `keyboard_mode` | Keyboard | `text`, `numeric`, `decimal`, or `email`. Defaults to `text`. |
|
|
19
|
+
| `keyboard_enter_key_hint` | Keyboard | `enter`, `done`, `go`, `next`, `search`, or `send`. Defaults to `done`. On iOS, `enter` uses the standard Return key; numeric and decimal keyboards do not use this hint. |
|
|
20
|
+
| `onKeyboardChange` | Keyboard | Receives the complete `VirtualKeyboardStatus` when keyboard visibility or geometry changes. On web, requires `navigator.virtualKeyboard`. |
|
|
21
|
+
|
|
22
|
+
On iOS, calling `open` while editing replaces the session's value, options, and
|
|
23
|
+
callbacks while keeping the native input focused. The keyboard configuration is
|
|
24
|
+
refreshed without closing it first. When transferring focus between app inputs,
|
|
25
|
+
call `open` for the next input without calling `close` from the previous input's
|
|
26
|
+
blur handler. Call `close` when focus leaves the inputs.
|
|
27
|
+
|
|
28
|
+
macOS has no on-screen keyboard, as in desktop browsers: `keyboard_visible` stays `false`,
|
|
29
|
+
`onKeyboardChange` is never called, and `keyboard_mode` and `keyboard_enter_key_hint` do not
|
|
30
|
+
apply. Return submits the input. With `input_visible`, the native input bar spans the bottom of
|
|
31
|
+
the window, where the iOS bar rests while its keyboard is hidden; `appearance` sets its appearance.
|
|
32
|
+
Moving the focus to another control emits `onInputBlur`.
|
|
33
|
+
|
|
34
|
+
## Status
|
|
35
|
+
|
|
36
|
+
`VirtualKeyboard.status()` always returns a new `VirtualKeyboardStatus` object with all ten fields,
|
|
37
|
+
including before the first `open` and after both elements are hidden:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
{
|
|
41
|
+
keyboard_visible: false,
|
|
42
|
+
keyboard_screen_x: 0,
|
|
43
|
+
keyboard_screen_y: 0,
|
|
44
|
+
keyboard_width: 0,
|
|
45
|
+
keyboard_height: 0,
|
|
46
|
+
input_visible: false,
|
|
47
|
+
input_screen_x: 0,
|
|
48
|
+
input_screen_y: 0,
|
|
49
|
+
input_width: 0,
|
|
50
|
+
input_height: 0,
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Each hidden element has `visible: false` and zero coordinates and dimensions.
|
|
55
|
+
The two visibility flags are independent. The keyboard rectangle excludes the
|
|
56
|
+
input. On iOS, the input rectangle covers the complete native input bar, including
|
|
57
|
+
its padding, in window points. On macOS, it uses the points of the window's content
|
|
58
|
+
area with a top-left origin, matching the canvas coordinates. With `input_visible: false`,
|
|
59
|
+
all input fields stay zero. On web, keyboard coordinates use viewport CSS pixels; without
|
|
60
|
+
`navigator.virtualKeyboard`, the keyboard fields stay at their hidden values.
|
|
61
|
+
|
|
62
|
+
For a layout with the input bar directly above the keyboard, the app can compute
|
|
63
|
+
its available height without checking for missing metrics:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const status = VirtualKeyboard.status()
|
|
67
|
+
const available_height = viewport_height - status.keyboard_height - status.input_height
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`onKeyboardChange` receives the same complete status, including the input state at
|
|
71
|
+
the time of the keyboard change. Input-only changes do not emit this event. Calling
|
|
72
|
+
`status()` does not emit events. Closing an editing session does not mark the
|
|
73
|
+
keyboard hidden until the platform reports that it is hidden.
|
|
74
|
+
|
|
75
|
+
`VirtualKeyboard` replaces `Keyboard`, and the `VirtualKeyboard*` types replace the `Keyboard*` types.
|
|
76
|
+
`VirtualKeyboard.status()` replaces `Keyboard.metrics()` and `Keyboard.isVisible()`;
|
|
77
|
+
`VirtualKeyboardStatus` replaces `KeyboardMetrics`.
|
|
78
|
+
|
|
79
|
+
## Custom web input
|
|
80
|
+
|
|
81
|
+
`getInputElement(options)` is an optional, synchronous web-only function called on
|
|
82
|
+
every `open` with the complete options object. It returns:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
{
|
|
86
|
+
element: HTMLInputElement,
|
|
87
|
+
getStatus: () => VirtualKeyboardInputStatus,
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`element` must be attached to the DOM and ready to focus. The consumer owns its
|
|
92
|
+
creation, reuse, replacement, styles, configuration (including the element's
|
|
93
|
+
`value` and keyboard attributes), and removal. `VirtualKeyboard` manages focus and session
|
|
94
|
+
events without replacing existing event handlers. Closing a session leaves the
|
|
95
|
+
element in the DOM.
|
|
96
|
+
|
|
97
|
+
`getStatus()` must synchronously return all five input fields:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
{
|
|
101
|
+
input_visible: true,
|
|
102
|
+
input_screen_x: 0,
|
|
103
|
+
input_screen_y: 448,
|
|
104
|
+
input_width: 400,
|
|
105
|
+
input_height: 52,
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It measures the area the app wants to reserve for the input, which may include a
|
|
110
|
+
surrounding container. Use viewport CSS pixels, matching the keyboard fields. The
|
|
111
|
+
function must read the current state without side effects. The runtime calls it on
|
|
112
|
+
every `status()` query and when preparing an `onKeyboardChange` payload. When
|
|
113
|
+
`input_visible` is `false`, the runtime reports zero input coordinates and dimensions.
|
|
114
|
+
|
|
115
|
+
The latest provider remains available to `status()` after editing ends, because a
|
|
116
|
+
consumer-owned input may still be visible. The next `open` replaces it. The
|
|
117
|
+
consumer should update the state read by `getStatus()` when hiding or removing its
|
|
118
|
+
input. No input polling or extra change notifications are used.
|
|
119
|
+
|
|
120
|
+
Without `getInputElement`, `VirtualKeyboard` creates, configures, and reuses its hidden
|
|
121
|
+
input, whose status always has `input_visible: false` and zero geometry. Native
|
|
122
|
+
iOS ignores `getInputElement` and measures its own input bar.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type VirtualKeyboardInputStatus = Readonly<{
|
|
2
|
+
input_visible: boolean
|
|
3
|
+
input_screen_x: number
|
|
4
|
+
input_screen_y: number
|
|
5
|
+
input_width: number
|
|
6
|
+
input_height: number
|
|
7
|
+
}>
|
|
8
|
+
|
|
9
|
+
export type VirtualKeyboardStatus = Readonly<{
|
|
10
|
+
keyboard_visible: boolean
|
|
11
|
+
keyboard_screen_x: number
|
|
12
|
+
keyboard_screen_y: number
|
|
13
|
+
keyboard_width: number
|
|
14
|
+
keyboard_height: number
|
|
15
|
+
}> &
|
|
16
|
+
VirtualKeyboardInputStatus
|
|
17
|
+
|
|
18
|
+
export type VirtualKeyboardOptions = {
|
|
19
|
+
appearance?: 'dark' | 'light' | 'default'
|
|
20
|
+
input_value: string
|
|
21
|
+
input_visible?: boolean
|
|
22
|
+
input_placeholder?: string
|
|
23
|
+
input_secure?: boolean
|
|
24
|
+
input_max_length?: number
|
|
25
|
+
onInputChange?: (value: string) => void
|
|
26
|
+
onInputSelectionChange?: (selection: { start: number; end: number }) => void
|
|
27
|
+
onInputSubmit?: (value: string) => void
|
|
28
|
+
onInputBlur?: (value: string) => void
|
|
29
|
+
keyboard_mode?: 'text' | 'numeric' | 'decimal' | 'email'
|
|
30
|
+
keyboard_enter_key_hint?: 'enter' | 'done' | 'go' | 'next' | 'search' | 'send'
|
|
31
|
+
onKeyboardChange?: (status: VirtualKeyboardStatus) => void
|
|
32
|
+
getInputElement?: (options: VirtualKeyboardOptions) => {
|
|
33
|
+
element: HTMLInputElement
|
|
34
|
+
getStatus: () => VirtualKeyboardInputStatus
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { getUnoModuleNative } from '../utils'
|
|
2
|
+
import type { VirtualKeyboardStatus, VirtualKeyboardOptions } from './types'
|
|
3
|
+
|
|
4
|
+
export type { VirtualKeyboardInputStatus, VirtualKeyboardStatus, VirtualKeyboardOptions } from './types'
|
|
5
|
+
|
|
6
|
+
export const VirtualKeyboard = getUnoModuleNative<{
|
|
7
|
+
open: (options: VirtualKeyboardOptions) => void
|
|
8
|
+
close: () => void
|
|
9
|
+
status: () => VirtualKeyboardStatus
|
|
10
|
+
}>('virtualkeyboard')
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { getGlobalScope } from '../utils'
|
|
2
|
+
import type { VirtualKeyboardInputStatus, VirtualKeyboardStatus, VirtualKeyboardOptions } from './types'
|
|
3
|
+
|
|
4
|
+
export type { VirtualKeyboardInputStatus, VirtualKeyboardStatus, VirtualKeyboardOptions } from './types'
|
|
5
|
+
|
|
6
|
+
const scope = getGlobalScope()
|
|
7
|
+
const HIDDEN_INPUT_STATUS: VirtualKeyboardInputStatus = {
|
|
8
|
+
input_visible: false,
|
|
9
|
+
input_screen_x: 0,
|
|
10
|
+
input_screen_y: 0,
|
|
11
|
+
input_width: 0,
|
|
12
|
+
input_height: 0,
|
|
13
|
+
}
|
|
14
|
+
const HIDDEN_KEYBOARD_STATUS = {
|
|
15
|
+
keyboard_visible: false,
|
|
16
|
+
keyboard_screen_x: 0,
|
|
17
|
+
keyboard_screen_y: 0,
|
|
18
|
+
keyboard_width: 0,
|
|
19
|
+
keyboard_height: 0,
|
|
20
|
+
}
|
|
21
|
+
const state = {
|
|
22
|
+
default_input: null as HTMLInputElement | null,
|
|
23
|
+
closeInput: undefined as (() => void) | undefined,
|
|
24
|
+
editing: false,
|
|
25
|
+
keyboard_status: HIDDEN_KEYBOARD_STATUS,
|
|
26
|
+
getInputStatus: (): VirtualKeyboardInputStatus => HIDDEN_INPUT_STATUS,
|
|
27
|
+
onKeyboardChange: undefined as VirtualKeyboardOptions['onKeyboardChange'],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const virtual_keyboard = scope.navigator?.virtualKeyboard
|
|
31
|
+
|
|
32
|
+
function readKeyboardStatus() {
|
|
33
|
+
const rect = virtual_keyboard.boundingRect
|
|
34
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
35
|
+
return HIDDEN_KEYBOARD_STATUS
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
keyboard_visible: true,
|
|
39
|
+
keyboard_screen_x: rect.x,
|
|
40
|
+
keyboard_screen_y: rect.y,
|
|
41
|
+
keyboard_width: rect.width,
|
|
42
|
+
keyboard_height: rect.height,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function onGeometryChange() {
|
|
47
|
+
const previous_status = state.keyboard_status
|
|
48
|
+
const next_status = readKeyboardStatus()
|
|
49
|
+
if (
|
|
50
|
+
previous_status.keyboard_visible === next_status.keyboard_visible &&
|
|
51
|
+
previous_status.keyboard_screen_x === next_status.keyboard_screen_x &&
|
|
52
|
+
previous_status.keyboard_screen_y === next_status.keyboard_screen_y &&
|
|
53
|
+
previous_status.keyboard_width === next_status.keyboard_width &&
|
|
54
|
+
previous_status.keyboard_height === next_status.keyboard_height
|
|
55
|
+
) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
state.keyboard_status = next_status
|
|
60
|
+
const { onKeyboardChange } = state
|
|
61
|
+
if (!next_status.keyboard_visible && !state.editing) {
|
|
62
|
+
state.onKeyboardChange = undefined
|
|
63
|
+
}
|
|
64
|
+
onKeyboardChange?.(status())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (virtual_keyboard !== undefined) {
|
|
68
|
+
virtual_keyboard.overlaysContent = true
|
|
69
|
+
state.keyboard_status = readKeyboardStatus()
|
|
70
|
+
virtual_keyboard.addEventListener('geometrychange', onGeometryChange)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function status(): VirtualKeyboardStatus {
|
|
74
|
+
const input_status = state.getInputStatus()
|
|
75
|
+
return {
|
|
76
|
+
...state.keyboard_status,
|
|
77
|
+
...(input_status.input_visible ? input_status : HIDDEN_INPUT_STATUS),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createInput(): HTMLInputElement {
|
|
82
|
+
const element = scope.document.createElement('input')
|
|
83
|
+
element.style.position = 'fixed'
|
|
84
|
+
element.style.width = '1px'
|
|
85
|
+
element.style.height = '1px'
|
|
86
|
+
element.style.opacity = '0'
|
|
87
|
+
element.style.pointerEvents = 'none'
|
|
88
|
+
element.style.left = '0'
|
|
89
|
+
element.style.bottom = '0'
|
|
90
|
+
element.tabIndex = -1
|
|
91
|
+
scope.document.body.appendChild(element)
|
|
92
|
+
return element
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getDefaultInputElement({
|
|
96
|
+
input_value,
|
|
97
|
+
input_placeholder = '',
|
|
98
|
+
input_secure = false,
|
|
99
|
+
keyboard_mode = 'text',
|
|
100
|
+
keyboard_enter_key_hint = 'done',
|
|
101
|
+
input_max_length,
|
|
102
|
+
}: VirtualKeyboardOptions) {
|
|
103
|
+
const type = input_secure ? 'password' : 'text'
|
|
104
|
+
if (
|
|
105
|
+
state.default_input !== null &&
|
|
106
|
+
(state.default_input.type !== type ||
|
|
107
|
+
state.default_input.inputMode !== keyboard_mode ||
|
|
108
|
+
state.default_input.enterKeyHint !== keyboard_enter_key_hint)
|
|
109
|
+
) {
|
|
110
|
+
state.default_input.remove()
|
|
111
|
+
state.default_input = null
|
|
112
|
+
}
|
|
113
|
+
if (state.default_input === null) {
|
|
114
|
+
state.default_input = createInput()
|
|
115
|
+
}
|
|
116
|
+
const input = state.default_input
|
|
117
|
+
input.value = input_value
|
|
118
|
+
input.placeholder = input_placeholder
|
|
119
|
+
input.type = type
|
|
120
|
+
input.inputMode = keyboard_mode
|
|
121
|
+
input.enterKeyHint = keyboard_enter_key_hint
|
|
122
|
+
if (input_max_length === undefined) {
|
|
123
|
+
input.removeAttribute('maxlength')
|
|
124
|
+
} else {
|
|
125
|
+
input.maxLength = input_max_length
|
|
126
|
+
}
|
|
127
|
+
return { element: input, getStatus: () => HIDDEN_INPUT_STATUS }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function open(options: VirtualKeyboardOptions) {
|
|
131
|
+
close()
|
|
132
|
+
const {
|
|
133
|
+
getInputElement = getDefaultInputElement,
|
|
134
|
+
appearance = 'default',
|
|
135
|
+
onInputChange,
|
|
136
|
+
onInputSelectionChange,
|
|
137
|
+
onInputSubmit,
|
|
138
|
+
onInputBlur,
|
|
139
|
+
onKeyboardChange,
|
|
140
|
+
} = options
|
|
141
|
+
const { element: input, getStatus } = getInputElement(options)
|
|
142
|
+
input.style.colorScheme = appearance === 'default' ? 'inherit' : appearance
|
|
143
|
+
state.getInputStatus = getStatus
|
|
144
|
+
state.onKeyboardChange = onKeyboardChange
|
|
145
|
+
state.editing = true
|
|
146
|
+
function onBlur() {
|
|
147
|
+
const value = input.value
|
|
148
|
+
close()
|
|
149
|
+
onInputBlur?.(value)
|
|
150
|
+
}
|
|
151
|
+
function onInput() {
|
|
152
|
+
onInputChange?.(input.value)
|
|
153
|
+
}
|
|
154
|
+
function onSelectionChange() {
|
|
155
|
+
const start = input.selectionStart
|
|
156
|
+
const end = input.selectionEnd
|
|
157
|
+
if (start !== null && end !== null) {
|
|
158
|
+
onInputSelectionChange?.({ start, end })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
162
|
+
if (event.key === 'Enter' && !event.isComposing) {
|
|
163
|
+
event.preventDefault()
|
|
164
|
+
const submitted_value = input.value
|
|
165
|
+
close()
|
|
166
|
+
onInputSubmit?.(submitted_value)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
input.addEventListener('blur', onBlur)
|
|
170
|
+
input.addEventListener('input', onInput)
|
|
171
|
+
input.addEventListener('selectionchange', onSelectionChange)
|
|
172
|
+
input.addEventListener('keydown', onKeyDown)
|
|
173
|
+
input.focus({ preventScroll: true })
|
|
174
|
+
state.closeInput = () => {
|
|
175
|
+
input.removeEventListener('blur', onBlur)
|
|
176
|
+
input.removeEventListener('input', onInput)
|
|
177
|
+
input.removeEventListener('selectionchange', onSelectionChange)
|
|
178
|
+
input.removeEventListener('keydown', onKeyDown)
|
|
179
|
+
input.blur()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function close() {
|
|
184
|
+
state.editing = false
|
|
185
|
+
if (!state.keyboard_status.keyboard_visible) {
|
|
186
|
+
state.onKeyboardChange = undefined
|
|
187
|
+
}
|
|
188
|
+
const { closeInput } = state
|
|
189
|
+
state.closeInput = undefined
|
|
190
|
+
closeInput?.()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export const VirtualKeyboard = {
|
|
194
|
+
open,
|
|
195
|
+
close,
|
|
196
|
+
status,
|
|
197
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getUnoModuleNative } from '../utils'
|
|
2
|
+
|
|
3
|
+
const moduleRef = getUnoModuleNative('yoga')
|
|
4
|
+
|
|
5
|
+
async function loadYoga() {
|
|
6
|
+
return {
|
|
7
|
+
Config: moduleRef.Config,
|
|
8
|
+
Node: moduleRef.Node,
|
|
9
|
+
...moduleRef.Constants,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Yoga = {
|
|
14
|
+
loadYoga,
|
|
15
|
+
...moduleRef.Enums,
|
|
16
|
+
}
|
package/yoga/yoga.web.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Yoga from 'yoga-layout/load'
|