elastic-input 0.3.4 → 0.3.5
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 +33 -0
- package/dist/components/AutocompleteDropdown.d.ts +3 -1
- package/dist/elastic-input.es.js +74 -22
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/utils/formatQuery.d.ts +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -455,6 +455,39 @@ 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
|
+
|
|
458
491
|
## Requirements
|
|
459
492
|
|
|
460
493
|
### 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
|
@@ -2749,6 +2749,7 @@ function AutocompleteDropdown({
|
|
|
2749
2749
|
renderSavedSearchItem,
|
|
2750
2750
|
renderDropdownHeader,
|
|
2751
2751
|
cursorContext,
|
|
2752
|
+
listRefCallback,
|
|
2752
2753
|
classNames
|
|
2753
2754
|
}) {
|
|
2754
2755
|
const portalRef = React.useRef(null);
|
|
@@ -2784,7 +2785,10 @@ function AutocompleteDropdown({
|
|
|
2784
2785
|
left: `${position.left}px`,
|
|
2785
2786
|
...fixedWidth != null ? { width: `${fixedWidth}px`, minWidth: "unset", maxWidth: "unset" } : {}
|
|
2786
2787
|
};
|
|
2787
|
-
const content = /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown", classNames == null ? void 0 : classNames.dropdown), style: dropdownStyle, ref:
|
|
2788
|
+
const content = /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown", classNames == null ? void 0 : classNames.dropdown), style: dropdownStyle, ref: (el) => {
|
|
2789
|
+
listRef.current = el;
|
|
2790
|
+
listRefCallback == null ? void 0 : listRefCallback(el);
|
|
2791
|
+
}, onMouseDown: (e) => e.preventDefault() }, hasHeader && /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown-header", classNames == null ? void 0 : classNames.dropdownHeader), style: {
|
|
2788
2792
|
padding: mergedStyles.dropdownItemPadding || "4px 10px",
|
|
2789
2793
|
fontSize: "11px",
|
|
2790
2794
|
color: mergedColors.placeholder,
|
|
@@ -3803,6 +3807,7 @@ function ElasticInput(props) {
|
|
|
3803
3807
|
const renderSavedSearchItem = dropdownConfig == null ? void 0 : dropdownConfig.renderSavedSearchItem;
|
|
3804
3808
|
const renderDropdownHeader = dropdownConfig == null ? void 0 : dropdownConfig.renderHeader;
|
|
3805
3809
|
const autoSelect = (dropdownConfig == null ? void 0 : dropdownConfig.autoSelect) ?? false;
|
|
3810
|
+
const homeEndKeys = (dropdownConfig == null ? void 0 : dropdownConfig.homeEndKeys) ?? false;
|
|
3806
3811
|
const multiline = (featuresConfig == null ? void 0 : featuresConfig.multiline) !== false;
|
|
3807
3812
|
const smartSelectAll = (featuresConfig == null ? void 0 : featuresConfig.smartSelectAll) ?? false;
|
|
3808
3813
|
const expandSelection = (featuresConfig == null ? void 0 : featuresConfig.expandSelection) ?? false;
|
|
@@ -3845,9 +3850,11 @@ function ElasticInput(props) {
|
|
|
3845
3850
|
setEditorEl(el);
|
|
3846
3851
|
}, []);
|
|
3847
3852
|
const containerRef = React.useRef(null);
|
|
3853
|
+
const dropdownListRef = React.useRef(null);
|
|
3848
3854
|
const currentValueRef = React.useRef(value || defaultValue || "");
|
|
3849
3855
|
const debounceTimerRef = React.useRef(null);
|
|
3850
3856
|
const isComposingRef = React.useRef(false);
|
|
3857
|
+
const keyConsumedByDropdownRef = React.useRef(false);
|
|
3851
3858
|
const undoStackRef = React.useRef(new UndoStack());
|
|
3852
3859
|
const typingGroupTimerRef = React.useRef(null);
|
|
3853
3860
|
const abortControllerRef = React.useRef(null);
|
|
@@ -4613,6 +4620,17 @@ function ElasticInput(props) {
|
|
|
4613
4620
|
if (onChange) onChange(entry.value, newAst);
|
|
4614
4621
|
if (onValidationChange) onValidationChange(newErrors);
|
|
4615
4622
|
}, [colors, onChange, onValidationChange, closeDropdown]);
|
|
4623
|
+
const getDropdownPageSize = React.useCallback(() => {
|
|
4624
|
+
const list = dropdownListRef.current;
|
|
4625
|
+
if (!list) return 10;
|
|
4626
|
+
const visibleHeight = list.clientHeight;
|
|
4627
|
+
const items = list.querySelectorAll(".ei-dropdown-item");
|
|
4628
|
+
const firstItem = items[0];
|
|
4629
|
+
if (!firstItem) return 10;
|
|
4630
|
+
const itemHeight = firstItem.offsetHeight;
|
|
4631
|
+
if (itemHeight <= 0) return 10;
|
|
4632
|
+
return Math.max(1, Math.floor(visibleHeight / itemHeight));
|
|
4633
|
+
}, []);
|
|
4616
4634
|
const handleKeyDown = React.useCallback((e) => {
|
|
4617
4635
|
var _a, _b;
|
|
4618
4636
|
if (onKeyDownProp) onKeyDownProp(e);
|
|
@@ -4768,24 +4786,49 @@ function ElasticInput(props) {
|
|
|
4768
4786
|
handleInput();
|
|
4769
4787
|
return;
|
|
4770
4788
|
}
|
|
4789
|
+
keyConsumedByDropdownRef.current = false;
|
|
4771
4790
|
if (s.showDropdown && s.suggestions.length > 0) {
|
|
4772
4791
|
switch (e.key) {
|
|
4773
4792
|
case "ArrowDown":
|
|
4774
4793
|
e.preventDefault();
|
|
4794
|
+
keyConsumedByDropdownRef.current = true;
|
|
4775
4795
|
setSelectedSuggestionIndex((i) => i >= s.suggestions.length - 1 ? 0 : i + 1);
|
|
4776
4796
|
return;
|
|
4777
4797
|
case "ArrowUp":
|
|
4778
4798
|
e.preventDefault();
|
|
4799
|
+
keyConsumedByDropdownRef.current = true;
|
|
4779
4800
|
setSelectedSuggestionIndex((i) => i <= 0 ? s.suggestions.length - 1 : i - 1);
|
|
4780
4801
|
return;
|
|
4781
|
-
case "PageDown":
|
|
4802
|
+
case "PageDown": {
|
|
4782
4803
|
e.preventDefault();
|
|
4783
|
-
|
|
4804
|
+
keyConsumedByDropdownRef.current = true;
|
|
4805
|
+
const pageSize = getDropdownPageSize();
|
|
4806
|
+
setSelectedSuggestionIndex((i) => Math.min(i + pageSize, s.suggestions.length - 1));
|
|
4784
4807
|
return;
|
|
4785
|
-
|
|
4808
|
+
}
|
|
4809
|
+
case "PageUp": {
|
|
4786
4810
|
e.preventDefault();
|
|
4787
|
-
|
|
4811
|
+
keyConsumedByDropdownRef.current = true;
|
|
4812
|
+
const pageSize = getDropdownPageSize();
|
|
4813
|
+
setSelectedSuggestionIndex((i) => Math.max(i - pageSize, 0));
|
|
4788
4814
|
return;
|
|
4815
|
+
}
|
|
4816
|
+
case "Home":
|
|
4817
|
+
if (homeEndKeys && s.selectedSuggestionIndex >= 0) {
|
|
4818
|
+
e.preventDefault();
|
|
4819
|
+
keyConsumedByDropdownRef.current = true;
|
|
4820
|
+
setSelectedSuggestionIndex(0);
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
break;
|
|
4824
|
+
case "End":
|
|
4825
|
+
if (homeEndKeys && s.selectedSuggestionIndex >= 0) {
|
|
4826
|
+
e.preventDefault();
|
|
4827
|
+
keyConsumedByDropdownRef.current = true;
|
|
4828
|
+
setSelectedSuggestionIndex(s.suggestions.length - 1);
|
|
4829
|
+
return;
|
|
4830
|
+
}
|
|
4831
|
+
break;
|
|
4789
4832
|
case "Enter":
|
|
4790
4833
|
if (s.selectedSuggestionIndex >= 0) {
|
|
4791
4834
|
const selected = s.suggestions[s.selectedSuggestionIndex];
|
|
@@ -4889,8 +4932,12 @@ function ElasticInput(props) {
|
|
|
4889
4932
|
if (onSearch) onSearch(currentValueRef.current, s.ast);
|
|
4890
4933
|
return;
|
|
4891
4934
|
}
|
|
4892
|
-
}, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection]);
|
|
4935
|
+
}, [onSearch, closeDropdown, acceptSuggestion, applyNewValue, restoreUndoEntry, multiline, dropdownOpenIsCallback, dropdownMode, updateSuggestionsFromTokens, onKeyDownProp, onTabProp, smartSelectAll, expandSelection, homeEndKeys, getDropdownPageSize]);
|
|
4893
4936
|
const handleKeyUp = React.useCallback((e) => {
|
|
4937
|
+
if (keyConsumedByDropdownRef.current) {
|
|
4938
|
+
keyConsumedByDropdownRef.current = false;
|
|
4939
|
+
return;
|
|
4940
|
+
}
|
|
4894
4941
|
const navKeys = ["ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"];
|
|
4895
4942
|
if (navKeys.includes(e.key)) {
|
|
4896
4943
|
if (!editorRef.current) return;
|
|
@@ -5034,6 +5081,9 @@ function ElasticInput(props) {
|
|
|
5034
5081
|
renderSavedSearchItem,
|
|
5035
5082
|
renderDropdownHeader,
|
|
5036
5083
|
cursorContext,
|
|
5084
|
+
listRefCallback: (el) => {
|
|
5085
|
+
dropdownListRef.current = el;
|
|
5086
|
+
},
|
|
5037
5087
|
classNames: classNames ? { dropdown: classNames.dropdown, dropdownHeader: classNames.dropdownHeader, dropdownItem: classNames.dropdownItem } : void 0
|
|
5038
5088
|
}
|
|
5039
5089
|
), showDatePicker && dropdownPosition ? /* @__PURE__ */ React.createElement(
|
|
@@ -5125,9 +5175,11 @@ function walk(node, groupField, out) {
|
|
|
5125
5175
|
break;
|
|
5126
5176
|
}
|
|
5127
5177
|
}
|
|
5128
|
-
const
|
|
5129
|
-
const
|
|
5130
|
-
function formatQuery(input) {
|
|
5178
|
+
const DEFAULT_MAX_LINE_LENGTH = 60;
|
|
5179
|
+
const DEFAULT_INDENT = " ";
|
|
5180
|
+
function formatQuery(input, options) {
|
|
5181
|
+
const maxLineLength = (options == null ? void 0 : options.maxLineLength) ?? DEFAULT_MAX_LINE_LENGTH;
|
|
5182
|
+
const indent = (options == null ? void 0 : options.indent) ?? DEFAULT_INDENT;
|
|
5131
5183
|
let ast;
|
|
5132
5184
|
if (typeof input === "string") {
|
|
5133
5185
|
const tokens = new Lexer(input, { savedSearches: true, historySearch: true }).tokenize();
|
|
@@ -5137,7 +5189,7 @@ function formatQuery(input) {
|
|
|
5137
5189
|
ast = input;
|
|
5138
5190
|
}
|
|
5139
5191
|
if (!ast) return typeof input === "string" ? input : "";
|
|
5140
|
-
return printNode(ast, 0);
|
|
5192
|
+
return printNode(ast, 0, maxLineLength, indent);
|
|
5141
5193
|
}
|
|
5142
5194
|
function inline(node) {
|
|
5143
5195
|
switch (node.type) {
|
|
@@ -5209,36 +5261,36 @@ function containsGroups(node) {
|
|
|
5209
5261
|
if (node.type === "Not") return containsGroups(node.expression);
|
|
5210
5262
|
return false;
|
|
5211
5263
|
}
|
|
5212
|
-
function shouldBreakGroup(expr) {
|
|
5264
|
+
function shouldBreakGroup(expr, maxLineLength) {
|
|
5213
5265
|
const inlined = inline(expr);
|
|
5214
|
-
if (inlined.length >
|
|
5266
|
+
if (inlined.length > maxLineLength) return true;
|
|
5215
5267
|
if (containsGroups(expr)) return true;
|
|
5216
5268
|
return false;
|
|
5217
5269
|
}
|
|
5218
|
-
function printNode(node, depth) {
|
|
5219
|
-
const pad =
|
|
5270
|
+
function printNode(node, depth, maxLineLength, indent) {
|
|
5271
|
+
const pad = indent.repeat(depth);
|
|
5220
5272
|
switch (node.type) {
|
|
5221
5273
|
case "BooleanExpr": {
|
|
5222
5274
|
const { operator, operands } = flattenChain(node);
|
|
5223
5275
|
const inlined = operands.map((o) => inline(o)).join(` ${operator} `);
|
|
5224
|
-
if (inlined.length <=
|
|
5276
|
+
if (inlined.length <= maxLineLength) {
|
|
5225
5277
|
return inlined;
|
|
5226
5278
|
}
|
|
5227
5279
|
const lines = operands.map((operand, i) => {
|
|
5228
|
-
const printed = printNode(operand, depth);
|
|
5280
|
+
const printed = printNode(operand, depth, maxLineLength, indent);
|
|
5229
5281
|
return i === 0 ? printed : `${pad}${operator} ${printed}`;
|
|
5230
5282
|
});
|
|
5231
5283
|
return lines.join("\n");
|
|
5232
5284
|
}
|
|
5233
5285
|
case "Group": {
|
|
5234
|
-
if (!shouldBreakGroup(node.expression)) {
|
|
5286
|
+
if (!shouldBreakGroup(node.expression, maxLineLength)) {
|
|
5235
5287
|
let s2 = `(${inline(node.expression)})`;
|
|
5236
5288
|
if (node.boost != null) s2 += `^${node.boost}`;
|
|
5237
5289
|
return s2;
|
|
5238
5290
|
}
|
|
5239
|
-
const inner = printNode(node.expression, depth + 1);
|
|
5291
|
+
const inner = printNode(node.expression, depth + 1, maxLineLength, indent);
|
|
5240
5292
|
let s = `(
|
|
5241
|
-
${indentLines(inner, depth + 1)}
|
|
5293
|
+
${indentLines(inner, depth + 1, indent)}
|
|
5242
5294
|
${pad})`;
|
|
5243
5295
|
if (node.boost != null) s += `^${node.boost}`;
|
|
5244
5296
|
return s;
|
|
@@ -5249,13 +5301,13 @@ ${pad})`;
|
|
|
5249
5301
|
return s;
|
|
5250
5302
|
}
|
|
5251
5303
|
case "Not":
|
|
5252
|
-
return `NOT ${printNode(node.expression, depth)}`;
|
|
5304
|
+
return `NOT ${printNode(node.expression, depth, maxLineLength, indent)}`;
|
|
5253
5305
|
default:
|
|
5254
5306
|
return inline(node);
|
|
5255
5307
|
}
|
|
5256
5308
|
}
|
|
5257
|
-
function indentLines(text, depth) {
|
|
5258
|
-
const pad =
|
|
5309
|
+
function indentLines(text, depth, indent) {
|
|
5310
|
+
const pad = indent.repeat(depth);
|
|
5259
5311
|
return text.split("\n").map((line) => {
|
|
5260
5312
|
return line.startsWith(pad) ? line : pad + line;
|
|
5261
5313
|
}).join("\n");
|
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/types.d.ts
CHANGED
|
@@ -269,6 +269,10 @@ 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;
|
|
272
276
|
}
|
|
273
277
|
/**
|
|
274
278
|
* Feature toggles for optional editing behaviors. All default to false except `multiline`.
|
|
@@ -1,7 +1,14 @@
|
|
|
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
|
+
}
|
|
3
10
|
/**
|
|
4
11
|
* Pretty-print an Elasticsearch query string.
|
|
5
12
|
* Accepts a raw query string or a pre-parsed AST node.
|
|
6
13
|
*/
|
|
7
|
-
export declare function formatQuery(input: string | ASTNode): string;
|
|
14
|
+
export declare function formatQuery(input: string | ASTNode, options?: FormatQueryOptions): string;
|