@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
JavaScript
"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;