@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 +21 -0
- package/README.md +96 -1
- package/dist/index.cjs +61 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +61 -81
- package/dist/index.js.map +1 -1
- package/package.json +30 -30
- package/src/emoji-picker-machine.tsx +84 -99
- package/src/emoji-picker.feature +50 -1
- package/src/emoji-picker.test.tsx +42 -5
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
|
|
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
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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
|
-
"
|
|
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 !== "
|
|
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 !== "
|
|
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 !== "
|
|
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: "
|
|
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
|
-
"
|
|
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
|
|
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
|
|
2618
|
+
actions: ["increment selected index", "update submit listener context"]
|
|
2639
2619
|
},
|
|
2640
2620
|
"navigate up": {
|
|
2641
|
-
actions: ["decrement selected index", "update
|
|
2621
|
+
actions: ["decrement selected index", "update submit listener context"]
|
|
2642
2622
|
},
|
|
2643
2623
|
"navigate to": {
|
|
2644
|
-
actions: ["set selected index", "update
|
|
2624
|
+
actions: ["set selected index", "update submit listener context"]
|
|
2645
2625
|
},
|
|
2646
2626
|
"insert selected match": {
|
|
2647
2627
|
actions: ["insert selected match"]
|