@schwitzerskills/emojipicker 1.0.5 → 1.0.6

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 CHANGED
@@ -13,7 +13,7 @@ picker.on('emojiClick', (emoji) => console.log(emoji.char)) // 😂
13
13
  ## Table of Contents
14
14
 
15
15
  - [Installation](#installation)
16
- - [Module Formats](#module-formats)
16
+ - [Setup — Two Files](#setup--two-files)
17
17
  - [Quick Start](#quick-start)
18
18
  - [Configuration Options](#configuration-options)
19
19
  - [Events](#events)
@@ -24,34 +24,70 @@ picker.on('emojiClick', (emoji) => console.log(emoji.char)) // 😂
24
24
  - [Theming & CSS Variables](#theming--css-variables)
25
25
  - [Custom Emojis](#custom-emojis)
26
26
  - [Helper: attachToInput()](#helper-attachtoinput)
27
+ - [Favorites & getTopFavorites()](#favorites--gettopfavorites)
28
+ - [i18n / Localization](#i18n--localization)
29
+ - [TypeScript](#typescript)
27
30
  - [Framework Integration](#framework-integration)
28
31
  - [Recipes / Examples](#recipes--examples)
29
32
  - [Accessibility](#accessibility)
30
33
  - [Browser Support](#browser-support)
34
+ - [How It Works Internally](#how-it-works-internally)
31
35
 
32
36
  ---
33
37
 
34
38
  ## Installation
35
39
 
36
- ### npm / yarn / pnpm
40
+ ### npm
37
41
 
38
42
  ```bash
39
43
  npm install @schwitzerskills/emojipicker
40
- # or
41
- yarn add @schwitzerskills/emojipicker
42
- # or
43
- pnpm add @schwitzerskills/emojipicker
44
44
  ```
45
45
 
46
46
  ### CDN (jsDelivr)
47
47
 
48
- No install needed — drop a single script tag into your HTML:
48
+ ```html
49
+ <script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Setup — Two Files
55
+
56
+ > **Important:** The library needs two files to work.
57
+
58
+ | File | Purpose |
59
+ |------|---------|
60
+ | `emoji-picker.js` | The picker core (~5 KB) |
61
+ | `emoji-data.json` | All emoji data (~850 KB, loaded once, cached forever) |
62
+
63
+ Both files must be accessible at the same URL path. The library auto-detects `emoji-data.json` relative to its own `<script src>` tag.
64
+
65
+ **If you use npm / a bundler**, copy `emoji-data.json` to your public/static folder and pass the URL manually:
66
+
67
+ ```js
68
+ new EmojiPicker({
69
+ container: '#btn',
70
+ dataUrl: '/static/emoji-data.json' // or your CDN URL
71
+ })
72
+ ```
73
+
74
+ **If you use CDN**, both files are already on jsDelivr — no config needed:
49
75
 
50
76
  ```html
51
77
  <script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
52
78
  ```
53
79
 
54
- Full working example:
80
+ **How the caching works:**
81
+
82
+ 1. First visit → `emoji-data.json` is fetched once (~850 KB)
83
+ 2. Data is stored in **IndexedDB** on the user's device
84
+ 3. Every visit after that → loaded from IndexedDB in milliseconds, zero network request
85
+
86
+ ---
87
+
88
+ ## Quick Start
89
+
90
+ ### CDN / Vanilla JS
55
91
 
56
92
  ```html
57
93
  <!DOCTYPE html>
@@ -78,87 +114,30 @@ Full working example:
78
114
  </html>
79
115
  ```
80
116
 
81
- ---
82
-
83
- ## Module Formats
84
-
85
- The library ships as a **UMD build** and supports all environments out of the box.
86
-
87
- ### CommonJS (Node.js / bundlers)
88
-
89
- ```js
90
- const EmojiPicker = require('@schwitzerskills/emojipicker')
91
- ```
92
-
93
- ### ES Module
117
+ ### npm / Bundler (Vite, Webpack, etc.)
94
118
 
95
119
  ```js
96
120
  import EmojiPicker from '@schwitzerskills/emojipicker'
97
- ```
98
-
99
- ### Browser global (CDN / script tag)
100
121
 
101
- ```html
102
- <script src="emoji-picker.js"></script>
103
- <script>
104
- const picker = new EmojiPicker({ container: '#btn' })
105
- </script>
106
- ```
107
-
108
- ### AMD (RequireJS)
109
-
110
- ```js
111
- define(['@schwitzerskills/emojipicker'], function(EmojiPicker) {
112
- const picker = new EmojiPicker({ container: '#btn' })
122
+ const picker = new EmojiPicker({
123
+ container: '#emoji-btn',
124
+ dataUrl: '/public/emoji-data.json' // adjust to your setup
113
125
  })
114
- ```
115
-
116
- The `package.json` fields are set up accordingly:
117
126
 
118
- ```json
119
- {
120
- "main": "emoji-picker.js",
121
- "module": "emoji-picker.esm.js",
122
- "browser": "emoji-picker.js",
123
- "exports": {
124
- ".": {
125
- "import": "./emoji-picker.esm.js",
126
- "require": "./emoji-picker.js"
127
- }
128
- }
129
- }
130
- ```
131
-
132
- ---
133
-
134
- ## Quick Start
135
-
136
- **Step 1 — Add a trigger button to your HTML:**
137
-
138
- ```html
139
- <button id="emoji-btn">😊</button>
140
- <input type="text" id="message" placeholder="Type a message...">
141
-
142
- <script src="emoji-picker.js"></script>
127
+ picker.on('emojiClick', (emoji) => {
128
+ document.querySelector('#message').value += emoji.char
129
+ })
143
130
  ```
144
131
 
145
- **Step 2 Initialize and listen for events:**
132
+ ### One-liner with `attachToInput()`
146
133
 
147
- ```html
148
- <script>
149
- const picker = new EmojiPicker({
150
- container: '#emoji-btn',
151
- theme: 'auto'
152
- })
134
+ ```js
135
+ import EmojiPicker from '@schwitzerskills/emojipicker'
153
136
 
154
- picker.on('emojiClick', (emoji) => {
155
- document.querySelector('#message').value += emoji.char
156
- })
157
- </script>
137
+ // Automatically adds a 😊 button and inserts emoji at cursor
138
+ EmojiPicker.attachToInput('#message')
158
139
  ```
159
140
 
160
- That's it. Clicking `#emoji-btn` opens the picker. Clicking an emoji fires `emojiClick`.
161
-
162
141
  ---
163
142
 
164
143
  ## Configuration Options
@@ -167,48 +146,50 @@ All properties are optional.
167
146
 
168
147
  ```js
169
148
  const picker = new EmojiPicker({
170
- container: '#my-button', // Trigger element (CSS selector or DOM node)
171
- theme: 'auto', // 'light' | 'dark' | 'auto'
172
- mode: 'dropdown', // 'dropdown' | 'inline' | 'popup'
173
- search: true, // Show search input
174
- recentEmojis: true, // Track & show recently used emojis
175
- maxRecent: 24, // Max number of recent emojis to store
176
- skinTone: 'default', // Default skin tone (see Skin Tone Support)
177
- customEmojis: [], // Array of custom emoji objects
178
- perRow: 8, // Emojis per row in the grid
179
- emojiSize: 28, // Emoji size in pixels
180
- autoClose: true, // Close picker after selecting an emoji
149
+ container: '#my-button',
150
+ theme: 'auto',
151
+ mode: 'dropdown',
152
+ locale: 'en',
153
+ search: true,
154
+ recentEmojis: true,
155
+ maxRecent: 24,
156
+ skinTone: 'default',
157
+ customEmojis: [],
158
+ perRow: 8,
159
+ emojiSize: 28,
160
+ autoClose: true,
161
+ dataUrl: null,
181
162
  })
182
163
  ```
183
164
 
184
165
  | Option | Type | Default | Description |
185
166
  |--------|------|---------|-------------|
186
- | `container` | `string \| HTMLElement` | `null` | Element that triggers open/close on click |
187
- | `theme` | `string` | `'auto'` | Color theme: `'light'`, `'dark'`, or `'auto'` |
188
- | `mode` | `string` | `'dropdown'` | Display mode (see [Modes](#modes)) |
189
- | `search` | `boolean` | `true` | Show/hide the search input |
190
- | `recentEmojis` | `boolean` | `true` | Enable recent emojis tab (uses localStorage) |
191
- | `maxRecent` | `number` | `24` | Max recent emojis to remember |
192
- | `skinTone` | `string` | `'default'` | Default skin tone modifier |
167
+ | `container` | `string \| HTMLElement` | `null` | Trigger element toggles picker on click |
168
+ | `theme` | `string` | `'auto'` | `'light'`, `'dark'`, or `'auto'` |
169
+ | `mode` | `string` | `'dropdown'` | See [Modes](#modes) |
170
+ | `locale` | `string` | `'en'` | UI language see [i18n](#i18n--localization) |
171
+ | `search` | `boolean` | `true` | Show/hide search input |
172
+ | `recentEmojis` | `boolean` | `true` | Track recents in IndexedDB |
173
+ | `maxRecent` | `number` | `24` | Max recent emojis to store |
174
+ | `skinTone` | `string` | `'default'` | Default skin tone |
193
175
  | `customEmojis` | `array` | `[]` | Custom emoji definitions |
194
176
  | `perRow` | `number` | `8` | Grid columns |
195
- | `emojiSize` | `number` | `28` | Emoji size in px |
196
- | `autoClose` | `boolean` | `true` | Auto-close picker on emoji select |
177
+ | `emojiSize` | `number` | `28` | Emoji size in px (`--ep-size`) |
178
+ | `autoClose` | `boolean` | `true` | Close after selecting |
179
+ | `dataUrl` | `string` | `null` | Custom URL to `emoji-data.json` |
197
180
 
198
181
  ---
199
182
 
200
183
  ## Events
201
184
 
202
- The library is fully event-driven. Attach any number of listeners to any event.
203
-
204
185
  ```js
205
- picker.on(eventName, handler)
206
- picker.off(eventName, handler) // Remove a specific listener
186
+ picker.on(eventName, handler) // add listener
187
+ picker.off(eventName, handler) // remove listener
207
188
  ```
208
189
 
209
190
  ### `emojiClick`
210
191
 
211
- Fired when the user clicks/selects an emoji. This is the main event you'll use.
192
+ Fired when the user selects an emoji. The main event you'll use.
212
193
 
213
194
  ```js
214
195
  picker.on('emojiClick', (emoji, mouseEvent) => {
@@ -216,34 +197,25 @@ picker.on('emojiClick', (emoji, mouseEvent) => {
216
197
  console.log(emoji.name) // "face_with_tears_of_joy"
217
198
  console.log(emoji.category) // "Smileys & Emotion"
218
199
  console.log(emoji.unicode) // "1F602"
219
- console.log(emoji.skinTone) // null | "light" | "medium" | ...
200
+ console.log(emoji.skinTone) // null | "medium" | ...
220
201
  })
221
202
  ```
222
203
 
223
204
  ### `emojiHover`
224
205
 
225
- Fired when the user hovers over an emoji.
206
+ Fired when hovering over an emoji.
226
207
 
227
208
  ```js
228
209
  picker.on('emojiHover', (emoji, mouseEvent) => {
229
- myPreview.textContent = `${emoji.char} ${emoji.name}`
210
+ myPreview.textContent = emoji.char + ' ' + emoji.name
230
211
  })
231
212
  ```
232
213
 
233
- ### `pickerOpen`
214
+ ### `pickerOpen` / `pickerClose`
234
215
 
235
216
  ```js
236
- picker.on('pickerOpen', () => {
237
- console.log('Picker opened')
238
- })
239
- ```
240
-
241
- ### `pickerClose`
242
-
243
- ```js
244
- picker.on('pickerClose', () => {
245
- console.log('Picker closed')
246
- })
217
+ picker.on('pickerOpen', () => console.log('opened'))
218
+ picker.on('pickerClose', () => console.log('closed'))
247
219
  ```
248
220
 
249
221
  ### `categoryChange`
@@ -258,7 +230,7 @@ picker.on('categoryChange', ({ category }) => {
258
230
 
259
231
  ```js
260
232
  picker.on('search', ({ query }) => {
261
- console.log('Searching for:', query)
233
+ console.log('User typed:', query)
262
234
  })
263
235
  ```
264
236
 
@@ -266,38 +238,65 @@ picker.on('search', ({ query }) => {
266
238
 
267
239
  ## Methods
268
240
 
269
- All methods return `this` (chainable, except `destroy()`).
241
+ All methods return `this` (chainable), except `destroy()` and the async methods.
242
+
243
+ ```js
244
+ picker.open() // open
245
+ picker.close() // close
246
+ picker.toggle() // toggle
247
+ picker.setTheme('dark') // switch theme
248
+ picker.setLocale('de') // switch language
249
+ picker.destroy() // remove from DOM, clean up listeners
250
+ ```
251
+
252
+ **Async methods:**
253
+
254
+ ```js
255
+ // Returns top N most-clicked emojis
256
+ const favs = await picker.getTopFavorites(8)
257
+ // → [{ name, char, count }, ...]
258
+
259
+ // Clear recent history
260
+ await picker.clearRecent()
261
+
262
+ // Clear favorite click counts
263
+ await picker.clearFavorites()
264
+ ```
265
+
266
+ **Static methods:**
270
267
 
271
268
  ```js
272
- picker.open() // Open the picker
273
- picker.close() // Close the picker
274
- picker.toggle() // Toggle open/close
275
- picker.setTheme('dark') // Switch theme at runtime
276
- picker.destroy() // Remove from DOM and clean up all listeners
269
+ // Attach to any input (see section below)
270
+ EmojiPicker.attachToInput('#message', opts)
271
+
272
+ // Pre-warm: fetch + cache data without showing any UI
273
+ // Call this on app startup so first open is instant
274
+ await EmojiPicker.preload({ dataUrl: '/static/emoji-data.json' })
277
275
  ```
278
276
 
279
277
  **Chaining:**
280
278
 
281
279
  ```js
282
- new EmojiPicker({ container: '#btn', theme: 'light' })
283
- .on('emojiClick', (e) => addEmoji(e.char))
284
- .on('pickerOpen', () => analytics.track('emoji_picker_opened'))
280
+ new EmojiPicker({ container: '#btn' })
281
+ .on('emojiClick', (e) => insertEmoji(e.char))
282
+ .on('pickerOpen', () => analytics.track('picker_opened'))
283
+ .on('pickerClose', () => analytics.track('picker_closed'))
285
284
  ```
286
285
 
287
286
  ---
288
287
 
289
288
  ## Emoji Object
290
289
 
291
- Every emoji-related event provides this data structure:
290
+ Every emoji-related event provides this structure:
292
291
 
293
292
  ```js
294
293
  {
295
- char: "👍🏽", // The emoji character (with skin tone applied)
296
- name: "thumbs_up", // Snake_case identifier
297
- category: "People & Body", // Category name
298
- unicode: "1F44D", // Base Unicode code point (hex)
299
- skinTone: "medium", // null if default, otherwise the tone name
300
- isCustom: false // true for custom emojis
294
+ char: "👍🏽", // emoji character, skin tone applied
295
+ name: "thumbs_up", // snake_case identifier
296
+ category: "People & Body", // category name
297
+ unicode: "1F44D", // base code point (hex)
298
+ skinTone: "medium", // null if default
299
+ isCustom: false // true for custom image emojis
301
300
  }
302
301
  ```
303
302
 
@@ -307,7 +306,7 @@ Every emoji-related event provides this data structure:
307
306
 
308
307
  ### `dropdown` *(default)*
309
308
 
310
- Opens as a floating panel anchored to the trigger element. Closes on outside click or `Esc`.
309
+ Floating panel anchored to the trigger element. Closes on outside click or `Esc`.
311
310
 
312
311
  ```js
313
312
  new EmojiPicker({ container: '#btn', mode: 'dropdown' })
@@ -315,7 +314,7 @@ new EmojiPicker({ container: '#btn', mode: 'dropdown' })
315
314
 
316
315
  ### `inline`
317
316
 
318
- Always visible, embedded directly inside the container element. `autoClose` is ignored.
317
+ Always visible, embedded inside the container. `autoClose` is ignored.
319
318
 
320
319
  ```js
321
320
  new EmojiPicker({ container: '#my-div', mode: 'inline', autoClose: false })
@@ -327,18 +326,18 @@ new EmojiPicker({ container: '#my-div', mode: 'inline', autoClose: false })
327
326
 
328
327
  ### `popup`
329
328
 
330
- Positions itself in the center of the viewport. Useful for modal-style pickers without a fixed trigger.
329
+ Centers in the viewport ideal for modals or custom trigger logic.
331
330
 
332
331
  ```js
333
332
  const picker = new EmojiPicker({ mode: 'popup' })
334
- document.getElementById('open-btn').addEventListener('click', () => picker.open())
333
+ document.getElementById('btn').addEventListener('click', () => picker.open())
335
334
  ```
336
335
 
337
336
  ---
338
337
 
339
338
  ## Skin Tone Support
340
339
 
341
- Users can select a skin tone in the picker footer. Set a default via options:
340
+ Users can pick a skin tone in the footer. Set a default in options:
342
341
 
343
342
  ```js
344
343
  new EmojiPicker({ skinTone: 'medium-dark' })
@@ -353,46 +352,41 @@ new EmojiPicker({ skinTone: 'medium-dark' })
353
352
  | `'medium-dark'` | 👍🏾 |
354
353
  | `'dark'` | 👍🏿 |
355
354
 
356
- The selected tone is reflected in `emoji.char` and reported in `emoji.skinTone`.
357
-
358
355
  ---
359
356
 
360
357
  ## Theming & CSS Variables
361
358
 
362
- Override CSS variables to match any design system:
363
-
364
359
  ```css
365
360
  .ep-picker {
366
- --ep-bg: #0f1117;
367
- --ep-surface: #1a1d2e;
368
- --ep-surface2: #21253a;
369
- --ep-border: rgba(255,255,255,0.08);
370
- --ep-text: #e4e7f3;
371
- --ep-text-dim: #6b738f;
372
- --ep-accent: #6c63ff;
373
- --ep-hover: rgba(108,99,255,0.12);
374
- --ep-size: 28px; /* Emoji size */
375
- --ep-radius: 16px; /* Picker border radius */
361
+ --ep-bg: #16192a; /* picker background */
362
+ --ep-surface: #1e2236; /* surface / hover bg */
363
+ --ep-border: rgba(255,255,255,0.07);
364
+ --ep-text: #e2e6f5;
365
+ --ep-text-dim: #636b86;
366
+ --ep-accent: #6c63ff; /* active tab, focus rings */
367
+ --ep-hover: rgba(108,99,255,0.13);
368
+ --ep-size: 28px; /* emoji size */
369
+ --ep-radius: 18px; /* picker border-radius */
376
370
  }
377
371
  ```
378
372
 
379
373
  ### Built-in themes
380
374
 
381
375
  ```js
382
- new EmojiPicker({ theme: 'light' }) // Light
383
- new EmojiPicker({ theme: 'dark' }) // Dark
384
- new EmojiPicker({ theme: 'auto' }) // Follows OS preference
376
+ new EmojiPicker({ theme: 'light' }) // light
377
+ new EmojiPicker({ theme: 'dark' }) // dark
378
+ new EmojiPicker({ theme: 'auto' }) // follows OS
385
379
 
386
- picker.setTheme('dark') // Switch at runtime
380
+ picker.setTheme('dark') // switch at runtime
387
381
  ```
388
382
 
389
- ### Custom accent color
383
+ ### Custom brand color
390
384
 
391
385
  ```css
392
386
  .ep-picker {
393
387
  --ep-accent: #e91e8c;
394
- --ep-hover: rgba(233, 30, 140, 0.1);
395
- --ep-active-tab: rgba(233, 30, 140, 0.15);
388
+ --ep-hover: rgba(233,30,140,0.10);
389
+ --ep-active-tab: rgba(233,30,140,0.18);
396
390
  }
397
391
  ```
398
392
 
@@ -400,50 +394,139 @@ picker.setTheme('dark') // Switch at runtime
400
394
 
401
395
  ## Custom Emojis
402
396
 
403
- Add your own images, GIFs or SVGs alongside the standard set:
397
+ Add your own GIFs, PNGs or SVGs alongside the standard set:
404
398
 
405
399
  ```js
406
400
  new EmojiPicker({
407
401
  customEmojis: [
408
- { name: 'party_parrot', url: '/assets/parrot.gif' },
409
- { name: 'company_logo', url: '/assets/logo.png' },
410
- { name: 'custom_star', url: 'https://example.com/star.svg' }
402
+ { name: 'party_parrot', url: '/assets/parrot.gif' },
403
+ { name: 'company_logo', url: '/assets/logo.png' },
404
+ { name: 'custom_star', url: 'https://cdn.example.com/star.svg' }
411
405
  ]
412
406
  })
413
407
  ```
414
408
 
415
- Custom emojis appear in their own **Custom** tab. The click event returns:
409
+ They appear in a dedicated **Custom** tab. Click event returns:
416
410
 
417
411
  ```js
418
- {
419
- char: null,
420
- name: 'party_parrot',
421
- category: 'custom',
422
- isCustom: true
423
- }
412
+ { char: null, name: 'party_parrot', category: 'custom', isCustom: true }
424
413
  ```
425
414
 
426
415
  ---
427
416
 
428
417
  ## Helper: attachToInput()
429
418
 
430
- Wraps any `<input>` or `<textarea>` and inserts the selected emoji at the cursor position automatically.
419
+ Wraps any `<input>` or `<textarea>` and handles cursor-position insertion automatically.
431
420
 
432
421
  ```js
433
- // Attach by selector
434
- EmojiPicker.attachToInput('#message-input')
422
+ // Basic
423
+ EmojiPicker.attachToInput('#message')
435
424
 
436
425
  // With options
437
426
  EmojiPicker.attachToInput('#chat-box', {
438
- theme: 'dark',
439
- skinTone: 'medium'
427
+ theme: 'dark',
428
+ skinTone: 'medium',
429
+ dataUrl: '/static/emoji-data.json'
440
430
  })
441
431
 
442
- // Returns the picker instance for further event binding
443
- const picker = EmojiPicker.attachToInput('#my-input')
444
- picker.on('emojiClick', (emoji) => {
445
- counter.textContent = myInput.value.length
432
+ // Returns the picker instance
433
+ const picker = EmojiPicker.attachToInput('#editor')
434
+ picker.on('emojiClick', () => updateCharCount())
435
+ ```
436
+
437
+ ---
438
+
439
+ ## Favorites & getTopFavorites()
440
+
441
+ Every emoji click is counted and stored in IndexedDB. Use this to build "most used" sections, reaction quick-bars, or analytics.
442
+
443
+ ```js
444
+ // Get top 8 most-clicked emojis
445
+ const favs = await picker.getTopFavorites(8)
446
+ // → [{ name: 'thumbs_up', char: '👍', count: 42 }, ...]
447
+
448
+ // Render a quick-access bar
449
+ favs.forEach(({ char }) => {
450
+ const btn = document.createElement('button')
451
+ btn.textContent = char
452
+ quickBar.appendChild(btn)
446
453
  })
454
+
455
+ // Reset counts
456
+ await picker.clearFavorites()
457
+ ```
458
+
459
+ ---
460
+
461
+ ## i18n / Localization
462
+
463
+ ### Built-in languages
464
+
465
+ | Code | Language |
466
+ |------|----------|
467
+ | `en` | English *(default)* |
468
+ | `de` | German |
469
+ | `fr` | French |
470
+ | `es` | Spanish |
471
+ | `pt` | Portuguese |
472
+ | `ja` | Japanese |
473
+
474
+ ```js
475
+ // Set at construction
476
+ new EmojiPicker({ locale: 'de' })
477
+
478
+ // Switch at runtime (re-renders if open)
479
+ picker.setLocale('fr')
480
+ ```
481
+
482
+ ### Add a custom language
483
+
484
+ ```js
485
+ EmojiPicker.LOCALES['nl'] = {
486
+ search: 'Zoek emoji…',
487
+ noResults: 'Geen resultaten voor',
488
+ noRecent: 'Nog geen recente emojis',
489
+ recent: 'Recent gebruikt',
490
+ custom: 'Aangepast',
491
+ loading: 'Laden…',
492
+ categories: {
493
+ recent: 'Recent', 'Smileys & Emotion': 'Smileys', 'People & Body': 'Mensen',
494
+ 'Animals & Nature': 'Natuur', 'Food & Drink': 'Eten', Activities: 'Activiteiten',
495
+ 'Travel & Places': 'Reizen', Objects: 'Objecten', Symbols: 'Symbolen',
496
+ Flags: 'Vlaggen', custom: 'Aangepast'
497
+ },
498
+ skinTones: {
499
+ default: 'Standaard', light: 'Licht', 'medium-light': 'Medium licht',
500
+ medium: 'Medium', 'medium-dark': 'Medium donker', dark: 'Donker'
501
+ }
502
+ }
503
+
504
+ new EmojiPicker({ locale: 'nl' })
505
+ ```
506
+
507
+ ---
508
+
509
+ ## TypeScript
510
+
511
+ The package ships with a `.d.ts` file. No `@types/` package needed.
512
+
513
+ ```ts
514
+ import EmojiPicker, { EmojiObject, EmojiPickerOptions, FavoriteEmoji } from '@schwitzerskills/emojipicker'
515
+
516
+ const options: EmojiPickerOptions = {
517
+ container: '#btn',
518
+ theme: 'auto',
519
+ locale: 'de',
520
+ dataUrl: '/static/emoji-data.json'
521
+ }
522
+
523
+ const picker = new EmojiPicker(options)
524
+
525
+ picker.on('emojiClick', (emoji: EmojiObject) => {
526
+ console.log(emoji.char, emoji.name)
527
+ })
528
+
529
+ const favs: FavoriteEmoji[] = await picker.getTopFavorites(10)
447
530
  ```
448
531
 
449
532
  ---
@@ -452,44 +535,48 @@ picker.on('emojiClick', (emoji) => {
452
535
 
453
536
  ### React
454
537
 
455
- Works out of the box with a standard `import`. Initialize inside `useEffect` so the library only runs in the browser.
456
-
457
- ```jsx
538
+ ```tsx
458
539
  import { useEffect, useRef } from 'react'
459
- import EmojiPicker from '@schwitzerskills/emojipicker'
540
+ import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
460
541
 
461
- function EmojiButton({ onSelect }) {
462
- const btnRef = useRef(null)
542
+ interface Props {
543
+ onSelect: (emoji: EmojiObject) => void
544
+ }
545
+
546
+ export function EmojiButton({ onSelect }: Props) {
547
+ const btnRef = useRef<HTMLButtonElement>(null)
463
548
 
464
549
  useEffect(() => {
550
+ if (!btnRef.current) return
465
551
  const picker = new EmojiPicker({
466
552
  container: btnRef.current,
467
- theme: 'auto'
553
+ theme: 'auto',
554
+ dataUrl: '/static/emoji-data.json'
468
555
  })
469
556
  picker.on('emojiClick', onSelect)
470
-
471
- return () => picker.destroy() // clean up on unmount
557
+ return () => picker.destroy()
472
558
  }, [onSelect])
473
559
 
474
- return <button ref={btnRef}>😊</button>
560
+ return <button ref={btnRef} type="button">😊</button>
475
561
  }
476
562
  ```
477
563
 
478
564
  ### Next.js
479
565
 
480
- EmojiPicker uses `window` and `document` internally, so it must only run on the client. There are two ways to handle this:
566
+ `EmojiPicker` uses `window` and `document` internally, so it must only run on the client.
481
567
 
482
568
  **Option A — dynamic import (recommended):**
483
569
 
484
- ```jsx
485
- // components/EmojiButton.jsx
570
+ ```tsx
571
+ // components/EmojiButton.tsx ← client-only wrapper
572
+ 'use client'
486
573
  import { useEffect, useRef } from 'react'
487
574
  import EmojiPicker from '@schwitzerskills/emojipicker'
488
575
 
489
576
  export default function EmojiButton({ onSelect }) {
490
577
  const btnRef = useRef(null)
491
578
  useEffect(() => {
492
- const picker = new EmojiPicker({ container: btnRef.current })
579
+ const picker = new EmojiPicker({ container: btnRef.current, dataUrl: '/emoji-data.json' })
493
580
  picker.on('emojiClick', onSelect)
494
581
  return () => picker.destroy()
495
582
  }, [onSelect])
@@ -497,25 +584,18 @@ export default function EmojiButton({ onSelect }) {
497
584
  }
498
585
  ```
499
586
 
500
- ```jsx
501
- // pages/index.jsx or any page
587
+ ```tsx
588
+ // app/page.tsx or pages/index.tsx
502
589
  import dynamic from 'next/dynamic'
503
590
 
504
- const EmojiButton = dynamic(() => import('../components/EmojiButton'), {
505
- ssr: false // prevents server-side execution
506
- })
591
+ const EmojiButton = dynamic(() => import('../components/EmojiButton'), { ssr: false })
507
592
  ```
508
593
 
509
- **Option B — useEffect guard (also safe):**
594
+ **Option B — App Router `'use client'` directive:**
510
595
 
511
- ```jsx
512
- useEffect(() => {
513
- // useEffect only runs in the browser, so this is always safe.
514
- // Add typeof window check only if you import EmojiPicker outside of useEffect.
515
- const picker = new EmojiPicker({ container: btnRef.current })
516
- picker.on('emojiClick', onSelect)
517
- return () => picker.destroy()
518
- }, [onSelect])
596
+ ```tsx
597
+ 'use client'
598
+ // useEffect only runs in the browser safe without ssr:false
519
599
  ```
520
600
 
521
601
  ### Vue 3
@@ -525,16 +605,16 @@ useEffect(() => {
525
605
  <button ref="btnRef">😊</button>
526
606
  </template>
527
607
 
528
- <script setup>
608
+ <script setup lang="ts">
529
609
  import { ref, onMounted, onUnmounted } from 'vue'
530
- import EmojiPicker from '@schwitzerskills/emojipicker'
610
+ import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
531
611
 
532
- const emit = defineEmits(['select'])
533
- const btnRef = ref(null)
534
- let picker = null
612
+ const emit = defineEmits<{ select: [emoji: EmojiObject] }>()
613
+ const btnRef = ref<HTMLButtonElement | null>(null)
614
+ let picker: EmojiPicker | null = null
535
615
 
536
616
  onMounted(() => {
537
- picker = new EmojiPicker({ container: btnRef.value, theme: 'auto' })
617
+ picker = new EmojiPicker({ container: btnRef.value!, dataUrl: '/emoji-data.json' })
538
618
  picker.on('emojiClick', (emoji) => emit('select', emoji))
539
619
  })
540
620
 
@@ -545,16 +625,16 @@ onUnmounted(() => picker?.destroy())
545
625
  ### Svelte
546
626
 
547
627
  ```svelte
548
- <script>
628
+ <script lang="ts">
549
629
  import { onMount, onDestroy, createEventDispatcher } from 'svelte'
550
630
  import EmojiPicker from '@schwitzerskills/emojipicker'
551
631
 
552
632
  const dispatch = createEventDispatcher()
553
- let btnEl
554
- let picker
633
+ let btnEl: HTMLButtonElement
634
+ let picker: EmojiPicker
555
635
 
556
636
  onMount(() => {
557
- picker = new EmojiPicker({ container: btnEl, theme: 'auto' })
637
+ picker = new EmojiPicker({ container: btnEl, dataUrl: '/emoji-data.json' })
558
638
  picker.on('emojiClick', (emoji) => dispatch('select', emoji))
559
639
  })
560
640
 
@@ -564,39 +644,22 @@ onUnmounted(() => picker?.destroy())
564
644
  <button bind:this={btnEl}>😊</button>
565
645
  ```
566
646
 
567
- ### Vanilla JS (no bundler)
568
-
569
- ```html
570
- <script src="https://cdn.example.com/emoji-picker.js"></script>
571
- <script>
572
- const picker = new EmojiPicker({ container: '#btn', theme: 'auto' })
573
- picker.on('emojiClick', (emoji) => {
574
- document.querySelector('#input').value += emoji.char
575
- })
576
- </script>
577
- ```
578
-
579
647
  ---
580
648
 
581
649
  ## Recipes / Examples
582
650
 
583
- ### Insert emoji at cursor in a textarea
651
+ ### Insert at cursor in a textarea
584
652
 
585
653
  ```js
586
654
  const textarea = document.querySelector('#editor')
587
- const picker = new EmojiPicker({ container: '#emoji-trigger' })
655
+ const picker = new EmojiPicker({ container: '#btn' })
588
656
 
589
657
  picker.on('emojiClick', (emoji) => {
590
- const start = textarea.selectionStart
591
- const end = textarea.selectionEnd
658
+ const s = textarea.selectionStart
659
+ const e = textarea.selectionEnd
592
660
  textarea.value =
593
- textarea.value.substring(0, start) +
594
- emoji.char +
595
- textarea.value.substring(end)
596
- textarea.setSelectionRange(
597
- start + emoji.char.length,
598
- start + emoji.char.length
599
- )
661
+ textarea.value.slice(0, s) + emoji.char + textarea.value.slice(e)
662
+ textarea.setSelectionRange(s + emoji.char.length, s + emoji.char.length)
600
663
  textarea.focus()
601
664
  })
602
665
  ```
@@ -605,76 +668,66 @@ picker.on('emojiClick', (emoji) => {
605
668
 
606
669
  ```js
607
670
  picker.on('emojiClick', (emoji) => {
608
- navigator.clipboard.writeText(emoji.char).then(() => {
609
- showToast(`Copied ${emoji.char}`)
610
- })
671
+ navigator.clipboard.writeText(emoji.char).then(() => showToast('Copied!'))
611
672
  })
612
673
  ```
613
674
 
614
- ### Send emoji reaction to a server
675
+ ### Send to a server
615
676
 
616
677
  ```js
617
678
  picker.on('emojiClick', (emoji) => {
618
679
  fetch('/api/reactions', {
619
- method: 'POST',
680
+ method: 'POST',
620
681
  headers: { 'Content-Type': 'application/json' },
621
- body: JSON.stringify({
622
- messageId: currentMessageId,
623
- emoji: emoji.name,
624
- char: emoji.char
625
- })
682
+ body: JSON.stringify({ messageId, emoji: emoji.name, char: emoji.char })
626
683
  })
627
684
  })
628
685
  ```
629
686
 
630
- ### Multiple pickers on the same page
687
+ ### Preload data on app startup
631
688
 
632
689
  ```js
633
- // Minimal reaction picker
634
- const reactionPicker = new EmojiPicker({
635
- container: '#reaction-btn',
636
- search: false,
637
- recentEmojis: false,
638
- autoClose: true
639
- })
640
-
641
- // Full editor picker
642
- const editorPicker = new EmojiPicker({
643
- container: '#editor-btn',
644
- search: true,
645
- skinTone: 'medium'
646
- })
690
+ // Call once at app init — data is fetched and cached in IndexedDB.
691
+ // Every picker opened after this is instant.
692
+ EmojiPicker.preload({ dataUrl: '/static/emoji-data.json' })
647
693
  ```
648
694
 
649
- ### Track analytics
695
+ ### Quick-access bar from favorites
650
696
 
651
697
  ```js
652
- picker.on('pickerOpen', () => analytics.track('picker_opened'))
653
- picker.on('emojiClick', (emoji) => analytics.track('emoji_used', { name: emoji.name }))
654
- picker.on('search', ({ query }) => analytics.track('emoji_search', { query }))
655
- picker.on('categoryChange', ({ category }) => analytics.track('category_view', { category }))
698
+ const bar = document.getElementById('quick-bar')
699
+
700
+ async function renderFavBar() {
701
+ const favs = await picker.getTopFavorites(6)
702
+ bar.innerHTML = favs.map(({ char, name }) =>
703
+ `<button title="${name}" onclick="insertEmoji('${char}')">${char}</button>`
704
+ ).join('')
705
+ }
706
+
707
+ picker.on('pickerClose', renderFavBar)
656
708
  ```
657
709
 
658
- ### Dynamically switch themes
710
+ ### Analytics
659
711
 
660
712
  ```js
661
- document.querySelector('#theme-toggle').addEventListener('click', () => {
662
- const isDark = document.body.classList.toggle('dark')
663
- picker.setTheme(isDark ? 'dark' : 'light')
664
- })
713
+ picker.on('pickerOpen', () => analytics.track('picker_opened'))
714
+ picker.on('emojiClick', ({ name }) => analytics.track('emoji_used', { name }))
715
+ picker.on('search', ({ query }) => analytics.track('emoji_search', { query }))
716
+ picker.on('categoryChange', ({ category }) => analytics.track('category_view', { category }))
665
717
  ```
666
718
 
667
719
  ---
668
720
 
669
721
  ## Accessibility
670
722
 
671
- - All interactive elements have `aria-label` and `role` attributes
672
- - The emoji grid uses `role="grid"` and `role="gridcell"`
673
- - The picker uses `role="dialog"` with `aria-label`
674
- - Category tabs use `role="tablist"`
675
- - Preview area uses `aria-live="polite"` for screen reader announcements
723
+ - `role="dialog"` + `aria-modal="true"` on the picker
724
+ - `role="tablist"` on category tabs, `aria-selected` on active tab
725
+ - `role="grid"` + `role="gridcell"` on emoji grid
726
+ - `aria-label` on every emoji button
727
+ - `aria-live="polite"` on category label and preview
676
728
  - `Esc` closes the picker
677
729
  - Focus moves to the search input on open
730
+ - `type="button"` on all buttons — safe inside `<form>` elements
678
731
 
679
732
  ---
680
733
 
@@ -689,24 +742,69 @@ document.querySelector('#theme-toggle').addEventListener('click', () => {
689
742
  | iOS Safari | 14+ |
690
743
  | Android Chrome | 80+ |
691
744
 
692
- Requires `localStorage` for recent emojis — gracefully disabled if unavailable (e.g. private browsing).
745
+ Requires **IndexedDB** for cachingavailable in all modern browsers, gracefully degraded if blocked (e.g. Firefox private mode with `resistFingerprinting`).
693
746
 
694
747
  ---
695
748
 
696
- ## Categories
697
-
698
- | Tab | Category | Example |
699
- |-----|----------|---------|
700
- | 🕐 | Recently Used | *dynamic* |
701
- | 😊 | Smileys & Emotion | 😀 😂 🥰 😎 |
702
- | 👋 | People & Body | 👋 💪 🙏 🤝 |
703
- | 🐶 | Animals & Nature | 🐶 🦊 🌸 🌈 |
704
- | 🍕 | Food & Drink | 🍕 🍜 🍺 🧋 |
705
- | ⚽ | Activities | ⚽ 🎮 🎸 🏆 |
706
- | ✈️ | Travel & Places | ✈️ 🚀 🏖️ 🏰 |
707
- | 💡 | Objects | 💡 💻 📷 🔑 |
708
- | ❤️ | Symbols | ❤️ ✅ ♻️ 💯 |
709
- | 🏳️ | Flags | 🏳️‍🌈 🇺🇸 🇩🇪 🇯🇵 |
749
+ ## How It Works Internally
750
+
751
+ ```
752
+ First visit:
753
+ emoji-picker.js (~5 KB) loads instantly
754
+
755
+ picker.open() is called
756
+
757
+ emoji-data.json is fetched (~850 KB, one time only)
758
+
759
+ data is stored in IndexedDB
760
+
761
+ Every visit after:
762
+ emoji-picker.js loads
763
+
764
+ picker.open() → data loads from IndexedDB in <5ms
765
+
766
+ zero network request for emoji data
767
+ ```
768
+
769
+ **IndexedDB stores:**
770
+
771
+ | Store | Contents | Key |
772
+ |-------|----------|-----|
773
+ | `cache` | Full emoji data JSON | `'emojidata'` |
774
+ | `recent` | Last used emojis + timestamps | `name` |
775
+ | `favorites` | Click counts per emoji | `name` |
776
+
777
+ **Emoji support detection:**
778
+
779
+ The library uses a canvas-based test to detect which Unicode Emoji version the OS supports, then hides emojis that would render as broken boxes. Tests run once per session and are cached in memory.
780
+
781
+ ---
782
+
783
+ ## package.json
784
+
785
+ No separate ESM build is needed. The UMD bundle handles `require()`, `import`, and `<script>` tags.
786
+
787
+ ```json
788
+ {
789
+ "name": "@schwitzerskills/emojipicker",
790
+ "version": "2.0.0",
791
+ "main": "emoji-picker.js",
792
+ "browser": "emoji-picker.js",
793
+ "types": "emoji-picker.d.ts",
794
+ "exports": {
795
+ ".": {
796
+ "require": "./emoji-picker.js",
797
+ "import": "./emoji-picker.js",
798
+ "types": "./emoji-picker.d.ts"
799
+ }
800
+ },
801
+ "files": [
802
+ "emoji-picker.js",
803
+ "emoji-picker.d.ts",
804
+ "emoji-data.json"
805
+ ]
806
+ }
807
+ ```
710
808
 
711
809
  ---
712
810