();
+
+ const pxToNum = (value: string | null) => {
+ if (!value) return 0;
+ const match = value.match(/^\d*(\.\d*)?/);
+ return match ? Number(match[0]) : 0;
+ };
+
+ const calcEllipsised = () => {
+ const cloneContainer = () => {
+ if (!root.value) return;
+
+ const originStyle = window.getComputedStyle(root.value);
+ 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.zIndex = '-9999';
+ container.style.top = '-9999px';
+ container.style.height = 'auto';
+ container.style.minHeight = 'auto';
+ container.style.maxHeight = 'auto';
+
+ container.innerText = props.content;
+ document.body.appendChild(container);
+ return container;
+ };
+
+ const calcEllipsisText = (
+ container: HTMLDivElement,
+ maxHeight: number
+ ) => {
+ const { content, expandText } = props;
+ const dot = '...';
+ let left = 0;
+ let right = content.length;
+ let res = -1;
+
+ while (left <= right) {
+ const mid = Math.floor((left + right) / 2);
+ container.innerText = content.slice(0, mid) + dot + expandText;
+ if (container.offsetHeight <= maxHeight) {
+ left = mid + 1;
+ res = mid;
+ } else {
+ right = mid - 1;
+ }
+ }
+ return content.slice(0, res) + dot;
+ };
+
+ const container = cloneContainer();
+ if (!container) return;
+
+ const { paddingBottom, paddingTop, lineHeight } = container.style;
+ const maxHeight =
+ (Number(props.rows) + 0.5) * pxToNum(lineHeight) +
+ pxToNum(paddingTop) +
+ pxToNum(paddingBottom);
+ if (maxHeight < container.offsetHeight) {
+ hasAction.value = true;
+ text.value = calcEllipsisText(container, maxHeight);
+ } else {
+ hasAction.value = false;
+ text.value = props.content;
+ }
+
+ document.body.removeChild(container);
+ };
+
+ const onClickAction = (event: MouseEvent) => {
+ expanded.value = !expanded.value;
+ emit('clickAction', event);
+ };
+
+ const renderAction = () => (
+
+ {expanded.value ? props.collapseText : props.expandText}
+
+ );
+
+ onMounted(() => {
+ calcEllipsised();
+ });
+
+ watch(() => [props.content, props.rows], calcEllipsised);
+
+ useEventListener('resize', calcEllipsised);
+
+ return () => (
+
+ {expanded.value ? props.content : text.value}
+ {hasAction.value ? renderAction() : null}
+
+ );
+ },
+});
diff --git a/packages/vant/src/text-ellipsis/demo/index.vue b/packages/vant/src/text-ellipsis/demo/index.vue
new file mode 100644
index 000000000..89d8040c3
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/demo/index.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/vant/src/text-ellipsis/index.less b/packages/vant/src/text-ellipsis/index.less
new file mode 100644
index 000000000..9a5a8004f
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/index.less
@@ -0,0 +1,17 @@
+:root {
+ --van-text-ellipsis-action-color: var(--van-blue);
+}
+
+.van-text-ellipsis {
+ line-height: 1.5;
+ white-space: pre-wrap;
+
+ &__action {
+ cursor: pointer;
+ color: var(--van-text-ellipsis-action-color);
+
+ &:active {
+ opacity: var(--van-active-opacity);
+ }
+ }
+}
diff --git a/packages/vant/src/text-ellipsis/index.ts b/packages/vant/src/text-ellipsis/index.ts
new file mode 100644
index 000000000..f04b7e300
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/index.ts
@@ -0,0 +1,15 @@
+import { withInstall } from '../utils';
+import _TextEllipsis from './TextEllipsis';
+
+export const TextEllipsis = withInstall(_TextEllipsis);
+export default TextEllipsis;
+export { textEllipsisProps } from './TextEllipsis';
+
+export type { TextEllipsisProps } from './TextEllipsis';
+export type { TextEllipsisThemeVars } from './types';
+
+declare module 'vue' {
+ export interface GlobalComponents {
+ VanTextEllipsis: typeof TextEllipsis;
+ }
+}
diff --git a/packages/vant/src/text-ellipsis/test/__snapshots__/demo.spec.ts.snap b/packages/vant/src/text-ellipsis/test/__snapshots__/demo.spec.ts.snap
new file mode 100644
index 000000000..2e97af25f
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/test/__snapshots__/demo.spec.ts.snap
@@ -0,0 +1,27 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render demo and match snapshot 1`] = `
+
+
+ Vant is a lightweight, customizable mobile component library that was open sourced in 2017. Currently Vant officially provides Vue 2 version, Vue 3 version and WeChat applet version, and the community team maintains React version and Alipay applet version...
+
+
+
+
+
+
+ Vant is a lightweight, customizable mobile component library that was open sourced in 2017. Currently Vant officially provides Vue 2 version, Vue 3 version and WeChat applet version, and the community team maintains React version and Alipay applet version...
+
+ expand
+
+
+
+
+
+ Vant is a lightweight, customizable mobile component library that was open sourced in 2017. Currently Vant officially provides Vue 2 version, Vue 3 version and WeChat applet version, and the community team maintains React version and Alipay applet version...
+
+ expand
+
+
+
+`;
diff --git a/packages/vant/src/text-ellipsis/test/__snapshots__/index.spec.ts.snap b/packages/vant/src/text-ellipsis/test/__snapshots__/index.spec.ts.snap
new file mode 100644
index 000000000..269bbc73e
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/test/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render content correctly 1`] = `
+
+ Vant is a lightweight, customizable mobile component library th...
+
+
+
+`;
diff --git a/packages/vant/src/text-ellipsis/test/demo.spec.ts b/packages/vant/src/text-ellipsis/test/demo.spec.ts
new file mode 100644
index 000000000..c0e0c95b9
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/test/demo.spec.ts
@@ -0,0 +1,4 @@
+import Demo from '../demo/index.vue';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/packages/vant/src/text-ellipsis/test/index.spec.ts b/packages/vant/src/text-ellipsis/test/index.spec.ts
new file mode 100644
index 000000000..080d193ab
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/test/index.spec.ts
@@ -0,0 +1,91 @@
+import { mount } from '../../../test';
+import { nextTick } from 'vue';
+import TextEllipsis from '..';
+
+const originGetComputedStyle = window.getComputedStyle;
+
+const lineHeight = 20;
+
+const content =
+ 'Vant is a lightweight, customizable mobile component library that was open sourced in 2017. Currently Vant officially provides Vue 2 version, Vue 3 version and WeChat applet version, and the community team maintains React version and Alipay applet version.';
+
+beforeAll(() => {
+ window.getComputedStyle = (el) => {
+ const style = originGetComputedStyle(el);
+ style.lineHeight = `${lineHeight}px`;
+ return style;
+ };
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
+ get() {
+ if (this.innerText.includes('...')) {
+ const row = Math.ceil(
+ (this.innerText.replace(/\.\.\./g, '中').length / content.length) * 4
+ );
+ return lineHeight * row;
+ }
+ return lineHeight * 4;
+ },
+ });
+});
+
+afterAll(() => {
+ window.getComputedStyle = originGetComputedStyle;
+});
+
+test('should render content correctly', async () => {
+ const wrapper = mount(TextEllipsis, {
+ props: {
+ content,
+ },
+ });
+
+ await nextTick();
+ expect(wrapper.html()).toMatchSnapshot();
+});
+
+test('Expand and Collapse should be work', async () => {
+ const wrapper = mount(TextEllipsis, {
+ props: {
+ content,
+ expandText: 'expand',
+ collapseText: 'collapse',
+ },
+ });
+
+ await nextTick();
+ expect(wrapper.text()).toMatch('...');
+ await wrapper.find('.van-text-ellipsis__action').trigger('click');
+ expect(wrapper.text()).not.toMatch('...');
+});
+
+test('should emit click event after Expand/Collapse is clicked', async () => {
+ const wrapper = mount(TextEllipsis, {
+ props: {
+ content,
+ expandText: 'expand',
+ collapseText: 'collapse',
+ },
+ });
+
+ await nextTick();
+ await wrapper.find('.van-text-ellipsis__action').trigger('click');
+ expect(wrapper.emitted('click')).toHaveLength(1);
+});
+
+test('text not exceeded', async () => {
+ Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
+ value: lineHeight,
+ });
+
+ const shortContent = 'Vant is a component library';
+ const wrapper = mount(TextEllipsis, {
+ props: {
+ content: shortContent,
+ expandText: 'expand',
+ collapseText: 'collapse',
+ },
+ });
+
+ await nextTick();
+ expect(wrapper.text()).not.toMatch('...');
+});
diff --git a/packages/vant/src/text-ellipsis/types.ts b/packages/vant/src/text-ellipsis/types.ts
new file mode 100644
index 000000000..c5e2f5297
--- /dev/null
+++ b/packages/vant/src/text-ellipsis/types.ts
@@ -0,0 +1,3 @@
+export type TextEllipsisThemeVars = {
+ textEllipsisActionColor?: string;
+};
diff --git a/packages/vant/vant.config.mjs b/packages/vant/vant.config.mjs
index f283d7685..e834ef52d 100644
--- a/packages/vant/vant.config.mjs
+++ b/packages/vant/vant.config.mjs
@@ -340,6 +340,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'tag',
title: 'Tag 标签',
},
+ {
+ path: 'text-ellipsis',
+ title: 'TextEllipsis 文本省略',
+ },
],
},
{
@@ -482,7 +486,8 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
'en-US': {
title: 'Vant 4',
subtitle: ' (for Vue 3)',
- description: 'A lightweight, customizable Vue UI library for mobile web apps.',
+ description:
+ 'A lightweight, customizable Vue UI library for mobile web apps.',
logo: 'https://fastly.jsdelivr.net/npm/@vant/assets/logo.png',
langLabel: 'EN',
links: [
@@ -775,6 +780,10 @@ location.href = location.href.replace('youzan.github.io', 'vant-ui.github.io');
path: 'tag',
title: 'Tag',
},
+ {
+ path: 'text-ellipsis',
+ title: 'TextEllipsis',
+ },
],
},
{