Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions site/docs/overview.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Component types will continue to be added according to business pra
spline: explain
---

<h3>Base<em class="tag">6</em></h3>
<h3>Base<em class="tag">7</em></h3>
<section class="image-group">
<div class="image-wrapper">
<a class="item" href="./components/button-en">
Expand Down Expand Up @@ -48,13 +48,13 @@ spline: explain
<p class="name">Link</p>
</a>
</div>
<!-- <div class="image-wrapper">
<div class="image-wrapper">
<a class="item" href="./components/typography-en">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-typography.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-typography-dark.png" />
<p class="name">Typography</p>
</a>
</div> -->
</div>
</section>

<h3>Navigation<em class="tag">8</em></h3>
Expand Down
6 changes: 3 additions & 3 deletions site/docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: 将根据业务实践持续新增组件类型,敬请留意组件
spline: explain
---

<h3>基础<em class="tag">6</em></h3>
<h3>基础<em class="tag">7</em></h3>
<section class="image-group">
<div class="image-wrapper">
<a class="item" href="./components/button">
Expand Down Expand Up @@ -48,13 +48,13 @@ spline: explain
<p class="name">Link 链接</p>
</a>
</div>
<!-- <div class="image-wrapper">
<div class="image-wrapper">
<a class="item" href="./components/typography">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-typography.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-typography-dark.png" />
<p class="name">Typography 排版</p>
</a>
</div> -->
</div>
</section>

<h3>导航<em class="tag">8</em></h3>
Expand Down
6 changes: 6 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,5 +422,11 @@ export default {
name: 'watermark',
component: () => import('tdesign-mobile-react/watermark/_example/index.tsx'),
},
{
title: 'Typography 排版',
titleEn: 'Typography',
name: 'typography',
component: () => import('tdesign-mobile-react/typography/_example/index.tsx'),
},
],
};
12 changes: 6 additions & 6 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ export const docs = [
component: () => import('tdesign-mobile-react/link/link.md'),
componentEn: () => import('tdesign-mobile-react/link/link.en-US.md'),
},
// {
// title: 'Typography 排版',
// name: 'typography',
// path: '/mobile-react/components/typography',
// component: () => import('tdesign-mobile-react/typography/typography.md'),
// },
{
title: 'Typography 排版',
name: 'typography',
path: '/mobile-react/components/typography',
component: () => import('tdesign-mobile-react/typography/typography.md'),
},
],
},
{
Expand Down
129 changes: 129 additions & 0 deletions src/_util/copy-to-clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
interface Options {
message?: string;
format?: string; // MIME type
onCopy?: (clipboardData: object) => void;
}

// inspired by https://github.com/sudodoki/toggle-selection, refactor to esm
const deselectCurrent = () => {
const selection = document.getSelection();
if (!selection.rangeCount) {
return () => {};
}
let active = document.activeElement as HTMLElement | null;

const ranges: Range[] = [];
for (let i = 0; i < selection.rangeCount; i++) {
ranges.push(selection.getRangeAt(i));
}

const tagName = active?.tagName?.toUpperCase(); // toUpperCase handles XHTML
switch (tagName) {
case 'INPUT':
case 'TEXTAREA':
(active as HTMLElement).blur();
break;

default:
active = null;
break;
}

selection.removeAllRanges();
return () => {
if (selection.type === 'Caret') {
selection.removeAllRanges();
}

if (!selection.rangeCount) {
ranges.forEach((range) => {
selection.addRange(range);
});
}

if (active) {
active.focus();
}
};
};

export const copy = (text: string, opts?: Options) => {
const options: Options = opts || {};
let reselectPrevious: (() => void) | undefined;
let range: Range | undefined;
let selection: Selection | null | undefined;
let mark: HTMLSpanElement | undefined;
let success = false;

try {
reselectPrevious = deselectCurrent();

range = document.createRange();
selection = document.getSelection();

mark = document.createElement('span');
mark.textContent = text;
// reset user styles for span element
mark.style.all = 'unset';
// prevents scrolling to the end of the page
mark.style.position = 'fixed';
mark.style.top = '0';
mark.style.clip = 'rect(0, 0, 0, 0)';
// used to preserve spaces and line breaks
mark.style.whiteSpace = 'pre';
// do not inherit user-select (it may be `none`)
mark.style.webkitUserSelect = 'text';
mark.style.userSelect = 'text';
mark.addEventListener('copy', (e: ClipboardEvent) => {
e.stopPropagation();
if (options.format) {
e.preventDefault();
e.clipboardData?.clearData();
e.clipboardData?.setData(options.format, text);
}
if (options.onCopy) {
e.preventDefault();
options.onCopy(e.clipboardData as object);
}
});

document.body.appendChild(mark);

range.selectNodeContents(mark);
selection?.addRange(range);

const successful = document.execCommand('copy');
if (!successful) {
throw new Error('copy command was unsuccessful');
}
success = true;
} catch {
try {
(window as any).clipboardData.setData(options.format || 'text', text);
if (options.onCopy) {
options.onCopy((window as any).clipboardData);
}
success = true;
} catch {
// 移动端不使用 prompt 兜底,静默失败
console.warn('Copy to clipboard failed');
}
} finally {
if (selection) {
if (typeof selection.removeRange === 'function') {
selection.removeRange(range as Range);
} else {
selection.removeAllRanges();
}
}

if (mark) {
document.body.removeChild(mark);
}
if (reselectPrevious) {
reselectPrevious();
}
}

return success;
};
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/**
* 基础组件(除icon,5个
* 基础组件(除icon,6个
*/
export * from './button';
export * from './divider';
export * from './fab';
export * from './layout';
export * from './link';
export * from './typography';

/**
* 导航(8个)
Expand Down
121 changes: 121 additions & 0 deletions src/typography/Ellipsis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useState, useMemo, useCallback, ReactNode, CSSProperties } from 'react';
import useConfig from '../hooks/useConfig';
import parseTNode from '../_util/parseTNode';

import type { TypographyEllipsis } from './type';

export interface EllipsisProps {
className?: string;
style?: CSSProperties;
children?: ReactNode;
ellipsis?: boolean | TypographyEllipsis;
renderCopy?: () => ReactNode;
}

const Ellipsis: React.FC<EllipsisProps> = (props) => {
const { className, style, children, ellipsis, renderCopy } = props;
const { classPrefix } = useConfig();
const prefixCls = `${classPrefix}-typography`;

const [isExpand, setIsExpand] = useState(false);

const ellipsisState = useMemo((): TypographyEllipsis => {
const defaults: TypographyEllipsis = { row: 1, expandable: false, collapsible: true };
if (typeof ellipsis === 'object') {
return { ...defaults, ...ellipsis };
}
return defaults;
}, [ellipsis]);

const ellipsisStyles = useMemo((): CSSProperties => {
if (!ellipsis) return {};

const base: CSSProperties = {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'normal',
display: '-webkit-box',
WebkitLineClamp: ellipsisState.row,
WebkitBoxOrient: 'vertical',
};

if (isExpand) {
base.overflow = 'visible';
base.whiteSpace = 'normal';
base.display = 'initial';
}

return base;
}, [ellipsis, ellipsisState.row, isExpand]);

const handleExpand = useCallback(() => {
setIsExpand(true);
if (typeof ellipsis === 'object') {
ellipsis.onExpand?.(true);
}
}, [ellipsis]);

const handleCollapse = useCallback(() => {
setIsExpand(false);
if (typeof ellipsis === 'object') {
ellipsis.onExpand?.(false);
}
}, [ellipsis]);

const renderSuffix = useCallback(
(expanded: boolean): ReactNode => {
const { suffix } = ellipsisState;
if (!suffix) return null;
return parseTNode(suffix, { expanded });
},
[ellipsisState],
);

const renderEllipsisExpand = () => {
const { suffix, expandable, collapsible } = ellipsisState;

const symbolStyle: CSSProperties = {
textDecoration: 'none',
whiteSpace: 'nowrap',
flex: 'none',
marginRight: renderCopy ? 8 : 0,
};

if (!isExpand && expandable) {
return (
<span className={`${prefixCls}-ellipsis-symbol`} onClick={handleExpand} style={symbolStyle}>
{suffix ? renderSuffix(false) : '展开'}
</span>
);
}

if (expandable && isExpand && collapsible) {
return (
<span className={`${prefixCls}-ellipsis-symbol`} onClick={handleCollapse} style={symbolStyle}>
{suffix ? renderSuffix(true) : '收起'}
</span>
);
}

return null;
};

return (
<div
className={className}
style={{
display: 'flex',
alignItems: 'flex-end',
...style,
}}
>
<p style={ellipsis ? ellipsisStyles : {}}>{children}</p>
{renderEllipsisExpand()}
{renderCopy?.()}
</div>
);
};

Ellipsis.displayName = 'Ellipsis';

export default Ellipsis;
39 changes: 39 additions & 0 deletions src/typography/Paragraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { forwardRef, ReactNode } from 'react';
import classNames from 'classnames';
import useConfig from '../hooks/useConfig';
import useDefaultProps from '../hooks/useDefaultProps';
import { paragraphDefaultProps } from './defaultProps';
import Ellipsis from './Ellipsis';

import type { StyledProps } from '../common';
import type { TdParagraphProps } from './type';

export interface ParagraphProps extends TdParagraphProps, StyledProps {}

const Paragraph = forwardRef<HTMLParagraphElement, ParagraphProps>((originalProps, ref) => {
const props = useDefaultProps<ParagraphProps>(originalProps, paragraphDefaultProps);
const { classPrefix } = useConfig();
const prefixCls = `${classPrefix}-typography`;

const { className, style, children, content, ellipsis } = props;

const renderContent = (children ?? content) as ReactNode;

if (ellipsis) {
return (
<Ellipsis className={classNames(prefixCls, className)} style={style} ellipsis={ellipsis}>
{renderContent}
</Ellipsis>
);
}

return (
<div className={classNames(prefixCls, className)} style={style} ref={ref}>
{renderContent}
</div>
);
});

Paragraph.displayName = 'Paragraph';

export default Paragraph;
Loading
Loading