testomatio-editor-blocks 0.4.77 → 0.4.79
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/editor/MentionMenu.d.ts +17 -0
- package/package/editor/MentionMenu.js +35 -0
- package/package/editor/mentionAutocomplete.d.ts +152 -0
- package/package/editor/mentionAutocomplete.js +218 -0
- package/package/editor/useMentionAutocomplete.d.ts +38 -0
- package/package/editor/useMentionAutocomplete.js +312 -0
- package/package/index.d.ts +3 -0
- package/package/index.js +3 -0
- package/package/styles.css +75 -0
- package/package.json +1 -1
- package/src/App.tsx +77 -0
- package/src/editor/MentionMenu.tsx +46 -0
- package/src/editor/mentionAutocomplete.test.ts +267 -0
- package/src/editor/mentionAutocomplete.ts +348 -0
- package/src/editor/styles.css +75 -0
- package/src/editor/useMentionAutocomplete.tsx +463 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type MentionSource } from "./mentionAutocomplete";
|
|
2
|
+
export type MentionMenuProps = {
|
|
3
|
+
/** Sources to use. Falls back to the global registry (`setMentionSources`). */
|
|
4
|
+
sources?: MentionSource[];
|
|
5
|
+
/** Character that opens the menu. Default `@`. */
|
|
6
|
+
triggerCharacter?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Universal `@`-mention menu for the main BlockNote editor. Render it as a child
|
|
10
|
+
* of `<BlockNoteView>`. On selection it inserts the source's token (e.g.
|
|
11
|
+
* `@T123456`, `@S98765`, `@username`) as inline text at the cursor.
|
|
12
|
+
*
|
|
13
|
+
* <BlockNoteView editor={editor}>
|
|
14
|
+
* <MentionMenu />
|
|
15
|
+
* </BlockNoteView>
|
|
16
|
+
*/
|
|
17
|
+
export declare function MentionMenu({ sources: sourcesProp, triggerCharacter }: MentionMenuProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SuggestionMenuController, useBlockNoteEditor } from "@blocknote/react";
|
|
3
|
+
import { buildMentionInsertText, getMentionSources, resolveMentionQuery, } from "./mentionAutocomplete";
|
|
4
|
+
/**
|
|
5
|
+
* Universal `@`-mention menu for the main BlockNote editor. Render it as a child
|
|
6
|
+
* of `<BlockNoteView>`. On selection it inserts the source's token (e.g.
|
|
7
|
+
* `@T123456`, `@S98765`, `@username`) as inline text at the cursor.
|
|
8
|
+
*
|
|
9
|
+
* <BlockNoteView editor={editor}>
|
|
10
|
+
* <MentionMenu />
|
|
11
|
+
* </BlockNoteView>
|
|
12
|
+
*/
|
|
13
|
+
export function MentionMenu({ sources: sourcesProp, triggerCharacter = "@" }) {
|
|
14
|
+
const editor = useBlockNoteEditor();
|
|
15
|
+
const getItems = async (query) => {
|
|
16
|
+
const sources = sourcesProp !== null && sourcesProp !== void 0 ? sourcesProp : getMentionSources();
|
|
17
|
+
const resolved = await resolveMentionQuery(query, sources);
|
|
18
|
+
if (!resolved)
|
|
19
|
+
return [];
|
|
20
|
+
const { source, items } = resolved;
|
|
21
|
+
return items.map((item) => {
|
|
22
|
+
var _a;
|
|
23
|
+
return ({
|
|
24
|
+
title: item.label,
|
|
25
|
+
subtext: (_a = item.detail) !== null && _a !== void 0 ? _a : undefined,
|
|
26
|
+
onItemClick: () => {
|
|
27
|
+
// The controller has already removed the trigger + query text; we insert
|
|
28
|
+
// the token plus a trailing space so the caret is ready for more typing.
|
|
29
|
+
editor.insertInlineContent([buildMentionInsertText(source, item), " "]);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
return _jsx(SuggestionMenuController, { triggerCharacter: triggerCharacter, getItems: getItems });
|
|
35
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal, prefix-based `@`-mention autocomplete.
|
|
3
|
+
*
|
|
4
|
+
* A single mechanism that turns typed tokens like `@`, `@T`, `@S` into inserted
|
|
5
|
+
* references such as `@T123456`, `@S98765`, or `@username`. Which list is shown
|
|
6
|
+
* (and what gets inserted) is decided by a configurable set of {@link MentionSource}s,
|
|
7
|
+
* each keyed by the characters that follow the `@`:
|
|
8
|
+
*
|
|
9
|
+
* { prefix: "T", search: (q) => api.findTests(q) } // @T -> fetch tests -> "@T123456"
|
|
10
|
+
* { prefix: "S", search: (q) => api.findSuites(q) } // @S -> fetch suites -> "@S98765"
|
|
11
|
+
* { prefix: "", items: users, insert: (u) => `@${u.label}` } // @ -> filter users -> "@john"
|
|
12
|
+
*
|
|
13
|
+
* This file is intentionally DOM-free: it holds only the pure logic (matching,
|
|
14
|
+
* source resolution, filtering, token building) plus the global source registry.
|
|
15
|
+
* The React wiring lives in `MentionAutocomplete.tsx`.
|
|
16
|
+
*/
|
|
17
|
+
export type MentionItem = {
|
|
18
|
+
/** Stable identifier. For test/suite sources this is what gets inserted (`@T{id}`). */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Primary text shown in the popup and used for client-side filtering. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** Optional secondary text (e.g. `#123`, an email, a description). */
|
|
23
|
+
detail?: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Optional explicit text to insert for this specific item. When omitted the
|
|
26
|
+
* source's `insert` builder (or the default `@{prefix}{id}`) is used.
|
|
27
|
+
*/
|
|
28
|
+
insertText?: string;
|
|
29
|
+
/** Sources may carry arbitrary extra data through to their renderers. */
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
/** Static item list, or a (possibly async) provider of one. */
|
|
33
|
+
export type MentionItemsInput = MentionItem[] | (() => MentionItem[] | Promise<MentionItem[]>);
|
|
34
|
+
/** Result of an async `search`. May be raw items or a JSON:API document. */
|
|
35
|
+
export type MentionSearchResult = MentionItem[] | MentionJsonApiDocument | MentionJsonApiResource[] | null | undefined;
|
|
36
|
+
export type MentionSource = {
|
|
37
|
+
/**
|
|
38
|
+
* Characters that must appear immediately after `@` to select this source.
|
|
39
|
+
* Use `""` for the default/fallback source (e.g. usernames on a bare `@`).
|
|
40
|
+
* Matching is case-sensitive and longest-prefix wins, so `@T` routes to the
|
|
41
|
+
* `"T"` source rather than the `""` source.
|
|
42
|
+
*/
|
|
43
|
+
prefix: string;
|
|
44
|
+
/** Human label for the popup header / grouping (e.g. "Tests", "Users"). */
|
|
45
|
+
label?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Static list (or provider). Filtered client-side by the typed query. Use
|
|
48
|
+
* this for data already in memory, like the list of project members.
|
|
49
|
+
*/
|
|
50
|
+
items?: MentionItemsInput;
|
|
51
|
+
/**
|
|
52
|
+
* Async fetcher invoked with the typed query (the text after the prefix).
|
|
53
|
+
* Use this for server-backed lookups like tests (`@T`) and suites (`@S`).
|
|
54
|
+
*/
|
|
55
|
+
search?: (query: string) => Promise<MentionSearchResult> | MentionSearchResult;
|
|
56
|
+
/**
|
|
57
|
+
* Builds the text inserted when an item is chosen. Defaults to
|
|
58
|
+
* `@{prefix}{id}` (so a test with id `123456` becomes `@T123456`).
|
|
59
|
+
*/
|
|
60
|
+
insert?: (item: MentionItem, source: MentionSource) => string;
|
|
61
|
+
/** Minimum query length before `search` runs. Default 0 (fires on empty query). */
|
|
62
|
+
minChars?: number;
|
|
63
|
+
/** Max items shown. Default 8. */
|
|
64
|
+
limit?: number;
|
|
65
|
+
};
|
|
66
|
+
/** A currently-active mention detected at the caret. */
|
|
67
|
+
export type ActiveMention = {
|
|
68
|
+
source: MentionSource;
|
|
69
|
+
/** Matched prefix (e.g. "T", "S", or ""). */
|
|
70
|
+
prefix: string;
|
|
71
|
+
/** Text typed after the prefix (what `search`/filter receives). */
|
|
72
|
+
query: string;
|
|
73
|
+
/** Index of the `@` in the source text. */
|
|
74
|
+
start: number;
|
|
75
|
+
/** Caret index (exclusive end of the token being edited). */
|
|
76
|
+
end: number;
|
|
77
|
+
/** Full token text between `@` (exclusive) and the caret, i.e. `prefix + query`. */
|
|
78
|
+
token: string;
|
|
79
|
+
};
|
|
80
|
+
export type MentionJsonApiAttributes = {
|
|
81
|
+
title?: string | null;
|
|
82
|
+
name?: string | null;
|
|
83
|
+
label?: string | null;
|
|
84
|
+
username?: string | null;
|
|
85
|
+
email?: string | null;
|
|
86
|
+
description?: string | null;
|
|
87
|
+
detail?: string | null;
|
|
88
|
+
[key: string]: unknown;
|
|
89
|
+
};
|
|
90
|
+
export type MentionJsonApiResource = {
|
|
91
|
+
id?: string | number | null;
|
|
92
|
+
type?: string | null;
|
|
93
|
+
attributes?: MentionJsonApiAttributes | null;
|
|
94
|
+
};
|
|
95
|
+
export type MentionJsonApiDocument = {
|
|
96
|
+
data?: MentionJsonApiResource[] | null;
|
|
97
|
+
};
|
|
98
|
+
/** Register the mention sources used when a consumer doesn't pass `sources` explicitly. */
|
|
99
|
+
export declare function setMentionSources(sources: MentionSource[] | null): void;
|
|
100
|
+
/** Read the globally-registered mention sources. */
|
|
101
|
+
export declare function getMentionSources(): MentionSource[];
|
|
102
|
+
/**
|
|
103
|
+
* Pick the source whose `prefix` matches the start of `token`. Longest prefix
|
|
104
|
+
* wins; the empty-prefix source is the ultimate fallback. Returns `null` when
|
|
105
|
+
* nothing matches (e.g. `@T` typed but only a `""` source is registered and it
|
|
106
|
+
* is absent).
|
|
107
|
+
*/
|
|
108
|
+
export declare function resolveMentionSource(token: string, sources: MentionSource[]): MentionSource | null;
|
|
109
|
+
/**
|
|
110
|
+
* Detect the mention being edited at `caret` within `text`.
|
|
111
|
+
*
|
|
112
|
+
* A mention is the run of non-whitespace characters after an `@` that sits at
|
|
113
|
+
* the start of the text or immediately after whitespace (so emails like
|
|
114
|
+
* `a@b.com` never trigger). Returns `null` when the caret is not inside such a
|
|
115
|
+
* token or no source matches.
|
|
116
|
+
*/
|
|
117
|
+
export declare function parseActiveMention(text: string, caret: number, sources: MentionSource[]): ActiveMention | null;
|
|
118
|
+
/** The text a chosen item inserts, honoring item/source overrides. */
|
|
119
|
+
export declare function buildMentionInsertText(source: MentionSource, item: MentionItem): string;
|
|
120
|
+
export type MentionApplication = {
|
|
121
|
+
/** The full text after replacing the active token with the inserted mention. */
|
|
122
|
+
text: string;
|
|
123
|
+
/** Caret position after insertion (after the token and its trailing space). */
|
|
124
|
+
caret: number;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Replace the active mention token in `text` with `insertText`, adding a single
|
|
128
|
+
* trailing space so the caret lands ready for the next word and the freshly
|
|
129
|
+
* inserted `@…` token does not immediately re-trigger the popup.
|
|
130
|
+
*/
|
|
131
|
+
export declare function applyMention(text: string, active: Pick<ActiveMention, "start" | "end">, insertText: string): MentionApplication;
|
|
132
|
+
/**
|
|
133
|
+
* Rank static items against a query: label starts-with beats label contains,
|
|
134
|
+
* which beats id matches. Case-insensitive. An empty query returns the head of
|
|
135
|
+
* the list unchanged.
|
|
136
|
+
*/
|
|
137
|
+
export declare function filterMentionItems(items: MentionItem[], query: string, limit?: number): MentionItem[];
|
|
138
|
+
/**
|
|
139
|
+
* Route a typed query (everything after the trigger char, e.g. `T123` or `bob`)
|
|
140
|
+
* to its source and resolve the items to show. Handles prefix stripping,
|
|
141
|
+
* `minChars`, async `search`, static `items` (incl. provider functions),
|
|
142
|
+
* normalization, and the `limit`. Returns `null` when no source matches.
|
|
143
|
+
*
|
|
144
|
+
* Shared by both mention frontends (the textarea hook and the BlockNote menu).
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveMentionQuery(query: string, sources: MentionSource[]): Promise<{
|
|
147
|
+
source: MentionSource;
|
|
148
|
+
items: MentionItem[];
|
|
149
|
+
} | null>;
|
|
150
|
+
export declare function parseMentionsFromJsonApi(document: MentionJsonApiDocument | MentionJsonApiResource[] | null | undefined): MentionItem[];
|
|
151
|
+
/** Coerce any supported `search`/`items` result into a clean `MentionItem[]`. */
|
|
152
|
+
export declare function normalizeMentionItems(result: MentionSearchResult): MentionItem[];
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal, prefix-based `@`-mention autocomplete.
|
|
3
|
+
*
|
|
4
|
+
* A single mechanism that turns typed tokens like `@`, `@T`, `@S` into inserted
|
|
5
|
+
* references such as `@T123456`, `@S98765`, or `@username`. Which list is shown
|
|
6
|
+
* (and what gets inserted) is decided by a configurable set of {@link MentionSource}s,
|
|
7
|
+
* each keyed by the characters that follow the `@`:
|
|
8
|
+
*
|
|
9
|
+
* { prefix: "T", search: (q) => api.findTests(q) } // @T -> fetch tests -> "@T123456"
|
|
10
|
+
* { prefix: "S", search: (q) => api.findSuites(q) } // @S -> fetch suites -> "@S98765"
|
|
11
|
+
* { prefix: "", items: users, insert: (u) => `@${u.label}` } // @ -> filter users -> "@john"
|
|
12
|
+
*
|
|
13
|
+
* This file is intentionally DOM-free: it holds only the pure logic (matching,
|
|
14
|
+
* source resolution, filtering, token building) plus the global source registry.
|
|
15
|
+
* The React wiring lives in `MentionAutocomplete.tsx`.
|
|
16
|
+
*/
|
|
17
|
+
/* ------------------------------------------------------------------ *
|
|
18
|
+
* Global source registry (dependency-injection seam, mirrors the rest
|
|
19
|
+
* of the library's `setXFetcher` pattern).
|
|
20
|
+
* ------------------------------------------------------------------ */
|
|
21
|
+
let globalSources = [];
|
|
22
|
+
/** Register the mention sources used when a consumer doesn't pass `sources` explicitly. */
|
|
23
|
+
export function setMentionSources(sources) {
|
|
24
|
+
globalSources = Array.isArray(sources) ? sources : [];
|
|
25
|
+
}
|
|
26
|
+
/** Read the globally-registered mention sources. */
|
|
27
|
+
export function getMentionSources() {
|
|
28
|
+
return globalSources;
|
|
29
|
+
}
|
|
30
|
+
/* ------------------------------------------------------------------ *
|
|
31
|
+
* Pure logic
|
|
32
|
+
* ------------------------------------------------------------------ */
|
|
33
|
+
const WHITESPACE = /\s/;
|
|
34
|
+
/**
|
|
35
|
+
* Pick the source whose `prefix` matches the start of `token`. Longest prefix
|
|
36
|
+
* wins; the empty-prefix source is the ultimate fallback. Returns `null` when
|
|
37
|
+
* nothing matches (e.g. `@T` typed but only a `""` source is registered and it
|
|
38
|
+
* is absent).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveMentionSource(token, sources) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
let best = null;
|
|
43
|
+
for (const source of sources) {
|
|
44
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
45
|
+
if (!token.startsWith(prefix))
|
|
46
|
+
continue;
|
|
47
|
+
if (best === null || prefix.length > ((_b = best.prefix) !== null && _b !== void 0 ? _b : "").length) {
|
|
48
|
+
best = source;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return best;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detect the mention being edited at `caret` within `text`.
|
|
55
|
+
*
|
|
56
|
+
* A mention is the run of non-whitespace characters after an `@` that sits at
|
|
57
|
+
* the start of the text or immediately after whitespace (so emails like
|
|
58
|
+
* `a@b.com` never trigger). Returns `null` when the caret is not inside such a
|
|
59
|
+
* token or no source matches.
|
|
60
|
+
*/
|
|
61
|
+
export function parseActiveMention(text, caret, sources) {
|
|
62
|
+
var _a;
|
|
63
|
+
if (!sources.length)
|
|
64
|
+
return null;
|
|
65
|
+
const pos = Math.max(0, Math.min(caret, text.length));
|
|
66
|
+
const before = text.slice(0, pos);
|
|
67
|
+
const at = before.lastIndexOf("@");
|
|
68
|
+
if (at === -1)
|
|
69
|
+
return null;
|
|
70
|
+
// The `@` must start a word: preceded by nothing or whitespace.
|
|
71
|
+
const charBefore = at === 0 ? "" : text[at - 1];
|
|
72
|
+
if (charBefore !== "" && !WHITESPACE.test(charBefore))
|
|
73
|
+
return null;
|
|
74
|
+
// The token is everything from just after `@` up to the caret. Whitespace
|
|
75
|
+
// ends a mention, so a caret past a space means we're no longer inside one.
|
|
76
|
+
const token = before.slice(at + 1);
|
|
77
|
+
if (WHITESPACE.test(token))
|
|
78
|
+
return null;
|
|
79
|
+
const source = resolveMentionSource(token, sources);
|
|
80
|
+
if (!source)
|
|
81
|
+
return null;
|
|
82
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
83
|
+
return {
|
|
84
|
+
source,
|
|
85
|
+
prefix,
|
|
86
|
+
query: token.slice(prefix.length),
|
|
87
|
+
start: at,
|
|
88
|
+
end: pos,
|
|
89
|
+
token,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The text a chosen item inserts, honoring item/source overrides. */
|
|
93
|
+
export function buildMentionInsertText(source, item) {
|
|
94
|
+
var _a;
|
|
95
|
+
if (typeof item.insertText === "string")
|
|
96
|
+
return item.insertText;
|
|
97
|
+
if (typeof source.insert === "function")
|
|
98
|
+
return source.insert(item, source);
|
|
99
|
+
return `@${(_a = source.prefix) !== null && _a !== void 0 ? _a : ""}${item.id}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Replace the active mention token in `text` with `insertText`, adding a single
|
|
103
|
+
* trailing space so the caret lands ready for the next word and the freshly
|
|
104
|
+
* inserted `@…` token does not immediately re-trigger the popup.
|
|
105
|
+
*/
|
|
106
|
+
export function applyMention(text, active, insertText) {
|
|
107
|
+
const before = text.slice(0, active.start);
|
|
108
|
+
const after = text.slice(active.end);
|
|
109
|
+
const hasLeadingSpace = after.length > 0 && WHITESPACE.test(after[0]);
|
|
110
|
+
const separator = hasLeadingSpace ? "" : " ";
|
|
111
|
+
return {
|
|
112
|
+
text: `${before}${insertText}${separator}${after}`,
|
|
113
|
+
// Land just past the single following space (added or pre-existing) so the
|
|
114
|
+
// caret is ready for the next word.
|
|
115
|
+
caret: before.length + insertText.length + 1,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Rank static items against a query: label starts-with beats label contains,
|
|
120
|
+
* which beats id matches. Case-insensitive. An empty query returns the head of
|
|
121
|
+
* the list unchanged.
|
|
122
|
+
*/
|
|
123
|
+
export function filterMentionItems(items, query, limit = 8) {
|
|
124
|
+
const q = query.trim().toLowerCase();
|
|
125
|
+
if (!q)
|
|
126
|
+
return items.slice(0, limit);
|
|
127
|
+
const scored = [];
|
|
128
|
+
items.forEach((item, order) => {
|
|
129
|
+
var _a, _b, _c;
|
|
130
|
+
const label = ((_a = item.label) !== null && _a !== void 0 ? _a : "").toLowerCase();
|
|
131
|
+
const id = String((_b = item.id) !== null && _b !== void 0 ? _b : "").toLowerCase();
|
|
132
|
+
const detail = ((_c = item.detail) !== null && _c !== void 0 ? _c : "").toString().toLowerCase();
|
|
133
|
+
let score = -1;
|
|
134
|
+
if (label.startsWith(q))
|
|
135
|
+
score = 0;
|
|
136
|
+
else if (label.includes(q))
|
|
137
|
+
score = 1;
|
|
138
|
+
else if (id.startsWith(q) || id.includes(q))
|
|
139
|
+
score = 2;
|
|
140
|
+
else if (detail.includes(q))
|
|
141
|
+
score = 3;
|
|
142
|
+
if (score >= 0)
|
|
143
|
+
scored.push({ item, score, order });
|
|
144
|
+
});
|
|
145
|
+
scored.sort((a, b) => (a.score - b.score) || (a.order - b.order));
|
|
146
|
+
return scored.slice(0, limit).map((s) => s.item);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Route a typed query (everything after the trigger char, e.g. `T123` or `bob`)
|
|
150
|
+
* to its source and resolve the items to show. Handles prefix stripping,
|
|
151
|
+
* `minChars`, async `search`, static `items` (incl. provider functions),
|
|
152
|
+
* normalization, and the `limit`. Returns `null` when no source matches.
|
|
153
|
+
*
|
|
154
|
+
* Shared by both mention frontends (the textarea hook and the BlockNote menu).
|
|
155
|
+
*/
|
|
156
|
+
export async function resolveMentionQuery(query, sources) {
|
|
157
|
+
var _a, _b, _c;
|
|
158
|
+
const source = resolveMentionSource(query, sources);
|
|
159
|
+
if (!source)
|
|
160
|
+
return null;
|
|
161
|
+
const prefix = (_a = source.prefix) !== null && _a !== void 0 ? _a : "";
|
|
162
|
+
const sub = query.slice(prefix.length);
|
|
163
|
+
const limit = (_b = source.limit) !== null && _b !== void 0 ? _b : 8;
|
|
164
|
+
if (typeof source.search === "function") {
|
|
165
|
+
const minChars = (_c = source.minChars) !== null && _c !== void 0 ? _c : 0;
|
|
166
|
+
if (sub.length < minChars)
|
|
167
|
+
return { source, items: [] };
|
|
168
|
+
const items = normalizeMentionItems(await source.search(sub)).slice(0, limit);
|
|
169
|
+
return { source, items };
|
|
170
|
+
}
|
|
171
|
+
const input = source.items;
|
|
172
|
+
const raw = typeof input === "function" ? await input() : input !== null && input !== void 0 ? input : [];
|
|
173
|
+
const items = filterMentionItems(normalizeMentionItems(raw), sub, limit);
|
|
174
|
+
return { source, items };
|
|
175
|
+
}
|
|
176
|
+
/* ------------------------------------------------------------------ *
|
|
177
|
+
* Normalization of async / JSON:API results into MentionItem[]
|
|
178
|
+
* ------------------------------------------------------------------ */
|
|
179
|
+
export function parseMentionsFromJsonApi(document) {
|
|
180
|
+
const resources = Array.isArray(document) ? document : document === null || document === void 0 ? void 0 : document.data;
|
|
181
|
+
if (!Array.isArray(resources) || resources.length === 0)
|
|
182
|
+
return [];
|
|
183
|
+
return resources
|
|
184
|
+
.map((resource) => normalizeJsonApiResource(resource))
|
|
185
|
+
.filter((value) => Boolean(value));
|
|
186
|
+
}
|
|
187
|
+
/** Coerce any supported `search`/`items` result into a clean `MentionItem[]`. */
|
|
188
|
+
export function normalizeMentionItems(result) {
|
|
189
|
+
if (!result)
|
|
190
|
+
return [];
|
|
191
|
+
if (Array.isArray(result)) {
|
|
192
|
+
if (result.length === 0)
|
|
193
|
+
return [];
|
|
194
|
+
if (isMentionItemArray(result))
|
|
195
|
+
return result;
|
|
196
|
+
return parseMentionsFromJsonApi(result);
|
|
197
|
+
}
|
|
198
|
+
return parseMentionsFromJsonApi(result);
|
|
199
|
+
}
|
|
200
|
+
function normalizeJsonApiResource(resource) {
|
|
201
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
202
|
+
if (!resource)
|
|
203
|
+
return null;
|
|
204
|
+
const attrs = (_a = resource.attributes) !== null && _a !== void 0 ? _a : {};
|
|
205
|
+
const id = resource.id;
|
|
206
|
+
if (id === null || id === undefined || id === "")
|
|
207
|
+
return null;
|
|
208
|
+
const label = (_e = (_d = (_c = (_b = attrs.title) !== null && _b !== void 0 ? _b : attrs.name) !== null && _c !== void 0 ? _c : attrs.label) !== null && _d !== void 0 ? _d : attrs.username) !== null && _e !== void 0 ? _e : String(id);
|
|
209
|
+
return {
|
|
210
|
+
id: String(id),
|
|
211
|
+
label: String(label),
|
|
212
|
+
detail: (_h = (_g = (_f = attrs.detail) !== null && _f !== void 0 ? _f : attrs.description) !== null && _g !== void 0 ? _g : attrs.email) !== null && _h !== void 0 ? _h : null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function isMentionItemArray(value) {
|
|
216
|
+
const first = value[0];
|
|
217
|
+
return Boolean(first) && typeof (first === null || first === void 0 ? void 0 : first.label) === "string";
|
|
218
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type MentionSource } from "./mentionAutocomplete";
|
|
3
|
+
export type UseMentionAutocompleteOptions = {
|
|
4
|
+
/** The textarea the mentions are typed into (e.g. an OverType instance's textarea). */
|
|
5
|
+
textarea: HTMLTextAreaElement | null;
|
|
6
|
+
/** Reads the current full text. Defaults to `textarea.value`. */
|
|
7
|
+
getText?: () => string;
|
|
8
|
+
/**
|
|
9
|
+
* Writes the full text back and positions the caret. Defaults to mutating
|
|
10
|
+
* `textarea.value` directly; pass a custom writer when a wrapper (OverType,
|
|
11
|
+
* a controlled component) owns the value.
|
|
12
|
+
*/
|
|
13
|
+
setText?: (text: string, caret: number) => void;
|
|
14
|
+
/** Sources to use. Falls back to the global registry (`setMentionSources`). */
|
|
15
|
+
sources?: MentionSource[];
|
|
16
|
+
/** Master switch; when false the popup never opens. Default true. */
|
|
17
|
+
enabled?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* When true (default) the hook attaches its own capture-phase keydown listener
|
|
20
|
+
* to the textarea. Set false to drive navigation yourself via the returned
|
|
21
|
+
* `onKeyDown` (useful when another handler must take priority ordering).
|
|
22
|
+
*/
|
|
23
|
+
attachKeyDown?: boolean;
|
|
24
|
+
/** Debounce (ms) before an async `search` runs. Default 150. */
|
|
25
|
+
searchDebounceMs?: number;
|
|
26
|
+
};
|
|
27
|
+
export type UseMentionAutocompleteResult = {
|
|
28
|
+
/** Render this somewhere in the tree; it portals the popup to `document.body`. */
|
|
29
|
+
overlay: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Feed keydown events here when `attachKeyDown` is false. Returns true when the
|
|
32
|
+
* event was consumed (it also calls preventDefault / stopImmediatePropagation).
|
|
33
|
+
*/
|
|
34
|
+
onKeyDown: (event: KeyboardEvent) => boolean;
|
|
35
|
+
isOpen: boolean;
|
|
36
|
+
close: () => void;
|
|
37
|
+
};
|
|
38
|
+
export declare function useMentionAutocomplete(options: UseMentionAutocompleteOptions): UseMentionAutocompleteResult;
|