@schwitzerskills/emojipicker 1.0.4 → 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,103 +24,119 @@ 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
 
40
+ ### npm
41
+
36
42
  ```bash
37
43
  npm install @schwitzerskills/emojipicker
38
- # or
39
- yarn add @schwitzerskills/emojipicker
40
- # or
41
- pnpm add @schwitzerskills/emojipicker
44
+ ```
45
+
46
+ ### CDN (jsDelivr)
47
+
48
+ ```html
49
+ <script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
42
50
  ```
43
51
 
44
52
  ---
45
53
 
46
- ## Module Formats
54
+ ## Setup — Two Files
47
55
 
48
- The library ships as a **UMD build** and supports all environments out of the box.
56
+ > **Important:** The library needs two files to work.
49
57
 
50
- ### CommonJS (Node.js / bundlers)
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) |
51
62
 
52
- ```js
53
- const EmojiPicker = require('emojipicker-js')
54
- ```
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.
55
64
 
56
- ### ES Module
65
+ **If you use npm / a bundler**, copy `emoji-data.json` to your public/static folder and pass the URL manually:
57
66
 
58
67
  ```js
59
- import EmojiPicker from 'emojipicker-js'
68
+ new EmojiPicker({
69
+ container: '#btn',
70
+ dataUrl: '/static/emoji-data.json' // or your CDN URL
71
+ })
60
72
  ```
61
73
 
62
- ### Browser global (CDN / script tag)
74
+ **If you use CDN**, both files are already on jsDelivr — no config needed:
63
75
 
64
76
  ```html
65
- <script src="emoji-picker.js"></script>
66
- <script>
67
- const picker = new EmojiPicker({ container: '#btn' })
68
- </script>
77
+ <script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
69
78
  ```
70
79
 
71
- ### AMD (RequireJS)
80
+ **How the caching works:**
72
81
 
73
- ```js
74
- define(['emojipicker-js'], function(EmojiPicker) {
75
- const picker = new EmojiPicker({ container: '#btn' })
76
- })
77
- ```
78
-
79
- The `package.json` fields are set up accordingly:
80
-
81
- ```json
82
- {
83
- "main": "emoji-picker.js",
84
- "module": "emoji-picker.esm.js",
85
- "browser": "emoji-picker.js",
86
- "exports": {
87
- ".": {
88
- "import": "./emoji-picker.esm.js",
89
- "require": "./emoji-picker.js"
90
- }
91
- }
92
- }
93
- ```
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
94
85
 
95
86
  ---
96
87
 
97
88
  ## Quick Start
98
89
 
99
- **Step 1 Add a trigger button to your HTML:**
90
+ ### CDN / Vanilla JS
100
91
 
101
92
  ```html
102
- <button id="emoji-btn">😊</button>
103
- <input type="text" id="message" placeholder="Type a message...">
93
+ <!DOCTYPE html>
94
+ <html lang="en">
95
+ <head>
96
+ <meta charset="UTF-8">
97
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
98
+ <title>My App</title>
99
+ </head>
100
+ <body>
101
+
102
+ <input type="text" id="message" placeholder="Type a message...">
103
+ <button id="emoji-btn">😊</button>
104
+
105
+ <script src="https://cdn.jsdelivr.net/npm/@schwitzerskills/emojipicker/emoji-picker.js"></script>
106
+ <script>
107
+ const picker = new EmojiPicker({ container: '#emoji-btn' })
108
+ picker.on('emojiClick', (emoji) => {
109
+ document.querySelector('#message').value += emoji.char
110
+ })
111
+ </script>
104
112
 
105
- <script src="emoji-picker.js"></script>
113
+ </body>
114
+ </html>
106
115
  ```
107
116
 
108
- **Step 2 Initialize and listen for events:**
117
+ ### npm / Bundler (Vite, Webpack, etc.)
109
118
 
110
- ```html
111
- <script>
112
- const picker = new EmojiPicker({
113
- container: '#emoji-btn',
114
- theme: 'auto'
115
- })
119
+ ```js
120
+ import EmojiPicker from '@schwitzerskills/emojipicker'
116
121
 
117
- picker.on('emojiClick', (emoji) => {
118
- document.querySelector('#message').value += emoji.char
119
- })
120
- </script>
122
+ const picker = new EmojiPicker({
123
+ container: '#emoji-btn',
124
+ dataUrl: '/public/emoji-data.json' // adjust to your setup
125
+ })
126
+
127
+ picker.on('emojiClick', (emoji) => {
128
+ document.querySelector('#message').value += emoji.char
129
+ })
121
130
  ```
122
131
 
123
- That's it. Clicking `#emoji-btn` opens the picker. Clicking an emoji fires `emojiClick`.
132
+ ### One-liner with `attachToInput()`
133
+
134
+ ```js
135
+ import EmojiPicker from '@schwitzerskills/emojipicker'
136
+
137
+ // Automatically adds a 😊 button and inserts emoji at cursor
138
+ EmojiPicker.attachToInput('#message')
139
+ ```
124
140
 
125
141
  ---
126
142
 
@@ -130,48 +146,50 @@ All properties are optional.
130
146
 
131
147
  ```js
132
148
  const picker = new EmojiPicker({
133
- container: '#my-button', // Trigger element (CSS selector or DOM node)
134
- theme: 'auto', // 'light' | 'dark' | 'auto'
135
- mode: 'dropdown', // 'dropdown' | 'inline' | 'popup'
136
- search: true, // Show search input
137
- recentEmojis: true, // Track & show recently used emojis
138
- maxRecent: 24, // Max number of recent emojis to store
139
- skinTone: 'default', // Default skin tone (see Skin Tone Support)
140
- customEmojis: [], // Array of custom emoji objects
141
- perRow: 8, // Emojis per row in the grid
142
- emojiSize: 28, // Emoji size in pixels
143
- 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,
144
162
  })
145
163
  ```
146
164
 
147
165
  | Option | Type | Default | Description |
148
166
  |--------|------|---------|-------------|
149
- | `container` | `string \| HTMLElement` | `null` | Element that triggers open/close on click |
150
- | `theme` | `string` | `'auto'` | Color theme: `'light'`, `'dark'`, or `'auto'` |
151
- | `mode` | `string` | `'dropdown'` | Display mode (see [Modes](#modes)) |
152
- | `search` | `boolean` | `true` | Show/hide the search input |
153
- | `recentEmojis` | `boolean` | `true` | Enable recent emojis tab (uses localStorage) |
154
- | `maxRecent` | `number` | `24` | Max recent emojis to remember |
155
- | `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 |
156
175
  | `customEmojis` | `array` | `[]` | Custom emoji definitions |
157
176
  | `perRow` | `number` | `8` | Grid columns |
158
- | `emojiSize` | `number` | `28` | Emoji size in px |
159
- | `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` |
160
180
 
161
181
  ---
162
182
 
163
183
  ## Events
164
184
 
165
- The library is fully event-driven. Attach any number of listeners to any event.
166
-
167
185
  ```js
168
- picker.on(eventName, handler)
169
- picker.off(eventName, handler) // Remove a specific listener
186
+ picker.on(eventName, handler) // add listener
187
+ picker.off(eventName, handler) // remove listener
170
188
  ```
171
189
 
172
190
  ### `emojiClick`
173
191
 
174
- 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.
175
193
 
176
194
  ```js
177
195
  picker.on('emojiClick', (emoji, mouseEvent) => {
@@ -179,34 +197,25 @@ picker.on('emojiClick', (emoji, mouseEvent) => {
179
197
  console.log(emoji.name) // "face_with_tears_of_joy"
180
198
  console.log(emoji.category) // "Smileys & Emotion"
181
199
  console.log(emoji.unicode) // "1F602"
182
- console.log(emoji.skinTone) // null | "light" | "medium" | ...
200
+ console.log(emoji.skinTone) // null | "medium" | ...
183
201
  })
184
202
  ```
185
203
 
186
204
  ### `emojiHover`
187
205
 
188
- Fired when the user hovers over an emoji.
206
+ Fired when hovering over an emoji.
189
207
 
190
208
  ```js
191
209
  picker.on('emojiHover', (emoji, mouseEvent) => {
192
- myPreview.textContent = `${emoji.char} ${emoji.name}`
193
- })
194
- ```
195
-
196
- ### `pickerOpen`
197
-
198
- ```js
199
- picker.on('pickerOpen', () => {
200
- console.log('Picker opened')
210
+ myPreview.textContent = emoji.char + ' ' + emoji.name
201
211
  })
202
212
  ```
203
213
 
204
- ### `pickerClose`
214
+ ### `pickerOpen` / `pickerClose`
205
215
 
206
216
  ```js
207
- picker.on('pickerClose', () => {
208
- console.log('Picker closed')
209
- })
217
+ picker.on('pickerOpen', () => console.log('opened'))
218
+ picker.on('pickerClose', () => console.log('closed'))
210
219
  ```
211
220
 
212
221
  ### `categoryChange`
@@ -221,7 +230,7 @@ picker.on('categoryChange', ({ category }) => {
221
230
 
222
231
  ```js
223
232
  picker.on('search', ({ query }) => {
224
- console.log('Searching for:', query)
233
+ console.log('User typed:', query)
225
234
  })
226
235
  ```
227
236
 
@@ -229,38 +238,65 @@ picker.on('search', ({ query }) => {
229
238
 
230
239
  ## Methods
231
240
 
232
- 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:**
233
267
 
234
268
  ```js
235
- picker.open() // Open the picker
236
- picker.close() // Close the picker
237
- picker.toggle() // Toggle open/close
238
- picker.setTheme('dark') // Switch theme at runtime
239
- 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' })
240
275
  ```
241
276
 
242
277
  **Chaining:**
243
278
 
244
279
  ```js
245
- new EmojiPicker({ container: '#btn', theme: 'light' })
246
- .on('emojiClick', (e) => addEmoji(e.char))
247
- .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'))
248
284
  ```
249
285
 
250
286
  ---
251
287
 
252
288
  ## Emoji Object
253
289
 
254
- Every emoji-related event provides this data structure:
290
+ Every emoji-related event provides this structure:
255
291
 
256
292
  ```js
257
293
  {
258
- char: "👍🏽", // The emoji character (with skin tone applied)
259
- name: "thumbs_up", // Snake_case identifier
260
- category: "People & Body", // Category name
261
- unicode: "1F44D", // Base Unicode code point (hex)
262
- skinTone: "medium", // null if default, otherwise the tone name
263
- 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
264
300
  }
265
301
  ```
266
302
 
@@ -270,7 +306,7 @@ Every emoji-related event provides this data structure:
270
306
 
271
307
  ### `dropdown` *(default)*
272
308
 
273
- 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`.
274
310
 
275
311
  ```js
276
312
  new EmojiPicker({ container: '#btn', mode: 'dropdown' })
@@ -278,7 +314,7 @@ new EmojiPicker({ container: '#btn', mode: 'dropdown' })
278
314
 
279
315
  ### `inline`
280
316
 
281
- Always visible, embedded directly inside the container element. `autoClose` is ignored.
317
+ Always visible, embedded inside the container. `autoClose` is ignored.
282
318
 
283
319
  ```js
284
320
  new EmojiPicker({ container: '#my-div', mode: 'inline', autoClose: false })
@@ -290,18 +326,18 @@ new EmojiPicker({ container: '#my-div', mode: 'inline', autoClose: false })
290
326
 
291
327
  ### `popup`
292
328
 
293
- 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.
294
330
 
295
331
  ```js
296
332
  const picker = new EmojiPicker({ mode: 'popup' })
297
- document.getElementById('open-btn').addEventListener('click', () => picker.open())
333
+ document.getElementById('btn').addEventListener('click', () => picker.open())
298
334
  ```
299
335
 
300
336
  ---
301
337
 
302
338
  ## Skin Tone Support
303
339
 
304
- 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:
305
341
 
306
342
  ```js
307
343
  new EmojiPicker({ skinTone: 'medium-dark' })
@@ -316,46 +352,41 @@ new EmojiPicker({ skinTone: 'medium-dark' })
316
352
  | `'medium-dark'` | 👍🏾 |
317
353
  | `'dark'` | 👍🏿 |
318
354
 
319
- The selected tone is reflected in `emoji.char` and reported in `emoji.skinTone`.
320
-
321
355
  ---
322
356
 
323
357
  ## Theming & CSS Variables
324
358
 
325
- Override CSS variables to match any design system:
326
-
327
359
  ```css
328
360
  .ep-picker {
329
- --ep-bg: #0f1117;
330
- --ep-surface: #1a1d2e;
331
- --ep-surface2: #21253a;
332
- --ep-border: rgba(255,255,255,0.08);
333
- --ep-text: #e4e7f3;
334
- --ep-text-dim: #6b738f;
335
- --ep-accent: #6c63ff;
336
- --ep-hover: rgba(108,99,255,0.12);
337
- --ep-size: 28px; /* Emoji size */
338
- --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 */
339
370
  }
340
371
  ```
341
372
 
342
373
  ### Built-in themes
343
374
 
344
375
  ```js
345
- new EmojiPicker({ theme: 'light' }) // Light
346
- new EmojiPicker({ theme: 'dark' }) // Dark
347
- 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
348
379
 
349
- picker.setTheme('dark') // Switch at runtime
380
+ picker.setTheme('dark') // switch at runtime
350
381
  ```
351
382
 
352
- ### Custom accent color
383
+ ### Custom brand color
353
384
 
354
385
  ```css
355
386
  .ep-picker {
356
387
  --ep-accent: #e91e8c;
357
- --ep-hover: rgba(233, 30, 140, 0.1);
358
- --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);
359
390
  }
360
391
  ```
361
392
 
@@ -363,50 +394,139 @@ picker.setTheme('dark') // Switch at runtime
363
394
 
364
395
  ## Custom Emojis
365
396
 
366
- Add your own images, GIFs or SVGs alongside the standard set:
397
+ Add your own GIFs, PNGs or SVGs alongside the standard set:
367
398
 
368
399
  ```js
369
400
  new EmojiPicker({
370
401
  customEmojis: [
371
- { name: 'party_parrot', url: '/assets/parrot.gif' },
372
- { name: 'company_logo', url: '/assets/logo.png' },
373
- { 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' }
374
405
  ]
375
406
  })
376
407
  ```
377
408
 
378
- Custom emojis appear in their own **Custom** tab. The click event returns:
409
+ They appear in a dedicated **Custom** tab. Click event returns:
379
410
 
380
411
  ```js
381
- {
382
- char: null,
383
- name: 'party_parrot',
384
- category: 'custom',
385
- isCustom: true
386
- }
412
+ { char: null, name: 'party_parrot', category: 'custom', isCustom: true }
387
413
  ```
388
414
 
389
415
  ---
390
416
 
391
417
  ## Helper: attachToInput()
392
418
 
393
- 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.
394
420
 
395
421
  ```js
396
- // Attach by selector
397
- EmojiPicker.attachToInput('#message-input')
422
+ // Basic
423
+ EmojiPicker.attachToInput('#message')
398
424
 
399
425
  // With options
400
426
  EmojiPicker.attachToInput('#chat-box', {
401
- theme: 'dark',
402
- skinTone: 'medium'
427
+ theme: 'dark',
428
+ skinTone: 'medium',
429
+ dataUrl: '/static/emoji-data.json'
403
430
  })
404
431
 
405
- // Returns the picker instance for further event binding
406
- const picker = EmojiPicker.attachToInput('#my-input')
407
- picker.on('emojiClick', (emoji) => {
408
- 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)
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)
409
527
  })
528
+
529
+ const favs: FavoriteEmoji[] = await picker.getTopFavorites(10)
410
530
  ```
411
531
 
412
532
  ---
@@ -415,44 +535,48 @@ picker.on('emojiClick', (emoji) => {
415
535
 
416
536
  ### React
417
537
 
418
- Works out of the box with a standard `import`. Initialize inside `useEffect` so the library only runs in the browser.
419
-
420
- ```jsx
538
+ ```tsx
421
539
  import { useEffect, useRef } from 'react'
422
- import EmojiPicker from 'emojipicker-js'
540
+ import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
423
541
 
424
- function EmojiButton({ onSelect }) {
425
- 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)
426
548
 
427
549
  useEffect(() => {
550
+ if (!btnRef.current) return
428
551
  const picker = new EmojiPicker({
429
552
  container: btnRef.current,
430
- theme: 'auto'
553
+ theme: 'auto',
554
+ dataUrl: '/static/emoji-data.json'
431
555
  })
432
556
  picker.on('emojiClick', onSelect)
433
-
434
- return () => picker.destroy() // clean up on unmount
557
+ return () => picker.destroy()
435
558
  }, [onSelect])
436
559
 
437
- return <button ref={btnRef}>😊</button>
560
+ return <button ref={btnRef} type="button">😊</button>
438
561
  }
439
562
  ```
440
563
 
441
564
  ### Next.js
442
565
 
443
- 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.
444
567
 
445
568
  **Option A — dynamic import (recommended):**
446
569
 
447
- ```jsx
448
- // components/EmojiButton.jsx
570
+ ```tsx
571
+ // components/EmojiButton.tsx ← client-only wrapper
572
+ 'use client'
449
573
  import { useEffect, useRef } from 'react'
450
- import EmojiPicker from 'emojipicker-js'
574
+ import EmojiPicker from '@schwitzerskills/emojipicker'
451
575
 
452
576
  export default function EmojiButton({ onSelect }) {
453
577
  const btnRef = useRef(null)
454
578
  useEffect(() => {
455
- const picker = new EmojiPicker({ container: btnRef.current })
579
+ const picker = new EmojiPicker({ container: btnRef.current, dataUrl: '/emoji-data.json' })
456
580
  picker.on('emojiClick', onSelect)
457
581
  return () => picker.destroy()
458
582
  }, [onSelect])
@@ -460,25 +584,18 @@ export default function EmojiButton({ onSelect }) {
460
584
  }
461
585
  ```
462
586
 
463
- ```jsx
464
- // pages/index.jsx or any page
587
+ ```tsx
588
+ // app/page.tsx or pages/index.tsx
465
589
  import dynamic from 'next/dynamic'
466
590
 
467
- const EmojiButton = dynamic(() => import('../components/EmojiButton'), {
468
- ssr: false // prevents server-side execution
469
- })
591
+ const EmojiButton = dynamic(() => import('../components/EmojiButton'), { ssr: false })
470
592
  ```
471
593
 
472
- **Option B — useEffect guard (also safe):**
594
+ **Option B — App Router `'use client'` directive:**
473
595
 
474
- ```jsx
475
- useEffect(() => {
476
- // useEffect only runs in the browser, so this is always safe.
477
- // Add typeof window check only if you import EmojiPicker outside of useEffect.
478
- const picker = new EmojiPicker({ container: btnRef.current })
479
- picker.on('emojiClick', onSelect)
480
- return () => picker.destroy()
481
- }, [onSelect])
596
+ ```tsx
597
+ 'use client'
598
+ // useEffect only runs in the browser safe without ssr:false
482
599
  ```
483
600
 
484
601
  ### Vue 3
@@ -488,16 +605,16 @@ useEffect(() => {
488
605
  <button ref="btnRef">😊</button>
489
606
  </template>
490
607
 
491
- <script setup>
608
+ <script setup lang="ts">
492
609
  import { ref, onMounted, onUnmounted } from 'vue'
493
- import EmojiPicker from 'emojipicker-js'
610
+ import EmojiPicker, { EmojiObject } from '@schwitzerskills/emojipicker'
494
611
 
495
- const emit = defineEmits(['select'])
496
- const btnRef = ref(null)
497
- let picker = null
612
+ const emit = defineEmits<{ select: [emoji: EmojiObject] }>()
613
+ const btnRef = ref<HTMLButtonElement | null>(null)
614
+ let picker: EmojiPicker | null = null
498
615
 
499
616
  onMounted(() => {
500
- picker = new EmojiPicker({ container: btnRef.value, theme: 'auto' })
617
+ picker = new EmojiPicker({ container: btnRef.value!, dataUrl: '/emoji-data.json' })
501
618
  picker.on('emojiClick', (emoji) => emit('select', emoji))
502
619
  })
503
620
 
@@ -508,16 +625,16 @@ onUnmounted(() => picker?.destroy())
508
625
  ### Svelte
509
626
 
510
627
  ```svelte
511
- <script>
628
+ <script lang="ts">
512
629
  import { onMount, onDestroy, createEventDispatcher } from 'svelte'
513
- import EmojiPicker from 'emojipicker-js'
630
+ import EmojiPicker from '@schwitzerskills/emojipicker'
514
631
 
515
632
  const dispatch = createEventDispatcher()
516
- let btnEl
517
- let picker
633
+ let btnEl: HTMLButtonElement
634
+ let picker: EmojiPicker
518
635
 
519
636
  onMount(() => {
520
- picker = new EmojiPicker({ container: btnEl, theme: 'auto' })
637
+ picker = new EmojiPicker({ container: btnEl, dataUrl: '/emoji-data.json' })
521
638
  picker.on('emojiClick', (emoji) => dispatch('select', emoji))
522
639
  })
523
640
 
@@ -527,39 +644,22 @@ onUnmounted(() => picker?.destroy())
527
644
  <button bind:this={btnEl}>😊</button>
528
645
  ```
529
646
 
530
- ### Vanilla JS (no bundler)
531
-
532
- ```html
533
- <script src="https://cdn.example.com/emoji-picker.js"></script>
534
- <script>
535
- const picker = new EmojiPicker({ container: '#btn', theme: 'auto' })
536
- picker.on('emojiClick', (emoji) => {
537
- document.querySelector('#input').value += emoji.char
538
- })
539
- </script>
540
- ```
541
-
542
647
  ---
543
648
 
544
649
  ## Recipes / Examples
545
650
 
546
- ### Insert emoji at cursor in a textarea
651
+ ### Insert at cursor in a textarea
547
652
 
548
653
  ```js
549
654
  const textarea = document.querySelector('#editor')
550
- const picker = new EmojiPicker({ container: '#emoji-trigger' })
655
+ const picker = new EmojiPicker({ container: '#btn' })
551
656
 
552
657
  picker.on('emojiClick', (emoji) => {
553
- const start = textarea.selectionStart
554
- const end = textarea.selectionEnd
658
+ const s = textarea.selectionStart
659
+ const e = textarea.selectionEnd
555
660
  textarea.value =
556
- textarea.value.substring(0, start) +
557
- emoji.char +
558
- textarea.value.substring(end)
559
- textarea.setSelectionRange(
560
- start + emoji.char.length,
561
- start + emoji.char.length
562
- )
661
+ textarea.value.slice(0, s) + emoji.char + textarea.value.slice(e)
662
+ textarea.setSelectionRange(s + emoji.char.length, s + emoji.char.length)
563
663
  textarea.focus()
564
664
  })
565
665
  ```
@@ -568,76 +668,66 @@ picker.on('emojiClick', (emoji) => {
568
668
 
569
669
  ```js
570
670
  picker.on('emojiClick', (emoji) => {
571
- navigator.clipboard.writeText(emoji.char).then(() => {
572
- showToast(`Copied ${emoji.char}`)
573
- })
671
+ navigator.clipboard.writeText(emoji.char).then(() => showToast('Copied!'))
574
672
  })
575
673
  ```
576
674
 
577
- ### Send emoji reaction to a server
675
+ ### Send to a server
578
676
 
579
677
  ```js
580
678
  picker.on('emojiClick', (emoji) => {
581
679
  fetch('/api/reactions', {
582
- method: 'POST',
680
+ method: 'POST',
583
681
  headers: { 'Content-Type': 'application/json' },
584
- body: JSON.stringify({
585
- messageId: currentMessageId,
586
- emoji: emoji.name,
587
- char: emoji.char
588
- })
682
+ body: JSON.stringify({ messageId, emoji: emoji.name, char: emoji.char })
589
683
  })
590
684
  })
591
685
  ```
592
686
 
593
- ### Multiple pickers on the same page
687
+ ### Preload data on app startup
594
688
 
595
689
  ```js
596
- // Minimal reaction picker
597
- const reactionPicker = new EmojiPicker({
598
- container: '#reaction-btn',
599
- search: false,
600
- recentEmojis: false,
601
- autoClose: true
602
- })
603
-
604
- // Full editor picker
605
- const editorPicker = new EmojiPicker({
606
- container: '#editor-btn',
607
- search: true,
608
- skinTone: 'medium'
609
- })
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' })
610
693
  ```
611
694
 
612
- ### Track analytics
695
+ ### Quick-access bar from favorites
613
696
 
614
697
  ```js
615
- picker.on('pickerOpen', () => analytics.track('picker_opened'))
616
- picker.on('emojiClick', (emoji) => analytics.track('emoji_used', { name: emoji.name }))
617
- picker.on('search', ({ query }) => analytics.track('emoji_search', { query }))
618
- 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)
619
708
  ```
620
709
 
621
- ### Dynamically switch themes
710
+ ### Analytics
622
711
 
623
712
  ```js
624
- document.querySelector('#theme-toggle').addEventListener('click', () => {
625
- const isDark = document.body.classList.toggle('dark')
626
- picker.setTheme(isDark ? 'dark' : 'light')
627
- })
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 }))
628
717
  ```
629
718
 
630
719
  ---
631
720
 
632
721
  ## Accessibility
633
722
 
634
- - All interactive elements have `aria-label` and `role` attributes
635
- - The emoji grid uses `role="grid"` and `role="gridcell"`
636
- - The picker uses `role="dialog"` with `aria-label`
637
- - Category tabs use `role="tablist"`
638
- - 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
639
728
  - `Esc` closes the picker
640
729
  - Focus moves to the search input on open
730
+ - `type="button"` on all buttons — safe inside `<form>` elements
641
731
 
642
732
  ---
643
733
 
@@ -652,26 +742,72 @@ document.querySelector('#theme-toggle').addEventListener('click', () => {
652
742
  | iOS Safari | 14+ |
653
743
  | Android Chrome | 80+ |
654
744
 
655
- 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`).
746
+
747
+ ---
748
+
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.
656
780
 
657
781
  ---
658
782
 
659
- ## Categories
660
-
661
- | Tab | Category | Example |
662
- |-----|----------|---------|
663
- | 🕐 | Recently Used | *dynamic* |
664
- | 😊 | Smileys & Emotion | 😀 😂 🥰 😎 |
665
- | 👋 | People & Body | 👋 💪 🙏 🤝 |
666
- | 🐶 | Animals & Nature | 🐶 🦊 🌸 🌈 |
667
- | 🍕 | Food & Drink | 🍕 🍜 🍺 🧋 |
668
- | ⚽ | Activities | ⚽ 🎮 🎸 🏆 |
669
- | ✈️ | Travel & Places | ✈️ 🚀 🏖️ 🏰 |
670
- | 💡 | Objects | 💡 💻 📷 🔑 |
671
- | ❤️ | Symbols | ❤️ ✅ ♻️ 💯 |
672
- | 🏳️ | Flags | 🏳️‍🌈 🇺🇸 🇩🇪 🇯🇵 |
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
+ ```
673
808
 
674
809
  ---
675
810
 
676
811
  ## License
677
- Apache — free
812
+
813
+ Apache