@ioca/react 1.5.28 → 1.5.30

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/index.js CHANGED
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import classNames from 'classnames';
3
3
  import { debounce, uid, crush, throttle } from 'radash';
4
4
  import { useState, useRef, useEffect, useCallback, useMemo, Children, cloneElement, createElement, isValidElement, memo, Fragment as Fragment$1, useTransition, forwardRef, useLayoutEffect, useContext, createContext, useImperativeHandle } from 'react';
5
- import { SkipPreviousRound, CloseRound, MinusRound, PlusRound, InboxTwotone, UndoRound, RedoRound, FormatBoldRound, FormatItalicRound, FormatUnderlinedRound, StrikethroughSRound, ClearAllRound, PlayArrowRound, PauseRound, StopRound, VolumeDownRound, VolumeOffRound, FullscreenRound, FullscreenExitRound, FeedOutlined, AspectRatioRound, OpenInNewRound, FileDownloadOutlined, RotateRightRound, RotateLeftRound, KeyboardArrowLeftRound, KeyboardArrowRightRound, KeyboardDoubleArrowUpRound, SyncAltRound, VisibilityRound, VisibilityOffRound, MoreHorizRound, SearchRound, CheckRound, UnfoldMoreRound, CalendarMonthTwotone, AccessTimeRound, KeyboardArrowDownRound, MoveToInboxTwotone, OutboxTwotone, FilePresentOutlined, DriveFolderUploadOutlined } from '@ricons/material';
5
+ import { SkipPreviousRound, CloseRound, MinusRound, PlusRound, InboxTwotone, UndoRound, RedoRound, FormatBoldRound, FormatItalicRound, FormatUnderlinedRound, StrikethroughSRound, ClearAllRound, PlayArrowRound, PauseRound, StopRound, VolumeDownRound, VolumeOffRound, FullscreenRound, FullscreenExitRound, FeedOutlined, AspectRatioRound, OpenInNewRound, FileDownloadOutlined, RotateRightRound, RotateLeftRound, KeyboardArrowLeftRound, KeyboardArrowRightRound, KeyboardDoubleArrowUpRound, SyncAltRound, VisibilityRound, VisibilityOffRound, MoreHorizRound, SearchRound, CheckRound, UnfoldMoreRound, CalendarMonthTwotone, AccessTimeRound, AddRound, KeyboardArrowDownRound, MoveToInboxTwotone, OutboxTwotone, FilePresentOutlined, DriveFolderUploadOutlined } from '@ricons/material';
6
6
  import { createRoot } from 'react-dom/client';
7
7
  import { getScrollbarSize, List as List$2 } from 'react-window';
8
8
  import { createPortal, flushSync } from 'react-dom';
@@ -575,14 +575,6 @@ const Badge = (props) => {
575
575
  }), style: { fontSize: dotSize }, children: content })] }));
576
576
  };
577
577
 
578
- const Card = (props) => {
579
- const { hideShadow, border, className, children, header, footer, ...restProps } = props;
580
- return (jsxs("div", { className: classNames("i-card", className, {
581
- shadow: !hideShadow,
582
- "i-card-bordered": border,
583
- }), ...restProps, children: [header && jsx("div", { className: 'i-card-header', children: header }), children && jsx("div", { className: 'i-card-content', children: children }), footer && jsx("div", { className: 'i-card-footer', children: footer })] }));
584
- };
585
-
586
578
  function getPosition($source, $popup, options = {}) {
587
579
  const { refWindow, gap = 0, offset = 0, position = "top", align } = options;
588
580
  if (!$source || !$popup)
@@ -917,7 +909,7 @@ function Checkbox(props) {
917
909
  "i-input-inline": labelInline,
918
910
  }, className), ...restProps, children: [label && (jsxs("span", { className: "i-input-label-text", children: [required && jsx("span", { className: "error", children: "*" }), label, message && jsx("p", { className: "i-checkbox-message", children: message })] })), jsx("div", { className: classNames("i-checkbox-options", {
919
911
  "i-options-block": !optionInline,
920
- "i-checkbox-options-button": type === "button",
912
+ [`i-checkbox-options-${type}`]: true,
921
913
  }), children: formattedOptions.map((option) => {
922
914
  return (jsx(CheckboxItem, { name: name, value: selectedValues.includes(option.value), optionValue: option.value, type: type, disabled: disabled || option.disabled, onChange: (checked, e) => handleChange(checked, option, e), children: renderItem ?? option.label }, option.value));
923
915
  }) })] }));
@@ -3084,8 +3076,7 @@ class IFormInstance {
3084
3076
  for (let i = 1; i < parts.length; i++) {
3085
3077
  const ancestor = parts.slice(0, i).join(".");
3086
3078
  if (ancestor in this.data) {
3087
- console.warn(`[ioca-form] Field "${field}" conflicts with "${ancestor}". ` +
3088
- "Nested representation in form.get() may be inconsistent.");
3079
+ console.warn(`[ioca-form] Field "${field}" conflicts with "${ancestor}". ` + "Nested representation in form.get() may be inconsistent.");
3089
3080
  }
3090
3081
  }
3091
3082
  setDeep(this.data, field, value);
@@ -3137,7 +3128,7 @@ class IFormInstance {
3137
3128
  if (!field && o === undefined)
3138
3129
  return;
3139
3130
  const rule = {
3140
- validator: (v) => Array.isArray(v) ? v.length > 0 : ![undefined, null, ""].includes(v),
3131
+ validator: (v) => (Array.isArray(v) ? v.length > 0 : ![undefined, null, ""].includes(v)),
3141
3132
  };
3142
3133
  if (typeof o === "function")
3143
3134
  rule.validator = o;
@@ -3162,7 +3153,7 @@ class IFormInstance {
3162
3153
  });
3163
3154
  }
3164
3155
  });
3165
- return field ? isAllValid : isAllValid ? data : false;
3156
+ return isAllValid ? data : false;
3166
3157
  }
3167
3158
  }
3168
3159
  function useForm(form) {
@@ -3429,25 +3420,33 @@ function Modal(props) {
3429
3420
  if (!toggable.current)
3430
3421
  return;
3431
3422
  toggable.current = false;
3432
- if (!closable) {
3433
- setBounced(true);
3423
+ const canClose = typeof closable === 'function' ? closable() : closable;
3424
+ const exec = (result) => {
3425
+ if (!result) {
3426
+ setBounced(true);
3427
+ const timer = setTimeout(() => {
3428
+ setBounced(false);
3429
+ toggable.current = true;
3430
+ }, 400);
3431
+ return () => clearTimeout(timer);
3432
+ }
3433
+ setActive(false);
3434
+ updateVisible(mid, false);
3434
3435
  const timer = setTimeout(() => {
3435
- setBounced(false);
3436
+ if (!keepDOM)
3437
+ setShow(false);
3436
3438
  toggable.current = true;
3437
- }, 400);
3439
+ onVisibleChange?.(false);
3440
+ onClose?.();
3441
+ }, 240);
3438
3442
  return () => clearTimeout(timer);
3443
+ };
3444
+ if (canClose instanceof Promise) {
3445
+ canClose.then(exec);
3446
+ return;
3439
3447
  }
3440
- setActive(false);
3441
- updateVisible(mid, false);
3442
- const timer = setTimeout(() => {
3443
- if (!keepDOM)
3444
- setShow(false);
3445
- toggable.current = true;
3446
- onVisibleChange?.(false);
3447
- onClose?.();
3448
- }, 240);
3449
- return () => clearTimeout(timer);
3450
- }, [closable, keepDOM, onClose, onVisibleChange]);
3448
+ return exec(canClose);
3449
+ }, [closable, keepDOM, mid, onClose, onVisibleChange]);
3451
3450
  const handleBackdropClick = () => {
3452
3451
  backdropClosable && handleHide();
3453
3452
  };
@@ -3507,7 +3506,7 @@ function Modal(props) {
3507
3506
  e.stopPropagation();
3508
3507
  handleClick();
3509
3508
  onClick?.(e);
3510
- }, role: "dialog", "aria-modal": top, "data-mid": mid, ...restProps, children: jsxs(ModalContext.Provider, { value: true, children: [customized && children, !customized && (jsx(Content$2, { title: title, hideCloseButton: hideCloseButton, footer: footer, okButtonProps: okButtonProps, cancelButtonProps: cancelButtonProps, children: children, footerLeft: footerLeft, onOk: onOk, onClose: handleHide }))] }) }) }), getContainer());
3509
+ }, role: "dialog", "aria-modal": top, "data-mid": mid, ...restProps, children: jsxs(ModalContext.Provider, { value: true, children: [customized && children, !customized && jsx(Content$2, { title: title, hideCloseButton: hideCloseButton, footer: footer, okButtonProps: okButtonProps, cancelButtonProps: cancelButtonProps, children: children, footerLeft: footerLeft, onOk: onOk, onClose: handleHide })] }) }) }), getContainer());
3511
3510
  }
3512
3511
  Modal.useModal = useModal;
3513
3512
 
@@ -3521,7 +3520,17 @@ const HookModal = (props) => {
3521
3520
  },
3522
3521
  close: () => {
3523
3522
  state.visible = false;
3524
- if (mergedProps.closable ?? true)
3523
+ const canClose = typeof mergedProps.closable === 'function'
3524
+ ? mergedProps.closable()
3525
+ : (mergedProps.closable ?? true);
3526
+ if (canClose instanceof Promise) {
3527
+ canClose.then((result) => {
3528
+ if (!result)
3529
+ state.visible = true;
3530
+ });
3531
+ return;
3532
+ }
3533
+ if (canClose)
3525
3534
  return;
3526
3535
  Promise.resolve().then(() => {
3527
3536
  state.visible = true;
@@ -4167,7 +4176,7 @@ function InputContainer(props) {
4167
4176
  }
4168
4177
 
4169
4178
  const Number$1 = (props) => {
4170
- const { ref, label, name, value = "", labelInline, step = 1, min = -Infinity, max = Infinity, thousand, precision, type, className, width, status = "normal", append, border, underline, prepend, disabled, message, tip, hideControl, showMax, style, onChange, onEnter, onInput, onBlur, ...restProps } = props;
4179
+ const { ref, label, name, value = "", labelInline, step = 1, min = -Infinity, max = Infinity, thousand, precision, type, className, width, status = "normal", append, border = true, underline, prepend, disabled, message, tip, hideControl, showMax, style, onChange, onEnter, onInput, onBlur, ...restProps } = props;
4171
4180
  const [inputValue, setInputValue] = useState(value === undefined || value === null ? "" : String(value));
4172
4181
  const formatOut = (num) => {
4173
4182
  const v = clamp(num, min, max);
@@ -4180,9 +4189,7 @@ const Number$1 = (props) => {
4180
4189
  const body = negative ? s.slice(1) : s;
4181
4190
  const [integer, decimal] = body.split(".");
4182
4191
  const withThousand = integer.replace(/\B(?=(\d{3})+(?!\d))/g, thousand);
4183
- return decimal
4184
- ? `${negative ? "-" : ""}${withThousand}.${decimal}`
4185
- : `${negative ? "-" : ""}${withThousand}`;
4192
+ return decimal ? `${negative ? "-" : ""}${withThousand}.${decimal}` : `${negative ? "-" : ""}${withThousand}`;
4186
4193
  };
4187
4194
  const sanitizeNumberInput = (raw) => {
4188
4195
  const hasMinus = raw.startsWith("-");
@@ -4263,11 +4270,11 @@ const Number$1 = (props) => {
4263
4270
  [`i-input-${status}`]: status !== "normal",
4264
4271
  "i-input-borderless": !border,
4265
4272
  "i-input-underline": underline,
4266
- }), children: [prepend && jsx("div", { className: 'i-input-prepend', children: prepend }), !hideControl && !disabled && (jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: () => handleOperate(-step) })), jsx("input", { ...inputProps }), !hideControl && !disabled && (jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: () => handleOperate(step) })), showMax && max && !disabled && (jsx(Helpericon, { active: true, icon: jsx(KeyboardDoubleArrowUpRound, {}), onClick: handleMax })), append && jsx("div", { className: 'i-input-append', children: append })] }) }));
4273
+ }), children: [prepend && jsx("div", { className: "i-input-prepend", children: prepend }), !hideControl && !disabled && jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: () => handleOperate(-step) }), jsx("input", { ...inputProps }), !hideControl && !disabled && jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: () => handleOperate(step) }), showMax && max && !disabled && jsx(Helpericon, { active: true, icon: jsx(KeyboardDoubleArrowUpRound, {}), onClick: handleMax }), append && jsx("div", { className: "i-input-append", children: append })] }) }));
4267
4274
  };
4268
4275
 
4269
4276
  const Range = (props) => {
4270
- const { label, name, value, labelInline, min = -Infinity, max = Infinity, type, className, status = "normal", message, tip, append, prepend, step = 1, width, thousand, precision, hideControl, placeholder, border, underline, autoSwitch, onChange, onBlur, style, ...restProps } = props;
4277
+ const { label, name, value, labelInline, min = -Infinity, max = Infinity, type, className, status = "normal", message, tip, append, prepend, step = 1, width, thousand, precision, hideControl, placeholder, border = true, underline, autoSwitch, onChange, onBlur, style, ...restProps } = props;
4271
4278
  const [rangeValue, setRangeValue] = useState(value);
4272
4279
  const getRangeNumber = (v) => clamp(v, min, max);
4273
4280
  const getFormatNumber = (v) => formatNumber(v, { precision, thousand });
@@ -4327,11 +4334,11 @@ const Range = (props) => {
4327
4334
  [`i-input-${status}`]: status !== "normal",
4328
4335
  "i-input-borderless": !border,
4329
4336
  "i-input-underline": underline,
4330
- }), children: [prepend && jsx("div", { className: 'i-input-prepend', children: prepend }), !hideControl && (jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: (e) => handleOperate(e, -step, 0) })), jsx("input", { value: rangeValue?.[0] || "", placeholder: placeholder?.[0], ...inputProps, onBlur: handleBlur, onChange: (e) => handleChange(e, 0) }), !hideControl && (jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: (e) => handleOperate(e, step, 0) })), jsx(Helpericon, { active: true, icon: jsx(SyncAltRound, {}), style: { margin: 0 }, onClick: handleSwitch }), !hideControl && (jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: (e) => handleOperate(e, -step, 1) })), jsx("input", { value: rangeValue?.[1] || "", placeholder: placeholder?.[1], ...inputProps, onBlur: handleBlur, onChange: (e) => handleChange(e, 1) }), !hideControl && (jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: (e) => handleOperate(e, step, 1) })), append && jsx("div", { className: 'i-input-append', children: append })] }) }));
4337
+ }), children: [prepend && jsx("div", { className: "i-input-prepend", children: prepend }), !hideControl && jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: (e) => handleOperate(e, -step, 0) }), jsx("input", { value: rangeValue?.[0] || "", placeholder: placeholder?.[0], ...inputProps, onBlur: handleBlur, onChange: (e) => handleChange(e, 0) }), !hideControl && jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: (e) => handleOperate(e, step, 0) }), jsx(Helpericon, { active: true, icon: jsx(SyncAltRound, {}), style: { margin: 0 }, onClick: handleSwitch }), !hideControl && jsx(Helpericon, { active: true, icon: jsx(MinusRound, {}), onClick: (e) => handleOperate(e, -step, 1) }), jsx("input", { value: rangeValue?.[1] || "", placeholder: placeholder?.[1], ...inputProps, onBlur: handleBlur, onChange: (e) => handleChange(e, 1) }), !hideControl && jsx(Helpericon, { active: true, icon: jsx(PlusRound, {}), onClick: (e) => handleOperate(e, step, 1) }), append && jsx("div", { className: "i-input-append", children: append })] }) }));
4331
4338
  };
4332
4339
 
4333
4340
  const Textarea = (props) => {
4334
- const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border, width, style, resize, onChange, onEnter, ...restProps } = props;
4341
+ const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border = true, width, style, resize, onChange, onEnter, ...restProps } = props;
4335
4342
  const [textareaValue, setTextareaValue] = useState(value);
4336
4343
  const refTextarea = useRef(null);
4337
4344
  const syncTextareaHeight = () => {
@@ -4380,7 +4387,7 @@ const Textarea = (props) => {
4380
4387
  };
4381
4388
 
4382
4389
  const Input = ((props) => {
4383
- const { ref, type = "text", label, name, value = "", prepend, append, labelInline, className, status = "normal", message, tip, clear, width, hideVisible, border, underline, required, maxLength, onChange, onEnter, onClear, style, ...restProps } = props;
4390
+ const { ref, type = "text", label, name, value = "", prepend, append, labelInline, className, status = "normal", message, tip, clear, width, hideVisible, border = true, underline, required, maxLength, onChange, onEnter, onClear, style, ...restProps } = props;
4384
4391
  const [inputValue, setInputValue] = useState(value);
4385
4392
  const [inputType, setInputType] = useState(type);
4386
4393
  const [visible, setVisible] = useState(false);
@@ -4438,7 +4445,7 @@ const Input = ((props) => {
4438
4445
  [`i-input-${status}`]: status !== "normal",
4439
4446
  "i-input-borderless": !border,
4440
4447
  "i-input-underline": underline,
4441
- }), children: [prepend && jsx("div", { className: 'i-input-prepend', children: prepend }), jsx("input", { ...inputProps }), maxLength && inputValue?.length > 0 && (jsxs("span", { className: 'color-8 pr-4 font-sm', children: [inputValue.length, " / ", maxLength] })), jsx(Helpericon, { active: !!clearable || showHelper, icon: HelperIcon, className: classNames({ "i-helpericon-clear": isClearBtn }), onClick: handleHelperClick }), append && jsx("div", { className: 'i-input-append', children: append })] }) }));
4448
+ }), children: [prepend && jsx("div", { className: "i-input-prepend", children: prepend }), jsx("input", { ...inputProps }), maxLength && inputValue?.length > 0 && (jsxs("span", { className: "color-8 pr-4 font-sm", children: [inputValue.length, " / ", maxLength] })), jsx(Helpericon, { active: !!clearable || showHelper, icon: HelperIcon, className: classNames({ "i-helpericon-clear": isClearBtn }), onClick: handleHelperClick }), append && jsx("div", { className: "i-input-append", children: append })] }) }));
4442
4449
  });
4443
4450
  Input.Textarea = Textarea;
4444
4451
  Input.Number = Number$1;
@@ -4721,31 +4728,27 @@ const Pagination = (props) => {
4721
4728
  };
4722
4729
 
4723
4730
  const Tag = (props) => {
4724
- const { dot, dotClass, outline, round, size = "normal", hoverShowClose, className, children, onClose, onClick, ...restProps } = props;
4731
+ const { dot, dotClass, outline, round, size = "normal", className, children, onClose, onClick, ...restProps } = props;
4725
4732
  return (jsxs("span", { className: classNames("i-tag", {
4726
4733
  "i-tag-outline": outline,
4727
4734
  "i-tag-clickable": onClick,
4728
4735
  [`i-tag-${size}`]: size !== "normal",
4729
4736
  round,
4730
- }, className), onClick: onClick, ...restProps, children: [dot && jsx("span", { className: classNames("i-tag-dot", dotClass) }), children, onClose && (jsx(Helpericon, { active: true, className: classNames("i-tag-close", {
4731
- "i-tag-hover-close": hoverShowClose,
4732
- }), onClick: onClose }))] }));
4737
+ }, 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 }))] }));
4733
4738
  };
4734
4739
 
4735
4740
  const Options = (props) => {
4736
- const { value: val, options, filter, filterPlaceholder, multiple, empty = jsx(Empty, {}), onSelect, onFilter, } = props;
4741
+ const { value: val, options, filter, filterPlaceholder, multiple, empty = jsx(Empty, {}), onSelect, onFilter } = props;
4737
4742
  return (jsxs("div", { className: classNames("i-select-options", {
4738
4743
  "i-select-options-multiple": multiple,
4739
- }), children: [filter && multiple && (jsxs("div", { className: 'i-select-filter', children: [jsx(Icon, { icon: jsx(SearchRound, {}), className: 'color-8 ml-8 my-auto', size: '1.2em' }), jsx("input", { type: 'text', className: 'i-input', placeholder: filterPlaceholder, onChange: onFilter })] })), options.length === 0 && empty, options.map((option, i) => {
4744
+ }), children: [filter && multiple && (jsxs("div", { className: "i-select-filter", children: [jsx(Icon, { icon: jsx(SearchRound, {}), className: "color-8 ml-8 my-auto", size: "1.2em" }), jsx("input", { type: "text", className: "i-input", placeholder: filterPlaceholder, onChange: onFilter })] })), options.length === 0 && empty, options.map((option, i) => {
4740
4745
  const { label, value, disabled } = option;
4741
- const isActive = multiple
4742
- ? val?.includes(value)
4743
- : val === value;
4744
- return (jsxs(List$1.Item, { active: isActive, type: 'option', onClick: () => onSelect?.(value, option), disabled: disabled, children: [multiple && (jsx(Icon, { icon: jsx(CheckRound, {}), className: 'i-select-option-check', size: '1em' })), label] }, value || i));
4746
+ const isActive = multiple ? val?.includes(value) : val === value;
4747
+ return (jsxs(List$1.Item, { active: isActive, type: "option", onClick: () => onSelect?.(value, option), disabled: disabled, children: [multiple && jsx(Icon, { icon: jsx(CheckRound, {}), className: "i-select-option-check", size: "1em" }), label] }, value || i));
4745
4748
  })] }));
4746
4749
  };
4747
4750
  const activeOptions = (options = [], value = [], max = 3) => {
4748
- const total = options.flatMap((opt) => value.includes(opt.value) ? [opt] : []);
4751
+ const total = options.flatMap((opt) => (value.includes(opt.value) ? [opt] : []));
4749
4752
  if (max >= total.length)
4750
4753
  return total;
4751
4754
  const rest = total.length - max;
@@ -4760,7 +4763,7 @@ const displayValue = (config) => {
4760
4763
  if (typeof opt === "number")
4761
4764
  return jsxs(Tag, { children: ["+", opt] }, i);
4762
4765
  const { label, value } = opt;
4763
- return (jsx(Tag, { hoverShowClose: true, onClose: (e) => {
4766
+ return (jsx(Tag, { onClose: (e) => {
4764
4767
  e?.stopPropagation();
4765
4768
  onSelect?.(value, opt);
4766
4769
  }, children: label }, value));
@@ -4770,7 +4773,7 @@ const displayValue = (config) => {
4770
4773
  };
4771
4774
 
4772
4775
  const Select = (props) => {
4773
- const { ref, type = "text", name, label, value = "", placeholder, required, options = [], multiple, prepend, append, labelInline, style, className, message, status = "normal", hideClear, hideArrow, maxDisplay, border, filter, tip, filterPlaceholder = "...", popupProps, onSelect, onChange, ...restProps } = props;
4776
+ const { ref, type = "text", name, label, value = "", placeholder, required, options = [], multiple, prepend, append, labelInline, style, className, message, status = "normal", hideClear, hideArrow, maxDisplay, border = true, filter, tip, filterPlaceholder = "...", popupProps, onSelect, onChange, ...restProps } = props;
4774
4777
  const [filterValue, setFilterValue] = useState("");
4775
4778
  const [selectedValue, setSelectedValue] = useState(value);
4776
4779
  const [active, setActive] = useState(false);
@@ -4785,9 +4788,7 @@ const Select = (props) => {
4785
4788
  if (!fv || !filter)
4786
4789
  return formattedOptions;
4787
4790
  const lowerFv = fv.toLowerCase();
4788
- const filterFn = typeof filter === "function"
4789
- ? filter
4790
- : (opt) => opt._value.includes(lowerFv) || opt._label.includes(lowerFv);
4791
+ const filterFn = typeof filter === "function" ? filter : (opt) => opt._value.includes(lowerFv) || opt._label.includes(lowerFv);
4791
4792
  return formattedOptions.filter(filterFn);
4792
4793
  }, [formattedOptions, filter, filterValue]);
4793
4794
  const changeValue = (v) => {
@@ -4836,9 +4837,7 @@ const Select = (props) => {
4836
4837
  useEffect(() => {
4837
4838
  setSelectedValue(value);
4838
4839
  }, [value]);
4839
- const hasValue = multiple
4840
- ? selectedValue.length > 0
4841
- : !!selectedValue;
4840
+ const hasValue = multiple ? selectedValue.length > 0 : !!selectedValue;
4842
4841
  const clearable = !hideClear && active && hasValue;
4843
4842
  const text = message ?? tip;
4844
4843
  return (jsxs("label", { className: classNames("i-input-label", className, {
@@ -4855,7 +4854,7 @@ const Select = (props) => {
4855
4854
  multiple,
4856
4855
  maxDisplay,
4857
4856
  onSelect: handleSelect,
4858
- }) })) : (jsx("input", { className: "i-input i-select", placeholder: placeholder, readOnly: true }))) : null, !multiple && (jsx("input", { value: active ? filterValue : displayLabel, className: "i-input i-select", placeholder: displayLabel || placeholder, onChange: handleInputChange, readOnly: !filter })), jsx(Helpericon, { active: !hideArrow, icon: clearable ? undefined : jsx(UnfoldMoreRound, {}), onClick: handleHelperClick }), append] }) }), text && jsx("span", { className: "i-input-message", children: text })] }));
4857
+ }) })) : (jsx("input", { className: "i-input i-select", placeholder: placeholder, readOnly: true }))) : null, !multiple && jsx("input", { value: active ? filterValue : displayLabel, className: "i-input i-select", placeholder: displayLabel || placeholder, onChange: handleInputChange, readOnly: !filter }), jsx(Helpericon, { active: !hideArrow, icon: clearable ? undefined : jsx(UnfoldMoreRound, {}), onClick: handleHelperClick }), append] }) }), text && jsx("span", { className: "i-input-message", children: text })] }));
4859
4858
  };
4860
4859
 
4861
4860
  const ColorMethods = {
@@ -5325,6 +5324,214 @@ const DateRange = (props) => {
5325
5324
  return (jsx(Popup, { visible: active, trigger: 'click', position: 'bottom', arrow: false, align: 'start', onVisibleChange: handleVisibleChange, content: jsx(DoublePanel, { value: dayJsValue, weeks: weeks, unitYear: unitYear, unitMonth: unitMonth, renderDate: renderDate, renderMonth: renderMonth, renderYear: renderYear, disabledDate: disabledDate, onSelected: handleSelected }), children: jsx(Input, { value: inputValue, placeholder: placeholder, readOnly: true, clear: clear, onClear: handleClear, append: jsx(Icon, { icon: jsx(CalendarMonthTwotone, {}), className: 'i-datepicker-icon', size: '1em' }), className: classNames("i-datepicker-label", className), ...restProps }) }));
5326
5325
  };
5327
5326
 
5327
+ const CreateTag = memo(function CreateTag(props) {
5328
+ const { isEditing, isLoading, createTagProps, tagProps, onBlur, onKeyDown, onStartCreate } = props;
5329
+ if (isEditing) {
5330
+ return (jsx(Tag, { ...createTagProps, className: classNames("i-pill", tagProps?.className, "i-pill-editing"), contentEditable: true, suppressContentEditableWarning: true, onBlur: () => onBlur(-1), onKeyDown: (e) => onKeyDown(e, -1), children: isLoading && jsx(Loading, { size: ".86em", className: "ml-4" }) }, "pill-editing"));
5331
+ }
5332
+ return (jsx(Tag, { ...createTagProps, className: classNames("i-pill", tagProps?.className, "i-pill-create"), onClick: onStartCreate, children: jsx("b", { children: "\uFF0B" }) }, "pill-create"));
5333
+ });
5334
+
5335
+ const TagItem = memo(function TagItem(props) {
5336
+ const { item, index, isEditing, isLoading, tagProps, editable, readonly, renderItem, onClose, onClick, onBlur, onKeyDown } = props;
5337
+ const isClickable = !isEditing && editable && !readonly;
5338
+ if (renderItem) {
5339
+ return renderItem({
5340
+ value: item,
5341
+ index,
5342
+ editing: isEditing,
5343
+ loading: isLoading,
5344
+ readonly: !!readonly,
5345
+ remove: () => onClose(index),
5346
+ });
5347
+ }
5348
+ return (jsxs(Tag, { ...tagProps, className: classNames("i-pill", tagProps?.className, { "i-pill-editing": isEditing }), contentEditable: isEditing, suppressContentEditableWarning: true, onClose: !isEditing && !isLoading && !readonly ? () => onClose(index) : undefined, onClick: isClickable ? (e) => onClick(e, index) : undefined, onBlur: isEditing ? () => onBlur(index) : undefined, onKeyDown: isEditing ? (e) => onKeyDown(e, index) : undefined, children: [item, isLoading && jsx(Loading, { size: ".86em", className: "ml-4" })] }));
5349
+ });
5350
+
5351
+ function Pill(props) {
5352
+ const { value = [], tagProps, max, icon = jsx(AddRound, {}), className, label, labelInline, readonly, editable, onChange, onUpdate, validator, format, renderItem, hideCreate, ...restProps } = props;
5353
+ const [editingIndex, setEditingIndex] = useState(null);
5354
+ const [loadingSet, setLoadingSet] = useState(new Set());
5355
+ const instRef = useRef({
5356
+ props,
5357
+ editingIndex,
5358
+ loadingSet,
5359
+ setEditingIndex,
5360
+ setLoadingSet,
5361
+ });
5362
+ instRef.current.props = props;
5363
+ instRef.current.editingIndex = editingIndex;
5364
+ instRef.current.loadingSet = loadingSet;
5365
+ instRef.current.setEditingIndex = setEditingIndex;
5366
+ instRef.current.setLoadingSet = setLoadingSet;
5367
+ useEffect(() => {
5368
+ if (editingIndex !== null) {
5369
+ const el = document.querySelector(".i-pill-editing");
5370
+ el?.focus();
5371
+ }
5372
+ }, [editingIndex]);
5373
+ const cleanTagProps = useMemo(() => {
5374
+ if (!tagProps)
5375
+ return {};
5376
+ const { onClose, dot, dotClass, ...rest } = tagProps;
5377
+ return rest;
5378
+ }, [tagProps]);
5379
+ const handleClose = useCallback((index) => {
5380
+ const inst = instRef.current;
5381
+ if (inst.props.readonly)
5382
+ return;
5383
+ const hasAsync = !!inst.props.onUpdate;
5384
+ if (hasAsync)
5385
+ inst.setLoadingSet((prev) => new Set(prev).add(index));
5386
+ setTimeout(async () => {
5387
+ try {
5388
+ const { props } = instRef.current;
5389
+ const values = props.value ?? [];
5390
+ const item = values[index];
5391
+ if (item === undefined)
5392
+ return;
5393
+ const result = props.onUpdate?.(undefined, item, "delete");
5394
+ if (result instanceof Promise) {
5395
+ const ok = await result;
5396
+ if (ok === false)
5397
+ return;
5398
+ }
5399
+ const next = [...values];
5400
+ next.splice(index, 1);
5401
+ props.onChange?.(next);
5402
+ }
5403
+ finally {
5404
+ if (hasAsync) {
5405
+ instRef.current.setLoadingSet((prev) => {
5406
+ const s = new Set(prev);
5407
+ s.delete(index);
5408
+ return s;
5409
+ });
5410
+ }
5411
+ }
5412
+ }, 0);
5413
+ }, []);
5414
+ const handleItemClick = useCallback((e, index) => {
5415
+ if (e.target.closest(".i-helpericon"))
5416
+ return;
5417
+ const inst = instRef.current;
5418
+ if (inst.props.readonly)
5419
+ return;
5420
+ if (index === -1 && inst.props.max !== undefined && (inst.props.value?.length ?? 0) >= inst.props.max)
5421
+ return;
5422
+ inst.setEditingIndex(index);
5423
+ }, []);
5424
+ const commitEdit = useCallback((index, text) => {
5425
+ const inst = instRef.current;
5426
+ const formatted = inst.props.format ? inst.props.format(text) : text;
5427
+ const hasAsync = !!(inst.props.validator || inst.props.onUpdate);
5428
+ if (hasAsync)
5429
+ inst.setLoadingSet((prev) => new Set(prev).add(index));
5430
+ setTimeout(async () => {
5431
+ try {
5432
+ const { props } = instRef.current;
5433
+ if (props.validator) {
5434
+ const valid = await Promise.resolve(props.validator(formatted));
5435
+ if (!valid)
5436
+ return;
5437
+ }
5438
+ const values = props.value ?? [];
5439
+ if (index === -1) {
5440
+ if (values.includes(formatted))
5441
+ return;
5442
+ const result = props.onUpdate?.(formatted, undefined, "create");
5443
+ if (result instanceof Promise) {
5444
+ const ok = await result;
5445
+ if (ok === false)
5446
+ return;
5447
+ }
5448
+ props.onChange?.([...values, formatted]);
5449
+ }
5450
+ else {
5451
+ const oldValue = values[index];
5452
+ if (oldValue === formatted)
5453
+ return;
5454
+ const result = props.onUpdate?.(formatted, oldValue, "update");
5455
+ if (result instanceof Promise) {
5456
+ const ok = await result;
5457
+ if (ok === false)
5458
+ return;
5459
+ }
5460
+ const next = [...values];
5461
+ next[index] = formatted;
5462
+ props.onChange?.(next);
5463
+ }
5464
+ }
5465
+ finally {
5466
+ if (hasAsync) {
5467
+ instRef.current.setLoadingSet((prev) => {
5468
+ const s = new Set(prev);
5469
+ s.delete(index);
5470
+ return s;
5471
+ });
5472
+ }
5473
+ instRef.current.setEditingIndex(null);
5474
+ }
5475
+ }, 0);
5476
+ }, []);
5477
+ const handleBlur = useCallback((index) => {
5478
+ const inst = instRef.current;
5479
+ if (inst.loadingSet.has(index))
5480
+ return;
5481
+ const el = document.querySelector(".i-pill-editing");
5482
+ const text = el?.textContent?.trim();
5483
+ if (!text) {
5484
+ if (index !== -1) {
5485
+ handleClose(index);
5486
+ }
5487
+ else {
5488
+ inst.setEditingIndex(null);
5489
+ }
5490
+ return;
5491
+ }
5492
+ commitEdit(index, text);
5493
+ }, []);
5494
+ const handleKeyDown = useCallback((e, index) => {
5495
+ const inst = instRef.current;
5496
+ if (inst.loadingSet.has(index))
5497
+ return;
5498
+ if (e.key === "Enter") {
5499
+ e.preventDefault();
5500
+ const text = e.currentTarget.textContent?.trim();
5501
+ if (!text) {
5502
+ if (index !== -1) {
5503
+ handleClose(index);
5504
+ }
5505
+ else {
5506
+ inst.setEditingIndex(null);
5507
+ }
5508
+ return;
5509
+ }
5510
+ commitEdit(index, text);
5511
+ }
5512
+ else if (e.key === "Escape") {
5513
+ e.preventDefault();
5514
+ if (index !== -1) {
5515
+ const original = inst.props.value?.[index];
5516
+ if (original !== undefined) {
5517
+ e.currentTarget.textContent = original;
5518
+ }
5519
+ }
5520
+ inst.setEditingIndex(null);
5521
+ }
5522
+ }, []);
5523
+ const handleStartCreate = useCallback(() => {
5524
+ const inst = instRef.current;
5525
+ if (inst.props.readonly)
5526
+ return;
5527
+ if (inst.props.max !== undefined && (inst.props.value?.length ?? 0) >= inst.props.max)
5528
+ return;
5529
+ inst.setEditingIndex(-1);
5530
+ }, []);
5531
+ const canCreate = !hideCreate && !readonly && (max === undefined || value.length < max);
5532
+ 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 })] })] }));
5533
+ }
5534
+
5328
5535
  const defaultOk = {
5329
5536
  children: "确定",
5330
5537
  };
@@ -6976,4 +7183,4 @@ const useTheme = (props) => {
6976
7183
  };
6977
7184
  };
6978
7185
 
6979
- export { Affix, Badge, Button, Card, Checkbox, Collapse, ColorPicker, Datagrid, Datepicker as DatePicker, DateRange, Description, Drawer, Dropdown, Editor, Flex, Form, Icon, MemoImage as Image, Input, List$1 as List, Loading, Message, Modal, Pagination, Popconfirm, Popup, Progress, Radio, Resizable, River, Scroll, Select, Step, Swiper, Tabs, Tag, Text, TimePicker, Tree, Upload, Video, usePreview, useTheme };
7186
+ export { Affix, Badge, Button, Checkbox, Collapse, ColorPicker, Datagrid, Datepicker as DatePicker, DateRange, Description, Drawer, Dropdown, Editor, Flex, Form, Icon, MemoImage as Image, Input, List$1 as List, Loading, Message, Modal, Pagination, Pill, Popconfirm, Popup, Progress, Radio, Resizable, River, Scroll, Select, Step, Swiper, Tabs, Tag, Text, TimePicker, Tree, Upload, Video, usePreview, useTheme };
@@ -10,7 +10,7 @@ declare class IFormInstance {
10
10
  set(field: any, value?: any): void;
11
11
  delete(field: string): void;
12
12
  clear(): void;
13
- validate(field?: string): Promise<boolean | Record<string, any>>;
13
+ validate(field?: string): Promise<false | Record<string, any>>;
14
14
  }
15
15
  declare function useForm(form?: IFormInstance): IFormInstance;
16
16
 
@@ -6,7 +6,7 @@ interface IModal extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
6
6
  visible?: boolean;
7
7
  title?: ReactNode;
8
8
  footer?: ReactNode;
9
- closable?: boolean;
9
+ closable?: boolean | (() => boolean | Promise<boolean>);
10
10
  hideCloseButton?: boolean;
11
11
  hideBackdrop?: boolean;
12
12
  backdropClosable?: boolean;
@@ -0,0 +1,5 @@
1
+ import Pill from './pill.js';
2
+
3
+
4
+
5
+ export { Pill as default };
@@ -0,0 +1,6 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { IPill } from './type.js';
3
+
4
+ declare function Pill(props: IPill): react_jsx_runtime.JSX.Element;
5
+
6
+ export { Pill as default };
@@ -0,0 +1,28 @@
1
+ import { HTMLAttributes, ReactNode } from 'react';
2
+ import { ITag } from '../tag/type.js';
3
+
4
+ interface IPill extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "value"> {
5
+ value?: any[];
6
+ onChange?: (value: any[]) => void;
7
+ tagProps?: Partial<ITag>;
8
+ max?: number;
9
+ icon?: ReactNode;
10
+ editable?: boolean;
11
+ readonly?: boolean;
12
+ label?: ReactNode;
13
+ labelInline?: boolean;
14
+ validator?: (value: any) => boolean | Promise<boolean>;
15
+ format?: (value: any) => any;
16
+ onUpdate?: (newValue: any, oldValue: any, type: "delete" | "create" | "update") => void | Promise<boolean>;
17
+ hideCreate?: boolean;
18
+ renderItem?: (context: {
19
+ value: any;
20
+ index: number;
21
+ editing: boolean;
22
+ loading: boolean;
23
+ readonly: boolean;
24
+ remove: () => void;
25
+ }) => ReactNode;
26
+ }
27
+
28
+ export type { IPill };
@@ -6,7 +6,6 @@ interface ITag extends HTMLAttributes<HTMLSpanElement> {
6
6
  outline?: boolean;
7
7
  round?: boolean;
8
8
  size?: "small" | "normal" | "large" | "extreme";
9
- hoverShowClose?: boolean;
10
9
  onClick?: (e: MouseEvent) => void;
11
10
  onClose?: (e: MouseEvent) => void;
12
11
  }
@@ -1,7 +1,6 @@
1
1
  export { default as Affix } from './components/affix/affix.js';
2
2
  export { default as Badge } from './components/badge/badge.js';
3
3
  export { default as Button } from './components/button/button.js';
4
- export { default as Card } from './components/card/card.js';
5
4
  export { default as Checkbox } from './components/checkbox/checkbox.js';
6
5
  export { default as Collapse } from './components/collapse/collapse.js';
7
6
  export { default as Datagrid } from './components/datagrid/datagrid.js';
@@ -23,6 +22,7 @@ export { default as ColorPicker } from './components/picker/colors/index.js';
23
22
  export { default as TimePicker } from './components/picker/time/index.js';
24
23
  export { default as DateRange } from './components/picker/daterange/daterange.js';
25
24
  export { default as DatePicker } from './components/picker/dates/index.js';
25
+ export { default as Pill } from './components/pill/pill.js';
26
26
  export { default as Popconfirm } from './components/popconfirm/popconfirm.js';
27
27
  export { default as Popup } from './components/popup/popup.js';
28
28
  export { default as Progress } from './components/progress/progress.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ioca/react",
3
- "version": "1.5.28",
3
+ "version": "1.5.30",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -1,12 +0,0 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import classNames from 'classnames';
3
-
4
- const Card = (props) => {
5
- const { hideShadow, border, className, children, header, footer, ...restProps } = props;
6
- return (jsxs("div", { className: classNames("i-card", className, {
7
- shadow: !hideShadow,
8
- "i-card-bordered": border,
9
- }), ...restProps, children: [header && jsx("div", { className: 'i-card-header', children: header }), children && jsx("div", { className: 'i-card-content', children: children }), footer && jsx("div", { className: 'i-card-footer', children: footer })] }));
10
- };
11
-
12
- export { Card as default };
@@ -1,5 +0,0 @@
1
- import Card from './card.js';
2
-
3
-
4
-
5
- export { Card as default };
@@ -1,6 +0,0 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ICard } from './type.js';
3
-
4
- declare const Card: (props: ICard) => react_jsx_runtime.JSX.Element;
5
-
6
- export { Card as default };