@portabletext/plugin-emoji-picker 0.0.15 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 - 2025 Sanity.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,98 @@
1
1
  # `@portabletext/plugin-emoji-picker`
2
2
 
3
- > Easily configure an Emoji Picker for the Portable Text Editor
3
+ > ⚡️ Easily configure an Emoji Picker for the Portable Text Editor
4
+
5
+ ## Quick Start
6
+
7
+ The `useEmojiPicker` hook handles the state and logic needed to create an emoji picker for the Portable Text Editor. It manages keyword matching, keyboard navigation, and emoji insertion, but is not concerned with the UI, how the picker is rendered, or how it's positioned in the document.
8
+
9
+ ```tsx
10
+ import {matchEmojis, useEmojiPicker} from '@portabletext/plugin-emoji-picker'
11
+
12
+ function EmojiPicker() {
13
+ const {keyword, matches, selectedIndex, onNavigateTo, onSelect, onDismiss} =
14
+ useEmojiPicker({matchEmojis})
15
+
16
+ if (keyword.length < 2) {
17
+ return null
18
+ }
19
+
20
+ return (
21
+ <div className="emoji-picker">
22
+ {matches.length === 0 ? (
23
+ <div>
24
+ No emojis found for "{keyword}"
25
+ <button onClick={onDismiss}>Dismiss</button>
26
+ </div>
27
+ ) : (
28
+ <ul>
29
+ {matches.map((match, index) => (
30
+ <li
31
+ key={match.key}
32
+ onMouseEnter={() => onNavigateTo(index)}
33
+ onClick={onSelect}
34
+ className={selectedIndex === index ? 'selected' : ''}
35
+ >
36
+ {match.emoji} {match.keyword}
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ )}
41
+ </div>
42
+ )
43
+ }
44
+ ```
45
+
46
+ ## How It Works
47
+
48
+ The emoji picker activates when users type `:` followed by a keyword (e.g., `:smile` or `:joy`).
49
+
50
+ - **Keyboard shortcuts are built-in**:
51
+ - `Enter` or `Tab` inserts the selected emoji
52
+ - `↑` / `↓` navigate through matches
53
+ - `Esc` dismisses the picker
54
+ - **Mouse interactions are opt-in**: Use `onNavigateTo` and `onSelect` to enable hover and click
55
+
56
+ ## API Reference
57
+
58
+ ### `useEmojiPicker(props)`
59
+
60
+ **Props:**
61
+
62
+ - `matchEmojis`: A function that takes a keyword and returns matching emojis
63
+
64
+ **Returns:**
65
+
66
+ - `keyword`: The matches keyword, including colons.
67
+ - `matches`: Emoji matches found for the current keyword.
68
+ - `selectedIndex`: Index of the selected match
69
+ - `onNavigateTo(index)`: Navigate to a specific match by index.
70
+ - `onSelect()`: Select the current match.
71
+ - `onDismiss()`: Dismiss the emoji picker.
72
+
73
+ ## Custom Emoji Sets
74
+
75
+ The `matchEmojis` function is generic and can return any shape of emoji match required for your UI. However, the default implementation returns `EmojiMatch` objects and can be created using `createMatchEmojis`:
76
+
77
+ ```tsx
78
+ import {
79
+ createMatchEmojis,
80
+ useEmojiPicker,
81
+ } from '@portabletext/plugin-emoji-picker'
82
+
83
+ const myMatchEmojis = createMatchEmojis({
84
+ emojis: {
85
+ '😂': ['joy', 'laugh'],
86
+ '😹': ['joy_cat', 'laugh_cat'],
87
+ '❤️': ['heart', 'love'],
88
+ '🎉': ['party', 'celebrate'],
89
+ },
90
+ })
91
+
92
+ function MyEmojiPicker() {
93
+ const picker = useEmojiPicker({matchEmojis: myMatchEmojis})
94
+ // ... render your UI
95
+ }
96
+ ```
97
+
98
+ The default `matchEmojis` export includes a comprehensive set of emojis. You can also implement a completely custom matching function that returns any shape you need for your specific UI requirements.
package/dist/index.cjs CHANGED
@@ -2049,7 +2049,7 @@ function createKeywordFoundEvent(payload) {
2049
2049
  ...payload
2050
2050
  };
2051
2051
  }
2052
- const colonListenerCallback = ({
2052
+ const triggerListenerCallback = ({
2053
2053
  sendBack,
2054
2054
  input
2055
2055
  }) => {
@@ -2072,10 +2072,7 @@ const colonListenerCallback = ({
2072
2072
  actions: [({
2073
2073
  event
2074
2074
  }) => [behaviors.effect(() => {
2075
- sendBack({
2076
- ...event,
2077
- type: "colon inserted"
2078
- });
2075
+ sendBack(event);
2079
2076
  })]]
2080
2077
  })
2081
2078
  }), input.editor.registerBehavior({
@@ -2084,10 +2081,7 @@ const colonListenerCallback = ({
2084
2081
  actions: [({
2085
2082
  event
2086
2083
  }) => [behaviors.effect(() => {
2087
- sendBack({
2088
- ...event,
2089
- type: "colon inserted"
2090
- });
2084
+ sendBack(event);
2091
2085
  })]]
2092
2086
  })
2093
2087
  })];
@@ -2145,65 +2139,28 @@ const colonListenerCallback = ({
2145
2139
  };
2146
2140
  }, emojiInsertListener = ({
2147
2141
  sendBack,
2148
- input,
2149
- receive
2150
- }) => {
2151
- let context = input.context;
2152
- receive((event) => {
2153
- context = event.context;
2154
- });
2155
- const unregisterBehaviors = [input.context.editor.registerBehavior({
2156
- behavior: behaviors.defineBehavior({
2157
- on: "custom.insert emoji",
2158
- actions: [({
2159
- event
2160
- }) => [behaviors.effect(() => {
2161
- sendBack({
2162
- type: "dismiss"
2163
- });
2164
- }), behaviors.raise({
2165
- type: "delete.text",
2166
- at: {
2167
- anchor: event.anchor,
2168
- focus: event.focus
2169
- }
2170
- }), behaviors.raise({
2171
- type: "insert.text",
2172
- text: event.emoji
2173
- })]]
2174
- })
2175
- }), input.context.editor.registerBehavior({
2176
- behavior: behaviors.defineBehavior({
2177
- on: "insert.text",
2178
- guard: ({
2179
- event
2180
- }) => {
2181
- if (event.text !== ":")
2182
- return !1;
2183
- const anchor = context.keywordAnchor?.blockOffset, focus = context.keywordFocus, match = context.matches[context.selectedIndex];
2184
- return match && match.type === "exact" && anchor && focus ? {
2185
- anchor,
2186
- focus,
2187
- emoji: match.emoji
2188
- } : !1;
2189
- },
2190
- actions: [(_, {
2191
- anchor,
2192
- focus,
2193
- emoji
2194
- }) => [behaviors.raise({
2195
- type: "custom.insert emoji",
2196
- emoji,
2197
- anchor,
2198
- focus
2199
- })]]
2200
- })
2201
- })];
2202
- return () => {
2203
- for (const unregister of unregisterBehaviors)
2204
- unregister();
2205
- };
2206
- }, submitListenerCallback = ({
2142
+ input
2143
+ }) => input.context.editor.registerBehavior({
2144
+ behavior: behaviors.defineBehavior({
2145
+ on: "custom.insert emoji",
2146
+ actions: [({
2147
+ event
2148
+ }) => [behaviors.effect(() => {
2149
+ sendBack({
2150
+ type: "dismiss"
2151
+ });
2152
+ }), behaviors.raise({
2153
+ type: "delete.text",
2154
+ at: {
2155
+ anchor: event.anchor,
2156
+ focus: event.focus
2157
+ }
2158
+ }), behaviors.raise({
2159
+ type: "insert.text",
2160
+ text: event.emoji
2161
+ })]]
2162
+ })
2163
+ }), submitListenerCallback = ({
2207
2164
  sendBack,
2208
2165
  input,
2209
2166
  receive
@@ -2250,6 +2207,19 @@ const colonListenerCallback = ({
2250
2207
  });
2251
2208
  })]]
2252
2209
  })
2210
+ }), input.context.editor.registerBehavior({
2211
+ behavior: pluginInputRule.defineInputRuleBehavior({
2212
+ rules: [keywordRule]
2213
+ })
2214
+ }), input.context.editor.registerBehavior({
2215
+ behavior: behaviors.defineBehavior({
2216
+ on: "custom.keyword found",
2217
+ actions: [({
2218
+ event
2219
+ }) => [behaviors.effect(() => {
2220
+ sendBack(event);
2221
+ })]]
2222
+ })
2253
2223
  })];
2254
2224
  return () => {
2255
2225
  for (const unregister of unregisterBehaviors)
@@ -2342,7 +2312,7 @@ const colonListenerCallback = ({
2342
2312
  "emoji insert listener": xstate.fromCallback(emojiInsertListener),
2343
2313
  "submit listener": xstate.fromCallback(submitListenerCallback),
2344
2314
  "arrow listener": xstate.fromCallback(arrowListenerCallback),
2345
- "colon listener": xstate.fromCallback(colonListenerCallback),
2315
+ "trigger listener": xstate.fromCallback(triggerListenerCallback),
2346
2316
  "escape listener": xstate.fromCallback(escapeListenerCallback),
2347
2317
  "selection listener": xstate.fromCallback(selectionListenerCallback),
2348
2318
  "text change listener": xstate.fromCallback(textChangeListener)
@@ -2352,19 +2322,19 @@ const colonListenerCallback = ({
2352
2322
  keyword: ({
2353
2323
  context,
2354
2324
  event
2355
- }) => event.type !== "colon inserted" && event.type !== "custom.keyword found" ? context.keyword : event.keyword
2325
+ }) => event.type !== "custom.trigger found" && event.type !== "custom.partial keyword found" && event.type !== "custom.keyword found" ? context.keyword : event.keyword
2356
2326
  }),
2357
2327
  "set keyword anchor": xstate.assign({
2358
2328
  keywordAnchor: ({
2359
2329
  context,
2360
2330
  event
2361
- }) => event.type !== "colon inserted" && event.type !== "custom.keyword found" ? context.keywordAnchor : event.keywordAnchor
2331
+ }) => event.type !== "custom.trigger found" && event.type !== "custom.partial keyword found" && event.type !== "custom.keyword found" ? context.keywordAnchor : event.keywordAnchor
2362
2332
  }),
2363
2333
  "set keyword focus": xstate.assign({
2364
2334
  keywordFocus: ({
2365
2335
  context,
2366
2336
  event
2367
- }) => event.type !== "colon inserted" && event.type !== "custom.keyword found" ? context.keywordFocus : event.keywordFocus
2337
+ }) => event.type !== "custom.trigger found" && event.type !== "custom.partial keyword found" && event.type !== "custom.keyword found" ? context.keywordFocus : event.keywordFocus
2368
2338
  }),
2369
2339
  "update keyword focus": xstate.assign({
2370
2340
  keywordFocus: ({
@@ -2440,10 +2410,11 @@ const colonListenerCallback = ({
2440
2410
  context
2441
2411
  })),
2442
2412
  "insert selected match": ({
2443
- context
2413
+ context,
2414
+ event
2444
2415
  }) => {
2445
2416
  const match = context.matches[context.selectedIndex];
2446
- !match || !context.keywordAnchor || !context.keywordFocus || context.editor.send({
2417
+ !match || !context.keywordAnchor || !context.keywordFocus || event.type === "custom.keyword found" && match.type !== "exact" || context.editor.send({
2447
2418
  type: "custom.insert emoji",
2448
2419
  emoji: match.emoji,
2449
2420
  anchor: context.keywordAnchor.blockOffset,
@@ -2536,7 +2507,7 @@ const colonListenerCallback = ({
2536
2507
  idle: {
2537
2508
  entry: ["reset"],
2538
2509
  invoke: {
2539
- src: "colon listener",
2510
+ src: "trigger listener",
2540
2511
  input: ({
2541
2512
  context
2542
2513
  }) => ({
@@ -2544,12 +2515,18 @@ const colonListenerCallback = ({
2544
2515
  })
2545
2516
  },
2546
2517
  on: {
2547
- "colon inserted": {
2518
+ "custom.trigger found": {
2519
+ target: "searching",
2520
+ actions: ["set keyword anchor", "set keyword focus", "init keyword"]
2521
+ },
2522
+ "custom.partial keyword found": {
2548
2523
  target: "searching",
2549
2524
  actions: ["set keyword anchor", "set keyword focus", "init keyword"]
2550
2525
  },
2551
2526
  "custom.keyword found": {
2552
- actions: ["set keyword anchor", "set keyword focus", "init keyword", "update matches", "insert selected match"]
2527
+ actions: ["set keyword anchor", "set keyword focus", "init keyword", "update matches", "insert selected match"],
2528
+ target: "idle",
2529
+ reenter: !0
2553
2530
  }
2554
2531
  }
2555
2532
  },
@@ -2585,6 +2562,9 @@ const colonListenerCallback = ({
2585
2562
  })
2586
2563
  }],
2587
2564
  on: {
2565
+ "custom.keyword found": {
2566
+ actions: ["set keyword anchor", "set keyword focus", "init keyword", "update matches", "insert selected match"]
2567
+ },
2588
2568
  "insert.text": [{
2589
2569
  guard: "unexpected text insertion",
2590
2570
  target: "idle"
@@ -2604,7 +2584,7 @@ const colonListenerCallback = ({
2604
2584
  guard: "selection moved unexpectedly",
2605
2585
  target: "idle"
2606
2586
  }, {
2607
- actions: ["update keyword", "update matches", "reset selected index", "update emoji insert listener context", "update submit listener context"]
2587
+ actions: ["update keyword", "update matches", "reset selected index", "update submit listener context"]
2608
2588
  }]
2609
2589
  },
2610
2590
  always: [{
@@ -2635,13 +2615,13 @@ const colonListenerCallback = ({
2635
2615
  }],
2636
2616
  on: {
2637
2617
  "navigate down": {
2638
- actions: ["increment selected index", "update emoji insert listener context", "update submit listener context"]
2618
+ actions: ["increment selected index", "update submit listener context"]
2639
2619
  },
2640
2620
  "navigate up": {
2641
- actions: ["decrement selected index", "update emoji insert listener context", "update submit listener context"]
2621
+ actions: ["decrement selected index", "update submit listener context"]
2642
2622
  },
2643
2623
  "navigate to": {
2644
- actions: ["set selected index", "update emoji insert listener context", "update submit listener context"]
2624
+ actions: ["set selected index", "update submit listener context"]
2645
2625
  },
2646
2626
  "insert selected match": {
2647
2627
  actions: ["insert selected match"]