@pythia-software/query-table-ui 0.5.0 → 0.6.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.
@@ -0,0 +1,739 @@
1
+ // src/FormulaEditor.tsx
2
+ import {
3
+ useEffect as useEffect2,
4
+ useId as useId2,
5
+ useRef as useRef2,
6
+ useState as useState2
7
+ } from "react";
8
+ import { FormulaError } from "@pythia-software/query-table-core";
9
+
10
+ // src/formulaEditorHelpers.ts
11
+ import {
12
+ FORMULA_FUNCTIONS
13
+ } from "@pythia-software/query-table-core";
14
+ var FORMULA_EDITOR_HELP = "Use [field] references. Ctrl+Space opens suggestions.";
15
+ function formulaContext(text, end = text.length) {
16
+ let quoted = false;
17
+ let fieldStart = -1;
18
+ let escaped = false;
19
+ for (let i = 0; i < end; i++) {
20
+ const c = text[i];
21
+ if (quoted) {
22
+ if (c === "\\") {
23
+ escaped = i + 1 >= end;
24
+ i++;
25
+ } else if (c === '"') quoted = false;
26
+ } else if (fieldStart >= 0) {
27
+ if (c === "]") {
28
+ if (text[i + 1] === "]") i++;
29
+ else fieldStart = -1;
30
+ }
31
+ } else if (c === '"') quoted = true;
32
+ else if (c === "[") fieldStart = i;
33
+ }
34
+ return { quoted, fieldStart, escaped };
35
+ }
36
+ function formulaCompletions(text, start, end, fields, explicit = false) {
37
+ const before = text.slice(0, start);
38
+ const { quoted, fieldStart } = formulaContext(text, start);
39
+ if (quoted || !explicit && start !== end) return null;
40
+ const word = before.match(/\b[A-Za-z_][A-Za-z_0-9]*$/)?.[0] ?? "";
41
+ if (!explicit && fieldStart < 0 && !word) return null;
42
+ const from = start !== end ? start : fieldStart >= 0 ? fieldStart : start - word.length;
43
+ let to = end;
44
+ if (start === end && fieldStart >= 0) {
45
+ for (let i = fieldStart + 1; i < text.length; i++) {
46
+ if (text[i] === "]") {
47
+ if (text[i + 1] === "]") {
48
+ i++;
49
+ continue;
50
+ }
51
+ to = i + 1;
52
+ break;
53
+ }
54
+ if (text[i] === "\n" || text[i] === "[") break;
55
+ }
56
+ } else if (start === end) {
57
+ to += text.slice(end).match(/^[A-Za-z_0-9]*/)?.[0].length ?? 0;
58
+ }
59
+ const prefix = start !== end ? "" : before.slice(from).toLowerCase();
60
+ const options = fields.map((f) => ({
61
+ label: `[${f.name.replace(/\]/g, "]]")}]`,
62
+ detail: `${f.label} \xB7 ${f.type}`,
63
+ description: f.name,
64
+ insert: `[${f.name.replace(/\]/g, "]]")}]`
65
+ }));
66
+ if (fieldStart < 0) {
67
+ options.push(
68
+ ...FORMULA_FUNCTIONS.map((f) => ({
69
+ label: f.name,
70
+ detail: f.signature,
71
+ description: f.description,
72
+ insert: text[to] === "(" ? f.name : `${f.name}()`,
73
+ caretOffset: f.name.length + 1
74
+ })),
75
+ ...["TRUE", "FALSE", "NULL", "AND", "OR", "NOT", "IN", "BETWEEN"].map(
76
+ (label) => ({
77
+ label,
78
+ detail: "Keyword",
79
+ description: "",
80
+ insert: label
81
+ })
82
+ )
83
+ );
84
+ }
85
+ return {
86
+ from,
87
+ to,
88
+ options: options.filter(
89
+ (option) => option.label.toLowerCase().startsWith(prefix) || fieldStart >= 0 && option.detail.toLowerCase().includes(prefix.slice(1))
90
+ )
91
+ };
92
+ }
93
+ function signatureAt(text) {
94
+ const stack = [];
95
+ let quoted = false, field = false;
96
+ for (let i = 0; i < text.length; i++) {
97
+ const c = text[i];
98
+ if (quoted) {
99
+ if (c === "\\") i++;
100
+ else if (c === '"') quoted = false;
101
+ continue;
102
+ }
103
+ if (field) {
104
+ if (c === "]") {
105
+ if (text[i + 1] === "]") i++;
106
+ else field = false;
107
+ }
108
+ continue;
109
+ }
110
+ if (c === '"') {
111
+ quoted = true;
112
+ continue;
113
+ }
114
+ if (c === "[") {
115
+ field = true;
116
+ continue;
117
+ }
118
+ if (c === "(")
119
+ stack.push({
120
+ name: text.slice(0, i).match(/([A-Za-z_]+)\s*$/)?.[1]?.toUpperCase() ?? "",
121
+ argument: 1
122
+ });
123
+ else if (c === ")") stack.pop();
124
+ else if (c === "," && stack.length) stack[stack.length - 1].argument++;
125
+ }
126
+ const call = stack[stack.length - 1], spec = FORMULA_FUNCTIONS.find((f) => f.name === call?.name);
127
+ return spec && call ? `${spec.signature} \xB7 Argument ${call.argument} \u2014 ${spec.description}` : FORMULA_EDITOR_HELP;
128
+ }
129
+
130
+ // src/FormulaFunctionBrowser.tsx
131
+ import { useEffect, useId, useRef, useState } from "react";
132
+
133
+ // src/formulaFunctionCatalogue.ts
134
+ import {
135
+ FORMULA_FUNCTIONS as FORMULA_FUNCTIONS2
136
+ } from "@pythia-software/query-table-core";
137
+ var FUNCTION_CATEGORIES = [
138
+ {
139
+ id: "strings",
140
+ label: "String functions",
141
+ description: "Slice, combine, format, and clean text.",
142
+ keywords: "string text characters whitespace case concatenate"
143
+ },
144
+ {
145
+ id: "matching",
146
+ label: "Matching & regex",
147
+ description: "Find text, test patterns, and extract matches.",
148
+ keywords: "search matching regex regular expression pattern capture"
149
+ },
150
+ {
151
+ id: "numbers",
152
+ label: "Numbers & statistics",
153
+ description: "Round, transform, and compare numbers within a row.",
154
+ keywords: "math numeric arithmetic statistics statistical rounding"
155
+ },
156
+ {
157
+ id: "logic",
158
+ label: "Conditions & missing values",
159
+ description: "Choose results, handle errors, and work with empty values.",
160
+ keywords: "logic boolean conditional null missing empty fallback error"
161
+ },
162
+ {
163
+ id: "conversion",
164
+ label: "Type conversion",
165
+ description: "Convert values to text, numbers, booleans, or dates.",
166
+ keywords: "convert conversion cast type parse"
167
+ },
168
+ {
169
+ id: "arrays",
170
+ label: "Array functions",
171
+ description: "Split, join, count, and organize lists of text.",
172
+ keywords: "array list collection elements unique deduplicate"
173
+ },
174
+ {
175
+ id: "dates",
176
+ label: "Dates & times",
177
+ description: "Extract date parts, format timestamps, and calculate durations.",
178
+ keywords: "date datetime time timestamp calendar duration UTC"
179
+ }
180
+ ];
181
+ var FUNCTION_RESULT_TYPES = [
182
+ { value: "text", label: "Text" },
183
+ { value: "number", label: "Number" },
184
+ { value: "bool", label: "Boolean" },
185
+ { value: "datetime", label: "Date/time" },
186
+ { value: "textarray", label: "Text array" },
187
+ { value: "branch", label: "Depends on arguments" }
188
+ ];
189
+ function functionResultLabel(result) {
190
+ return FUNCTION_RESULT_TYPES.find((type) => type.value === result)?.label ?? result;
191
+ }
192
+ function functionCategory(fn) {
193
+ if (fn.name.startsWith("TO_")) return "conversion";
194
+ if (fn.name.startsWith("REGEX_") || ["CONTAINS", "STARTS_WITH", "ENDS_WITH"].includes(fn.name))
195
+ return "matching";
196
+ if (fn.name.startsWith("ARRAY_") || ["SPLIT", "JOIN"].includes(fn.name))
197
+ return "arrays";
198
+ if (fn.name.includes("DATE") || fn.args !== "any" && fn.args.includes("datetime"))
199
+ return "dates";
200
+ if (fn.result === "branch" || fn.args === "any") return "logic";
201
+ if (fn.args.includes("text")) return "strings";
202
+ return "numbers";
203
+ }
204
+ var SEARCH_ALIASES = {
205
+ LEFT: "prefix beginning first characters",
206
+ RIGHT: "suffix ending last characters",
207
+ SUBSTRING: "slice middle characters",
208
+ LENGTH: "size count characters",
209
+ LOWER: "lowercase",
210
+ UPPER: "uppercase",
211
+ CONCAT: "concatenate combine append",
212
+ CONCAT_WS: "concatenate separator combine append",
213
+ LEAST: "minimum smallest min",
214
+ GREATEST: "maximum largest max",
215
+ ABS: "absolute magnitude",
216
+ SQRT: "square root",
217
+ CEIL: "ceiling round up",
218
+ FLOOR: "round down",
219
+ IF: "if else decision",
220
+ COALESCE: "default first non-null missing fallback",
221
+ NULLIF: "equal missing null",
222
+ IFERROR: "recover error default fallback",
223
+ ARRAY_LENGTH: "size count elements",
224
+ DATE_DIFF: "difference elapsed duration"
225
+ };
226
+ var normalize = (text) => text.toLowerCase().replace(/[_-]/g, " ").trim();
227
+ function findFormulaFunctions(search, category = "all", result = "all") {
228
+ const query = normalize(search);
229
+ const terms = query.split(/\s+/).filter(Boolean);
230
+ const functions = FORMULA_FUNCTIONS2.filter((fn) => {
231
+ const concept = functionCategory(fn);
232
+ if (category !== "all" && concept !== category || result !== "all" && fn.result !== result)
233
+ return false;
234
+ const metadata = FUNCTION_CATEGORIES.find((item) => item.id === concept);
235
+ const haystack = normalize(
236
+ `${fn.name} ${fn.signature} ${fn.description} ${metadata.label} ${metadata.keywords} ${SEARCH_ALIASES[fn.name] ?? ""} ${functionResultLabel(fn.result)}`
237
+ );
238
+ return terms.every((term) => haystack.includes(term));
239
+ });
240
+ const rank = (fn) => !query ? 2 : normalize(fn.name) === query ? 0 : normalize(fn.name).startsWith(query) ? 1 : 2;
241
+ return functions.sort(
242
+ (a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name)
243
+ );
244
+ }
245
+
246
+ // src/FormulaFunctionBrowser.tsx
247
+ import { jsx, jsxs } from "react/jsx-runtime";
248
+ function FormulaFunctionBrowser({
249
+ onInsert
250
+ }) {
251
+ const id = useId();
252
+ const list = useRef(null);
253
+ const [search, setSearch] = useState("");
254
+ const [category, setCategory] = useState("all");
255
+ const [result, setResult] = useState(
256
+ "all"
257
+ );
258
+ const [active, setActive] = useState(0);
259
+ const matches = findFormulaFunctions(search, category, result);
260
+ const concept = FUNCTION_CATEGORIES.find((item) => item.id === category);
261
+ useEffect(() => {
262
+ const option = list.current?.children[active];
263
+ const viewport = list.current;
264
+ if (!option || !viewport) return;
265
+ const top = option.offsetTop;
266
+ if (top < viewport.scrollTop) viewport.scrollTop = top;
267
+ else if (top + option.offsetHeight > viewport.scrollTop + viewport.clientHeight)
268
+ viewport.scrollTop = top + option.offsetHeight - viewport.clientHeight;
269
+ }, [active, search, category, result]);
270
+ return /* @__PURE__ */ jsxs("aside", { className: "qt-function-library", "aria-labelledby": `${id}-title`, children: [
271
+ /* @__PURE__ */ jsx("h4", { id: `${id}-title`, children: "Function library" }),
272
+ /* @__PURE__ */ jsx("p", { className: "qt-muted", children: "Find a function by name or what it does." }),
273
+ /* @__PURE__ */ jsxs("label", { className: "qt-function-search", children: [
274
+ "Search functions",
275
+ /* @__PURE__ */ jsx(
276
+ "input",
277
+ {
278
+ className: "qt-input",
279
+ type: "search",
280
+ role: "combobox",
281
+ "aria-autocomplete": "list",
282
+ "aria-expanded": matches.length > 0,
283
+ "aria-controls": `${id}-results`,
284
+ "aria-activedescendant": matches[active] ? `${id}-${matches[active].name}` : void 0,
285
+ placeholder: "e.g. trim, minimum, date\u2026",
286
+ value: search,
287
+ onChange: (event) => {
288
+ setSearch(event.target.value);
289
+ setActive(0);
290
+ },
291
+ onKeyDown: (event) => {
292
+ if (event.nativeEvent.isComposing || event.ctrlKey || event.metaKey || event.altKey || !matches.length)
293
+ return;
294
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
295
+ event.preventDefault();
296
+ setActive(
297
+ (index) => (index + (event.key === "ArrowDown" ? 1 : matches.length - 1)) % matches.length
298
+ );
299
+ } else if (event.key === "Enter") {
300
+ event.preventDefault();
301
+ const fn = matches[active];
302
+ if (fn) onInsert(fn.name);
303
+ }
304
+ }
305
+ }
306
+ )
307
+ ] }),
308
+ /* @__PURE__ */ jsxs("div", { className: "qt-function-filters", children: [
309
+ /* @__PURE__ */ jsxs("label", { children: [
310
+ "Browse by concept",
311
+ /* @__PURE__ */ jsxs(
312
+ "select",
313
+ {
314
+ value: category,
315
+ onChange: (event) => {
316
+ setCategory(event.target.value);
317
+ setActive(0);
318
+ },
319
+ children: [
320
+ /* @__PURE__ */ jsx("option", { value: "all", children: "All functions" }),
321
+ FUNCTION_CATEGORIES.map((item) => /* @__PURE__ */ jsx("option", { value: item.id, children: item.label }, item.id))
322
+ ]
323
+ }
324
+ )
325
+ ] }),
326
+ /* @__PURE__ */ jsxs("label", { children: [
327
+ "Return type",
328
+ /* @__PURE__ */ jsxs(
329
+ "select",
330
+ {
331
+ value: result,
332
+ onChange: (event) => {
333
+ setResult(
334
+ event.target.value
335
+ );
336
+ setActive(0);
337
+ },
338
+ children: [
339
+ /* @__PURE__ */ jsx("option", { value: "all", children: "Any type" }),
340
+ FUNCTION_RESULT_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type.value, children: type.label }, type.value))
341
+ ]
342
+ }
343
+ )
344
+ ] })
345
+ ] }),
346
+ concept && /* @__PURE__ */ jsx("p", { className: "qt-function-concept", children: concept.description }),
347
+ /* @__PURE__ */ jsxs("p", { className: "qt-function-count", "aria-live": "polite", children: [
348
+ matches.length,
349
+ " ",
350
+ matches.length === 1 ? "function" : "functions"
351
+ ] }),
352
+ /* @__PURE__ */ jsx(
353
+ "div",
354
+ {
355
+ ref: list,
356
+ className: "qt-function-list",
357
+ id: `${id}-results`,
358
+ role: "listbox",
359
+ "aria-label": "Function library results",
360
+ children: matches.map((fn, index) => /* @__PURE__ */ jsxs(
361
+ "button",
362
+ {
363
+ type: "button",
364
+ role: "option",
365
+ id: `${id}-${fn.name}`,
366
+ "aria-selected": index === active,
367
+ "aria-label": `Insert ${fn.name}`,
368
+ onMouseDown: (event) => event.preventDefault(),
369
+ onFocus: () => setActive(index),
370
+ onClick: () => onInsert(fn.name),
371
+ children: [
372
+ /* @__PURE__ */ jsxs("span", { className: "qt-function-card-title", children: [
373
+ /* @__PURE__ */ jsx("strong", { children: fn.name }),
374
+ /* @__PURE__ */ jsx("span", { children: functionResultLabel(fn.result) })
375
+ ] }),
376
+ /* @__PURE__ */ jsx("code", { children: fn.signature }),
377
+ /* @__PURE__ */ jsx("small", { children: fn.description }),
378
+ /* @__PURE__ */ jsx("span", { className: "qt-function-card-concept", children: FUNCTION_CATEGORIES.find(
379
+ (item) => item.id === functionCategory(fn)
380
+ ).label })
381
+ ]
382
+ },
383
+ fn.name
384
+ ))
385
+ }
386
+ ),
387
+ !matches.length && /* @__PURE__ */ jsx("p", { className: "qt-function-empty", children: "No functions match. Try a shorter search or a different concept or return type." }),
388
+ /* @__PURE__ */ jsx("p", { className: "qt-function-insert-hint", children: "Click a function or use \u2191\u2193 and Enter in search to insert it at the cursor. Select formula text first to wrap it." })
389
+ ] });
390
+ }
391
+
392
+ // src/FormulaEditor.tsx
393
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
394
+ function FormulaEditor({
395
+ value,
396
+ onChange,
397
+ fields,
398
+ compile,
399
+ suggestions = true,
400
+ showLibrary = true,
401
+ ariaLabel = "Computed column formula",
402
+ inputRef,
403
+ onFocus,
404
+ onReady,
405
+ rows = 6
406
+ }) {
407
+ const id = useId2();
408
+ const editor = useRef2(null);
409
+ const applyingEdit = useRef2(false);
410
+ const composing = useRef2(false);
411
+ const paired = useRef2({ value, ranges: [] });
412
+ const trackEdit = (next) => {
413
+ const previous = paired.current.value;
414
+ if (previous === next) return;
415
+ let from = 0;
416
+ while (from < previous.length && from < next.length && previous[from] === next[from])
417
+ from++;
418
+ let oldEnd = previous.length, newEnd = next.length;
419
+ while (oldEnd > from && newEnd > from && previous[oldEnd - 1] === next[newEnd - 1]) {
420
+ oldEnd--;
421
+ newEnd--;
422
+ }
423
+ const shift = newEnd - oldEnd;
424
+ paired.current.ranges = paired.current.ranges.filter(
425
+ ({ open, close }) => !(open >= from && open < oldEnd) && !(close >= from && close < oldEnd)
426
+ ).map(({ open, close }) => ({
427
+ open: open >= oldEnd ? open + shift : open,
428
+ close: close >= oldEnd ? close + shift : close
429
+ }));
430
+ paired.current.value = next;
431
+ };
432
+ const selection = useRef2({ start: 0, end: 0 });
433
+ const latestCompile = useRef2(compile);
434
+ latestCompile.current = compile;
435
+ const [caret, setCaret] = useState2(0);
436
+ const [completions, setCompletions] = useState2(
437
+ null
438
+ );
439
+ const [active, setActive] = useState2(0);
440
+ const [diagnostic, setDiagnostic] = useState2(null);
441
+ const options = suggestions ? completions?.options.slice(0, 50) ?? [] : [];
442
+ const hint = suggestions ? signatureAt(value.slice(0, caret)) : "";
443
+ useEffect2(() => {
444
+ const input = editor.current;
445
+ if (input && input.value !== value) {
446
+ input.value = value;
447
+ paired.current = { value, ranges: [] };
448
+ setCaret(input.selectionStart);
449
+ setCompletions(null);
450
+ }
451
+ setDiagnostic(null);
452
+ const timer = setTimeout(() => {
453
+ try {
454
+ latestCompile.current(value);
455
+ setDiagnostic(null);
456
+ } catch (error) {
457
+ const from = error instanceof FormulaError ? error.from : 0;
458
+ const to = error instanceof FormulaError ? error.to : value.length;
459
+ setDiagnostic({
460
+ message: error instanceof Error ? error.message : String(error),
461
+ from: Math.max(0, Math.min(from, value.length)),
462
+ to: Math.max(0, Math.min(to, value.length))
463
+ });
464
+ }
465
+ }, 250);
466
+ return () => clearTimeout(timer);
467
+ }, [value]);
468
+ useEffect2(() => {
469
+ if (editor.current) onReady?.(editor.current);
470
+ }, [onReady]);
471
+ useEffect2(() => {
472
+ document.getElementById(`${id}-option-${active}`)?.scrollIntoView?.({ block: "nearest" });
473
+ }, [active, id]);
474
+ const suggest = (explicit = false) => {
475
+ const input = editor.current;
476
+ if (!suggestions || !input || composing.current) return;
477
+ selection.current = {
478
+ start: input.selectionStart,
479
+ end: input.selectionEnd
480
+ };
481
+ setCaret(input.selectionStart);
482
+ setActive(0);
483
+ setCompletions(
484
+ formulaCompletions(
485
+ input.value,
486
+ input.selectionStart,
487
+ input.selectionEnd,
488
+ fields,
489
+ explicit
490
+ )
491
+ );
492
+ };
493
+ const insert = (from, to, text, caretOffset = text.length, pair) => {
494
+ const input = editor.current;
495
+ if (!input) return;
496
+ input.focus();
497
+ input.setSelectionRange(from, to);
498
+ applyingEdit.current = true;
499
+ try {
500
+ const before = input.value;
501
+ try {
502
+ document.execCommand?.("insertText", false, text);
503
+ } catch {
504
+ }
505
+ if (input.value === before) input.setRangeText(text, from, to, "end");
506
+ } finally {
507
+ applyingEdit.current = false;
508
+ }
509
+ trackEdit(input.value);
510
+ if (pair)
511
+ paired.current.ranges.push({
512
+ open: from + pair.open,
513
+ close: from + pair.close
514
+ });
515
+ input.setSelectionRange(from + caretOffset, from + caretOffset);
516
+ setCaret(input.selectionStart);
517
+ setCompletions(null);
518
+ onChange(input.value);
519
+ };
520
+ const accept = (option) => {
521
+ if (completions)
522
+ insert(
523
+ completions.from,
524
+ completions.to,
525
+ option.insert,
526
+ option.caretOffset,
527
+ option.insert.endsWith("()") ? { open: option.insert.length - 2, close: option.insert.length - 1 } : void 0
528
+ );
529
+ };
530
+ const onKeyDown = (event) => {
531
+ const input = event.currentTarget;
532
+ if (composing.current || event.nativeEvent.isComposing) return;
533
+ if (suggestions && event.ctrlKey && event.code === "Space") {
534
+ event.preventDefault();
535
+ suggest(true);
536
+ return;
537
+ }
538
+ if (options.length) {
539
+ if (event.key === "Escape") {
540
+ event.preventDefault();
541
+ event.stopPropagation();
542
+ setCompletions(null);
543
+ return;
544
+ }
545
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
546
+ event.preventDefault();
547
+ setActive(
548
+ (index) => (index + (event.key === "ArrowDown" ? 1 : options.length - 1)) % options.length
549
+ );
550
+ return;
551
+ }
552
+ if (event.key === "Enter") {
553
+ event.preventDefault();
554
+ const option = options[active];
555
+ if (option) accept(option);
556
+ return;
557
+ }
558
+ }
559
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
560
+ const start = input.selectionStart, end = input.selectionEnd;
561
+ const context = formulaContext(input.value, start);
562
+ const close = event.key === "(" ? ")" : event.key === '"' ? '"' : void 0;
563
+ if (close && !context.quoted && context.fieldStart < 0 && (start !== end || end === input.value.length || /[\s)\]}]/.test(input.value[end]))) {
564
+ event.preventDefault();
565
+ const selection2 = input.value.slice(start, end);
566
+ insert(start, end, event.key + selection2 + close, selection2.length + 1, {
567
+ open: 0,
568
+ close: selection2.length + 1
569
+ });
570
+ } else if (start === end && input.value[start] === event.key && paired.current.ranges.some((pair) => pair.close === start) && (event.key === ")" && !context.quoted && context.fieldStart < 0 || event.key === '"' && context.quoted && !context.escaped)) {
571
+ event.preventDefault();
572
+ paired.current.ranges = paired.current.ranges.filter(
573
+ (pair) => pair.close !== start
574
+ );
575
+ input.setSelectionRange(start + 1, start + 1);
576
+ setCaret(start + 1);
577
+ setCompletions(null);
578
+ } else if (event.key === "Backspace" && start === end && start > 0 && ["()", "[]", '""'].includes(input.value.slice(start - 1, start + 1)) && (!context.quoted || input.value.slice(start - 1, start + 1) === '""' && !context.escaped)) {
579
+ event.preventDefault();
580
+ insert(start - 1, start + 1, "");
581
+ }
582
+ };
583
+ return /* @__PURE__ */ jsxs2("div", { className: "qt-formula-layout", children: [
584
+ /* @__PURE__ */ jsxs2("div", { className: "qt-formula-editor", children: [
585
+ /* @__PURE__ */ jsx2(
586
+ "textarea",
587
+ {
588
+ ref: (input) => {
589
+ editor.current = input;
590
+ if (inputRef) inputRef.current = input;
591
+ },
592
+ className: "qt-formula-input",
593
+ "aria-label": ariaLabel,
594
+ "aria-describedby": `${id}-hint${diagnostic ? ` ${id}-error` : ""}`,
595
+ "aria-invalid": diagnostic ? true : void 0,
596
+ "aria-autocomplete": suggestions ? "list" : "none",
597
+ "aria-controls": options.length ? `${id}-suggestions` : void 0,
598
+ "aria-activedescendant": options.length ? `${id}-option-${active}` : void 0,
599
+ defaultValue: value,
600
+ rows,
601
+ spellCheck: false,
602
+ autoCapitalize: "off",
603
+ autoCorrect: "off",
604
+ onChange: (event) => {
605
+ if (applyingEdit.current) return;
606
+ trackEdit(event.currentTarget.value);
607
+ onChange(event.currentTarget.value);
608
+ if (!composing.current) suggest();
609
+ },
610
+ onSelect: (event) => {
611
+ setCaret(event.currentTarget.selectionStart);
612
+ const input = event.currentTarget;
613
+ if (!applyingEdit.current && (input.selectionStart !== selection.current.start || input.selectionEnd !== selection.current.end))
614
+ setCompletions(null);
615
+ },
616
+ onKeyDown,
617
+ onCompositionStart: () => {
618
+ composing.current = true;
619
+ setCompletions(null);
620
+ },
621
+ onCompositionEnd: () => {
622
+ composing.current = false;
623
+ suggest();
624
+ },
625
+ onBlur: () => setCompletions(null),
626
+ onFocus
627
+ }
628
+ ),
629
+ /* @__PURE__ */ jsxs2("div", { className: "qt-formula-tools", children: [
630
+ suggestions && /* @__PURE__ */ jsx2(
631
+ "button",
632
+ {
633
+ type: "button",
634
+ className: "qt-link-btn",
635
+ onMouseDown: (event) => event.preventDefault(),
636
+ onClick: () => {
637
+ editor.current?.focus();
638
+ suggest(true);
639
+ },
640
+ children: "Suggestions"
641
+ }
642
+ ),
643
+ /* @__PURE__ */ jsxs2("span", { children: [
644
+ "Line ",
645
+ value.slice(0, caret).split("\n").length,
646
+ ", column",
647
+ " ",
648
+ caret - value.slice(0, caret).lastIndexOf("\n")
649
+ ] })
650
+ ] }),
651
+ options.length > 0 && /* @__PURE__ */ jsxs2(
652
+ "div",
653
+ {
654
+ className: "qt-formula-completions",
655
+ "data-qt-formula-suggestions": "",
656
+ children: [
657
+ /* @__PURE__ */ jsx2(
658
+ "ul",
659
+ {
660
+ id: `${id}-suggestions`,
661
+ role: "listbox",
662
+ "aria-label": "Formula suggestions",
663
+ onMouseDown: (event) => event.preventDefault(),
664
+ children: options.map((option, index) => /* @__PURE__ */ jsxs2(
665
+ "li",
666
+ {
667
+ id: `${id}-option-${index}`,
668
+ role: "option",
669
+ "aria-selected": index === active,
670
+ title: option.description,
671
+ onClick: () => accept(option),
672
+ children: [
673
+ /* @__PURE__ */ jsx2("strong", { children: option.label }),
674
+ /* @__PURE__ */ jsx2("small", { children: option.detail })
675
+ ]
676
+ },
677
+ option.label
678
+ ))
679
+ }
680
+ ),
681
+ /* @__PURE__ */ jsxs2("p", { children: [
682
+ options[active]?.description,
683
+ " \xB7 \u2191\u2193 choose, Enter inserts, Esc closes"
684
+ ] }),
685
+ completions && completions.options.length > options.length && /* @__PURE__ */ jsxs2("p", { children: [
686
+ "Type to narrow the first ",
687
+ options.length,
688
+ " suggestions."
689
+ ] })
690
+ ]
691
+ }
692
+ ),
693
+ /* @__PURE__ */ jsx2("p", { id: `${id}-hint`, className: "qt-formula-hint", "aria-live": "polite", children: hint }),
694
+ diagnostic && /* @__PURE__ */ jsxs2("div", { id: `${id}-error`, className: "qt-formula-error", role: "status", children: [
695
+ diagnostic.message,
696
+ " ",
697
+ /* @__PURE__ */ jsx2(
698
+ "button",
699
+ {
700
+ type: "button",
701
+ className: "qt-link-btn",
702
+ onClick: () => {
703
+ editor.current?.focus();
704
+ editor.current?.setSelectionRange(
705
+ diagnostic.from,
706
+ diagnostic.to
707
+ );
708
+ setCaret(diagnostic.from);
709
+ },
710
+ children: "Select error location"
711
+ }
712
+ )
713
+ ] })
714
+ ] }),
715
+ showLibrary && /* @__PURE__ */ jsx2(
716
+ FormulaFunctionBrowser,
717
+ {
718
+ onInsert: (name) => {
719
+ const input = editor.current;
720
+ if (!input) return;
721
+ const from = input.selectionStart;
722
+ const to = input.selectionEnd;
723
+ const selected = input.value.slice(from, to);
724
+ insert(
725
+ from,
726
+ to,
727
+ `${name}(${selected})`,
728
+ name.length + selected.length + 1,
729
+ { open: name.length, close: name.length + selected.length + 1 }
730
+ );
731
+ }
732
+ }
733
+ )
734
+ ] });
735
+ }
736
+ export {
737
+ FormulaEditor as default
738
+ };
739
+ //# sourceMappingURL=FormulaEditor-FZPLQRWN.js.map