@ioca/react 1.5.3 → 1.5.4

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 +26 -39
  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 +167 -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 +27 -36
  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 +156 -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 +598 -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
- import xss from 'xss';
8
+ import xss, { escapeAttrValue } 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,454 @@ 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
  };
1832
1852
  const xssOptions = {
1833
- onIgnoreTagAttr: function (tag, name, value) {
1834
- if (["data-", "style"].includes(name.substr(0, 5))) {
1853
+ onIgnoreTagAttr(tag, name, value) {
1854
+ if (["class", "contenteditable"].includes(name)) {
1855
+ return name + '="' + escapeAttrValue(value) + '"';
1856
+ }
1857
+ if (["data-", "style"].includes(name.substring(0, 5))) {
1835
1858
  return name + '="' + escapeAttrValue(value) + '"';
1836
1859
  }
1837
1860
  },
1838
1861
  };
1862
+ const handleMouseDown = (e) => {
1863
+ e.preventDefault();
1864
+ };
1839
1865
  const fnMap = {
1840
1866
  bold: {
1841
1867
  icon: jsx(FormatBoldRound, {}),
1842
1868
  onClick: () => exec("bold"),
1843
- tip: "粗体",
1844
1869
  },
1845
1870
  italic: {
1846
1871
  icon: jsx(FormatItalicRound, {}),
1847
1872
  onClick: () => exec("italic"),
1848
- tip: "斜体",
1849
1873
  },
1850
1874
  underline: {
1851
1875
  icon: jsx(FormatUnderlinedRound, {}),
1852
1876
  onClick: () => exec("underline"),
1853
- tip: "下划线",
1854
1877
  },
1855
1878
  strike: {
1856
1879
  icon: jsx(StrikethroughSRound, {}),
1857
1880
  onClick: () => exec("strikeThrough"),
1858
- tip: "删除线",
1859
1881
  },
1860
1882
  redo: {
1861
1883
  icon: jsx(RedoRound, {}),
1862
1884
  onClick: () => exec("redo"),
1863
- tip: "重做",
1864
1885
  },
1865
1886
  undo: {
1866
1887
  icon: jsx(UndoRound, {}),
1867
1888
  onClick: () => exec("undo"),
1868
- tip: "撤销",
1869
1889
  },
1870
- // color: {
1871
- // icon: <FormatColorTextRound />,
1872
- // onClick: () => exec("foreColor", false, ""),
1873
- // },
1874
- // backColor: {
1875
- // icon: <FormatColorFillRound />,
1876
- // onClick: () => exec("backColor", false, ""),
1877
- // },
1878
1890
  clear: {
1879
1891
  icon: jsx(ClearAllRound, {}),
1880
1892
  onClick: () => exec("removeFormat"),
1881
- tip: "清除格式",
1882
1893
  },
1883
1894
  };
1884
- const aliasMap = {
1885
- simple: ["undo", "redo", "bold", "italic", "underline", "strike", "clear"],
1886
- all: Object.keys(fnMap),
1895
+ const defaultKeys = [
1896
+ "undo",
1897
+ "redo",
1898
+ "bold",
1899
+ "italic",
1900
+ "underline",
1901
+ "strike",
1902
+ "clear",
1903
+ ];
1904
+ const typedFnMap = fnMap;
1905
+ function getControls(options) {
1906
+ const { controlBtnProps, addtionControls, getSelection } = options;
1907
+ const controls = defaultKeys.map((k) => {
1908
+ const { icon, onClick } = typedFnMap[k];
1909
+ return (jsx(Button, { ...controlBtnProps, onMouseDown: handleMouseDown, onClick: onClick, children: jsx(Icon, { icon: icon }) }, k));
1910
+ });
1911
+ const extControls = (addtionControls ?? []).map((item, index) => (jsx(Button, { ...controlBtnProps, onMouseDown: handleMouseDown, onClick: (e) => item.onClick?.(getSelection(), e), children: item.icon }, `addtion-${index}`)));
1912
+ return [...controls, ...extControls];
1913
+ }
1914
+
1915
+ const MEMTION_TAG_CLASS_NAME = "i-memtion-tag";
1916
+ const getInsertNode = (memtion, option) => memtion?.insert?.(option) ?? option.value;
1917
+ const getInsertText = (memtion, option) => {
1918
+ const nextNode = getInsertNode(memtion, option);
1919
+ if (typeof nextNode === "string" || typeof nextNode === "number") {
1920
+ return `${String(nextNode)} `;
1921
+ }
1922
+ return `${String(option.value)} `;
1923
+ };
1924
+ const getInsertHtml = (memtion, option, sanitizeValue) => {
1925
+ const nextNode = getInsertNode(memtion, option);
1926
+ if (nextNode === null || nextNode === undefined || nextNode === false) {
1927
+ return "";
1928
+ }
1929
+ const content = sanitizeValue(renderToStaticMarkup(jsx(Fragment, { children: nextNode })));
1930
+ return `<span class="${MEMTION_TAG_CLASS_NAME}" contenteditable="false" data-memtion-value="${escapeAttrValue(String(option.value))}">${content}</span>`;
1931
+ };
1932
+ const getSelectionRect = (range) => {
1933
+ if (!range)
1934
+ return null;
1935
+ const rect = range.getBoundingClientRect();
1936
+ if (rect.width || rect.height) {
1937
+ return rect;
1938
+ }
1939
+ return range.getClientRects()[0] ?? rect;
1940
+ };
1941
+ const insertMemtionOption = ({ editor, range, mode, memtion, option, sanitizeValue, }) => {
1942
+ if (!editor || !range)
1943
+ return null;
1944
+ const browserSelection = window.getSelection();
1945
+ if (!browserSelection)
1946
+ return null;
1947
+ const nextRange = range.cloneRange();
1948
+ browserSelection.removeAllRanges();
1949
+ browserSelection.addRange(nextRange);
1950
+ editor.focus();
1951
+ nextRange.deleteContents();
1952
+ if (mode === "plaintext") {
1953
+ const text = document.createTextNode(getInsertText(memtion, option));
1954
+ nextRange.insertNode(text);
1955
+ nextRange.setStartAfter(text);
1956
+ }
1957
+ else {
1958
+ const html = getInsertHtml(memtion, option, sanitizeValue);
1959
+ const fragment = nextRange.createContextualFragment(html);
1960
+ const lastNode = fragment.lastChild;
1961
+ const spacing = document.createTextNode(" ");
1962
+ nextRange.insertNode(fragment);
1963
+ if (lastNode) {
1964
+ nextRange.setStartAfter(lastNode);
1965
+ nextRange.collapse(true);
1966
+ }
1967
+ nextRange.insertNode(spacing);
1968
+ nextRange.setStartAfter(spacing);
1969
+ }
1970
+ nextRange.collapse(true);
1971
+ browserSelection.removeAllRanges();
1972
+ browserSelection.addRange(nextRange);
1973
+ return nextRange;
1887
1974
  };
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);
1975
+ const getMemtionText = (triggerRange, selectionRange) => {
1976
+ if (!triggerRange || !selectionRange)
1977
+ return "";
1978
+ const range = triggerRange.cloneRange();
1979
+ range.setEnd(selectionRange.endContainer, selectionRange.endOffset);
1980
+ return range.toString();
1981
+ };
1982
+ const getMemtionReplaceRange = (triggerRange, selectionRange) => {
1983
+ if (!triggerRange || !selectionRange)
1984
+ return null;
1985
+ const range = triggerRange.cloneRange();
1986
+ range.setEnd(selectionRange.endContainer, selectionRange.endOffset);
1987
+ return range;
1988
+ };
1989
+ const filterMemtionOptions = (options, keyword) => {
1990
+ const normalizedKeyword = keyword.trim().toLowerCase();
1991
+ if (!normalizedKeyword) {
1992
+ return options;
1993
+ }
1994
+ return options.filter((option) => String(option.value).toLowerCase().includes(normalizedKeyword));
1995
+ };
1996
+ const isMemtionTag = (node) => node instanceof HTMLElement &&
1997
+ node.classList.contains(MEMTION_TAG_CLASS_NAME);
1998
+ const getAdjacentMemtionTag = (range, direction) => {
1999
+ const { startContainer, startOffset } = range;
2000
+ if (startContainer.nodeType === Node.TEXT_NODE) {
2001
+ const textNode = startContainer;
2002
+ if (direction === "backward" && startOffset === 0) {
2003
+ return isMemtionTag(textNode.previousSibling)
2004
+ ? textNode.previousSibling
2005
+ : null;
2006
+ }
2007
+ if (direction === "forward" && startOffset === textNode.data.length) {
2008
+ return isMemtionTag(textNode.nextSibling)
2009
+ ? textNode.nextSibling
2010
+ : null;
2011
+ }
2012
+ return null;
2013
+ }
2014
+ const element = startContainer;
2015
+ const targetIndex = direction === "backward" ? startOffset - 1 : startOffset;
2016
+ const targetNode = element.childNodes.item(targetIndex);
2017
+ return isMemtionTag(targetNode) ? targetNode : null;
2018
+ };
2019
+ const removeAdjacentMemtionTag = (editor, key) => {
2020
+ if (!editor)
2021
+ return false;
2022
+ const selection = window.getSelection();
2023
+ if (!selection?.rangeCount || !selection.isCollapsed)
2024
+ return false;
2025
+ const activeRange = selection.getRangeAt(0);
2026
+ const tag = getAdjacentMemtionTag(activeRange, key === "Backspace" ? "backward" : "forward");
2027
+ if (!tag || !tag.parentNode)
2028
+ return false;
2029
+ const parent = tag.parentNode;
2030
+ const nextSibling = tag.nextSibling;
2031
+ const index = Array.prototype.indexOf.call(parent.childNodes, tag);
2032
+ if (nextSibling?.nodeType === Node.TEXT_NODE) {
2033
+ const textNode = nextSibling;
2034
+ if (textNode.data.startsWith(" ")) {
2035
+ textNode.deleteData(0, 1);
2036
+ if (!textNode.data.length) {
2037
+ nextSibling.remove();
1896
2038
  }
1897
- return (jsxs(Button, { ...controlBtnProps, onClick: onClick, children: [jsx(Icon, { icon: icon }), tip && jsx("span", { className: 'i-editor-control-tip', children: tip })] }, k));
1898
2039
  }
1899
- return jsx(Fragment, {}, k);
1900
- });
1901
- }
2040
+ }
2041
+ tag.remove();
2042
+ const range = document.createRange();
2043
+ range.setStart(parent, Math.min(index, parent.childNodes.length));
2044
+ range.collapse(true);
2045
+ selection.removeAllRanges();
2046
+ selection.addRange(range);
2047
+ editor.focus();
2048
+ return true;
2049
+ };
2050
+ const Memtion = (props) => {
2051
+ const { visible, rect, options, activeIndex, onActiveChange, onSelect } = props;
2052
+ if (!visible || !rect || !options?.length) {
2053
+ return null;
2054
+ }
2055
+ return (jsx(List$1, { className: "i-editor-memtion", type: "option", style: {
2056
+ position: "fixed",
2057
+ top: rect.bottom,
2058
+ left: rect.left,
2059
+ }, 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}`))) }));
2060
+ };
2061
+ var Memtion$1 = memo(Memtion);
1902
2062
 
2063
+ const controlBtnProps = {
2064
+ square: true,
2065
+ flat: true,
2066
+ size: "small",
2067
+ };
1903
2068
  const Editor = (props) => {
1904
- const { ref, width, height = "10em", placeholder, autosize, border = true, richPaste, controls = "simple", className, style, onInput, onPaste, onKeyDown, ...restProps } = props;
2069
+ 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
2070
  const editorRef = useRef(null);
1906
- const controlBtnProps = {
1907
- square: true,
1908
- flat: true,
1909
- size: "small",
2071
+ const selectionRef = useRef(null);
2072
+ const memtionTriggerRangeRef = useRef(null);
2073
+ const pendingMemtionRef = useRef(false);
2074
+ const [memtionVisible, setMemtionVisible] = useState(false);
2075
+ const [memtionRect, setMemtionRect] = useState(null);
2076
+ const [memtionKeyword, setMemtionKeyword] = useState("");
2077
+ const [memtionActiveIndex, setMemtionActiveIndex] = useState(0);
2078
+ const memtionOptions = useMemo(() => filterMemtionOptions(memtion?.options ?? [], memtionKeyword), [memtion?.options, memtionKeyword]);
2079
+ const sanitizeValue = (nextValue) => {
2080
+ if (mode === "plaintext") {
2081
+ return nextValue === "\n" ? "" : nextValue;
2082
+ }
2083
+ const safeHtml = xss(nextValue, xssOptions);
2084
+ return safeHtml === "<br>" ? "" : safeHtml;
2085
+ };
2086
+ const rememberSelection = () => {
2087
+ if (!editorRef.current)
2088
+ return;
2089
+ const selection = window.getSelection();
2090
+ if (!selection?.rangeCount)
2091
+ return;
2092
+ const range = selection.getRangeAt(0);
2093
+ const container = range.commonAncestorContainer;
2094
+ const parent = container.nodeType === Node.ELEMENT_NODE
2095
+ ? container
2096
+ : container.parentElement;
2097
+ if (!parent || !editorRef.current.contains(parent))
2098
+ return;
2099
+ selectionRef.current = range.cloneRange();
2100
+ };
2101
+ const setEditorValue = (nextValue) => {
2102
+ if (!editorRef.current)
2103
+ return;
2104
+ const safeValue = sanitizeValue(nextValue);
2105
+ if (mode === "plaintext") {
2106
+ editorRef.current.textContent = safeValue;
2107
+ return;
2108
+ }
2109
+ editorRef.current.innerHTML = safeValue;
1910
2110
  };
1911
- const handlePaste = async (e) => {
2111
+ const getEditorValue = (sanitize = false) => {
2112
+ if (!editorRef.current)
2113
+ return "";
2114
+ const nextValue = mode === "plaintext"
2115
+ ? (editorRef.current.textContent ?? "")
2116
+ : editorRef.current.innerHTML;
2117
+ return sanitize ? sanitizeValue(nextValue) : nextValue;
2118
+ };
2119
+ const hideMemtion = () => {
2120
+ pendingMemtionRef.current = false;
2121
+ memtionTriggerRangeRef.current = null;
2122
+ setMemtionVisible(false);
2123
+ setMemtionRect(null);
2124
+ setMemtionKeyword("");
2125
+ setMemtionActiveIndex(0);
2126
+ };
2127
+ const insertMemtion = (option) => {
2128
+ const replaceRange = getMemtionReplaceRange(memtionTriggerRangeRef.current, selectionRef.current);
2129
+ const range = insertMemtionOption({
2130
+ editor: editorRef.current,
2131
+ range: replaceRange,
2132
+ mode,
2133
+ memtion,
2134
+ option,
2135
+ sanitizeValue,
2136
+ });
2137
+ if (!range || !editorRef.current)
2138
+ return;
2139
+ selectionRef.current = range.cloneRange();
2140
+ hideMemtion();
2141
+ editorRef.current.dispatchEvent(new Event("input", { bubbles: true }));
2142
+ };
2143
+ const handlePaste = (e) => {
1912
2144
  onPaste?.(e);
1913
- if (richPaste)
2145
+ if (e.defaultPrevented)
1914
2146
  return;
1915
2147
  e.preventDefault();
2148
+ if (mode === "plaintext") {
2149
+ const text = e.clipboardData.getData("text/plain");
2150
+ exec("insertText", false, text);
2151
+ return;
2152
+ }
2153
+ const html = e.clipboardData.getData("text/html");
2154
+ if (html) {
2155
+ exec("insertHTML", false, sanitizeValue(html));
2156
+ return;
2157
+ }
1916
2158
  const text = e.clipboardData.getData("text/plain");
1917
2159
  exec("insertText", false, text);
1918
2160
  };
1919
2161
  const handleKeyDown = (e) => {
1920
2162
  onKeyDown?.(e);
2163
+ if (mode === "rich" &&
2164
+ (e.key === "Backspace" || e.key === "Delete") &&
2165
+ removeAdjacentMemtionTag(editorRef.current, e.key)) {
2166
+ e.preventDefault();
2167
+ rememberSelection();
2168
+ editorRef.current?.dispatchEvent(new Event("input", { bubbles: true }));
2169
+ return;
2170
+ }
2171
+ const memtionKey = memtion?.key ?? "@";
2172
+ if (memtionVisible && e.key === " ") {
2173
+ hideMemtion();
2174
+ }
2175
+ if (memtionVisible && memtionOptions.length) {
2176
+ switch (e.key) {
2177
+ case "ArrowDown":
2178
+ e.preventDefault();
2179
+ setMemtionActiveIndex((index) => index + 1 >= memtionOptions.length ? 0 : index + 1);
2180
+ return;
2181
+ case "ArrowUp":
2182
+ e.preventDefault();
2183
+ setMemtionActiveIndex((index) => index - 1 < 0 ? memtionOptions.length - 1 : index - 1);
2184
+ return;
2185
+ case "Enter":
2186
+ e.preventDefault();
2187
+ insertMemtion(memtionOptions[memtionActiveIndex]);
2188
+ return;
2189
+ }
2190
+ }
2191
+ if (memtion && e.key === memtionKey) {
2192
+ rememberSelection();
2193
+ memtionTriggerRangeRef.current =
2194
+ selectionRef.current?.cloneRange() ?? null;
2195
+ pendingMemtionRef.current = true;
2196
+ }
1921
2197
  switch (e.key) {
1922
2198
  case "Tab":
1923
2199
  e.preventDefault();
1924
- exec("insertHTML", false, "&#09;");
2200
+ exec(mode === "plaintext" ? "insertText" : "insertHTML", false, mode === "plaintext" ? "\t" : "&#09;");
1925
2201
  break;
1926
2202
  case "Enter":
2203
+ if (!onEnter)
2204
+ break;
1927
2205
  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`;
2206
+ onEnter(e);
1938
2207
  break;
1939
2208
  }
1940
2209
  };
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
- });
2210
+ useEffect(() => {
2211
+ if (!editorRef.current)
2212
+ return;
2213
+ const nextValue = sanitizeValue(value);
2214
+ if (getEditorValue(true) === nextValue)
2215
+ return;
2216
+ setEditorValue(nextValue);
2217
+ if (autosize) {
2218
+ editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
2219
+ }
2220
+ }, [autosize, mode, value]);
2221
+ useEffect(() => {
2222
+ if (!memtionOptions.length) {
2223
+ setMemtionActiveIndex(0);
2224
+ return;
2225
+ }
2226
+ setMemtionActiveIndex((index) => index >= memtionOptions.length ? 0 : index);
2227
+ }, [memtionOptions]);
1956
2228
  const handleInput = (e) => {
1957
- let html = editorRef.current?.innerHTML ?? "";
1958
- if (["<br>", "\n"].includes(html) && editorRef.current) {
1959
- html = "";
1960
- editorRef.current.innerHTML = html;
2229
+ const rawValue = getEditorValue();
2230
+ let nextValue = sanitizeValue(rawValue);
2231
+ if (!nextValue && rawValue && editorRef.current) {
2232
+ nextValue = "";
2233
+ setEditorValue(nextValue);
2234
+ }
2235
+ rememberSelection();
2236
+ if (memtion && (pendingMemtionRef.current || memtionVisible)) {
2237
+ const memtionKey = memtion?.key ?? "@";
2238
+ const memtionText = getMemtionText(memtionTriggerRangeRef.current, selectionRef.current);
2239
+ if (!memtionText.startsWith(memtionKey) || /\s/.test(memtionText)) {
2240
+ hideMemtion();
2241
+ }
2242
+ else {
2243
+ const keyword = memtionText.slice(memtionKey.length);
2244
+ pendingMemtionRef.current = false;
2245
+ setMemtionRect(getSelectionRect(selectionRef.current));
2246
+ setMemtionKeyword(keyword);
2247
+ setMemtionActiveIndex(0);
2248
+ setMemtionVisible(true);
2249
+ }
2250
+ }
2251
+ if (autosize && editorRef.current) {
2252
+ editorRef.current.style.height = `${editorRef.current.scrollHeight}px`;
1961
2253
  }
1962
- onInput?.(html, e);
2254
+ onChange?.(nextValue, e);
2255
+ };
2256
+ const handleFocus = (e) => {
2257
+ rememberSelection();
2258
+ onFocus?.(e);
2259
+ };
2260
+ const handleBlur = (e) => {
2261
+ hideMemtion();
2262
+ onBlur?.(e);
1963
2263
  };
2264
+ const handleMouseUp = (e) => {
2265
+ rememberSelection();
2266
+ onMouseUp?.(e);
2267
+ };
2268
+ const handleKeyUp = (e) => {
2269
+ rememberSelection();
2270
+ onKeyUp?.(e);
2271
+ };
2272
+ const handleRef = (node) => {
2273
+ editorRef.current = node;
2274
+ if (typeof ref === "function") {
2275
+ ref(node);
2276
+ return;
2277
+ }
2278
+ if (ref) {
2279
+ ref.current = node;
2280
+ }
2281
+ };
2282
+ const getSelection = useCallback(() => selectionRef.current?.cloneRange() ?? null, []);
2283
+ const controls = useMemo(() => getControls({
2284
+ controlBtnProps,
2285
+ addtionControls,
2286
+ getSelection,
2287
+ }), [addtionControls, getSelection]);
1964
2288
  return (jsxs("div", { className: classNames("i-editor", className, {
1965
2289
  "i-editor-borderless": !border,
1966
2290
  }), style: {
1967
2291
  ...style,
1968
2292
  [autosize ? "minHeight" : "height"]: height,
1969
2293
  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 })] }));
2294
+ }, 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
2295
  };
1974
2296
 
1975
2297
  const Flex = (props) => {
@@ -2405,7 +2727,7 @@ const HookModal = (props) => {
2405
2727
  },
2406
2728
  close: () => {
2407
2729
  state.visible = false;
2408
- if (mergedProps.closable)
2730
+ if (mergedProps.closable ?? true)
2409
2731
  return;
2410
2732
  Promise.resolve().then(() => {
2411
2733
  state.visible = true;
@@ -2952,27 +3274,34 @@ function List(props) {
2952
3274
  if (!files.length)
2953
3275
  return "";
2954
3276
  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));
3277
+ return (jsx(MemoImage, { src: img.src, usePreview: false, onClick: (e) => handleClick(e, i), ...restProps }, i));
2956
3278
  }) }));
2957
3279
  }
2958
3280
 
3281
+ const STATUS_LOADING = "loading";
3282
+ const STATUS_ERROR = "error";
2959
3283
  const Image = (props) => {
2960
3284
  const { src, thumb, round, size, height, width, ratio, initSize, lazyload, fallback, fit, style, className, cover, coverClass, usePreview: previewable, onLoad, onError, onClick, ...restProps } = props;
2961
3285
  const state = useReactive({
2962
- status: "loading",
3286
+ status: STATUS_LOADING,
2963
3287
  });
2964
3288
  const ref = useRef(null);
2965
3289
  const { observe, unobserve } = useIntersectionObserver();
2966
3290
  const preview = usePreview();
2967
- const handleError = (err) => {
3291
+ const setStatus = useCallback((status) => {
3292
+ if (state.status === status)
3293
+ return;
3294
+ state.status = status;
3295
+ }, [state]);
3296
+ const handleError = useCallback((err) => {
2968
3297
  onError?.(err);
2969
- state.status = "error";
2970
- };
2971
- const handleLoad = (e) => {
3298
+ setStatus(STATUS_ERROR);
3299
+ }, [onError, setStatus]);
3300
+ const handleLoad = useCallback((e) => {
2972
3301
  onLoad?.(e);
2973
- state.status = undefined;
2974
- };
2975
- const handleClick = (e) => {
3302
+ setStatus(undefined);
3303
+ }, [onLoad, setStatus]);
3304
+ const handleClick = useCallback((e) => {
2976
3305
  onClick?.(e);
2977
3306
  if (!previewable || !src)
2978
3307
  return;
@@ -2986,44 +3315,53 @@ const Image = (props) => {
2986
3315
  },
2987
3316
  ],
2988
3317
  });
2989
- };
3318
+ }, [onClick, preview, previewable, src]);
2990
3319
  useEffect(() => {
2991
3320
  if (!src || typeof window === "undefined")
2992
3321
  return;
2993
3322
  const img = ref.current;
3323
+ if (!img)
3324
+ return;
2994
3325
  const hasSrcAttr = img?.getAttribute("src");
2995
3326
  const canSyncStatus = Boolean(img && (!lazyload || hasSrcAttr));
2996
- if (canSyncStatus && img?.complete) {
2997
- state.status = img.naturalWidth > 0 ? undefined : "error";
3327
+ if (canSyncStatus && img.complete) {
3328
+ setStatus(img.naturalWidth > 0 ? undefined : STATUS_ERROR);
2998
3329
  }
2999
- if (!ref.current?.complete && observe && lazyload) {
3000
- state.status = "loading";
3330
+ if (!img.complete && observe && lazyload) {
3331
+ setStatus(STATUS_LOADING);
3001
3332
  }
3002
- if (!lazyload || !ref.current || !observe)
3333
+ if (!lazyload || !observe)
3003
3334
  return;
3004
- observe(ref.current, (tar, visible) => {
3335
+ observe(img, (tar, visible) => {
3005
3336
  if (!visible)
3006
3337
  return;
3007
3338
  tar.setAttribute("src", tar.dataset.src || "");
3008
3339
  unobserve(tar);
3009
3340
  });
3010
3341
  return () => {
3011
- ref.current && unobserve(ref.current);
3342
+ unobserve(img);
3012
3343
  };
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 }))] })) }));
3344
+ }, [lazyload, observe, setStatus, src, unobserve]);
3345
+ const imageStatus = state.status;
3346
+ const iSize = imageStatus === STATUS_LOADING ? initSize : undefined;
3347
+ const wrapperStyle = useMemo(() => ({
3348
+ width: width ?? size ?? iSize,
3349
+ height: height ?? size ?? iSize,
3350
+ aspectRatio: ratio,
3351
+ ...style,
3352
+ }), [height, iSize, ratio, size, style, width]);
3353
+ const wrapperClassName = useMemo(() => classNames("i-image", className, {
3354
+ rounded: round,
3355
+ [`i-image-${imageStatus}`]: imageStatus,
3356
+ }), [className, imageStatus, round]);
3357
+ const imageStyle = useMemo(() => ({ objectFit: fit }), [fit]);
3358
+ const imageSrcProps = lazyload
3359
+ ? { "data-src": thumb ?? src }
3360
+ : { src: thumb ?? src };
3361
+ 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
3362
  };
3026
- Image.List = List;
3363
+ const MemoImage = memo(Image);
3364
+ MemoImage.List = List;
3027
3365
 
3028
3366
  function InputContainer(props) {
3029
3367
  const { as: As = "label", label, className, labelInline, style, children, status, tip, required, } = props;
@@ -3199,16 +3537,19 @@ const Range = (props) => {
3199
3537
  };
3200
3538
 
3201
3539
  const Textarea = (props) => {
3202
- const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border, width, style, onChange, onEnter, ...restProps } = props;
3540
+ const { ref, label, name, value = "", labelInline, className, status = "normal", message, tip, autoSize, border, width, style, resize, onChange, onEnter, ...restProps } = props;
3203
3541
  const [textareaValue, setTextareaValue] = useState(value);
3204
3542
  const refTextarea = useRef(null);
3543
+ const syncTextareaHeight = () => {
3544
+ const ta = refTextarea.current;
3545
+ if (!autoSize || !ta)
3546
+ return;
3547
+ ta.style.height = "auto";
3548
+ ta.style.height = `${ta.scrollHeight}px`;
3549
+ };
3205
3550
  const handleChange = (e) => {
3206
3551
  const v = e.target.value;
3207
3552
  setTextareaValue(v);
3208
- const ta = refTextarea.current;
3209
- if (autoSize && ta) {
3210
- ta.style.height = `${ta.scrollHeight}px`;
3211
- }
3212
3553
  onChange?.(v, e);
3213
3554
  };
3214
3555
  const handleKeydown = (e) => {
@@ -3220,6 +3561,9 @@ const Textarea = (props) => {
3220
3561
  useEffect(() => {
3221
3562
  setTextareaValue(value);
3222
3563
  }, [value]);
3564
+ useEffect(() => {
3565
+ syncTextareaHeight();
3566
+ }, [autoSize, textareaValue]);
3223
3567
  useImperativeHandle(ref, () => {
3224
3568
  return {
3225
3569
  input: refTextarea.current,
@@ -3230,6 +3574,7 @@ const Textarea = (props) => {
3230
3574
  name,
3231
3575
  value: textareaValue,
3232
3576
  className: "i-input i-textarea",
3577
+ style: resize === false ? { resize: "none" } : undefined,
3233
3578
  onChange: handleChange,
3234
3579
  onKeyDown: handleKeydown,
3235
3580
  ...restProps,
@@ -3687,7 +4032,7 @@ function Footer(props) {
3687
4032
  setInputValue(value);
3688
4033
  setColorType(type);
3689
4034
  }, [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, {}) }) })] }));
4035
+ 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
4036
  }
3692
4037
 
3693
4038
  const Handle = (props) => {
@@ -4029,10 +4374,10 @@ const defaultOk = {
4029
4374
  };
4030
4375
  const defaultCancel = {
4031
4376
  children: "取消",
4032
- secondary: true,
4377
+ flat: true,
4033
4378
  };
4034
4379
  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;
4380
+ 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
4381
  const state = useReactive({
4037
4382
  loading: false,
4038
4383
  visible,
@@ -4063,7 +4408,7 @@ const Popconfirm = (props) => {
4063
4408
  await onClose?.();
4064
4409
  state.visible = false;
4065
4410
  };
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 }))] })] }));
4411
+ 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
4412
  return (jsx(Popup, { content: popconfirmContent, ...restProps, trigger: trigger, visible: state.visible, align: align, offset: offset, position: position, onVisibleChange: handleVisibleChange, children: children }));
4068
4413
  };
4069
4414
 
@@ -4549,7 +4894,7 @@ const Item = (props) => {
4549
4894
  };
4550
4895
 
4551
4896
  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;
4897
+ 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
4898
  const navRefs = useRef([]);
4554
4899
  const barRef = useRef(null);
4555
4900
  const navsRef = useRef(null);
@@ -4559,10 +4904,15 @@ const Tabs = ((props) => {
4559
4904
  const [barStyle, setBarStyle] = useState({});
4560
4905
  const [cachedTabs, setCachedTabs] = useState([]);
4561
4906
  const [overflow, setOverflow] = useState(false);
4562
- const [moreTabs, setMoreTabs] = useState([]);
4563
4907
  const [tabs, setTabs] = useState([]);
4564
4908
  const { observe, unobserve } = useIntersectionObserver();
4565
4909
  const size = useSize(navsRef);
4910
+ const tabsRef = useRef(tabs);
4911
+ tabsRef.current = tabs;
4912
+ const activeKeyRef = useRef(activeKey);
4913
+ activeKeyRef.current = activeKey;
4914
+ const prevActiveKeyRef = useRef(prevActiveKey);
4915
+ prevActiveKeyRef.current = prevActiveKey;
4566
4916
  useEffect(() => {
4567
4917
  contentsRef.current.clear();
4568
4918
  if (!items) {
@@ -4599,10 +4949,11 @@ const Tabs = ((props) => {
4599
4949
  }));
4600
4950
  }, [children, items]);
4601
4951
  const add = (tab) => {
4602
- const tkey = String(tab.key ?? tabs.length);
4603
- const i = tabs.findIndex((t) => t.key === tkey);
4952
+ const currentTabs = tabsRef.current;
4953
+ const tkey = String(tab.key ?? currentTabs.length);
4954
+ const i = currentTabs.findIndex((t) => t.key === tkey);
4604
4955
  if (i > -1) {
4605
- open(tabs[i].key ?? `${i}`);
4956
+ open(currentTabs[i].key ?? `${i}`);
4606
4957
  return;
4607
4958
  }
4608
4959
  contentsRef.current.set(tkey, tab.content);
@@ -4611,20 +4962,34 @@ const Tabs = ((props) => {
4611
4962
  open(tkey);
4612
4963
  };
4613
4964
  const close = (key) => {
4614
- const i = tabs.findIndex((t) => t.key === key);
4965
+ const currentTabs = tabsRef.current;
4966
+ const i = currentTabs.findIndex((t) => t.key === key);
4615
4967
  if (i < 0)
4616
4968
  return;
4617
4969
  contentsRef.current.delete(key);
4618
- const nextTabs = [...tabs];
4970
+ const nextTabs = [...currentTabs];
4619
4971
  nextTabs.splice(i, 1);
4620
4972
  setTabs(nextTabs);
4621
- if (activeKey !== key)
4973
+ if (activeKeyRef.current !== key)
4622
4974
  return;
4623
4975
  const next = nextTabs[i] || nextTabs[i - 1];
4624
- open(prevActiveKey ?? next?.key ?? "");
4976
+ const prev = prevActiveKeyRef.current;
4977
+ const nextKey = prev && nextTabs.some((t) => t.key === prev) ? prev : next?.key;
4978
+ open(nextKey ?? "");
4625
4979
  };
4626
4980
  const open = (key) => {
4627
- if (key === activeKey) {
4981
+ const nextKey = key || undefined;
4982
+ if (nextKey === undefined) {
4983
+ onTabChange?.(undefined, activeKey);
4984
+ setPrevActiveKey(activeKey);
4985
+ setActiveKey(undefined);
4986
+ setBarStyle({
4987
+ height: 0,
4988
+ width: 0,
4989
+ });
4990
+ return;
4991
+ }
4992
+ if (nextKey === activeKey) {
4628
4993
  if (!toggable)
4629
4994
  return;
4630
4995
  onTabChange?.(undefined, key);
@@ -4636,38 +5001,75 @@ const Tabs = ((props) => {
4636
5001
  return;
4637
5002
  }
4638
5003
  setPrevActiveKey(activeKey);
4639
- onTabChange?.(key, activeKey);
4640
- setActiveKey(key);
5004
+ onTabChange?.(nextKey, activeKey);
5005
+ setActiveKey(nextKey);
5006
+ };
5007
+ const handleKeyAction = (e, action) => {
5008
+ if (!["Enter", " "].includes(e.key))
5009
+ return;
5010
+ e.preventDefault();
5011
+ action();
5012
+ };
5013
+ const scrollToTab = (key) => {
5014
+ const index = tabsRef.current.findIndex((tab) => tab.key === key);
5015
+ const nav = navRefs.current[index];
5016
+ nav?.scrollIntoView({
5017
+ behavior: "smooth",
5018
+ block: "nearest",
5019
+ inline: "nearest",
5020
+ });
5021
+ };
5022
+ const handleMoreTabClick = (key) => {
5023
+ open(key);
5024
+ scrollToTab(key);
4641
5025
  };
4642
5026
  useEffect(() => {
4643
- if (!size || hideMore || !observe)
5027
+ if (!size || hideMore || !observe || !unobserve)
4644
5028
  return;
4645
5029
  const { scrollHeight, scrollWidth } = navsRef.current;
4646
5030
  const { width, height } = size;
4647
5031
  const nextOverflow = scrollHeight > height || scrollWidth > width;
4648
- setOverflow(nextOverflow);
4649
- if (!nextOverflow)
5032
+ setOverflow((v) => (v === nextOverflow ? v : nextOverflow));
5033
+ if (!nextOverflow) {
5034
+ setTabs((ts) => {
5035
+ let changed = false;
5036
+ const next = ts.map((t) => {
5037
+ if (t.intersecting === undefined ||
5038
+ t.intersecting === true) {
5039
+ return t;
5040
+ }
5041
+ changed = true;
5042
+ return { ...t, intersecting: true };
5043
+ });
5044
+ return changed ? next : ts;
5045
+ });
4650
5046
  return;
5047
+ }
5048
+ const observed = [];
4651
5049
  navRefs.current.map((nav, i) => {
4652
5050
  if (!nav)
4653
5051
  return;
4654
- observe(nav, (tar, visible) => {
5052
+ observed.push(nav);
5053
+ observe(nav, (_tar, visible) => {
4655
5054
  setTabs((ts) => {
4656
5055
  if (!ts[i])
4657
5056
  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;
5057
+ if (ts[i]?.intersecting === visible)
5058
+ return ts;
5059
+ return ts.map((t, idx) => idx === i ? { ...t, intersecting: visible } : t);
4661
5060
  });
4662
5061
  });
4663
5062
  });
4664
- }, [size, hideMore, tabs.length, observe]);
5063
+ return () => {
5064
+ observed.map((el) => unobserve(el));
5065
+ };
5066
+ }, [size, hideMore, tabs.length, observe, unobserve]);
4665
5067
  useEffect(() => {
4666
5068
  if (!bar || type === "pane" || activeKey === undefined) {
4667
5069
  return;
4668
5070
  }
4669
5071
  const index = tabs.findIndex((tab) => tab.key === activeKey);
4670
- setTimeout(() => {
5072
+ const timer = window.setTimeout(() => {
4671
5073
  const nav = navRefs.current[index];
4672
5074
  if (!nav)
4673
5075
  return;
@@ -4686,19 +5088,15 @@ const Tabs = ((props) => {
4686
5088
  transform: `translate(${offsetLeft}px, ${offsetTop}px)`,
4687
5089
  });
4688
5090
  }, 16);
5091
+ return () => {
5092
+ window.clearTimeout(timer);
5093
+ };
4689
5094
  }, [activeKey, bar, size, tabs, type, vertical]);
4690
5095
  useEffect(() => {
4691
5096
  if (active === undefined || activeKey === active)
4692
5097
  return;
4693
5098
  open(active);
4694
5099
  }, [active]);
4695
- useEffect(() => {
4696
- if (hideMore || !unobserve)
4697
- return;
4698
- return () => {
4699
- navRefs.current?.map(unobserve);
4700
- };
4701
- }, [tabs.length, hideMore, unobserve]);
4702
5100
  useEffect(() => {
4703
5101
  if (!navsRef.current || vertical)
4704
5102
  return;
@@ -4719,30 +5117,35 @@ const Tabs = ((props) => {
4719
5117
  return;
4720
5118
  navsRef.current.removeEventListener("wheel", handleMouseWheel);
4721
5119
  };
4722
- }, [navsRef.current]);
5120
+ }, [vertical]);
4723
5121
  useImperativeHandle(ref, () => ({
4724
5122
  open,
4725
5123
  close,
4726
5124
  add,
4727
5125
  navs: navsRef,
4728
5126
  }));
5127
+ const moreTabs = !hideMore && overflow
5128
+ ? tabs.filter((tab) => tab.intersecting === false)
5129
+ : [];
4729
5130
  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
5131
  "i-tab-navs-vertical": vertical,
4731
- }), children: [prepend, jsxs("div", { ref: navsRef, className: classNames("i-tab-navs", `justify-${navsJustify}`), children: [tabs.map((tab, i) => {
5132
+ }), 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
5133
  const { title, key = `${i}`, closable } = tab;
5134
+ const isActive = activeKey === key;
4733
5135
  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) => {
5136
+ "i-tab-active": isActive,
5137
+ }), 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) => {
5138
+ e.preventDefault();
4736
5139
  e.stopPropagation();
4737
5140
  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) => {
5141
+ }, onKeyDown: (e) => handleKeyAction(e, () => close(key)) }))] }, key));
5142
+ }), 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
5143
  const { key = `${i}`, title } = tab;
4741
5144
  const isActive = activeKey === key;
4742
5145
  return (jsx("a", { className: classNames("i-tab-nav", {
4743
5146
  "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) => {
5147
+ }), role: "button", tabIndex: 0, onClick: () => handleMoreTabClick(key), onKeyDown: (e) => handleKeyAction(e, () => handleMoreTabClick(key)), children: title }, key));
5148
+ }) }), children: renderMore(moreTabs) })), append] }), jsx("div", { className: "i-tab-contents", children: tabs.map((tab, i) => {
4746
5149
  const key = tab.key ?? `${i}`;
4747
5150
  const content = contentsRef.current.get(key);
4748
5151
  const isActive = activeKey === key;
@@ -4750,7 +5153,7 @@ const Tabs = ((props) => {
4750
5153
  (key !== undefined && cachedTabs.includes(key));
4751
5154
  return (show && (jsx("div", { className: classNames("i-tab-content", {
4752
5155
  "i-tab-active": isActive,
4753
- }), children: content }, key)));
5156
+ }), role: "tabpanel", "aria-hidden": !isActive, children: content }, key)));
4754
5157
  }) })] }));
4755
5158
  });
4756
5159
  Tabs.Item = Item;
@@ -4976,7 +5379,7 @@ const FileListItem = (props) => {
4976
5379
  let node = jsx(Fragment, {});
4977
5380
  switch (type) {
4978
5381
  case TFileType.IMAGE:
4979
- node = (jsx(Image, { lazyload: true, src: url || src, fit: 'cover', onMouseDown: (e) => e.preventDefault() }));
5382
+ node = (jsx(MemoImage, { lazyload: true, src: url || src, fit: 'cover', onMouseDown: (e) => e.preventDefault() }));
4980
5383
  break;
4981
5384
  case TFileType.VIDEO:
4982
5385
  node = jsx("video", { src: url || src, preload: 'none' });
@@ -5241,4 +5644,4 @@ const useTheme = (props) => {
5241
5644
  };
5242
5645
  };
5243
5646
 
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 };
5647
+ 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 };