Base.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { Fragment as _Fragment, resolveDirective as _resolveDirective, createVNode as _createVNode } from "vue";
  4. var __rest = this && this.__rest || function (s, e) {
  5. var t = {};
  6. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
  7. if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  8. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
  9. }
  10. return t;
  11. };
  12. import LocaleReceiver from '../locale-provider/LocaleReceiver';
  13. import warning from '../_util/warning';
  14. import TransButton from '../_util/transButton';
  15. import raf from '../_util/raf';
  16. import { isStyleSupport } from '../_util/styleChecker';
  17. import Editable from './Editable';
  18. import measure from './util';
  19. import Typography from './Typography';
  20. import ResizeObserver from '../vc-resize-observer';
  21. import Tooltip from '../tooltip';
  22. import copy from '../_util/copy-to-clipboard';
  23. import CheckOutlined from "@ant-design/icons-vue/es/icons/CheckOutlined";
  24. import CopyOutlined from "@ant-design/icons-vue/es/icons/CopyOutlined";
  25. import EditOutlined from "@ant-design/icons-vue/es/icons/EditOutlined";
  26. import { defineComponent, reactive, ref, onMounted, onBeforeUnmount, watch, watchEffect, nextTick, computed, toRaw } from 'vue';
  27. import useConfigInject from '../config-provider/hooks/useConfigInject';
  28. import omit from '../_util/omit';
  29. import useMergedState from '../_util/hooks/useMergedState';
  30. import { findDOMNode } from '../_util/props-util';
  31. const isLineClampSupport = isStyleSupport('webkitLineClamp');
  32. const isTextOverflowSupport = isStyleSupport('textOverflow');
  33. const ELLIPSIS_STR = '...';
  34. export const baseProps = () => ({
  35. editable: {
  36. type: [Boolean, Object],
  37. default: undefined
  38. },
  39. copyable: {
  40. type: [Boolean, Object],
  41. default: undefined
  42. },
  43. prefixCls: String,
  44. component: String,
  45. type: String,
  46. disabled: {
  47. type: Boolean,
  48. default: undefined
  49. },
  50. ellipsis: {
  51. type: [Boolean, Object],
  52. default: undefined
  53. },
  54. code: {
  55. type: Boolean,
  56. default: undefined
  57. },
  58. mark: {
  59. type: Boolean,
  60. default: undefined
  61. },
  62. underline: {
  63. type: Boolean,
  64. default: undefined
  65. },
  66. delete: {
  67. type: Boolean,
  68. default: undefined
  69. },
  70. strong: {
  71. type: Boolean,
  72. default: undefined
  73. },
  74. keyboard: {
  75. type: Boolean,
  76. default: undefined
  77. },
  78. content: String,
  79. 'onUpdate:content': Function
  80. });
  81. const Base = defineComponent({
  82. compatConfig: {
  83. MODE: 3
  84. },
  85. name: 'TypographyBase',
  86. inheritAttrs: false,
  87. props: baseProps(),
  88. // emits: ['update:content'],
  89. setup(props, _ref) {
  90. let {
  91. slots,
  92. attrs,
  93. emit
  94. } = _ref;
  95. const {
  96. prefixCls,
  97. direction
  98. } = useConfigInject('typography', props);
  99. const state = reactive({
  100. copied: false,
  101. ellipsisText: '',
  102. ellipsisContent: null,
  103. isEllipsis: false,
  104. expanded: false,
  105. clientRendered: false,
  106. //locale
  107. expandStr: '',
  108. copyStr: '',
  109. copiedStr: '',
  110. editStr: '',
  111. copyId: undefined,
  112. rafId: undefined,
  113. prevProps: undefined,
  114. originContent: ''
  115. });
  116. const contentRef = ref();
  117. const editIcon = ref();
  118. const ellipsis = computed(() => {
  119. const ellipsis = props.ellipsis;
  120. if (!ellipsis) return {};
  121. return _extends({
  122. rows: 1,
  123. expandable: false
  124. }, typeof ellipsis === 'object' ? ellipsis : null);
  125. });
  126. onMounted(() => {
  127. state.clientRendered = true;
  128. syncEllipsis();
  129. });
  130. onBeforeUnmount(() => {
  131. clearTimeout(state.copyId);
  132. raf.cancel(state.rafId);
  133. });
  134. watch([() => ellipsis.value.rows, () => props.content], () => {
  135. nextTick(() => {
  136. resizeOnNextFrame();
  137. });
  138. }, {
  139. flush: 'post',
  140. deep: true
  141. });
  142. watchEffect(() => {
  143. if (props.content === undefined) {
  144. warning(!props.editable, 'Typography', 'When `editable` is enabled, please use `content` instead of children');
  145. warning(!props.ellipsis, 'Typography', 'When `ellipsis` is enabled, please use `content` instead of children');
  146. }
  147. });
  148. function getChildrenText() {
  149. var _a;
  150. return props.ellipsis || props.editable ? props.content : (_a = findDOMNode(contentRef.value)) === null || _a === void 0 ? void 0 : _a.innerText;
  151. }
  152. // =============== Expand ===============
  153. function onExpandClick(e) {
  154. const {
  155. onExpand
  156. } = ellipsis.value;
  157. state.expanded = true;
  158. onExpand === null || onExpand === void 0 ? void 0 : onExpand(e);
  159. }
  160. // ================ Edit ================
  161. function onEditClick(e) {
  162. e.preventDefault();
  163. state.originContent = props.content;
  164. triggerEdit(true);
  165. }
  166. function onEditChange(value) {
  167. onContentChange(value);
  168. triggerEdit(false);
  169. }
  170. function onContentChange(value) {
  171. const {
  172. onChange
  173. } = editable.value;
  174. if (value !== props.content) {
  175. emit('update:content', value);
  176. onChange === null || onChange === void 0 ? void 0 : onChange(value);
  177. }
  178. }
  179. function onEditCancel() {
  180. var _a, _b;
  181. (_b = (_a = editable.value).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
  182. triggerEdit(false);
  183. }
  184. // ================ Copy ================
  185. function onCopyClick(e) {
  186. e.preventDefault();
  187. e.stopPropagation();
  188. const {
  189. copyable
  190. } = props;
  191. const copyConfig = _extends({}, typeof copyable === 'object' ? copyable : null);
  192. if (copyConfig.text === undefined) {
  193. copyConfig.text = getChildrenText();
  194. }
  195. copy(copyConfig.text || '');
  196. state.copied = true;
  197. nextTick(() => {
  198. if (copyConfig.onCopy) {
  199. copyConfig.onCopy(e);
  200. }
  201. state.copyId = setTimeout(() => {
  202. state.copied = false;
  203. }, 3000);
  204. });
  205. }
  206. const editable = computed(() => {
  207. const editable = props.editable;
  208. if (!editable) return {
  209. editing: false
  210. };
  211. return _extends({}, typeof editable === 'object' ? editable : null);
  212. });
  213. const [editing, setEditing] = useMergedState(false, {
  214. value: computed(() => {
  215. return editable.value.editing;
  216. })
  217. });
  218. function triggerEdit(edit) {
  219. const {
  220. onStart
  221. } = editable.value;
  222. if (edit && onStart) {
  223. onStart();
  224. }
  225. setEditing(edit);
  226. }
  227. watch(editing, val => {
  228. var _a;
  229. if (!val) {
  230. (_a = editIcon.value) === null || _a === void 0 ? void 0 : _a.focus();
  231. }
  232. }, {
  233. flush: 'post'
  234. });
  235. // ============== Ellipsis ==============
  236. function resizeOnNextFrame(sizeInfo) {
  237. if (sizeInfo) {
  238. const {
  239. width,
  240. height
  241. } = sizeInfo;
  242. if (!width || !height) return;
  243. }
  244. raf.cancel(state.rafId);
  245. state.rafId = raf(() => {
  246. // Do not bind `syncEllipsis`. It need for test usage on prototype
  247. syncEllipsis();
  248. });
  249. }
  250. const canUseCSSEllipsis = computed(() => {
  251. const {
  252. rows,
  253. expandable,
  254. suffix,
  255. onEllipsis,
  256. tooltip
  257. } = ellipsis.value;
  258. if (suffix || tooltip) return false;
  259. // Can't use css ellipsis since we need to provide the place for button
  260. if (props.editable || props.copyable || expandable || onEllipsis) {
  261. return false;
  262. }
  263. if (rows === 1) {
  264. return isTextOverflowSupport;
  265. }
  266. return isLineClampSupport;
  267. });
  268. const syncEllipsis = () => {
  269. const {
  270. ellipsisText,
  271. isEllipsis
  272. } = state;
  273. const {
  274. rows,
  275. suffix,
  276. onEllipsis
  277. } = ellipsis.value;
  278. if (!rows || rows < 0 || !findDOMNode(contentRef.value) || state.expanded || props.content === undefined) return;
  279. // Do not measure if css already support ellipsis
  280. if (canUseCSSEllipsis.value) return;
  281. const {
  282. content,
  283. text,
  284. ellipsis: ell
  285. } = measure(findDOMNode(contentRef.value), {
  286. rows,
  287. suffix
  288. }, props.content, renderOperations(true), ELLIPSIS_STR);
  289. if (ellipsisText !== text || state.isEllipsis !== ell) {
  290. state.ellipsisText = text;
  291. state.ellipsisContent = content;
  292. state.isEllipsis = ell;
  293. if (isEllipsis !== ell && onEllipsis) {
  294. onEllipsis(ell);
  295. }
  296. }
  297. };
  298. function wrapperDecorations(_ref2, content) {
  299. let {
  300. mark,
  301. code,
  302. underline,
  303. delete: del,
  304. strong,
  305. keyboard
  306. } = _ref2;
  307. let currentContent = content;
  308. function wrap(needed, Tag) {
  309. if (!needed) return;
  310. const _currentContent = function () {
  311. return currentContent;
  312. }();
  313. currentContent = _createVNode(Tag, null, {
  314. default: () => [_currentContent]
  315. });
  316. }
  317. wrap(strong, 'strong');
  318. wrap(underline, 'u');
  319. wrap(del, 'del');
  320. wrap(code, 'code');
  321. wrap(mark, 'mark');
  322. wrap(keyboard, 'kbd');
  323. return currentContent;
  324. }
  325. function renderExpand(forceRender) {
  326. const {
  327. expandable,
  328. symbol
  329. } = ellipsis.value;
  330. if (!expandable) return null;
  331. // force render expand icon for measure usage or it will cause dead loop
  332. if (!forceRender && (state.expanded || !state.isEllipsis)) return null;
  333. const expandContent = (slots.ellipsisSymbol ? slots.ellipsisSymbol() : symbol) || state.expandStr;
  334. return _createVNode("a", {
  335. "key": "expand",
  336. "class": `${prefixCls.value}-expand`,
  337. "onClick": onExpandClick,
  338. "aria-label": state.expandStr
  339. }, [expandContent]);
  340. }
  341. function renderEdit() {
  342. if (!props.editable) return;
  343. const {
  344. tooltip,
  345. triggerType = ['icon']
  346. } = props.editable;
  347. const icon = slots.editableIcon ? slots.editableIcon() : _createVNode(EditOutlined, {
  348. "role": "button"
  349. }, null);
  350. const title = slots.editableTooltip ? slots.editableTooltip() : state.editStr;
  351. const ariaLabel = typeof title === 'string' ? title : '';
  352. return triggerType.indexOf('icon') !== -1 ? _createVNode(Tooltip, {
  353. "key": "edit",
  354. "title": tooltip === false ? '' : title
  355. }, {
  356. default: () => [_createVNode(TransButton, {
  357. "ref": editIcon,
  358. "class": `${prefixCls.value}-edit`,
  359. "onClick": onEditClick,
  360. "aria-label": ariaLabel
  361. }, {
  362. default: () => [icon]
  363. })]
  364. }) : null;
  365. }
  366. function renderCopy() {
  367. if (!props.copyable) return;
  368. const {
  369. tooltip
  370. } = props.copyable;
  371. const defaultTitle = state.copied ? state.copiedStr : state.copyStr;
  372. const title = slots.copyableTooltip ? slots.copyableTooltip({
  373. copied: state.copied
  374. }) : defaultTitle;
  375. const ariaLabel = typeof title === 'string' ? title : '';
  376. const defaultIcon = state.copied ? _createVNode(CheckOutlined, null, null) : _createVNode(CopyOutlined, null, null);
  377. const icon = slots.copyableIcon ? slots.copyableIcon({
  378. copied: !!state.copied
  379. }) : defaultIcon;
  380. return _createVNode(Tooltip, {
  381. "key": "copy",
  382. "title": tooltip === false ? '' : title
  383. }, {
  384. default: () => [_createVNode(TransButton, {
  385. "class": [`${prefixCls.value}-copy`, {
  386. [`${prefixCls.value}-copy-success`]: state.copied
  387. }],
  388. "onClick": onCopyClick,
  389. "aria-label": ariaLabel
  390. }, {
  391. default: () => [icon]
  392. })]
  393. });
  394. }
  395. function renderEditInput() {
  396. const {
  397. class: className,
  398. style
  399. } = attrs;
  400. const {
  401. maxlength,
  402. autoSize,
  403. onEnd
  404. } = editable.value;
  405. return _createVNode(Editable, {
  406. "class": className,
  407. "style": style,
  408. "prefixCls": prefixCls.value,
  409. "value": props.content,
  410. "originContent": state.originContent,
  411. "maxlength": maxlength,
  412. "autoSize": autoSize,
  413. "onSave": onEditChange,
  414. "onChange": onContentChange,
  415. "onCancel": onEditCancel,
  416. "onEnd": onEnd,
  417. "direction": direction.value,
  418. "component": props.component
  419. }, {
  420. enterIcon: slots.editableEnterIcon
  421. });
  422. }
  423. function renderOperations(forceRenderExpanded) {
  424. return [renderExpand(forceRenderExpanded), renderEdit(), renderCopy()].filter(node => node);
  425. }
  426. return () => {
  427. var _a;
  428. const {
  429. triggerType = ['icon']
  430. } = editable.value;
  431. const children = props.ellipsis || props.editable ? props.content !== undefined ? props.content : (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots) : slots.default ? slots.default() : props.content;
  432. if (editing.value) {
  433. return renderEditInput();
  434. }
  435. return _createVNode(LocaleReceiver, {
  436. "componentName": "Text",
  437. "children": locale => {
  438. const _a = _extends(_extends({}, props), attrs),
  439. {
  440. type,
  441. disabled,
  442. content,
  443. class: className,
  444. style
  445. } = _a,
  446. restProps = __rest(_a, ["type", "disabled", "content", "class", "style"]);
  447. const {
  448. rows,
  449. suffix,
  450. tooltip
  451. } = ellipsis.value;
  452. const {
  453. edit,
  454. copy: copyStr,
  455. copied,
  456. expand
  457. } = locale;
  458. state.editStr = edit;
  459. state.copyStr = copyStr;
  460. state.copiedStr = copied;
  461. state.expandStr = expand;
  462. const textProps = omit(restProps, ['prefixCls', 'editable', 'copyable', 'ellipsis', 'mark', 'code', 'delete', 'underline', 'strong', 'keyboard', 'onUpdate:content']);
  463. const cssEllipsis = canUseCSSEllipsis.value;
  464. const cssTextOverflow = rows === 1 && cssEllipsis;
  465. const cssLineClamp = rows && rows > 1 && cssEllipsis;
  466. let textNode = children;
  467. let ariaLabel;
  468. // Only use js ellipsis when css ellipsis not support
  469. if (rows && state.isEllipsis && !state.expanded && !cssEllipsis) {
  470. const {
  471. title
  472. } = restProps;
  473. let restContent = title || '';
  474. if (!title && (typeof children === 'string' || typeof children === 'number')) {
  475. restContent = String(children);
  476. }
  477. // show rest content as title on symbol
  478. restContent = restContent === null || restContent === void 0 ? void 0 : restContent.slice(String(state.ellipsisContent || '').length);
  479. // We move full content to outer element to avoid repeat read the content by accessibility
  480. textNode = _createVNode(_Fragment, null, [toRaw(state.ellipsisContent), _createVNode("span", {
  481. "title": restContent,
  482. "aria-hidden": "true"
  483. }, [ELLIPSIS_STR]), suffix]);
  484. } else {
  485. textNode = _createVNode(_Fragment, null, [children, suffix]);
  486. }
  487. textNode = wrapperDecorations(props, textNode);
  488. const showTooltip = tooltip && rows && state.isEllipsis && !state.expanded && !cssEllipsis;
  489. const title = slots.ellipsisTooltip ? slots.ellipsisTooltip() : tooltip;
  490. return _createVNode(ResizeObserver, {
  491. "onResize": resizeOnNextFrame,
  492. "disabled": !rows
  493. }, {
  494. default: () => [_createVNode(Typography, _objectSpread({
  495. "ref": contentRef,
  496. "class": [{
  497. [`${prefixCls.value}-${type}`]: type,
  498. [`${prefixCls.value}-disabled`]: disabled,
  499. [`${prefixCls.value}-ellipsis`]: rows,
  500. [`${prefixCls.value}-single-line`]: rows === 1 && !state.isEllipsis,
  501. [`${prefixCls.value}-ellipsis-single-line`]: cssTextOverflow,
  502. [`${prefixCls.value}-ellipsis-multiple-line`]: cssLineClamp
  503. }, className],
  504. "style": _extends(_extends({}, style), {
  505. WebkitLineClamp: cssLineClamp ? rows : undefined
  506. }),
  507. "aria-label": ariaLabel,
  508. "direction": direction.value,
  509. "onClick": triggerType.indexOf('text') !== -1 ? onEditClick : () => {}
  510. }, textProps), {
  511. default: () => [showTooltip ? _createVNode(Tooltip, {
  512. "title": tooltip === true ? children : title
  513. }, {
  514. default: () => [_createVNode("span", null, [textNode])]
  515. }) : textNode, renderOperations()]
  516. })]
  517. });
  518. }
  519. }, null);
  520. };
  521. }
  522. });
  523. export default Base;