@symbiote-native/clipboard 0.0.1 → 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 +180 -26
- package/build/angular/index.d.ts +2 -0
- package/build/angular/index.js +7 -0
- package/build/angular/services/clipboard.service/index.d.ts +6 -0
- package/build/angular/services/clipboard.service/index.js +77 -0
- package/build/core/clipboard.d.ts +86 -0
- package/build/core/clipboard.js +142 -0
- package/build/core/index.d.ts +3 -0
- package/build/core/index.js +2 -0
- package/build/core/native-module.d.ts +17 -0
- package/build/core/native-module.js +7 -0
- package/build/core/types.d.ts +67 -0
- package/build/core/types.js +28 -0
- package/build/react/hooks/use-clipboard/index.d.ts +2 -0
- package/build/react/hooks/use-clipboard/index.js +14 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.js +8 -0
- package/build/svelte/index.d.ts +2 -0
- package/build/svelte/index.js +7 -0
- package/build/svelte/runes/use-clipboard.svelte.d.ts +4 -0
- package/build/svelte/runes/use-clipboard.svelte.js +30 -0
- package/build/vue/composables/use-clipboard/index.d.ts +3 -0
- package/build/vue/composables/use-clipboard/index.js +20 -0
- package/build/vue/index.d.ts +2 -0
- package/build/vue/index.js +8 -0
- package/build-ngc/angular/index.d.ts +2 -0
- package/build-ngc/angular/index.js +8 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/angular/services/clipboard.service/index.d.ts +9 -0
- package/build-ngc/angular/services/clipboard.service/index.js +36 -0
- package/build-ngc/angular/services/clipboard.service/index.js.map +1 -0
- package/build-ngc/core/clipboard.d.ts +86 -0
- package/build-ngc/core/clipboard.js +143 -0
- package/build-ngc/core/clipboard.js.map +1 -0
- package/build-ngc/core/index.d.ts +3 -0
- package/build-ngc/core/index.js +3 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +17 -0
- package/build-ngc/core/native-module.js +8 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/types.d.ts +67 -0
- package/build-ngc/core/types.js +29 -0
- package/build-ngc/core/types.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +106 -1
- package/src/angular/index.ts +8 -0
- package/src/angular/services/clipboard.service/index.ts +35 -0
- package/src/core/clipboard.ts +168 -0
- package/src/core/index.ts +23 -0
- package/src/core/native-module.ts +42 -0
- package/src/core/types.ts +81 -0
- package/src/react/hooks/use-clipboard/index.ts +17 -0
- package/src/react/index.ts +9 -0
- package/src/svelte/index.ts +8 -0
- package/src/svelte/runes/use-clipboard.svelte.ts +33 -0
- package/src/svelte/svelte-ambient.d.ts +6 -0
- package/src/vue/composables/use-clipboard/index.ts +25 -0
- package/src/vue/index.ts +9 -0
package/README.md
CHANGED
|
@@ -1,44 +1,198 @@
|
|
|
1
1
|
# @symbiote-native/clipboard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
|
+
[`expo-clipboard`](https://docs.expo.dev/versions/latest/sdk/clipboard/) — read/write clipboard
|
|
5
|
+
text, URLs, and images, plus a clipboard-change listener — usable from **every** adapter, React,
|
|
6
|
+
Vue, and Angular, not just React. Unlike `@symbiote-native/sensors`, which is all
|
|
7
|
+
`DeviceSensor`-shaped classes plus one free-function module (`Pedometer`), clipboard is closer to
|
|
8
|
+
`@symbiote-native/local-auth`'s shape — mostly stateless async functions — plus **one**
|
|
9
|
+
listener-based subscription (`addClipboardListener`) that each adapter wraps in its own
|
|
10
|
+
mount/unmount lifecycle (`useClipboard`).
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
for [SymbioteNative](../../README.md) — read/write clipboard text, URLs, and images, plus a
|
|
7
|
-
clipboard-change listener, reachable from every adapter (React, Vue, Angular), not just React.
|
|
8
|
-
Nothing beyond this `package.json` + `README.md` exists yet.
|
|
12
|
+
## Install
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
```bash
|
|
15
|
+
npm install @symbiote-native/clipboard
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Depends on `expo-clipboard` and `expo-modules-core` directly (regular dependencies, pinned to an
|
|
19
|
+
exact version — never a caret range, since this package's `core/` is hand-ported against one
|
|
20
|
+
specific native API shape and a newer resolve could silently drift the two apart). Never install
|
|
21
|
+
`expo-clipboard` yourself, and never add the `expo` meta-package to this project — it bundles its
|
|
22
|
+
own Metro/Babel pipeline that conflicts with this project's own.
|
|
23
|
+
|
|
24
|
+
## Required one-time step: native autolinking wiring
|
|
25
|
+
|
|
26
|
+
Unlike a plain RN native module, `expo-clipboard`'s native code is discovered by
|
|
27
|
+
`expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism — this needs wiring
|
|
28
|
+
into the native host app **once**, covering this package and every other `expo-modules-core`
|
|
29
|
+
package (`@symbiote-native/sensors`, `@symbiote-native/local-auth`) with zero further changes:
|
|
30
|
+
|
|
31
|
+
| Platform | Touches |
|
|
32
|
+
|---|---|
|
|
33
|
+
| iOS | `ios/Podfile` — add `use_expo_modules!` |
|
|
34
|
+
| iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
|
|
35
|
+
| Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
|
|
36
|
+
| Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
|
|
37
|
+
|
|
38
|
+
Full mechanics live in the `symbiote-expo-native-module` skill. Clipboard itself needs no
|
|
39
|
+
`Info.plist`/`AndroidManifest.xml` permission entry on either platform — reading/writing the
|
|
40
|
+
clipboard requires no platform permission string.
|
|
41
|
+
|
|
42
|
+
## Shape
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/core/ getStringAsync/setStringAsync/hasStringAsync, getUrlAsync/setUrlAsync/
|
|
46
|
+
hasUrlAsync (iOS only), getImageAsync/setImageAsync/hasImageAsync, and
|
|
47
|
+
addClipboardListener/removeClipboardListener. native-module.ts resolves
|
|
48
|
+
the ExpoClipboard native module via expo-modules-core's
|
|
49
|
+
requireNativeModule.
|
|
50
|
+
src/react/hooks/ @symbiote-native/clipboard/react — useClipboard
|
|
51
|
+
src/vue/composables/ @symbiote-native/clipboard/vue — useClipboard (same name)
|
|
52
|
+
src/angular/services/ @symbiote-native/clipboard/angular — ClipboardService
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every function except the listener is plain stateless async and re-exported as-is. The listener
|
|
56
|
+
subscription (`addClipboardListener`) lives once in `core`, framework-agnostic; each adapter's
|
|
57
|
+
`useClipboard` hook/composable/`ClipboardService.connect()` is a thin mount/unmount (or DI-scoped
|
|
58
|
+
`effect()`, for Angular) wrapper around that same subscription — the plumbing is written once and
|
|
59
|
+
shared by all three.
|
|
60
|
+
|
|
61
|
+
## Use it
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { getStringAsync, setStringAsync, addClipboardListener } from '@symbiote-native/clipboard';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`useClipboard()`'s event only carries the clipboard's changed content *types*
|
|
68
|
+
(`IClipboardEvent.contentTypes`), never the string itself — every adapter's demo screen treats a
|
|
69
|
+
change as a cue to re-fetch via `getStringAsync()`, not a value to render directly.
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// React
|
|
73
|
+
import { useEffect, useState } from 'react';
|
|
74
|
+
import { getStringAsync, setStringAsync } from '@symbiote-native/clipboard';
|
|
75
|
+
import { useClipboard } from '@symbiote-native/clipboard/react';
|
|
76
|
+
|
|
77
|
+
function ClipboardScreen() {
|
|
78
|
+
const clipboardEvent = useClipboard(); // IClipboardEvent | null
|
|
79
|
+
const [text, setText] = useState<string | null>(null);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
getStringAsync().then(setText);
|
|
83
|
+
}, [clipboardEvent]);
|
|
84
|
+
|
|
85
|
+
const handleCopy = (input: string) => setStringAsync(input).then(() => getStringAsync().then(setText));
|
|
86
|
+
|
|
87
|
+
return <Text>{text ?? 'checking…'}</Text>;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```vue
|
|
92
|
+
<!-- Vue -->
|
|
93
|
+
<script setup lang="ts">
|
|
94
|
+
import { onMounted, ref, watch } from 'vue';
|
|
95
|
+
import { getStringAsync, setStringAsync } from '@symbiote-native/clipboard';
|
|
96
|
+
import { useClipboard } from '@symbiote-native/clipboard/vue';
|
|
97
|
+
|
|
98
|
+
const text = ref('checking…');
|
|
99
|
+
function refresh(): void {
|
|
100
|
+
void getStringAsync().then(value => { text.value = value; });
|
|
101
|
+
}
|
|
102
|
+
onMounted(refresh);
|
|
11
103
|
|
|
12
|
-
|
|
13
|
-
|
|
104
|
+
const clipboardEvent = useClipboard(); // Ref<IClipboardEvent | null>
|
|
105
|
+
watch(clipboardEvent, event => { if (event) refresh(); });
|
|
106
|
+
|
|
107
|
+
function handleCopy(input: string): void {
|
|
108
|
+
void setStringAsync(input).then(refresh);
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
<template>
|
|
112
|
+
<Text>{{ text }}</Text>
|
|
113
|
+
</template>
|
|
114
|
+
```
|
|
14
115
|
|
|
15
116
|
```ts
|
|
16
|
-
|
|
17
|
-
|
|
117
|
+
// Angular
|
|
118
|
+
import { Component, Injector, effect, inject, signal } from '@angular/core';
|
|
119
|
+
import { ClipboardService, getStringAsync, setStringAsync } from '@symbiote-native/clipboard/angular';
|
|
120
|
+
|
|
121
|
+
@Component({ /* ... */ })
|
|
122
|
+
export class ClipboardScreen {
|
|
123
|
+
private readonly injector = inject(Injector);
|
|
124
|
+
private readonly clipboardEvent = inject(ClipboardService).connect(); // Signal<IClipboardEvent | null>
|
|
125
|
+
readonly text = signal('checking…');
|
|
126
|
+
|
|
127
|
+
constructor() {
|
|
128
|
+
this.refresh();
|
|
129
|
+
effect(() => { if (this.clipboardEvent() !== null) this.refresh(); }, { injector: this.injector });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
handleCopy(input: string): void {
|
|
133
|
+
setStringAsync(input).then(() => this.refresh());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private refresh(): void {
|
|
137
|
+
getStringAsync().then(value => this.text.set(value));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
These are trimmed from the real demo screens — `examples/expo-react/screens/ClipboardScreen.tsx`,
|
|
143
|
+
`examples/expo-vue-sfc/screens/ClipboardScreen.vue`, `examples/expo-vue-tsx/screens/ClipboardScreen.tsx`,
|
|
144
|
+
`examples/expo-angular/src/screens/ClipboardScreen.ts` — which additionally show `hasStringAsync()`
|
|
145
|
+
status badges and, iOS-only, the `getUrlAsync`/`setUrlAsync`/`hasUrlAsync` URL surface.
|
|
146
|
+
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
getStringAsync(options?: IGetStringOptions): Promise<string>
|
|
151
|
+
setStringAsync(text: string, options?: ISetStringOptions): Promise<boolean>
|
|
18
152
|
hasStringAsync(): Promise<boolean>
|
|
19
153
|
getUrlAsync(): Promise<string | null> // iOS only
|
|
20
154
|
setUrlAsync(url: string): Promise<void> // iOS only
|
|
21
155
|
hasUrlAsync(): Promise<boolean> // iOS only
|
|
22
|
-
getImageAsync(options:
|
|
156
|
+
getImageAsync(options: IGetImageOptions): Promise<IClipboardImage | null>
|
|
23
157
|
setImageAsync(base64Image: string): Promise<void>
|
|
24
158
|
hasImageAsync(): Promise<boolean>
|
|
25
|
-
addClipboardListener(listener: (event:
|
|
159
|
+
addClipboardListener(listener: (event: IClipboardEvent) => void): EventSubscription
|
|
26
160
|
removeClipboardListener(subscription: EventSubscription) // deprecated, use subscription.remove()
|
|
27
161
|
```
|
|
28
162
|
|
|
29
|
-
Plus `ContentType`, `StringFormat`, `
|
|
30
|
-
`
|
|
31
|
-
this repo's `I`-prefix convention for exported types
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
163
|
+
Plus `ContentType`, `StringFormat`, `IGetStringOptions`, `ISetStringOptions`, `IGetImageOptions`,
|
|
164
|
+
`IClipboardImage`, `IClipboardEvent` — ported from upstream's `Clipboard.types.ts`, renamed with
|
|
165
|
+
this repo's `I`-prefix convention for exported types (`ts-js-best-practices`); `ContentType` and
|
|
166
|
+
`StringFormat` stay unprefixed enums, matching `AuthenticationType`/`SecurityLevel` in
|
|
167
|
+
`@symbiote-native/local-auth`.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// React
|
|
171
|
+
import { useClipboard } from '@symbiote-native/clipboard/react';
|
|
172
|
+
const clipboardEvent = useClipboard(); // IClipboardEvent | null
|
|
173
|
+
|
|
174
|
+
// Vue
|
|
175
|
+
import { useClipboard } from '@symbiote-native/clipboard/vue';
|
|
176
|
+
const clipboardEvent = useClipboard(); // Ref<IClipboardEvent | null>
|
|
177
|
+
|
|
178
|
+
// Angular
|
|
179
|
+
import { ClipboardService } from '@symbiote-native/clipboard/angular';
|
|
180
|
+
readonly clipboardEvent = inject(ClipboardService).connect(); // Signal<IClipboardEvent | null>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`ClipboardPasteButton` (upstream's native paste-button view component, iOS 16+) is **not**
|
|
184
|
+
ported — out of scope for this pass. If it's ever wrapped, it follows
|
|
185
|
+
`symbiote-third-party-native-view`, not this package's `expo-modules-core` recipe.
|
|
186
|
+
|
|
187
|
+
## Test it
|
|
35
188
|
|
|
36
|
-
|
|
189
|
+
Tests exercise the JS layer only, against a fake native module in place of the real
|
|
190
|
+
`requireNativeModule` resolution (`src/core/**/*.test.ts`,
|
|
191
|
+
`src/{react,vue,angular}/**/*.test.{ts,tsx}`) — no Fabric/Descriptor angle at all, since clipboard
|
|
192
|
+
is a pure async-function + one-listener surface, never a view. Native rendering itself is verified
|
|
193
|
+
on-device (see the parent [README](../../README.md) for the project's testing model).
|
|
37
194
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`expo` meta-package, why upstream JS is hand-ported rather than imported, how autolinking picks
|
|
43
|
-
up the native module) are documented in the
|
|
44
|
-
[`symbiote-expo-native-module`](../../.claude/skills/symbiote-expo-native-module/SKILL.md) skill.
|
|
195
|
+
Native autolinking wiring (Android's 3-layer registration, iOS Podfile/pod install) is done across
|
|
196
|
+
all four `examples/expo-*` canary apps. It isn't wired into the public non-Expo canaries
|
|
197
|
+
(`examples/react`, `examples/vue-sfc`, `examples/vue-tsx`, `examples/angular`) yet — those don't
|
|
198
|
+
depend on any `expo-modules-core` package today.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @symbiote-native/clipboard/angular: the Angular entry over the framework-agnostic core.
|
|
2
|
+
// Unlike @symbiote-native/local-auth (all stateless functions, plain re-export), clipboard has
|
|
3
|
+
// one listener-based piece — ClipboardService.connect() is the Angular-only lifecycle half; the
|
|
4
|
+
// addClipboardListener subscription plumbing all lives in core, shared with React/Vue. Mirrors
|
|
5
|
+
// @symbiote-native/sensors' AccelerometerService.
|
|
6
|
+
export { ClipboardService } from './services/clipboard.service/index.js';
|
|
7
|
+
export * from '../core/index.js';
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
2
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
3
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
4
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
5
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
6
|
+
var _, done = false;
|
|
7
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
8
|
+
var context = {};
|
|
9
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
10
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
11
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
12
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
13
|
+
if (kind === "accessor") {
|
|
14
|
+
if (result === void 0) continue;
|
|
15
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
16
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
17
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
18
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
19
|
+
}
|
|
20
|
+
else if (_ = accept(result)) {
|
|
21
|
+
if (kind === "field") initializers.unshift(_);
|
|
22
|
+
else descriptor[key] = _;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
26
|
+
done = true;
|
|
27
|
+
};
|
|
28
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
29
|
+
var useValue = arguments.length > 2;
|
|
30
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
31
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
32
|
+
}
|
|
33
|
+
return useValue ? value : void 0;
|
|
34
|
+
};
|
|
35
|
+
import { effect, inject, Injectable, Injector, signal } from '@angular/core';
|
|
36
|
+
import { addClipboardListener } from '../../../core/index.js';
|
|
37
|
+
// Angular twin of React's `useClipboard` hook and Vue's `useClipboard` composable. Angular has
|
|
38
|
+
// no per-instance hook — state and lifecycle live in DI instead, so `connect()` stands in for
|
|
39
|
+
// the hook's role: call it ONCE (typically from a component's field initializer, inside an
|
|
40
|
+
// injection context).
|
|
41
|
+
//
|
|
42
|
+
// readonly clipboardEvent = inject(ClipboardService).connect();
|
|
43
|
+
// // template: {{ clipboardEvent()?.contentTypes }}
|
|
44
|
+
//
|
|
45
|
+
// Mirrors AccelerometerService.connect() from @symbiote-native/sensors: the subscription doesn't
|
|
46
|
+
// depend on anything the caller's own signals could change between renders, so a single
|
|
47
|
+
// effect() that subscribes once and cleans up once is enough.
|
|
48
|
+
let ClipboardService = (() => {
|
|
49
|
+
let _classDecorators = [Injectable({ providedIn: 'root' })];
|
|
50
|
+
let _classDescriptor;
|
|
51
|
+
let _classExtraInitializers = [];
|
|
52
|
+
let _classThis;
|
|
53
|
+
var ClipboardService = class {
|
|
54
|
+
static { _classThis = this; }
|
|
55
|
+
static {
|
|
56
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
57
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
58
|
+
ClipboardService = _classThis = _classDescriptor.value;
|
|
59
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
60
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
61
|
+
}
|
|
62
|
+
// Captured in the constructor (itself always run inside an injection context by Angular's own
|
|
63
|
+
// DI) so `connect()` can create an `effect()` even when called from plain field-initializer
|
|
64
|
+
// code that is not, on its own, an active injection context — mirrors AccelerometerService.
|
|
65
|
+
injector = inject(Injector);
|
|
66
|
+
connect() {
|
|
67
|
+
const event = signal(null);
|
|
68
|
+
effect(onCleanup => {
|
|
69
|
+
const subscription = addClipboardListener(next => event.set(next));
|
|
70
|
+
onCleanup(() => subscription.remove());
|
|
71
|
+
}, { injector: this.injector });
|
|
72
|
+
return event.asReadonly();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return ClipboardService = _classThis;
|
|
76
|
+
})();
|
|
77
|
+
export { ClipboardService };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { IClipboardEvent, IClipboardImage, IGetImageOptions, IGetStringOptions, ISetStringOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Gets the content of the user's clipboard.
|
|
5
|
+
*
|
|
6
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns an empty string.
|
|
7
|
+
* Due to iOS platform limitations, there is no way to distinguish between an empty clipboard
|
|
8
|
+
* and denied permission.
|
|
9
|
+
*
|
|
10
|
+
* @param options Options for the clipboard content to be retrieved.
|
|
11
|
+
* @returns A promise that resolves to the content of the clipboard, or an empty string if the
|
|
12
|
+
* clipboard is empty or permission was denied.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getStringAsync(options?: IGetStringOptions): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* Sets the content of the user's clipboard.
|
|
17
|
+
*
|
|
18
|
+
* @param text The string to save to the clipboard.
|
|
19
|
+
* @param options Options for the clipboard content to be set.
|
|
20
|
+
* @returns A promise that resolves to `true` once the string has been saved to the clipboard.
|
|
21
|
+
*/
|
|
22
|
+
export declare function setStringAsync(text: string, options?: ISetStringOptions): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Returns whether the clipboard has text content. Returns `true` for both plain text and rich
|
|
25
|
+
* text (e.g. HTML).
|
|
26
|
+
*/
|
|
27
|
+
export declare function hasStringAsync(): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Gets the URL from the user's clipboard.
|
|
30
|
+
*
|
|
31
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
|
|
32
|
+
* platform limitations, there is no way to distinguish between no URL in clipboard and denied
|
|
33
|
+
* permission.
|
|
34
|
+
* @platform ios
|
|
35
|
+
*/
|
|
36
|
+
export declare function getUrlAsync(): Promise<string | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Sets a URL in the user's clipboard. Behaves the same as `setStringAsync`, except that it sets
|
|
39
|
+
* the clipboard content type to be a URL, letting your app or other apps know that the clipboard
|
|
40
|
+
* contains a URL and behave accordingly.
|
|
41
|
+
* @platform ios
|
|
42
|
+
*/
|
|
43
|
+
export declare function setUrlAsync(url: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Returns whether the clipboard has URL content.
|
|
46
|
+
* @platform ios
|
|
47
|
+
*/
|
|
48
|
+
export declare function hasUrlAsync(): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Gets the image from the user's clipboard and returns it in the specified format.
|
|
51
|
+
*
|
|
52
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
|
|
53
|
+
* platform limitations, there is no way to distinguish between no image in clipboard and denied
|
|
54
|
+
* permission.
|
|
55
|
+
*
|
|
56
|
+
* @param options Specifies the desired format of the image.
|
|
57
|
+
* @returns If there was an image in the clipboard, resolves to an `IClipboardImage` object
|
|
58
|
+
* containing the base64 string and metadata of the image. Otherwise resolves to `null` (this
|
|
59
|
+
* includes cases where permission was denied).
|
|
60
|
+
*/
|
|
61
|
+
export declare function getImageAsync(options: IGetImageOptions): Promise<IClipboardImage | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Sets an image in the user's clipboard.
|
|
64
|
+
*
|
|
65
|
+
* @param base64Image Image encoded as a base64 string, without MIME type.
|
|
66
|
+
*/
|
|
67
|
+
export declare function setImageAsync(base64Image: string): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Returns whether the clipboard has image content.
|
|
70
|
+
*/
|
|
71
|
+
export declare function hasImageAsync(): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Adds a listener that fires whenever the content of the user's clipboard changes. Kept here at
|
|
74
|
+
* the core level, framework-agnostic, exactly like `Accelerometer.addListener` lives in
|
|
75
|
+
* `@symbiote-native/sensors`' core — each adapter's `useClipboard` hook/composable/service wraps
|
|
76
|
+
* this in its own mount/unmount lifecycle rather than reimplementing the subscription.
|
|
77
|
+
*
|
|
78
|
+
* @param listener Callback invoked with an `IClipboardEvent` describing the new clipboard
|
|
79
|
+
* content types whenever the clipboard changes.
|
|
80
|
+
*/
|
|
81
|
+
export declare function addClipboardListener(listener: (event: IClipboardEvent) => void): EventSubscription;
|
|
82
|
+
/**
|
|
83
|
+
* Removes the listener added by `addClipboardListener`.
|
|
84
|
+
* @deprecated use `subscription.remove()` instead.
|
|
85
|
+
*/
|
|
86
|
+
export declare function removeClipboardListener(subscription: EventSubscription): void;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { UnavailabilityError } from 'expo-modules-core';
|
|
2
|
+
import { CLIPBOARD_CHANGED_EVENT_NAME, expoClipboard } from './native-module.js';
|
|
3
|
+
// Matches the module name upstream's own Clipboard.ts passes to every UnavailabilityError.
|
|
4
|
+
const NATIVE_MODULE_NAME = 'Clipboard';
|
|
5
|
+
/**
|
|
6
|
+
* Gets the content of the user's clipboard.
|
|
7
|
+
*
|
|
8
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns an empty string.
|
|
9
|
+
* Due to iOS platform limitations, there is no way to distinguish between an empty clipboard
|
|
10
|
+
* and denied permission.
|
|
11
|
+
*
|
|
12
|
+
* @param options Options for the clipboard content to be retrieved.
|
|
13
|
+
* @returns A promise that resolves to the content of the clipboard, or an empty string if the
|
|
14
|
+
* clipboard is empty or permission was denied.
|
|
15
|
+
*/
|
|
16
|
+
export async function getStringAsync(options = {}) {
|
|
17
|
+
if (!expoClipboard.getStringAsync) {
|
|
18
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getStringAsync');
|
|
19
|
+
}
|
|
20
|
+
return expoClipboard.getStringAsync(options);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sets the content of the user's clipboard.
|
|
24
|
+
*
|
|
25
|
+
* @param text The string to save to the clipboard.
|
|
26
|
+
* @param options Options for the clipboard content to be set.
|
|
27
|
+
* @returns A promise that resolves to `true` once the string has been saved to the clipboard.
|
|
28
|
+
*/
|
|
29
|
+
export async function setStringAsync(text, options = {}) {
|
|
30
|
+
if (!expoClipboard.setStringAsync) {
|
|
31
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setStringAsync');
|
|
32
|
+
}
|
|
33
|
+
return expoClipboard.setStringAsync(text, options);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Returns whether the clipboard has text content. Returns `true` for both plain text and rich
|
|
37
|
+
* text (e.g. HTML).
|
|
38
|
+
*/
|
|
39
|
+
// DELIBERATE DIVERGENCE from expo: Clipboard.ts:57 declares this one plain `function`, not
|
|
40
|
+
// `async` like its 8 siblings, so on a build without the native method the UnavailabilityError
|
|
41
|
+
// escapes synchronously at the call site and `hasStringAsync().catch(handler)` never catches it.
|
|
42
|
+
// Parity would hand callers a guard that fires differently from every other method in this API,
|
|
43
|
+
// so we add `async` here instead of tagging it. Everything else stays a verbatim port.
|
|
44
|
+
export async function hasStringAsync() {
|
|
45
|
+
if (!expoClipboard.hasStringAsync) {
|
|
46
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasStringAsync');
|
|
47
|
+
}
|
|
48
|
+
return expoClipboard.hasStringAsync();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Gets the URL from the user's clipboard.
|
|
52
|
+
*
|
|
53
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
|
|
54
|
+
* platform limitations, there is no way to distinguish between no URL in clipboard and denied
|
|
55
|
+
* permission.
|
|
56
|
+
* @platform ios
|
|
57
|
+
*/
|
|
58
|
+
export async function getUrlAsync() {
|
|
59
|
+
if (!expoClipboard.getUrlAsync) {
|
|
60
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getUrlAsync');
|
|
61
|
+
}
|
|
62
|
+
return expoClipboard.getUrlAsync();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sets a URL in the user's clipboard. Behaves the same as `setStringAsync`, except that it sets
|
|
66
|
+
* the clipboard content type to be a URL, letting your app or other apps know that the clipboard
|
|
67
|
+
* contains a URL and behave accordingly.
|
|
68
|
+
* @platform ios
|
|
69
|
+
*/
|
|
70
|
+
export async function setUrlAsync(url) {
|
|
71
|
+
if (!expoClipboard.setUrlAsync) {
|
|
72
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setUrlAsync');
|
|
73
|
+
}
|
|
74
|
+
return expoClipboard.setUrlAsync(url);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns whether the clipboard has URL content.
|
|
78
|
+
* @platform ios
|
|
79
|
+
*/
|
|
80
|
+
export async function hasUrlAsync() {
|
|
81
|
+
if (!expoClipboard.hasUrlAsync) {
|
|
82
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasUrlAsync');
|
|
83
|
+
}
|
|
84
|
+
return expoClipboard.hasUrlAsync();
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Gets the image from the user's clipboard and returns it in the specified format.
|
|
88
|
+
*
|
|
89
|
+
* Note: On iOS 16+, if the user denies paste permission, this method returns `null`. Due to iOS
|
|
90
|
+
* platform limitations, there is no way to distinguish between no image in clipboard and denied
|
|
91
|
+
* permission.
|
|
92
|
+
*
|
|
93
|
+
* @param options Specifies the desired format of the image.
|
|
94
|
+
* @returns If there was an image in the clipboard, resolves to an `IClipboardImage` object
|
|
95
|
+
* containing the base64 string and metadata of the image. Otherwise resolves to `null` (this
|
|
96
|
+
* includes cases where permission was denied).
|
|
97
|
+
*/
|
|
98
|
+
export async function getImageAsync(options) {
|
|
99
|
+
if (!expoClipboard.getImageAsync) {
|
|
100
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'getImageAsync');
|
|
101
|
+
}
|
|
102
|
+
return expoClipboard.getImageAsync(options);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Sets an image in the user's clipboard.
|
|
106
|
+
*
|
|
107
|
+
* @param base64Image Image encoded as a base64 string, without MIME type.
|
|
108
|
+
*/
|
|
109
|
+
export async function setImageAsync(base64Image) {
|
|
110
|
+
if (!expoClipboard.setImageAsync) {
|
|
111
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'setImageAsync');
|
|
112
|
+
}
|
|
113
|
+
return expoClipboard.setImageAsync(base64Image);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Returns whether the clipboard has image content.
|
|
117
|
+
*/
|
|
118
|
+
export async function hasImageAsync() {
|
|
119
|
+
if (!expoClipboard.hasImageAsync) {
|
|
120
|
+
throw new UnavailabilityError(NATIVE_MODULE_NAME, 'hasImageAsync');
|
|
121
|
+
}
|
|
122
|
+
return expoClipboard.hasImageAsync();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Adds a listener that fires whenever the content of the user's clipboard changes. Kept here at
|
|
126
|
+
* the core level, framework-agnostic, exactly like `Accelerometer.addListener` lives in
|
|
127
|
+
* `@symbiote-native/sensors`' core — each adapter's `useClipboard` hook/composable/service wraps
|
|
128
|
+
* this in its own mount/unmount lifecycle rather than reimplementing the subscription.
|
|
129
|
+
*
|
|
130
|
+
* @param listener Callback invoked with an `IClipboardEvent` describing the new clipboard
|
|
131
|
+
* content types whenever the clipboard changes.
|
|
132
|
+
*/
|
|
133
|
+
export function addClipboardListener(listener) {
|
|
134
|
+
return expoClipboard.addListener(CLIPBOARD_CHANGED_EVENT_NAME, listener);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Removes the listener added by `addClipboardListener`.
|
|
138
|
+
* @deprecated use `subscription.remove()` instead.
|
|
139
|
+
*/
|
|
140
|
+
export function removeClipboardListener(subscription) {
|
|
141
|
+
subscription.remove();
|
|
142
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { getStringAsync, setStringAsync, hasStringAsync, getUrlAsync, setUrlAsync, hasUrlAsync, getImageAsync, setImageAsync, hasImageAsync, addClipboardListener, removeClipboardListener, } from './clipboard';
|
|
2
|
+
export { ContentType, StringFormat, type IGetStringOptions, type ISetStringOptions, type IGetImageOptions, type IClipboardImage, type IClipboardEvent, } from './types';
|
|
3
|
+
export type { EventSubscription } from 'expo-modules-core';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EventSubscription } from 'expo-modules-core';
|
|
2
|
+
import type { IClipboardEvent, IClipboardImage, IGetImageOptions, IGetStringOptions, ISetStringOptions } from './types';
|
|
3
|
+
export declare const CLIPBOARD_CHANGED_EVENT_NAME = "onClipboardChanged";
|
|
4
|
+
export type INativeClipboardModule = {
|
|
5
|
+
addListener(eventName: typeof CLIPBOARD_CHANGED_EVENT_NAME, listener: (event: IClipboardEvent) => void): EventSubscription;
|
|
6
|
+
removeAllListeners(eventName: typeof CLIPBOARD_CHANGED_EVENT_NAME): void;
|
|
7
|
+
getStringAsync?(options?: IGetStringOptions): Promise<string>;
|
|
8
|
+
setStringAsync?(text: string, options?: ISetStringOptions): Promise<boolean>;
|
|
9
|
+
hasStringAsync?(): Promise<boolean>;
|
|
10
|
+
getUrlAsync?(): Promise<string | null>;
|
|
11
|
+
setUrlAsync?(url: string): Promise<void>;
|
|
12
|
+
hasUrlAsync?(): Promise<boolean>;
|
|
13
|
+
getImageAsync?(options: IGetImageOptions): Promise<IClipboardImage | null>;
|
|
14
|
+
setImageAsync?(base64Image: string): Promise<void>;
|
|
15
|
+
hasImageAsync?(): Promise<boolean>;
|
|
16
|
+
};
|
|
17
|
+
export declare const expoClipboard: INativeClipboardModule;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
const EXPO_CLIPBOARD_MODULE_NAME = 'ExpoClipboard';
|
|
3
|
+
// The event name upstream's own ExpoClipboard.ts pairs with the native module's event map —
|
|
4
|
+
// kept here, next to the module resolution, since core/clipboard.ts's addClipboardListener is
|
|
5
|
+
// the only caller.
|
|
6
|
+
export const CLIPBOARD_CHANGED_EVENT_NAME = 'onClipboardChanged';
|
|
7
|
+
export const expoClipboard = requireNativeModule(EXPO_CLIPBOARD_MODULE_NAME);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type used to define what type of data is stored in the clipboard.
|
|
3
|
+
*/
|
|
4
|
+
export declare enum ContentType {
|
|
5
|
+
PLAIN_TEXT = "plain-text",
|
|
6
|
+
HTML = "html",
|
|
7
|
+
IMAGE = "image",
|
|
8
|
+
/**
|
|
9
|
+
* @platform iOS
|
|
10
|
+
*/
|
|
11
|
+
URL = "url"
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Type used to determine string format stored in the clipboard.
|
|
15
|
+
*/
|
|
16
|
+
export declare enum StringFormat {
|
|
17
|
+
PLAIN_TEXT = "plainText",
|
|
18
|
+
HTML = "html"
|
|
19
|
+
}
|
|
20
|
+
export type IGetStringOptions = {
|
|
21
|
+
/**
|
|
22
|
+
* The target format of the clipboard string to be converted to, if possible.
|
|
23
|
+
* @default StringFormat.PLAIN_TEXT
|
|
24
|
+
*/
|
|
25
|
+
preferredFormat?: StringFormat;
|
|
26
|
+
};
|
|
27
|
+
export type ISetStringOptions = {
|
|
28
|
+
/**
|
|
29
|
+
* The input format of the provided string. Adjusting this option can help other applications
|
|
30
|
+
* interpret copied string properly.
|
|
31
|
+
* @default StringFormat.PLAIN_TEXT
|
|
32
|
+
*/
|
|
33
|
+
inputFormat?: StringFormat;
|
|
34
|
+
};
|
|
35
|
+
export type IGetImageOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* The format of the clipboard image to be converted to.
|
|
38
|
+
*/
|
|
39
|
+
format: 'png' | 'jpeg';
|
|
40
|
+
/**
|
|
41
|
+
* Specify the quality of the returned image, between `0` and `1`. Defaults to `1` (highest
|
|
42
|
+
* quality). Applicable only when `format` is set to `jpeg`, ignored otherwise.
|
|
43
|
+
* @default 1
|
|
44
|
+
*/
|
|
45
|
+
jpegQuality?: number;
|
|
46
|
+
};
|
|
47
|
+
export type IClipboardImage = {
|
|
48
|
+
/**
|
|
49
|
+
* A Base64-encoded string of the image data, already prepended with a
|
|
50
|
+
* `data:image/png;base64,` or `data:image/jpeg;base64,` prefix. Its format depends on the
|
|
51
|
+
* `format` option passed to `getImageAsync`.
|
|
52
|
+
*/
|
|
53
|
+
data: string;
|
|
54
|
+
/**
|
|
55
|
+
* Dimensions (`width` and `height`) of the image pasted from clipboard.
|
|
56
|
+
*/
|
|
57
|
+
size: {
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export type IClipboardEvent = {
|
|
63
|
+
/**
|
|
64
|
+
* An array of content types that are available on the clipboard.
|
|
65
|
+
*/
|
|
66
|
+
contentTypes: ContentType[];
|
|
67
|
+
};
|