@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.
@@ -0,0 +1,4 @@
1
+ declare module '*.feature?raw' {
2
+ const content: string
3
+ export default content
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './match-emojis'
2
+ export * from './use-emoji-picker'
@@ -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
+ }