@portabletext/plugin-typeahead-picker 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/dist/index.js ADDED
@@ -0,0 +1,1117 @@
1
+ import { c } from "react/compiler-runtime";
2
+ import { useEditor } from "@portabletext/editor";
3
+ import { useActor } from "@xstate/react";
4
+ import { defineBehavior, effect, forward, raise } from "@portabletext/editor/behaviors";
5
+ import { getFocusSpan, getNextSpan, isSelectionCollapsed, isPointAfterSelection, isPointBeforeSelection, getMarkState, getPreviousSpan } from "@portabletext/editor/selectors";
6
+ import { isEqualPaths, isEqualSelectionPoints } from "@portabletext/editor/utils";
7
+ import { createKeyboardShortcut } from "@portabletext/keyboard-shortcuts";
8
+ import { defineInputRuleBehavior, defineInputRule } from "@portabletext/plugin-input-rule";
9
+ import { setup, assign, sendTo, fromPromise, fromCallback } from "xstate";
10
+ function defineTypeaheadPicker(config) {
11
+ return {
12
+ ...config,
13
+ _id: /* @__PURE__ */ Symbol("typeahead-picker")
14
+ };
15
+ }
16
+ function extractKeywordFromPattern(matchedText, pattern, autoCompleteWith) {
17
+ if (autoCompleteWith && matchedText.endsWith(autoCompleteWith)) {
18
+ const strippedMatch = matchedText.slice(0, -autoCompleteWith.length).match(pattern), strippedKeyword = strippedMatch ? strippedMatch[1] ?? strippedMatch[0] : "";
19
+ if (strippedKeyword.length > 0)
20
+ return strippedKeyword;
21
+ }
22
+ const match = matchedText.match(pattern);
23
+ return match ? match[1] ?? match[0] : matchedText;
24
+ }
25
+ function escapeRegExp(str) {
26
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ }
28
+ function normalizePattern(pattern) {
29
+ return new RegExp(pattern.source);
30
+ }
31
+ function buildCompletePattern(definition) {
32
+ if (!definition.autoCompleteWith)
33
+ return;
34
+ const autoCompleteWith = escapeRegExp(definition.autoCompleteWith);
35
+ return new RegExp(`${definition.pattern.source}${autoCompleteWith}`);
36
+ }
37
+ const arrowUpShortcut = createKeyboardShortcut({
38
+ default: [{
39
+ key: "ArrowUp"
40
+ }]
41
+ }), arrowDownShortcut = createKeyboardShortcut({
42
+ default: [{
43
+ key: "ArrowDown"
44
+ }]
45
+ }), enterShortcut = createKeyboardShortcut({
46
+ default: [{
47
+ key: "Enter"
48
+ }]
49
+ }), tabShortcut = createKeyboardShortcut({
50
+ default: [{
51
+ key: "Tab"
52
+ }]
53
+ }), escapeShortcut = createKeyboardShortcut({
54
+ default: [{
55
+ key: "Escape"
56
+ }]
57
+ }), getTriggerState = (snapshot) => {
58
+ const focusSpan = getFocusSpan(snapshot), markState = getMarkState(snapshot);
59
+ if (!focusSpan || !markState || !snapshot.context.selection)
60
+ return;
61
+ const focusSpanTextBefore = focusSpan.node.text.slice(0, snapshot.context.selection.focus.offset), focusSpanTextAfter = focusSpan.node.text.slice(snapshot.context.selection.focus.offset), previousSpan = getPreviousSpan(snapshot), nextSpan = getNextSpan(snapshot);
62
+ return {
63
+ focusSpan,
64
+ markState,
65
+ focusSpanTextBefore,
66
+ focusSpanTextAfter,
67
+ previousSpan,
68
+ nextSpan
69
+ };
70
+ };
71
+ function createTriggerActions({
72
+ snapshot,
73
+ payload,
74
+ keywordState,
75
+ pickerId
76
+ }) {
77
+ if (payload.markState.state === "unchanged") {
78
+ const textBeforeMatch = payload.focusSpanTextBefore.slice(0, payload.lastMatch.targetOffsets.anchor.offset), focusSpan2 = {
79
+ node: {
80
+ _key: payload.focusSpan.node._key,
81
+ _type: payload.focusSpan.node._type,
82
+ text: `${textBeforeMatch}${payload.lastMatch.text}${payload.focusSpanTextAfter}`,
83
+ marks: payload.markState.marks
84
+ },
85
+ path: payload.focusSpan.path,
86
+ textBefore: textBeforeMatch,
87
+ textAfter: payload.focusSpanTextAfter
88
+ };
89
+ return keywordState === "complete" ? [raise(createKeywordFoundEvent({
90
+ focusSpan: focusSpan2,
91
+ extractedKeyword: payload.extractedKeyword,
92
+ pickerId
93
+ }))] : [raise(createTriggerFoundEvent({
94
+ focusSpan: focusSpan2,
95
+ extractedKeyword: payload.extractedKeyword,
96
+ pickerId
97
+ }))];
98
+ }
99
+ const newSpan = {
100
+ _key: snapshot.context.keyGenerator(),
101
+ _type: payload.focusSpan.node._type,
102
+ text: payload.lastMatch.text,
103
+ marks: payload.markState.marks
104
+ };
105
+ let focusSpan = {
106
+ node: {
107
+ _key: newSpan._key,
108
+ _type: newSpan._type,
109
+ text: `${newSpan.text}${payload.nextSpan?.node.text ?? payload.focusSpanTextAfter}`,
110
+ marks: payload.markState.marks
111
+ },
112
+ path: [{
113
+ _key: payload.focusSpan.path[0]._key
114
+ }, "children", {
115
+ _key: newSpan._key
116
+ }],
117
+ textBefore: "",
118
+ textAfter: payload.nextSpan?.node.text ?? payload.focusSpanTextAfter
119
+ };
120
+ return payload.previousSpan && payload.focusSpanTextBefore.length === 0 && JSON.stringify(payload.previousSpan.node.marks ?? []) === JSON.stringify(payload.markState.marks) && (focusSpan = {
121
+ node: {
122
+ _key: payload.previousSpan.node._key,
123
+ _type: newSpan._type,
124
+ text: `${payload.previousSpan.node.text}${newSpan.text}`,
125
+ marks: newSpan.marks
126
+ },
127
+ path: payload.previousSpan.path,
128
+ textBefore: payload.previousSpan.node.text,
129
+ textAfter: ""
130
+ }), [raise({
131
+ type: "select",
132
+ at: payload.lastMatch.targetOffsets
133
+ }), raise({
134
+ type: "delete",
135
+ at: payload.lastMatch.targetOffsets
136
+ }), raise({
137
+ type: "insert.child",
138
+ child: newSpan
139
+ }), ...keywordState === "complete" ? [raise(createKeywordFoundEvent({
140
+ focusSpan,
141
+ extractedKeyword: payload.extractedKeyword,
142
+ pickerId
143
+ }))] : [raise(createTriggerFoundEvent({
144
+ focusSpan,
145
+ extractedKeyword: payload.extractedKeyword,
146
+ pickerId
147
+ }))]];
148
+ }
149
+ function createTriggerFoundEvent(payload) {
150
+ return {
151
+ type: "custom.typeahead trigger found",
152
+ ...payload
153
+ };
154
+ }
155
+ function createKeywordFoundEvent(payload) {
156
+ return {
157
+ type: "custom.typeahead keyword found",
158
+ ...payload
159
+ };
160
+ }
161
+ function extractFullMatchFromFocusSpan(focusSpan) {
162
+ return focusSpan.textBefore.length > 0 && focusSpan.textAfter.length > 0 ? focusSpan.node.text.slice(focusSpan.textBefore.length, -focusSpan.textAfter.length) : focusSpan.textBefore.length > 0 ? focusSpan.node.text.slice(focusSpan.textBefore.length) : focusSpan.textAfter.length > 0 ? focusSpan.node.text.slice(0, -focusSpan.textAfter.length) : focusSpan.node.text;
163
+ }
164
+ function validateFocusSpan(currentFocusSpan, editor, partialPattern, completePattern) {
165
+ const snapshot = editor.getSnapshot(), focusSpan = getFocusSpan(snapshot);
166
+ if (!snapshot.context.selection || !focusSpan)
167
+ return;
168
+ const nextSpan = getNextSpan({
169
+ ...snapshot,
170
+ context: {
171
+ ...snapshot.context,
172
+ selection: {
173
+ anchor: {
174
+ path: currentFocusSpan.path,
175
+ offset: 0
176
+ },
177
+ focus: {
178
+ path: currentFocusSpan.path,
179
+ offset: 0
180
+ }
181
+ }
182
+ }
183
+ });
184
+ if (!isEqualPaths(focusSpan.path, currentFocusSpan.path))
185
+ return nextSpan && currentFocusSpan.textAfter.length === 0 && snapshot.context.selection.focus.offset === 0 && isSelectionCollapsed(snapshot) ? currentFocusSpan : void 0;
186
+ if (!focusSpan.node.text.startsWith(currentFocusSpan.textBefore) || !focusSpan.node.text.endsWith(currentFocusSpan.textAfter))
187
+ return;
188
+ const keywordAnchor = {
189
+ path: focusSpan.path,
190
+ offset: currentFocusSpan.textBefore.length
191
+ }, keywordFocus = {
192
+ path: focusSpan.path,
193
+ offset: focusSpan.node.text.length - currentFocusSpan.textAfter.length
194
+ }, selectionIsBeforeKeyword = isPointAfterSelection(keywordAnchor)(snapshot), selectionIsAfterKeyword = isPointBeforeSelection(keywordFocus)(snapshot);
195
+ if (selectionIsBeforeKeyword || selectionIsAfterKeyword)
196
+ return;
197
+ const keywordText = focusSpan.node.text.slice(currentFocusSpan.textBefore.length, currentFocusSpan.textAfter.length > 0 ? -currentFocusSpan.textAfter.length : void 0), patternMatch = keywordText.match(partialPattern);
198
+ if (!patternMatch || patternMatch.index !== 0)
199
+ return;
200
+ let matchEnd = currentFocusSpan.textBefore.length + patternMatch[0].length;
201
+ if (completePattern) {
202
+ const completeMatch = keywordText.match(completePattern);
203
+ completeMatch && completeMatch.index === 0 && completeMatch[0] === keywordText && (matchEnd = currentFocusSpan.textBefore.length + completeMatch[0].length);
204
+ }
205
+ if (!(snapshot.context.selection.focus.offset > matchEnd))
206
+ return {
207
+ node: focusSpan.node,
208
+ path: focusSpan.path,
209
+ textBefore: currentFocusSpan.textBefore,
210
+ textAfter: currentFocusSpan.textAfter
211
+ };
212
+ }
213
+ function extractKeywordFromMatch(match, pattern, autoCompleteWith) {
214
+ const firstGroupMatch = match.groupMatches[0];
215
+ return firstGroupMatch && firstGroupMatch.text.length > 0 ? firstGroupMatch.text : extractKeywordFromPattern(match.text, pattern, autoCompleteWith);
216
+ }
217
+ function createInputRules(definition) {
218
+ const rules = [], partialPattern = definition.pattern, completePattern = buildCompletePattern(definition);
219
+ if (completePattern) {
220
+ const completeTriggerRule = defineInputRule({
221
+ on: completePattern,
222
+ guard: ({
223
+ snapshot,
224
+ event
225
+ }) => {
226
+ const lastMatch = event.matches.at(-1);
227
+ if (!lastMatch || lastMatch.targetOffsets.anchor.offset < event.textBefore.length)
228
+ return !1;
229
+ const triggerState = getTriggerState(snapshot);
230
+ return triggerState ? {
231
+ ...triggerState,
232
+ lastMatch,
233
+ extractedKeyword: extractKeywordFromMatch(lastMatch, partialPattern, definition.autoCompleteWith)
234
+ } : !1;
235
+ },
236
+ actions: [({
237
+ snapshot
238
+ }, payload) => createTriggerActions({
239
+ snapshot,
240
+ payload,
241
+ keywordState: "complete",
242
+ pickerId: definition._id
243
+ })]
244
+ });
245
+ rules.push(completeTriggerRule);
246
+ }
247
+ const partialTriggerRule = defineInputRule({
248
+ on: partialPattern,
249
+ guard: ({
250
+ snapshot,
251
+ event
252
+ }) => {
253
+ const lastMatch = event.matches.at(-1);
254
+ if (!lastMatch || lastMatch.targetOffsets.anchor.offset < event.textBefore.length)
255
+ return !1;
256
+ const triggerState = getTriggerState(snapshot);
257
+ if (!triggerState)
258
+ return !1;
259
+ if (completePattern) {
260
+ const completeMatch = triggerState.focusSpan.node.text.slice(lastMatch.targetOffsets.anchor.offset).match(completePattern);
261
+ if (completeMatch && completeMatch.index === 0)
262
+ return !1;
263
+ }
264
+ return {
265
+ ...triggerState,
266
+ lastMatch,
267
+ extractedKeyword: extractKeywordFromMatch(lastMatch, partialPattern, definition.autoCompleteWith)
268
+ };
269
+ },
270
+ actions: [({
271
+ snapshot
272
+ }, payload) => createTriggerActions({
273
+ snapshot,
274
+ payload,
275
+ keywordState: "partial",
276
+ pickerId: definition._id
277
+ })]
278
+ });
279
+ return rules.push(partialTriggerRule), rules;
280
+ }
281
+ const triggerListenerCallback = () => ({
282
+ sendBack,
283
+ input
284
+ }) => {
285
+ const rules = createInputRules(input.definition), unregisterBehaviors = [input.editor.registerBehavior({
286
+ behavior: defineInputRuleBehavior({
287
+ rules
288
+ })
289
+ }), input.editor.registerBehavior({
290
+ behavior: defineBehavior({
291
+ on: "custom.typeahead keyword found",
292
+ guard: ({
293
+ event
294
+ }) => event.pickerId === input.definition._id,
295
+ actions: [({
296
+ event
297
+ }) => [effect(() => {
298
+ sendBack(event);
299
+ })]]
300
+ })
301
+ }), input.editor.registerBehavior({
302
+ behavior: defineBehavior({
303
+ on: "custom.typeahead trigger found",
304
+ guard: ({
305
+ event
306
+ }) => event.pickerId === input.definition._id,
307
+ actions: [({
308
+ event
309
+ }) => [effect(() => {
310
+ sendBack(event);
311
+ })]]
312
+ })
313
+ })];
314
+ return () => {
315
+ for (const unregister of unregisterBehaviors)
316
+ unregister();
317
+ };
318
+ }, escapeListenerCallback = () => ({
319
+ sendBack,
320
+ input
321
+ }) => input.editor.registerBehavior({
322
+ behavior: defineBehavior({
323
+ on: "keyboard.keydown",
324
+ guard: ({
325
+ event
326
+ }) => escapeShortcut.guard(event.originEvent),
327
+ actions: [() => [effect(() => {
328
+ sendBack({
329
+ type: "dismiss"
330
+ });
331
+ })]]
332
+ })
333
+ }), arrowListenerCallback = () => ({
334
+ sendBack,
335
+ input
336
+ }) => {
337
+ const unregisterBehaviors = [input.editor.registerBehavior({
338
+ behavior: defineBehavior({
339
+ on: "keyboard.keydown",
340
+ guard: ({
341
+ event
342
+ }) => arrowDownShortcut.guard(event.originEvent),
343
+ actions: [() => [effect(() => {
344
+ sendBack({
345
+ type: "navigate down"
346
+ });
347
+ })]]
348
+ })
349
+ }), input.editor.registerBehavior({
350
+ behavior: defineBehavior({
351
+ on: "keyboard.keydown",
352
+ guard: ({
353
+ event
354
+ }) => arrowUpShortcut.guard(event.originEvent),
355
+ actions: [() => [effect(() => {
356
+ sendBack({
357
+ type: "navigate up"
358
+ });
359
+ })]]
360
+ })
361
+ })];
362
+ return () => {
363
+ for (const unregister of unregisterBehaviors)
364
+ unregister();
365
+ };
366
+ }, selectionListenerCallback = () => ({
367
+ sendBack,
368
+ input
369
+ }) => input.editor.on("selection", () => {
370
+ sendBack({
371
+ type: "selection changed"
372
+ });
373
+ }).unsubscribe, submitListenerCallback = () => ({
374
+ sendBack,
375
+ input,
376
+ receive
377
+ }) => {
378
+ let context = input.context;
379
+ receive((event) => {
380
+ context = event.context;
381
+ });
382
+ const unregisterBehaviors = [input.context.editor.registerBehavior({
383
+ behavior: defineBehavior({
384
+ on: "keyboard.keydown",
385
+ guard: ({
386
+ event
387
+ }) => {
388
+ if (!enterShortcut.guard(event.originEvent) && !tabShortcut.guard(event.originEvent))
389
+ return !1;
390
+ const focusSpan = context.focusSpan, match = context.matches[context.selectedIndex];
391
+ return match && focusSpan ? {
392
+ focusSpan,
393
+ match
394
+ } : !1;
395
+ },
396
+ actions: [() => [effect(() => {
397
+ sendBack({
398
+ type: "select"
399
+ });
400
+ })]]
401
+ })
402
+ }), input.context.editor.registerBehavior({
403
+ behavior: defineBehavior({
404
+ on: "keyboard.keydown",
405
+ guard: ({
406
+ event
407
+ }) => (enterShortcut.guard(event.originEvent) || tabShortcut.guard(event.originEvent)) && context.fullMatch.length === 1,
408
+ actions: [({
409
+ event
410
+ }) => [forward(event), effect(() => {
411
+ sendBack({
412
+ type: "dismiss"
413
+ });
414
+ })]]
415
+ })
416
+ }), input.context.editor.registerBehavior({
417
+ behavior: defineBehavior({
418
+ on: "keyboard.keydown",
419
+ guard: ({
420
+ event
421
+ }) => (enterShortcut.guard(event.originEvent) || tabShortcut.guard(event.originEvent)) && context.fullMatch.length > 1 && context.matches.length === 0,
422
+ actions: [() => [effect(() => {
423
+ sendBack({
424
+ type: "dismiss"
425
+ });
426
+ })]]
427
+ })
428
+ })];
429
+ return () => {
430
+ for (const unregister of unregisterBehaviors)
431
+ unregister();
432
+ };
433
+ }, textInsertionListenerCallback = () => ({
434
+ sendBack,
435
+ input,
436
+ receive
437
+ }) => {
438
+ let context = input.context;
439
+ return receive((event) => {
440
+ context = event.context;
441
+ }), input.context.editor.registerBehavior({
442
+ behavior: defineBehavior({
443
+ on: "insert.text",
444
+ guard: ({
445
+ snapshot
446
+ }) => {
447
+ if (!context.focusSpan || !snapshot.context.selection)
448
+ return !1;
449
+ const keywordAnchor = {
450
+ path: context.focusSpan.path,
451
+ offset: context.focusSpan.textBefore.length
452
+ };
453
+ return isEqualSelectionPoints(snapshot.context.selection.focus, keywordAnchor);
454
+ },
455
+ actions: [({
456
+ event
457
+ }) => [forward(event), effect(() => {
458
+ sendBack({
459
+ type: "dismiss"
460
+ });
461
+ })]]
462
+ })
463
+ });
464
+ }, insertMatchListenerCallback = () => ({
465
+ sendBack,
466
+ input
467
+ }) => input.context.editor.registerBehavior({
468
+ behavior: defineBehavior({
469
+ on: "custom.typeahead insert match",
470
+ guard: ({
471
+ event
472
+ }) => event.pickerId === input.context.definition._id,
473
+ actions: [({
474
+ event,
475
+ snapshot,
476
+ dom
477
+ }) => {
478
+ const patternSelection = {
479
+ anchor: {
480
+ path: event.focusSpan.path,
481
+ offset: event.focusSpan.textBefore.length
482
+ },
483
+ focus: {
484
+ path: event.focusSpan.path,
485
+ offset: event.focusSpan.node.text.length - event.focusSpan.textAfter.length
486
+ }
487
+ }, allActions = [effect(() => {
488
+ sendBack({
489
+ type: "dismiss"
490
+ });
491
+ })];
492
+ for (const actionSet of input.context.definition.actions) {
493
+ const actions = actionSet({
494
+ snapshot,
495
+ dom,
496
+ event: {
497
+ type: "typeahead.select",
498
+ match: event.match,
499
+ keyword: event.keyword,
500
+ patternSelection
501
+ }
502
+ }, !0);
503
+ for (const action of actions)
504
+ allActions.push(action);
505
+ }
506
+ return allActions.push(effect(() => {
507
+ queueMicrotask(() => {
508
+ const currentSelection = input.context.editor.getSnapshot().context.selection;
509
+ currentSelection && input.context.editor.send({
510
+ type: "select",
511
+ at: currentSelection
512
+ });
513
+ });
514
+ })), allActions;
515
+ }]
516
+ })
517
+ });
518
+ function createTypeaheadPickerMachine() {
519
+ return setup({
520
+ types: {
521
+ context: {},
522
+ input: {},
523
+ events: {}
524
+ },
525
+ delays: {
526
+ DEBOUNCE: ({
527
+ context
528
+ }) => context.definition.debounceMs ?? 0
529
+ },
530
+ actors: {
531
+ "trigger listener": fromCallback(triggerListenerCallback()),
532
+ "escape listener": fromCallback(escapeListenerCallback()),
533
+ "arrow listener": fromCallback(arrowListenerCallback()),
534
+ "selection listener": fromCallback(selectionListenerCallback()),
535
+ "submit listener": fromCallback(submitListenerCallback()),
536
+ "text insertion listener": fromCallback(textInsertionListenerCallback()),
537
+ "insert match listener": fromCallback(insertMatchListenerCallback()),
538
+ "get matches": fromPromise(async ({
539
+ input
540
+ }) => {
541
+ const result = input.getMatches({
542
+ keyword: input.keyword
543
+ }), matches = await Promise.resolve(result);
544
+ return {
545
+ keyword: input.keyword,
546
+ matches
547
+ };
548
+ })
549
+ },
550
+ actions: {
551
+ "handle trigger found": assign(({
552
+ context,
553
+ event
554
+ }) => {
555
+ if (event.type !== "custom.typeahead trigger found" && event.type !== "custom.typeahead keyword found")
556
+ return {};
557
+ const focusSpan = event.focusSpan, fullMatch = extractFullMatchFromFocusSpan(focusSpan), keyword = event.extractedKeyword;
558
+ if (context.definition.mode === "async" || context.definition.debounceMs)
559
+ return {
560
+ focusSpan,
561
+ fullMatch,
562
+ keyword,
563
+ isLoading: !0,
564
+ selectedIndex: 0
565
+ };
566
+ const matches = context.definition.getMatches({
567
+ keyword
568
+ });
569
+ return {
570
+ focusSpan,
571
+ fullMatch,
572
+ keyword,
573
+ matches,
574
+ loadingFullMatch: fullMatch,
575
+ isLoading: !1,
576
+ selectedIndex: 0
577
+ };
578
+ }),
579
+ "handle selection changed": assign(({
580
+ context
581
+ }) => {
582
+ if (!context.focusSpan)
583
+ return {
584
+ focusSpan: void 0,
585
+ fullMatch: "",
586
+ keyword: "",
587
+ matches: [],
588
+ selectedIndex: 0,
589
+ isLoading: !1
590
+ };
591
+ const updatedFocusSpan = validateFocusSpan(context.focusSpan, context.editor, context.partialPattern, context.completePattern);
592
+ if (!updatedFocusSpan)
593
+ return {
594
+ focusSpan: void 0,
595
+ fullMatch: "",
596
+ keyword: "",
597
+ matches: [],
598
+ selectedIndex: 0,
599
+ isLoading: !1
600
+ };
601
+ const fullMatch = extractFullMatchFromFocusSpan(updatedFocusSpan);
602
+ if (fullMatch === context.fullMatch)
603
+ return {
604
+ focusSpan: updatedFocusSpan
605
+ };
606
+ const keyword = extractKeywordFromPattern(fullMatch, context.definition.pattern, context.definition.autoCompleteWith);
607
+ if (context.definition.mode === "async" || context.definition.debounceMs)
608
+ return {
609
+ focusSpan: updatedFocusSpan,
610
+ fullMatch,
611
+ keyword,
612
+ selectedIndex: 0,
613
+ isLoading: context.isLoading || context.loadingFullMatch !== fullMatch
614
+ };
615
+ const matches = context.definition.getMatches({
616
+ keyword
617
+ });
618
+ return {
619
+ focusSpan: updatedFocusSpan,
620
+ fullMatch,
621
+ keyword,
622
+ matches,
623
+ loadingFullMatch: fullMatch,
624
+ selectedIndex: 0,
625
+ isLoading: !1
626
+ };
627
+ }),
628
+ "handle async load complete": assign(({
629
+ context,
630
+ event
631
+ }) => {
632
+ const output = event.output;
633
+ return output.keyword !== context.keyword ? {
634
+ isLoading: context.fullMatch !== context.loadingFullMatch
635
+ } : {
636
+ matches: output.matches,
637
+ isLoading: context.fullMatch !== context.loadingFullMatch
638
+ };
639
+ }),
640
+ reset: assign({
641
+ fullMatch: "",
642
+ keyword: "",
643
+ matches: [],
644
+ selectedIndex: 0,
645
+ isLoading: !1,
646
+ loadingFullMatch: "",
647
+ focusSpan: void 0,
648
+ error: void 0
649
+ }),
650
+ navigate: assign(({
651
+ context,
652
+ event
653
+ }) => context.matches.length === 0 ? {
654
+ selectedIndex: 0
655
+ } : event.type === "navigate to" ? {
656
+ selectedIndex: event.index
657
+ } : event.type === "navigate up" ? {
658
+ selectedIndex: (context.selectedIndex - 1 + context.matches.length) % context.matches.length
659
+ } : {
660
+ selectedIndex: (context.selectedIndex + 1) % context.matches.length
661
+ }),
662
+ "insert match": ({
663
+ context
664
+ }, params) => {
665
+ if (!context.focusSpan)
666
+ return;
667
+ const match = params.exact ? getExactMatch(context.matches) : context.matches[context.selectedIndex];
668
+ match && context.editor.send({
669
+ type: "custom.typeahead insert match",
670
+ match,
671
+ focusSpan: context.focusSpan,
672
+ keyword: context.keyword,
673
+ pickerId: context.definition._id
674
+ });
675
+ },
676
+ "update submit listener context": sendTo("submit listener", ({
677
+ context
678
+ }) => ({
679
+ type: "context changed",
680
+ context
681
+ })),
682
+ "update text insertion listener context": sendTo("text insertion listener", ({
683
+ context
684
+ }) => ({
685
+ type: "context changed",
686
+ context
687
+ })),
688
+ "handle error": assign({
689
+ isLoading: !1,
690
+ error: ({
691
+ event
692
+ }) => event.error
693
+ })
694
+ },
695
+ guards: {
696
+ "no focus span": ({
697
+ context
698
+ }) => !context.focusSpan,
699
+ "no debounce": ({
700
+ context
701
+ }) => !context.definition.debounceMs || context.definition.debounceMs === 0,
702
+ "is complete keyword": ({
703
+ context
704
+ }) => {
705
+ if (!context.completePattern || !context.focusSpan)
706
+ return !1;
707
+ const fullKeywordText = context.focusSpan.node.text.slice(context.focusSpan.textBefore.length, context.focusSpan.textAfter.length > 0 ? -context.focusSpan.textAfter.length : void 0), completeMatch = fullKeywordText.match(context.completePattern);
708
+ return !completeMatch || completeMatch.index !== 0 || completeMatch[0] !== fullKeywordText ? !1 : hasExactlyOneExactMatch(context.matches);
709
+ },
710
+ "has matches": ({
711
+ context
712
+ }) => context.matches.length > 0,
713
+ "no matches": ({
714
+ context
715
+ }) => context.matches.length === 0,
716
+ "is loading": ({
717
+ context
718
+ }) => context.isLoading
719
+ }
720
+ }).createMachine({
721
+ id: "typeahead picker",
722
+ context: ({
723
+ input
724
+ }) => ({
725
+ editor: input.editor,
726
+ definition: input.definition,
727
+ partialPattern: normalizePattern(input.definition.pattern),
728
+ completePattern: buildCompletePattern(input.definition),
729
+ matches: [],
730
+ selectedIndex: 0,
731
+ focusSpan: void 0,
732
+ fullMatch: "",
733
+ keyword: "",
734
+ loadingFullMatch: "",
735
+ error: void 0,
736
+ isLoading: !1
737
+ }),
738
+ initial: "idle",
739
+ states: {
740
+ idle: {
741
+ entry: ["reset"],
742
+ invoke: {
743
+ src: "trigger listener",
744
+ input: ({
745
+ context
746
+ }) => ({
747
+ editor: context.editor,
748
+ definition: context.definition
749
+ })
750
+ },
751
+ on: {
752
+ "custom.typeahead trigger found": {
753
+ target: "active",
754
+ actions: ["handle trigger found"]
755
+ },
756
+ "custom.typeahead keyword found": {
757
+ target: "checking complete",
758
+ actions: ["handle trigger found"]
759
+ }
760
+ }
761
+ },
762
+ "checking complete": {
763
+ invoke: [{
764
+ src: "insert match listener",
765
+ input: ({
766
+ context
767
+ }) => ({
768
+ context
769
+ })
770
+ }, {
771
+ src: "get matches",
772
+ input: ({
773
+ context
774
+ }) => ({
775
+ keyword: context.keyword,
776
+ getMatches: context.definition.getMatches
777
+ }),
778
+ onDone: [{
779
+ guard: ({
780
+ event
781
+ }) => hasExactlyOneExactMatch(event.output.matches),
782
+ target: "idle",
783
+ actions: [assign({
784
+ matches: ({
785
+ event
786
+ }) => event.output.matches
787
+ }), {
788
+ type: "insert match",
789
+ params: {
790
+ exact: !0
791
+ }
792
+ }]
793
+ }, {
794
+ target: "active",
795
+ actions: [assign({
796
+ matches: ({
797
+ event
798
+ }) => event.output.matches
799
+ })]
800
+ }],
801
+ onError: {
802
+ target: "active.no matches",
803
+ actions: ["handle error"]
804
+ }
805
+ }]
806
+ },
807
+ active: {
808
+ invoke: [{
809
+ src: "insert match listener",
810
+ input: ({
811
+ context
812
+ }) => ({
813
+ context
814
+ })
815
+ }, {
816
+ src: "escape listener",
817
+ input: ({
818
+ context
819
+ }) => ({
820
+ editor: context.editor
821
+ })
822
+ }, {
823
+ src: "selection listener",
824
+ input: ({
825
+ context
826
+ }) => ({
827
+ editor: context.editor
828
+ })
829
+ }, {
830
+ src: "submit listener",
831
+ id: "submit listener",
832
+ input: ({
833
+ context
834
+ }) => ({
835
+ context
836
+ })
837
+ }, {
838
+ src: "text insertion listener",
839
+ id: "text insertion listener",
840
+ input: ({
841
+ context
842
+ }) => ({
843
+ context
844
+ })
845
+ }],
846
+ on: {
847
+ dismiss: {
848
+ target: "idle"
849
+ },
850
+ "selection changed": {
851
+ actions: ["handle selection changed", "update submit listener context", "update text insertion listener context"]
852
+ }
853
+ },
854
+ always: [{
855
+ guard: "no focus span",
856
+ target: "idle"
857
+ }, {
858
+ guard: "is complete keyword",
859
+ actions: [{
860
+ type: "insert match",
861
+ params: {
862
+ exact: !1
863
+ }
864
+ }],
865
+ target: "idle"
866
+ }],
867
+ initial: "evaluating",
868
+ states: {
869
+ evaluating: {
870
+ always: [{
871
+ guard: "is loading",
872
+ target: "loading"
873
+ }, {
874
+ guard: "has matches",
875
+ target: "showing matches"
876
+ }, {
877
+ target: "no matches"
878
+ }]
879
+ },
880
+ loading: {
881
+ entry: [assign({
882
+ loadingFullMatch: ({
883
+ context
884
+ }) => context.fullMatch
885
+ })],
886
+ initial: "debouncing",
887
+ states: {
888
+ debouncing: {
889
+ always: [{
890
+ guard: "no debounce",
891
+ target: "fetching"
892
+ }],
893
+ after: {
894
+ DEBOUNCE: "fetching"
895
+ }
896
+ },
897
+ fetching: {
898
+ invoke: {
899
+ src: "get matches",
900
+ input: ({
901
+ context
902
+ }) => ({
903
+ keyword: context.keyword,
904
+ getMatches: context.definition.getMatches
905
+ }),
906
+ onDone: {
907
+ target: "#typeahead picker.active.evaluating",
908
+ actions: [assign(({
909
+ context,
910
+ event
911
+ }) => event.output.keyword !== context.keyword ? {
912
+ isLoading: context.fullMatch !== context.loadingFullMatch
913
+ } : {
914
+ matches: event.output.matches,
915
+ isLoading: context.fullMatch !== context.loadingFullMatch
916
+ })]
917
+ },
918
+ onError: {
919
+ target: "#typeahead picker.active.no matches",
920
+ actions: ["handle error"]
921
+ }
922
+ }
923
+ }
924
+ }
925
+ },
926
+ "no matches": {
927
+ entry: [assign({
928
+ selectedIndex: 0
929
+ })],
930
+ always: [{
931
+ guard: "has matches",
932
+ target: "showing matches"
933
+ }],
934
+ initial: "idle",
935
+ states: {
936
+ idle: {
937
+ always: [{
938
+ guard: "is loading",
939
+ target: "loading"
940
+ }]
941
+ },
942
+ loading: {
943
+ entry: [assign({
944
+ loadingFullMatch: ({
945
+ context
946
+ }) => context.fullMatch
947
+ })],
948
+ initial: "debouncing",
949
+ states: {
950
+ debouncing: {
951
+ always: [{
952
+ guard: "no debounce",
953
+ target: "fetching"
954
+ }],
955
+ after: {
956
+ DEBOUNCE: "fetching"
957
+ }
958
+ },
959
+ fetching: {
960
+ invoke: {
961
+ src: "get matches",
962
+ input: ({
963
+ context
964
+ }) => ({
965
+ keyword: context.keyword,
966
+ getMatches: context.definition.getMatches
967
+ }),
968
+ onDone: {
969
+ target: "#typeahead picker.active.no matches.idle",
970
+ actions: ["handle async load complete"]
971
+ },
972
+ onError: {
973
+ target: "#typeahead picker.active.no matches.idle",
974
+ actions: ["handle error"]
975
+ }
976
+ }
977
+ }
978
+ }
979
+ }
980
+ }
981
+ },
982
+ "showing matches": {
983
+ entry: ["update submit listener context", "update text insertion listener context"],
984
+ invoke: {
985
+ src: "arrow listener",
986
+ input: ({
987
+ context
988
+ }) => ({
989
+ editor: context.editor
990
+ })
991
+ },
992
+ always: [{
993
+ guard: "no matches",
994
+ target: "no matches"
995
+ }],
996
+ on: {
997
+ "navigate down": {
998
+ actions: ["navigate", "update submit listener context", "update text insertion listener context"]
999
+ },
1000
+ "navigate up": {
1001
+ actions: ["navigate", "update submit listener context", "update text insertion listener context"]
1002
+ },
1003
+ "navigate to": {
1004
+ actions: ["navigate", "update submit listener context", "update text insertion listener context"]
1005
+ },
1006
+ select: {
1007
+ target: "#typeahead picker.idle",
1008
+ actions: [{
1009
+ type: "insert match",
1010
+ params: {
1011
+ exact: !1
1012
+ }
1013
+ }]
1014
+ }
1015
+ },
1016
+ initial: "idle",
1017
+ states: {
1018
+ idle: {
1019
+ always: [{
1020
+ guard: "is loading",
1021
+ target: "loading"
1022
+ }]
1023
+ },
1024
+ loading: {
1025
+ entry: [assign({
1026
+ loadingFullMatch: ({
1027
+ context
1028
+ }) => context.fullMatch
1029
+ })],
1030
+ initial: "debouncing",
1031
+ states: {
1032
+ debouncing: {
1033
+ always: [{
1034
+ guard: "no debounce",
1035
+ target: "fetching"
1036
+ }],
1037
+ after: {
1038
+ DEBOUNCE: "fetching"
1039
+ }
1040
+ },
1041
+ fetching: {
1042
+ invoke: {
1043
+ src: "get matches",
1044
+ input: ({
1045
+ context
1046
+ }) => ({
1047
+ keyword: context.keyword,
1048
+ getMatches: context.definition.getMatches
1049
+ }),
1050
+ onDone: {
1051
+ target: "#typeahead picker.active.showing matches.idle",
1052
+ actions: ["handle async load complete"]
1053
+ },
1054
+ onError: {
1055
+ target: "#typeahead picker.active.showing matches.idle",
1056
+ actions: ["handle error"]
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ }
1066
+ }
1067
+ });
1068
+ }
1069
+ function hasExactlyOneExactMatch(matches) {
1070
+ return matches.filter((match) => match?.type === "exact").length === 1;
1071
+ }
1072
+ function getExactMatch(matches) {
1073
+ const exactMatches = matches.filter((match) => match?.type === "exact");
1074
+ return exactMatches.length === 1 ? exactMatches[0] : void 0;
1075
+ }
1076
+ function useTypeaheadPicker(definition) {
1077
+ const $ = c(19), editor = useEditor();
1078
+ let t0;
1079
+ $[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t0 = createTypeaheadPickerMachine(), $[0] = t0) : t0 = $[0];
1080
+ let t1;
1081
+ $[1] !== definition || $[2] !== editor ? (t1 = {
1082
+ input: {
1083
+ editor,
1084
+ definition
1085
+ }
1086
+ }, $[1] = definition, $[2] = editor, $[3] = t1) : t1 = $[3];
1087
+ const [actorSnapshot, send] = useActor(t0, t1);
1088
+ let t2;
1089
+ $[4] !== actorSnapshot ? (t2 = (state) => actorSnapshot.matches(state), $[4] = actorSnapshot, $[5] = t2) : t2 = $[5];
1090
+ const t3 = actorSnapshot.context.matches;
1091
+ let t4;
1092
+ $[6] !== actorSnapshot.context.error || $[7] !== actorSnapshot.context.keyword || $[8] !== actorSnapshot.context.selectedIndex || $[9] !== t3 ? (t4 = {
1093
+ keyword: actorSnapshot.context.keyword,
1094
+ matches: t3,
1095
+ selectedIndex: actorSnapshot.context.selectedIndex,
1096
+ error: actorSnapshot.context.error
1097
+ }, $[6] = actorSnapshot.context.error, $[7] = actorSnapshot.context.keyword, $[8] = actorSnapshot.context.selectedIndex, $[9] = t3, $[10] = t4) : t4 = $[10];
1098
+ let t5;
1099
+ $[11] !== t2 || $[12] !== t4 ? (t5 = {
1100
+ matches: t2,
1101
+ context: t4
1102
+ }, $[11] = t2, $[12] = t4, $[13] = t5) : t5 = $[13];
1103
+ let t6;
1104
+ $[14] !== send ? (t6 = (event) => {
1105
+ send(event);
1106
+ }, $[14] = send, $[15] = t6) : t6 = $[15];
1107
+ let t7;
1108
+ return $[16] !== t5 || $[17] !== t6 ? (t7 = {
1109
+ snapshot: t5,
1110
+ send: t6
1111
+ }, $[16] = t5, $[17] = t6, $[18] = t7) : t7 = $[18], t7;
1112
+ }
1113
+ export {
1114
+ defineTypeaheadPicker,
1115
+ useTypeaheadPicker
1116
+ };
1117
+ //# sourceMappingURL=index.js.map