@ioca/react 1.5.27 → 1.5.29
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/lib/css/index.css +1 -1
- package/lib/es/components/datagrid/datagrid.js +23 -71
- package/lib/es/components/datagrid/helper.js +3 -11
- package/lib/es/components/pill/create.js +15 -0
- package/lib/es/components/pill/index.js +5 -0
- package/lib/es/components/pill/item.js +23 -0
- package/lib/es/components/pill/pill.js +192 -0
- package/lib/es/components/select/options.js +1 -1
- package/lib/es/components/select/select.js +8 -2
- package/lib/es/components/tag/tag.js +2 -4
- package/lib/es/components/tree/item.js +6 -6
- package/lib/es/components/tree/tree.js +7 -5
- package/lib/es/components/tree/virtual.js +2 -2
- package/lib/es/index.js +1 -0
- package/lib/index.js +268 -110
- package/lib/types/components/pill/index.d.ts +5 -0
- package/lib/types/components/pill/pill.d.ts +6 -0
- package/lib/types/components/pill/type.d.ts +27 -0
- package/lib/types/components/tag/type.d.ts +0 -1
- package/lib/types/components/tree/type.d.ts +3 -2
- package/lib/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
+
import { AddRound } from '@ricons/material';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
5
|
+
import CreateTag from './create.js';
|
|
6
|
+
import TagItem from './item.js';
|
|
7
|
+
|
|
8
|
+
function Pill(props) {
|
|
9
|
+
const { value = [], tagProps, max, icon = jsx(AddRound, {}), className, label, labelInline, readonly, editable, onChange, onUpdate, validator, format, renderItem, ...restProps } = props;
|
|
10
|
+
const [editingIndex, setEditingIndex] = useState(null);
|
|
11
|
+
const [loadingSet, setLoadingSet] = useState(new Set());
|
|
12
|
+
const instRef = useRef({
|
|
13
|
+
props,
|
|
14
|
+
editingIndex,
|
|
15
|
+
loadingSet,
|
|
16
|
+
setEditingIndex,
|
|
17
|
+
setLoadingSet,
|
|
18
|
+
});
|
|
19
|
+
instRef.current.props = props;
|
|
20
|
+
instRef.current.editingIndex = editingIndex;
|
|
21
|
+
instRef.current.loadingSet = loadingSet;
|
|
22
|
+
instRef.current.setEditingIndex = setEditingIndex;
|
|
23
|
+
instRef.current.setLoadingSet = setLoadingSet;
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (editingIndex !== null) {
|
|
26
|
+
const el = document.querySelector(".i-pill-editing");
|
|
27
|
+
el?.focus();
|
|
28
|
+
}
|
|
29
|
+
}, [editingIndex]);
|
|
30
|
+
const cleanTagProps = useMemo(() => {
|
|
31
|
+
if (!tagProps)
|
|
32
|
+
return {};
|
|
33
|
+
const { onClose, dot, dotClass, ...rest } = tagProps;
|
|
34
|
+
return rest;
|
|
35
|
+
}, [tagProps]);
|
|
36
|
+
const handleClose = useCallback((index) => {
|
|
37
|
+
const inst = instRef.current;
|
|
38
|
+
if (inst.props.readonly)
|
|
39
|
+
return;
|
|
40
|
+
const hasAsync = !!inst.props.onUpdate;
|
|
41
|
+
if (hasAsync)
|
|
42
|
+
inst.setLoadingSet((prev) => new Set(prev).add(index));
|
|
43
|
+
setTimeout(async () => {
|
|
44
|
+
try {
|
|
45
|
+
const { props } = instRef.current;
|
|
46
|
+
const values = props.value ?? [];
|
|
47
|
+
const item = values[index];
|
|
48
|
+
if (item === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const result = props.onUpdate?.(undefined, item, "delete");
|
|
51
|
+
if (result instanceof Promise) {
|
|
52
|
+
const ok = await result;
|
|
53
|
+
if (ok === false)
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const next = [...values];
|
|
57
|
+
next.splice(index, 1);
|
|
58
|
+
props.onChange?.(next);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
if (hasAsync) {
|
|
62
|
+
instRef.current.setLoadingSet((prev) => {
|
|
63
|
+
const s = new Set(prev);
|
|
64
|
+
s.delete(index);
|
|
65
|
+
return s;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, 0);
|
|
70
|
+
}, []);
|
|
71
|
+
const handleItemClick = useCallback((e, index) => {
|
|
72
|
+
if (e.target.closest(".i-helpericon"))
|
|
73
|
+
return;
|
|
74
|
+
const inst = instRef.current;
|
|
75
|
+
if (inst.props.readonly)
|
|
76
|
+
return;
|
|
77
|
+
if (index === -1 && inst.props.max !== undefined && (inst.props.value?.length ?? 0) >= inst.props.max)
|
|
78
|
+
return;
|
|
79
|
+
inst.setEditingIndex(index);
|
|
80
|
+
}, []);
|
|
81
|
+
const commitEdit = useCallback((index, text) => {
|
|
82
|
+
const inst = instRef.current;
|
|
83
|
+
const formatted = inst.props.format ? inst.props.format(text) : text;
|
|
84
|
+
const hasAsync = !!(inst.props.validator || inst.props.onUpdate);
|
|
85
|
+
if (hasAsync)
|
|
86
|
+
inst.setLoadingSet((prev) => new Set(prev).add(index));
|
|
87
|
+
setTimeout(async () => {
|
|
88
|
+
try {
|
|
89
|
+
const { props } = instRef.current;
|
|
90
|
+
if (props.validator) {
|
|
91
|
+
const valid = await Promise.resolve(props.validator(formatted));
|
|
92
|
+
if (!valid)
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const values = props.value ?? [];
|
|
96
|
+
if (index === -1) {
|
|
97
|
+
if (values.includes(formatted))
|
|
98
|
+
return;
|
|
99
|
+
const result = props.onUpdate?.(formatted, undefined, "create");
|
|
100
|
+
if (result instanceof Promise) {
|
|
101
|
+
const ok = await result;
|
|
102
|
+
if (ok === false)
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
props.onChange?.([...values, formatted]);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const oldValue = values[index];
|
|
109
|
+
if (oldValue === formatted)
|
|
110
|
+
return;
|
|
111
|
+
const result = props.onUpdate?.(formatted, oldValue, "update");
|
|
112
|
+
if (result instanceof Promise) {
|
|
113
|
+
const ok = await result;
|
|
114
|
+
if (ok === false)
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const next = [...values];
|
|
118
|
+
next[index] = formatted;
|
|
119
|
+
props.onChange?.(next);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
if (hasAsync) {
|
|
124
|
+
instRef.current.setLoadingSet((prev) => {
|
|
125
|
+
const s = new Set(prev);
|
|
126
|
+
s.delete(index);
|
|
127
|
+
return s;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
instRef.current.setEditingIndex(null);
|
|
131
|
+
}
|
|
132
|
+
}, 0);
|
|
133
|
+
}, []);
|
|
134
|
+
const handleBlur = useCallback((index) => {
|
|
135
|
+
const inst = instRef.current;
|
|
136
|
+
if (inst.loadingSet.has(index))
|
|
137
|
+
return;
|
|
138
|
+
const el = document.querySelector(".i-pill-editing");
|
|
139
|
+
const text = el?.textContent?.trim();
|
|
140
|
+
if (!text) {
|
|
141
|
+
if (index !== -1) {
|
|
142
|
+
handleClose(index);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
inst.setEditingIndex(null);
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
commitEdit(index, text);
|
|
150
|
+
}, []);
|
|
151
|
+
const handleKeyDown = useCallback((e, index) => {
|
|
152
|
+
const inst = instRef.current;
|
|
153
|
+
if (inst.loadingSet.has(index))
|
|
154
|
+
return;
|
|
155
|
+
if (e.key === "Enter") {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
const text = e.currentTarget.textContent?.trim();
|
|
158
|
+
if (!text) {
|
|
159
|
+
if (index !== -1) {
|
|
160
|
+
handleClose(index);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
inst.setEditingIndex(null);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
commitEdit(index, text);
|
|
168
|
+
}
|
|
169
|
+
else if (e.key === "Escape") {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
if (index !== -1) {
|
|
172
|
+
const original = inst.props.value?.[index];
|
|
173
|
+
if (original !== undefined) {
|
|
174
|
+
e.currentTarget.textContent = original;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
inst.setEditingIndex(null);
|
|
178
|
+
}
|
|
179
|
+
}, []);
|
|
180
|
+
const handleStartCreate = useCallback(() => {
|
|
181
|
+
const inst = instRef.current;
|
|
182
|
+
if (inst.props.readonly)
|
|
183
|
+
return;
|
|
184
|
+
if (inst.props.max !== undefined && (inst.props.value?.length ?? 0) >= inst.props.max)
|
|
185
|
+
return;
|
|
186
|
+
inst.setEditingIndex(-1);
|
|
187
|
+
}, []);
|
|
188
|
+
const canCreate = !readonly && (max === undefined || value.length < max);
|
|
189
|
+
return (jsxs("div", { className: classNames("i-pills i-input-label", { "i-input-inline": labelInline }, className), ...restProps, children: [label && jsx("span", { className: "i-input-label-text", children: label }), jsxs("div", { className: "i-pill-list", children: [value.map((item, i) => (jsx(TagItem, { item: item, index: i, isEditing: editingIndex === i, isLoading: loadingSet.has(i), tagProps: tagProps, editable: editable, readonly: readonly, renderItem: renderItem, onClose: handleClose, onClick: handleItemClick, onBlur: handleBlur, onKeyDown: handleKeyDown }, i))), canCreate && jsx(CreateTag, { isEditing: editingIndex === -1, isLoading: loadingSet.has(-1), createTagProps: cleanTagProps, tagProps: tagProps, onBlur: handleBlur, onKeyDown: handleKeyDown, onStartCreate: handleStartCreate })] })] }));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export { Pill as default };
|
|
@@ -34,7 +34,7 @@ const displayValue = (config) => {
|
|
|
34
34
|
if (typeof opt === "number")
|
|
35
35
|
return jsxs(Tag, { children: ["+", opt] }, i);
|
|
36
36
|
const { label, value } = opt;
|
|
37
|
-
return (jsx(Tag, {
|
|
37
|
+
return (jsx(Tag, { onClose: (e) => {
|
|
38
38
|
e?.stopPropagation();
|
|
39
39
|
onSelect?.(value, opt);
|
|
40
40
|
}, children: label }, value));
|
|
@@ -13,14 +13,20 @@ const Select = (props) => {
|
|
|
13
13
|
const [filterValue, setFilterValue] = useState("");
|
|
14
14
|
const [selectedValue, setSelectedValue] = useState(value);
|
|
15
15
|
const [active, setActive] = useState(false);
|
|
16
|
-
const formattedOptions = useMemo(() =>
|
|
16
|
+
const formattedOptions = useMemo(() => {
|
|
17
|
+
return formatOption(options).map((opt) => {
|
|
18
|
+
const label = typeof opt.label === "string" ? opt.label : String(opt.value ?? "");
|
|
19
|
+
return { ...opt, _label: label.toLowerCase(), _value: String(opt.value ?? "").toLowerCase() };
|
|
20
|
+
});
|
|
21
|
+
}, [options]);
|
|
17
22
|
const filterOptions = useMemo(() => {
|
|
18
23
|
const fv = filterValue;
|
|
19
24
|
if (!fv || !filter)
|
|
20
25
|
return formattedOptions;
|
|
26
|
+
const lowerFv = fv.toLowerCase();
|
|
21
27
|
const filterFn = typeof filter === "function"
|
|
22
28
|
? filter
|
|
23
|
-
: (opt) => opt.
|
|
29
|
+
: (opt) => opt._value.includes(lowerFv) || opt._label.includes(lowerFv);
|
|
24
30
|
return formattedOptions.filter(filterFn);
|
|
25
31
|
}, [formattedOptions, filter, filterValue]);
|
|
26
32
|
const changeValue = (v) => {
|
|
@@ -3,15 +3,13 @@ import classNames from 'classnames';
|
|
|
3
3
|
import Helpericon from '../utils/helpericon/helpericon.js';
|
|
4
4
|
|
|
5
5
|
const Tag = (props) => {
|
|
6
|
-
const { dot, dotClass, outline, round, size = "normal",
|
|
6
|
+
const { dot, dotClass, outline, round, size = "normal", className, children, onClose, onClick, ...restProps } = props;
|
|
7
7
|
return (jsxs("span", { className: classNames("i-tag", {
|
|
8
8
|
"i-tag-outline": outline,
|
|
9
9
|
"i-tag-clickable": onClick,
|
|
10
10
|
[`i-tag-${size}`]: size !== "normal",
|
|
11
11
|
round,
|
|
12
|
-
}, className), onClick: onClick, ...restProps, children: [dot && jsx("span", { className: classNames("i-tag-dot", dotClass) }), children, onClose && (jsx(Helpericon, { active: true, className:
|
|
13
|
-
"i-tag-hover-close": hoverShowClose,
|
|
14
|
-
}), onClick: onClose }))] }));
|
|
12
|
+
}, className), onClick: onClick, ...restProps, children: [dot && jsx("span", { className: classNames("i-tag-dot", dotClass) }), children, onClose && (jsx(Helpericon, { active: true, className: "i-tag-close i-tag-hover-close", onClick: onClose }))] }));
|
|
15
13
|
};
|
|
16
14
|
|
|
17
15
|
export { Tag as default };
|
|
@@ -6,21 +6,21 @@ import Icon from '../icon/icon.js';
|
|
|
6
6
|
import Loading from '../loading/loading.js';
|
|
7
7
|
|
|
8
8
|
const TreeItemHeader = (props) => {
|
|
9
|
-
const { as: Tag = "a", href, selected, children, ...restProps } = props;
|
|
9
|
+
const { as: Tag = "a", href, selected, children, attrs, ...restProps } = props;
|
|
10
10
|
const className = classNames("i-tree-item-header", {
|
|
11
11
|
"i-tree-item-selected": selected,
|
|
12
12
|
});
|
|
13
13
|
if (typeof Tag === "string") {
|
|
14
|
-
return (jsx(Tag, { href: href, className: className, ...restProps, children: children }));
|
|
14
|
+
return (jsx(Tag, { href: href, className: className, ...attrs, ...restProps, children: children }));
|
|
15
15
|
}
|
|
16
|
-
return (jsx(Tag, { to: href || "", className: className, ...restProps, children: children }));
|
|
16
|
+
return (jsx(Tag, { to: href || "", className: className, ...attrs, ...restProps, children: children }));
|
|
17
17
|
};
|
|
18
18
|
function TreeRow(props) {
|
|
19
19
|
const { flatNode, wrapperStyle, virtualMode, selected, checkedSet, partofs = {}, checkable, nodeProps, renderExtra, loadingKeys, onExpand, onItemClick, onItemSelect, onItemCheck, } = props;
|
|
20
20
|
const { node, depth, isExpanded } = flatNode;
|
|
21
|
-
const { key = "", as, href, icon, title, disabled, type } = node;
|
|
21
|
+
const { key = "", as, href, icon, title, disabled, type, attrs } = node;
|
|
22
22
|
const children = node[nodeProps.children];
|
|
23
|
-
const hasChildren = children instanceof Promise || (Array.isArray(children) && children.length > 0);
|
|
23
|
+
const hasChildren = children instanceof Promise || typeof children === "function" || (Array.isArray(children) && children.length > 0);
|
|
24
24
|
const loading = loadingKeys?.includes(key);
|
|
25
25
|
if (type === "title") {
|
|
26
26
|
return jsx("div", { style: wrapperStyle, className: "i-tree-group-title", children: title });
|
|
@@ -28,7 +28,7 @@ function TreeRow(props) {
|
|
|
28
28
|
if (type && type !== "item") {
|
|
29
29
|
return jsx("div", { style: wrapperStyle, className: `i-tree-type-${type}`, children: title });
|
|
30
30
|
}
|
|
31
|
-
return (jsx("div", { className: !virtualMode ? classNames("i-tree-item", { "i-tree-expand": isExpanded }) : undefined, style: wrapperStyle, children: jsxs(TreeItemHeader, { as: as, href: href, style: { paddingLeft: `${depth * 1.5 + 0.5}em` }, selected: selected === key, onClick: (e) => {
|
|
31
|
+
return (jsx("div", { className: !virtualMode ? classNames("i-tree-item", { "i-tree-expand": isExpanded }) : undefined, style: wrapperStyle, children: jsxs(TreeItemHeader, { as: as, attrs: attrs, href: href, style: { paddingLeft: `${depth * 1.5 + 0.5}em` }, selected: selected === key, onClick: (e) => {
|
|
32
32
|
if (disabled) {
|
|
33
33
|
e.preventDefault();
|
|
34
34
|
e.stopPropagation();
|
|
@@ -27,7 +27,7 @@ function flattenTree(nodes, expandedMap, nodeProps, depth = 0, parentItem, async
|
|
|
27
27
|
return result;
|
|
28
28
|
}
|
|
29
29
|
const Tree = (props) => {
|
|
30
|
-
const { data = [], ref, selected, checked = [], disabledRelated, nodeProps, height,
|
|
30
|
+
const { data = [], ref, selected, checked = [], disabledRelated, nodeProps, height, virtual, onItemSelect, onItemCheck, ...restProps } = props;
|
|
31
31
|
const [selectedKey, setSelectedKey] = useState(selected);
|
|
32
32
|
const [checkedKeys, setCheckedKeys] = useState(checked);
|
|
33
33
|
const [partofs, setPartofs] = useState({});
|
|
@@ -59,14 +59,16 @@ const Tree = (props) => {
|
|
|
59
59
|
if (!item)
|
|
60
60
|
return;
|
|
61
61
|
const rawChildren = item[oNodeProps.children];
|
|
62
|
-
const
|
|
62
|
+
const isLazy = typeof rawChildren === "function";
|
|
63
|
+
const isAsync = rawChildren instanceof Promise || isLazy;
|
|
63
64
|
const isExpanded = !!expandedMap[key];
|
|
64
65
|
if (isAsync && !isExpanded) {
|
|
65
66
|
flushSync(() => {
|
|
66
67
|
setLoadingMap((prev) => ({ ...prev, [key]: true }));
|
|
67
68
|
setExpandedMap((prev) => ({ ...prev, [key]: true }));
|
|
68
69
|
});
|
|
69
|
-
rawChildren
|
|
70
|
+
const promise = isLazy ? rawChildren() : rawChildren;
|
|
71
|
+
promise
|
|
70
72
|
.then((resolved) => {
|
|
71
73
|
item[oNodeProps.children] = resolved;
|
|
72
74
|
setAsyncChildrenMap((prev) => ({ ...prev, [key]: resolved }));
|
|
@@ -223,8 +225,8 @@ const Tree = (props) => {
|
|
|
223
225
|
},
|
|
224
226
|
};
|
|
225
227
|
});
|
|
226
|
-
if (
|
|
227
|
-
return (jsx(VirtualTree, { flatNodes: flatNodes, onExpand: handleExpand, height: height,
|
|
228
|
+
if (virtual) {
|
|
229
|
+
return (jsx(VirtualTree, { flatNodes: flatNodes, onExpand: handleExpand, height: height, virtual: virtual, selected: selectedKey, checkedSet: checkedSet, partofs: partofs, nodeProps: oNodeProps, loadingKeys: loadingKeys, onItemCheck: handleCheck, onItemSelect: handleSelect, ...restProps }));
|
|
228
230
|
}
|
|
229
231
|
return (jsx(TreeList, { flatNodes: flatNodes, onExpand: handleExpand, selected: selectedKey, checkedSet: checkedSet, partofs: partofs, nodeProps: oNodeProps, loadingKeys: loadingKeys, onItemCheck: handleCheck, onItemSelect: handleSelect, ...restProps }));
|
|
230
232
|
};
|
|
@@ -6,7 +6,7 @@ import { useResizeObserver } from '../../js/hooks.js';
|
|
|
6
6
|
import { TreeRow } from './item.js';
|
|
7
7
|
|
|
8
8
|
function VirtualTree(props) {
|
|
9
|
-
const { flatNodes, onExpand, selected, checkedSet, partofs = {}, checkable, nodeProps, renderExtra, loadingKeys, height,
|
|
9
|
+
const { flatNodes, onExpand, selected, checkedSet, partofs = {}, checkable, nodeProps, renderExtra, loadingKeys, height, virtual, className, style, onItemClick, onItemSelect, onItemCheck, } = props;
|
|
10
10
|
const listRef = useRef(null);
|
|
11
11
|
const wrapRef = useRef(null);
|
|
12
12
|
const ro = useResizeObserver();
|
|
@@ -33,7 +33,7 @@ function VirtualTree(props) {
|
|
|
33
33
|
return null;
|
|
34
34
|
return (jsx(TreeRow, { flatNode: flatNode, wrapperStyle: style, virtualMode: true, selected: p.selected, checkedSet: p.checkedSet, partofs: p.partofs, checkable: p.checkable, nodeProps: p.nodeProps, renderExtra: p.renderExtra, loadingKeys: p.loadingKeys, onExpand: p.onExpand, onItemClick: p.onItemClick, onItemSelect: p.onItemSelect, onItemCheck: p.onItemCheck }));
|
|
35
35
|
}, []);
|
|
36
|
-
return (jsx("div", { ref: wrapRef, className: classNames("i-tree", className), style: { display: "block", width: "100%", height: "100%", ...style }, children: jsx(List, { listRef: listRef, rowCount: flatNodes.length, rowHeight:
|
|
36
|
+
return (jsx("div", { ref: wrapRef, className: classNames("i-tree", className), style: { display: "block", width: "100%", height: "100%", ...style }, children: jsx(List, { listRef: listRef, rowCount: flatNodes.length, rowHeight: virtual.rowHeight, overscanCount: Math.max(3, virtual.threshold ?? 8), rowProps: {}, style: {
|
|
37
37
|
width: "100%",
|
|
38
38
|
height: listHeight,
|
|
39
39
|
overflow: "auto",
|
package/lib/es/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { default as Button } from './components/button/button.js';
|
|
|
4
4
|
export { default as Card } from './components/card/card.js';
|
|
5
5
|
export { default as Checkbox } from './components/checkbox/checkbox.js';
|
|
6
6
|
export { default as Collapse } from './components/collapse/collapse.js';
|
|
7
|
+
export { default as Pill } from './components/pill/pill.js';
|
|
7
8
|
export { default as Datagrid } from './components/datagrid/datagrid.js';
|
|
8
9
|
export { default as Description } from './components/description/description.js';
|
|
9
10
|
export { default as Drawer } from './components/drawer/drawer.js';
|