@portabletext/plugin-emoji-picker 0.0.15
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 +3 -0
- package/dist/index.cjs +2711 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +209 -0
- package/dist/index.d.ts +209 -0
- package/dist/index.js +2703 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
- package/src/emoji-picker-machine.tsx +1009 -0
- package/src/emoji-picker.feature +127 -0
- package/src/emoji-picker.test.tsx +45 -0
- package/src/emojis.ts +15219 -0
- package/src/global.d.ts +4 -0
- package/src/index.ts +2 -0
- package/src/match-emojis.ts +121 -0
- package/src/use-emoji-picker.ts +181 -0
package/src/global.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {emojis} from './emojis'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proposed, but not required type, to represent an emoji match.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* {
|
|
9
|
+
* type: 'exact',
|
|
10
|
+
* key: '😂-joy',
|
|
11
|
+
* emoji: '😂',
|
|
12
|
+
* keyword: 'joy',
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* {
|
|
18
|
+
* type: 'partial',
|
|
19
|
+
* key: '😹-joy-_cat',
|
|
20
|
+
* emoji: '😹',
|
|
21
|
+
* keyword: 'joy',
|
|
22
|
+
* startSlice: '',
|
|
23
|
+
* endSlice: '_cat',
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @beta
|
|
28
|
+
*/
|
|
29
|
+
export type EmojiMatch =
|
|
30
|
+
| {
|
|
31
|
+
type: 'exact'
|
|
32
|
+
key: string
|
|
33
|
+
emoji: string
|
|
34
|
+
keyword: string
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: 'partial'
|
|
38
|
+
key: string
|
|
39
|
+
emoji: string
|
|
40
|
+
keyword: string
|
|
41
|
+
startSlice: string
|
|
42
|
+
endSlice: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A function that returns an array of emoji matches for a given keyword.
|
|
47
|
+
*
|
|
48
|
+
* @beta
|
|
49
|
+
*/
|
|
50
|
+
export type MatchEmojis<TEmojiMatch = EmojiMatch> = (query: {
|
|
51
|
+
keyword: string
|
|
52
|
+
}) => ReadonlyArray<TEmojiMatch>
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Proposed, but not required, default implementation of `MatchEmojis`.
|
|
56
|
+
*
|
|
57
|
+
* @beta
|
|
58
|
+
*/
|
|
59
|
+
export const matchEmojis: MatchEmojis = createMatchEmojis({emojis})
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Proposed, but not required, function to create a `MatchEmojis` function.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* const matchEmojis = createMatchEmojis({
|
|
67
|
+
* emojis: {
|
|
68
|
+
* '😂': ['joy'],
|
|
69
|
+
* '😹': ['joy_cat'],
|
|
70
|
+
* },
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @beta
|
|
75
|
+
*/
|
|
76
|
+
export function createMatchEmojis(config: {
|
|
77
|
+
emojis: Record<string, ReadonlyArray<string>>
|
|
78
|
+
}): MatchEmojis {
|
|
79
|
+
return ({keyword}: {keyword: string}) => {
|
|
80
|
+
const foundEmojis: Array<EmojiMatch> = []
|
|
81
|
+
|
|
82
|
+
if (keyword.length < 1) {
|
|
83
|
+
return foundEmojis
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const emoji in config.emojis) {
|
|
87
|
+
const emojiKeywords = config.emojis[emoji] ?? []
|
|
88
|
+
|
|
89
|
+
for (const emojiKeyword of emojiKeywords) {
|
|
90
|
+
const keywordIndex = emojiKeyword.indexOf(keyword)
|
|
91
|
+
|
|
92
|
+
if (keywordIndex === -1) {
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (emojiKeyword === keyword) {
|
|
97
|
+
foundEmojis.push({
|
|
98
|
+
type: 'exact',
|
|
99
|
+
key: `${emoji}-${keyword}`,
|
|
100
|
+
emoji,
|
|
101
|
+
keyword,
|
|
102
|
+
})
|
|
103
|
+
} else {
|
|
104
|
+
const start = emojiKeyword.slice(0, keywordIndex)
|
|
105
|
+
const end = emojiKeyword.slice(keywordIndex + keyword.length)
|
|
106
|
+
|
|
107
|
+
foundEmojis.push({
|
|
108
|
+
type: 'partial',
|
|
109
|
+
key: `${emoji}-${start}${keyword}${end}`,
|
|
110
|
+
emoji,
|
|
111
|
+
keyword,
|
|
112
|
+
startSlice: start,
|
|
113
|
+
endSlice: end,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return foundEmojis
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {useEditor} from '@portabletext/editor'
|
|
2
|
+
import {useActorRef, useSelector} from '@xstate/react'
|
|
3
|
+
import {useCallback} from 'react'
|
|
4
|
+
import {emojiPickerMachine} from './emoji-picker-machine'
|
|
5
|
+
import type {EmojiMatch, MatchEmojis} from './match-emojis'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @beta
|
|
9
|
+
*/
|
|
10
|
+
export type EmojiPicker<TEmojiMatch = EmojiMatch> = {
|
|
11
|
+
/**
|
|
12
|
+
* The matched keyword, including colons.
|
|
13
|
+
*
|
|
14
|
+
* Can be used to display the keyword in the UI or conditionally render the
|
|
15
|
+
* list of matches.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* if (keyword.length < 2) {
|
|
20
|
+
* return null
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
keyword: string
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Emoji matches found for the current keyword.
|
|
28
|
+
*
|
|
29
|
+
* Can be used to display the matches in a list.
|
|
30
|
+
*/
|
|
31
|
+
matches: ReadonlyArray<TEmojiMatch>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The index of the selected match.
|
|
35
|
+
*
|
|
36
|
+
* Can be used to highlight the selected match in the list.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <EmojiListItem
|
|
41
|
+
* key={match.key}
|
|
42
|
+
* match={match}
|
|
43
|
+
* selected={selectedIndex === index}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
selectedIndex: number
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Navigate to a specific match by index.
|
|
51
|
+
*
|
|
52
|
+
* Can be used to control the `selectedIndex`. For example, using
|
|
53
|
+
* `onMouseEnter`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <EmojiListItem
|
|
58
|
+
* key={match.key}
|
|
59
|
+
* match={match}
|
|
60
|
+
* selected={selectedIndex === index}
|
|
61
|
+
* onMouseEnter={() => {onNavigateTo(index)}}
|
|
62
|
+
* />
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
onNavigateTo: (index: number) => void
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Select the current match.
|
|
69
|
+
*
|
|
70
|
+
* Can be used to insert the currently selected match.
|
|
71
|
+
*
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```tsx
|
|
75
|
+
* <EmojiListItem
|
|
76
|
+
* key={match.key}
|
|
77
|
+
* match={match}
|
|
78
|
+
* selected={selectedIndex === index}
|
|
79
|
+
* onMouseEnter={() => {onNavigateTo(index)}}
|
|
80
|
+
* onSelect={() => {onSelect()}}
|
|
81
|
+
* />
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* Note: The currently selected match is automatically inserted on Enter or
|
|
85
|
+
* Tab.
|
|
86
|
+
*/
|
|
87
|
+
onSelect: () => void
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Dismiss the emoji picker. Can be used to let the user dismiss the picker
|
|
91
|
+
* by clicking a button.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* {matches.length === 0 ? (
|
|
96
|
+
* <Button onPress={onDismiss}>Dismiss</Button>
|
|
97
|
+
* ) : <EmojiListBox {...props} />}
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Note: The emoji picker is automatically dismissed on Escape.
|
|
101
|
+
*/
|
|
102
|
+
onDismiss: () => void
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @beta
|
|
107
|
+
*/
|
|
108
|
+
export type EmojiPickerProps<TEmojiMatch = EmojiMatch> = {
|
|
109
|
+
matchEmojis: MatchEmojis<TEmojiMatch>
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Handles the state and logic needed to create an emoji picker.
|
|
114
|
+
*
|
|
115
|
+
* The `matchEmojis` function is generic and can return any shape of emoji
|
|
116
|
+
* match required for the emoji picker.
|
|
117
|
+
*
|
|
118
|
+
* However, the default implementation of `matchEmojis` returns an array of
|
|
119
|
+
* `EmojiMatch` objects and can be created using the `createMatchEmojis`
|
|
120
|
+
* function.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
*
|
|
124
|
+
* ```tsx
|
|
125
|
+
* const matchEmojis = createMatchEmojis({emojis: {
|
|
126
|
+
* '😂': ['joy'],
|
|
127
|
+
* '😹': ['joy_cat'],
|
|
128
|
+
* }})
|
|
129
|
+
*
|
|
130
|
+
* const {keyword, matches, selectedIndex, onDismiss, onNavigateTo, onSelect} =
|
|
131
|
+
* useEmojiPicker({matchEmojis})
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* Note: This hook is not concerned with the UI, how the emoji picker is
|
|
135
|
+
* rendered or positioned in the document.
|
|
136
|
+
*
|
|
137
|
+
* @beta
|
|
138
|
+
*/
|
|
139
|
+
export function useEmojiPicker<TEmojiMatch = EmojiMatch>(
|
|
140
|
+
props: EmojiPickerProps<TEmojiMatch>,
|
|
141
|
+
): EmojiPicker<TEmojiMatch> {
|
|
142
|
+
const editor = useEditor()
|
|
143
|
+
const emojiPickerActor = useActorRef(emojiPickerMachine, {
|
|
144
|
+
input: {editor, matchEmojis: props.matchEmojis as MatchEmojis<EmojiMatch>},
|
|
145
|
+
})
|
|
146
|
+
const keyword = useSelector(
|
|
147
|
+
emojiPickerActor,
|
|
148
|
+
(snapshot) => snapshot.context.keyword,
|
|
149
|
+
)
|
|
150
|
+
const matches = useSelector(
|
|
151
|
+
emojiPickerActor,
|
|
152
|
+
(snapshot) => snapshot.context.matches as ReadonlyArray<TEmojiMatch>,
|
|
153
|
+
)
|
|
154
|
+
const selectedIndex = useSelector(
|
|
155
|
+
emojiPickerActor,
|
|
156
|
+
(snapshot) => snapshot.context.selectedIndex,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const onDismiss = useCallback(() => {
|
|
160
|
+
emojiPickerActor.send({type: 'dismiss'})
|
|
161
|
+
}, [emojiPickerActor])
|
|
162
|
+
const onNavigateTo = useCallback(
|
|
163
|
+
(index: number) => {
|
|
164
|
+
emojiPickerActor.send({type: 'navigate to', index})
|
|
165
|
+
},
|
|
166
|
+
[emojiPickerActor],
|
|
167
|
+
)
|
|
168
|
+
const onSelect = useCallback(() => {
|
|
169
|
+
emojiPickerActor.send({type: 'insert selected match'})
|
|
170
|
+
editor.send({type: 'focus'})
|
|
171
|
+
}, [emojiPickerActor, editor])
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
keyword,
|
|
175
|
+
matches,
|
|
176
|
+
selectedIndex,
|
|
177
|
+
onDismiss,
|
|
178
|
+
onNavigateTo,
|
|
179
|
+
onSelect,
|
|
180
|
+
}
|
|
181
|
+
}
|