elastic-input 0.3.4 → 0.3.6
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 +34 -0
- package/dist/components/AutocompleteDropdown.d.ts +3 -1
- package/dist/elastic-input.es.js +257 -155
- package/dist/index.d.ts +1 -0
- package/dist/parser/ast.d.ts +2 -0
- package/dist/types.d.ts +13 -0
- package/dist/utils/formatQuery.d.ts +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -455,6 +455,40 @@ Pass `HighlightOptions` for matched-paren highlighting:
|
|
|
455
455
|
buildHighlightedHTML(tokens, DEFAULT_COLORS, { cursorOffset: 5 });
|
|
456
456
|
```
|
|
457
457
|
|
|
458
|
+
## Query Formatting
|
|
459
|
+
|
|
460
|
+
Pretty-print messy or minified queries with `formatQuery` — a pure function (no React or DOM required):
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
import { formatQuery } from 'elastic-input';
|
|
464
|
+
|
|
465
|
+
formatQuery('(status:active OR status:lead) AND deal_value:>5000 AND NOT tags:churned');
|
|
466
|
+
// (status:active OR status:lead)
|
|
467
|
+
// AND deal_value:>5000
|
|
468
|
+
// AND NOT tags:churned
|
|
469
|
+
|
|
470
|
+
formatQuery('( (status:active AND deal_value:>10000) OR (status:lead AND tags:enterprise) ) AND created:[2024-01-01 TO 2024-12-31]');
|
|
471
|
+
// (
|
|
472
|
+
// status:active AND deal_value:>10000
|
|
473
|
+
// OR status:lead AND tags:enterprise
|
|
474
|
+
// )
|
|
475
|
+
// AND created:[2024-01-01 TO 2024-12-31]
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Accepts a raw query string or a pre-parsed `ASTNode`. Options control line-break threshold and indentation:
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
import type { FormatQueryOptions } from 'elastic-input';
|
|
482
|
+
|
|
483
|
+
formatQuery(query, { maxLineLength: 80, indent: '\t' });
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
| Option | Type | Default | Description |
|
|
487
|
+
|--------|------|---------|-------------|
|
|
488
|
+
| `maxLineLength` | `number` | `60` | Lines shorter than this stay inline |
|
|
489
|
+
| `indent` | `string` | `' '` (2 spaces) | Indent string per nesting level |
|
|
490
|
+
| `whitespaceOperator` | `string` | — | Replace implicit AND (whitespace) with this operator (e.g. `'AND'`, `'&&'`) |
|
|
491
|
+
|
|
458
492
|
## Requirements
|
|
459
493
|
|
|
460
494
|
### Runtime (Browser)
|
|
@@ -19,6 +19,8 @@ interface AutocompleteDropdownProps {
|
|
|
19
19
|
renderSavedSearchItem?: (search: SavedSearch, isSelected: boolean) => React.ReactNode | null | undefined;
|
|
20
20
|
renderDropdownHeader?: (context: CursorContext) => React.ReactNode | null | undefined;
|
|
21
21
|
cursorContext?: CursorContext | null;
|
|
22
|
+
/** Ref callback to expose the dropdown list element for page-size calculations. */
|
|
23
|
+
listRefCallback?: (el: HTMLDivElement | null) => void;
|
|
22
24
|
/** Custom class names for dropdown elements. */
|
|
23
25
|
classNames?: {
|
|
24
26
|
dropdown?: string;
|
|
@@ -26,5 +28,5 @@ interface AutocompleteDropdownProps {
|
|
|
26
28
|
dropdownItem?: string;
|
|
27
29
|
};
|
|
28
30
|
}
|
|
29
|
-
export declare function AutocompleteDropdown({ suggestions, selectedIndex, onSelect, position, colors, styles, visible, fixedWidth, renderHistoryItem, renderSavedSearchItem, renderDropdownHeader, cursorContext, classNames, }: AutocompleteDropdownProps): React.ReactPortal | null;
|
|
31
|
+
export declare function AutocompleteDropdown({ suggestions, selectedIndex, onSelect, position, colors, styles, visible, fixedWidth, renderHistoryItem, renderSavedSearchItem, renderDropdownHeader, cursorContext, listRefCallback, classNames, }: AutocompleteDropdownProps): React.ReactPortal | null;
|
|
30
32
|
export {};
|
package/dist/elastic-input.es.js
CHANGED
|
@@ -444,6 +444,7 @@ class Parser {
|
|
|
444
444
|
operator: "AND",
|
|
445
445
|
left: result,
|
|
446
446
|
right,
|
|
447
|
+
implicit: true,
|
|
447
448
|
start: result.start,
|
|
448
449
|
end: right.end
|
|
449
450
|
};
|
|
@@ -526,6 +527,7 @@ class Parser {
|
|
|
526
527
|
operator: "AND",
|
|
527
528
|
left,
|
|
528
529
|
right,
|
|
530
|
+
implicit: true,
|
|
529
531
|
start: left.start,
|
|
530
532
|
end: right.end
|
|
531
533
|
};
|
|
@@ -2749,6 +2751,7 @@ function AutocompleteDropdown({
|
|
|
2749
2751
|
renderSavedSearchItem,
|
|
2750
2752
|
renderDropdownHeader,
|
|
2751
2753
|
cursorContext,
|
|
2754
|
+
listRefCallback,
|
|
2752
2755
|
classNames
|
|
2753
2756
|
}) {
|
|
2754
2757
|
const portalRef = React.useRef(null);
|
|
@@ -2784,7 +2787,10 @@ function AutocompleteDropdown({
|
|
|
2784
2787
|
left: `${position.left}px`,
|
|
2785
2788
|
...fixedWidth != null ? { width: `${fixedWidth}px`, minWidth: "unset", maxWidth: "unset" } : {}
|
|
2786
2789
|
};
|
|
2787
|
-
const content = /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown", classNames == null ? void 0 : classNames.dropdown), style: dropdownStyle, ref:
|
|
2790
|
+
const content = /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown", classNames == null ? void 0 : classNames.dropdown), style: dropdownStyle, ref: (el) => {
|
|
2791
|
+
listRef.current = el;
|
|
2792
|
+
listRefCallback == null ? void 0 : listRefCallback(el);
|
|
2793
|
+
}, onMouseDown: (e) => e.preventDefault() }, hasHeader && /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown-header", classNames == null ? void 0 : classNames.dropdownHeader), style: {
|
|
2788
2794
|
padding: mergedStyles.dropdownItemPadding || "4px 10px",
|
|
2789
2795
|
fontSize: "11px",
|
|
2790
2796
|
color: mergedColors.placeholder,
|
|
@@ -2817,6 +2823,9 @@ function AutocompleteDropdown({
|
|
|
2817
2823
|
if (suggestion.type === "error") {
|
|
2818
2824
|
return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--error", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.8 } }, /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-label", style: { ...getDropdownItemLabelStyle(), color: mergedColors.error } }, suggestion.label || "Error loading suggestions"));
|
|
2819
2825
|
}
|
|
2826
|
+
if (suggestion.type === "noResults") {
|
|
2827
|
+
return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--no-results", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.7 } }, suggestion.customContent);
|
|
2828
|
+
}
|
|
2820
2829
|
if (suggestion.type === "loading") {
|
|
2821
2830
|
return /* @__PURE__ */ React.createElement("div", { key: i, className: cx("ei-dropdown-item", "ei-dropdown-item--loading", classNames == null ? void 0 : classNames.dropdownItem), style: { ...itemStyle, cursor: "default", opacity: 0.6, justifyContent: "center" } }, /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-label", style: { ...getDropdownItemLabelStyle(), fontStyle: "italic" } }, suggestion.label || "Searching..."), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-spinner", style: { marginLeft: "6px", display: "inline-block", animation: "elastic-input-spin 1s linear infinite", width: "14px", height: "14px", border: "2px solid", borderColor: `${mergedColors.placeholder} transparent ${mergedColors.placeholder} transparent`, borderRadius: "50%" } }));
|
|
2822
2831
|
}
|
|
@@ -3622,6 +3631,155 @@ function dedup(ranges) {
|
|
|
3622
3631
|
}
|
|
3623
3632
|
return result;
|
|
3624
3633
|
}
|
|
3634
|
+
const DEFAULT_MAX_LINE_LENGTH = 60;
|
|
3635
|
+
const DEFAULT_INDENT = " ";
|
|
3636
|
+
function formatQuery(input, options) {
|
|
3637
|
+
const maxLineLength = (options == null ? void 0 : options.maxLineLength) ?? DEFAULT_MAX_LINE_LENGTH;
|
|
3638
|
+
const indent = (options == null ? void 0 : options.indent) ?? DEFAULT_INDENT;
|
|
3639
|
+
const whitespaceOperator = options == null ? void 0 : options.whitespaceOperator;
|
|
3640
|
+
let ast;
|
|
3641
|
+
if (typeof input === "string") {
|
|
3642
|
+
const tokens = new Lexer(input, { savedSearches: true, historySearch: true }).tokenize();
|
|
3643
|
+
const parser = new Parser(tokens);
|
|
3644
|
+
ast = parser.parse();
|
|
3645
|
+
} else {
|
|
3646
|
+
ast = input;
|
|
3647
|
+
}
|
|
3648
|
+
if (!ast) return typeof input === "string" ? input : "";
|
|
3649
|
+
return printNode(ast, 0, maxLineLength, indent, whitespaceOperator);
|
|
3650
|
+
}
|
|
3651
|
+
function resolveOperator(node, whitespaceOperator) {
|
|
3652
|
+
if (node.implicit) return whitespaceOperator ?? "";
|
|
3653
|
+
return node.operator;
|
|
3654
|
+
}
|
|
3655
|
+
function inline(node, whitespaceOperator) {
|
|
3656
|
+
switch (node.type) {
|
|
3657
|
+
case "FieldValue": {
|
|
3658
|
+
const val = node.quoted ? `"${node.value}"` : node.value;
|
|
3659
|
+
const op = node.operator === ":" ? ":" : `:${node.operator}`;
|
|
3660
|
+
let s = `${node.field}${op}${val}`;
|
|
3661
|
+
if (node.fuzzy != null) s += `~${node.fuzzy}`;
|
|
3662
|
+
if (node.proximity != null) s += `~${node.proximity}`;
|
|
3663
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3664
|
+
return s;
|
|
3665
|
+
}
|
|
3666
|
+
case "BareTerm": {
|
|
3667
|
+
let s = node.quoted ? `"${node.value}"` : node.value;
|
|
3668
|
+
if (node.fuzzy != null) s += `~${node.fuzzy}`;
|
|
3669
|
+
if (node.proximity != null) s += `~${node.proximity}`;
|
|
3670
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3671
|
+
return s;
|
|
3672
|
+
}
|
|
3673
|
+
case "Range": {
|
|
3674
|
+
const lb = node.lowerInclusive ? "[" : "{";
|
|
3675
|
+
const rb = node.upperInclusive ? "]" : "}";
|
|
3676
|
+
const lower = node.lowerQuoted ? `"${node.lower}"` : node.lower;
|
|
3677
|
+
const upper = node.upperQuoted ? `"${node.upper}"` : node.upper;
|
|
3678
|
+
const range = `${lb}${lower} TO ${upper}${rb}`;
|
|
3679
|
+
return node.field ? `${node.field}:${range}` : range;
|
|
3680
|
+
}
|
|
3681
|
+
case "Regex":
|
|
3682
|
+
return `/${node.pattern}/`;
|
|
3683
|
+
case "SavedSearch":
|
|
3684
|
+
return `#${node.name}`;
|
|
3685
|
+
case "HistoryRef":
|
|
3686
|
+
return `!${node.ref}`;
|
|
3687
|
+
case "Not":
|
|
3688
|
+
return `NOT ${inline(node.expression, whitespaceOperator)}`;
|
|
3689
|
+
case "Group": {
|
|
3690
|
+
let s = `(${inline(node.expression, whitespaceOperator)})`;
|
|
3691
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3692
|
+
return s;
|
|
3693
|
+
}
|
|
3694
|
+
case "FieldGroup": {
|
|
3695
|
+
let s = `${node.field}:(${inline(node.expression, whitespaceOperator)})`;
|
|
3696
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3697
|
+
return s;
|
|
3698
|
+
}
|
|
3699
|
+
case "BooleanExpr": {
|
|
3700
|
+
const op = resolveOperator(node, whitespaceOperator);
|
|
3701
|
+
const sep = op ? ` ${op} ` : " ";
|
|
3702
|
+
return `${inline(node.left, whitespaceOperator)}${sep}${inline(node.right, whitespaceOperator)}`;
|
|
3703
|
+
}
|
|
3704
|
+
case "Error":
|
|
3705
|
+
return node.value;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
function flattenChain(node) {
|
|
3709
|
+
const op = node.operator;
|
|
3710
|
+
const implicit = !!node.implicit;
|
|
3711
|
+
const operands = [];
|
|
3712
|
+
const collect = (n) => {
|
|
3713
|
+
if (n.type === "BooleanExpr" && n.operator === op && !!n.implicit === implicit) {
|
|
3714
|
+
collect(n.left);
|
|
3715
|
+
collect(n.right);
|
|
3716
|
+
} else {
|
|
3717
|
+
operands.push(n);
|
|
3718
|
+
}
|
|
3719
|
+
};
|
|
3720
|
+
collect(node);
|
|
3721
|
+
return { operator: op, implicit, operands };
|
|
3722
|
+
}
|
|
3723
|
+
function containsGroups(node) {
|
|
3724
|
+
if (node.type === "Group" || node.type === "FieldGroup") return true;
|
|
3725
|
+
if (node.type === "BooleanExpr") return containsGroups(node.left) || containsGroups(node.right);
|
|
3726
|
+
if (node.type === "Not") return containsGroups(node.expression);
|
|
3727
|
+
return false;
|
|
3728
|
+
}
|
|
3729
|
+
function shouldBreakGroup(expr, maxLineLength) {
|
|
3730
|
+
const inlined = inline(expr);
|
|
3731
|
+
if (inlined.length > maxLineLength) return true;
|
|
3732
|
+
if (containsGroups(expr)) return true;
|
|
3733
|
+
return false;
|
|
3734
|
+
}
|
|
3735
|
+
function printNode(node, depth, maxLineLength, indent, whitespaceOperator) {
|
|
3736
|
+
const pad = indent.repeat(depth);
|
|
3737
|
+
switch (node.type) {
|
|
3738
|
+
case "BooleanExpr": {
|
|
3739
|
+
const { operator, implicit, operands } = flattenChain(node);
|
|
3740
|
+
const displayOp = implicit ? whitespaceOperator ?? "" : operator;
|
|
3741
|
+
const sep = displayOp ? ` ${displayOp} ` : " ";
|
|
3742
|
+
const inlined = operands.map((o) => inline(o, whitespaceOperator)).join(sep);
|
|
3743
|
+
if (inlined.length <= maxLineLength) {
|
|
3744
|
+
return inlined;
|
|
3745
|
+
}
|
|
3746
|
+
const lines = operands.map((operand, i) => {
|
|
3747
|
+
const printed = printNode(operand, depth, maxLineLength, indent, whitespaceOperator);
|
|
3748
|
+
if (i === 0) return printed;
|
|
3749
|
+
return displayOp ? `${pad}${displayOp} ${printed}` : `${pad}${printed}`;
|
|
3750
|
+
});
|
|
3751
|
+
return lines.join("\n");
|
|
3752
|
+
}
|
|
3753
|
+
case "Group": {
|
|
3754
|
+
if (!shouldBreakGroup(node.expression, maxLineLength)) {
|
|
3755
|
+
let s2 = `(${inline(node.expression, whitespaceOperator)})`;
|
|
3756
|
+
if (node.boost != null) s2 += `^${node.boost}`;
|
|
3757
|
+
return s2;
|
|
3758
|
+
}
|
|
3759
|
+
const inner = printNode(node.expression, depth + 1, maxLineLength, indent, whitespaceOperator);
|
|
3760
|
+
let s = `(
|
|
3761
|
+
${indentLines(inner, depth + 1, indent)}
|
|
3762
|
+
${pad})`;
|
|
3763
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3764
|
+
return s;
|
|
3765
|
+
}
|
|
3766
|
+
case "FieldGroup": {
|
|
3767
|
+
let s = `${node.field}:(${inline(node.expression, whitespaceOperator)})`;
|
|
3768
|
+
if (node.boost != null) s += `^${node.boost}`;
|
|
3769
|
+
return s;
|
|
3770
|
+
}
|
|
3771
|
+
case "Not":
|
|
3772
|
+
return `NOT ${printNode(node.expression, depth, maxLineLength, indent, whitespaceOperator)}`;
|
|
3773
|
+
default:
|
|
3774
|
+
return inline(node, whitespaceOperator);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
function indentLines(text, depth, indent) {
|
|
3778
|
+
const pad = indent.repeat(depth);
|
|
3779
|
+
return text.split("\n").map((line) => {
|
|
3780
|
+
return line.startsWith(pad) ? line : pad + line;
|
|
3781
|
+
}).join("\n");
|
|
3782
|
+
}
|
|
3625
3783
|
class UndoStack {
|
|
3626
3784
|
constructor(maxSize = 100) {
|
|
3627
3785
|
__publicField(this, "stack", []);
|
|
@@ -3802,11 +3960,14 @@ function ElasticInput(props) {
|
|
|
3802
3960
|
const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
|
|
3803
3961
|
const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
|
|
3804
3962
|
const renderDropdownHeader = dropdownConfig == null ? void 0 : dropdownConfig.renderHeader;
|
|
3963
|
+
const renderNoResults = dropdownConfig == null ? void 0 : dropdownConfig.renderNoResults;
|
|
3805
3964
|
const autoSelect = (dropdownConfig == null ? void 0 : dropdownConfig.autoSelect) ?? false;
|
|
3965
|
+
const homeEndKeys = (dropdownConfig == null ? void 0 : dropdownConfig.homeEndKeys) ?? false;
|
|
3806
3966
|
const multiline = (featuresConfig == null ? void 0 : featuresConfig.multiline) !== false;
|
|
3807
3967
|
const smartSelectAll = (featuresConfig == null ? void 0 : featuresConfig.smartSelectAll) ?? false;
|
|
3808
3968
|
const expandSelection = (featuresConfig == null ? void 0 : featuresConfig.expandSelection) ?? false;
|
|
3809
3969
|
const wildcardWrap = (featuresConfig == null ? void 0 : featuresConfig.wildcardWrap) ?? false;
|
|
3970
|
+
const enableFormatQuery = (featuresConfig == null ? void 0 : featuresConfig.formatQuery) ?? false;
|
|
3810
3971
|
const lexerOptions = React.useMemo(() => ({
|
|
3811
3972
|
savedSearches: enableSavedSearches,
|
|
3812
3973
|
historySearch: enableHistorySearch
|
|
@@ -3845,9 +4006,11 @@ function ElasticInput(props) {
|
|
|
3845
4006
|
setEditorEl(el);
|
|
3846
4007
|
}, []);
|
|
3847
4008
|
const containerRef = React.useRef(null);
|
|
4009
|
+
const dropdownListRef = React.useRef(null);
|
|
3848
4010
|
const currentValueRef = React.useRef(value || defaultValue || "");
|
|
3849
4011
|
const debounceTimerRef = React.useRef(null);
|
|
3850
4012
|
const isComposingRef = React.useRef(false);
|
|
4013
|
+
const keyConsumedByDropdownRef = React.useRef(false);
|
|
3851
4014
|
const undoStackRef = React.useRef(new UndoStack());
|
|
3852
4015
|
const typingGroupTimerRef = React.useRef(null);
|
|
3853
4016
|
const abortControllerRef = React.useRef(null);
|
|
@@ -4037,6 +4200,24 @@ function ElasticInput(props) {
|
|
|
4037
4200
|
return { ...s, customContent: custom };
|
|
4038
4201
|
});
|
|
4039
4202
|
}, [renderFieldHint]);
|
|
4203
|
+
const tryShowNoResults = React.useCallback((context) => {
|
|
4204
|
+
if (!renderNoResults) return false;
|
|
4205
|
+
const content = renderNoResults({ cursorContext: context, partial: context.partial });
|
|
4206
|
+
if (content == null) return false;
|
|
4207
|
+
const noResultsSuggestion = {
|
|
4208
|
+
text: "",
|
|
4209
|
+
label: "",
|
|
4210
|
+
type: "noResults",
|
|
4211
|
+
customContent: content,
|
|
4212
|
+
replaceStart: 0,
|
|
4213
|
+
replaceEnd: 0
|
|
4214
|
+
};
|
|
4215
|
+
setSuggestions([noResultsSuggestion]);
|
|
4216
|
+
setSelectedSuggestionIndex(-1);
|
|
4217
|
+
setShowDatePicker(false);
|
|
4218
|
+
showDropdownAtPosition(32, 300);
|
|
4219
|
+
return true;
|
|
4220
|
+
}, [renderNoResults]);
|
|
4040
4221
|
const updateSuggestionsFromTokens = React.useCallback((toks, offset) => {
|
|
4041
4222
|
var _a, _b, _c;
|
|
4042
4223
|
const result = engineRef.current.getSuggestions(toks, offset);
|
|
@@ -4131,10 +4312,12 @@ function ElasticInput(props) {
|
|
|
4131
4312
|
setAutocompleteContext(contextType);
|
|
4132
4313
|
showDropdownAtPosition(newSuggestions.length * 32, 300);
|
|
4133
4314
|
} else {
|
|
4134
|
-
setShowDropdown(false);
|
|
4135
|
-
setShowDatePicker(false);
|
|
4136
|
-
setSuggestions([]);
|
|
4137
4315
|
setAutocompleteContext(contextType);
|
|
4316
|
+
if (!tryShowNoResults(result.context)) {
|
|
4317
|
+
setShowDropdown(false);
|
|
4318
|
+
setShowDatePicker(false);
|
|
4319
|
+
setSuggestions([]);
|
|
4320
|
+
}
|
|
4138
4321
|
}
|
|
4139
4322
|
} else {
|
|
4140
4323
|
const token = result.context.token;
|
|
@@ -4230,17 +4413,20 @@ function ElasticInput(props) {
|
|
|
4230
4413
|
showDropdownAtPosition(mapped.length * 32, 300);
|
|
4231
4414
|
} else {
|
|
4232
4415
|
const syncResult = engineRef.current.getSuggestions(stateRef.current.tokens, stateRef.current.cursorOffset);
|
|
4233
|
-
|
|
4234
|
-
syncResult.suggestions.filter((s) => s.type === "hint"),
|
|
4235
|
-
syncResult.context
|
|
4236
|
-
);
|
|
4237
|
-
if (hintSuggestions.length > 0) {
|
|
4238
|
-
setSuggestions(hintSuggestions);
|
|
4239
|
-
setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
|
|
4240
|
-
showDropdownAtPosition(hintSuggestions.length * 32, 300);
|
|
4416
|
+
if (partial && tryShowNoResults(syncResult.context)) {
|
|
4241
4417
|
} else {
|
|
4242
|
-
|
|
4243
|
-
|
|
4418
|
+
const hintSuggestions = applyFieldHint(
|
|
4419
|
+
syncResult.suggestions.filter((s) => s.type === "hint"),
|
|
4420
|
+
syncResult.context
|
|
4421
|
+
);
|
|
4422
|
+
if (hintSuggestions.length > 0) {
|
|
4423
|
+
setSuggestions(hintSuggestions);
|
|
4424
|
+
setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
|
|
4425
|
+
showDropdownAtPosition(hintSuggestions.length * 32, 300);
|
|
4426
|
+
} else if (!tryShowNoResults(syncResult.context)) {
|
|
4427
|
+
setShowDropdown(false);
|
|
4428
|
+
setSuggestions([]);
|
|
4429
|
+
}
|
|
4244
4430
|
}
|
|
4245
4431
|
}
|
|
4246
4432
|
} catch (e) {
|
|
@@ -4265,7 +4451,7 @@ function ElasticInput(props) {
|
|
|
4265
4451
|
}
|
|
4266
4452
|
}, debounceMs);
|
|
4267
4453
|
}
|
|
4268
|
-
}, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay, autoSelect]);
|
|
4454
|
+
}, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay, autoSelect, tryShowNoResults]);
|
|
4269
4455
|
updateSuggestionsRef.current = updateSuggestionsFromTokens;
|
|
4270
4456
|
const closeDropdown = React.useCallback(() => {
|
|
4271
4457
|
var _a;
|
|
@@ -4613,11 +4799,30 @@ function ElasticInput(props) {
|
|
|
4613
4799
|
if (onChange) onChange(entry.value, newAst);
|
|
4614
4800
|
if (onValidationChange) onValidationChange(newErrors);
|
|
4615
4801
|
}, [colors, onChange, onValidationChange, closeDropdown]);
|
|
4802
|
+
const getDropdownPageSize = React.useCallback(() => {
|
|
4803
|
+
const list = dropdownListRef.current;
|
|
4804
|
+
if (!list) return 10;
|
|
4805
|
+
const visibleHeight = list.clientHeight;
|
|
4806
|
+
const items = list.querySelectorAll(".ei-dropdown-item");
|
|
4807
|
+
const firstItem = items[0];
|
|
4808
|
+
if (!firstItem) return 10;
|
|
4809
|
+
const itemHeight = firstItem.offsetHeight;
|
|
4810
|
+
if (itemHeight <= 0) return 10;
|
|
4811
|
+
return Math.max(1, Math.floor(visibleHeight / itemHeight));
|
|
4812
|
+
}, []);
|
|
4616
4813
|
const handleKeyDown = React.useCallback((e) => {
|
|
4617
4814
|
var _a, _b;
|
|
4618
4815
|
if (onKeyDownProp) onKeyDownProp(e);
|
|
4619
4816
|
if (e.defaultPrevented) return;
|
|
4620
4817
|
const s = stateRef.current;
|
|
4818
|
+
if (enableFormatQuery && e.key === "F" && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
|
4819
|
+
e.preventDefault();
|
|
4820
|
+
const formatted = formatQuery(currentValueRef.current);
|
|
4821
|
+
if (formatted !== currentValueRef.current) {
|
|
4822
|
+
applyNewValue(formatted, formatted.length);
|
|
4823
|
+
}
|
|
4824
|
+
return;
|
|
4825
|
+
}
|
|
4621
4826
|
if (editorRef.current && editorRef.current.childNodes.length > 40) {
|
|
4622
4827
|
const sel = window.getSelection();
|
|
4623
4828
|
const hasSelection = sel != null && !sel.isCollapsed;
|
|
@@ -4768,24 +4973,49 @@ function ElasticInput(props) {
|
|
|
4768
4973
|
handleInput();
|
|
4769
4974
|
return;
|
|
4770
4975
|
}
|
|
4976
|
+
keyConsumedByDropdownRef.current = false;
|
|
4771
4977
|
if (s.showDropdown && s.suggestions.length > 0) {
|
|
4772
4978
|
switch (e.key) {
|
|
4773
4979
|
case "ArrowDown":
|
|
4774
4980
|
e.preventDefault();
|
|
4981
|
+
keyConsumedByDropdownRef.current = true;
|
|
4775
4982
|
setSelectedSuggestionIndex((i) => i >= s.suggestions.length - 1 ? 0 : i + 1);
|
|
4776
4983
|
return;
|
|
4777
4984
|
case "ArrowUp":
|
|
4778
4985
|
e.preventDefault();
|
|
4986
|
+
keyConsumedByDropdownRef.current = true;
|
|
4779
4987
|
setSelectedSuggestionIndex((i) => i <= 0 ? s.suggestions.length - 1 : i - 1);
|
|
4780
4988
|
return;
|
|
4781
|
-
case "PageDown":
|
|
4989
|
+
case "PageDown": {
|
|
4782
4990
|
e.preventDefault();
|
|
4783
|
-
|
|
4991
|
+
keyConsumedByDropdownRef.current = true;
|
|
4992
|
+
const pageSize = getDropdownPageSize();
|
|
4993
|
+
setSelectedSuggestionIndex((i) => Math.min(i + pageSize, s.suggestions.length - 1));
|
|
4784
4994
|
return;
|
|
4785
|
-
|
|
4995
|
+
}
|
|
4996
|
+
case "PageUp": {
|
|
4786
4997
|
e.preventDefault();
|
|
4787
|
-
|
|
4998
|
+
keyConsumedByDropdownRef.current = true;
|
|
4999
|
+
const pageSize = getDropdownPageSize();
|
|
5000
|
+
setSelectedSuggestionIndex((i) => Math.max(i - pageSize, 0));
|
|
4788
5001
|
return;
|
|
5002
|
+
}
|
|
5003
|
+
case "Home":
|
|
5004
|
+
if (homeEndKeys && s.selectedSuggestionIndex >= 0) {
|
|
5005
|
+
e.preventDefault();
|
|
5006
|
+
keyConsumedByDropdownRef.current = true;
|
|
5007
|
+
setSelectedSuggestionIndex(0);
|
|
5008
|
+
return;
|
|
5009
|
+
}
|
|
5010
|
+
break;
|
|
5011
|
+
case "End":
|
|
5012
|
+
if (homeEndKeys && s.selectedSuggestionIndex >= 0) {
|
|
5013
|
+
e.preventDefault();
|
|
5014
|
+
keyConsumedByDropdownRef.current = true;
|
|
5015
|
+
setSelectedSuggestionIndex(s.suggestions.length - 1);
|
|
5016
|
+
return;
|
|
5017
|
+
}
|
|
5018
|
+
break;
|
|
4789
5019
|
case "Enter":
|
|
4790
5020
|
if (s.selectedSuggestionIndex >= 0) {
|
|
4791
5021
|
const selected = s.suggestions[s.selectedSuggestionIndex];
|
|
@@ -4889,8 +5119,12 @@ function ElasticInput(props) {
|
|
|
4889
5119
|
if (onSearch) onSearch(currentValueRef.current, s.ast);
|
|
4890
5120
|
return;
|
|
4891
5121
|
}
|
|
4892
|
-
}, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection]);
|
|
5122
|
+
}, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection, homeEndKeys, getDropdownPageSize, enableFormatQuery]);
|
|
4893
5123
|
const handleKeyUp = React.useCallback((e) => {
|
|
5124
|
+
if (keyConsumedByDropdownRef.current) {
|
|
5125
|
+
keyConsumedByDropdownRef.current = false;
|
|
5126
|
+
return;
|
|
5127
|
+
}
|
|
4894
5128
|
const navKeys = ["ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"];
|
|
4895
5129
|
if (navKeys.includes(e.key)) {
|
|
4896
5130
|
if (!editorRef.current) return;
|
|
@@ -5034,6 +5268,9 @@ function ElasticInput(props) {
|
|
|
5034
5268
|
renderSavedSearchItem,
|
|
5035
5269
|
renderDropdownHeader,
|
|
5036
5270
|
cursorContext,
|
|
5271
|
+
listRefCallback: (el) => {
|
|
5272
|
+
dropdownListRef.current = el;
|
|
5273
|
+
},
|
|
5037
5274
|
classNames: classNames ? { dropdown: classNames.dropdown, dropdownHeader: classNames.dropdownHeader, dropdownItem: classNames.dropdownItem } : void 0
|
|
5038
5275
|
}
|
|
5039
5276
|
), showDatePicker && dropdownPosition ? /* @__PURE__ */ React.createElement(
|
|
@@ -5125,141 +5362,6 @@ function walk(node, groupField, out) {
|
|
|
5125
5362
|
break;
|
|
5126
5363
|
}
|
|
5127
5364
|
}
|
|
5128
|
-
const INLINE_MAX_LENGTH = 60;
|
|
5129
|
-
const INDENT = " ";
|
|
5130
|
-
function formatQuery(input) {
|
|
5131
|
-
let ast;
|
|
5132
|
-
if (typeof input === "string") {
|
|
5133
|
-
const tokens = new Lexer(input, { savedSearches: true, historySearch: true }).tokenize();
|
|
5134
|
-
const parser = new Parser(tokens);
|
|
5135
|
-
ast = parser.parse();
|
|
5136
|
-
} else {
|
|
5137
|
-
ast = input;
|
|
5138
|
-
}
|
|
5139
|
-
if (!ast) return typeof input === "string" ? input : "";
|
|
5140
|
-
return printNode(ast, 0);
|
|
5141
|
-
}
|
|
5142
|
-
function inline(node) {
|
|
5143
|
-
switch (node.type) {
|
|
5144
|
-
case "FieldValue": {
|
|
5145
|
-
const val = node.quoted ? `"${node.value}"` : node.value;
|
|
5146
|
-
const op = node.operator === ":" ? ":" : `:${node.operator}`;
|
|
5147
|
-
let s = `${node.field}${op}${val}`;
|
|
5148
|
-
if (node.fuzzy != null) s += `~${node.fuzzy}`;
|
|
5149
|
-
if (node.proximity != null) s += `~${node.proximity}`;
|
|
5150
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5151
|
-
return s;
|
|
5152
|
-
}
|
|
5153
|
-
case "BareTerm": {
|
|
5154
|
-
let s = node.quoted ? `"${node.value}"` : node.value;
|
|
5155
|
-
if (node.fuzzy != null) s += `~${node.fuzzy}`;
|
|
5156
|
-
if (node.proximity != null) s += `~${node.proximity}`;
|
|
5157
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5158
|
-
return s;
|
|
5159
|
-
}
|
|
5160
|
-
case "Range": {
|
|
5161
|
-
const lb = node.lowerInclusive ? "[" : "{";
|
|
5162
|
-
const rb = node.upperInclusive ? "]" : "}";
|
|
5163
|
-
const lower = node.lowerQuoted ? `"${node.lower}"` : node.lower;
|
|
5164
|
-
const upper = node.upperQuoted ? `"${node.upper}"` : node.upper;
|
|
5165
|
-
const range = `${lb}${lower} TO ${upper}${rb}`;
|
|
5166
|
-
return node.field ? `${node.field}:${range}` : range;
|
|
5167
|
-
}
|
|
5168
|
-
case "Regex":
|
|
5169
|
-
return `/${node.pattern}/`;
|
|
5170
|
-
case "SavedSearch":
|
|
5171
|
-
return `#${node.name}`;
|
|
5172
|
-
case "HistoryRef":
|
|
5173
|
-
return `!${node.ref}`;
|
|
5174
|
-
case "Not":
|
|
5175
|
-
return `NOT ${inline(node.expression)}`;
|
|
5176
|
-
case "Group": {
|
|
5177
|
-
let s = `(${inline(node.expression)})`;
|
|
5178
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5179
|
-
return s;
|
|
5180
|
-
}
|
|
5181
|
-
case "FieldGroup": {
|
|
5182
|
-
let s = `${node.field}:(${inline(node.expression)})`;
|
|
5183
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5184
|
-
return s;
|
|
5185
|
-
}
|
|
5186
|
-
case "BooleanExpr":
|
|
5187
|
-
return `${inline(node.left)} ${node.operator} ${inline(node.right)}`;
|
|
5188
|
-
case "Error":
|
|
5189
|
-
return node.value;
|
|
5190
|
-
}
|
|
5191
|
-
}
|
|
5192
|
-
function flattenChain(node) {
|
|
5193
|
-
const op = node.operator;
|
|
5194
|
-
const operands = [];
|
|
5195
|
-
const collect = (n) => {
|
|
5196
|
-
if (n.type === "BooleanExpr" && n.operator === op) {
|
|
5197
|
-
collect(n.left);
|
|
5198
|
-
collect(n.right);
|
|
5199
|
-
} else {
|
|
5200
|
-
operands.push(n);
|
|
5201
|
-
}
|
|
5202
|
-
};
|
|
5203
|
-
collect(node);
|
|
5204
|
-
return { operator: op, operands };
|
|
5205
|
-
}
|
|
5206
|
-
function containsGroups(node) {
|
|
5207
|
-
if (node.type === "Group" || node.type === "FieldGroup") return true;
|
|
5208
|
-
if (node.type === "BooleanExpr") return containsGroups(node.left) || containsGroups(node.right);
|
|
5209
|
-
if (node.type === "Not") return containsGroups(node.expression);
|
|
5210
|
-
return false;
|
|
5211
|
-
}
|
|
5212
|
-
function shouldBreakGroup(expr) {
|
|
5213
|
-
const inlined = inline(expr);
|
|
5214
|
-
if (inlined.length > INLINE_MAX_LENGTH) return true;
|
|
5215
|
-
if (containsGroups(expr)) return true;
|
|
5216
|
-
return false;
|
|
5217
|
-
}
|
|
5218
|
-
function printNode(node, depth) {
|
|
5219
|
-
const pad = INDENT.repeat(depth);
|
|
5220
|
-
switch (node.type) {
|
|
5221
|
-
case "BooleanExpr": {
|
|
5222
|
-
const { operator, operands } = flattenChain(node);
|
|
5223
|
-
const inlined = operands.map((o) => inline(o)).join(` ${operator} `);
|
|
5224
|
-
if (inlined.length <= INLINE_MAX_LENGTH) {
|
|
5225
|
-
return inlined;
|
|
5226
|
-
}
|
|
5227
|
-
const lines = operands.map((operand, i) => {
|
|
5228
|
-
const printed = printNode(operand, depth);
|
|
5229
|
-
return i === 0 ? printed : `${pad}${operator} ${printed}`;
|
|
5230
|
-
});
|
|
5231
|
-
return lines.join("\n");
|
|
5232
|
-
}
|
|
5233
|
-
case "Group": {
|
|
5234
|
-
if (!shouldBreakGroup(node.expression)) {
|
|
5235
|
-
let s2 = `(${inline(node.expression)})`;
|
|
5236
|
-
if (node.boost != null) s2 += `^${node.boost}`;
|
|
5237
|
-
return s2;
|
|
5238
|
-
}
|
|
5239
|
-
const inner = printNode(node.expression, depth + 1);
|
|
5240
|
-
let s = `(
|
|
5241
|
-
${indentLines(inner, depth + 1)}
|
|
5242
|
-
${pad})`;
|
|
5243
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5244
|
-
return s;
|
|
5245
|
-
}
|
|
5246
|
-
case "FieldGroup": {
|
|
5247
|
-
let s = `${node.field}:(${inline(node.expression)})`;
|
|
5248
|
-
if (node.boost != null) s += `^${node.boost}`;
|
|
5249
|
-
return s;
|
|
5250
|
-
}
|
|
5251
|
-
case "Not":
|
|
5252
|
-
return `NOT ${printNode(node.expression, depth)}`;
|
|
5253
|
-
default:
|
|
5254
|
-
return inline(node);
|
|
5255
|
-
}
|
|
5256
|
-
}
|
|
5257
|
-
function indentLines(text, depth) {
|
|
5258
|
-
const pad = INDENT.repeat(depth);
|
|
5259
|
-
return text.split("\n").map((line) => {
|
|
5260
|
-
return line.startsWith(pad) ? line : pad + line;
|
|
5261
|
-
}).join("\n");
|
|
5262
|
-
}
|
|
5263
5365
|
export {
|
|
5264
5366
|
AutocompleteEngine,
|
|
5265
5367
|
DARK_COLORS,
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type { AutocompleteOptions } from './autocomplete/AutocompleteEngine';
|
|
|
10
10
|
export { extractValues } from './utils/extractValues';
|
|
11
11
|
export type { ExtractedValue, ExtractedValueKind } from './utils/extractValues';
|
|
12
12
|
export { formatQuery } from './utils/formatQuery';
|
|
13
|
+
export type { FormatQueryOptions } from './utils/formatQuery';
|
|
13
14
|
export { DEFAULT_COLORS, DARK_COLORS, DEFAULT_STYLES, DARK_STYLES } from './constants';
|
|
14
15
|
export type { ElasticInputProps, ElasticInputAPI, FieldConfig, FieldsSource, FieldType, SavedSearch, HistoryEntry, SuggestionItem, ColorConfig, StyleConfig, ValidateValueContext, ValidationResult, ValidateReturn, TabContext, TabActionResult, DropdownConfig, DropdownOpenContext, DropdownOpenProp, FeaturesConfig, ClassNamesConfig, } from './types';
|
|
15
16
|
export type { Token, TokenType } from './lexer/tokens';
|
package/dist/parser/ast.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -269,6 +269,17 @@ export interface DropdownConfig {
|
|
|
269
269
|
* empty partial. When false, the first item is only pre-selected after the user
|
|
270
270
|
* starts typing a partial match. @default false */
|
|
271
271
|
autoSelect?: boolean;
|
|
272
|
+
/** When true, Home/End keys navigate to the first/last dropdown item while the
|
|
273
|
+
* dropdown is open and an item is already selected. When no item is selected
|
|
274
|
+
* (index = -1), the keys pass through for normal text cursor movement. @default false */
|
|
275
|
+
homeEndKeys?: boolean;
|
|
276
|
+
/** Called when the engine returns zero suggestions. Return a React element to display
|
|
277
|
+
* in the dropdown (e.g. "No results for …"), or null/undefined to hide the dropdown.
|
|
278
|
+
* Not called during async loading (the spinner handles that). */
|
|
279
|
+
renderNoResults?: (context: {
|
|
280
|
+
cursorContext: CursorContext;
|
|
281
|
+
partial: string;
|
|
282
|
+
}) => React.ReactNode | null | undefined;
|
|
272
283
|
}
|
|
273
284
|
/**
|
|
274
285
|
* Feature toggles for optional editing behaviors. All default to false except `multiline`.
|
|
@@ -282,6 +293,8 @@ export interface FeaturesConfig {
|
|
|
282
293
|
expandSelection?: boolean;
|
|
283
294
|
/** Pressing `*` with a single value token selected wraps it in wildcards. @default false */
|
|
284
295
|
wildcardWrap?: boolean;
|
|
296
|
+
/** Enable Alt+Shift+F to pretty-print the query in-place using `formatQuery`. @default false */
|
|
297
|
+
formatQuery?: boolean;
|
|
285
298
|
/** Enable `#name` saved-search syntax and autocomplete. When false, `#` is a regular character. @default false */
|
|
286
299
|
savedSearches?: boolean;
|
|
287
300
|
/** Enable `!query` history-search syntax and autocomplete. When false, `!` is a regular character. @default false */
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { ASTNode } from '../parser/ast';
|
|
2
2
|
|
|
3
|
+
/** Options for `formatQuery` pretty-printing. */
|
|
4
|
+
export interface FormatQueryOptions {
|
|
5
|
+
/** Max length before a line is broken into multiple lines. @default 60 */
|
|
6
|
+
maxLineLength?: number;
|
|
7
|
+
/** Indent string for each nesting level. @default ' ' (2 spaces) */
|
|
8
|
+
indent?: string;
|
|
9
|
+
/** When set, replaces implicit AND (whitespace between terms) with this operator
|
|
10
|
+
* string in the output. By default, implicit AND is preserved as whitespace.
|
|
11
|
+
* @example 'AND' — turns `status:active name:john` into `status:active AND name:john` */
|
|
12
|
+
whitespaceOperator?: string;
|
|
13
|
+
}
|
|
3
14
|
/**
|
|
4
15
|
* Pretty-print an Elasticsearch query string.
|
|
5
16
|
* Accepts a raw query string or a pre-parsed AST node.
|
|
6
17
|
*/
|
|
7
|
-
export declare function formatQuery(input: string | ASTNode): string;
|
|
18
|
+
export declare function formatQuery(input: string | ASTNode, options?: FormatQueryOptions): string;
|