|
| 1 | +# Rich Paste |
| 2 | + |
| 3 | +Rich paste and drag-and-drop support for NativeScript text inputs. Handles images, GIFs, files, and text from the clipboard with one shared TypeScript API across iOS and Android. |
| 4 | + |
| 5 | +The plugin supports: |
| 6 | + |
| 7 | +- Pasting images, GIFs, and static images from the clipboard |
| 8 | +- Pasting files (PDF, RTF, HTML) from the clipboard |
| 9 | +- Drag-and-drop of images and files into text inputs |
| 10 | +- MIME-based accept filtering (`all`, `image/*`, `text/plain`, etc.) |
| 11 | +- Drop-in replacements for TextField and TextView |
| 12 | +- Automatic temp file management for pasted binary content |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +```bash |
| 17 | +npm install @nstudio/nativescript-rich-paste |
| 18 | +``` |
| 19 | + |
| 20 | +## Usage |
| 21 | + |
| 22 | +### NativeScript Core |
| 23 | + |
| 24 | +Drop-in replacements for [TextField](https://docs.nativescript.org/ui/text-field) and [TextView](https://docs.nativescript.org/ui/text-view) which work identically but with enhanced rich paste features. Just append `RichPaste` to the end of the element name. |
| 25 | + |
| 26 | +```xml |
| 27 | +<Page xmlns:rp="@nstudio/nativescript-rich-paste"> |
| 28 | + <StackLayout class="p-20"> |
| 29 | + <rp:TextFieldRichPaste accept="all" hint="Paste rich data..." paste="{{ onPaste }}" /> |
| 30 | + <rp:TextViewRichPaste accept="image/*" hint="Drop images here..." enableDragDrop="true" paste="{{ onPaste }}" drop="{{ onDrop }}" /> |
| 31 | + </StackLayout> |
| 32 | +</Page> |
| 33 | +``` |
| 34 | + |
| 35 | +```typescript |
| 36 | +import { EventData, Page } from '@nativescript/core'; |
| 37 | +import { PasteEventData } from '@nstudio/nativescript-rich-paste'; |
| 38 | + |
| 39 | +export function navigatingTo(args: EventData) { |
| 40 | + const page = args.object as Page; |
| 41 | + page.bindingContext = new DemoModel(); |
| 42 | +} |
| 43 | + |
| 44 | +class DemoModel { |
| 45 | + onPaste(args: PasteEventData) { |
| 46 | + const payload = args.data; |
| 47 | + switch (payload.type) { |
| 48 | + case 'text': |
| 49 | + console.log('Text:', payload.value); |
| 50 | + break; |
| 51 | + case 'images': |
| 52 | + payload.items.forEach((img) => { |
| 53 | + console.log(img.uri, img.mimeType, img.animated); |
| 54 | + }); |
| 55 | + break; |
| 56 | + case 'files': |
| 57 | + payload.items.forEach((file) => { |
| 58 | + console.log(file.name, file.mimeType, file.size); |
| 59 | + }); |
| 60 | + break; |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + onDrop(args: PasteEventData) { |
| 65 | + console.log('Dropped:', args.data); |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +### Angular |
| 71 | + |
| 72 | +```typescript |
| 73 | +import { registerElement } from '@nativescript/angular'; |
| 74 | +import { TextFieldRichPaste, TextViewRichPaste } from '@nstudio/nativescript-rich-paste'; |
| 75 | + |
| 76 | +registerElement('TextFieldRichPaste', () => TextFieldRichPaste); |
| 77 | +registerElement('TextViewRichPaste', () => TextViewRichPaste); |
| 78 | +``` |
| 79 | + |
| 80 | +```html |
| 81 | +<TextFieldRichPaste |
| 82 | + accept="all" |
| 83 | + hint="Paste rich data..." |
| 84 | + (paste)="onPaste($event)" |
| 85 | +></TextFieldRichPaste> |
| 86 | + |
| 87 | +<TextViewRichPaste |
| 88 | + accept="image/*" |
| 89 | + hint="Drop images here..." |
| 90 | + enableDragDrop="true" |
| 91 | + (paste)="onPaste($event)" |
| 92 | + (drop)="onDrop($event)" |
| 93 | +></TextViewRichPaste> |
| 94 | +``` |
| 95 | + |
| 96 | +### Other Flavors |
| 97 | + |
| 98 | +```typescript |
| 99 | +import { TextFieldRichPaste, TextViewRichPaste } from '@nstudio/nativescript-rich-paste'; |
| 100 | + |
| 101 | +// Vue |
| 102 | +registerElement('TextFieldRichPaste', () => TextFieldRichPaste); |
| 103 | +registerElement('TextViewRichPaste', () => TextViewRichPaste); |
| 104 | + |
| 105 | +// React |
| 106 | +registerElement('textFieldRichPaste', () => TextFieldRichPaste); |
| 107 | +registerElement('textViewRichPaste', () => TextViewRichPaste); |
| 108 | + |
| 109 | +// Svelte |
| 110 | +registerNativeViewElement('textFieldRichPaste', () => TextFieldRichPaste); |
| 111 | +registerNativeViewElement('textViewRichPaste', () => TextViewRichPaste); |
| 112 | + |
| 113 | +// Solid |
| 114 | +registerElement('textFieldRichPaste', TextFieldRichPaste); |
| 115 | +registerElement('textViewRichPaste', TextViewRichPaste); |
| 116 | +``` |
| 117 | + |
| 118 | +## Paste Payload |
| 119 | + |
| 120 | +The `paste` and `drop` events emit a `PasteEventData` object whose `data` property is a discriminated union: |
| 121 | + |
| 122 | +### Text |
| 123 | + |
| 124 | +```typescript |
| 125 | +interface PasteTextPayload { |
| 126 | + type: 'text'; |
| 127 | + value: string; |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +### Images |
| 132 | + |
| 133 | +```typescript |
| 134 | +interface PasteImagesPayload { |
| 135 | + type: 'images'; |
| 136 | + items: PasteImageItem[]; |
| 137 | +} |
| 138 | + |
| 139 | +interface PasteImageItem { |
| 140 | + uri: string; // file:// URI to a temp file |
| 141 | + mimeType: string; // e.g. 'image/png', 'image/gif' |
| 142 | + width?: number; |
| 143 | + height?: number; |
| 144 | + animated: boolean; // true for GIFs |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +### Files |
| 149 | + |
| 150 | +```typescript |
| 151 | +interface PasteFilesPayload { |
| 152 | + type: 'files'; |
| 153 | + items: PasteFileItem[]; |
| 154 | +} |
| 155 | + |
| 156 | +interface PasteFileItem { |
| 157 | + uri: string; // file:// URI to a temp file |
| 158 | + mimeType: string; // e.g. 'application/pdf' |
| 159 | + name?: string; |
| 160 | + size?: number; |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +### Unsupported |
| 165 | + |
| 166 | +Returned when the clipboard contains data that does not match the `accept` filter. |
| 167 | + |
| 168 | +```typescript |
| 169 | +interface PasteUnsupportedPayload { |
| 170 | + type: 'unsupported'; |
| 171 | + availableTypes: string[]; |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +## Events |
| 176 | + |
| 177 | +### paste |
| 178 | + |
| 179 | +Emitted when rich content is pasted from the clipboard. |
| 180 | + |
| 181 | +```typescript |
| 182 | +import { PasteEventData } from '@nstudio/nativescript-rich-paste'; |
| 183 | + |
| 184 | +function onPaste(args: PasteEventData) { |
| 185 | + const payload = args.data; |
| 186 | + console.log('Paste type:', payload.type); |
| 187 | +} |
| 188 | +``` |
| 189 | + |
| 190 | +For text pastes, the default TextField/TextView behavior still applies (the text is inserted into the field) and the `paste` event is also fired. |
| 191 | + |
| 192 | +### drop |
| 193 | + |
| 194 | +Emitted when content is dragged and dropped onto the input. Requires `enableDragDrop="true"`. |
| 195 | + |
| 196 | +```typescript |
| 197 | +import { DropEventData } from '@nstudio/nativescript-rich-paste'; |
| 198 | + |
| 199 | +function onDrop(args: DropEventData) { |
| 200 | + const payload = args.data; |
| 201 | + console.log('Drop type:', payload.type); |
| 202 | +} |
| 203 | +``` |
| 204 | + |
| 205 | +## Properties |
| 206 | + |
| 207 | +| Property | Type | Default | Description | |
| 208 | +| --- | --- | --- | --- | |
| 209 | +| accept | string | `'all'` | MIME filter for accepted paste content | |
| 210 | +| enableDragDrop | boolean | `false` | Enable drag-and-drop support | |
| 211 | + |
| 212 | +### accept filter |
| 213 | + |
| 214 | +The `accept` property controls which content types are processed. Supported values: |
| 215 | + |
| 216 | +- `'all'` — accept everything (default) |
| 217 | +- `'image/*'` — accept all image types |
| 218 | +- `'image/png'` — accept only PNG images |
| 219 | +- `'image/gif'` — accept only GIF images |
| 220 | +- `'text/plain'` — accept only plain text |
| 221 | +- `'application/pdf'` — accept only PDF files |
| 222 | +- Comma-separated — e.g. `'image/*,application/pdf'` |
| 223 | + |
| 224 | +## Classes |
| 225 | + |
| 226 | +- **TextFieldRichPaste** — extends NativeScript TextField |
| 227 | +- **TextViewRichPaste** — extends NativeScript TextView |
| 228 | + |
| 229 | +Both classes expose the same `accept`, `enableDragDrop` properties and emit `paste` and `drop` events. |
| 230 | + |
| 231 | +## Temp File Management |
| 232 | + |
| 233 | +Pasted binary content (images, files) is written to temporary files under the app's temp directory. You can clean up temp files at any time: |
| 234 | + |
| 235 | +```typescript |
| 236 | +import { PasteInputTempFiles } from '@nstudio/nativescript-rich-paste'; |
| 237 | + |
| 238 | +// Remove all temp files created by rich paste |
| 239 | +PasteInputTempFiles.cleanupAll(); |
| 240 | +``` |
| 241 | + |
| 242 | +## Platform Notes |
| 243 | + |
| 244 | +### iOS |
| 245 | + |
| 246 | +- Uses custom UITextField/UITextView subclasses that intercept the native `paste:` action |
| 247 | +- Reads from `UIPasteboard.generalPasteboard` with support for images, GIFs, file URLs, and UTI-based document types |
| 248 | +- Drag-and-drop uses `UIDropInteraction` with a native delegate |
| 249 | +- GIF animation is preserved by detecting `com.compuserve.gif` UTI and writing raw data to temp files |
| 250 | +- Supports file reference URL resolution including Finder bookmark data |
| 251 | + |
| 252 | +### Android |
| 253 | + |
| 254 | +- Uses `OnReceiveContentListener` on API 31+ for unified paste and drag-and-drop handling |
| 255 | +- Falls back to `onTextContextMenuItem` interception on older API levels |
| 256 | +- Resolves MIME types via ContentResolver, file extension mapping, and ClipDescription |
| 257 | +- Validates GIF files by checking the file header magic bytes |
| 258 | +- Images are decoded via `BitmapFactory` and compressed to JPEG temp files |
| 259 | + |
| 260 | +## License |
| 261 | + |
| 262 | +Apache License Version 2.0 |
0 commit comments