elastic-input 0.3.3 → 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 +277 -28
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +50 -15
- 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
|
@@ -1180,7 +1180,8 @@ const DEFAULT_COLORS = {
|
|
|
1180
1180
|
regexAlternation: "#8250df",
|
|
1181
1181
|
regexText: "#0a3069",
|
|
1182
1182
|
matchedParenBg: "#fff3cd",
|
|
1183
|
-
warning: "#d4a72c"
|
|
1183
|
+
warning: "#d4a72c",
|
|
1184
|
+
valueTypes: {}
|
|
1184
1185
|
};
|
|
1185
1186
|
const DARK_COLORS = {
|
|
1186
1187
|
fieldName: "#79c0ff",
|
|
@@ -1208,7 +1209,8 @@ const DARK_COLORS = {
|
|
|
1208
1209
|
regexAlternation: "#d2a8ff",
|
|
1209
1210
|
regexText: "#a5d6ff",
|
|
1210
1211
|
matchedParenBg: "#3d3222",
|
|
1211
|
-
warning: "#e3b341"
|
|
1212
|
+
warning: "#e3b341",
|
|
1213
|
+
valueTypes: {}
|
|
1212
1214
|
};
|
|
1213
1215
|
const DEFAULT_STYLES = {
|
|
1214
1216
|
fontFamily: "'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace",
|
|
@@ -2623,9 +2625,60 @@ function buildHighlightedHTML(tokens, colorConfig, options) {
|
|
|
2623
2625
|
const colors = mergeColors(colorConfig);
|
|
2624
2626
|
if (tokens.length === 0) return "";
|
|
2625
2627
|
const parenMatch = (options == null ? void 0 : options.cursorOffset) !== void 0 ? findMatchingParen(tokens, options.cursorOffset) : null;
|
|
2626
|
-
|
|
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) => {
|
|
2627
2674
|
const colorKey = TOKEN_COLOR_MAP[token.type] || "text";
|
|
2628
|
-
|
|
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
|
+
}
|
|
2629
2682
|
const escapedValue = escapeHTML(token.value);
|
|
2630
2683
|
if (token.type === TokenType.WHITESPACE) {
|
|
2631
2684
|
return escapedValue.replace(/\n/g, "<br>");
|
|
@@ -3725,7 +3778,9 @@ function ElasticInput(props) {
|
|
|
3725
3778
|
onBlur: onBlurProp,
|
|
3726
3779
|
onTab: onTabProp,
|
|
3727
3780
|
validateValue,
|
|
3728
|
-
parseDate: parseDateProp
|
|
3781
|
+
parseDate: parseDateProp,
|
|
3782
|
+
plainModeLength,
|
|
3783
|
+
interceptPaste
|
|
3729
3784
|
} = props;
|
|
3730
3785
|
const dropdownOpen = (dropdownConfig == null ? void 0 : dropdownConfig.open) ?? (dropdownConfig == null ? void 0 : dropdownConfig.mode) ?? "always";
|
|
3731
3786
|
const dropdownOpenIsCallback = typeof dropdownOpen === "function";
|
|
@@ -3747,6 +3802,7 @@ function ElasticInput(props) {
|
|
|
3747
3802
|
const renderHistoryItem = dropdownConfig == null ? void 0 : dropdownConfig.renderHistoryItem;
|
|
3748
3803
|
const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
|
|
3749
3804
|
const renderDropdownHeader = dropdownConfig == null ? void 0 : dropdownConfig.renderHeader;
|
|
3805
|
+
const autoSelect = (dropdownConfig == null ? void 0 : dropdownConfig.autoSelect) ?? false;
|
|
3750
3806
|
const multiline = (featuresConfig == null ? void 0 : featuresConfig.multiline) !== false;
|
|
3751
3807
|
const smartSelectAll = (featuresConfig == null ? void 0 : featuresConfig.smartSelectAll) ?? false;
|
|
3752
3808
|
const expandSelection = (featuresConfig == null ? void 0 : featuresConfig.expandSelection) ?? false;
|
|
@@ -3772,6 +3828,16 @@ function ElasticInput(props) {
|
|
|
3772
3828
|
cancelled = true;
|
|
3773
3829
|
};
|
|
3774
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]);
|
|
3775
3841
|
const editorRef = React.useRef(null);
|
|
3776
3842
|
const [editorEl, setEditorEl] = React.useState(null);
|
|
3777
3843
|
const editorRefCallback = React.useCallback((el) => {
|
|
@@ -3818,6 +3884,9 @@ function ElasticInput(props) {
|
|
|
3818
3884
|
const [validationErrors, setValidationErrors] = React.useState([]);
|
|
3819
3885
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
3820
3886
|
const [isEmpty, setIsEmpty] = React.useState(!currentValueRef.current);
|
|
3887
|
+
const [isPlainMode, setIsPlainMode] = React.useState(
|
|
3888
|
+
plainModeLength != null && currentValueRef.current.length >= plainModeLength
|
|
3889
|
+
);
|
|
3821
3890
|
const [cursorOffset, setCursorOffset] = React.useState(0);
|
|
3822
3891
|
const [selectionEnd, setSelectionEnd] = React.useState(0);
|
|
3823
3892
|
const [autocompleteContext, setAutocompleteContext] = React.useState("");
|
|
@@ -3887,11 +3956,34 @@ function ElasticInput(props) {
|
|
|
3887
3956
|
const HIGHLIGHT_DEBOUNCE_MS = 60;
|
|
3888
3957
|
const applyHighlight = React.useCallback((tokens2, offset) => {
|
|
3889
3958
|
if (!editorRef.current) return;
|
|
3890
|
-
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 });
|
|
3891
3960
|
editorRef.current.innerHTML = html;
|
|
3892
3961
|
setCaretCharOffset(editorRef.current, offset);
|
|
3893
3962
|
}, [colors]);
|
|
3894
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
|
+
}
|
|
3895
3987
|
const lexer = new Lexer(text, lexerOptions);
|
|
3896
3988
|
const newTokens = lexer.tokenize();
|
|
3897
3989
|
const parser = new Parser(newTokens);
|
|
@@ -3933,7 +4025,7 @@ function ElasticInput(props) {
|
|
|
3933
4025
|
}
|
|
3934
4026
|
if (onChange) onChange(text, newAst);
|
|
3935
4027
|
if (onValidationChange) onValidationChange(newErrors);
|
|
3936
|
-
}, [colors, onChange, onValidationChange, applyHighlight]);
|
|
4028
|
+
}, [colors, onChange, onValidationChange, applyHighlight, plainModeLength]);
|
|
3937
4029
|
const applyFieldHint = React.useCallback((suggestions2, context) => {
|
|
3938
4030
|
if (!renderFieldHint || context.type !== "FIELD_VALUE" || !context.fieldName) return suggestions2;
|
|
3939
4031
|
const resolved = engineRef.current.resolveField(context.fieldName);
|
|
@@ -4035,7 +4127,7 @@ function ElasticInput(props) {
|
|
|
4035
4127
|
setSuggestions(newSuggestions);
|
|
4036
4128
|
if (!dropdownAlignToInput) setShowDropdown(false);
|
|
4037
4129
|
setShowDatePicker(false);
|
|
4038
|
-
setSelectedSuggestionIndex(result.context.partial ? 0 : -1);
|
|
4130
|
+
setSelectedSuggestionIndex(result.context.partial || autoSelect ? 0 : -1);
|
|
4039
4131
|
setAutocompleteContext(contextType);
|
|
4040
4132
|
showDropdownAtPosition(newSuggestions.length * 32, 300);
|
|
4041
4133
|
} else {
|
|
@@ -4134,7 +4226,7 @@ function ElasticInput(props) {
|
|
|
4134
4226
|
mapped = mapped.slice(0, effectiveMaxSuggestions);
|
|
4135
4227
|
if (mapped.length > 0) {
|
|
4136
4228
|
setSuggestions(mapped);
|
|
4137
|
-
setSelectedSuggestionIndex(partial ? 0 : -1);
|
|
4229
|
+
setSelectedSuggestionIndex(partial || autoSelect ? 0 : -1);
|
|
4138
4230
|
showDropdownAtPosition(mapped.length * 32, 300);
|
|
4139
4231
|
} else {
|
|
4140
4232
|
const syncResult = engineRef.current.getSuggestions(stateRef.current.tokens, stateRef.current.cursorOffset);
|
|
@@ -4144,7 +4236,7 @@ function ElasticInput(props) {
|
|
|
4144
4236
|
);
|
|
4145
4237
|
if (hintSuggestions.length > 0) {
|
|
4146
4238
|
setSuggestions(hintSuggestions);
|
|
4147
|
-
setSelectedSuggestionIndex(syncResult.context.partial ? 0 : -1);
|
|
4239
|
+
setSelectedSuggestionIndex(syncResult.context.partial || autoSelect ? 0 : -1);
|
|
4148
4240
|
showDropdownAtPosition(hintSuggestions.length * 32, 300);
|
|
4149
4241
|
} else {
|
|
4150
4242
|
setShowDropdown(false);
|
|
@@ -4173,7 +4265,7 @@ function ElasticInput(props) {
|
|
|
4173
4265
|
}
|
|
4174
4266
|
}, debounceMs);
|
|
4175
4267
|
}
|
|
4176
|
-
}, [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]);
|
|
4177
4269
|
updateSuggestionsRef.current = updateSuggestionsFromTokens;
|
|
4178
4270
|
const closeDropdown = React.useCallback(() => {
|
|
4179
4271
|
var _a;
|
|
@@ -4231,7 +4323,7 @@ function ElasticInput(props) {
|
|
|
4231
4323
|
const syntaxErrors = parser.getErrors().map((e) => ({ message: e.message, start: e.start, end: e.end }));
|
|
4232
4324
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4233
4325
|
if (editorRef.current) {
|
|
4234
|
-
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 });
|
|
4235
4327
|
editorRef.current.innerHTML = html;
|
|
4236
4328
|
setCaretCharOffset(editorRef.current, newCursorPos);
|
|
4237
4329
|
}
|
|
@@ -4441,7 +4533,7 @@ function ElasticInput(props) {
|
|
|
4441
4533
|
if (matchKey === prevParenMatchRef.current && !colorsChanged) return;
|
|
4442
4534
|
prevParenMatchRef.current = matchKey;
|
|
4443
4535
|
const savedOffset = getCaretCharOffset(editorRef.current);
|
|
4444
|
-
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 });
|
|
4445
4537
|
editorRef.current.innerHTML = html;
|
|
4446
4538
|
setCaretCharOffset(editorRef.current, savedOffset);
|
|
4447
4539
|
}, [cursorOffset, selectionEnd, isFocused, colors]);
|
|
@@ -4503,7 +4595,7 @@ function ElasticInput(props) {
|
|
|
4503
4595
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4504
4596
|
const hasSelection = entry.selStart != null && entry.selStart !== entry.cursorPos;
|
|
4505
4597
|
if (editorRef.current) {
|
|
4506
|
-
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 });
|
|
4507
4599
|
editorRef.current.innerHTML = html;
|
|
4508
4600
|
if (hasSelection) {
|
|
4509
4601
|
setSelectionCharRange(editorRef.current, entry.selStart, entry.cursorPos);
|
|
@@ -4590,7 +4682,7 @@ function ElasticInput(props) {
|
|
|
4590
4682
|
const newAst = parser.parse();
|
|
4591
4683
|
const syntaxErrors = parser.getErrors().map((err) => ({ message: err.message, start: err.start, end: err.end }));
|
|
4592
4684
|
const newErrors = [...syntaxErrors, ...validatorRef.current.validate(newAst, validateValueRef.current, parseDateProp)];
|
|
4593
|
-
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 });
|
|
4594
4686
|
editorRef.current.innerHTML = html;
|
|
4595
4687
|
setSelectionCharRange(editorRef.current, newSelStart, newSelEnd);
|
|
4596
4688
|
setTokens(newTokens);
|
|
@@ -4680,11 +4772,19 @@ function ElasticInput(props) {
|
|
|
4680
4772
|
switch (e.key) {
|
|
4681
4773
|
case "ArrowDown":
|
|
4682
4774
|
e.preventDefault();
|
|
4683
|
-
setSelectedSuggestionIndex((i) =>
|
|
4775
|
+
setSelectedSuggestionIndex((i) => i >= s.suggestions.length - 1 ? 0 : i + 1);
|
|
4684
4776
|
return;
|
|
4685
4777
|
case "ArrowUp":
|
|
4686
4778
|
e.preventDefault();
|
|
4687
|
-
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));
|
|
4688
4788
|
return;
|
|
4689
4789
|
case "Enter":
|
|
4690
4790
|
if (s.selectedSuggestionIndex >= 0) {
|
|
@@ -4806,12 +4906,10 @@ function ElasticInput(props) {
|
|
|
4806
4906
|
requestAnimationFrame(() => {
|
|
4807
4907
|
if (editorRef.current) {
|
|
4808
4908
|
const toks = stateRef.current.tokens;
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
triggerSuggestionsFromNavigation([], 0);
|
|
4814
|
-
}
|
|
4909
|
+
const offset = toks.length > 0 ? getCaretCharOffset(editorRef.current) : 0;
|
|
4910
|
+
setCursorOffset(offset);
|
|
4911
|
+
setSelectionEnd(offset);
|
|
4912
|
+
triggerSuggestionsFromNavigation(toks, offset);
|
|
4815
4913
|
}
|
|
4816
4914
|
});
|
|
4817
4915
|
}, [handleInput, triggerSuggestionsFromNavigation, onFocusProp]);
|
|
@@ -4836,16 +4934,31 @@ function ElasticInput(props) {
|
|
|
4836
4934
|
}
|
|
4837
4935
|
triggerSuggestionsFromNavigation(stateRef.current.tokens, selRange.start);
|
|
4838
4936
|
}, [triggerSuggestionsFromNavigation, closeDropdown]);
|
|
4839
|
-
const
|
|
4840
|
-
e.preventDefault();
|
|
4841
|
-
const pastedText = normalizeTypographicChars(e.clipboardData.getData("text/plain"));
|
|
4937
|
+
const doPaste = React.useCallback((text) => {
|
|
4842
4938
|
if (typingGroupTimerRef.current) {
|
|
4843
4939
|
clearTimeout(typingGroupTimerRef.current);
|
|
4844
4940
|
typingGroupTimerRef.current = null;
|
|
4845
4941
|
}
|
|
4846
|
-
insertTextAtCursor(
|
|
4942
|
+
insertTextAtCursor(text);
|
|
4847
4943
|
handleInput();
|
|
4848
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]);
|
|
4849
4962
|
const handleDateSelect = React.useCallback((dateStr) => {
|
|
4850
4963
|
const s = stateRef.current;
|
|
4851
4964
|
const saved = datePickerReplaceRef.current;
|
|
@@ -5012,6 +5125,141 @@ function walk(node, groupField, out) {
|
|
|
5012
5125
|
break;
|
|
5013
5126
|
}
|
|
5014
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
|
+
}
|
|
5015
5263
|
export {
|
|
5016
5264
|
AutocompleteEngine,
|
|
5017
5265
|
DARK_COLORS,
|
|
@@ -5023,5 +5271,6 @@ export {
|
|
|
5023
5271
|
Parser,
|
|
5024
5272
|
Validator,
|
|
5025
5273
|
buildHighlightedHTML,
|
|
5026
|
-
extractValues
|
|
5274
|
+
extractValues,
|
|
5275
|
+
formatQuery
|
|
5027
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,6 +140,14 @@ export interface ColorConfig {
|
|
|
140
140
|
matchedParenBg?: string;
|
|
141
141
|
/** Warning-severity squiggly underlines (e.g. ambiguous precedence). */
|
|
142
142
|
warning?: string;
|
|
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
|
+
};
|
|
143
151
|
}
|
|
144
152
|
/**
|
|
145
153
|
* Structural and layout style overrides for the input and dropdown.
|
|
@@ -257,6 +265,10 @@ export interface DropdownConfig {
|
|
|
257
265
|
/** Custom renderer for a header above the suggestion list. Return a React element,
|
|
258
266
|
* or null/undefined for no header. */
|
|
259
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;
|
|
260
272
|
}
|
|
261
273
|
/**
|
|
262
274
|
* Feature toggles for optional editing behaviors. All default to false except `multiline`.
|
|
@@ -295,21 +307,6 @@ export interface ElasticInputAPI {
|
|
|
295
307
|
/** Selects a character range in the input. Focuses the input if not already focused. */
|
|
296
308
|
setSelection: (start: number, end: number) => void;
|
|
297
309
|
}
|
|
298
|
-
/**
|
|
299
|
-
* Props for the ElasticInput component.
|
|
300
|
-
*
|
|
301
|
-
* @example
|
|
302
|
-
* ```tsx
|
|
303
|
-
* <ElasticInput
|
|
304
|
-
* fields={[
|
|
305
|
-
* { name: 'status', type: 'string', suggestions: ['active', 'inactive'] },
|
|
306
|
-
* { name: 'price', type: 'number' },
|
|
307
|
-
* ]}
|
|
308
|
-
* onSearch={(query, ast) => console.log('Search:', query)}
|
|
309
|
-
* placeholder="Search..."
|
|
310
|
-
* />
|
|
311
|
-
* ```
|
|
312
|
-
*/
|
|
313
310
|
/** Context passed to the `onTab` callback. */
|
|
314
311
|
export interface TabContext {
|
|
315
312
|
/** The currently selected suggestion, or `null` if nothing is highlighted. */
|
|
@@ -356,6 +353,21 @@ export interface ClassNamesConfig {
|
|
|
356
353
|
}
|
|
357
354
|
/** Field definitions — either a static array or an async loader function. */
|
|
358
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
|
+
*/
|
|
359
371
|
export interface ElasticInputProps {
|
|
360
372
|
/** Field definitions that determine autocomplete, validation, and syntax highlighting. Accepts a static array or an async loader function. */
|
|
361
373
|
fields: FieldsSource;
|
|
@@ -438,4 +450,27 @@ export interface ElasticInputProps {
|
|
|
438
450
|
* The built-in parser handles YYYY-MM-DD, ISO 8601, and `now±Xd` syntax.
|
|
439
451
|
*/
|
|
440
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>;
|
|
441
476
|
}
|