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.
@@ -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;