index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. "use client";
  2. import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
  3. var __rest = this && this.__rest || function (s, e) {
  4. var t = {};
  5. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
  6. if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  7. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
  8. }
  9. return t;
  10. };
  11. import * as React from 'react';
  12. import EditOutlined from "@ant-design/icons/es/icons/EditOutlined";
  13. import classNames from 'classnames';
  14. import ResizeObserver from 'rc-resize-observer';
  15. import toArray from "rc-util/es/Children/toArray";
  16. import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
  17. import useMergedState from "rc-util/es/hooks/useMergedState";
  18. import omit from "rc-util/es/omit";
  19. import { composeRef } from "rc-util/es/ref";
  20. import { isStyleSupport } from '../../_util/styleChecker';
  21. import { ConfigContext } from '../../config-provider';
  22. import useLocale from '../../locale/useLocale';
  23. import Tooltip from '../../tooltip';
  24. import Editable from '../Editable';
  25. import useCopyClick from '../hooks/useCopyClick';
  26. import useMergedConfig from '../hooks/useMergedConfig';
  27. import usePrevious from '../hooks/usePrevious';
  28. import useTooltipProps from '../hooks/useTooltipProps';
  29. import Typography from '../Typography';
  30. import CopyBtn from './CopyBtn';
  31. import Ellipsis from './Ellipsis';
  32. import EllipsisTooltip from './EllipsisTooltip';
  33. import { isEleEllipsis, isValidText } from './util';
  34. function wrapperDecorations({
  35. mark,
  36. code,
  37. underline,
  38. delete: del,
  39. strong,
  40. keyboard,
  41. italic
  42. }, content) {
  43. let currentContent = content;
  44. function wrap(tag, needed) {
  45. if (!needed) {
  46. return;
  47. }
  48. currentContent = /*#__PURE__*/React.createElement(tag, {}, currentContent);
  49. }
  50. wrap('strong', strong);
  51. wrap('u', underline);
  52. wrap('del', del);
  53. wrap('code', code);
  54. wrap('mark', mark);
  55. wrap('kbd', keyboard);
  56. wrap('i', italic);
  57. return currentContent;
  58. }
  59. const ELLIPSIS_STR = '...';
  60. const DECORATION_PROPS = ['delete', 'mark', 'code', 'underline', 'strong', 'keyboard', 'italic'];
  61. const Base = /*#__PURE__*/React.forwardRef((props, ref) => {
  62. var _a;
  63. const {
  64. prefixCls: customizePrefixCls,
  65. className,
  66. style,
  67. type,
  68. disabled,
  69. children,
  70. ellipsis,
  71. editable,
  72. copyable,
  73. component,
  74. title
  75. } = props,
  76. restProps = __rest(props, ["prefixCls", "className", "style", "type", "disabled", "children", "ellipsis", "editable", "copyable", "component", "title"]);
  77. const {
  78. getPrefixCls,
  79. direction
  80. } = React.useContext(ConfigContext);
  81. const [textLocale] = useLocale('Text');
  82. const typographyRef = React.useRef(null);
  83. const editIconRef = React.useRef(null);
  84. // ============================ MISC ============================
  85. const prefixCls = getPrefixCls('typography', customizePrefixCls);
  86. const textProps = omit(restProps, DECORATION_PROPS);
  87. // ========================== Editable ==========================
  88. const [enableEdit, editConfig] = useMergedConfig(editable);
  89. const [editing, setEditing] = useMergedState(false, {
  90. value: editConfig.editing
  91. });
  92. const {
  93. triggerType = ['icon']
  94. } = editConfig;
  95. const triggerEdit = edit => {
  96. var _a;
  97. if (edit) {
  98. (_a = editConfig.onStart) === null || _a === void 0 ? void 0 : _a.call(editConfig);
  99. }
  100. setEditing(edit);
  101. };
  102. // Focus edit icon when back
  103. const prevEditing = usePrevious(editing);
  104. useLayoutEffect(() => {
  105. var _a;
  106. if (!editing && prevEditing) {
  107. (_a = editIconRef.current) === null || _a === void 0 ? void 0 : _a.focus();
  108. }
  109. }, [editing]);
  110. const onEditClick = e => {
  111. e === null || e === void 0 ? void 0 : e.preventDefault();
  112. triggerEdit(true);
  113. };
  114. const onEditChange = value => {
  115. var _a;
  116. (_a = editConfig.onChange) === null || _a === void 0 ? void 0 : _a.call(editConfig, value);
  117. triggerEdit(false);
  118. };
  119. const onEditCancel = () => {
  120. var _a;
  121. (_a = editConfig.onCancel) === null || _a === void 0 ? void 0 : _a.call(editConfig);
  122. triggerEdit(false);
  123. };
  124. // ========================== Copyable ==========================
  125. const [enableCopy, copyConfig] = useMergedConfig(copyable);
  126. const {
  127. copied,
  128. copyLoading,
  129. onClick: onCopyClick
  130. } = useCopyClick({
  131. copyConfig,
  132. children
  133. });
  134. // ========================== Ellipsis ==========================
  135. const [isLineClampSupport, setIsLineClampSupport] = React.useState(false);
  136. const [isTextOverflowSupport, setIsTextOverflowSupport] = React.useState(false);
  137. const [isJsEllipsis, setIsJsEllipsis] = React.useState(false);
  138. const [isNativeEllipsis, setIsNativeEllipsis] = React.useState(false);
  139. const [isNativeVisible, setIsNativeVisible] = React.useState(true);
  140. const [enableEllipsis, ellipsisConfig] = useMergedConfig(ellipsis, {
  141. expandable: false,
  142. symbol: isExpanded => isExpanded ? textLocale === null || textLocale === void 0 ? void 0 : textLocale.collapse : textLocale === null || textLocale === void 0 ? void 0 : textLocale.expand
  143. });
  144. const [expanded, setExpanded] = useMergedState(ellipsisConfig.defaultExpanded || false, {
  145. value: ellipsisConfig.expanded
  146. });
  147. const mergedEnableEllipsis = enableEllipsis && (!expanded || ellipsisConfig.expandable === 'collapsible');
  148. // Shared prop to reduce bundle size
  149. const {
  150. rows = 1
  151. } = ellipsisConfig;
  152. const needMeasureEllipsis = React.useMemo(() =>
  153. // Disable ellipsis
  154. mergedEnableEllipsis && (
  155. // Provide suffix
  156. ellipsisConfig.suffix !== undefined || ellipsisConfig.onEllipsis ||
  157. // Can't use css ellipsis since we need to provide the place for button
  158. ellipsisConfig.expandable || enableEdit || enableCopy), [mergedEnableEllipsis, ellipsisConfig, enableEdit, enableCopy]);
  159. useLayoutEffect(() => {
  160. if (enableEllipsis && !needMeasureEllipsis) {
  161. setIsLineClampSupport(isStyleSupport('webkitLineClamp'));
  162. setIsTextOverflowSupport(isStyleSupport('textOverflow'));
  163. }
  164. }, [needMeasureEllipsis, enableEllipsis]);
  165. const [cssEllipsis, setCssEllipsis] = React.useState(mergedEnableEllipsis);
  166. const canUseCssEllipsis = React.useMemo(() => {
  167. if (needMeasureEllipsis) {
  168. return false;
  169. }
  170. if (rows === 1) {
  171. return isTextOverflowSupport;
  172. }
  173. return isLineClampSupport;
  174. }, [needMeasureEllipsis, isTextOverflowSupport, isLineClampSupport]);
  175. // We use effect to change from css ellipsis to js ellipsis.
  176. // To make SSR still can see the ellipsis.
  177. useLayoutEffect(() => {
  178. setCssEllipsis(canUseCssEllipsis && mergedEnableEllipsis);
  179. }, [canUseCssEllipsis, mergedEnableEllipsis]);
  180. const isMergedEllipsis = mergedEnableEllipsis && (cssEllipsis ? isNativeEllipsis : isJsEllipsis);
  181. const cssTextOverflow = mergedEnableEllipsis && rows === 1 && cssEllipsis;
  182. const cssLineClamp = mergedEnableEllipsis && rows > 1 && cssEllipsis;
  183. // >>>>> Expand
  184. const onExpandClick = (e, info) => {
  185. var _a;
  186. setExpanded(info.expanded);
  187. (_a = ellipsisConfig.onExpand) === null || _a === void 0 ? void 0 : _a.call(ellipsisConfig, e, info);
  188. };
  189. const [ellipsisWidth, setEllipsisWidth] = React.useState(0);
  190. const onResize = ({
  191. offsetWidth
  192. }) => {
  193. setEllipsisWidth(offsetWidth);
  194. };
  195. // >>>>> JS Ellipsis
  196. const onJsEllipsis = jsEllipsis => {
  197. var _a;
  198. setIsJsEllipsis(jsEllipsis);
  199. // Trigger if changed
  200. if (isJsEllipsis !== jsEllipsis) {
  201. (_a = ellipsisConfig.onEllipsis) === null || _a === void 0 ? void 0 : _a.call(ellipsisConfig, jsEllipsis);
  202. }
  203. };
  204. // >>>>> Native ellipsis
  205. React.useEffect(() => {
  206. const textEle = typographyRef.current;
  207. if (enableEllipsis && cssEllipsis && textEle) {
  208. const currentEllipsis = isEleEllipsis(textEle);
  209. if (isNativeEllipsis !== currentEllipsis) {
  210. setIsNativeEllipsis(currentEllipsis);
  211. }
  212. }
  213. }, [enableEllipsis, cssEllipsis, children, cssLineClamp, isNativeVisible, ellipsisWidth]);
  214. // https://github.com/ant-design/ant-design/issues/36786
  215. // Use IntersectionObserver to check if element is invisible
  216. React.useEffect(() => {
  217. const textEle = typographyRef.current;
  218. if (typeof IntersectionObserver === 'undefined' || !textEle || !cssEllipsis || !mergedEnableEllipsis) {
  219. return;
  220. }
  221. /* eslint-disable-next-line compat/compat */
  222. const observer = new IntersectionObserver(() => {
  223. setIsNativeVisible(!!textEle.offsetParent);
  224. });
  225. observer.observe(textEle);
  226. return () => {
  227. observer.disconnect();
  228. };
  229. }, [cssEllipsis, mergedEnableEllipsis]);
  230. // ========================== Tooltip ===========================
  231. const tooltipProps = useTooltipProps(ellipsisConfig.tooltip, editConfig.text, children);
  232. const topAriaLabel = React.useMemo(() => {
  233. if (!enableEllipsis || cssEllipsis) {
  234. return undefined;
  235. }
  236. return [editConfig.text, children, title, tooltipProps.title].find(isValidText);
  237. }, [enableEllipsis, cssEllipsis, title, tooltipProps.title, isMergedEllipsis]);
  238. // =========================== Render ===========================
  239. // >>>>>>>>>>> Editing input
  240. if (editing) {
  241. return /*#__PURE__*/React.createElement(Editable, {
  242. value: (_a = editConfig.text) !== null && _a !== void 0 ? _a : typeof children === 'string' ? children : '',
  243. onSave: onEditChange,
  244. onCancel: onEditCancel,
  245. onEnd: editConfig.onEnd,
  246. prefixCls: prefixCls,
  247. className: className,
  248. style: style,
  249. direction: direction,
  250. component: component,
  251. maxLength: editConfig.maxLength,
  252. autoSize: editConfig.autoSize,
  253. enterIcon: editConfig.enterIcon
  254. });
  255. }
  256. // >>>>>>>>>>> Typography
  257. // Expand
  258. const renderExpand = () => {
  259. const {
  260. expandable,
  261. symbol
  262. } = ellipsisConfig;
  263. return expandable ? (/*#__PURE__*/React.createElement("button", {
  264. type: "button",
  265. key: "expand",
  266. className: `${prefixCls}-${expanded ? 'collapse' : 'expand'}`,
  267. onClick: e => onExpandClick(e, {
  268. expanded: !expanded
  269. }),
  270. "aria-label": expanded ? textLocale.collapse : textLocale === null || textLocale === void 0 ? void 0 : textLocale.expand
  271. }, typeof symbol === 'function' ? symbol(expanded) : symbol)) : null;
  272. };
  273. // Edit
  274. const renderEdit = () => {
  275. if (!enableEdit) {
  276. return;
  277. }
  278. const {
  279. icon,
  280. tooltip,
  281. tabIndex
  282. } = editConfig;
  283. const editTitle = toArray(tooltip)[0] || (textLocale === null || textLocale === void 0 ? void 0 : textLocale.edit);
  284. const ariaLabel = typeof editTitle === 'string' ? editTitle : '';
  285. return triggerType.includes('icon') ? (/*#__PURE__*/React.createElement(Tooltip, {
  286. key: "edit",
  287. title: tooltip === false ? '' : editTitle
  288. }, /*#__PURE__*/React.createElement("button", {
  289. type: "button",
  290. ref: editIconRef,
  291. className: `${prefixCls}-edit`,
  292. onClick: onEditClick,
  293. "aria-label": ariaLabel,
  294. tabIndex: tabIndex
  295. }, icon || /*#__PURE__*/React.createElement(EditOutlined, {
  296. role: "button"
  297. })))) : null;
  298. };
  299. // Copy
  300. const renderCopy = () => {
  301. if (!enableCopy) {
  302. return null;
  303. }
  304. return /*#__PURE__*/React.createElement(CopyBtn, Object.assign({
  305. key: "copy"
  306. }, copyConfig, {
  307. prefixCls: prefixCls,
  308. copied: copied,
  309. locale: textLocale,
  310. onCopy: onCopyClick,
  311. loading: copyLoading,
  312. iconOnly: children === null || children === undefined
  313. }));
  314. };
  315. const renderOperations = canEllipsis => [canEllipsis && renderExpand(), renderEdit(), renderCopy()];
  316. const renderEllipsis = canEllipsis => [canEllipsis && !expanded && (/*#__PURE__*/React.createElement("span", {
  317. "aria-hidden": true,
  318. key: "ellipsis"
  319. }, ELLIPSIS_STR)), ellipsisConfig.suffix, renderOperations(canEllipsis)];
  320. return /*#__PURE__*/React.createElement(ResizeObserver, {
  321. onResize: onResize,
  322. disabled: !mergedEnableEllipsis
  323. }, resizeRef => (/*#__PURE__*/React.createElement(EllipsisTooltip, {
  324. tooltipProps: tooltipProps,
  325. enableEllipsis: mergedEnableEllipsis,
  326. isEllipsis: isMergedEllipsis
  327. }, /*#__PURE__*/React.createElement(Typography, Object.assign({
  328. className: classNames({
  329. [`${prefixCls}-${type}`]: type,
  330. [`${prefixCls}-disabled`]: disabled,
  331. [`${prefixCls}-ellipsis`]: enableEllipsis,
  332. [`${prefixCls}-ellipsis-single-line`]: cssTextOverflow,
  333. [`${prefixCls}-ellipsis-multiple-line`]: cssLineClamp
  334. }, className),
  335. prefixCls: customizePrefixCls,
  336. style: Object.assign(Object.assign({}, style), {
  337. WebkitLineClamp: cssLineClamp ? rows : undefined
  338. }),
  339. component: component,
  340. ref: composeRef(resizeRef, typographyRef, ref),
  341. direction: direction,
  342. onClick: triggerType.includes('text') ? onEditClick : undefined,
  343. "aria-label": topAriaLabel === null || topAriaLabel === void 0 ? void 0 : topAriaLabel.toString(),
  344. title: title
  345. }, textProps), /*#__PURE__*/React.createElement(Ellipsis, {
  346. enableMeasure: mergedEnableEllipsis && !cssEllipsis,
  347. text: children,
  348. rows: rows,
  349. width: ellipsisWidth,
  350. onEllipsis: onJsEllipsis,
  351. expanded: expanded,
  352. miscDeps: [copied, expanded, copyLoading, enableEdit, enableCopy, textLocale].concat(_toConsumableArray(DECORATION_PROPS.map(key => props[key])))
  353. }, (node, canEllipsis) => wrapperDecorations(props, /*#__PURE__*/React.createElement(React.Fragment, null, node.length > 0 && canEllipsis && !expanded && topAriaLabel ? (/*#__PURE__*/React.createElement("span", {
  354. key: "show-content",
  355. "aria-hidden": true
  356. }, node)) : node, renderEllipsis(canEllipsis))))))));
  357. });
  358. export default Base;