@portabletext/plugin-emoji-picker 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-emoji-picker",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Easily configure an Emoji Picker for the Portable Text Editor",
5
5
  "keywords": [
6
6
  "portabletext",
@@ -56,9 +56,9 @@
56
56
  "typescript": "5.9.3",
57
57
  "typescript-eslint": "^8.46.1",
58
58
  "vitest": "^3.2.4",
59
- "@portabletext/editor": "2.15.5",
60
59
  "@portabletext/schema": "1.2.0",
61
- "racejar": "1.3.2"
60
+ "racejar": "1.3.2",
61
+ "@portabletext/editor": "2.15.5"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@portabletext/editor": "^2.15.5",
@@ -0,0 +1,113 @@
1
+ import {emojis} from './emojis'
2
+ import type {MatchEmojis} from './match-emojis'
3
+
4
+ /**
5
+ * Proposed, but not required type, to represent an emoji match.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * {
10
+ * type: 'exact',
11
+ * key: '😂-joy',
12
+ * emoji: '😂',
13
+ * keyword: 'joy',
14
+ * }
15
+ * ```
16
+ * @example
17
+ * ```tsx
18
+ * {
19
+ * type: 'partial',
20
+ * key: '😹-joy-_cat',
21
+ * emoji: '😹',
22
+ * keyword: 'joy',
23
+ * startSlice: '',
24
+ * endSlice: '_cat',
25
+ * }
26
+ * ```
27
+ *
28
+ * @beta
29
+ */
30
+ export type EmojiMatch =
31
+ | {
32
+ type: 'exact'
33
+ key: string
34
+ emoji: string
35
+ keyword: string
36
+ }
37
+ | {
38
+ type: 'partial'
39
+ key: string
40
+ emoji: string
41
+ keyword: string
42
+ startSlice: string
43
+ endSlice: string
44
+ }
45
+
46
+ /**
47
+ * Proposed, but not required, default implementation of `MatchEmojis`.
48
+ *
49
+ * @beta
50
+ */
51
+ export const matchEmojis = createMatchEmojis({emojis})
52
+
53
+ /**
54
+ * Proposed, but not required, function to create a `MatchEmojis` function.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const matchEmojis = createMatchEmojis({
59
+ * emojis: {
60
+ * '😂': ['joy'],
61
+ * '😹': ['joy_cat'],
62
+ * },
63
+ * })
64
+ * ```
65
+ *
66
+ * @beta
67
+ */
68
+ export function createMatchEmojis(config: {
69
+ emojis: Record<string, ReadonlyArray<string>>
70
+ }): MatchEmojis<EmojiMatch> {
71
+ return ({keyword}: {keyword: string}) => {
72
+ const foundEmojis: Array<EmojiMatch> = []
73
+
74
+ if (keyword.length < 1) {
75
+ return foundEmojis
76
+ }
77
+
78
+ for (const emoji in config.emojis) {
79
+ const emojiKeywords = config.emojis[emoji] ?? []
80
+
81
+ for (const emojiKeyword of emojiKeywords) {
82
+ const keywordIndex = emojiKeyword.indexOf(keyword)
83
+
84
+ if (keywordIndex === -1) {
85
+ continue
86
+ }
87
+
88
+ if (emojiKeyword === keyword) {
89
+ foundEmojis.push({
90
+ type: 'exact',
91
+ key: `${emoji}-${keyword}`,
92
+ emoji,
93
+ keyword,
94
+ })
95
+ } else {
96
+ const start = emojiKeyword.slice(0, keywordIndex)
97
+ const end = emojiKeyword.slice(keywordIndex + keyword.length)
98
+
99
+ foundEmojis.push({
100
+ type: 'partial',
101
+ key: `${emoji}-${start}${keyword}${end}`,
102
+ emoji,
103
+ keyword,
104
+ startSlice: start,
105
+ endSlice: end,
106
+ })
107
+ }
108
+ }
109
+ }
110
+
111
+ return foundEmojis
112
+ }
113
+ }
@@ -28,7 +28,7 @@ import {
28
28
  type AnyEventObject,
29
29
  type CallbackLogicFunction,
30
30
  } from 'xstate'
31
- import type {EmojiMatch, MatchEmojis} from './match-emojis'
31
+ import type {BaseEmojiMatch, MatchEmojis} from './match-emojis'
32
32
 
33
33
  /*******************
34
34
  * Keyboard shortcuts
@@ -175,10 +175,10 @@ function createKeywordFoundEvent(payload: {
175
175
  } as const
176
176
  }
177
177
 
178
- type EmojiPickerContext<TEmojiMatch = EmojiMatch> = {
178
+ type EmojiPickerContext = {
179
179
  editor: Editor
180
- matches: ReadonlyArray<TEmojiMatch>
181
- matchEmojis: MatchEmojis<TEmojiMatch>
180
+ matches: ReadonlyArray<BaseEmojiMatch>
181
+ matchEmojis: MatchEmojis<BaseEmojiMatch>
182
182
  selectedIndex: number
183
183
  keywordAnchor:
184
184
  | {
@@ -32,14 +32,14 @@ Feature: Emoji Picker
32
32
  Scenario: Picking wrong direct hit
33
33
  When ":jo:" is typed
34
34
  Then the text is ":jo:"
35
- And the keyword is ":jo:"
35
+ And the keyword is "jo"
36
36
  And the matches are "😂, 😹"
37
37
 
38
38
  Scenario: Colon after wrong direct hit
39
39
  When ":jo:" is typed
40
40
  And ":" is typed
41
41
  Then the text is ":jo::"
42
- And the keyword is ":jo::"
42
+ And the keyword is "jo:"
43
43
  And the matches are ""
44
44
 
45
45
  Scenario: Picking wrong direct hit after undoing direct hit
@@ -9,8 +9,8 @@ import {page, type Locator} from '@vitest/browser/context'
9
9
  import {Before, Then} from 'racejar'
10
10
  import {Feature} from 'racejar/vitest'
11
11
  import {expect, vi} from 'vitest'
12
+ import {createMatchEmojis} from './create-match-emojis'
12
13
  import emojiPickerFeature from './emoji-picker.feature?raw'
13
- import {createMatchEmojis} from './match-emojis'
14
14
  import {useEmojiPicker} from './use-emoji-picker'
15
15
 
16
16
  type EmojiPickerTestContext = Context & {
package/src/emojis.ts CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * @beta
3
- */
4
1
  export const emojis: Record<string, Array<string>> = {
5
2
  '😀': [
6
3
  'grinning_face',
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './create-match-emojis'
1
2
  export * from './match-emojis'
2
3
  export * from './use-emoji-picker'
@@ -1,45 +1,16 @@
1
- import {emojis} from './emojis'
2
-
3
1
  /**
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
- * ```
2
+ * The base type representing an emoji match.
26
3
  *
27
4
  * @beta
28
5
  */
29
- export type EmojiMatch =
6
+ export type BaseEmojiMatch =
30
7
  | {
31
8
  type: 'exact'
32
- key: string
33
9
  emoji: string
34
- keyword: string
35
10
  }
36
11
  | {
37
12
  type: 'partial'
38
- key: string
39
13
  emoji: string
40
- keyword: string
41
- startSlice: string
42
- endSlice: string
43
14
  }
44
15
 
45
16
  /**
@@ -47,75 +18,5 @@ export type EmojiMatch =
47
18
  *
48
19
  * @beta
49
20
  */
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
- }
21
+ export type MatchEmojis<TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch> =
22
+ (query: {keyword: string}) => ReadonlyArray<TEmojiMatch>
@@ -2,21 +2,21 @@ import {useEditor} from '@portabletext/editor'
2
2
  import {useActorRef, useSelector} from '@xstate/react'
3
3
  import {useCallback} from 'react'
4
4
  import {emojiPickerMachine} from './emoji-picker-machine'
5
- import type {EmojiMatch, MatchEmojis} from './match-emojis'
5
+ import type {BaseEmojiMatch, MatchEmojis} from './match-emojis'
6
6
 
7
7
  /**
8
8
  * @beta
9
9
  */
10
- export type EmojiPicker<TEmojiMatch = EmojiMatch> = {
10
+ export type EmojiPicker<TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch> = {
11
11
  /**
12
- * The matched keyword, including colons.
12
+ * The matched keyword.
13
13
  *
14
14
  * Can be used to display the keyword in the UI or conditionally render the
15
15
  * list of matches.
16
16
  *
17
17
  * @example
18
18
  * ```tsx
19
- * if (keyword.length < 2) {
19
+ * if (keyword.length < 1) {
20
20
  * return null
21
21
  * }
22
22
  * ```
@@ -105,7 +105,9 @@ export type EmojiPicker<TEmojiMatch = EmojiMatch> = {
105
105
  /**
106
106
  * @beta
107
107
  */
108
- export type EmojiPickerProps<TEmojiMatch = EmojiMatch> = {
108
+ export type EmojiPickerProps<
109
+ TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch,
110
+ > = {
109
111
  matchEmojis: MatchEmojis<TEmojiMatch>
110
112
  }
111
113
 
@@ -136,17 +138,19 @@ export type EmojiPickerProps<TEmojiMatch = EmojiMatch> = {
136
138
  *
137
139
  * @beta
138
140
  */
139
- export function useEmojiPicker<TEmojiMatch = EmojiMatch>(
140
- props: EmojiPickerProps<TEmojiMatch>,
141
- ): EmojiPicker<TEmojiMatch> {
141
+ export function useEmojiPicker<
142
+ TEmojiMatch extends BaseEmojiMatch = BaseEmojiMatch,
143
+ >(props: EmojiPickerProps<TEmojiMatch>): EmojiPicker<TEmojiMatch> {
142
144
  const editor = useEditor()
143
145
  const emojiPickerActor = useActorRef(emojiPickerMachine, {
144
- input: {editor, matchEmojis: props.matchEmojis as MatchEmojis<EmojiMatch>},
146
+ input: {editor, matchEmojis: props.matchEmojis},
147
+ })
148
+ const keyword = useSelector(emojiPickerActor, (snapshot) => {
149
+ const rawKeyword = snapshot.context.keyword.startsWith(':')
150
+ ? snapshot.context.keyword.slice(1)
151
+ : snapshot.context.keyword
152
+ return rawKeyword.endsWith(':') ? rawKeyword.slice(0, -1) : rawKeyword
145
153
  })
146
- const keyword = useSelector(
147
- emojiPickerActor,
148
- (snapshot) => snapshot.context.keyword,
149
- )
150
154
  const matches = useSelector(
151
155
  emojiPickerActor,
152
156
  (snapshot) => snapshot.context.matches as ReadonlyArray<TEmojiMatch>,