UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

733 lines (732 loc) 26.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _isFunction2 = _interopRequireDefault(require("lodash/isFunction")); var _isNull2 = _interopRequireDefault(require("lodash/isNull")); var _isString2 = _interopRequireDefault(require("lodash/isString")); var _merge2 = _interopRequireDefault(require("lodash/merge")); var _omit2 = _interopRequireDefault(require("lodash/omit")); var _isUndefined2 = _interopRequireDefault(require("lodash/isUndefined")); var _react = _interopRequireWildcard(require("react")); var _classnames = _interopRequireDefault(require("classnames")); var _propTypes = _interopRequireDefault(require("prop-types")); var _constants = require("@douyinfe/semi-foundation/lib/cjs/typography/constants"); var _typography = _interopRequireDefault(require("./typography")); var _copyable = _interopRequireDefault(require("./copyable")); var _index = _interopRequireDefault(require("../tooltip/index")); var _index2 = _interopRequireDefault(require("../popover/index")); var _util = _interopRequireDefault(require("./util")); var _warning = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/warning")); var _isEnterPress = _interopRequireDefault(require("@douyinfe/semi-foundation/lib/cjs/utils/isEnterPress")); var _localeConsumer = _interopRequireDefault(require("../locale/localeConsumer")); var _utils = require("../_utils"); var _context = _interopRequireDefault(require("./context")); var _resizeObserver = _interopRequireWildcard(require("../resizeObserver")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } var __awaiter = void 0 && (void 0).__awaiter || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = void 0 && (void 0).__rest || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; const prefixCls = _constants.cssClasses.PREFIX; const ELLIPSIS_STR = '...'; const wrapperDecorations = (props, content) => { const { mark, code, underline, strong, link, disabled } = props; let wrapped = content; const wrap = (isNeeded, tag) => { let wrapProps = {}; if (!isNeeded) { return; } if (typeof isNeeded === 'object') { wrapProps = Object.assign({}, isNeeded); } wrapped = /*#__PURE__*/_react.default.createElement(tag, wrapProps, wrapped); }; wrap(mark, 'mark'); wrap(code, 'code'); wrap(underline && !link, 'u'); wrap(strong, 'strong'); wrap(props.delete, 'del'); wrap(link, disabled ? 'span' : 'a'); return wrapped; }; class Base extends _react.Component { constructor(props) { super(props); this.observerTakingEffect = false; this.onResize = entries => __awaiter(this, void 0, void 0, function* () { if (this.rafId) { window.cancelAnimationFrame(this.rafId); } return new Promise(resolve => { this.rafId = window.requestAnimationFrame(() => __awaiter(this, void 0, void 0, function* () { yield this.getEllipsisState(); resolve(); })); }); }); // if it needs to use js overflowed: // 1. text is expandable 2. expandText need to be shown 3. has extra operation 4. text need to ellipse from mid this.canUseCSSEllipsis = () => { const { copyable } = this.props; const { expandable, expandText, pos, suffix } = this.getEllipsisOpt(); return !expandable && (0, _isUndefined2.default)(expandText) && !copyable && pos === 'end' && !suffix.length; }; /** * whether truncated * rows < = 1 if there is overflow content, return true * rows > 1 if there is overflow height, return true * @param {Number} rows * @returns {Boolean} */ this.shouldTruncated = rows => { if (!rows || rows < 1) { return false; } const updateOverflow = rows <= 1 ? this.compareSingleRow() : this.wrapperRef.current.scrollHeight > this.wrapperRef.current.offsetHeight; return updateOverflow; }; /** * 通过将 content 给到 Range 对象,借助 Range 的 getBoundingClientRect 拿到 content 的准确 width * 不受 css ellipsis 与否的影响 * By giving the content to the Range object, get the exact width of the content with the help of Range's getBoundingClientRect * Not affected by css ellipsis or not * https://github.com/DouyinFE/semi-design/issues/1731 */ this.compareSingleRow = () => { if (!(document && document.createRange)) { return false; } const containerNode = this.wrapperRef.current; const containerWidth = containerNode.getBoundingClientRect().width; const childNodes = Array.from(containerNode.childNodes); const range = document.createRange(); const contentWidth = childNodes.reduce((acc, node) => { var _a; range.selectNodeContents(node); return acc + ((_a = range.getBoundingClientRect().width) !== null && _a !== void 0 ? _a : 0); }, 0); range.detach(); return contentWidth > containerWidth; }; this.showTooltip = () => { var _a, _b; const { isOverflowed, isTruncated, expanded } = this.state; const { showTooltip, expandable, expandText } = this.getEllipsisOpt(); const canUseCSSEllipsis = this.canUseCSSEllipsis(); // If the css is truncated, use isOverflowed to judge. If the css is truncated, use isTruncated to judge. const overflowed = !expanded && (canUseCSSEllipsis ? isOverflowed : isTruncated); const noExpandText = !expandable && (0, _isUndefined2.default)(expandText); const show = noExpandText && overflowed && showTooltip; if (!show) { return show; } const defaultOpts = { type: 'tooltip' }; if (typeof showTooltip === 'object') { if (showTooltip.type && showTooltip.type.toLowerCase() === 'popover') { return (0, _merge2.default)({ opts: { // style: { width: '240px' }, showArrow: true } }, showTooltip, { opts: { className: (0, _classnames.default)({ [`${prefixCls}-ellipsis-popover`]: true, [(_a = showTooltip === null || showTooltip === void 0 ? void 0 : showTooltip.opts) === null || _a === void 0 ? void 0 : _a.className]: Boolean((_b = showTooltip === null || showTooltip === void 0 ? void 0 : showTooltip.opts) === null || _b === void 0 ? void 0 : _b.className) }) } }); } return Object.assign(Object.assign({}, defaultOpts), showTooltip); } return defaultOpts; }; this.onHover = () => { const canUseCSSEllipsis = this.canUseCSSEllipsis(); if (canUseCSSEllipsis) { const { rows, suffix, pos } = this.getEllipsisOpt(); const updateOverflow = this.shouldTruncated(rows); // isOverflowed needs to be updated to show tooltip when using css ellipsis this.setState({ isOverflowed: updateOverflow, isTruncated: false }); return undefined; } }; this.getEllipsisState = () => __awaiter(this, void 0, void 0, function* () { const { rows, suffix, pos } = this.getEllipsisOpt(); const { children, strong } = this.props; // wait until element mounted if (!this.wrapperRef || !this.wrapperRef.current) { yield this.onResize(); return; } const { expanded } = this.state; const canUseCSSEllipsis = this.canUseCSSEllipsis(); if (canUseCSSEllipsis) { // const updateOverflow = this.shouldTruncated(rows); // // isOverflowed needs to be updated to show tooltip when using css ellipsis // this.setState({ // isOverflowed: updateOverflow, // isTruncated: false // }); return; } // If children is null, css/js truncated flag isTruncate is false if ((0, _isNull2.default)(children)) { return new Promise(resolve => { this.setState({ isTruncated: false, isOverflowed: false }, resolve); }); } // Currently only text truncation is supported, if there is non-text, // both css truncation and js truncation should throw a warning (0, _warning.default)('children' in this.props && typeof children !== 'string', "[Semi Typography] Only children with pure text could be used with ellipsis at this moment."); if (!rows || rows < 0 || expanded) { return; } const extraNode = { expand: this.expandRef.current, copy: this.copyRef && this.copyRef.current }; // Perform type conversion on children to prevent component crash due to non-string type of children // https://github.com/DouyinFE/semi-design/issues/2167 const realChildren = Array.isArray(children) ? children.join('') : String(children); const content = (0, _util.default)(this.wrapperRef.current, rows, realChildren, extraNode, ELLIPSIS_STR, suffix, pos, strong); return new Promise(resolve => { this.setState({ isOverflowed: false, ellipsisContent: content, isTruncated: realChildren !== content }, resolve); }); }); /** * Triggered when the fold button is clicked to save the latest expanded state * @param {Event} e */ this.toggleOverflow = e => { const { onExpand, expandable, collapsible } = this.getEllipsisOpt(); const { expanded } = this.state; onExpand && onExpand(!expanded, e); if (expandable && !expanded || collapsible && expanded) { this.setState({ expanded: !expanded }); } }; this.getEllipsisOpt = () => { const { ellipsis } = this.props; if (!ellipsis) { return {}; } const opt = Object.assign({ rows: 1, expandable: false, pos: 'end', suffix: '', showTooltip: false, collapsible: false, expandText: ellipsis.expandable ? this.expandStr : undefined, collapseText: ellipsis.collapsible ? this.collapseStr : undefined }, typeof ellipsis === 'object' ? ellipsis : null); return opt; }; this.renderExpandable = () => { const { expanded, isTruncated } = this.state; if (!isTruncated) return null; const { expandText, expandable, collapseText, collapsible } = this.getEllipsisOpt(); const noExpandText = !expandable && (0, _isUndefined2.default)(expandText); const noCollapseText = !collapsible && (0, _isUndefined2.default)(collapseText); let text; if (!expanded && !noExpandText) { text = expandText; } else if (expanded && !noCollapseText) { text = collapseText; } if (!noExpandText || !noCollapseText) { return ( /*#__PURE__*/ // TODO: replace `a` tag with `span` in next major version // NOTE: may have effect on style // eslint-disable-next-line jsx-a11y/anchor-is-valid _react.default.createElement("a", { role: "button", tabIndex: 0, className: `${prefixCls}-ellipsis-expand`, key: "expand", ref: this.expandRef, "aria-label": text, onClick: this.toggleOverflow, onKeyPress: e => (0, _isEnterPress.default)(e) && this.toggleOverflow(e) }, text) ); } return null; }; /** * 获取文本的缩略class和style * * 截断类型: * - 当设置中间截断(pos='middle')、可展开(expandable)、有后缀(suffix 非空)、可复制(copyable),启用 JS 截断策略 * - 非以上场景,启用 CSS 截断策略 * 相关变量 * props: * - ellipsis: * - rows * - expandable * - pos * - suffix * state: * - isOverflowed,文本是否处于overflow状态 * - expanded,文本是否处于折叠状态 * - isTruncated,文本是否被js截断 * * Get the abbreviated class and style of the text * * Truncation type: * -When setting middle ellipsis (pos='middle')、expandable、suffix is not empty、copyable, the JS ellipsis strategy is enabled * -Otherwise, enable the CSS ellipsis strategy * related variables * props: * -ellipsis: * -rows * -expandable * -pos * -suffix * state: * -isOverflowed, whether the text is in an overflow state * -expanded, whether the text is in a collapsed state * -isTruncated, whether the text is truncated by js * @returns {Object} */ this.getEllipsisStyle = () => { const { ellipsis, component } = this.props; if (!ellipsis) { return { ellipsisCls: '', ellipsisStyle: {} // ellipsisAttr: {} }; } const { rows } = this.getEllipsisOpt(); const { expanded } = this.state; const useCSS = !expanded && this.canUseCSSEllipsis(); const ellipsisCls = (0, _classnames.default)({ [`${prefixCls}-ellipsis`]: true, [`${prefixCls}-ellipsis-single-line`]: rows === 1, [`${prefixCls}-ellipsis-multiple-line`]: rows > 1, // component === 'span', Text component, It should be externally displayed inline [`${prefixCls}-ellipsis-multiple-line-text`]: rows > 1 && component === 'span', [`${prefixCls}-ellipsis-overflow-ellipsis`]: rows === 1 && useCSS, // component === 'span', Text component, It should be externally displayed inline [`${prefixCls}-ellipsis-overflow-ellipsis-text`]: rows === 1 && useCSS && component === 'span' }); const ellipsisStyle = useCSS && rows > 1 ? { WebkitLineClamp: rows } : {}; return { ellipsisCls, ellipsisStyle }; }; this.renderEllipsisText = opt => { const { suffix } = opt; const { children } = this.props; const { isTruncated, expanded, ellipsisContent } = this.state; if (expanded || !isTruncated) { return /*#__PURE__*/_react.default.createElement("span", { onMouseEnter: this.onHover }, children, suffix && suffix.length ? suffix : null); } return /*#__PURE__*/_react.default.createElement("span", { onMouseEnter: this.onHover }, ellipsisContent, suffix); }; this.state = { editable: false, copied: false, // ellipsis // if text is overflow in container isOverflowed: false, ellipsisContent: props.children, expanded: false, // if text is truncated with js isTruncated: false, prevChildren: null }; this.wrapperRef = /*#__PURE__*/_react.default.createRef(); this.expandRef = /*#__PURE__*/_react.default.createRef(); this.copyRef = /*#__PURE__*/_react.default.createRef(); } componentDidMount() { if (this.props.ellipsis) { // runAfterTicks: make sure start observer on the next tick this.onResize().then(() => (0, _utils.runAfterTicks)(() => this.observerTakingEffect = true, 1)); } } static getDerivedStateFromProps(props, prevState) { const { prevChildren } = prevState; const newState = {}; newState.prevChildren = props.children; if (props.ellipsis && prevChildren !== props.children) { // reset ellipsis state if children update newState.isOverflowed = false; newState.ellipsisContent = props.children; newState.expanded = false; newState.isTruncated = true; } return newState; } componentDidUpdate(prevProps) { // Render was based on outdated refs and needs to be rerun if (this.props.children !== prevProps.children) { this.forceUpdate(); if (this.props.ellipsis) { this.onResize(); } } } componentWillUnmount() { if (this.rafId) { window.cancelAnimationFrame(this.rafId); } } renderOperations() { return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, this.renderExpandable(), this.renderCopy()); } renderCopy() { var _a; const { copyable, children } = this.props; if (!copyable) { return null; } // If it is configured in the content of copyable, the copied content will be the content in copyable const willCopyContent = (_a = copyable === null || copyable === void 0 ? void 0 : copyable.content) !== null && _a !== void 0 ? _a : children; let copyContent; let hasObject = false; if (Array.isArray(willCopyContent)) { copyContent = ''; willCopyContent.forEach(value => { if (typeof value === 'object') { hasObject = true; } copyContent += String(value); }); } else if (typeof willCopyContent !== 'object') { copyContent = String(willCopyContent); } else { hasObject = true; copyContent = String(willCopyContent); } (0, _warning.default)(hasObject, 'Content to be copied in Typography is a object, it will case a [object Object] mistake when copy to clipboard.'); const copyConfig = Object.assign({ content: copyContent, duration: 3 }, typeof copyable === 'object' ? copyable : null); return /*#__PURE__*/_react.default.createElement(_copyable.default, Object.assign({}, copyConfig, { forwardRef: this.copyRef })); } renderIcon() { const { icon, size } = this.props; const realSize = size === 'inherit' ? this.context : size; if (!icon) { return null; } const iconSize = realSize === 'small' ? 'small' : 'default'; return /*#__PURE__*/_react.default.createElement("span", { className: `${prefixCls}-icon`, "x-semi-prop": "icon" }, (0, _utils.isSemiIcon)(icon) ? /*#__PURE__*/_react.default.cloneElement(icon, { size: iconSize }) : icon); } renderContent() { const _a = this.props, { component, children, className, type, spacing, disabled, style, ellipsis, icon, size, link, heading, weight } = _a, rest = __rest(_a, ["component", "children", "className", "type", "spacing", "disabled", "style", "ellipsis", "icon", "size", "link", "heading", "weight"]); const textProps = (0, _omit2.default)(rest, ['strong', 'editable', 'mark', 'copyable', 'underline', 'code', // 'link', 'delete']); const realSize = size === 'inherit' ? this.context : size; const iconNode = this.renderIcon(); const ellipsisOpt = this.getEllipsisOpt(); const { ellipsisCls, ellipsisStyle } = this.getEllipsisStyle(); let textNode = ellipsis ? this.renderEllipsisText(ellipsisOpt) : children; const linkCls = (0, _classnames.default)({ [`${prefixCls}-link-text`]: link, [`${prefixCls}-link-underline`]: this.props.underline && link }); textNode = wrapperDecorations(this.props, /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, iconNode, this.props.link ? /*#__PURE__*/_react.default.createElement("span", { className: linkCls }, textNode) : textNode)); const hTagReg = /^h[1-6]$/; const isHeader = (0, _isString2.default)(heading) && hTagReg.test(heading); const wrapperCls = (0, _classnames.default)(className, ellipsisCls, { // [`${prefixCls}-primary`]: !type || type === 'primary', [`${prefixCls}-${type}`]: type && !link, [`${prefixCls}-${realSize}`]: realSize, [`${prefixCls}-link`]: link, [`${prefixCls}-disabled`]: disabled, [`${prefixCls}-${spacing}`]: spacing, [`${prefixCls}-${heading}`]: isHeader, [`${prefixCls}-${heading}-weight-${weight}`]: isHeader && weight && isNaN(Number(weight)) }); const textStyle = Object.assign(Object.assign({}, isNaN(Number(weight)) ? {} : { fontWeight: weight }), style); return /*#__PURE__*/_react.default.createElement(_typography.default, Object.assign({ className: wrapperCls, style: Object.assign(Object.assign({}, textStyle), ellipsisStyle), component: component, forwardRef: this.wrapperRef }, textProps), textNode, this.renderOperations()); } renderTipWrapper() { const { children } = this.props; const showTooltip = this.showTooltip(); const content = this.renderContent(); if (showTooltip) { const { type, opts, renderTooltip } = showTooltip; if ((0, _isFunction2.default)(renderTooltip)) { return renderTooltip(children, content); } else if (type.toLowerCase() === 'popover') { return /*#__PURE__*/_react.default.createElement(_index2.default, Object.assign({ content: children, position: "top" }, opts), content); } return /*#__PURE__*/_react.default.createElement(_index.default, Object.assign({ content: children, position: "top" }, opts), content); } else { return content; } } render() { var _this = this; const { size } = this.props; const realSize = size === 'inherit' ? this.context : size; const content = /*#__PURE__*/_react.default.createElement(_context.default.Provider, { value: realSize }, /*#__PURE__*/_react.default.createElement(_localeConsumer.default, { componentName: "Typography" }, locale => { this.expandStr = locale.expand; this.collapseStr = locale.collapse; return this.renderTipWrapper(); })); if (this.props.ellipsis) { return /*#__PURE__*/_react.default.createElement(_resizeObserver.default, { onResize: function () { if (_this.observerTakingEffect) { _this.onResize(...arguments); } }, observeParent: true, observerProperty: _resizeObserver.ObserverProperty.Width }, content); } return content; } } exports.default = Base; Base.propTypes = { children: _propTypes.default.node, copyable: _propTypes.default.oneOfType([_propTypes.default.shape({ text: _propTypes.default.string, onCopy: _propTypes.default.func, successTip: _propTypes.default.node, copyTip: _propTypes.default.node }), _propTypes.default.bool]), delete: _propTypes.default.bool, disabled: _propTypes.default.bool, // editable: PropTypes.bool, ellipsis: _propTypes.default.oneOfType([_propTypes.default.shape({ rows: _propTypes.default.number, expandable: _propTypes.default.bool, expandText: _propTypes.default.string, onExpand: _propTypes.default.func, suffix: _propTypes.default.string, showTooltip: _propTypes.default.oneOfType([_propTypes.default.shape({ type: _propTypes.default.string, opts: _propTypes.default.object }), _propTypes.default.bool]), collapsible: _propTypes.default.bool, collapseText: _propTypes.default.string, pos: _propTypes.default.oneOf(['end', 'middle']) }), _propTypes.default.bool]), mark: _propTypes.default.bool, underline: _propTypes.default.bool, link: _propTypes.default.oneOfType([_propTypes.default.object, _propTypes.default.bool]), spacing: _propTypes.default.oneOf(_constants.strings.SPACING), strong: _propTypes.default.bool, size: _propTypes.default.oneOf(_constants.strings.SIZE), type: _propTypes.default.oneOf(_constants.strings.TYPE), style: _propTypes.default.object, className: _propTypes.default.string, icon: _propTypes.default.oneOfType([_propTypes.default.node, _propTypes.default.string]), heading: _propTypes.default.string, component: _propTypes.default.string }; Base.defaultProps = { children: null, copyable: false, delete: false, disabled: false, // editable: false, ellipsis: false, icon: '', mark: false, underline: false, strong: false, link: false, type: 'primary', spacing: 'normal', size: 'normal', style: {}, className: '' }; Base.contextType = _context.default;