Skip to content

Commit

Permalink
feat: add ellipsis
Browse files Browse the repository at this point in the history
  • Loading branch information
taoyage committed Aug 3, 2022
1 parent 70b4cac commit 073b991
Show file tree
Hide file tree
Showing 9 changed files with 260 additions and 2 deletions.
2 changes: 1 addition & 1 deletion demos/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
border: 1px solid #dce1e5;
height: 100%;
overflow: hidden;
overflow-y: scroll;
overflow-y: auto;
display: flex;
flex-direction: column;
}
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 145 additions & 0 deletions packages/ellipsis/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';
import useResizeObserver from '@/hooks/useResizeObserver';
import { pxToNumber } from '@/ellipsis/utils';

import './styles/index.scss';

export interface EllipsisProps {
text: string;
rows?: number;
/** 收起 */
collapse?: React.ReactNode;
/** 展开 */
expand?: React.ReactNode;
}

const classPrefix = 'ygm-ellipsis';

const ellipsisTailing = '...';

const Ellipsis: React.FC<EllipsisProps> = (props) => {
const [exceeded, setExceeded] = React.useState(false);
const [expanded, setExpanded] = React.useState(false);
const [ellipsised, setEllipsised] = React.useState('');
const containerRef = React.useRef<HTMLDivElement>(null);

const calcEllipsised = React.useCallback(() => {
const element = containerRef.current;
if (!element) return;

const originStyle = window.getComputedStyle(element);
const container = document.createElement('div');

const styleNames: string[] = Array.prototype.slice.apply(originStyle);
styleNames.forEach((name) => {
container.style.setProperty(name, originStyle.getPropertyValue(name));
});

container.style.position = 'fixed';
container.style.height = 'auto';
container.style.visibility = 'hidden';

container.innerText = props.text;

document.body.appendChild(container);

const lineHeight = pxToNumber(originStyle.lineHeight);
const maxHeight = lineHeight * props.rows!;
const height = container.getBoundingClientRect().height;

const check = (left: number, right: number) => {
let l = left;
let r = right;
let text = '';

while (l < r) {
const m = Math.floor((l + r) / 2);
if (l === m) {
break;
}

const tempText = props.text.slice(l, m);
container.innerText = `${text}${tempText}...${props.expand}`;
const height = container.getBoundingClientRect().height;

if (height > maxHeight) {
r = m;
} else {
text += tempText;
l = m;
}
}

return text;
};

if (maxHeight >= height) {
setExceeded(false);
} else {
setExceeded(true);
const end = props.text.length;
const ellipsisedValue = check(0, end);
setEllipsised(ellipsisedValue);
}
document.body.removeChild(container);
}, [props.expand, props.rows, props.text]);

useIsomorphicLayoutEffect(() => {
calcEllipsised();
}, [calcEllipsised]);

useResizeObserver(calcEllipsised, containerRef);

const renderContent = () => {
if (!exceeded) {
return props.text;
}
if (expanded) {
return (
<>
{props.text}
{props.collapse && <a>{props.collapse}</a>}
</>
);
} else {
return (
<>
{ellipsised}
{ellipsisTailing}
{props.expand && <a>{props.expand}</a>}
</>
);
}
};

const onContent = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();

if (!props.expand && !props.collapse) return;

if (props.expand && !props.collapse) {
setExpanded(true);
return;
}

setExpanded(!expanded);
};

return (
<div className={classPrefix} ref={containerRef} onClick={onContent}>
{renderContent()}
</div>
);
};

Ellipsis.defaultProps = {
text: '',
rows: 1,
expand: '',
collapse: '',
};

Ellipsis.displayName = 'Ellipsis';

export default Ellipsis;
8 changes: 8 additions & 0 deletions packages/ellipsis/styles/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
$class-prefix-ellipsis: 'ygm-ellipsis';

.#{$class-prefix-ellipsis} {
overflow: hidden;
word-break: break-all;
line-height: 1.5;
cursor: pointer;
}
4 changes: 4 additions & 0 deletions packages/ellipsis/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const pxToNumber = (value: string) => {
const num = parseFloat(value);
return isNaN(num) ? 0 : num;
};
15 changes: 15 additions & 0 deletions packages/hooks/useMemoizedFn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';

import useLatest from '@/hooks/useLatest';

const useMemoizedFn = (fn: (...args: unknown[]) => void) => {
const latestfnRef = useLatest(fn);

const memoizedFn = React.useRef((...args: unknown[]) => {
latestfnRef.current?.(...args);
});

return memoizedFn.current;
};

export default useMemoizedFn;
25 changes: 25 additions & 0 deletions packages/hooks/useResizeObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

import useIsomorphicLayoutEffect from '@/hooks/useIsomorphicLayoutEffect';

const useResizeObserver = <T extends HTMLElement>(callback: (target: T) => void, targetRef: React.RefObject<T>) => {
useIsomorphicLayoutEffect(() => {
const element = targetRef.current;
if (!element) return;

if (window.ResizeObserver) {
const observer = new ResizeObserver(() => {
callback(element);
});
observer.observe(element);
return () => {
observer.disconnect();
};
}
callback(element);

return () => null;
}, []);
};

export default useResizeObserver;
13 changes: 13 additions & 0 deletions packages/styles/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,16 @@ body {
font-size: var(--ygm-font-size-m);
font-family: var(--ygm-font-family);
}

a,
button {
cursor: pointer;
}

a {
color: var(--ygm-color-primary);
transition: opacity ease-in-out 0.2s;
}
a:active {
opacity: 0.8;
}
48 changes: 48 additions & 0 deletions stories/ellipsis/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { Meta } from '@storybook/react';

import Ellipsis from '@/ellipsis';

import DemoWrap from '../../demos/demo-wrap';
import DemoBlock from '../../demos/demo-block';

const EllipsisStory: Meta = {
title: '信息展示/Ellipsis 文本省略',
component: Ellipsis,
};

export const Basic = () => {
return (
<DemoWrap>
<DemoBlock title="一行省略" style={{ padding: 20 }}>
<Ellipsis text="React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes" />
</DemoBlock>

<DemoBlock title="多行省略" style={{ padding: 20 }}>
<Ellipsis
rows={3}
text="React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes"
/>
</DemoBlock>

<DemoBlock title="展开和收起" style={{ padding: 20 }}>
<Ellipsis
expand="展开"
collapse="收起"
text="React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes"
/>
</DemoBlock>

<DemoBlock title="仅展开" style={{ padding: 20 }}>
<Ellipsis
expand="展开"
text="React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes"
/>
</DemoBlock>
</DemoWrap>
);
};

Basic.storyName = '基本用法';

export default EllipsisStory;

0 comments on commit 073b991

Please sign in to comment.