@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.
- package/dist/components/FilterInput/FilterInput.d.ts +0 -25
- package/dist/components/FilterInput/FilterInput.js +48 -6
- package/dist/components/FilterInput/FilterInputContext/types.d.ts +2 -0
- package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -1
- package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -2
- package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.d.ts +1 -1
- package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +8 -0
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +4 -2
- package/dist/components/FilterInput/FilterInputField/FilterInputSearch.d.ts +0 -1
- package/dist/components/FilterInput/FilterInputField/FilterInputSearch.js +4 -3
- package/dist/components/FilterInput/FilterInputField/classes.js +1 -1
- package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js +2 -1
- package/dist/components/FilterInput/hooks/index.d.ts +1 -0
- package/dist/components/FilterInput/hooks/index.js +2 -1
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +1 -0
- package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +2 -1
- package/dist/components/FilterInput/hooks/useFilterInputSelection/index.d.ts +1 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/index.js +2 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.d.ts +3 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.js +4 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.d.ts +8 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.js +34 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.d.ts +3 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.js +4 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.d.ts +3 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.js +15 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.d.ts +10 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.js +44 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts +25 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.js +54 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.d.ts +19 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.js +77 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.d.ts +14 -0
- package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.js +40 -0
- package/dist/components/FilterInput/index.d.ts +1 -0
- package/dist/components/FilterInput/index.js +2 -1
- package/dist/components/FilterInput/lib/index.d.ts +2 -0
- package/dist/components/FilterInput/lib/index.js +3 -1
- package/dist/components/FilterInput/lib/parseExpression/error.d.ts +6 -0
- package/dist/components/FilterInput/lib/parseExpression/error.js +6 -0
- package/dist/components/FilterInput/lib/parseExpression/index.d.ts +2 -0
- package/dist/components/FilterInput/lib/parseExpression/index.js +3 -0
- package/dist/components/FilterInput/lib/parseExpression/parseExpression.d.ts +8 -0
- package/dist/components/FilterInput/lib/parseExpression/parseExpression.js +21 -0
- package/dist/components/FilterInput/lib/parseExpression/parser.d.ts +4 -0
- package/dist/components/FilterInput/lib/parseExpression/parser.js +146 -0
- package/dist/components/FilterInput/lib/parseExpression/tokenizer.d.ts +8 -0
- package/dist/components/FilterInput/lib/parseExpression/tokenizer.js +101 -0
- package/dist/components/FilterInput/lib/parseExpression/types.d.ts +7 -0
- package/dist/components/FilterInput/lib/parseExpression/types.js +0 -0
- package/dist/components/FilterInput/lib/parseExpression/validators.d.ts +5 -0
- package/dist/components/FilterInput/lib/parseExpression/validators.js +28 -0
- package/dist/components/FilterInput/lib/serializeExpression.d.ts +6 -0
- package/dist/components/FilterInput/lib/serializeExpression.js +36 -0
- package/dist/metadata/components.json +3 -9
- 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
|
|
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
|
-
|
|
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,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
|
|
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(
|
|
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,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 =
|
|
5
|
-
const
|
|
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(
|
|
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
|
},
|
package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js
CHANGED
|
@@ -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,3 +1,4 @@
|
|
|
1
1
|
import { useFilterInputAutocomplete } from "./useFilterInputAutocomplete/index.js";
|
|
2
2
|
import { useFilterInputExpression } from "./useFilterInputExpression/index.js";
|
|
3
|
-
|
|
3
|
+
import { useFilterInputSelection } from "./useFilterInputSelection/index.js";
|
|
4
|
+
export { useFilterInputAutocomplete, useFilterInputExpression, useFilterInputSelection };
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js
CHANGED
|
@@ -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,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 };
|
package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts
ADDED
|
@@ -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 {};
|