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