@skalfa/skalfa-component 1.0.24 → 1.0.25
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.
|
@@ -13,7 +13,7 @@ export interface SelectProps {
|
|
|
13
13
|
tip?: string | ReactNode;
|
|
14
14
|
leftIcon?: any;
|
|
15
15
|
rightIcon?: any;
|
|
16
|
-
value?: string | number | (string | number)[]
|
|
16
|
+
value?: string | number | (string | number)[] | Record<string, any>;
|
|
17
17
|
invalid?: string;
|
|
18
18
|
disabled?: boolean;
|
|
19
19
|
validations?: ValidationRules;
|
|
@@ -36,7 +36,9 @@ export interface SelectProps {
|
|
|
36
36
|
tempOptions?: SelectOptionProps[];
|
|
37
37
|
newOption?: SelectOptionProps;
|
|
38
38
|
maxShowOption?: number;
|
|
39
|
-
|
|
39
|
+
creatable?: boolean | string;
|
|
40
|
+
creatableLabel?: string;
|
|
41
|
+
onChange?: (value: any, data?: any) => any;
|
|
40
42
|
register?: (name: string, validations?: ValidationRules) => void;
|
|
41
43
|
unregister?: (name: string) => void;
|
|
42
44
|
onFocus?: () => void;
|
|
@@ -44,4 +46,4 @@ export interface SelectProps {
|
|
|
44
46
|
/** Use custom class with: "label::", "tip::", "error::", "icon::", "suggest::", "suggest-item::". */
|
|
45
47
|
className?: string;
|
|
46
48
|
}
|
|
47
|
-
export declare function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, value, invalid, disabled, validations, multiple, autoFocus, clearable, options, searchable, serverOptionControl, idbOptionControl, serverSearchable, includedOptions, exceptOptions, tempOptions, newOption, maxShowOption, register, unregister, onChange, onFocus, onBlur, className, }: SelectProps): import("@types/react").JSX.Element;
|
|
49
|
+
export declare function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, value, invalid, disabled, validations, multiple, autoFocus, clearable, options, searchable, serverOptionControl, idbOptionControl, serverSearchable, includedOptions, exceptOptions, tempOptions, newOption, maxShowOption, creatable, creatableLabel, register, unregister, onChange, onFocus, onBlur, className, }: SelectProps): import("@types/react").JSX.Element;
|
|
@@ -6,8 +6,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
6
6
|
const skalfa_icon_1 = require("@skalfa/skalfa-icon");
|
|
7
7
|
const react_1 = require("react");
|
|
8
8
|
const _utils_1 = require("@utils");
|
|
9
|
+
const OutsideClick_component_1 = require("../wrap/OutsideClick.component");
|
|
9
10
|
;
|
|
10
|
-
function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, value, invalid, disabled, validations, multiple, autoFocus, clearable, options = [], searchable, serverOptionControl, idbOptionControl, serverSearchable, includedOptions = [], exceptOptions = [], tempOptions, newOption, maxShowOption = 10, register, unregister, onChange, onFocus, onBlur, className = "", }) {
|
|
11
|
+
function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, value, invalid, disabled, validations, multiple, autoFocus, clearable, options = [], searchable, serverOptionControl, idbOptionControl, serverSearchable, includedOptions = [], exceptOptions = [], tempOptions, newOption, maxShowOption = 10, creatable, creatableLabel, register, unregister, onChange, onFocus, onBlur, className = "", }) {
|
|
11
12
|
const [inputShowValue, setInputShowValue] = (0, react_1.useState)("");
|
|
12
13
|
const [keydown, setKeydown] = (0, react_1.useState)(false);
|
|
13
14
|
const [useTemp, setUseTemp] = (0, react_1.useState)(true);
|
|
@@ -18,6 +19,26 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
18
19
|
const [showOption, setShowOption] = (0, react_1.useState)(false);
|
|
19
20
|
const [keyword, setKeyword] = (0, react_1.useState)("");
|
|
20
21
|
const [keywordSearch] = (0, _utils_1.useLazySearch)(keyword);
|
|
22
|
+
const creatableKey = typeof creatable === "string" ? (creatable || "name") : (creatable ? "name" : null);
|
|
23
|
+
const [isCreatingNew, setIsCreatingNew] = (0, react_1.useState)(false);
|
|
24
|
+
const [customText, setCustomText] = (0, react_1.useState)("");
|
|
25
|
+
const [dropdownCustomText, setDropdownCustomText] = (0, react_1.useState)("");
|
|
26
|
+
const isFocusingCreatable = (0, react_1.useRef)(false);
|
|
27
|
+
const handleCreateNewSubmit = () => {
|
|
28
|
+
if (!dropdownCustomText.trim() || !creatableKey)
|
|
29
|
+
return;
|
|
30
|
+
isFocusingCreatable.current = false;
|
|
31
|
+
const valText = dropdownCustomText.trim();
|
|
32
|
+
setIsCreatingNew(true);
|
|
33
|
+
setCustomText(valText);
|
|
34
|
+
setInputShowValue(valText);
|
|
35
|
+
setShowOption(false);
|
|
36
|
+
setFilteredOptions([]);
|
|
37
|
+
const objVal = { [creatableKey]: valText };
|
|
38
|
+
inputHandler.setValue(objVal);
|
|
39
|
+
inputHandler.setIdle(false);
|
|
40
|
+
onChange?.(objVal);
|
|
41
|
+
};
|
|
21
42
|
// =========================>
|
|
22
43
|
// ## Initial
|
|
23
44
|
// =========================>
|
|
@@ -28,29 +49,43 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
28
49
|
// =========================>
|
|
29
50
|
const [invalidMessage] = (0, _utils_1.useValidation)(inputHandler.value, validations, invalid, inputHandler.idle);
|
|
30
51
|
// =========================>
|
|
31
|
-
// ##
|
|
52
|
+
// ## options handler
|
|
32
53
|
// =========================>
|
|
54
|
+
const optionsKey = typeof options === "object" ? JSON.stringify(options) : String(options || "");
|
|
55
|
+
const includedOptionsKey = typeof includedOptions === "object" ? JSON.stringify(includedOptions) : String(includedOptions || "");
|
|
56
|
+
const exceptOptionsKey = typeof exceptOptions === "object" ? JSON.stringify(exceptOptions) : String(exceptOptions || "");
|
|
33
57
|
(0, react_1.useEffect)(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
inputHandler.setIdle(false);
|
|
58
|
+
let base = options?.length ? [...options, ...includedOptions] : [...includedOptions];
|
|
59
|
+
if (exceptOptions?.length) {
|
|
60
|
+
base = base.filter((op) => !exceptOptions.includes(op.value));
|
|
38
61
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}, [value, dataOptions]);
|
|
62
|
+
setDataOptions(base);
|
|
63
|
+
}, [optionsKey, includedOptionsKey, exceptOptionsKey]);
|
|
64
|
+
const dataOptionsKey = dataOptions.map((op) => String(op.value)).join("|");
|
|
44
65
|
// =========================>
|
|
45
|
-
// ##
|
|
66
|
+
// ## change value handler
|
|
46
67
|
// =========================>
|
|
47
68
|
(0, react_1.useEffect)(() => {
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
const currentVal = inputHandler.value;
|
|
70
|
+
if (currentVal && typeof currentVal === "object" && !Array.isArray(currentVal) && creatableKey && creatableKey in currentVal) {
|
|
71
|
+
setIsCreatingNew(true);
|
|
72
|
+
const text = String(currentVal[creatableKey] || "");
|
|
73
|
+
setCustomText(text);
|
|
74
|
+
setInputShowValue(text);
|
|
75
|
+
}
|
|
76
|
+
else if (currentVal !== undefined && currentVal !== null && currentVal !== "" && typeof currentVal !== "object") {
|
|
77
|
+
setIsCreatingNew(false);
|
|
78
|
+
const matched = (newOption ? [newOption, ...dataOptions] : dataOptions)?.find((option) => option.value == currentVal);
|
|
79
|
+
setInputShowValue(matched?.label || currentVal);
|
|
80
|
+
}
|
|
81
|
+
else if (!currentVal && !isCreatingNew) {
|
|
82
|
+
setInputShowValue("");
|
|
83
|
+
}
|
|
84
|
+
}, [inputHandler.value, dataOptionsKey, creatableKey]);
|
|
50
85
|
const filterOption = (e) => {
|
|
51
|
-
if (dataOptions?.length) {
|
|
86
|
+
if (dataOptions?.length || creatableKey) {
|
|
52
87
|
let newFilteredOptions = [];
|
|
53
|
-
if (searchable && !serverSearchable) {
|
|
88
|
+
if (searchable && !serverSearchable && dataOptions?.length) {
|
|
54
89
|
if (e.target.value) {
|
|
55
90
|
newFilteredOptions = dataOptions.filter((Option) => Option.label?.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1).slice(0, maxShowOption);
|
|
56
91
|
}
|
|
@@ -59,7 +94,7 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
59
94
|
}
|
|
60
95
|
}
|
|
61
96
|
else {
|
|
62
|
-
newFilteredOptions = dataOptions;
|
|
97
|
+
newFilteredOptions = dataOptions || [];
|
|
63
98
|
}
|
|
64
99
|
setActiveOption(-1);
|
|
65
100
|
setFilteredOptions(newFilteredOptions);
|
|
@@ -73,6 +108,16 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
73
108
|
setActiveOption(-1);
|
|
74
109
|
setFilteredOptions([]);
|
|
75
110
|
setShowOption(false);
|
|
111
|
+
if (resultValue?.value === "__CREATE_NEW__" && creatableKey) {
|
|
112
|
+
setIsCreatingNew(true);
|
|
113
|
+
setCustomText("");
|
|
114
|
+
setInputShowValue("");
|
|
115
|
+
const objVal = { [creatableKey]: "" };
|
|
116
|
+
inputHandler.setValue(objVal);
|
|
117
|
+
onChange?.(objVal, resultValue);
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
76
121
|
if (!multiple) {
|
|
77
122
|
setInputShowValue(resultValue?.label || inputShowValue);
|
|
78
123
|
inputHandler.setValue(resultValue?.value || inputShowValue);
|
|
@@ -176,25 +221,42 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
176
221
|
}
|
|
177
222
|
}
|
|
178
223
|
}, [keywordSearch, serverOptionControl?.path, serverOptionControl?.url]);
|
|
179
|
-
return ((0, jsx_runtime_1.jsx)(
|
|
224
|
+
return ((0, jsx_runtime_1.jsx)(OutsideClick_component_1.OutsideClickComponent, { onOutsideClick: () => {
|
|
225
|
+
isFocusingCreatable.current = false;
|
|
226
|
+
setShowOption(false);
|
|
227
|
+
inputHandler.setFocus(false);
|
|
228
|
+
}, children: (0, jsx_runtime_1.jsxs)("div", { className: "input-container", children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: randomId, className: (0, _utils_1.cn)("input-label", disabled && "input-label-disabled", inputHandler.focus && "input-label-focus", invalidMessage && "input-label-error", (0, _utils_1.pcn)(className, "label"), disabled && (0, _utils_1.pcn)(className, "label", "disabled"), inputHandler.focus && (0, _utils_1.pcn)(className, "label", "focus"), invalidMessage && (0, _utils_1.pcn)(className, "label", "error")), children: [label, validations && _utils_1.validation.hasRules(validations, "required") && (0, jsx_runtime_1.jsx)("span", { className: "text-danger ml-1", children: "*" })] }), tip && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-tip", disabled && "input-tip-disabled", (0, _utils_1.pcn)(className, "tip"), disabled && (0, _utils_1.pcn)(className, "tip", "disabled")), children: tip })), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [isCreatingNew && typeof inputHandler.value === "object" && inputHandler.value !== null ? (Object.entries(inputHandler.value).map(([subK, subV]) => ((0, jsx_runtime_1.jsx)("input", { type: "hidden", name: `${name.endsWith("_id") ? name.slice(0, -3) : name}[${subK}]`, value: String(subV ?? "") }, subK)))) : ((0, jsx_runtime_1.jsx)("input", { type: "hidden", value: !multiple ? (typeof inputHandler.value === "object" ? JSON.stringify(inputHandler.value) : String(inputHandler.value ?? "")) : Array().concat(inputHandler.value).map((val) => String(val)), name: name })), isCreatingNew && ((0, jsx_runtime_1.jsx)("div", { className: "absolute left-1.5 top-1/2 -translate-y-1/2 bg-stroke text-sm px-3.5 py-1.5 rounded-lg select-none pointer-events-none z-10 font-normal", children: "Baru" })), (0, jsx_runtime_1.jsx)("input", { type: "text", readOnly: !searchable, id: randomId, placeholder: !inputHandler.value || (Array.isArray(inputHandler.value) && !inputHandler.value.length) ? placeholder : "", disabled: disabled, className: (0, _utils_1.cn)("input cursor-pointer", leftIcon && "input-with-left-icon", rightIcon && "input-with-right-icon", isCreatingNew && "pl-[4.8rem]", invalidMessage && "input-error", (0, _utils_1.pcn)(className, "input"), invalidMessage && (0, _utils_1.pcn)(className, "input", "error")), value: (useTemp && tempOptions ? tempOptions.at(0)?.label : serverSearchable ? keyword : (isCreatingNew ? customText : inputShowValue)), onChange: (e) => {
|
|
180
229
|
setUseTemp(false);
|
|
181
230
|
searchable && setInputShowValue(e.target.value);
|
|
182
231
|
serverSearchable && setKeyword(e.target.value);
|
|
183
232
|
inputHandler.setIdle(false);
|
|
184
|
-
dataOptions?.length && filterOption(e);
|
|
233
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
185
234
|
}, onFocus: (e) => {
|
|
186
235
|
setUseTemp(false);
|
|
187
236
|
inputHandler.setFocus(true);
|
|
188
237
|
onFocus?.();
|
|
189
|
-
dataOptions?.length && filterOption(e);
|
|
238
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
190
239
|
searchable && e.target.select();
|
|
191
240
|
}, onBlur: (e) => {
|
|
192
241
|
setUseTemp(false);
|
|
242
|
+
if (isFocusingCreatable.current)
|
|
243
|
+
return;
|
|
244
|
+
if (isCreatingNew) {
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
if (!isFocusingCreatable.current) {
|
|
247
|
+
inputHandler.setFocus(false);
|
|
248
|
+
}
|
|
249
|
+
}, 100);
|
|
250
|
+
onBlur?.();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
193
253
|
const value = e.target.value;
|
|
194
254
|
const valueOption = dataOptions?.find((option) => option.label?.toLowerCase() == value?.toLowerCase());
|
|
195
255
|
if (!keydown) {
|
|
196
256
|
if (!multiple) {
|
|
197
257
|
setTimeout(() => {
|
|
258
|
+
if (isFocusingCreatable.current)
|
|
259
|
+
return;
|
|
198
260
|
if (valueOption?.value) {
|
|
199
261
|
setInputShowValue(valueOption.label);
|
|
200
262
|
inputHandler.setValue(valueOption.value);
|
|
@@ -216,7 +278,9 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
216
278
|
}
|
|
217
279
|
}
|
|
218
280
|
setTimeout(() => {
|
|
219
|
-
|
|
281
|
+
if (!isFocusingCreatable.current) {
|
|
282
|
+
inputHandler.setFocus(false);
|
|
283
|
+
}
|
|
220
284
|
}, 100);
|
|
221
285
|
onBlur?.();
|
|
222
286
|
}, onKeyDown: (e) => {
|
|
@@ -233,42 +297,79 @@ function SelectComponent({ name, label, placeholder, tip, leftIcon, rightIcon, v
|
|
|
233
297
|
}
|
|
234
298
|
} })] }, key));
|
|
235
299
|
}) }) })), leftIcon && ((0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { className: (0, _utils_1.cn)("input-icon", "input-icon-left", disabled && "input-icon-disabled", inputHandler.focus && "input-icon-focus", (0, _utils_1.pcn)(className, "icon"), disabled && (0, _utils_1.pcn)(className, "icon", "disabled"), inputHandler.focus && (0, _utils_1.pcn)(className, "icon", "focus")), icon: leftIcon })), !multiple && clearable && inputHandler.value && ((0, jsx_runtime_1.jsx)("div", { className: (0, _utils_1.cn)("input-icon", "input-icon-clear", disabled && "input-icon-disabled", (0, _utils_1.pcn)(className, "icon"), disabled && (0, _utils_1.pcn)(className, "icon", "disabled")), onClick: () => {
|
|
300
|
+
setIsCreatingNew(false);
|
|
301
|
+
setCustomText("");
|
|
302
|
+
setDropdownCustomText("");
|
|
236
303
|
setInputShowValue("");
|
|
237
304
|
inputHandler.setValue("");
|
|
238
305
|
onChange?.("");
|
|
239
|
-
}, children: (0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/times" }) })), (0, jsx_runtime_1.jsx)("label", { htmlFor: randomId, className: (0, _utils_1.cn)("input-icon", "input-icon-right", "select-icon-dropdown", disabled && "input-icon-disabled", (0, _utils_1.pcn)(className, "icon"), disabled && (0, _utils_1.pcn)(className, "icon", "disabled")),
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
306
|
+
}, children: (0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/times" }) })), (0, jsx_runtime_1.jsx)("label", { htmlFor: randomId, className: (0, _utils_1.cn)("input-icon", "input-icon-right", "select-icon-dropdown cursor-pointer", disabled && "input-icon-disabled", (0, _utils_1.pcn)(className, "icon"), disabled && (0, _utils_1.pcn)(className, "icon", "disabled")), onClick: (e) => {
|
|
307
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
308
|
+
}, children: (0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/chevron-down" }) })] }), (!!dataOptions?.length || creatableKey) && showOption && !loadingOption && ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsxs)("ul", { className: (0, _utils_1.cn)("input-suggest-container scroll-sm", inputHandler.focus && "input-suggest-container-active", (0, _utils_1.pcn)(className, "suggest")), children: [creatableKey && ((0, jsx_runtime_1.jsxs)("li", { className: "p-2 border-b border-stroke flex items-center gap-2 cursor-default bg-white sticky top-0 z-10 mb-2", onMouseDown: (e) => {
|
|
309
|
+
isFocusingCreatable.current = true;
|
|
310
|
+
e.stopPropagation();
|
|
311
|
+
}, onMouseUp: (e) => {
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
}, onClick: (e) => {
|
|
314
|
+
e.stopPropagation();
|
|
315
|
+
}, children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "input flex-1 text-sm py-1.5 px-3 rounded-lg border border-stroke focus:outline-none focus:border-primary placeholder:text-slate-300 text-slate-700 bg-white", placeholder: creatableLabel || "Masukkan pilihan baru...", value: dropdownCustomText, onMouseDown: (e) => {
|
|
316
|
+
isFocusingCreatable.current = true;
|
|
317
|
+
e.stopPropagation();
|
|
318
|
+
}, onFocus: () => {
|
|
319
|
+
isFocusingCreatable.current = true;
|
|
320
|
+
}, onBlur: () => {
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
if (!isFocusingCreatable.current) {
|
|
323
|
+
setShowOption(false);
|
|
324
|
+
inputHandler.setFocus(false);
|
|
325
|
+
}
|
|
326
|
+
}, 200);
|
|
327
|
+
}, onChange: (e) => setDropdownCustomText(e.target.value), onKeyDown: (e) => {
|
|
328
|
+
e.stopPropagation();
|
|
329
|
+
if (e.key === "Enter") {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
handleCreateNewSubmit();
|
|
332
|
+
}
|
|
333
|
+
} }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "bg-light-primary text-primary p-2 rounded-lg flex items-center justify-center transition-colors shrink-0 shadow-sm cursor-pointer border-none", onMouseDown: (e) => {
|
|
334
|
+
isFocusingCreatable.current = true;
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
}, onClick: (e) => {
|
|
337
|
+
e.stopPropagation();
|
|
338
|
+
handleCreateNewSubmit();
|
|
339
|
+
}, children: (0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/check", className: "w-4 h-4" }) })] })), (newOption ? [newOption, ...filteredOptions] : filteredOptions).map((option, key) => {
|
|
340
|
+
const selected = !isCreatingNew && (!!((typeof inputHandler.value == "string" || typeof inputHandler.value == "number") && inputHandler.value == option.value) ||
|
|
341
|
+
(Array.isArray(inputHandler.value) && Array().concat(inputHandler.value).find((val) => val == option.value)));
|
|
342
|
+
return ((0, jsx_runtime_1.jsxs)("li", { className: (0, _utils_1.cn)("input-suggest", (key == activeOption || selected) && "input-suggest-active", (0, _utils_1.pcn)(className, "suggest-item"), (key == activeOption || selected) && (0, _utils_1.pcn)(className, "suggest-item", "active")), onMouseDown: () => {
|
|
343
|
+
setKeydown(true);
|
|
344
|
+
setTimeout(() => inputHandler.setFocus(true), 110);
|
|
345
|
+
}, onMouseUp: () => {
|
|
346
|
+
setKeydown(false);
|
|
347
|
+
setActiveOption(key);
|
|
348
|
+
setFilteredOptions([]);
|
|
349
|
+
setShowOption(false);
|
|
350
|
+
setIsCreatingNew(false);
|
|
351
|
+
if (!multiple) {
|
|
352
|
+
setInputShowValue(option.label);
|
|
353
|
+
serverSearchable && setKeyword(option.label);
|
|
354
|
+
inputHandler.setValue(option.value);
|
|
355
|
+
onChange?.(option.value, option);
|
|
265
356
|
}
|
|
266
357
|
else {
|
|
267
|
-
|
|
268
|
-
|
|
358
|
+
const values = Array.isArray(inputHandler.value)
|
|
359
|
+
? Array().concat(inputHandler.value).filter((val) => val != option.value)
|
|
360
|
+
: [];
|
|
361
|
+
setInputShowValue("");
|
|
362
|
+
serverSearchable && setKeyword("");
|
|
363
|
+
if (Array.isArray(inputHandler.value) && Array().concat(inputHandler.value).find((val) => val == option.value)) {
|
|
364
|
+
inputHandler.setValue(values);
|
|
365
|
+
onChange?.(values);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
inputHandler.setValue([...Array().concat(values), option.value]);
|
|
369
|
+
onChange?.([...Array().concat(values), option.value]);
|
|
370
|
+
}
|
|
269
371
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}) }) })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage }))] }) }));
|
|
372
|
+
setTimeout(() => inputHandler.setFocus(false), 120);
|
|
373
|
+
}, children: [selected && ((0, jsx_runtime_1.jsx)(skalfa_icon_1.Icon, { icon: "solid/check", className: "select-suggest-check" })), option.label] }, key));
|
|
374
|
+
})] }) })), invalidMessage && ((0, jsx_runtime_1.jsx)("small", { className: (0, _utils_1.cn)("input-error-message", (0, _utils_1.pcn)(className, "error")), children: invalidMessage }))] }) }));
|
|
274
375
|
}
|
package/package.json
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { Icon, type IconName } from "@skalfa/skalfa-icon";
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
import { ReactNode, useEffect, useState } from "react";
|
|
5
|
+
import { ReactNode, useEffect, useRef, useState } from "react";
|
|
6
6
|
import { api, ApiType, cavity, cn, pcn, registry, useInputHandler, useInputRandomId, useLazySearch, useValidation, validation, ValidationRules,} from "@utils";
|
|
7
|
+
import { OutsideClickComponent } from "../wrap/OutsideClick.component";
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
|
|
@@ -24,7 +25,7 @@ export interface SelectProps {
|
|
|
24
25
|
leftIcon ?: any;
|
|
25
26
|
rightIcon ?: any;
|
|
26
27
|
|
|
27
|
-
value ?: string | number | (string | number)[]
|
|
28
|
+
value ?: string | number | (string | number)[] | Record<string, any>;
|
|
28
29
|
invalid ?: string;
|
|
29
30
|
disabled ?: boolean;
|
|
30
31
|
validations ?: ValidationRules;
|
|
@@ -42,8 +43,10 @@ export interface SelectProps {
|
|
|
42
43
|
tempOptions ?: SelectOptionProps[];
|
|
43
44
|
newOption ?: SelectOptionProps;
|
|
44
45
|
maxShowOption ?: number;
|
|
46
|
+
creatable ?: boolean | string;
|
|
47
|
+
creatableLabel ?: string;
|
|
45
48
|
|
|
46
|
-
onChange ?: (value:
|
|
49
|
+
onChange ?: (value: any, data?: any) => any;
|
|
47
50
|
register ?: (name: string, validations?: ValidationRules) => void;
|
|
48
51
|
unregister ?: (name: string) => void;
|
|
49
52
|
onFocus ?: () => void;
|
|
@@ -81,6 +84,8 @@ export function SelectComponent({
|
|
|
81
84
|
tempOptions,
|
|
82
85
|
newOption,
|
|
83
86
|
maxShowOption = 10,
|
|
87
|
+
creatable,
|
|
88
|
+
creatableLabel,
|
|
84
89
|
|
|
85
90
|
register,
|
|
86
91
|
unregister,
|
|
@@ -101,6 +106,28 @@ export function SelectComponent({
|
|
|
101
106
|
const [keyword, setKeyword] = useState("");
|
|
102
107
|
const [keywordSearch] = useLazySearch(keyword);
|
|
103
108
|
|
|
109
|
+
const creatableKey = typeof creatable === "string" ? (creatable || "name") : (creatable ? "name" : null);
|
|
110
|
+
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
|
111
|
+
const [customText, setCustomText] = useState("");
|
|
112
|
+
const [dropdownCustomText, setDropdownCustomText] = useState("");
|
|
113
|
+
const isFocusingCreatable = useRef(false);
|
|
114
|
+
|
|
115
|
+
const handleCreateNewSubmit = () => {
|
|
116
|
+
if (!dropdownCustomText.trim() || !creatableKey) return;
|
|
117
|
+
|
|
118
|
+
isFocusingCreatable.current = false;
|
|
119
|
+
const valText = dropdownCustomText.trim();
|
|
120
|
+
setIsCreatingNew(true);
|
|
121
|
+
setCustomText(valText);
|
|
122
|
+
setInputShowValue(valText);
|
|
123
|
+
setShowOption(false);
|
|
124
|
+
setFilteredOptions([]);
|
|
125
|
+
|
|
126
|
+
const objVal = { [creatableKey]: valText };
|
|
127
|
+
inputHandler.setValue(objVal);
|
|
128
|
+
inputHandler.setIdle(false);
|
|
129
|
+
onChange?.(objVal);
|
|
130
|
+
};
|
|
104
131
|
|
|
105
132
|
// =========================>
|
|
106
133
|
// ## Initial
|
|
@@ -116,40 +143,54 @@ export function SelectComponent({
|
|
|
116
143
|
|
|
117
144
|
|
|
118
145
|
// =========================>
|
|
119
|
-
// ##
|
|
146
|
+
// ## options handler
|
|
120
147
|
// =========================>
|
|
148
|
+
const optionsKey = typeof options === "object" ? JSON.stringify(options) : String(options || "");
|
|
149
|
+
const includedOptionsKey = typeof includedOptions === "object" ? JSON.stringify(includedOptions) : String(includedOptions || "");
|
|
150
|
+
const exceptOptionsKey = typeof exceptOptions === "object" ? JSON.stringify(exceptOptions) : String(exceptOptions || "");
|
|
151
|
+
|
|
121
152
|
useEffect(() => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
inputHandler.setIdle(false);
|
|
126
|
-
} else {
|
|
127
|
-
inputHandler.setValue("");
|
|
128
|
-
setInputShowValue("");
|
|
153
|
+
let base = options?.length ? [...options, ...includedOptions] : [...includedOptions];
|
|
154
|
+
if (exceptOptions?.length) {
|
|
155
|
+
base = base.filter((op: SelectOptionProps) => !exceptOptions.includes(op.value));
|
|
129
156
|
}
|
|
130
|
-
|
|
157
|
+
setDataOptions(base);
|
|
158
|
+
}, [optionsKey, includedOptionsKey, exceptOptionsKey]);
|
|
131
159
|
|
|
160
|
+
const dataOptionsKey = dataOptions.map((op) => String(op.value)).join("|");
|
|
132
161
|
|
|
133
162
|
// =========================>
|
|
134
|
-
// ##
|
|
163
|
+
// ## change value handler
|
|
135
164
|
// =========================>
|
|
136
165
|
useEffect(() => {
|
|
137
|
-
|
|
138
|
-
|
|
166
|
+
const currentVal = inputHandler.value;
|
|
167
|
+
if (currentVal && typeof currentVal === "object" && !Array.isArray(currentVal) && creatableKey && creatableKey in currentVal) {
|
|
168
|
+
setIsCreatingNew(true);
|
|
169
|
+
const text = String((currentVal as Record<string, any>)[creatableKey] || "");
|
|
170
|
+
setCustomText(text);
|
|
171
|
+
setInputShowValue(text);
|
|
172
|
+
} else if (currentVal !== undefined && currentVal !== null && currentVal !== "" && typeof currentVal !== "object") {
|
|
173
|
+
setIsCreatingNew(false);
|
|
174
|
+
const matched = (newOption ? [newOption, ...dataOptions] : dataOptions)?.find((option) => option.value == currentVal);
|
|
175
|
+
setInputShowValue(matched?.label || currentVal);
|
|
176
|
+
} else if (!currentVal && !isCreatingNew) {
|
|
177
|
+
setInputShowValue("");
|
|
178
|
+
}
|
|
179
|
+
}, [inputHandler.value, dataOptionsKey, creatableKey]);
|
|
139
180
|
|
|
140
181
|
|
|
141
182
|
const filterOption = (e: any) => {
|
|
142
|
-
if (dataOptions?.length) {
|
|
183
|
+
if (dataOptions?.length || creatableKey) {
|
|
143
184
|
let newFilteredOptions: SelectOptionProps[] = [];
|
|
144
185
|
|
|
145
|
-
if (searchable && !serverSearchable) {
|
|
186
|
+
if (searchable && !serverSearchable && dataOptions?.length) {
|
|
146
187
|
if (e.target.value) {
|
|
147
188
|
newFilteredOptions = dataOptions.filter((Option) => (Option.label as string)?.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1).slice(0, maxShowOption);
|
|
148
189
|
} else {
|
|
149
190
|
newFilteredOptions = dataOptions.slice(0, maxShowOption);
|
|
150
191
|
}
|
|
151
192
|
} else {
|
|
152
|
-
newFilteredOptions = dataOptions;
|
|
193
|
+
newFilteredOptions = dataOptions || [];
|
|
153
194
|
}
|
|
154
195
|
|
|
155
196
|
setActiveOption(-1);
|
|
@@ -166,6 +207,18 @@ export function SelectComponent({
|
|
|
166
207
|
setActiveOption(-1);
|
|
167
208
|
setFilteredOptions([]);
|
|
168
209
|
setShowOption(false);
|
|
210
|
+
|
|
211
|
+
if (resultValue?.value === "__CREATE_NEW__" && creatableKey) {
|
|
212
|
+
setIsCreatingNew(true);
|
|
213
|
+
setCustomText("");
|
|
214
|
+
setInputShowValue("");
|
|
215
|
+
const objVal = { [creatableKey]: "" };
|
|
216
|
+
inputHandler.setValue(objVal);
|
|
217
|
+
onChange?.(objVal, resultValue);
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
169
222
|
if (!multiple) {
|
|
170
223
|
setInputShowValue(resultValue?.label || inputShowValue);
|
|
171
224
|
inputHandler.setValue(resultValue?.value || inputShowValue);
|
|
@@ -284,7 +337,13 @@ export function SelectComponent({
|
|
|
284
337
|
}, [keywordSearch, serverOptionControl?.path, serverOptionControl?.url]);
|
|
285
338
|
|
|
286
339
|
return (
|
|
287
|
-
|
|
340
|
+
<OutsideClickComponent
|
|
341
|
+
onOutsideClick={() => {
|
|
342
|
+
isFocusingCreatable.current = false;
|
|
343
|
+
setShowOption(false);
|
|
344
|
+
inputHandler.setFocus(false);
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
288
347
|
<div className="input-container">
|
|
289
348
|
<label
|
|
290
349
|
htmlFor={randomId}
|
|
@@ -315,11 +374,27 @@ export function SelectComponent({
|
|
|
315
374
|
)}
|
|
316
375
|
|
|
317
376
|
<div className="relative">
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
377
|
+
{isCreatingNew && typeof inputHandler.value === "object" && inputHandler.value !== null ? (
|
|
378
|
+
Object.entries(inputHandler.value).map(([subK, subV]) => (
|
|
379
|
+
<input
|
|
380
|
+
key={subK}
|
|
381
|
+
type="hidden"
|
|
382
|
+
name={`${name.endsWith("_id") ? name.slice(0, -3) : name}[${subK}]`}
|
|
383
|
+
value={String(subV ?? "")}
|
|
384
|
+
/>
|
|
385
|
+
))
|
|
386
|
+
) : (
|
|
387
|
+
<input
|
|
388
|
+
type="hidden"
|
|
389
|
+
value={!multiple ? (typeof inputHandler.value === "object" ? JSON.stringify(inputHandler.value) : String(inputHandler.value ?? "")) : Array().concat(inputHandler.value).map((val) => String(val))}
|
|
390
|
+
name={name}
|
|
391
|
+
/>
|
|
392
|
+
)}
|
|
393
|
+
{isCreatingNew && (
|
|
394
|
+
<div className="absolute left-1.5 top-1/2 -translate-y-1/2 bg-stroke text-sm px-3.5 py-1.5 rounded-lg select-none pointer-events-none z-10 font-normal">
|
|
395
|
+
Baru
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
323
398
|
<input
|
|
324
399
|
type="text"
|
|
325
400
|
readOnly={!searchable}
|
|
@@ -330,33 +405,46 @@ export function SelectComponent({
|
|
|
330
405
|
"input cursor-pointer",
|
|
331
406
|
leftIcon && "input-with-left-icon",
|
|
332
407
|
rightIcon && "input-with-right-icon",
|
|
408
|
+
isCreatingNew && "pl-[4.8rem]",
|
|
333
409
|
invalidMessage && "input-error",
|
|
334
410
|
pcn<CT>(className, "input"),
|
|
335
411
|
invalidMessage && pcn<CT>(className, "input", "error")
|
|
336
412
|
)}
|
|
337
|
-
value={(useTemp && tempOptions ? tempOptions.at(0)?.label : serverSearchable ? keyword : inputShowValue) as string}
|
|
413
|
+
value={(useTemp && tempOptions ? tempOptions.at(0)?.label : serverSearchable ? keyword : (isCreatingNew ? customText : inputShowValue)) as string}
|
|
338
414
|
onChange={(e) => {
|
|
339
415
|
setUseTemp(false);
|
|
340
416
|
searchable && setInputShowValue(e.target.value);
|
|
341
417
|
serverSearchable && setKeyword(e.target.value);
|
|
342
418
|
inputHandler.setIdle(false);
|
|
343
|
-
dataOptions?.length && filterOption(e);
|
|
419
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
344
420
|
}}
|
|
345
421
|
onFocus={(e) => {
|
|
346
422
|
setUseTemp(false);
|
|
347
423
|
inputHandler.setFocus(true);
|
|
348
424
|
onFocus?.();
|
|
349
|
-
dataOptions?.length && filterOption(e);
|
|
425
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
350
426
|
searchable && e.target.select();
|
|
351
427
|
}}
|
|
352
428
|
onBlur={(e) => {
|
|
353
429
|
setUseTemp(false);
|
|
430
|
+
if (isFocusingCreatable.current) return;
|
|
431
|
+
|
|
432
|
+
if (isCreatingNew) {
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
if (!isFocusingCreatable.current) {
|
|
435
|
+
inputHandler.setFocus(false);
|
|
436
|
+
}
|
|
437
|
+
}, 100);
|
|
438
|
+
onBlur?.();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
354
441
|
const value = e.target.value;
|
|
355
442
|
const valueOption = dataOptions?.find((option) => (option.label as string)?.toLowerCase() == value?.toLowerCase());
|
|
356
443
|
|
|
357
444
|
if (!keydown) {
|
|
358
445
|
if (!multiple) {
|
|
359
446
|
setTimeout(() => {
|
|
447
|
+
if (isFocusingCreatable.current) return;
|
|
360
448
|
if (valueOption?.value) {
|
|
361
449
|
setInputShowValue(valueOption.label);
|
|
362
450
|
inputHandler.setValue(valueOption.value);
|
|
@@ -377,7 +465,9 @@ export function SelectComponent({
|
|
|
377
465
|
}
|
|
378
466
|
|
|
379
467
|
setTimeout(() => {
|
|
380
|
-
|
|
468
|
+
if (!isFocusingCreatable.current) {
|
|
469
|
+
inputHandler.setFocus(false);
|
|
470
|
+
}
|
|
381
471
|
}, 100);
|
|
382
472
|
|
|
383
473
|
onBlur?.();
|
|
@@ -452,6 +542,9 @@ export function SelectComponent({
|
|
|
452
542
|
disabled && pcn<CT>(className, "icon", "disabled")
|
|
453
543
|
)}
|
|
454
544
|
onClick={() => {
|
|
545
|
+
setIsCreatingNew(false);
|
|
546
|
+
setCustomText("");
|
|
547
|
+
setDropdownCustomText("");
|
|
455
548
|
setInputShowValue("");
|
|
456
549
|
inputHandler.setValue("");
|
|
457
550
|
onChange?.("");
|
|
@@ -466,17 +559,20 @@ export function SelectComponent({
|
|
|
466
559
|
className={cn(
|
|
467
560
|
"input-icon",
|
|
468
561
|
"input-icon-right",
|
|
469
|
-
"select-icon-dropdown",
|
|
562
|
+
"select-icon-dropdown cursor-pointer",
|
|
470
563
|
disabled && "input-icon-disabled",
|
|
471
564
|
pcn<CT>(className, "icon"),
|
|
472
565
|
disabled && pcn<CT>(className, "icon", "disabled")
|
|
473
566
|
)}
|
|
567
|
+
onClick={(e) => {
|
|
568
|
+
(dataOptions?.length || creatableKey) && filterOption(e);
|
|
569
|
+
}}
|
|
474
570
|
>
|
|
475
571
|
<Icon icon="solid/chevron-down" />
|
|
476
572
|
</label>
|
|
477
573
|
</div>
|
|
478
574
|
|
|
479
|
-
{!!dataOptions?.length && showOption && !loadingOption &&
|
|
575
|
+
{(!!dataOptions?.length || creatableKey) && showOption && !loadingOption && (
|
|
480
576
|
<div>
|
|
481
577
|
<ul
|
|
482
578
|
className={cn(
|
|
@@ -485,9 +581,71 @@ export function SelectComponent({
|
|
|
485
581
|
pcn<CT>(className, "suggest"),
|
|
486
582
|
)}
|
|
487
583
|
>
|
|
488
|
-
{
|
|
489
|
-
|
|
490
|
-
|
|
584
|
+
{creatableKey && (
|
|
585
|
+
<li
|
|
586
|
+
className="p-2 border-b border-stroke flex items-center gap-2 cursor-default bg-white sticky top-0 z-10 mb-2"
|
|
587
|
+
onMouseDown={(e) => {
|
|
588
|
+
isFocusingCreatable.current = true;
|
|
589
|
+
e.stopPropagation();
|
|
590
|
+
}}
|
|
591
|
+
onMouseUp={(e) => {
|
|
592
|
+
e.stopPropagation();
|
|
593
|
+
}}
|
|
594
|
+
onClick={(e) => {
|
|
595
|
+
e.stopPropagation();
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
<input
|
|
599
|
+
type="text"
|
|
600
|
+
className="input flex-1 text-sm py-1.5 px-3 rounded-lg border border-stroke focus:outline-none focus:border-primary placeholder:text-slate-300 text-slate-700 bg-white"
|
|
601
|
+
placeholder={creatableLabel || "Masukkan pilihan baru..."}
|
|
602
|
+
value={dropdownCustomText}
|
|
603
|
+
onMouseDown={(e) => {
|
|
604
|
+
isFocusingCreatable.current = true;
|
|
605
|
+
e.stopPropagation();
|
|
606
|
+
}}
|
|
607
|
+
onFocus={() => {
|
|
608
|
+
isFocusingCreatable.current = true;
|
|
609
|
+
}}
|
|
610
|
+
onBlur={() => {
|
|
611
|
+
setTimeout(() => {
|
|
612
|
+
if (!isFocusingCreatable.current) {
|
|
613
|
+
setShowOption(false);
|
|
614
|
+
inputHandler.setFocus(false);
|
|
615
|
+
}
|
|
616
|
+
}, 200);
|
|
617
|
+
}}
|
|
618
|
+
onChange={(e) => setDropdownCustomText(e.target.value)}
|
|
619
|
+
onKeyDown={(e) => {
|
|
620
|
+
e.stopPropagation();
|
|
621
|
+
if (e.key === "Enter") {
|
|
622
|
+
e.preventDefault();
|
|
623
|
+
handleCreateNewSubmit();
|
|
624
|
+
}
|
|
625
|
+
}}
|
|
626
|
+
/>
|
|
627
|
+
<button
|
|
628
|
+
type="button"
|
|
629
|
+
className="bg-light-primary text-primary p-2 rounded-lg flex items-center justify-center transition-colors shrink-0 shadow-sm cursor-pointer border-none"
|
|
630
|
+
onMouseDown={(e) => {
|
|
631
|
+
isFocusingCreatable.current = true;
|
|
632
|
+
e.stopPropagation();
|
|
633
|
+
}}
|
|
634
|
+
onClick={(e) => {
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
handleCreateNewSubmit();
|
|
637
|
+
}}
|
|
638
|
+
>
|
|
639
|
+
<Icon icon="solid/check" className="w-4 h-4" />
|
|
640
|
+
</button>
|
|
641
|
+
</li>
|
|
642
|
+
)}
|
|
643
|
+
|
|
644
|
+
{(newOption ? [newOption, ...filteredOptions] : filteredOptions).map((option, key) => {
|
|
645
|
+
const selected = !isCreatingNew && (
|
|
646
|
+
!!((typeof inputHandler.value == "string" || typeof inputHandler.value == "number") && inputHandler.value == option.value) ||
|
|
647
|
+
(Array.isArray(inputHandler.value) && Array().concat(inputHandler.value).find((val: string | number) => val == option.value))
|
|
648
|
+
);
|
|
491
649
|
|
|
492
650
|
return (
|
|
493
651
|
<li
|
|
@@ -508,6 +666,8 @@ export function SelectComponent({
|
|
|
508
666
|
setFilteredOptions([]);
|
|
509
667
|
setShowOption(false);
|
|
510
668
|
|
|
669
|
+
setIsCreatingNew(false);
|
|
670
|
+
|
|
511
671
|
if (!multiple) {
|
|
512
672
|
setInputShowValue(option.label);
|
|
513
673
|
serverSearchable && setKeyword(option.label as string);
|
|
@@ -552,6 +712,6 @@ export function SelectComponent({
|
|
|
552
712
|
<small className={cn("input-error-message", pcn<CT>(className, "error"))}>{invalidMessage}</small>
|
|
553
713
|
)}
|
|
554
714
|
</div>
|
|
555
|
-
|
|
715
|
+
</OutsideClickComponent>
|
|
556
716
|
);
|
|
557
717
|
}
|