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 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 {};
@@ -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: listRef, onMouseDown: (e) => e.preventDefault() }, hasHeader && /* @__PURE__ */ React.createElement("div", { className: cx("ei-dropdown-header", classNames == null ? void 0 : classNames.dropdownHeader), style: {
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
- setSelectedSuggestionIndex((i) => Math.min(i + 10, s.suggestions.length - 1));
4804
+ keyConsumedByDropdownRef.current = true;
4805
+ const pageSize = getDropdownPageSize();
4806
+ setSelectedSuggestionIndex((i) => Math.min(i + pageSize, s.suggestions.length - 1));
4784
4807
  return;
4785
- case "PageUp":
4808
+ }
4809
+ case "PageUp": {
4786
4810
  e.preventDefault();
4787
- setSelectedSuggestionIndex((i) => Math.max(i - 10, 0));
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 INLINE_MAX_LENGTH = 60;
5129
- const INDENT = " ";
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 > INLINE_MAX_LENGTH) return true;
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 = INDENT.repeat(depth);
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 <= INLINE_MAX_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 = INDENT.repeat(depth);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elastic-input",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Syntax-aware smart autocomplete input for Elastic query syntax",
5
5
  "license": "MIT",
6
6
  "repository": {