@wallarm-org/design-system 0.20.0 → 0.21.0

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.
Files changed (56) hide show
  1. package/dist/components/FilterInput/FilterInput.d.ts +0 -25
  2. package/dist/components/FilterInput/FilterInput.js +48 -6
  3. package/dist/components/FilterInput/FilterInputContext/types.d.ts +2 -0
  4. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -1
  5. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -2
  6. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.d.ts +1 -1
  7. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +8 -0
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +4 -2
  9. package/dist/components/FilterInput/FilterInputField/FilterInputSearch.d.ts +0 -1
  10. package/dist/components/FilterInput/FilterInputField/FilterInputSearch.js +4 -3
  11. package/dist/components/FilterInput/FilterInputField/classes.js +1 -1
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js +2 -1
  13. package/dist/components/FilterInput/hooks/index.d.ts +1 -0
  14. package/dist/components/FilterInput/hooks/index.js +2 -1
  15. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +1 -0
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +2 -1
  17. package/dist/components/FilterInput/hooks/useFilterInputSelection/index.d.ts +1 -0
  18. package/dist/components/FilterInput/hooks/useFilterInputSelection/index.js +2 -0
  19. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.d.ts +3 -0
  20. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.js +4 -0
  21. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.d.ts +8 -0
  22. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.js +34 -0
  23. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.d.ts +3 -0
  24. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.js +4 -0
  25. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.d.ts +3 -0
  26. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.js +15 -0
  27. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.d.ts +10 -0
  28. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.js +44 -0
  29. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts +25 -0
  30. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.js +54 -0
  31. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.d.ts +19 -0
  32. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.js +77 -0
  33. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.d.ts +14 -0
  34. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.js +40 -0
  35. package/dist/components/FilterInput/index.d.ts +1 -0
  36. package/dist/components/FilterInput/index.js +2 -1
  37. package/dist/components/FilterInput/lib/index.d.ts +2 -0
  38. package/dist/components/FilterInput/lib/index.js +3 -1
  39. package/dist/components/FilterInput/lib/parseExpression/error.d.ts +6 -0
  40. package/dist/components/FilterInput/lib/parseExpression/error.js +6 -0
  41. package/dist/components/FilterInput/lib/parseExpression/index.d.ts +2 -0
  42. package/dist/components/FilterInput/lib/parseExpression/index.js +3 -0
  43. package/dist/components/FilterInput/lib/parseExpression/parseExpression.d.ts +8 -0
  44. package/dist/components/FilterInput/lib/parseExpression/parseExpression.js +21 -0
  45. package/dist/components/FilterInput/lib/parseExpression/parser.d.ts +4 -0
  46. package/dist/components/FilterInput/lib/parseExpression/parser.js +146 -0
  47. package/dist/components/FilterInput/lib/parseExpression/tokenizer.d.ts +8 -0
  48. package/dist/components/FilterInput/lib/parseExpression/tokenizer.js +101 -0
  49. package/dist/components/FilterInput/lib/parseExpression/types.d.ts +7 -0
  50. package/dist/components/FilterInput/lib/parseExpression/types.js +0 -0
  51. package/dist/components/FilterInput/lib/parseExpression/validators.d.ts +5 -0
  52. package/dist/components/FilterInput/lib/parseExpression/validators.js +28 -0
  53. package/dist/components/FilterInput/lib/serializeExpression.d.ts +6 -0
  54. package/dist/components/FilterInput/lib/serializeExpression.js +36 -0
  55. package/dist/metadata/components.json +3 -9
  56. package/package.json +1 -1
@@ -1,36 +1,11 @@
1
1
  import type { FC, HTMLAttributes } from 'react';
2
2
  import type { ExprNode, FieldMetadata } from './types';
3
3
  export interface FilterInputProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onChange'> {
4
- /**
5
- * Available fields from backend API config
6
- */
7
4
  fields?: FieldMetadata[];
8
- /**
9
- * Current filter expression value (controlled mode)
10
- */
11
5
  value?: ExprNode | null;
12
- /**
13
- * Callback when filter expression changes
14
- */
15
6
  onChange?: (expression: ExprNode | null) => void;
16
- /**
17
- * Placeholder text to display when field is empty
18
- * @default "Type to filter..."
19
- */
20
7
  placeholder?: string;
21
- /**
22
- * Whether the field has a validation error
23
- */
24
8
  error?: boolean;
25
- /**
26
- * Whether to show the keyboard hint (⌘K or Ctrl+K)
27
- */
28
9
  showKeyboardHint?: boolean;
29
10
  }
30
- /**
31
- * FilterInput - Self-contained filter component.
32
- * Handles autocomplete flow (field → operator → value), chip creation, and expression management.
33
- * Supports multiple conditions joined by AND/OR connectors.
34
- * Just pass `fields` config from backend API and it works.
35
- */
36
11
  export declare const FilterInput: FC<FilterInputProps>;
@@ -1,16 +1,21 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo, useRef } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef } from "react";
3
3
  import { cn } from "../../utils/cn.js";
4
4
  import { FilterInputProvider, useFilterInputContextValue } from "./FilterInputContext/index.js";
5
5
  import { FilterInputErrors, parseFilterInputErrors } from "./FilterInputErrors/index.js";
6
6
  import { FilterInputField } from "./FilterInputField/index.js";
7
7
  import { FilterInputMenu } from "./FilterInputMenu/FilterInputMenu.js";
8
- import { useFilterInputAutocomplete, useFilterInputExpression } from "./hooks/index.js";
8
+ import { useFilterInputAutocomplete, useFilterInputExpression, useFilterInputSelection } from "./hooks/index.js";
9
9
  const FilterInput = ({ fields = [], value, onChange, placeholder = 'Type to filter...', error = false, showKeyboardHint = false, className, ...props })=>{
10
10
  const inputRef = useRef(null);
11
11
  const containerRef = useRef(null);
12
12
  const buildingChipRef = useRef(null);
13
- const { conditions, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue } = useFilterInputExpression({
13
+ const chipRegistryRef = useRef(new Map());
14
+ const registerChipRef = useCallback((id, el)=>{
15
+ if (el) chipRegistryRef.current.set(id, el);
16
+ else chipRegistryRef.current.delete(id);
17
+ }, []);
18
+ const { conditions, connectors, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue } = useFilterInputExpression({
14
19
  fields,
15
20
  value,
16
21
  onChange,
@@ -29,6 +34,17 @@ const FilterInput = ({ fields = [], value, onChange, placeholder = 'Type to filt
29
34
  buildingChipRef,
30
35
  inputRef
31
36
  });
37
+ const { allSelected, pasteError, clearSelection, dismissPasteError, handleMouseDown, handleKeyDown, handleCopy, handlePaste, retryParse } = useFilterInputSelection({
38
+ conditions,
39
+ connectors,
40
+ fields,
41
+ chipRegistryRef,
42
+ inputRef,
43
+ clearAll,
44
+ setInputText: autocomplete.setInputText,
45
+ closeMenu: autocomplete.closeAutocompleteMenu,
46
+ onChange
47
+ });
32
48
  const contextValue = useFilterInputContextValue({
33
49
  chips,
34
50
  autocomplete,
@@ -36,17 +52,43 @@ const FilterInput = ({ fields = [], value, onChange, placeholder = 'Type to filt
36
52
  inputRef,
37
53
  placeholder,
38
54
  error,
39
- showKeyboardHint
55
+ showKeyboardHint,
56
+ registerChipRef
40
57
  });
41
- const errors = useMemo(()=>parseFilterInputErrors(conditions, fields), [
58
+ useEffect(()=>{
59
+ if (pasteError) dismissPasteError();
60
+ }, [
61
+ conditions.length
62
+ ]);
63
+ useEffect(()=>{
64
+ if (pasteError) retryParse(autocomplete.inputText);
65
+ }, [
66
+ autocomplete.inputText
67
+ ]);
68
+ const fieldErrors = useMemo(()=>parseFilterInputErrors(conditions, fields), [
42
69
  conditions,
43
70
  fields
44
71
  ]);
72
+ const errors = useMemo(()=>pasteError ? [
73
+ pasteError,
74
+ ...fieldErrors
75
+ ] : fieldErrors, [
76
+ pasteError,
77
+ fieldErrors
78
+ ]);
45
79
  return /*#__PURE__*/ jsxs("div", {
46
80
  ref: containerRef,
47
- className: cn('relative flex w-full flex-col gap-4', className),
81
+ className: cn('group/filter-input relative flex w-full flex-col gap-4', className),
48
82
  onFocus: autocomplete.handleFocus,
49
83
  onBlur: autocomplete.handleBlur,
84
+ onClick: allSelected ? clearSelection : void 0,
85
+ onKeyDown: handleKeyDown,
86
+ onMouseDown: handleMouseDown,
87
+ onCopy: handleCopy,
88
+ onPaste: handlePaste,
89
+ ...allSelected && {
90
+ 'data-selected-all': ''
91
+ },
50
92
  children: [
51
93
  /*#__PURE__*/ jsx(FilterInputProvider, {
52
94
  value: contextValue,
@@ -37,4 +37,6 @@ export interface FilterInputContextValue {
37
37
  menuRef: RefObject<HTMLDivElement | null>;
38
38
  /** Close autocomplete menu (used by connector chip to enforce single-dropdown constraint) */
39
39
  closeAutocompleteMenu: () => void;
40
+ /** Register/unregister a chip DOM element for selection tracking */
41
+ registerChipRef: (id: string, el: HTMLElement | null) => void;
40
42
  }
@@ -34,6 +34,7 @@ interface UseFilterInputContextValueOptions {
34
34
  placeholder: string;
35
35
  error: boolean;
36
36
  showKeyboardHint: boolean;
37
+ registerChipRef: (id: string, el: HTMLElement | null) => void;
37
38
  }
38
- export declare const useFilterInputContextValue: ({ chips, autocomplete, buildingChipRef, inputRef, placeholder, error, showKeyboardHint, }: UseFilterInputContextValueOptions) => FilterInputContextValue;
39
+ export declare const useFilterInputContextValue: ({ chips, autocomplete, buildingChipRef, inputRef, placeholder, error, showKeyboardHint, registerChipRef, }: UseFilterInputContextValueOptions) => FilterInputContextValue;
39
40
  export {};
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inputRef, placeholder, error, showKeyboardHint })=>useMemo(()=>({
2
+ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inputRef, placeholder, error, showKeyboardHint, registerChipRef })=>useMemo(()=>({
3
3
  chips,
4
4
  buildingChipData: autocomplete.buildingChipData,
5
5
  buildingChipRef,
@@ -27,7 +27,8 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
27
27
  onCustomValueCommit: autocomplete.handleCustomValueCommit,
28
28
  onCustomAttributeCommit: autocomplete.handleCustomAttributeCommit,
29
29
  menuRef: autocomplete.menuRef,
30
- closeAutocompleteMenu: autocomplete.closeAutocompleteMenu
30
+ closeAutocompleteMenu: autocomplete.closeAutocompleteMenu,
31
+ registerChipRef
31
32
  }), [
32
33
  chips,
33
34
  autocomplete.buildingChipData,
@@ -52,6 +53,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
52
53
  autocomplete.handleCustomAttributeCommit,
53
54
  autocomplete.menuRef,
54
55
  autocomplete.closeAutocompleteMenu,
56
+ registerChipRef,
55
57
  buildingChipRef,
56
58
  inputRef,
57
59
  placeholder,
@@ -1,4 +1,4 @@
1
- import type { FC } from 'react';
1
+ import { type FC } from 'react';
2
2
  import type { FilterInputChipData } from '../types';
3
3
  import type { ChipSegment } from './FilterInputChip';
4
4
  interface ChipsWithGapsProps {
@@ -1,9 +1,15 @@
1
1
  import { Fragment, jsx } from "react/jsx-runtime";
2
+ import { useCallback } from "react";
3
+ import { useFilterInputContext } from "../FilterInputContext/index.js";
2
4
  import { CONNECTOR_ID_PATTERN } from "../lib/index.js";
3
5
  import { FilterInputChip } from "./FilterInputChip/FilterInputChip.js";
4
6
  import { FilterInputConnectorChip } from "./FilterInputConnectorChip/index.js";
5
7
  import { InsertionGap } from "./InsertionGap/index.js";
6
8
  const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, onConnectorChange, onChipRemove, onGapClick })=>{
9
+ const { registerChipRef } = useFilterInputContext();
10
+ const chipRef = useCallback((id)=>(el)=>registerChipRef(id, el), [
11
+ registerChipRef
12
+ ]);
7
13
  const elements = [];
8
14
  let connectorIndex = 0;
9
15
  const connectorCount = chips.filter((c)=>'and' === c.variant || 'or' === c.variant).length;
@@ -11,6 +17,7 @@ const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, on
11
17
  const isCondition = 'chip' === chip.variant;
12
18
  const isConnector = 'and' === chip.variant || 'or' === chip.variant;
13
19
  if (isCondition) elements.push(/*#__PURE__*/ jsx("div", {
20
+ ref: chipRef(chip.id),
14
21
  className: "shrink-0 cursor-pointer hover:z-10",
15
22
  children: /*#__PURE__*/ jsx(FilterInputChip, {
16
23
  chipId: chip.id,
@@ -35,6 +42,7 @@ const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, on
35
42
  onClick: ()=>onGapClick(condIdx, false)
36
43
  }, `gap-before-${chip.id}`));
37
44
  elements.push(/*#__PURE__*/ jsx("div", {
45
+ ref: chipRef(chip.id),
38
46
  className: "shrink-0",
39
47
  children: /*#__PURE__*/ jsx(FilterInputConnectorChip, {
40
48
  chipId: chip.id,
@@ -1,5 +1,7 @@
1
1
  import { cva } from "class-variance-authority";
2
- const chipVariants = cva('h-22 group/chip relative flex items-center justify-center px-5 py-0 border border-solid rounded-8 gap-4', {
2
+ const selectionHighlight = "group-data-[selected-all]/filter-input:bg-bg-light-info group-data-[selected-all]/filter-input:border-border-info [[data-drag-selected]_&]:bg-bg-light-info [[data-drag-selected]_&]:border-border-info";
3
+ const hiddenWhenSelected = "group-data-[selected-all]/filter-input:hidden [[data-drag-selected]_&]:hidden";
4
+ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-center px-5 py-0 border border-solid rounded-8 gap-4 ${selectionHighlight}`, {
3
5
  variants: {
4
6
  error: {
5
7
  true: 'bg-bg-light-danger border-border-danger',
@@ -61,7 +63,7 @@ const segmentTextVariants = cva('truncate text-sm', {
61
63
  error: false
62
64
  }
63
65
  });
64
- const removeButtonVariants = cva('absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 group-hover/chip:opacity-100 focus:opacity-100 transition-opacity', {
66
+ const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 group-hover/chip:opacity-100 focus:opacity-100 transition-opacity ${hiddenWhenSelected}`, {
65
67
  variants: {
66
68
  error: {
67
69
  true: 'border-border-danger bg-bg-light-danger text-text-danger',
@@ -1,7 +1,6 @@
1
1
  import type { FC } from 'react';
2
2
  interface FilterInputSearchProps {
3
3
  hasContent: boolean;
4
- minWidth?: number;
5
4
  }
6
5
  export declare const FilterInputSearch: FC<FilterInputSearchProps>;
7
6
  export {};
@@ -1,8 +1,9 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useFilterInputContext } from "../FilterInputContext/index.js";
3
3
  import { filterInputInputVariants } from "./classes.js";
4
- const CHAR_WIDTH_PX = 8;
5
- const FilterInputSearch = ({ hasContent, minWidth = 4 })=>{
4
+ const CHAR_WIDTH_PX = 10;
5
+ const MIN_INPUT_WIDTH = 20;
6
+ const FilterInputSearch = ({ hasContent })=>{
6
7
  const { inputText, inputRef, placeholder, error, menuOpen, onInputChange, onInputKeyDown, onInputClick } = useFilterInputContext();
7
8
  return /*#__PURE__*/ jsx("input", {
8
9
  ref: inputRef,
@@ -18,7 +19,7 @@ const FilterInputSearch = ({ hasContent, minWidth = 4 })=>{
18
19
  onClick: onInputClick,
19
20
  placeholder: hasContent ? void 0 : placeholder,
20
21
  style: hasContent ? {
21
- width: `${Math.max(minWidth, inputText.length * CHAR_WIDTH_PX)}px`
22
+ width: `${Math.max(MIN_INPUT_WIDTH, (inputText.length + 1) * CHAR_WIDTH_PX)}px`
22
23
  } : void 0,
23
24
  className: filterInputInputVariants({
24
25
  hasContent
@@ -41,7 +41,7 @@ const filterInputInnerVariants = cva('flex min-h-[40px] w-full cursor-text flex-
41
41
  const filterInputInputVariants = cva('h-24 border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0', {
42
42
  variants: {
43
43
  hasContent: {
44
- true: 'ml-4',
44
+ true: 'ml-4 shrink-0',
45
45
  false: 'flex-1'
46
46
  }
47
47
  },
@@ -97,8 +97,9 @@ const FilterInputFieldMenu = ({ fields, filterText = '', onSelect, open = false,
97
97
  inputRef,
98
98
  menuRef
99
99
  });
100
+ const hasResults = filteredFields.length > 0 || !filterText;
100
101
  return /*#__PURE__*/ jsx(DropdownMenu, {
101
- open: open,
102
+ open: open && hasResults,
102
103
  onOpenChange: onOpenChange,
103
104
  closeOnSelect: false,
104
105
  positioning: positioning,
@@ -1,2 +1,3 @@
1
1
  export { useFilterInputAutocomplete } from './useFilterInputAutocomplete';
2
2
  export { useFilterInputExpression } from './useFilterInputExpression';
3
+ export { useFilterInputSelection } from './useFilterInputSelection';
@@ -1,3 +1,4 @@
1
1
  import { useFilterInputAutocomplete } from "./useFilterInputAutocomplete/index.js";
2
2
  import { useFilterInputExpression } from "./useFilterInputExpression/index.js";
3
- export { useFilterInputAutocomplete, useFilterInputExpression };
3
+ import { useFilterInputSelection } from "./useFilterInputSelection/index.js";
4
+ export { useFilterInputAutocomplete, useFilterInputExpression, useFilterInputSelection };
@@ -69,5 +69,6 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
69
69
  menuRef: RefObject<HTMLDivElement | null>;
70
70
  closeAutocompleteMenu: () => void;
71
71
  blurCommitRef: RefObject<(() => boolean) | null>;
72
+ setInputText: import("react").Dispatch<import("react").SetStateAction<string>>;
72
73
  };
73
74
  export {};
@@ -233,7 +233,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
233
233
  handleCustomAttributeCommit,
234
234
  menuRef,
235
235
  closeAutocompleteMenu,
236
- blurCommitRef
236
+ blurCommitRef,
237
+ setInputText
237
238
  };
238
239
  };
239
240
  export { useFilterInputAutocomplete };
@@ -0,0 +1 @@
1
+ export { useFilterInputSelection } from './useFilterInputSelection';
@@ -0,0 +1,2 @@
1
+ import { useFilterInputSelection } from "./useFilterInputSelection.js";
2
+ export { useFilterInputSelection };
@@ -0,0 +1,3 @@
1
+ export declare const PASTE_ERROR_TIMEOUT = 5000;
2
+ export declare const DRAG_THRESHOLD = 4;
3
+ export declare const CHIP_ID_PREFIX = "chip-";
@@ -0,0 +1,4 @@
1
+ const PASTE_ERROR_TIMEOUT = 5000;
2
+ const DRAG_THRESHOLD = 4;
3
+ const CHIP_ID_PREFIX = 'chip-';
4
+ export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT };
@@ -0,0 +1,8 @@
1
+ export declare const clearDragAttributes: (chips: Map<string, HTMLElement>) => void;
2
+ export declare const hasDragSelection: (chips: Map<string, HTMLElement>) => boolean;
3
+ /** Get condition indices of drag-selected chips (chip-0, chip-2 → [0, 2]) */
4
+ export declare const getSelectedConditionIndices: (chips: Map<string, HTMLElement>) => number[];
5
+ /** Mark chips as drag-selected based on horizontal range. Returns true if any chip was selected. */
6
+ export declare const updateDragSelection: (registry: Map<string, HTMLElement>, startX: number, currentX: number) => boolean;
7
+ /** Check if every condition chip in the registry is drag-selected */
8
+ export declare const areAllConditionsDragged: (registry: Map<string, HTMLElement>) => boolean;
@@ -0,0 +1,34 @@
1
+ import { CHIP_ID_PREFIX } from "./constants.js";
2
+ const DRAG_ATTR = 'data-drag-selected';
3
+ const isChipInRange = (chip, x1, x2)=>{
4
+ const rect = chip.getBoundingClientRect();
5
+ const minX = Math.min(x1, x2);
6
+ const maxX = Math.max(x1, x2);
7
+ return rect.left <= maxX && rect.right >= minX;
8
+ };
9
+ const clearDragAttributes = (chips)=>{
10
+ [
11
+ ...chips.values()
12
+ ].forEach((chip)=>chip.removeAttribute(DRAG_ATTR));
13
+ };
14
+ const hasDragSelection = (chips)=>[
15
+ ...chips.values()
16
+ ].some((chip)=>chip.hasAttribute(DRAG_ATTR));
17
+ const getSelectedConditionIndices = (chips)=>[
18
+ ...chips.entries()
19
+ ].filter(([id, el])=>id.startsWith(CHIP_ID_PREFIX) && el.hasAttribute(DRAG_ATTR)).map(([id])=>Number(id.slice(CHIP_ID_PREFIX.length))).filter((index)=>!Number.isNaN(index)).sort((a, b)=>a - b);
20
+ const updateDragSelection = (registry, startX, currentX)=>[
21
+ ...registry.values()
22
+ ].reduce((found, chip)=>{
23
+ const inRange = isChipInRange(chip, startX, currentX);
24
+ if (inRange) chip.setAttribute(DRAG_ATTR, '');
25
+ else chip.removeAttribute(DRAG_ATTR);
26
+ return found || inRange;
27
+ }, false);
28
+ const areAllConditionsDragged = (registry)=>{
29
+ const conditions = [
30
+ ...registry.entries()
31
+ ].filter(([id])=>id.startsWith(CHIP_ID_PREFIX));
32
+ return conditions.length > 0 && conditions.every(([, el])=>el.hasAttribute(DRAG_ATTR));
33
+ };
34
+ export { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection };
@@ -0,0 +1,3 @@
1
+ export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT } from './constants';
2
+ export { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection, } from './dom';
3
+ export { serializeSelectedOrAll } from './serialize';
@@ -0,0 +1,4 @@
1
+ import { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT } from "./constants.js";
2
+ import { areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, updateDragSelection } from "./dom.js";
3
+ import { serializeSelectedOrAll } from "./serialize.js";
4
+ export { CHIP_ID_PREFIX, DRAG_THRESHOLD, PASTE_ERROR_TIMEOUT, areAllConditionsDragged, clearDragAttributes, getSelectedConditionIndices, hasDragSelection, serializeSelectedOrAll, updateDragSelection };
@@ -0,0 +1,3 @@
1
+ import type { Condition } from '../../../types';
2
+ /** Serialize selected (drag) or all conditions into a text string */
3
+ export declare const serializeSelectedOrAll: (conditions: Condition[], connectors: Array<"and" | "or">, chipRegistry: Map<string, HTMLElement>) => string;
@@ -0,0 +1,15 @@
1
+ import { serializeExpression } from "../../../lib/index.js";
2
+ import { buildExpression } from "../../useFilterInputExpression/expression.js";
3
+ import { getSelectedConditionIndices } from "./dom.js";
4
+ const serializeSelectedOrAll = (conditions, connectors, chipRegistry)=>{
5
+ const selectedIndices = getSelectedConditionIndices(chipRegistry);
6
+ if (selectedIndices.length > 0) {
7
+ const selected = selectedIndices.flatMap((i)=>conditions[i] ? [
8
+ conditions[i]
9
+ ] : []);
10
+ const selectedConnectors = selectedIndices.slice(1).map((_, i)=>connectors[selectedIndices[i]] ?? 'and');
11
+ return serializeExpression(buildExpression(selected, selectedConnectors));
12
+ }
13
+ return serializeExpression(buildExpression(conditions, connectors));
14
+ };
15
+ export { serializeSelectedOrAll };
@@ -0,0 +1,10 @@
1
+ import type { MouseEvent, RefObject } from 'react';
2
+ interface UseDragSelectionOptions {
3
+ chipRegistryRef: RefObject<Map<string, HTMLElement>>;
4
+ inputRef: RefObject<HTMLInputElement | null>;
5
+ onSelectAll: () => void;
6
+ }
7
+ export declare const useDragSelection: ({ chipRegistryRef, inputRef, onSelectAll, }: UseDragSelectionOptions) => {
8
+ handleMouseDown: (e: MouseEvent<HTMLDivElement>) => void;
9
+ };
10
+ export {};
@@ -0,0 +1,44 @@
1
+ import { useCallback, useRef } from "react";
2
+ import { DRAG_THRESHOLD, areAllConditionsDragged, clearDragAttributes, updateDragSelection } from "./lib/index.js";
3
+ const useDragSelection = ({ chipRegistryRef, inputRef, onSelectAll })=>{
4
+ const isDraggingRef = useRef(false);
5
+ const dragStartXRef = useRef(0);
6
+ const handleMouseDown = useCallback((e)=>{
7
+ if (0 !== e.button) return;
8
+ const target = e.target;
9
+ if (target.closest('input, button, [role="combobox"]')) return;
10
+ dragStartXRef.current = e.clientX;
11
+ isDraggingRef.current = false;
12
+ const handleMouseMove = (moveEvent)=>{
13
+ if (Math.abs(moveEvent.clientX - dragStartXRef.current) < DRAG_THRESHOLD) return;
14
+ if (!isDraggingRef.current) {
15
+ isDraggingRef.current = true;
16
+ document.body.style.userSelect = 'none';
17
+ }
18
+ const hasSelected = updateDragSelection(chipRegistryRef.current, dragStartXRef.current, moveEvent.clientX);
19
+ if (hasSelected) inputRef.current?.blur();
20
+ };
21
+ const handleMouseUp = ()=>{
22
+ document.removeEventListener('mousemove', handleMouseMove);
23
+ document.removeEventListener('mouseup', handleMouseUp);
24
+ document.body.style.userSelect = '';
25
+ if (!isDraggingRef.current) return;
26
+ const registry = chipRegistryRef.current;
27
+ if (areAllConditionsDragged(registry)) {
28
+ clearDragAttributes(registry);
29
+ onSelectAll();
30
+ }
31
+ isDraggingRef.current = false;
32
+ };
33
+ document.addEventListener('mousemove', handleMouseMove);
34
+ document.addEventListener('mouseup', handleMouseUp);
35
+ }, [
36
+ chipRegistryRef,
37
+ inputRef,
38
+ onSelectAll
39
+ ]);
40
+ return {
41
+ handleMouseDown
42
+ };
43
+ };
44
+ export { useDragSelection };
@@ -0,0 +1,25 @@
1
+ import type { RefObject } from 'react';
2
+ import type { Condition, ExprNode, FieldMetadata } from '../../types';
3
+ interface UseFilterInputSelectionOptions {
4
+ conditions: Condition[];
5
+ connectors: Array<'and' | 'or'>;
6
+ fields: FieldMetadata[];
7
+ chipRegistryRef: RefObject<Map<string, HTMLElement>>;
8
+ inputRef: RefObject<HTMLInputElement | null>;
9
+ clearAll: () => void;
10
+ setInputText: (text: string) => void;
11
+ closeMenu: () => void;
12
+ onChange?: (expression: ExprNode | null) => void;
13
+ }
14
+ export declare const useFilterInputSelection: ({ conditions, connectors, fields, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange, }: UseFilterInputSelectionOptions) => {
15
+ allSelected: boolean;
16
+ pasteError: string | null;
17
+ clearSelection: () => void;
18
+ dismissPasteError: () => void;
19
+ handleMouseDown: (e: import("react").MouseEvent<HTMLDivElement>) => void;
20
+ handleKeyDown: (e: import("react").KeyboardEvent<HTMLDivElement>) => void;
21
+ handleCopy: (e: import("react").ClipboardEvent<HTMLDivElement>) => void;
22
+ handlePaste: (e: import("react").ClipboardEvent<HTMLDivElement>) => void;
23
+ retryParse: (text: string) => void;
24
+ };
25
+ export {};
@@ -0,0 +1,54 @@
1
+ import { useCallback, useState } from "react";
2
+ import { clearDragAttributes } from "./lib/index.js";
3
+ import { useDragSelection } from "./useDragSelection.js";
4
+ import { useSelectionClipboard } from "./useSelectionClipboard.js";
5
+ import { useSelectionKeyboard } from "./useSelectionKeyboard.js";
6
+ const useFilterInputSelection = ({ conditions, connectors, fields, chipRegistryRef, inputRef, clearAll, setInputText, closeMenu, onChange })=>{
7
+ const [allSelected, setAllSelected] = useState(false);
8
+ const [pasteError, setPasteError] = useState(null);
9
+ const onSelectAll = useCallback(()=>setAllSelected(true), []);
10
+ const clearSelection = useCallback(()=>{
11
+ setAllSelected(false);
12
+ clearDragAttributes(chipRegistryRef.current);
13
+ }, [
14
+ chipRegistryRef
15
+ ]);
16
+ const dismissPasteError = useCallback(()=>setPasteError(null), []);
17
+ const { handleMouseDown } = useDragSelection({
18
+ chipRegistryRef,
19
+ inputRef,
20
+ onSelectAll
21
+ });
22
+ const { handleKeyDown } = useSelectionKeyboard({
23
+ allSelected,
24
+ conditionsCount: conditions.length,
25
+ chipRegistryRef,
26
+ inputRef,
27
+ clearAll,
28
+ clearSelection,
29
+ onSelectAll
30
+ });
31
+ const { handleCopy, handlePaste, retryParse } = useSelectionClipboard({
32
+ conditions,
33
+ connectors,
34
+ fields,
35
+ chipRegistryRef,
36
+ clearSelection,
37
+ setPasteError,
38
+ setInputText,
39
+ closeMenu,
40
+ onChange
41
+ });
42
+ return {
43
+ allSelected,
44
+ pasteError,
45
+ clearSelection,
46
+ dismissPasteError,
47
+ handleMouseDown,
48
+ handleKeyDown,
49
+ handleCopy,
50
+ handlePaste,
51
+ retryParse
52
+ };
53
+ };
54
+ export { useFilterInputSelection };
@@ -0,0 +1,19 @@
1
+ import type { ClipboardEvent, RefObject } from 'react';
2
+ import type { Condition, ExprNode, FieldMetadata } from '../../types';
3
+ interface UseSelectionClipboardOptions {
4
+ conditions: Condition[];
5
+ connectors: Array<'and' | 'or'>;
6
+ fields: FieldMetadata[];
7
+ chipRegistryRef: RefObject<Map<string, HTMLElement>>;
8
+ clearSelection: () => void;
9
+ setPasteError: (error: string | null) => void;
10
+ setInputText: (text: string) => void;
11
+ closeMenu: () => void;
12
+ onChange?: (expression: ExprNode | null) => void;
13
+ }
14
+ export declare const useSelectionClipboard: ({ conditions, connectors, fields, chipRegistryRef, clearSelection, setPasteError, setInputText, closeMenu, onChange, }: UseSelectionClipboardOptions) => {
15
+ handleCopy: (e: ClipboardEvent<HTMLDivElement>) => void;
16
+ handlePaste: (e: ClipboardEvent<HTMLDivElement>) => void;
17
+ retryParse: (text: string) => void;
18
+ };
19
+ export {};