diff --git a/packages/vant/src/text-ellipsis/README.md b/packages/vant/src/text-ellipsis/README.md new file mode 100644 index 000000000..639a0860e --- /dev/null +++ b/packages/vant/src/text-ellipsis/README.md @@ -0,0 +1,114 @@ +# TextEllipsis + +### Intro + +Show ellipsis for long text and support for Expand/Collapse. + +### Install + +Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways. + +```js +import { createApp } from 'vue'; +import { TextEllipsis } from 'vant'; + +const app = createApp(); +app.use(TextEllipsis); +``` + +## Usage + +### Basic Usage + +Show one rows by default. + +```html + +``` + +```js +export default { + setup() { + const text = + '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/Collapse + +Support Expand/Collapse. + +```html + +``` + +```js +export default { + setup() { + const text = + '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.'; + }, +}; +``` + +### Customize rows + +Display the number of `rows` by setting rows. + +```html + +``` + +```js +export default { + setup() { + const text = + '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.'; + }, +}; +``` + +## API + +### Props + +| Attribute | Description | Type | Default | +| ------------- | ------------------------ | ------------------ | ------- | +| rows | Number of rows displayed | _number \| string_ | `1` | +| content | The text displayed | _string_ | - | +| expand-text | Expand operation text | _string_ | - | +| collapse-text | Collapse operation text | _string_ | - | + +### Events + +| Event | Description | Arguments | +| ------------ | --------------------------------------- | ------------------- | +| click-action | Emitted when Expand/Collapse is clicked | _event: MouseEvent_ | + +### Types + +The component exports the following type definitions: + +```ts +import type { TextEllipsisProps, TextEllipsisThemeVars } from 'vant'; +``` + +## Theming + +### CSS Variables + +The component provides the following CSS variables, which can be used to customize styles. Please refer to [ConfigProvider component](#/en-US/config-provider). + +| Name | Default Value | Description | +| -------------------------------- | ----------------- | ----------- | +| --van-text-ellipsis-action-color | _var(--van-blue)_ | - | diff --git a/packages/vant/src/text-ellipsis/README.zh-CN.md b/packages/vant/src/text-ellipsis/README.zh-CN.md new file mode 100644 index 000000000..f0f09e4d5 --- /dev/null +++ b/packages/vant/src/text-ellipsis/README.zh-CN.md @@ -0,0 +1,110 @@ +# TextEllipsis 文本省略 + +### 介绍 + +对长文本进行省略,支持展开/收起。 + +### 引入 + +通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。 + +```js +import { createApp } from 'vue'; +import { TextEllipsis } from 'vant'; + +const app = createApp(); +app.use(TextEllipsis); +``` + +## 代码演示 + +### 基础用法 + +默认展示`1`行,超过`1`行显示省略号。 + +```html + +``` + +```js +export default { + setup() { + const text = + 'Vant 是一个轻量、可定制的移动端组件库,于 2017 年开源。目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。'; + }, +}; +``` + +### 展开/收起 + +超过行数支持展开/收起。 + +```html + +``` + +```js +export default { + setup() { + const text = + 'Vant 是一个轻量、可定制的移动端组件库,于 2017 年开源。目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。'; + }, +}; +``` + +### 自定义展示行数 + +通过设置 `rows` 限制展示行数。 + +```html + +``` + +```js +export default { + setup() { + const text = + 'Vant 是一个轻量、可定制的移动端组件库,于 2017 年开源。目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。'; + }, +}; +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| ------------- | -------------- | ------------------ | ------ | +| rows | 展示的行数 | _number \| string_ | `1` | +| content | 需要展示的文本 | _string_ | - | +| expand-text | 展开操作的文案 | _string_ | - | +| collapse-text | 收起操作的文案 | _string_ | - | + +### Events + +| 事件 | 说明 | 回调参数 | +| ------------ | ------------------- | ------------------- | +| click-action | 点击展开/收起时触发 | _event: MouseEvent_ | + +### 类型定义 + +组件导出以下类型定义: + +```ts +import type { TextEllipsisProps, TextEllipsisThemeVars } from 'vant'; +``` + +## 主题定制 + +### 样式变量 + +组件提供了下列 CSS 变量,可用于自定义样式,使用方法请参考 [ConfigProvider 组件](#/zh-CN/config-provider)。 + +| 名称 | 默认值 | 描述 | +| -------------------------------- | ----------------- | ---- | +| --van-text-ellipsis-action-color | _var(--van-blue)_ | - | diff --git a/packages/vant/src/text-ellipsis/TextEllipsis.tsx b/packages/vant/src/text-ellipsis/TextEllipsis.tsx new file mode 100644 index 000000000..3d88a9f00 --- /dev/null +++ b/packages/vant/src/text-ellipsis/TextEllipsis.tsx @@ -0,0 +1,138 @@ +import { + ref, + watch, + onMounted, + defineComponent, + type ExtractPropTypes, +} from 'vue'; + +// Composables +import { useEventListener } from '@vant/use'; + +// Utils +import { makeNumericProp, makeStringProp, createNamespace } from '../utils'; + +const [name, bem] = createNamespace('text-ellipsis'); + +export const textEllipsisProps = { + rows: makeNumericProp(1), + content: makeStringProp(''), + expandText: makeStringProp(''), + collapseText: makeStringProp(''), +}; + +export type TextEllipsisProps = ExtractPropTypes; + +export default defineComponent({ + name, + + inheritAttrs: false, + + props: textEllipsisProps, + + emits: ['clickAction'], + + setup(props, { emit }) { + const text = ref(''); + const expanded = ref(false); + const hasAction = ref(false); + const root = ref(); + + 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', + }, ], }, {