@ioca/react 1.5.3 → 1.5.5

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.
Files changed (57) hide show
  1. package/lib/cjs/components/editor/controls.js +31 -40
  2. package/lib/cjs/components/editor/controls.js.map +1 -1
  3. package/lib/cjs/components/editor/editor.js +204 -41
  4. package/lib/cjs/components/editor/editor.js.map +1 -1
  5. package/lib/cjs/components/editor/memtion.js +171 -0
  6. package/lib/cjs/components/editor/memtion.js.map +1 -0
  7. package/lib/cjs/components/image/image.js +46 -30
  8. package/lib/cjs/components/image/image.js.map +1 -1
  9. package/lib/cjs/components/input/textarea.js +12 -5
  10. package/lib/cjs/components/input/textarea.js.map +1 -1
  11. package/lib/cjs/components/modal/hookModal.js +1 -1
  12. package/lib/cjs/components/modal/hookModal.js.map +1 -1
  13. package/lib/cjs/components/picker/colors/footer.js +1 -1
  14. package/lib/cjs/components/picker/colors/footer.js.map +1 -1
  15. package/lib/cjs/components/popconfirm/popconfirm.js +3 -3
  16. package/lib/cjs/components/popconfirm/popconfirm.js.map +1 -1
  17. package/lib/cjs/components/tabs/tabs.js +95 -37
  18. package/lib/cjs/components/tabs/tabs.js.map +1 -1
  19. package/lib/cjs/js/hooks.js +60 -40
  20. package/lib/cjs/js/hooks.js.map +1 -1
  21. package/lib/css/colors.css +13 -8
  22. package/lib/css/index.css +1 -1
  23. package/lib/css/index.css.map +1 -1
  24. package/lib/css/input.css +12 -6
  25. package/lib/css/reset.css +2 -5
  26. package/lib/css/utilities.css +9 -10
  27. package/lib/es/components/editor/controls.js +32 -37
  28. package/lib/es/components/editor/controls.js.map +1 -1
  29. package/lib/es/components/editor/editor.js +205 -42
  30. package/lib/es/components/editor/editor.js.map +1 -1
  31. package/lib/es/components/editor/memtion.js +160 -0
  32. package/lib/es/components/editor/memtion.js.map +1 -0
  33. package/lib/es/components/image/image.js +47 -31
  34. package/lib/es/components/image/image.js.map +1 -1
  35. package/lib/es/components/image/index.js +2 -2
  36. package/lib/es/components/image/list.js +2 -2
  37. package/lib/es/components/image/list.js.map +1 -1
  38. package/lib/es/components/input/textarea.js +12 -5
  39. package/lib/es/components/input/textarea.js.map +1 -1
  40. package/lib/es/components/modal/hookModal.js +1 -1
  41. package/lib/es/components/modal/hookModal.js.map +1 -1
  42. package/lib/es/components/picker/colors/footer.js +1 -1
  43. package/lib/es/components/picker/colors/footer.js.map +1 -1
  44. package/lib/es/components/popconfirm/popconfirm.js +3 -3
  45. package/lib/es/components/popconfirm/popconfirm.js.map +1 -1
  46. package/lib/es/components/tabs/tabs.js +95 -37
  47. package/lib/es/components/tabs/tabs.js.map +1 -1
  48. package/lib/es/components/upload/renderFile.js +2 -2
  49. package/lib/es/components/upload/renderFile.js.map +1 -1
  50. package/lib/es/js/hooks.js +61 -41
  51. package/lib/es/js/hooks.js.map +1 -1
  52. package/lib/index.js +608 -195
  53. package/lib/types/components/editor/type.d.ts +25 -12
  54. package/lib/types/components/image/image.d.ts +2 -2
  55. package/lib/types/components/image/index.d.ts +2 -2
  56. package/lib/types/components/input/type.d.ts +1 -0
  57. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import classNames from 'classnames';
3
3
  import { debounce, uid, throttle, title } from 'radash';
4
- import { useState, useRef, useEffect, useMemo, Children, cloneElement, createElement, isValidElement, Fragment as Fragment$1, useTransition, forwardRef, useLayoutEffect, useImperativeHandle, createContext, useContext } from 'react';
4
+ import { useState, useRef, useCallback, useEffect, useMemo, Children, cloneElement, createElement, isValidElement, Fragment as Fragment$1, useTransition, forwardRef, useLayoutEffect, memo, createContext, useContext, useImperativeHandle } from 'react';
5
5
  import { SkipPreviousRound, CloseRound, MinusRound, PlusRound, InboxTwotone, ClearAllRound, UndoRound, RedoRound, StrikethroughSRound, FormatUnderlinedRound, FormatItalicRound, FormatBoldRound, 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, InfoOutlined, KeyboardArrowDownRound, ListAltRound, DriveFolderUploadOutlined, PlusSharp } from '@ricons/material';
6
6
  import { createRoot } from 'react-dom/client';
7
7
  import { createPortal } from 'react-dom';
8
8
  import xss from 'xss';
9
+ import { renderToStaticMarkup } from 'react-dom/server';
9
10
  import PubSub from 'pubsub-js';
10
11
  import { findAll } from 'highlight-words-core';
11
12
  import ColorsPanel from '@rc-component/color-picker';
@@ -234,31 +235,41 @@ const defaultObserver = {
234
235
  disconnect: undefined,
235
236
  };
236
237
  function useIntersectionObserver(configs) {
238
+ const wmRef = useRef(new WeakMap());
239
+ const ioRef = useRef(undefined);
240
+ if (typeof window !== "undefined" && !ioRef.current) {
241
+ ioRef.current = new IntersectionObserver((entries) => {
242
+ entries.map((entry) => {
243
+ const callback = wmRef.current.get(entry.target);
244
+ callback?.(entry.target, entry.isIntersecting);
245
+ });
246
+ }, configs);
247
+ }
248
+ const observe = useCallback((target, callback) => {
249
+ if (!target || !ioRef.current || wmRef.current.get(target))
250
+ return;
251
+ wmRef.current.set(target, callback);
252
+ ioRef.current.observe(target);
253
+ }, []);
254
+ const unobserve = useCallback((target) => {
255
+ if (!target || !ioRef.current)
256
+ return;
257
+ ioRef.current.unobserve(target);
258
+ wmRef.current.delete(target);
259
+ }, []);
260
+ const disconnect = useCallback(() => {
261
+ ioRef.current?.disconnect();
262
+ }, []);
263
+ useEffect(() => {
264
+ return () => {
265
+ ioRef.current?.disconnect();
266
+ };
267
+ }, []);
237
268
  if (typeof window === "undefined") {
238
269
  return {
239
270
  ...defaultObserver,
240
271
  };
241
272
  }
242
- const WM = new WeakMap();
243
- const IO = new IntersectionObserver((entries) => {
244
- entries.map((entry) => {
245
- const callback = WM.get(entry.target);
246
- callback?.(entry.target, entry.isIntersecting);
247
- });
248
- }, configs);
249
- function observe(target, callback) {
250
- if (WM.get(target))
251
- return;
252
- WM.set(target, callback);
253
- target && IO.observe(target);
254
- }
255
- function unobserve(target) {
256
- target && IO.unobserve(target);
257
- WM.delete(target);
258
- }
259
- function disconnect() {
260
- IO.disconnect();
261
- }
262
273
  return {
263
274
  observe,
264
275
  unobserve,
@@ -266,31 +277,41 @@ function useIntersectionObserver(configs) {
266
277
  };
267
278
  }
268
279
  function useResizeObserver() {
280
+ const wmRef = useRef(new WeakMap());
281
+ const ioRef = useRef(undefined);
282
+ if (typeof window !== "undefined" && !ioRef.current) {
283
+ ioRef.current = new ResizeObserver((entries) => {
284
+ entries.map((entry) => {
285
+ const callback = wmRef.current.get(entry.target);
286
+ callback?.(entry.target);
287
+ });
288
+ });
289
+ }
290
+ const observe = useCallback((target, callback) => {
291
+ if (!target || !ioRef.current || wmRef.current.get(target))
292
+ return;
293
+ ioRef.current.observe(target);
294
+ wmRef.current.set(target, callback);
295
+ }, []);
296
+ const unobserve = useCallback((target) => {
297
+ if (!target || !ioRef.current)
298
+ return;
299
+ ioRef.current.unobserve(target);
300
+ wmRef.current.delete(target);
301
+ }, []);
302
+ const disconnect = useCallback(() => {
303
+ ioRef.current?.disconnect();
304
+ }, []);
305
+ useEffect(() => {
306
+ return () => {
307
+ ioRef.current?.disconnect();
308
+ };
309
+ }, []);
269
310
  if (typeof window === "undefined") {
270
311
  return {
271
312
  ...defaultObserver,
272
313
  };
273
314
  }
274
- const WM = new WeakMap();
275
- const IO = new ResizeObserver((entries) => {
276
- entries.map((entry) => {
277
- const callback = WM.get(entry.target);
278
- callback?.(entry.target);
279
- });
280
- });
281
- function observe(target, callback) {
282
- if (WM.get(target))
283
- return;
284
- target && IO.observe(target);
285
- WM.set(target, callback);
286
- }
287
- function unobserve(target) {
288
- target && IO.unobserve(target);
289
- WM.delete(target);
290
- }
291
- function disconnect() {
292
- IO.disconnect();
293
- }
294
315
  return {
295
316
  observe,
296
317
  unobserve,
@@ -1823,153 +1844,464 @@ const Dropdown = (props) => {
1823
1844
  };
1824
1845
  Dropdown.Item = Item$3;
1825
1846
 
1826
- const { escapeAttrValue } = xss;
1827
1847
  const exec = (a, b, c) => {
1828
1848
  if (typeof document === "undefined")
1829
1849
  return;
1830
1850
  return document.execCommand(a, b, c);
1831
1851
  };
1852
+ const escapeHtmlAttr$1 = (value) => value
1853
+ .replaceAll("&", "&")
1854
+ .replaceAll('"', """)
1855
+ .replaceAll("<", "&lt;")
1856
+ .replaceAll(">", "&gt;");
1832
1857
  const xssOptions = {
1833
- onIgnoreTagAttr: function (tag, name, value) {
1834
- if (["data-", "style"].includes(name.substr(0, 5))) {
1835
- return name + '="' + escapeAttrValue(value) + '"';
1858
+ onIgnoreTagAttr(tag, name, value) {
1859
+ if (["class", "contenteditable"].includes(name)) {
1860
+ return name + '="' + escapeHtmlAttr$1(value) + '"';
1861
+ }
1862
+ if (["data-", "style"].includes(name.substring(0, 5))) {
1863
+ return name + '="' + escapeHtmlAttr$1(value) + '"';
1836
1864
  }
1837
1865
  },
1838
1866
  };
1867
+ const handleMouseDown = (e) => {
1868
+ e.preventDefault();
1869
+ };
1839
1870
  const fnMap = {
1840
1871
  bold: {
1841
1872
  icon: jsx(FormatBoldRound, {}),
1842
1873
  onClick: () => exec("bold"),
1843
- tip: "粗体",
1844
1874
  },
1845
1875
  italic: {
1846
1876
  icon: jsx(FormatItalicRound, {}),
1847
1877
  onClick: () => exec("italic"),
1848
- tip: "斜体",
1849
1878
  },
1850
1879
  underline: {
1851
1880
  icon: jsx(FormatUnderlinedRound, {}),
1852
1881
  onClick: () => exec("underline"),
1853
- tip: "下划线",
1854
1882
  },
1855
1883
  strike: {
1856
1884
  icon: jsx(StrikethroughSRound, {}),
1857
1885
  onClick: () => exec("strikeThrough"),
1858
- tip: "删除线",
1859
1886
  },
1860
1887
  redo: {
1861
1888
  icon: jsx(RedoRound, {}),
1862
1889
  onClick: () => exec("redo"),
1863
- tip: "重做",
1864
1890
  },
1865
1891
  undo: {
1866
1892
  icon: jsx(UndoRound, {}),
1867
1893
  onClick: () => exec("undo"),
1868
- tip: "撤销",
1869
1894
  },
1870
- // color: {
1871
- // icon: <FormatColorTextRound />,
1872
- // onClick: () => exec("foreColor", false, ""),
1873
- // },
1874
- // backColor: {
1875
- // icon: <FormatColorFillRound />,
1876
- // onClick: () => exec("backColor", false, ""),
1877
- // },
1878
1895
  clear: {
1879
1896
  icon: jsx(ClearAllRound, {}),
1880
1897
  onClick: () => exec("removeFormat"),
1881
- tip: "清除格式",
1882
1898
  },
1883
1899
  };
1884
- const aliasMap = {
1885
- simple: ["undo", "redo", "bold", "italic", "underline", "strike", "clear"],
1886
- all: Object.keys(fnMap),
1900
+ const defaultKeys = [
1901
+ "undo",
1902
+ "redo",
1903
+ "bold",
1904
+ "italic",
1905
+ "underline",
1906
+ "strike",
1907
+ "clear",
1908
+ ];
1909
+ const typedFnMap = fnMap;
1910
+ function getControls(options) {
1911
+ const { controlBtnProps, addtionControls, getSelection } = options;
1912
+ const controls = defaultKeys.map((k) => {
1913
+ const { icon, onClick } = typedFnMap[k];
1914
+ return (jsx(Button, { ...controlBtnProps, onMouseDown: handleMouseDown, onClick: onClick, children: jsx(Icon, { icon: icon }) }, k));
1915
+ });
1916
+ const extControls = (addtionControls ?? []).map((item, index) => (jsx(Button, { ...controlBtnProps, onMouseDown: handleMouseDown, onClick: (e) => item.onClick?.(getSelection(), e), children: item.icon }, `addtion-${index}`)));
1917
+ return [...controls, ...extControls];
1918
+ }
1919
+
1920
+ const MEMTION_TAG_CLASS_NAME = "i-memtion-tag";
1921
+ const escapeHtmlAttr = (value) => value
1922
+ .replaceAll("&", "&amp;")
1923
+ .replaceAll('"', "&quot;")
1924
+ .replaceAll("<", "&lt;")
1925
+ .replaceAll(">", "&gt;");
1926
+ const getInsertNode = (memtion, option) => memtion?.insert?.(option) ?? option.value;
1927
+ const getInsertText = (memtion, option) => {
1928
+ const nextNode = getInsertNode(memtion, option);
1929
+ if (typeof nextNode === "string" || typeof nextNode === "number") {
1930
+ return `${String(nextNode)} `;
1931
+ }
1932
+ return `${String(option.value)} `;
1887
1933
  };
1888
- function getControls(fns, options) {
1889
- const { controlBtnProps } = options;
1890
- const keys = typeof fns === "string" ? aliasMap[fns] : fns;
1891
- return keys.map((k) => {
1892
- if (fnMap[k]) {
1893
- const { icon, render, tip, onClick } = fnMap[k];
1894
- if (render) {
1895
- return render(options);
1934
+ const getInsertHtml = (memtion, option, sanitizeValue) => {
1935
+ const nextNode = getInsertNode(memtion, option);
1936
+ if (nextNode === null || nextNode === undefined || nextNode === false) {
1937
+ return "";
1938
+ }
1939
+ const content = sanitizeValue(renderToStaticMarkup(jsx(Fragment, { children: nextNode })));
1940
+ return `<span class="${MEMTION_TAG_CLASS_NAME}" contenteditable="false" data-memtion-value="${escapeHtmlAttr(String(option.value))}">${content}</span>`;
1941
+ };
1942
+ const getSelectionRect = (range) => {
1943
+ if (!range)
1944
+ return null;
1945
+ const rect = range.getBoundingClientRect();
1946
+ if (rect.width || rect.height) {
1947
+ return rect;
1948
+ }
1949
+ return range.getClientRects()[0] ?? rect;
1950
+ };
1951
+ const insertMemtionOption = ({ editor, range, mode, memtion, option, sanitizeValue, }) => {
1952
+ if (!editor || !range)
1953
+ return null;
1954
+ const browserSelection = window.getSelection();
1955
+ if (!browserSelection)
1956
+ return null;
1957
+ const nextRange = range.cloneRange();
1958
+ browserSelection.removeAllRanges();
1959
+ browserSelection.addRange(nextRange);
1960
+ editor.focus();
1961
+ nextRange.deleteContents();
1962
+ if (mode === "plaintext") {
1963
+ const text = document.createTextNode(getInsertText(memtion, option));
1964
+ nextRange.insertNode(text);
1965
+ nextRange.setStartAfter(text);
1966
+ }
1967
+ else {
1968
+ const html = getInsertHtml(memtion, option, sanitizeValue);
1969
+ const fragment = nextRange.createContextualFragment(html);
1970
+ const lastNode = fragment.lastChild;
1971
+ const spacing = document.createTextNode(" ");
1972
+ nextRange.insertNode(fragment);
1973
+ if (lastNode) {
1974
+ nextRange.setStartAfter(lastNode);
1975
+ nextRange.collapse(true);
1976
+ }
1977
+ nextRange.insertNode(spacing);
1978
+ nextRange.setStartAfter(spacing);
1979
+ }
1980
+ nextRange.collapse(true);
1981
+ browserSelection.removeAllRanges();
1982
+ browserSelection.addRange(nextRange);
1983
+ return nextRange;
1984
+ };
1985
+ const getMemtionText = (triggerRange, selectionRange) => {
1986
+ if (!triggerRange || !selectionRange)
1987
+ return "";
1988
+ const range = triggerRange.cloneRange();
1989
+ range.setEnd(selectionRange.endContainer, selectionRange.endOffset);
1990
+ return range.toString();
1991
+ };
1992
+ const getMemtionReplaceRange = (triggerRange, selectionRange) => {
1993
+ if (!triggerRange || !selectionRange)
1994
+ return null;
1995
+ const range = triggerRange.cloneRange();
1996
+ range.setEnd(selectionRange.endContainer, selectionRange.endOffset);
1997
+ return range;
1998
+ };
1999
+ const filterMemtionOptions = (options, keyword) => {
2000
+ const normalizedKeyword = keyword.trim().toLowerCase();
2001
+ if (!normalizedKeyword) {
2002
+ return options;
2003
+ }
2004
+ return options.filter((option) => String(option.value).toLowerCase().includes(normalizedKeyword));
2005
+ };
2006
+ const isMemtionTag = (node) => node instanceof HTMLElement &&
2007
+ node.classList.contains(MEMTION_TAG_CLASS_NAME);
2008
+ const getAdjacentMemtionTag = (range, direction) => {
2009
+ const { startContainer, startOffset } = range;
2010
+ if (startContainer.nodeType === Node.TEXT_NODE) {
2011
+ const textNode = startContainer;
2012
+ if (direction === "backward" && startOffset === 0) {
2013
+ return isMemtionTag(textNode.previousSibling)
2014
+ ? textNode.previousSibling
2015
+ : null;
2016
+ }
2017
+ if (direction === "forward" && startOffset === textNode.data.length) {
2018
+ return isMemtionTag(textNode.nextSibling)
2019
+ ? textNode.nextSibling
2020
+ : null;
2021
+ }
2022
+ return null;
2023
+ }
2024
+ const element = startContainer;
2025
+ const targetIndex = direction === "backward" ? startOffset - 1 : startOffset;
2026
+ const targetNode = element.childNodes.item(targetIndex);
2027
+ return isMemtionTag(targetNode) ? targetNode : null;
2028
+ };
2029
+ const removeAdjacentMemtionTag = (editor, key) => {
2030
+ if (!editor)
2031
+ return false;
2032
+ const selection = window.getSelection();
2033
+ if (!selection?.rangeCount || !selection.isCollapsed)
2034
+ return false;
2035
+ const activeRange = selection.getRangeAt(0);
2036
+ const tag = getAdjacentMemtionTag(activeRange, key === "Backspace" ? "backward" : "forward");
2037
+ if (!tag || !tag.parentNode)
2038
+ return false;
2039
+ const parent = tag.parentNode;
2040
+ const nextSibling = tag.nextSibling;
2041
+ const index = Array.prototype.indexOf.call(parent.childNodes, tag);
2042
+ if (nextSibling?.nodeType === Node.TEXT_NODE) {
2043
+ const textNode = nextSibling;
2044
+ if (textNode.data.startsWith(" ")) {
2045
+ textNode.deleteData(0, 1);
2046
+ if (!textNode.data.length) {
2047
+ nextSibling.remove();
1896
2048
  }
1897
- return (jsxs(Button, { ...controlBtnProps, onClick: onClick, children: [jsx(Icon, { icon: icon }), tip && jsx("span", { className: 'i-editor-control-tip', children: tip })] }, k));
1898
2049
  }
1899
- return jsx(Fragment, {}, k);
1900
- });
1901
- }
2050
+ }
2051
+ tag.remove();
2052
+ const range = document.createRange();
2053
+ range.setStart(parent, Math.min(index, parent.childNodes.length));
2054
+ range.collapse(true);
2055
+ selection.removeAllRanges();
2056
+ selection.addRange(range);
2057
+ editor.focus();
2058
+ return true;
2059
+ };
2060
+ const Memtion = (props) => {
2061
+ const { visible, rect, options, activeIndex, onActiveChange, onSelect } = props;
2062
+ if (!visible || !rect || !options?.length) {
2063
+ return null;
2064
+ }
2065
+ return (jsx(List$1, { className: "i-editor-memtion", type: "option", style: {
2066
+ position: "fixed",
2067
+ top: rect.bottom,
2068
+ left: rect.left,
2069
+ }, children: options.map((option, i) => (jsx(List$1.Item, { type: "option", active: i === activeIndex, onMouseDown: (e) => e.preventDefault(), onMouseEnter: () => onActiveChange?.(i), onClick: () => onSelect?.(option), children: option.label }, `${option.value}-${i}`))) }));
2070
+ };
2071
+ var Memtion$1 = memo(Memtion);
1902
2072
 
2073
+ const controlBtnProps = {
2074
+ square: true,
2075
+ flat: true,
2076
+ size: "small",
2077
+ };
1903
2078
  const Editor = (props) => {
1904
- const { ref, width, height = "10em", placeholder, autosize, border = true, richPaste, controls = "simple", className, style, onInput, onPaste, onKeyDown, ...restProps } = props;
2079
+ const { ref, value = "", width, height = "10em", placeholder, autosize, border = true, mode = "rich", hideControl, addtionControls, memtion, className, style, onChange, onEnter, onFocus, onBlur, onPaste, onMouseUp, onKeyUp, onKeyDown, ...restProps } = props;
1905
2080
  const editorRef = useRef(null);
1906
- const controlBtnProps = {
1907
- square: true,
1908
- flat: true,
1909
- size: "small",
2081
+ const selectionRef = useRef(null);
2082
+ const memtionTriggerRangeRef = useRef(null);
2083
+ const pendingMemtionRef = useRef(false);
2084
+ const [memtionVisible, setMemtionVisible] = useState(false);
2085
+ const [memtionRect, setMemtionRect] = useState(null);
2086
+ const [memtionKeyword, setMemtionKeyword] = useState("");
2087
+ const [memtionActiveIndex, setMemtionActiveIndex] = useState(0);
2088
+ const memtionOptions = useMemo(() => filterMemtionOptions(memtion?.options ?? [], memtionKeyword), [memtion?.options, memtionKeyword]);
2089
+ const sanitizeValue = (nextValue) => {
2090
+ if (mode === "plaintext") {
2091
+ return nextValue === "\n" ? "" : nextValue;
2092
+ }
2093
+ const safeHtml = xss(nextValue, xssOptions);
2094
+ return safeHtml === "<br>" ? "" : safeHtml;
2095
+ };
2096
+ const rememberSelection = () => {
2097
+ if (!editorRef.current)
2098
+ return;
2099
+ const selection = window.getSelection();
2100
+ if (!selection?.rangeCount)
2101
+ return;
2102
+ const range = selection.getRangeAt(0);
2103
+ const container = range.commonAncestorContainer;
2104
+ const parent = container.nodeType === Node.ELEMENT_NODE
2105
+ ? container
2106
+ : container.parentElement;
2107
+ if (!parent || !editorRef.current.contains(parent))
2108
+ return;
2109
+ selectionRef.current = range.cloneRange();
2110
+ };
2111
+ const setEditorValue = (nextValue) => {
2112
+ if (!editorRef.current)
2113
+ return;
2114
+ const safeValue = sanitizeValue(nextValue);
2115
+ if (mode === "plaintext") {
2116
+ editorRef.current.textContent = safeValue;
2117
+ return;
2118
+ }
2119
+ editorRef.current.innerHTML = safeValue;
1910
2120
  };
1911
- const handlePaste = async (e) => {
2121
+ const getEditorValue = (sanitize = false) => {
2122
+ if (!editorRef.current)
2123
+ return "";
2124
+ const nextValue = mode === "plaintext"
2125
+ ? (editorRef.current.textContent ?? "")
2126
+ : editorRef.current.innerHTML;
2127
+ return sanitize ? sanitizeValue(nextValue) : nextValue;
2128
+ };
2129
+ const hideMemtion = () => {
2130
+ pendingMemtionRef.current = false;
2131
+ memtionTriggerRangeRef.current = null;
2132
+ setMemtionVisible(false);
2133
+ setMemtionRect(null);
2134
+ setMemtionKeyword("");
2135
+ setMemtionActiveIndex(0);
2136
+ };
2137
+ const insertMemtion = (option) => {
2138
+ const replaceRange = getMemtionReplaceRange(memtionTriggerRangeRef.current, selectionRef.current);
2139
+ const range = insertMemtionOption({
2140
+ editor: editorRef.current,
2141
+ range: replaceRange,
2142
+ mode,
2143
+ memtion,
2144
+ option,
2145
+ sanitizeValue,
2146
+ });
2147
+ if (!range || !editorRef.current)
2148
+ return;
2149
+ selectionRef.current = range.cloneRange();
2150
+ hideMemtion();
2151
+ editorRef.current.dispatchEvent(new Event("input", { bubbles: true }));
2152
+ };
2153
+ const handlePaste = (e) => {
1912
2154
  onPaste?.(e);
1913
- if (richPaste)
2155
+ if (e.defaultPrevented)
1914
2156
  return;
1915
2157
  e.preventDefault();
2158
+ if (mode === "plaintext") {
2159
+ const text = e.clipboardData.getData("text/plain");
2160
+ exec("insertText", false, text);
2161
+ return;
2162
+ }
2163
+ const html = e.clipboardData.getData("text/html");
2164
+ if (html) {
2165
+ exec("insertHTML", false, sanitizeValue(html));
2166
+ return;
2167
+ }
1916
2168
  const text = e.clipboardData.getData("text/plain");
1917
2169
  exec("insertText", false, text);
1918
2170
  };
1919
2171
  const handleKeyDown = (e) => {
1920
2172
  onKeyDown?.(e);
2173
+ if (mode === "rich" &&
2174
+ (e.key === "Backspace" || e.key === "Delete") &&
2175
+ removeAdjacentMemtionTag(editorRef.current, e.key)) {
2176
+ e.preventDefault();
2177
+ rememberSelection();
2178
+ editorRef.current?.dispatchEvent(new Event("input", { bubbles: true }));
2179
+ return;
2180
+ }
2181
+ const memtionKey = memtion?.key ?? "@";
2182
+ if (memtionVisible && e.key === " ") {
2183
+ hideMemtion();
2184
+ }
2185
+ if (memtionVisible && memtionOptions.length) {
2186
+ switch (e.key) {
2187
+ case "ArrowDown":
2188
+ e.preventDefault();
2189
+ setMemtionActiveIndex((index) => index + 1 >= memtionOptions.length ? 0 : index + 1);
2190
+ return;
2191
+ case "ArrowUp":
2192
+ e.preventDefault();
2193
+ setMemtionActiveIndex((index) => index - 1 < 0 ? memtionOptions.length - 1 : index - 1);
2194
+ return;
2195
+ case "Enter":
2196
+ e.preventDefault();
2197
+ insertMemtion(memtionOptions[memtionActiveIndex]);
2198
+ return;
2199
+ }
2200
+ }
2201
+ if (memtion && e.key === memtionKey) {
2202
+ rememberSelection();
2203
+ memtionTriggerRangeRef.current =
2204
+ selectionRef.current?.cloneRange() ?? null;
2205
+ pendingMemtionRef.current = true;
2206
+ }
1921
2207
  switch (e.key) {
1922
2208
  case "Tab":
1923
2209
  e.preventDefault();
1924
- exec("insertHTML", false, "&#09;");
2210
+ exec(mode === "plaintext" ? "insertText" : "insertHTML", false, mode === "plaintext" ? "\t" : "&#09;");
1925
2211
  break;
1926
2212
  case "Enter":
2213
+ if (!onEnter)
2214
+ break;
1927
2215
  e.preventDefault();
1928
- exec("insertLineBreak");
1929
- if (!editorRef.current)
1930
- return;
1931
- editorRef.current.scrollBy({
1932
- top: 20,
1933
- left: -1e3,
1934
- });
1935
- if (!autosize)
1936
- return;
1937
- editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
2216
+ onEnter(e);
1938
2217
  break;
1939
2218
  }
1940
2219
  };
1941
- useImperativeHandle(ref, () => {
1942
- return {
1943
- input: editorRef.current,
1944
- setValue(html) {
1945
- if (!editorRef.current)
1946
- return;
1947
- const safeHtml = xss(html, xssOptions);
1948
- editorRef.current.innerHTML = safeHtml;
1949
- },
1950
- getSafeValue() {
1951
- const html = editorRef.current?.innerHTML ?? "";
1952
- return xss(html, xssOptions);
1953
- },
1954
- };
1955
- });
2220
+ useEffect(() => {
2221
+ if (!editorRef.current)
2222
+ return;
2223
+ const nextValue = sanitizeValue(value);
2224
+ if (getEditorValue(true) === nextValue)
2225
+ return;
2226
+ setEditorValue(nextValue);
2227
+ if (autosize) {
2228
+ editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
2229
+ }
2230
+ }, [autosize, mode, value]);
2231
+ useEffect(() => {
2232
+ if (!memtionOptions.length) {
2233
+ setMemtionActiveIndex(0);
2234
+ return;
2235
+ }
2236
+ setMemtionActiveIndex((index) => index >= memtionOptions.length ? 0 : index);
2237
+ }, [memtionOptions]);
1956
2238
  const handleInput = (e) => {
1957
- let html = editorRef.current?.innerHTML ?? "";
1958
- if (["<br>", "\n"].includes(html) && editorRef.current) {
1959
- html = "";
1960
- editorRef.current.innerHTML = html;
2239
+ const rawValue = getEditorValue();
2240
+ let nextValue = sanitizeValue(rawValue);
2241
+ if (!nextValue && rawValue && editorRef.current) {
2242
+ nextValue = "";
2243
+ setEditorValue(nextValue);
2244
+ }
2245
+ rememberSelection();
2246
+ if (memtion && (pendingMemtionRef.current || memtionVisible)) {
2247
+ const memtionKey = memtion?.key ?? "@";
2248
+ const memtionText = getMemtionText(memtionTriggerRangeRef.current, selectionRef.current);
2249
+ if (!memtionText.startsWith(memtionKey) || /\s/.test(memtionText)) {
2250
+ hideMemtion();
2251
+ }
2252
+ else {
2253
+ const keyword = memtionText.slice(memtionKey.length);
2254
+ pendingMemtionRef.current = false;
2255
+ setMemtionRect(getSelectionRect(selectionRef.current));
2256
+ setMemtionKeyword(keyword);
2257
+ setMemtionActiveIndex(0);
2258
+ setMemtionVisible(true);
2259
+ }
2260
+ }
2261
+ if (autosize && editorRef.current) {
2262
+ editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
1961
2263
  }
1962
- onInput?.(html, e);
2264
+ onChange?.(nextValue, e);
2265
+ };
2266
+ const handleFocus = (e) => {
2267
+ rememberSelection();
2268
+ onFocus?.(e);
2269
+ };
2270
+ const handleBlur = (e) => {
2271
+ hideMemtion();
2272
+ onBlur?.(e);
1963
2273
  };
2274
+ const handleMouseUp = (e) => {
2275
+ rememberSelection();
2276
+ onMouseUp?.(e);
2277
+ };
2278
+ const handleKeyUp = (e) => {
2279
+ rememberSelection();
2280
+ onKeyUp?.(e);
2281
+ };
2282
+ const handleRef = (node) => {
2283
+ editorRef.current = node;
2284
+ if (typeof ref === "function") {
2285
+ ref(node);
2286
+ return;
2287
+ }
2288
+ if (ref) {
2289
+ ref.current = node;
2290
+ }
2291
+ };
2292
+ const getSelection = useCallback(() => selectionRef.current?.cloneRange() ?? null, []);
2293
+ const controls = useMemo(() => getControls({
2294
+ controlBtnProps,
2295
+ addtionControls,
2296
+ getSelection,
2297
+ }), [addtionControls, getSelection]);
1964
2298
  return (jsxs("div", { className: classNames("i-editor", className, {
1965
2299
  "i-editor-borderless": !border,
1966
2300
  }), style: {
1967
2301
  ...style,
1968
2302
  [autosize ? "minHeight" : "height"]: height,
1969
2303
  width,
1970
- }, children: [controls !== "none" && (jsx("div", { className: 'i-editor-controls', children: getControls(controls, {
1971
- controlBtnProps,
1972
- }) })), jsx("div", { ref: editorRef, className: 'i-editor-content', "data-placeholder": placeholder, contentEditable: true, onPaste: handlePaste, onInput: handleInput, onKeyDown: handleKeyDown, ...restProps })] }));
2304
+ }, children: [!hideControl && (jsx("div", { className: "i-editor-controls", children: controls })), memtion && (jsx(Memtion$1, { visible: memtionVisible, rect: memtionRect, options: memtionOptions, activeIndex: memtionActiveIndex, onActiveChange: setMemtionActiveIndex, onSelect: insertMemtion })), jsx("div", { ref: handleRef, className: "i-editor-content", "data-placeholder": placeholder, contentEditable: mode === "plaintext" ? "plaintext-only" : true, onFocus: handleFocus, onBlur: handleBlur, onMouseUp: handleMouseUp, onPaste: handlePaste, onInput: handleInput, onKeyUp: handleKeyUp, onKeyDown: handleKeyDown, ...restProps })] }));
1973
2305
  };
1974
2306
 
1975
2307
  const Flex = (props) => {
@@ -2405,7 +2737,7 @@ const HookModal = (props) => {
2405
2737
  },
2406
2738
  close: () => {
2407
2739
  state.visible = false;
2408
- if (mergedProps.closable)
2740
+ if (mergedProps.closable ?? true)
2409
2741
  return;
2410
2742
  Promise.resolve().then(() => {
2411
2743
  state.visible = true;
@@ -2952,27 +3284,34 @@ function List(props) {
2952
3284
  if (!files.length)
2953
3285
  return "";
2954
3286
  return (jsx(Flex, { className: 'i-image-list', gap: gap, columns: columns, wrap: wrap, direction: direction, children: files.map((img, i) => {
2955
- return (jsx(Image, { src: img.src, usePreview: false, onClick: (e) => handleClick(e, i), ...restProps }, i));
3287
+ return (jsx(MemoImage, { src: img.src, usePreview: false, onClick: (e) => handleClick(e, i), ...restProps }, i));
2956
3288
  }) }));
2957
3289
  }
2958
3290
 
3291
+ const STATUS_LOADING = "loading";
3292
+ const STATUS_ERROR = "error";
2959
3293
  const Image = (props) => {
2960
3294
  const { src, thumb, round, size, height, width, ratio, initSize, lazyload, fallback, fit, style, className, cover, coverClass, usePreview: previewable, onLoad, onError, onClick, ...restProps } = props;
2961
3295
  const state = useReactive({
2962
- status: "loading",
3296
+ status: STATUS_LOADING,
2963
3297
  });
2964
3298
  const ref = useRef(null);
2965
3299
  const { observe, unobserve } = useIntersectionObserver();
2966
3300
  const preview = usePreview();
2967
- const handleError = (err) => {
3301
+ const setStatus = useCallback((status) => {
3302
+ if (state.status === status)
3303
+ return;
3304
+ state.status = status;
3305
+ }, [state]);
3306
+ const handleError = useCallback((err) => {
2968
3307
  onError?.(err);
2969
- state.status = "error";
2970
- };
2971
- const handleLoad = (e) => {
3308
+ setStatus(STATUS_ERROR);
3309
+ }, [onError, setStatus]);
3310
+ const handleLoad = useCallback((e) => {
2972
3311
  onLoad?.(e);
2973
- state.status = undefined;
2974
- };
2975
- const handleClick = (e) => {
3312
+ setStatus(undefined);
3313
+ }, [onLoad, setStatus]);
3314
+ const handleClick = useCallback((e) => {
2976
3315
  onClick?.(e);
2977
3316
  if (!previewable || !src)
2978
3317
  return;
@@ -2986,44 +3325,53 @@ const Image = (props) => {
2986
3325
  },
2987
3326
  ],
2988
3327
  });
2989
- };
3328
+ }, [onClick, preview, previewable, src]);
2990
3329
  useEffect(() => {
2991
3330
  if (!src || typeof window === "undefined")
2992
3331
  return;
2993
3332
  const img = ref.current;
3333
+ if (!img)
3334
+ return;
2994
3335
  const hasSrcAttr = img?.getAttribute("src");
2995
3336
  const canSyncStatus = Boolean(img && (!lazyload || hasSrcAttr));
2996
- if (canSyncStatus && img?.complete) {
2997
- state.status = img.naturalWidth > 0 ? undefined : "error";
3337
+ if (canSyncStatus && img.complete) {
3338
+ setStatus(img.naturalWidth > 0 ? undefined : STATUS_ERROR);
2998
3339
  }
2999
- if (!ref.current?.complete && observe && lazyload) {
3000
- state.status = "loading";
3340
+ if (!img.complete && observe && lazyload) {
3341
+ setStatus(STATUS_LOADING);
3001
3342
  }
3002
- if (!lazyload || !ref.current || !observe)
3343
+ if (!lazyload || !observe)
3003
3344
  return;
3004
- observe(ref.current, (tar, visible) => {
3345
+ observe(img, (tar, visible) => {
3005
3346
  if (!visible)
3006
3347
  return;
3007
3348
  tar.setAttribute("src", tar.dataset.src || "");
3008
3349
  unobserve(tar);
3009
3350
  });
3010
3351
  return () => {
3011
- ref.current && unobserve(ref.current);
3352
+ unobserve(img);
3012
3353
  };
3013
- }, [src]);
3014
- restProps[lazyload ? "data-src" : "src"] = thumb ?? src;
3015
- const iSize = state.status === "loading" ? initSize : undefined;
3016
- return (jsx("div", { style: {
3017
- width: width ?? size ?? iSize,
3018
- height: height ?? size ?? iSize,
3019
- aspectRatio: ratio,
3020
- ...style,
3021
- }, className: classNames("i-image", className, {
3022
- rounded: round,
3023
- [`i-image-${state.status}`]: state.status,
3024
- }), onClick: handleClick, children: state.status === "error" ? ((fallback ?? null)) : (jsxs(Fragment, { children: [src && (jsx("img", { ref: ref, style: { objectFit: fit }, ...restProps, onLoad: handleLoad, onError: handleError })), src && state.status === "loading" && jsx(Loading, { absolute: true }), cover && (jsx("div", { className: classNames("i-image-cover", coverClass), children: cover }))] })) }));
3354
+ }, [lazyload, observe, setStatus, src, unobserve]);
3355
+ const imageStatus = state.status;
3356
+ const iSize = imageStatus === STATUS_LOADING ? initSize : undefined;
3357
+ const wrapperStyle = useMemo(() => ({
3358
+ width: width ?? size ?? iSize,
3359
+ height: height ?? size ?? iSize,
3360
+ aspectRatio: ratio,
3361
+ ...style,
3362
+ }), [height, iSize, ratio, size, style, width]);
3363
+ const wrapperClassName = useMemo(() => classNames("i-image", className, {
3364
+ rounded: round,
3365
+ [`i-image-${imageStatus}`]: imageStatus,
3366
+ }), [className, imageStatus, round]);
3367
+ const imageStyle = useMemo(() => ({ objectFit: fit }), [fit]);
3368
+ const imageSrcProps = lazyload
3369
+ ? { "data-src": thumb ?? src }
3370
+ : { src: thumb ?? src };
3371
+ return (jsx("div", { style: wrapperStyle, className: wrapperClassName, onClick: handleClick, children: imageStatus === STATUS_ERROR ? ((fallback ?? null)) : (jsxs(Fragment, { children: [src && (jsx("img", { ref: ref, style: imageStyle, ...imageSrcProps, ...restProps, onLoad: handleLoad, onError: handleError })), src && imageStatus === STATUS_LOADING && (jsx(Loading, { absolute: true })), cover && (jsx("div", { className: classNames("i-image-cover", coverClass), children: cover }))] })) }));
3025
3372
  };
3026
- Image.List = List;
3373
+ const MemoImage = memo(Image);
3374
+ MemoImage.List = List;
3027
3375
 
3028
3376
  function InputContainer(props) {
3029
3377
  const { as: As = "label", label, className, labelInline, style, children, status, tip, required, } = props;
@@ -3199,16 +3547,19 @@ const Range = (props) => {
3199
3547
  };
3200
3548
 
3201
3549
  const Textarea = (props) => {
3202
- const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border, width, style, onChange, onEnter, ...restProps } = props;
3550
+ const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border, width, style, resize, onChange, onEnter, ...restProps } = props;
3203
3551
  const [textareaValue, setTextareaValue] = useState(value);
3204
3552
  const refTextarea = useRef(null);
3553
+ const syncTextareaHeight = () => {
3554
+ const ta = refTextarea.current;
3555
+ if (!autoSize || !ta)
3556
+ return;
3557
+ ta.style.height = "auto";
3558
+ ta.style.height = `${ta.scrollHeight}px`;
3559
+ };
3205
3560
  const handleChange = (e) => {
3206
3561
  const v = e.target.value;
3207
3562
  setTextareaValue(v);
3208
- const ta = refTextarea.current;
3209
- if (autoSize && ta) {
3210
- ta.style.height = `${ta.scrollHeight}px`;
3211
- }
3212
3563
  onChange?.(v, e);
3213
3564
  };
3214
3565
  const handleKeydown = (e) => {
@@ -3220,6 +3571,9 @@ const Textarea = (props) => {
3220
3571
  useEffect(() => {
3221
3572
  setTextareaValue(value);
3222
3573
  }, [value]);
3574
+ useEffect(() => {
3575
+ syncTextareaHeight();
3576
+ }, [autoSize, textareaValue]);
3223
3577
  useImperativeHandle(ref, () => {
3224
3578
  return {
3225
3579
  input: refTextarea.current,
@@ -3230,6 +3584,7 @@ const Textarea = (props) => {
3230
3584
  name,
3231
3585
  value: textareaValue,
3232
3586
  className: "i-input i-textarea",
3587
+ style: resize === false ? { resize: "none" } : undefined,
3233
3588
  onChange: handleChange,
3234
3589
  onKeyDown: handleKeydown,
3235
3590
  ...restProps,
@@ -3687,7 +4042,7 @@ function Footer(props) {
3687
4042
  setInputValue(value);
3688
4043
  setColorType(type);
3689
4044
  }, [value, type]);
3690
- return (jsxs("div", { className: 'i-colorpicker-footer', children: [jsx(Select, { readOnly: true, hideClear: true, hideArrow: true, style: { width: "5.6em" }, options: ["RGB", "HEX", "HSB"], value: colorType, onChange: handleTypeChange }), jsx(Input, { placeholder: 'color', value: inputValue, onChange: handleChange }), jsx(Button, { square: true, onClick: onOk, children: jsx(Icon, { icon: jsx(CheckRound, {}) }) })] }));
4045
+ return (jsxs("div", { className: "i-colorpicker-footer", children: [jsx(Select, { readOnly: true, hideClear: true, hideArrow: true, style: { width: "5.6em" }, options: ["RGB", "HEX", "HSB"], value: colorType, onChange: handleTypeChange, popupProps: { fitSize: false } }), jsx(Input, { placeholder: "color", value: inputValue, onChange: handleChange }), jsx(Button, { square: true, onClick: onOk, children: jsx(Icon, { icon: jsx(CheckRound, {}) }) })] }));
3691
4046
  }
3692
4047
 
3693
4048
  const Handle = (props) => {
@@ -4029,10 +4384,10 @@ const defaultOk = {
4029
4384
  };
4030
4385
  const defaultCancel = {
4031
4386
  children: "取消",
4032
- secondary: true,
4387
+ flat: true,
4033
4388
  };
4034
4389
  const Popconfirm = (props) => {
4035
- const { trigger = "click", visible, icon = jsx(Icon, { icon: jsx(InfoOutlined, {}), className: 'error', size: '1.2em' }), content, okButtonProps, cancelButtonProps, children, align, position = "top", offset = 12, extra, onOk, onClose, ...restProps } = props;
4390
+ const { trigger = "click", visible, icon = jsx(Icon, { icon: jsx(InfoOutlined, {}), className: "error", size: "1.2em" }), content, okButtonProps, cancelButtonProps, children, align, position = "top", offset = 12, extra, onOk, onClose, ...restProps } = props;
4036
4391
  const state = useReactive({
4037
4392
  loading: false,
4038
4393
  visible,
@@ -4063,7 +4418,7 @@ const Popconfirm = (props) => {
4063
4418
  await onClose?.();
4064
4419
  state.visible = false;
4065
4420
  };
4066
- const popconfirmContent = (jsxs("div", { className: 'i-popconfirm', children: [jsxs(Flex, { gap: '.5em', children: [icon, jsx("div", { className: 'i-popconfirm-content', children: content })] }), jsxs(Flex, { gap: 12, justify: 'flex-end', className: 'i-popconfirm-footer', children: [cancelButtonProps !== null && (jsx(Button, { ...cancel, onClick: handleCancel })), extra, okButtonProps !== null && (jsx(Button, { loading: state.loading, ...ok, onClick: handleOk }))] })] }));
4421
+ const popconfirmContent = (jsxs("div", { className: "i-popconfirm", children: [jsxs(Flex, { gap: ".5em", children: [icon, jsx("div", { className: "i-popconfirm-content", children: content })] }), jsxs(Flex, { gap: 12, justify: "flex-end", className: "i-popconfirm-footer", children: [cancelButtonProps !== null && (jsx(Button, { ...cancel, onClick: handleCancel })), extra, okButtonProps !== null && (jsx(Button, { loading: state.loading, ...ok, onClick: handleOk }))] })] }));
4067
4422
  return (jsx(Popup, { content: popconfirmContent, ...restProps, trigger: trigger, visible: state.visible, align: align, offset: offset, position: position, onVisibleChange: handleVisibleChange, children: children }));
4068
4423
  };
4069
4424
 
@@ -4549,7 +4904,7 @@ const Item = (props) => {
4549
4904
  };
4550
4905
 
4551
4906
  const Tabs = ((props) => {
4552
- const { ref, active, tabs: items, type = "default", prepend, append, children, className, vertical, toggable, navsJustify = "start", bar = true, hideMore, barClass, renderMore = () => (jsx(Button, { flat: true, square: true, size: 'small', children: jsx(Icon, { icon: jsx(MoreHorizRound, {}) }) })), onTabChange, ...rest } = props;
4907
+ const { ref, active, tabs: items, type = "default", prepend, append, children, className, vertical, toggable, navsJustify = "start", bar = true, hideMore, barClass, renderMore = () => (jsx(Button, { flat: true, square: true, size: "small", children: jsx(Icon, { icon: jsx(MoreHorizRound, {}) }) })), onTabChange, ...rest } = props;
4553
4908
  const navRefs = useRef([]);
4554
4909
  const barRef = useRef(null);
4555
4910
  const navsRef = useRef(null);
@@ -4559,10 +4914,15 @@ const Tabs = ((props) => {
4559
4914
  const [barStyle, setBarStyle] = useState({});
4560
4915
  const [cachedTabs, setCachedTabs] = useState([]);
4561
4916
  const [overflow, setOverflow] = useState(false);
4562
- const [moreTabs, setMoreTabs] = useState([]);
4563
4917
  const [tabs, setTabs] = useState([]);
4564
4918
  const { observe, unobserve } = useIntersectionObserver();
4565
4919
  const size = useSize(navsRef);
4920
+ const tabsRef = useRef(tabs);
4921
+ tabsRef.current = tabs;
4922
+ const activeKeyRef = useRef(activeKey);
4923
+ activeKeyRef.current = activeKey;
4924
+ const prevActiveKeyRef = useRef(prevActiveKey);
4925
+ prevActiveKeyRef.current = prevActiveKey;
4566
4926
  useEffect(() => {
4567
4927
  contentsRef.current.clear();
4568
4928
  if (!items) {
@@ -4599,10 +4959,11 @@ const Tabs = ((props) => {
4599
4959
  }));
4600
4960
  }, [children, items]);
4601
4961
  const add = (tab) => {
4602
- const tkey = String(tab.key ?? tabs.length);
4603
- const i = tabs.findIndex((t) => t.key === tkey);
4962
+ const currentTabs = tabsRef.current;
4963
+ const tkey = String(tab.key ?? currentTabs.length);
4964
+ const i = currentTabs.findIndex((t) => t.key === tkey);
4604
4965
  if (i > -1) {
4605
- open(tabs[i].key ?? `${i}`);
4966
+ open(currentTabs[i].key ?? `${i}`);
4606
4967
  return;
4607
4968
  }
4608
4969
  contentsRef.current.set(tkey, tab.content);
@@ -4611,20 +4972,34 @@ const Tabs = ((props) => {
4611
4972
  open(tkey);
4612
4973
  };
4613
4974
  const close = (key) => {
4614
- const i = tabs.findIndex((t) => t.key === key);
4975
+ const currentTabs = tabsRef.current;
4976
+ const i = currentTabs.findIndex((t) => t.key === key);
4615
4977
  if (i < 0)
4616
4978
  return;
4617
4979
  contentsRef.current.delete(key);
4618
- const nextTabs = [...tabs];
4980
+ const nextTabs = [...currentTabs];
4619
4981
  nextTabs.splice(i, 1);
4620
4982
  setTabs(nextTabs);
4621
- if (activeKey !== key)
4983
+ if (activeKeyRef.current !== key)
4622
4984
  return;
4623
4985
  const next = nextTabs[i] || nextTabs[i - 1];
4624
- open(prevActiveKey ?? next?.key ?? "");
4986
+ const prev = prevActiveKeyRef.current;
4987
+ const nextKey = prev && nextTabs.some((t) => t.key === prev) ? prev : next?.key;
4988
+ open(nextKey ?? "");
4625
4989
  };
4626
4990
  const open = (key) => {
4627
- if (key === activeKey) {
4991
+ const nextKey = key || undefined;
4992
+ if (nextKey === undefined) {
4993
+ onTabChange?.(undefined, activeKey);
4994
+ setPrevActiveKey(activeKey);
4995
+ setActiveKey(undefined);
4996
+ setBarStyle({
4997
+ height: 0,
4998
+ width: 0,
4999
+ });
5000
+ return;
5001
+ }
5002
+ if (nextKey === activeKey) {
4628
5003
  if (!toggable)
4629
5004
  return;
4630
5005
  onTabChange?.(undefined, key);
@@ -4636,38 +5011,75 @@ const Tabs = ((props) => {
4636
5011
  return;
4637
5012
  }
4638
5013
  setPrevActiveKey(activeKey);
4639
- onTabChange?.(key, activeKey);
4640
- setActiveKey(key);
5014
+ onTabChange?.(nextKey, activeKey);
5015
+ setActiveKey(nextKey);
5016
+ };
5017
+ const handleKeyAction = (e, action) => {
5018
+ if (!["Enter", " "].includes(e.key))
5019
+ return;
5020
+ e.preventDefault();
5021
+ action();
5022
+ };
5023
+ const scrollToTab = (key) => {
5024
+ const index = tabsRef.current.findIndex((tab) => tab.key === key);
5025
+ const nav = navRefs.current[index];
5026
+ nav?.scrollIntoView({
5027
+ behavior: "smooth",
5028
+ block: "nearest",
5029
+ inline: "nearest",
5030
+ });
5031
+ };
5032
+ const handleMoreTabClick = (key) => {
5033
+ open(key);
5034
+ scrollToTab(key);
4641
5035
  };
4642
5036
  useEffect(() => {
4643
- if (!size || hideMore || !observe)
5037
+ if (!size || hideMore || !observe || !unobserve)
4644
5038
  return;
4645
5039
  const { scrollHeight, scrollWidth } = navsRef.current;
4646
5040
  const { width, height } = size;
4647
5041
  const nextOverflow = scrollHeight > height || scrollWidth > width;
4648
- setOverflow(nextOverflow);
4649
- if (!nextOverflow)
5042
+ setOverflow((v) => (v === nextOverflow ? v : nextOverflow));
5043
+ if (!nextOverflow) {
5044
+ setTabs((ts) => {
5045
+ let changed = false;
5046
+ const next = ts.map((t) => {
5047
+ if (t.intersecting === undefined ||
5048
+ t.intersecting === true) {
5049
+ return t;
5050
+ }
5051
+ changed = true;
5052
+ return { ...t, intersecting: true };
5053
+ });
5054
+ return changed ? next : ts;
5055
+ });
4650
5056
  return;
5057
+ }
5058
+ const observed = [];
4651
5059
  navRefs.current.map((nav, i) => {
4652
5060
  if (!nav)
4653
5061
  return;
4654
- observe(nav, (tar, visible) => {
5062
+ observed.push(nav);
5063
+ observe(nav, (_tar, visible) => {
4655
5064
  setTabs((ts) => {
4656
5065
  if (!ts[i])
4657
5066
  return ts;
4658
- const nextTabs = ts.map((t, idx) => idx === i ? { ...t, intersecting: visible } : t);
4659
- setMoreTabs(nextTabs.filter((tab) => !tab.intersecting));
4660
- return nextTabs;
5067
+ if (ts[i]?.intersecting === visible)
5068
+ return ts;
5069
+ return ts.map((t, idx) => idx === i ? { ...t, intersecting: visible } : t);
4661
5070
  });
4662
5071
  });
4663
5072
  });
4664
- }, [size, hideMore, tabs.length, observe]);
5073
+ return () => {
5074
+ observed.map((el) => unobserve(el));
5075
+ };
5076
+ }, [size, hideMore, tabs.length, observe, unobserve]);
4665
5077
  useEffect(() => {
4666
5078
  if (!bar || type === "pane" || activeKey === undefined) {
4667
5079
  return;
4668
5080
  }
4669
5081
  const index = tabs.findIndex((tab) => tab.key === activeKey);
4670
- setTimeout(() => {
5082
+ const timer = window.setTimeout(() => {
4671
5083
  const nav = navRefs.current[index];
4672
5084
  if (!nav)
4673
5085
  return;
@@ -4686,19 +5098,15 @@ const Tabs = ((props) => {
4686
5098
  transform: `translate(${offsetLeft}px, ${offsetTop}px)`,
4687
5099
  });
4688
5100
  }, 16);
5101
+ return () => {
5102
+ window.clearTimeout(timer);
5103
+ };
4689
5104
  }, [activeKey, bar, size, tabs, type, vertical]);
4690
5105
  useEffect(() => {
4691
5106
  if (active === undefined || activeKey === active)
4692
5107
  return;
4693
5108
  open(active);
4694
5109
  }, [active]);
4695
- useEffect(() => {
4696
- if (hideMore || !unobserve)
4697
- return;
4698
- return () => {
4699
- navRefs.current?.map(unobserve);
4700
- };
4701
- }, [tabs.length, hideMore, unobserve]);
4702
5110
  useEffect(() => {
4703
5111
  if (!navsRef.current || vertical)
4704
5112
  return;
@@ -4719,30 +5127,35 @@ const Tabs = ((props) => {
4719
5127
  return;
4720
5128
  navsRef.current.removeEventListener("wheel", handleMouseWheel);
4721
5129
  };
4722
- }, [navsRef.current]);
5130
+ }, [vertical]);
4723
5131
  useImperativeHandle(ref, () => ({
4724
5132
  open,
4725
5133
  close,
4726
5134
  add,
4727
5135
  navs: navsRef,
4728
5136
  }));
5137
+ const moreTabs = !hideMore && overflow
5138
+ ? tabs.filter((tab) => tab.intersecting === false)
5139
+ : [];
4729
5140
  return (jsxs("div", { className: classNames("i-tabs", { flex: vertical, [`i-tabs-${type}`]: type !== "default" }, className), ...rest, children: [jsxs("div", { className: classNames("i-tab-navs-container", {
4730
5141
  "i-tab-navs-vertical": vertical,
4731
- }), children: [prepend, jsxs("div", { ref: navsRef, className: classNames("i-tab-navs", `justify-${navsJustify}`), children: [tabs.map((tab, i) => {
5142
+ }), children: [prepend, jsxs("div", { ref: navsRef, className: classNames("i-tab-navs", `justify-${navsJustify}`), role: "tablist", "aria-orientation": vertical ? "vertical" : "horizontal", children: [tabs.map((tab, i) => {
4732
5143
  const { title, key = `${i}`, closable } = tab;
5144
+ const isActive = activeKey === key;
4733
5145
  return (jsxs("a", { ref: (ref) => (navRefs.current[i] = ref), className: classNames("i-tab-nav", {
4734
- "i-tab-active": activeKey === key,
4735
- }), onClick: () => open(key), children: [title, closable && (jsx(Helpericon, { as: 'i', active: true, className: 'i-tab-nav-close', onClick: (e) => {
5146
+ "i-tab-active": isActive,
5147
+ }), role: "tab", tabIndex: isActive ? 0 : -1, "aria-selected": isActive, onClick: () => open(key), onKeyDown: (e) => handleKeyAction(e, () => open(key)), children: [title, closable && (jsx(Helpericon, { as: "i", active: true, className: "i-tab-nav-close", role: "button", tabIndex: 0, "aria-label": "\u5173\u95ED\u6807\u7B7E\u9875", onClick: (e) => {
5148
+ e.preventDefault();
4736
5149
  e.stopPropagation();
4737
5150
  close(key);
4738
- } }))] }, key));
4739
- }), bar && (jsx("span", { ref: barRef, className: classNames("i-tab-navs-bar", barClass), style: barStyle }))] }), !hideMore && overflow && moreTabs.length > 0 && (jsx(Popup, { arrow: false, position: vertical ? "right" : "bottom", align: 'end', touchable: true, hideDelay: 500, content: jsx("div", { className: 'i-tabs-morelist pd-4', children: moreTabs.map((tab, i) => {
5151
+ }, onKeyDown: (e) => handleKeyAction(e, () => close(key)) }))] }, key));
5152
+ }), bar && (jsx("span", { ref: barRef, className: classNames("i-tab-navs-bar", barClass), style: barStyle }))] }), !hideMore && overflow && moreTabs.length > 0 && (jsx(Popup, { arrow: false, position: vertical ? "right" : "bottom", align: "end", touchable: true, hideDelay: 500, content: jsx("div", { className: "i-tabs-morelist pd-4", children: moreTabs.map((tab, i) => {
4740
5153
  const { key = `${i}`, title } = tab;
4741
5154
  const isActive = activeKey === key;
4742
5155
  return (jsx("a", { className: classNames("i-tab-nav", {
4743
5156
  "i-tab-active": isActive,
4744
- }), onClick: () => open(key), children: title }, key));
4745
- }) }), children: renderMore(moreTabs) })), append] }), jsx("div", { className: 'i-tab-contents', children: tabs.map((tab, i) => {
5157
+ }), role: "button", tabIndex: 0, onClick: () => handleMoreTabClick(key), onKeyDown: (e) => handleKeyAction(e, () => handleMoreTabClick(key)), children: title }, key));
5158
+ }) }), children: renderMore(moreTabs) })), append] }), jsx("div", { className: "i-tab-contents", children: tabs.map((tab, i) => {
4746
5159
  const key = tab.key ?? `${i}`;
4747
5160
  const content = contentsRef.current.get(key);
4748
5161
  const isActive = activeKey === key;
@@ -4750,7 +5163,7 @@ const Tabs = ((props) => {
4750
5163
  (key !== undefined && cachedTabs.includes(key));
4751
5164
  return (show && (jsx("div", { className: classNames("i-tab-content", {
4752
5165
  "i-tab-active": isActive,
4753
- }), children: content }, key)));
5166
+ }), role: "tabpanel", "aria-hidden": !isActive, children: content }, key)));
4754
5167
  }) })] }));
4755
5168
  });
4756
5169
  Tabs.Item = Item;
@@ -4976,7 +5389,7 @@ const FileListItem = (props) => {
4976
5389
  let node = jsx(Fragment, {});
4977
5390
  switch (type) {
4978
5391
  case TFileType.IMAGE:
4979
- node = (jsx(Image, { lazyload: true, src: url || src, fit: 'cover', onMouseDown: (e) => e.preventDefault() }));
5392
+ node = (jsx(MemoImage, { lazyload: true, src: url || src, fit: 'cover', onMouseDown: (e) => e.preventDefault() }));
4980
5393
  break;
4981
5394
  case TFileType.VIDEO:
4982
5395
  node = jsx("video", { src: url || src, preload: 'none' });
@@ -5241,4 +5654,4 @@ const useTheme = (props) => {
5241
5654
  };
5242
5655
  };
5243
5656
 
5244
- export { Affix, Badge, Button, Card, Checkbox, Collapse, ColorPicker, Datagrid, Datepicker as DatePicker, Description, Drawer, Dropdown, Editor, Flex, Form, Icon, Image, Input, List$1 as List, Loading, message as Message, Modal, Pagination, Popconfirm, Popup, Progress, Radio, Resizable, River, Select, Step, Swiper, Tabs, Tag, Text, TimePicker, Tree, Upload, Video, usePreview, useTheme };
5657
+ export { Affix, Badge, Button, Card, Checkbox, Collapse, ColorPicker, Datagrid, Datepicker as DatePicker, Description, Drawer, Dropdown, Editor, Flex, Form, Icon, MemoImage as Image, Input, List$1 as List, Loading, message as Message, Modal, Pagination, Popconfirm, Popup, Progress, Radio, Resizable, River, Select, Step, Swiper, Tabs, Tag, Text, TimePicker, Tree, Upload, Video, usePreview, useTheme };