@xeplr/ui-table 1.0.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/LICENSE +21 -0
- package/package.json +17 -0
- package/src/XeplrTable.jsx +332 -0
- package/src/actions/ActionsCell.jsx +75 -0
- package/src/actions/ChildTable.jsx +159 -0
- package/src/actions/RecordDetail.jsx +213 -0
- package/src/actions/RecordModal.jsx +159 -0
- package/src/buildChangeSet.js +126 -0
- package/src/detectTypes.js +102 -0
- package/src/filters/BooleanFilter.jsx +66 -0
- package/src/filters/DateFilter.jsx +75 -0
- package/src/filters/FilterWrapper.jsx +66 -0
- package/src/filters/NumberFilter.jsx +105 -0
- package/src/filters/StringFilter.jsx +142 -0
- package/src/index.js +32 -0
- package/src/operators.js +113 -0
- package/src/resolveCellStyle.js +252 -0
- package/src/useActionsController.js +453 -0
- package/src/useTableController.js +131 -0
- package/src/xeplr-table.css +913 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import FilterWrapper from './FilterWrapper.jsx';
|
|
3
|
+
import { resolveDate } from '../operators.js';
|
|
4
|
+
|
|
5
|
+
var OPERATORS = [
|
|
6
|
+
{ value: 'eq', label: 'Equals' },
|
|
7
|
+
{ value: 'neq', label: 'Not Equals' },
|
|
8
|
+
{ value: 'gt', label: 'After' },
|
|
9
|
+
{ value: 'gte', label: 'On or After' },
|
|
10
|
+
{ value: 'lt', label: 'Before' },
|
|
11
|
+
{ value: 'lte', label: 'On or Before' },
|
|
12
|
+
{ value: 'between', label: 'Between' }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Date filter: operator select + date input(s).
|
|
17
|
+
*
|
|
18
|
+
* Filter value shape: { operator, value, valueTo }
|
|
19
|
+
*/
|
|
20
|
+
export default function DateFilter({ column }) {
|
|
21
|
+
var currentFilter = column.getFilterValue() || { operator: 'eq', value: '', valueTo: '' };
|
|
22
|
+
var isActive = currentFilter.value !== '';
|
|
23
|
+
|
|
24
|
+
function update(patch) {
|
|
25
|
+
column.setFilterValue(Object.assign({}, currentFilter, patch));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleClear() {
|
|
29
|
+
column.setFilterValue(undefined);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<FilterWrapper isActive={isActive} onClear={handleClear}>
|
|
34
|
+
<div className="xeplr-table-date-filter">
|
|
35
|
+
<select
|
|
36
|
+
className="xeplr-table-filter-operator"
|
|
37
|
+
value={currentFilter.operator}
|
|
38
|
+
onChange={function(e) { update({ operator: e.target.value }); }}
|
|
39
|
+
>
|
|
40
|
+
{OPERATORS.map(function(op) {
|
|
41
|
+
return <option key={op.value} value={op.value}>{op.label}</option>;
|
|
42
|
+
})}
|
|
43
|
+
</select>
|
|
44
|
+
|
|
45
|
+
<input
|
|
46
|
+
className="xeplr-table-filter-input"
|
|
47
|
+
type="date"
|
|
48
|
+
value={currentFilter.value}
|
|
49
|
+
onChange={function(e) { update({ value: e.target.value }); }}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
{currentFilter.operator === 'between' && (
|
|
53
|
+
<input
|
|
54
|
+
className="xeplr-table-filter-input"
|
|
55
|
+
type="date"
|
|
56
|
+
value={currentFilter.valueTo}
|
|
57
|
+
onChange={function(e) { update({ valueTo: e.target.value }); }}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</FilterWrapper>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Custom filter function for date columns.
|
|
67
|
+
*/
|
|
68
|
+
export function dateFilterFn(row, columnId, filterValue) {
|
|
69
|
+
if (!filterValue || !filterValue.value) return true;
|
|
70
|
+
|
|
71
|
+
var raw = row.getValue(columnId);
|
|
72
|
+
if (raw === null || raw === undefined || raw === '') return false;
|
|
73
|
+
|
|
74
|
+
return resolveDate(raw, filterValue.operator, filterValue.value, filterValue.valueTo);
|
|
75
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common filter popover shell.
|
|
5
|
+
* Renders a trigger icon and a dropdown panel that closes on outside click.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} props
|
|
8
|
+
* @param {boolean} props.isActive - Whether filter is currently applied
|
|
9
|
+
* @param {React.ReactNode} props.children - Filter UI content
|
|
10
|
+
* @param {Function} props.onClear - Called when user clears the filter
|
|
11
|
+
*/
|
|
12
|
+
export default function FilterWrapper({ isActive, children, onClear }) {
|
|
13
|
+
var [open, setOpen] = useState(false);
|
|
14
|
+
var wrapperRef = useRef(null);
|
|
15
|
+
|
|
16
|
+
// Close on outside click
|
|
17
|
+
useEffect(function() {
|
|
18
|
+
if (!open) return;
|
|
19
|
+
|
|
20
|
+
function handleClick(e) {
|
|
21
|
+
if (wrapperRef.current && !wrapperRef.current.contains(e.target)) {
|
|
22
|
+
setOpen(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
document.addEventListener('mousedown', handleClick);
|
|
27
|
+
return function() { document.removeEventListener('mousedown', handleClick); };
|
|
28
|
+
}, [open]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="xeplr-table-filter-wrapper" ref={wrapperRef}>
|
|
32
|
+
<button
|
|
33
|
+
className={'xeplr-table-filter-trigger' + (isActive ? ' xeplr-table-filter-active' : '')}
|
|
34
|
+
onClick={function() { setOpen(!open); }}
|
|
35
|
+
title="Filter"
|
|
36
|
+
type="button"
|
|
37
|
+
>
|
|
38
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
39
|
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
|
|
43
|
+
{open && (
|
|
44
|
+
<div className="xeplr-table-filter-panel">
|
|
45
|
+
{children}
|
|
46
|
+
<div className="xeplr-table-filter-actions">
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
className="xeplr-table-filter-clear-btn"
|
|
50
|
+
onClick={function() { onClear(); setOpen(false); }}
|
|
51
|
+
>
|
|
52
|
+
Clear
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
className="xeplr-table-filter-close-btn"
|
|
57
|
+
onClick={function() { setOpen(false); }}
|
|
58
|
+
>
|
|
59
|
+
Done
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import FilterWrapper from './FilterWrapper.jsx';
|
|
3
|
+
import { resolveNumber, isNullish } from '../operators.js';
|
|
4
|
+
|
|
5
|
+
var OPERATORS = [
|
|
6
|
+
{ value: 'eq', label: '= Equals' },
|
|
7
|
+
{ value: 'neq', label: '!= Not Equals' },
|
|
8
|
+
{ value: 'gt', label: '> Greater Than' },
|
|
9
|
+
{ value: 'gte', label: '>= Greater or Equal' },
|
|
10
|
+
{ value: 'lt', label: '< Less Than' },
|
|
11
|
+
{ value: 'lte', label: '<= Less or Equal' },
|
|
12
|
+
{ value: 'between', label: 'Between' }
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Number filter: operator select + input(s) + null checks.
|
|
17
|
+
*
|
|
18
|
+
* Filter value shape: { operator, value, valueTo, isNull: boolean | null }
|
|
19
|
+
*/
|
|
20
|
+
export default function NumberFilter({ column }) {
|
|
21
|
+
var currentFilter = column.getFilterValue() || { operator: 'eq', value: '', valueTo: '', isNull: null };
|
|
22
|
+
var isActive = currentFilter.value !== '' || currentFilter.isNull !== null;
|
|
23
|
+
|
|
24
|
+
function update(patch) {
|
|
25
|
+
column.setFilterValue(Object.assign({}, currentFilter, patch));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function setNullMode(mode) {
|
|
29
|
+
var next = currentFilter.isNull === mode ? null : mode;
|
|
30
|
+
update({ isNull: next });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function handleClear() {
|
|
34
|
+
column.setFilterValue(undefined);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<FilterWrapper isActive={isActive} onClear={handleClear}>
|
|
39
|
+
<div className="xeplr-table-number-filter">
|
|
40
|
+
<select
|
|
41
|
+
className="xeplr-table-filter-operator"
|
|
42
|
+
value={currentFilter.operator}
|
|
43
|
+
onChange={function(e) { update({ operator: e.target.value }); }}
|
|
44
|
+
>
|
|
45
|
+
{OPERATORS.map(function(op) {
|
|
46
|
+
return <option key={op.value} value={op.value}>{op.label}</option>;
|
|
47
|
+
})}
|
|
48
|
+
</select>
|
|
49
|
+
|
|
50
|
+
<input
|
|
51
|
+
className="xeplr-table-filter-input"
|
|
52
|
+
type="number"
|
|
53
|
+
placeholder={currentFilter.operator === 'between' ? 'Min' : 'Value'}
|
|
54
|
+
value={currentFilter.value}
|
|
55
|
+
onChange={function(e) { update({ value: e.target.value }); }}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{currentFilter.operator === 'between' && (
|
|
59
|
+
<input
|
|
60
|
+
className="xeplr-table-filter-input"
|
|
61
|
+
type="number"
|
|
62
|
+
placeholder="Max"
|
|
63
|
+
value={currentFilter.valueTo}
|
|
64
|
+
onChange={function(e) { update({ valueTo: e.target.value }); }}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<div className="xeplr-table-filter-null-controls">
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className={currentFilter.isNull === true ? 'active' : ''}
|
|
72
|
+
onClick={function() { setNullMode(true); }}
|
|
73
|
+
>
|
|
74
|
+
Is Null
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
className={currentFilter.isNull === false ? 'active' : ''}
|
|
79
|
+
onClick={function() { setNullMode(false); }}
|
|
80
|
+
>
|
|
81
|
+
Is Not Null
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</FilterWrapper>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Custom filter function for number columns.
|
|
91
|
+
*/
|
|
92
|
+
export function numberFilterFn(row, columnId, filterValue) {
|
|
93
|
+
if (!filterValue) return true;
|
|
94
|
+
|
|
95
|
+
var raw = row.getValue(columnId);
|
|
96
|
+
|
|
97
|
+
// Null mode
|
|
98
|
+
if (filterValue.isNull === true) return isNullish(raw);
|
|
99
|
+
if (filterValue.isNull === false && isNullish(raw)) return false;
|
|
100
|
+
|
|
101
|
+
// No value entered — pass through
|
|
102
|
+
if (filterValue.value === '' || filterValue.value === undefined) return true;
|
|
103
|
+
|
|
104
|
+
return resolveNumber(raw, filterValue.operator, filterValue.value, filterValue.valueTo);
|
|
105
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import FilterWrapper from './FilterWrapper.jsx';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Excel-like string filter: searchable multi-select of unique values + null checks.
|
|
6
|
+
*
|
|
7
|
+
* Filter value shape: { selected: string[], isNull: boolean | null }
|
|
8
|
+
* - selected: array of checked values (empty = all selected = no filter)
|
|
9
|
+
* - isNull: true = show only nulls, false = exclude nulls, null = don't care
|
|
10
|
+
*/
|
|
11
|
+
export default function StringFilter({ column }) {
|
|
12
|
+
var currentFilter = column.getFilterValue() || { selected: [], isNull: null };
|
|
13
|
+
var [search, setSearch] = useState('');
|
|
14
|
+
|
|
15
|
+
// Collect unique non-null values from the full (pre-filtered) data
|
|
16
|
+
var uniqueValues = useMemo(function() {
|
|
17
|
+
var vals = new Set();
|
|
18
|
+
var rows = column.getFacetedRowModel().rows;
|
|
19
|
+
for (var i = 0; i < rows.length; i++) {
|
|
20
|
+
var val = rows[i].getValue(column.id);
|
|
21
|
+
if (val !== null && val !== undefined && val !== '') {
|
|
22
|
+
vals.add(String(val));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var arr = Array.from(vals);
|
|
26
|
+
arr.sort();
|
|
27
|
+
return arr;
|
|
28
|
+
}, [column.getFacetedRowModel()]);
|
|
29
|
+
|
|
30
|
+
var filteredValues = useMemo(function() {
|
|
31
|
+
if (!search) return uniqueValues;
|
|
32
|
+
var lower = search.toLowerCase();
|
|
33
|
+
return uniqueValues.filter(function(v) { return v.toLowerCase().includes(lower); });
|
|
34
|
+
}, [uniqueValues, search]);
|
|
35
|
+
|
|
36
|
+
var selectedSet = new Set(currentFilter.selected);
|
|
37
|
+
var isActive = currentFilter.selected.length > 0 || currentFilter.isNull !== null;
|
|
38
|
+
|
|
39
|
+
function toggleValue(val) {
|
|
40
|
+
var next = new Set(selectedSet);
|
|
41
|
+
if (next.has(val)) {
|
|
42
|
+
next.delete(val);
|
|
43
|
+
} else {
|
|
44
|
+
next.add(val);
|
|
45
|
+
}
|
|
46
|
+
column.setFilterValue({ selected: Array.from(next), isNull: currentFilter.isNull });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selectAll() {
|
|
50
|
+
column.setFilterValue({ selected: [], isNull: currentFilter.isNull });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function selectNone() {
|
|
54
|
+
// Selecting "none" means selecting all visible as the filter — i.e. show nothing
|
|
55
|
+
// Actually, set selected to a sentinel empty-but-active state
|
|
56
|
+
column.setFilterValue({ selected: ['__xeplr_none__'], isNull: currentFilter.isNull });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function setNullMode(mode) {
|
|
60
|
+
var next = currentFilter.isNull === mode ? null : mode;
|
|
61
|
+
column.setFilterValue({ selected: currentFilter.selected, isNull: next });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleClear() {
|
|
65
|
+
column.setFilterValue(undefined);
|
|
66
|
+
setSearch('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<FilterWrapper isActive={isActive} onClear={handleClear}>
|
|
71
|
+
<div className="xeplr-table-string-filter">
|
|
72
|
+
<input
|
|
73
|
+
className="xeplr-table-filter-search"
|
|
74
|
+
type="text"
|
|
75
|
+
placeholder="Search..."
|
|
76
|
+
value={search}
|
|
77
|
+
onChange={function(e) { setSearch(e.target.value); }}
|
|
78
|
+
/>
|
|
79
|
+
|
|
80
|
+
<div className="xeplr-table-filter-bulk">
|
|
81
|
+
<button type="button" onClick={selectAll}>Select All</button>
|
|
82
|
+
<button type="button" onClick={selectNone}>Clear All</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="xeplr-table-filter-list">
|
|
86
|
+
{filteredValues.map(function(val) {
|
|
87
|
+
var checked = selectedSet.size === 0 || selectedSet.has(val);
|
|
88
|
+
return (
|
|
89
|
+
<label key={val} className="xeplr-table-filter-option">
|
|
90
|
+
<input
|
|
91
|
+
type="checkbox"
|
|
92
|
+
checked={checked}
|
|
93
|
+
onChange={function() { toggleValue(val); }}
|
|
94
|
+
/>
|
|
95
|
+
<span>{val}</span>
|
|
96
|
+
</label>
|
|
97
|
+
);
|
|
98
|
+
})}
|
|
99
|
+
{filteredValues.length === 0 && (
|
|
100
|
+
<div className="xeplr-table-filter-empty">No values</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="xeplr-table-filter-null-controls">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
className={currentFilter.isNull === true ? 'active' : ''}
|
|
108
|
+
onClick={function() { setNullMode(true); }}
|
|
109
|
+
>
|
|
110
|
+
Is Null
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
className={currentFilter.isNull === false ? 'active' : ''}
|
|
115
|
+
onClick={function() { setNullMode(false); }}
|
|
116
|
+
>
|
|
117
|
+
Is Not Null
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</FilterWrapper>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Custom filter function for string columns.
|
|
127
|
+
*/
|
|
128
|
+
export function stringFilterFn(row, columnId, filterValue) {
|
|
129
|
+
if (!filterValue) return true;
|
|
130
|
+
|
|
131
|
+
var val = row.getValue(columnId);
|
|
132
|
+
var isNullish = val === null || val === undefined || val === '';
|
|
133
|
+
|
|
134
|
+
// Null mode check
|
|
135
|
+
if (filterValue.isNull === true) return isNullish;
|
|
136
|
+
if (filterValue.isNull === false && isNullish) return false;
|
|
137
|
+
|
|
138
|
+
// Selected values check
|
|
139
|
+
if (filterValue.selected.length === 0) return true; // no selection = show all
|
|
140
|
+
if (filterValue.selected[0] === '__xeplr_none__') return false;
|
|
141
|
+
return filterValue.selected.includes(String(val));
|
|
142
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Main component
|
|
2
|
+
export { default as XeplrTable } from './XeplrTable.jsx';
|
|
3
|
+
|
|
4
|
+
// Controller hooks
|
|
5
|
+
export { default as useTableController } from './useTableController.js';
|
|
6
|
+
export { default as useActionsController } from './useActionsController.js';
|
|
7
|
+
|
|
8
|
+
// Type detection
|
|
9
|
+
export { detectTypes, TYPES } from './detectTypes.js';
|
|
10
|
+
|
|
11
|
+
// Individual filter components (for custom layouts)
|
|
12
|
+
export { default as StringFilter, stringFilterFn } from './filters/StringFilter.jsx';
|
|
13
|
+
export { default as NumberFilter, numberFilterFn } from './filters/NumberFilter.jsx';
|
|
14
|
+
export { default as DateFilter, dateFilterFn } from './filters/DateFilter.jsx';
|
|
15
|
+
export { default as BooleanFilter, booleanFilterFn } from './filters/BooleanFilter.jsx';
|
|
16
|
+
export { default as FilterWrapper } from './filters/FilterWrapper.jsx';
|
|
17
|
+
|
|
18
|
+
// Action components (for custom layouts)
|
|
19
|
+
export { default as ActionsCell } from './actions/ActionsCell.jsx';
|
|
20
|
+
export { default as RecordModal } from './actions/RecordModal.jsx';
|
|
21
|
+
export { default as ChildTable } from './actions/ChildTable.jsx';
|
|
22
|
+
export { default as RecordDetail } from './actions/RecordDetail.jsx';
|
|
23
|
+
export { CHILD_DISPLAY } from './XeplrTable.jsx';
|
|
24
|
+
|
|
25
|
+
// Change set builder
|
|
26
|
+
export { buildChangeSet } from './buildChangeSet.js';
|
|
27
|
+
|
|
28
|
+
// Operator resolver (shared comparison logic)
|
|
29
|
+
export { resolve as resolveOperator, resolveString, resolveNumber, resolveDate, isNullish } from './operators.js';
|
|
30
|
+
|
|
31
|
+
// Conditional formatting
|
|
32
|
+
export { resolveCellStyle } from './resolveCellStyle.js';
|
package/src/operators.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared operator resolver for comparing values.
|
|
3
|
+
* Used by filters, conditional formatting, and any future comparison logic.
|
|
4
|
+
*
|
|
5
|
+
* String operators: eq, neq, isnull, starts, ends, contains, notcontains
|
|
6
|
+
* Number operators: eq, neq, isnull, gt, gte, lt, lte, between
|
|
7
|
+
* Date operators: eq, neq, isnull, gt, gte, lt, lte, between
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ── Null check (works for all types) ──
|
|
11
|
+
|
|
12
|
+
function isNullish(value) {
|
|
13
|
+
return value === null || value === undefined || value === '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ── String ──
|
|
17
|
+
|
|
18
|
+
function resolveString(value, operator, target) {
|
|
19
|
+
if (operator === 'isnull') return isNullish(value);
|
|
20
|
+
if (isNullish(value)) return false;
|
|
21
|
+
|
|
22
|
+
var v = String(value).toLowerCase();
|
|
23
|
+
var t = (target != null ? String(target) : '').toLowerCase();
|
|
24
|
+
|
|
25
|
+
switch (operator) {
|
|
26
|
+
case 'eq': return v === t;
|
|
27
|
+
case 'neq': return v !== t;
|
|
28
|
+
case 'starts': return v.startsWith(t);
|
|
29
|
+
case 'ends': return v.endsWith(t);
|
|
30
|
+
case 'contains': return v.includes(t);
|
|
31
|
+
case 'notcontains': return !v.includes(t);
|
|
32
|
+
default: return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Number ──
|
|
37
|
+
|
|
38
|
+
function resolveNumber(value, operator, target, targetTo) {
|
|
39
|
+
if (operator === 'isnull') return isNullish(value);
|
|
40
|
+
if (isNullish(value)) return false;
|
|
41
|
+
|
|
42
|
+
var v = Number(value);
|
|
43
|
+
var t = Number(target);
|
|
44
|
+
|
|
45
|
+
switch (operator) {
|
|
46
|
+
case 'eq': return v === t;
|
|
47
|
+
case 'neq': return v !== t;
|
|
48
|
+
case 'gt': return v > t;
|
|
49
|
+
case 'gte': return v >= t;
|
|
50
|
+
case 'lt': return v < t;
|
|
51
|
+
case 'lte': return v <= t;
|
|
52
|
+
case 'between': {
|
|
53
|
+
var to = Number(targetTo);
|
|
54
|
+
if (isNaN(to)) return v >= t;
|
|
55
|
+
return v >= t && v <= to;
|
|
56
|
+
}
|
|
57
|
+
default: return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Date ──
|
|
62
|
+
|
|
63
|
+
function toDay(v) {
|
|
64
|
+
var d = new Date(v);
|
|
65
|
+
return Date.UTC(d.getFullYear(), d.getMonth(), d.getDate());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveDate(value, operator, target, targetTo) {
|
|
69
|
+
if (operator === 'isnull') return isNullish(value);
|
|
70
|
+
if (isNullish(value)) return false;
|
|
71
|
+
|
|
72
|
+
var v = toDay(value);
|
|
73
|
+
var t = toDay(target);
|
|
74
|
+
if (isNaN(v) || isNaN(t)) return false;
|
|
75
|
+
|
|
76
|
+
switch (operator) {
|
|
77
|
+
case 'eq': return v === t;
|
|
78
|
+
case 'neq': return v !== t;
|
|
79
|
+
case 'gt': return v > t;
|
|
80
|
+
case 'gte': return v >= t;
|
|
81
|
+
case 'lt': return v < t;
|
|
82
|
+
case 'lte': return v <= t;
|
|
83
|
+
case 'between': {
|
|
84
|
+
if (!targetTo) return v >= t;
|
|
85
|
+
var to = toDay(targetTo);
|
|
86
|
+
return v >= t && v <= to;
|
|
87
|
+
}
|
|
88
|
+
default: return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Unified entry point ──
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve an operator comparison.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} dataType - 'string' | 'number' | 'date'
|
|
98
|
+
* @param {*} value - The value to test
|
|
99
|
+
* @param {string} operator - The operator key
|
|
100
|
+
* @param {*} target - The comparison target
|
|
101
|
+
* @param {*} [targetTo] - Second target for 'between'
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function resolve(dataType, value, operator, target, targetTo) {
|
|
105
|
+
switch (dataType) {
|
|
106
|
+
case 'string': return resolveString(value, operator, target);
|
|
107
|
+
case 'number': return resolveNumber(value, operator, target, targetTo);
|
|
108
|
+
case 'date': return resolveDate(value, operator, target, targetTo);
|
|
109
|
+
default: return resolveString(value, operator, target);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { resolve, resolveString, resolveNumber, resolveDate, isNullish };
|