elastic-input 0.3.2 → 0.3.4
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 +10 -0
- package/dist/components/HighlightedContent.d.ts +3 -1
- package/dist/elastic-input.es.js +286 -30
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +50 -17
- package/dist/utils/formatQuery.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -127,6 +127,8 @@ Implicit AND is supported — `status:active level:ERROR` is equivalent to `stat
|
|
|
127
127
|
| `onTab` | `(context) => TabActionResult` | — | Override Tab key behavior (accept/blur/submit) |
|
|
128
128
|
| `validateValue` | `(ctx) => ValidateReturn` | — | Custom validation for all value types |
|
|
129
129
|
| `parseDate` | `(value: string) => Date \| null` | — | Custom date parser for validation and date picker init |
|
|
130
|
+
| `plainModeLength` | `number` | — | Character count at which highlighting, autocomplete, and validation are disabled for performance |
|
|
131
|
+
| `interceptPaste` | `(text, event) => string \| null \| Promise<…>` | — | Transform or cancel pasted text before insertion; supports async |
|
|
130
132
|
|
|
131
133
|
## Field Configuration
|
|
132
134
|
|
|
@@ -348,6 +350,14 @@ const myColors: ColorConfig = {
|
|
|
348
350
|
cursor: '#1f2328',
|
|
349
351
|
dropdownSelected: '#0969da',
|
|
350
352
|
dropdownHover: '#f6f8fa',
|
|
353
|
+
// Per-field-type value colors (overrides fieldValue for typed fields)
|
|
354
|
+
valueTypes: {
|
|
355
|
+
string: '#0550ae',
|
|
356
|
+
number: '#0a3069',
|
|
357
|
+
date: '#8250df',
|
|
358
|
+
boolean: '#cf222e',
|
|
359
|
+
ip: '#116329',
|
|
360
|
+
},
|
|
351
361
|
};
|
|
352
362
|
```
|
|
353
363
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Token } from '../lexer/tokens';
|
|
2
|
-
import { ColorConfig } from '../types';
|
|
2
|
+
import { ColorConfig, FieldType } from '../types';
|
|
3
3
|
|
|
4
4
|
export interface HighlightOptions {
|
|
5
5
|
cursorOffset?: number;
|
|
6
6
|
/** Custom class name appended to every token span. */
|
|
7
7
|
tokenClassName?: string;
|
|
8
|
+
/** Field lookup map for per-type value coloring (keyed by field name and aliases). */
|
|
9
|
+
fieldTypeMap?: Map<string, FieldType>;
|
|
8
10
|
}
|
|
9
11
|
export declare function buildHighlightedHTML(tokens: Token[], colorConfig?: ColorConfig, options?: HighlightOptions): string;
|
package/dist/elastic-input.es.js
CHANGED
|
@@ -1181,7 +1181,7 @@ const DEFAULT_COLORS = {
|
|
|
1181
1181
|
regexText: "#0a3069",
|
|
1182
1182
|
matchedParenBg: "#fff3cd",
|
|
1183
1183
|
warning: "#d4a72c",
|
|
1184
|
-
|
|
1184
|
+
valueTypes: {}
|
|
1185
1185
|
};
|
|
1186
1186
|
const DARK_COLORS = {
|
|
1187
1187
|
fieldName: "#79c0ff",
|
|
@@ -1210,7 +1210,7 @@ const DARK_COLORS = {
|
|
|
1210
1210
|
regexText: "#a5d6ff",
|
|
1211
1211
|
matchedParenBg: "#3d3222",
|
|
1212
1212
|
warning: "#e3b341",
|
|
1213
|
-
|
|
1213
|
+
valueTypes: {}
|
|
1214
1214
|
};
|
|
1215
1215
|
const DEFAULT_STYLES = {
|
|
1216
1216
|
fontFamily: "'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace",
|
|
@@ -2625,9 +2625,60 @@ function buildHighlightedHTML(tokens, colorConfig, options) {
|
|
|
2625
2625
|
const colors = mergeColors(colorConfig);
|
|
2626
2626
|
if (tokens.length === 0) return "";
|
|
2627
2627
|
const parenMatch = (options == null ? void 0 : options.cursorOffset) !== void 0 ? findMatchingParen(tokens, options.cursorOffset) : null;
|
|
2628
|
-
|
|
2628
|
+
const valueTypes = colorConfig == null ? void 0 : colorConfig.valueTypes;
|
|
2629
|
+
const fieldTypeMap = options == null ? void 0 : options.fieldTypeMap;
|
|
2630
|
+
let tokenFieldTypes;
|
|
2631
|
+
if (valueTypes && fieldTypeMap) {
|
|
2632
|
+
tokenFieldTypes = new Array(tokens.length);
|
|
2633
|
+
let pendingFieldName;
|
|
2634
|
+
let sawColon = false;
|
|
2635
|
+
const groupStack = [];
|
|
2636
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2637
|
+
const t = tokens[i];
|
|
2638
|
+
if (t.type === TokenType.FIELD_NAME) {
|
|
2639
|
+
pendingFieldName = t.value;
|
|
2640
|
+
sawColon = false;
|
|
2641
|
+
} else if (t.type === TokenType.COLON && pendingFieldName) {
|
|
2642
|
+
sawColon = true;
|
|
2643
|
+
} else if (t.type === TokenType.WHITESPACE) ;
|
|
2644
|
+
else if (t.type === TokenType.LPAREN) {
|
|
2645
|
+
if (sawColon && pendingFieldName) {
|
|
2646
|
+
groupStack.push(fieldTypeMap.get(pendingFieldName.toLowerCase()));
|
|
2647
|
+
} else {
|
|
2648
|
+
groupStack.push(void 0);
|
|
2649
|
+
}
|
|
2650
|
+
pendingFieldName = void 0;
|
|
2651
|
+
sawColon = false;
|
|
2652
|
+
} else if (t.type === TokenType.RPAREN) {
|
|
2653
|
+
groupStack.pop();
|
|
2654
|
+
pendingFieldName = void 0;
|
|
2655
|
+
sawColon = false;
|
|
2656
|
+
} else if (t.type === TokenType.VALUE || t.type === TokenType.QUOTED_VALUE || t.type === TokenType.RANGE || t.type === TokenType.REGEX || t.type === TokenType.WILDCARD) {
|
|
2657
|
+
if (sawColon && pendingFieldName) {
|
|
2658
|
+
tokenFieldTypes[i] = fieldTypeMap.get(pendingFieldName.toLowerCase());
|
|
2659
|
+
} else if (groupStack.length > 0) {
|
|
2660
|
+
tokenFieldTypes[i] = groupStack[groupStack.length - 1];
|
|
2661
|
+
}
|
|
2662
|
+
pendingFieldName = void 0;
|
|
2663
|
+
sawColon = false;
|
|
2664
|
+
} else if (t.type === TokenType.AND || t.type === TokenType.OR || t.type === TokenType.NOT) {
|
|
2665
|
+
pendingFieldName = void 0;
|
|
2666
|
+
sawColon = false;
|
|
2667
|
+
} else {
|
|
2668
|
+
pendingFieldName = void 0;
|
|
2669
|
+
sawColon = false;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return tokens.map((token, tokenIndex) => {
|
|
2629
2674
|
const colorKey = TOKEN_COLOR_MAP[token.type] || "text";
|
|
2630
|
-
|
|
2675
|
+
let color = colors[colorKey] || colors.text;
|
|
2676
|
+
if (valueTypes && tokenFieldTypes) {
|
|
2677
|
+
const ft = tokenFieldTypes[tokenIndex];
|
|
2678
|
+
if (ft && valueTypes[ft]) {
|
|
2679
|
+
color = valueTypes[ft];
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2631
2682
|
const escapedValue = escapeHTML(token.value);
|
|
2632
2683
|
if (token.type === TokenType.WHITESPACE) {
|
|
2633
2684
|
return escapedValue.replace(/\n/g, "<br>");
|
|
@@ -2801,7 +2852,7 @@ function AutocompleteDropdown({
|
|
|
2801
2852
|
whiteSpace: "normal",
|
|
2802
2853
|
wordBreak: "break-all",
|
|
2803
2854
|
width: "100%"
|
|
2804
|
-
} }, highlightMatch(suggestion.label, suggestion.matchPartial, isSelected)), /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "8px", width: "100%" } }, suggestion.description != null && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-desc", style: { ...getDropdownItemDescStyle(), flex: 1
|
|
2855
|
+
} }, highlightMatch(suggestion.label, suggestion.matchPartial, isSelected)), /* @__PURE__ */ React.createElement("span", { style: { display: "flex", alignItems: "center", gap: "8px", width: "100%" } }, suggestion.description != null && /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-desc", style: { ...getDropdownItemDescStyle(), flex: 1 } }, suggestion.description), /* @__PURE__ */ React.createElement("span", { className: "ei-dropdown-item-type", style: { ...getDropdownItemTypeStyle(isSelected, mergedStyles), marginLeft: "auto" } }, "history")))
|
|
2805
2856
|
);
|
|
2806
2857
|
}
|
|
2807
2858
|
if (suggestion.type === "savedSearch" && renderSavedSearchItem && suggestion.sourceData) {
|
|
@@ -3727,7 +3778,9 @@ function ElasticInput(props) {
|
|
|
3727
3778
|
onBlur: onBlurProp,
|
|
3728
3779
|
onTab: onTabProp,
|
|
3729
3780
|
validateValue,
|
|
3730
|
-
parseDate: parseDateProp
|
|
3781
|
+
parseDate: parseDateProp,
|
|
3782
|
+
plainModeLength,
|
|
3783
|
+
interceptPaste
|
|
3731
3784
|
} = props;
|
|
3732
3785
|
const dropdownOpen = (dropdownConfig == null ? void 0 : dropdownConfig.open) ?? (dropdownConfig == null ? void 0 : dropdownConfig.mode) ?? "always";
|
|
3733
3786
|
const dropdownOpenIsCallback = typeof dropdownOpen === "function";
|
|
@@ -3749,6 +3802,7 @@ function ElasticInput(props) {
|
|
|
3749
3802
|
const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
|
|
3750
3803
|
const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
|
|
3751
3804
|
const renderDropdownHeader = dropdownConfig == null ? void 0 : dropdownConfig.renderHeader;
|
|
3805
|
+
const autoSelect = (dropdownConfig == null ? void 0 : dropdownConfig.autoSelect) ?? false;
|
|
3752
3806
|
const multiline = (featuresConfig == null ? void 0 : featuresConfig.multiline) !== false;
|
|
3753
3807
|
const smartSelectAll = (featuresConfig == null ? void 0 : featuresConfig.smartSelectAll) ?? false;
|
|
3754
3808
|
const expandSelection = (featuresConfig == null ? void 0 : featuresConfig.expandSelection) ?? false;
|
|
@@ -3774,6 +3828,16 @@ function ElasticInput(props) {
|
|
|
3774
3828
|
cancelled = true;
|
|
3775
3829
|
};
|
|
3776
3830
|
}, [fieldsProp]);
|
|
3831
|
+
const fieldTypeMap = React.useMemo(() => {
|
|
3832
|
+
const map = /* @__PURE__ */ new Map();
|
|
3833
|
+
for (const f of resolvedFields) {
|
|
3834
|
+
map.set(f.name.toLowerCase(), f.type);
|
|
3835
|
+
if (f.aliases) {
|
|
3836
|
+
for (const a of f.aliases) map.set(a.toLowerCase(), f.type);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
return map;
|
|
3840
|
+
}, [resolvedFields]);
|
|
3777
3841
|
const editorRef = React.useRef(null);
|
|
3778
3842
|
const [editorEl, setEditorEl] = React.useState(null);
|
|
3779
3843
|
const editorRefCallback = React.useCallback((el) => {
|
|
@@ -3820,6 +3884,9 @@ function ElasticInput(props) {
|
|
|
3820
3884
|
const [validationErrors, setValidationErrors] = React.useState([]);
|
|
3821
3885
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
3822
3886
|
const [isEmpty, setIsEmpty] = React.useState(!currentValueRef.current);
|
|
3887
|
+
const [isPlainMode, setIsPlainMode] = React.useState(
|
|
3888
|
+
plainModeLength != null && currentValueRef.current.length >= plainModeLength
|
|
3889
|
+
);
|
|
3823
3890
|
const [cursorOffset, setCursorOffset] = React.useState(0);
|
|
3824
3891
|
const [selectionEnd, setSelectionEnd] = React.useState(0);
|
|
3825
3892
|
const [autocompleteContext, setAutocompleteContext] = React.useState("");
|
|
@@ -3889,11 +3956,34 @@ function ElasticInput(props) {
|
|
|
3889
3956
|
const HIGHLIGHT_DEBOUNCE_MS = 60;
|
|
3890
3957
|
const applyHighlight = React.useCallback((tokens2, offset) => {
|
|
3891
3958
|
if (!editorRef.current) return;
|
|
3892
|
-
const html = buildHighlightedHTML(tokens2, colors, { cursorOffset: offset, tokenClassName: classNames == null ? void 0 : classNames.token });
|
|
3959
|
+
const html = buildHighlightedHTML(tokens2, colors, { cursorOffset: offset, tokenClassName: classNames == null ? void 0 : classNames.token, fieldTypeMap });
|
|
3893
3960
|
editorRef.current.innerHTML = html;
|
|
3894
3961
|
setCaretCharOffset(editorRef.current, offset);
|
|
3895
3962
|
}, [colors]);
|
|
3896
3963
|
const processInput = React.useCallback((text, updateDropdown) => {
|
|
3964
|
+
const plain = plainModeLength != null && text.length >= plainModeLength;
|
|
3965
|
+
setIsPlainMode(plain);
|
|
3966
|
+
if (plain) {
|
|
3967
|
+
setTokens([]);
|
|
3968
|
+
setAst(null);
|
|
3969
|
+
setValidationErrors([]);
|
|
3970
|
+
setIsEmpty(text.length === 0);
|
|
3971
|
+
setSuggestions([]);
|
|
3972
|
+
setShowDropdown(false);
|
|
3973
|
+
setShowDatePicker(false);
|
|
3974
|
+
if (editorRef.current) {
|
|
3975
|
+
const offset = getCaretCharOffset(editorRef.current);
|
|
3976
|
+
if (editorRef.current.querySelector("span")) {
|
|
3977
|
+
editorRef.current.textContent = text;
|
|
3978
|
+
setCaretCharOffset(editorRef.current, offset);
|
|
3979
|
+
}
|
|
3980
|
+
setCursorOffset(offset);
|
|
3981
|
+
setSelectionEnd(offset);
|
|
3982
|
+
}
|
|
3983
|
+
if (onChange) onChange(text, null);
|
|
3984
|
+
if (onValidationChange) onValidationChange([]);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3897
3987
|
const lexer = new Lexer(text, lexerOptions);
|
|
3898
3988
|
const newTokens = lexer.tokenize();
|
|
3899
3989
|
const parser = new Parser(newTokens);
|
|
@@ -3935,7 +4025,7 @@ function ElasticInput(props) {
|
|
|
3935
4025
|
}
|
|
3936
4026
|
if (onChange) onChange(text, newAst);
|
|
3937
4027
|
if (onValidationChange) onValidationChange(newErrors);
|
|
3938
|
-
}, [colors, onChange, onValidationChange, applyHighlight]);
|
|
4028
|
+
}, [colors, onChange, onValidationChange, applyHighlight, plainModeLength]);
|
|
3939
4029
|
const applyFieldHint = React.useCallback((suggestions2, context) => {
|
|
3940
4030
|
if (!renderFieldHint || context.type !== "FIELD_VALUE" || !context.fieldName) return suggestions2;
|
|
3941
4031
|
const resolved = engineRef.current.resolveField(context.fieldName);
|
|
@@ -4037,7 +4127,7 @@ function ElasticInput(props) {
|
|
|
4037
4127
|
setSuggestions(newSuggestions);
|
|
4038
4128
|
if (!dropdownAlignToInput) setShowDropdown(false);
|
|
4039
4129
|
setShowDatePicker(false);
|
|
4040
|
-
setSelectedSuggestionIndex(result.context.partial ? 0 : -1);
|
|
4130
|
+
setSelectedSuggestionIndex(result.context.partial || autoSelect ? 0 : -1);
|
|
4041
4131
|
setAutocompleteContext(contextType);
|
|
4042
4132
|
showDropdownAtPosition(newSuggestions.length * 32, 300);
|
|
4043
4133
|
} else {
|
|
@@ -4136,7 +4226,7 @@ function ElasticInput(props) {
|
|
|
4136
4226
|
mapped = mapped.slice(0, effectiveMaxSuggestions);
|
|
4137
4227
|
if (mapped.length > 0) {
|
|
4138
4228
|
setSuggestions(mapped);
|
|
4139
|
-
setSelectedSuggestionIndex(partial ? 0 : -1);
|
|
4229
|
+
setSelectedSuggestionIndex(partial || autoSelect ? 0 : -1);
|
|
4140
4230
|
showDropdownAtPosition(mapped.length * 32, 300);
|
|
4141
4231
|
} else {
|
|
4142
4232
|
const syncResult = engineRef.current.getSuggestions(stateRef.current.tokens, stateRef.current.cursorOffset);
|
|
@@ -4146,7 +4236,7 @@ function ElasticInput(props) {
|
|
|
4146
4236
|
);
|
|
4147
4237
|
if (hintSuggestions.length > 0) {
|
|
4148
4238
|
setSuggestions(hintSuggestions);
|
|
4149
|
-
setSelectedSuggestionIndex(syncResult.context.partial ? 0 : -1);
|
|
4239
|
+
setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
|
|
4150
4240
|
showDropdownAtPosition(hintSuggestions.length * 32, 300);
|
|
4151
4241
|
} else {
|
|
4152
4242
|
setShowDropdown(false);
|
|
@@ -4175,7 +4265,7 @@ function ElasticInput(props) {
|
|
|
4175
4265
|
}
|
|
4176
4266
|
}, debounceMs);
|
|
4177
4267
|
}
|
|
4178
|
-
}, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay]);
|
|
4268
|
+
}, [fetchSuggestionsProp, savedSearches, searchHistory, suggestDebounceMs, applyFieldHint, computeDropdownPosition, showDropdownAtPosition, dropdownAlignToInput, dropdownOpen, dropdownOpenIsCallback, dropdownMode, showOperators, effectiveMaxSuggestions, loadingDelay, autoSelect]);
|
|
4179
4269
|
updateSuggestionsRef.current = updateSuggestionsFromTokens;
|
|
4180
4270
|
const closeDropdown = React.useCallback(() => {
|
|
4181
4271
|
var _a;
|
|
@@ -4233,7 +4323,7 @@ function ElasticInput(props) {
|
|
|
4233
4323
|
const syntaxErrors = parser.getErrors().map((e) => ({ message: e.message, start: e.start, end: e.end }));
|
|
4234
4324
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4235
4325
|
if (editorRef.current) {
|
|
4236
|
-
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: newCursorPos, tokenClassName: classNames == null ? void 0 : classNames.token });
|
|
4326
|
+
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: newCursorPos, tokenClassName: classNames == null ? void 0 : classNames.token, fieldTypeMap });
|
|
4237
4327
|
editorRef.current.innerHTML = html;
|
|
4238
4328
|
setCaretCharOffset(editorRef.current, newCursorPos);
|
|
4239
4329
|
}
|
|
@@ -4443,7 +4533,7 @@ function ElasticInput(props) {
|
|
|
4443
4533
|
if (matchKey === prevParenMatchRef.current && !colorsChanged) return;
|
|
4444
4534
|
prevParenMatchRef.current = matchKey;
|
|
4445
4535
|
const savedOffset = getCaretCharOffset(editorRef.current);
|
|
4446
|
-
const html = buildHighlightedHTML(currentTokens, colors, { cursorOffset: effectiveCursor, tokenClassName: classNames == null ? void 0 : classNames.token });
|
|
4536
|
+
const html = buildHighlightedHTML(currentTokens, colors, { cursorOffset: effectiveCursor, tokenClassName: classNames == null ? void 0 : classNames.token, fieldTypeMap });
|
|
4447
4537
|
editorRef.current.innerHTML = html;
|
|
4448
4538
|
setCaretCharOffset(editorRef.current, savedOffset);
|
|
4449
4539
|
}, [cursorOffset, selectionEnd, isFocused, colors]);
|
|
@@ -4505,7 +4595,7 @@ function ElasticInput(props) {
|
|
|
4505
4595
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4506
4596
|
const hasSelection = entry.selStart != null && entry.selStart !== entry.cursorPos;
|
|
4507
4597
|
if (editorRef.current) {
|
|
4508
|
-
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: entry.cursorPos, tokenClassName: classNames == null ? void 0 : classNames.token });
|
|
4598
|
+
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: entry.cursorPos, tokenClassName: classNames == null ? void 0 : classNames.token, fieldTypeMap });
|
|
4509
4599
|
editorRef.current.innerHTML = html;
|
|
4510
4600
|
if (hasSelection) {
|
|
4511
4601
|
setSelectionCharRange(editorRef.current, entry.selStart, entry.cursorPos);
|
|
@@ -4592,7 +4682,7 @@ function ElasticInput(props) {
|
|
|
4592
4682
|
const newAst = parser.parse();
|
|
4593
4683
|
const syntaxErrors = parser.getErrors().map((err) => ({ message: err.message, start: err.start, end: err.end }));
|
|
4594
4684
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4595
|
-
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: newSelEnd, tokenClassName: classNames == null ? void 0 : classNames.token });
|
|
4685
|
+
const html = buildHighlightedHTML(newTokens, colors, { cursorOffset: newSelEnd, tokenClassName: classNames == null ? void 0 : classNames.token, fieldTypeMap });
|
|
4596
4686
|
editorRef.current.innerHTML = html;
|
|
4597
4687
|
setSelectionCharRange(editorRef.current, newSelStart, newSelEnd);
|
|
4598
4688
|
setTokens(newTokens);
|
|
@@ -4682,15 +4772,29 @@ function ElasticInput(props) {
|
|
|
4682
4772
|
switch (e.key) {
|
|
4683
4773
|
case "ArrowDown":
|
|
4684
4774
|
e.preventDefault();
|
|
4685
|
-
setSelectedSuggestionIndex((i) =>
|
|
4775
|
+
setSelectedSuggestionIndex((i) => i >= s.suggestions.length - 1 ? 0 : i + 1);
|
|
4686
4776
|
return;
|
|
4687
4777
|
case "ArrowUp":
|
|
4688
4778
|
e.preventDefault();
|
|
4689
|
-
setSelectedSuggestionIndex((i) =>
|
|
4779
|
+
setSelectedSuggestionIndex((i) => i <= 0 ? s.suggestions.length - 1 : i - 1);
|
|
4780
|
+
return;
|
|
4781
|
+
case "PageDown":
|
|
4782
|
+
e.preventDefault();
|
|
4783
|
+
setSelectedSuggestionIndex((i) => Math.min(i + 10, s.suggestions.length - 1));
|
|
4784
|
+
return;
|
|
4785
|
+
case "PageUp":
|
|
4786
|
+
e.preventDefault();
|
|
4787
|
+
setSelectedSuggestionIndex((i) => Math.max(i - 10, 0));
|
|
4690
4788
|
return;
|
|
4691
4789
|
case "Enter":
|
|
4692
4790
|
if (s.selectedSuggestionIndex >= 0) {
|
|
4693
4791
|
const selected = s.suggestions[s.selectedSuggestionIndex];
|
|
4792
|
+
if (selected.type === "loading" || selected.type === "error") {
|
|
4793
|
+
e.preventDefault();
|
|
4794
|
+
closeDropdown();
|
|
4795
|
+
if (onSearch) onSearch(currentValueRef.current, s.ast);
|
|
4796
|
+
return;
|
|
4797
|
+
}
|
|
4694
4798
|
if (selected.type === "hint" && selected.text !== "#" && selected.text !== "!") {
|
|
4695
4799
|
e.preventDefault();
|
|
4696
4800
|
closeDropdown();
|
|
@@ -4713,7 +4817,10 @@ function ElasticInput(props) {
|
|
|
4713
4817
|
acceptSuggestion(selected, "Enter");
|
|
4714
4818
|
return;
|
|
4715
4819
|
}
|
|
4716
|
-
|
|
4820
|
+
e.preventDefault();
|
|
4821
|
+
closeDropdown();
|
|
4822
|
+
if (onSearch) onSearch(currentValueRef.current, s.ast);
|
|
4823
|
+
return;
|
|
4717
4824
|
case "Tab": {
|
|
4718
4825
|
if (onTabProp) {
|
|
4719
4826
|
e.preventDefault();
|
|
@@ -4799,12 +4906,10 @@ function ElasticInput(props) {
|
|
|
4799
4906
|
requestAnimationFrame(() => {
|
|
4800
4907
|
if (editorRef.current) {
|
|
4801
4908
|
const toks = stateRef.current.tokens;
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
triggerSuggestionsFromNavigation([], 0);
|
|
4807
|
-
}
|
|
4909
|
+
const offset = toks.length > 0 ? getCaretCharOffset(editorRef.current) : 0;
|
|
4910
|
+
setCursorOffset(offset);
|
|
4911
|
+
setSelectionEnd(offset);
|
|
4912
|
+
triggerSuggestionsFromNavigation(toks, offset);
|
|
4808
4913
|
}
|
|
4809
4914
|
});
|
|
4810
4915
|
}, [handleInput, triggerSuggestionsFromNavigation, onFocusProp]);
|
|
@@ -4829,16 +4934,31 @@ function ElasticInput(props) {
|
|
|
4829
4934
|
}
|
|
4830
4935
|
triggerSuggestionsFromNavigation(stateRef.current.tokens, selRange.start);
|
|
4831
4936
|
}, [triggerSuggestionsFromNavigation, closeDropdown]);
|
|
4832
|
-
const
|
|
4833
|
-
e.preventDefault();
|
|
4834
|
-
const pastedText = normalizeTypographicChars(e.clipboardData.getData("text/plain"));
|
|
4937
|
+
const doPaste = React.useCallback((text) => {
|
|
4835
4938
|
if (typingGroupTimerRef.current) {
|
|
4836
4939
|
clearTimeout(typingGroupTimerRef.current);
|
|
4837
4940
|
typingGroupTimerRef.current = null;
|
|
4838
4941
|
}
|
|
4839
|
-
insertTextAtCursor(
|
|
4942
|
+
insertTextAtCursor(text);
|
|
4840
4943
|
handleInput();
|
|
4841
4944
|
}, [handleInput]);
|
|
4945
|
+
const handlePaste = React.useCallback((e) => {
|
|
4946
|
+
e.preventDefault();
|
|
4947
|
+
const pastedText = normalizeTypographicChars(e.clipboardData.getData("text/plain"));
|
|
4948
|
+
if (interceptPaste) {
|
|
4949
|
+
const result = interceptPaste(pastedText, e);
|
|
4950
|
+
if (result != null && typeof (result == null ? void 0 : result.then) === "function") {
|
|
4951
|
+
result.then((transformed) => {
|
|
4952
|
+
if (transformed != null) doPaste(transformed);
|
|
4953
|
+
}).catch(() => {
|
|
4954
|
+
});
|
|
4955
|
+
} else if (result != null) {
|
|
4956
|
+
doPaste(result);
|
|
4957
|
+
}
|
|
4958
|
+
} else {
|
|
4959
|
+
doPaste(pastedText);
|
|
4960
|
+
}
|
|
4961
|
+
}, [interceptPaste, doPaste]);
|
|
4842
4962
|
const handleDateSelect = React.useCallback((dateStr) => {
|
|
4843
4963
|
const s = stateRef.current;
|
|
4844
4964
|
const saved = datePickerReplaceRef.current;
|
|
@@ -5005,6 +5125,141 @@ function walk(node, groupField, out) {
|
|
|
5005
5125
|
break;
|
|
5006
5126
|
}
|
|
5007
5127
|
}
|
|
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
|
+
}
|
|
5008
5263
|
export {
|
|
5009
5264
|
AutocompleteEngine,
|
|
5010
5265
|
DARK_COLORS,
|
|
@@ -5016,5 +5271,6 @@ export {
|
|
|
5016
5271
|
Parser,
|
|
5017
5272
|
Validator,
|
|
5018
5273
|
buildHighlightedHTML,
|
|
5019
|
-
extractValues
|
|
5274
|
+
extractValues,
|
|
5275
|
+
formatQuery
|
|
5020
5276
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { AutocompleteEngine } from './autocomplete/AutocompleteEngine';
|
|
|
9
9
|
export type { AutocompleteOptions } from './autocomplete/AutocompleteEngine';
|
|
10
10
|
export { extractValues } from './utils/extractValues';
|
|
11
11
|
export type { ExtractedValue, ExtractedValueKind } from './utils/extractValues';
|
|
12
|
+
export { formatQuery } from './utils/formatQuery';
|
|
12
13
|
export { DEFAULT_COLORS, DARK_COLORS, DEFAULT_STYLES, DARK_STYLES } from './constants';
|
|
13
14
|
export type { ElasticInputProps, ElasticInputAPI, FieldConfig, FieldsSource, FieldType, SavedSearch, HistoryEntry, SuggestionItem, ColorConfig, StyleConfig, ValidateValueContext, ValidationResult, ValidateReturn, TabContext, TabActionResult, DropdownConfig, DropdownOpenContext, DropdownOpenProp, FeaturesConfig, ClassNamesConfig, } from './types';
|
|
14
15
|
export type { Token, TokenType } from './lexer/tokens';
|
package/dist/types.d.ts
CHANGED
|
@@ -140,8 +140,14 @@ export interface ColorConfig {
|
|
|
140
140
|
matchedParenBg?: string;
|
|
141
141
|
/** Warning-severity squiggly underlines (e.g. ambiguous precedence). */
|
|
142
142
|
warning?: string;
|
|
143
|
-
/**
|
|
144
|
-
|
|
143
|
+
/** Per-field-type value colors. Overrides `fieldValue` for values belonging to a typed field. */
|
|
144
|
+
valueTypes?: {
|
|
145
|
+
string?: string;
|
|
146
|
+
number?: string;
|
|
147
|
+
date?: string;
|
|
148
|
+
boolean?: string;
|
|
149
|
+
ip?: string;
|
|
150
|
+
};
|
|
145
151
|
}
|
|
146
152
|
/**
|
|
147
153
|
* Structural and layout style overrides for the input and dropdown.
|
|
@@ -259,6 +265,10 @@ export interface DropdownConfig {
|
|
|
259
265
|
/** Custom renderer for a header above the suggestion list. Return a React element,
|
|
260
266
|
* or null/undefined for no header. */
|
|
261
267
|
renderHeader?: (context: CursorContext) => React.ReactNode | null | undefined;
|
|
268
|
+
/** Automatically select the first suggestion when the dropdown opens, even with an
|
|
269
|
+
* empty partial. When false, the first item is only pre-selected after the user
|
|
270
|
+
* starts typing a partial match. @default false */
|
|
271
|
+
autoSelect?: boolean;
|
|
262
272
|
}
|
|
263
273
|
/**
|
|
264
274
|
* Feature toggles for optional editing behaviors. All default to false except `multiline`.
|
|
@@ -297,21 +307,6 @@ export interface ElasticInputAPI {
|
|
|
297
307
|
/** Selects a character range in the input. Focuses the input if not already focused. */
|
|
298
308
|
setSelection: (start: number, end: number) => void;
|
|
299
309
|
}
|
|
300
|
-
/**
|
|
301
|
-
* Props for the ElasticInput component.
|
|
302
|
-
*
|
|
303
|
-
* @example
|
|
304
|
-
* ```tsx
|
|
305
|
-
* <ElasticInput
|
|
306
|
-
* fields={[
|
|
307
|
-
* { name: 'status', type: 'string', suggestions: ['active', 'inactive'] },
|
|
308
|
-
* { name: 'price', type: 'number' },
|
|
309
|
-
* ]}
|
|
310
|
-
* onSearch={(query, ast) => console.log('Search:', query)}
|
|
311
|
-
* placeholder="Search..."
|
|
312
|
-
* />
|
|
313
|
-
* ```
|
|
314
|
-
*/
|
|
315
310
|
/** Context passed to the `onTab` callback. */
|
|
316
311
|
export interface TabContext {
|
|
317
312
|
/** The currently selected suggestion, or `null` if nothing is highlighted. */
|
|
@@ -358,6 +353,21 @@ export interface ClassNamesConfig {
|
|
|
358
353
|
}
|
|
359
354
|
/** Field definitions — either a static array or an async loader function. */
|
|
360
355
|
export type FieldsSource = FieldConfig[] | (() => Promise<FieldConfig[]>);
|
|
356
|
+
/**
|
|
357
|
+
* Props for the ElasticInput component.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```tsx
|
|
361
|
+
* <ElasticInput
|
|
362
|
+
* fields={[
|
|
363
|
+
* { name: 'status', type: 'string', suggestions: ['active', 'inactive'] },
|
|
364
|
+
* { name: 'price', type: 'number' },
|
|
365
|
+
* ]}
|
|
366
|
+
* onSearch={(query, ast) => console.log('Search:', query)}
|
|
367
|
+
* placeholder="Search..."
|
|
368
|
+
* />
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
361
371
|
export interface ElasticInputProps {
|
|
362
372
|
/** Field definitions that determine autocomplete, validation, and syntax highlighting. Accepts a static array or an async loader function. */
|
|
363
373
|
fields: FieldsSource;
|
|
@@ -440,4 +450,27 @@ export interface ElasticInputProps {
|
|
|
440
450
|
* The built-in parser handles YYYY-MM-DD, ISO 8601, and `now±Xd` syntax.
|
|
441
451
|
*/
|
|
442
452
|
parseDate?: (value: string) => Date | null;
|
|
453
|
+
/** When the input text reaches this character count, syntax highlighting, autocomplete,
|
|
454
|
+
* and validation are disabled and the input becomes a plain text box. `0` = always plain. */
|
|
455
|
+
plainModeLength?: number;
|
|
456
|
+
/**
|
|
457
|
+
* Intercept paste events before text is inserted. Receives the plain-text clipboard
|
|
458
|
+
* content and the original ClipboardEvent. Return:
|
|
459
|
+
* - A `string` to insert that text instead of the original
|
|
460
|
+
* - `null` to cancel the paste entirely
|
|
461
|
+
* - A `Promise` resolving to either — the component remains fully interactive while
|
|
462
|
+
* the promise is pending (no text is inserted until it resolves)
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```tsx
|
|
466
|
+
* interceptPaste={async (text) => {
|
|
467
|
+
* if (text.includes('\n')) {
|
|
468
|
+
* const choice = await showDialog('Join lines with AND?');
|
|
469
|
+
* return choice ? text.split('\n').join(' AND ') : text;
|
|
470
|
+
* }
|
|
471
|
+
* return text; // pass through unchanged
|
|
472
|
+
* }}
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
interceptPaste?: (text: string, event: React.ClipboardEvent) => string | null | Promise<string | null>;
|
|
443
476
|
}
|