diff --git a/.github/workflows/typos-config.toml b/.github/workflows/typos-config.toml index 0fff42a2c..f246a9260 100644 --- a/.github/workflows/typos-config.toml +++ b/.github/workflows/typos-config.toml @@ -8,4 +8,4 @@ ded = "ded" Hel = "Hel" [files] -extend-exclude = ["CHANGELOG.md"] +extend-exclude = ["CHANGELOG.md", "*.snap"] diff --git a/site/docs.config.js b/site/docs.config.js index 6a9c8192e..6c9ec1e8a 100644 --- a/site/docs.config.js +++ b/site/docs.config.js @@ -495,6 +495,14 @@ export const docs = [ component: () => import('@/dialog/dialog.md'), componentEn: () => import('@/dialog/dialog.en-US.md'), }, + { + title: 'Guide 引导', + titleEn: 'Guide', + name: 'guide', + path: '/mobile-vue/components/guide', + component: () => import('@/guide/guide.md'), + componentEn: () => import('@/guide/guide.en-US.md'), + }, { title: 'Popover 弹出气泡', titleEn: 'Popover', diff --git a/src/_common b/src/_common index b3d58500b..6e7dece66 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit b3d58500b007f2c5d75742274e29acf2b0e2def1 +Subproject commit 6e7dece66605fc33704bbc98ef62b7dd4cc243a1 diff --git a/src/common.ts b/src/common.ts index 492cad08a..345da9b0e 100644 --- a/src/common.ts +++ b/src/common.ts @@ -51,7 +51,7 @@ export type OptionData = { } & PlainObject; export type TreeOptionData = { - children?: Array>; + children?: Array> | boolean; /** option label content */ label?: string | TNode; /** option search text */ @@ -68,6 +68,8 @@ export type HorizontalAlignEnum = 'left' | 'center' | 'right'; export type VerticalAlignEnum = 'top' | 'middle' | 'bottom'; +export type LayoutEnum = 'vertical' | 'horizontal'; + export type ClassName = { [className: string]: any } | ClassName[] | string; export type CSSSelector = string; @@ -119,7 +121,7 @@ export type InfinityScroll = TScroll; export interface ScrollToElementParams { /** 跳转元素下标 */ - index: number; + index?: number; /** 跳转元素距离顶部的距离 */ top?: number; /** 单个元素高度非固定场景下,即 isFixedRowHeight = false。延迟设置元素位置,一般用于依赖不同高度异步渲染等场景,单位:毫秒 */ @@ -128,5 +130,5 @@ export interface ScrollToElementParams { } export interface ComponentScrollToElementParams extends ScrollToElementParams { - key: string | number; + key?: string | number; } diff --git a/src/components.ts b/src/components.ts index 176a49002..1c4191263 100644 --- a/src/components.ts +++ b/src/components.ts @@ -56,6 +56,7 @@ export { default as Icon } from './icon'; export { default as Table } from './table'; export { SideBar, SideBarItem } from './side-bar'; export { default as TPopover } from './popover'; +export { default as Guide } from './guide'; // 全局配置 export * from './config-provider'; diff --git a/src/config-provider/type.ts b/src/config-provider/type.ts index 4e55e1142..2e0c4cb8d 100644 --- a/src/config-provider/type.ts +++ b/src/config-provider/type.ts @@ -57,6 +57,10 @@ export interface GlobalConfigProvider { * 上传组件全局配置 */ upload?: UploadConfig; + /** + * 引导组件全局配置 + * */ + guide?: GuideConfig; } export interface ActionSheetConfig { @@ -195,3 +199,14 @@ export interface UploadConfigProgress { */ waitingText?: string; } + +export interface GuideConfig { + /** 语言配置, “下一步” 描述文本 */ + next?: string; + /** 语言配置, “跳过” 描述文本 */ + skip?: string; + /** 语言配置, “完成” 描述文本 */ + finish?: string; + /** 语言配置, “返回” 描述文本 */ + back?: string; +} diff --git a/src/guide/demos/base.vue b/src/guide/demos/base.vue new file mode 100644 index 000000000..8c326a6e3 --- /dev/null +++ b/src/guide/demos/base.vue @@ -0,0 +1,127 @@ + + + + diff --git a/src/guide/demos/custom-popover.vue b/src/guide/demos/custom-popover.vue new file mode 100644 index 000000000..72094d8c7 --- /dev/null +++ b/src/guide/demos/custom-popover.vue @@ -0,0 +1,133 @@ + + + + diff --git a/src/guide/demos/dialog-body.vue b/src/guide/demos/dialog-body.vue new file mode 100644 index 000000000..42cb901c6 --- /dev/null +++ b/src/guide/demos/dialog-body.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/guide/demos/dialog.vue b/src/guide/demos/dialog.vue new file mode 100644 index 000000000..78046892a --- /dev/null +++ b/src/guide/demos/dialog.vue @@ -0,0 +1,129 @@ + + + + diff --git a/src/guide/demos/mobile.vue b/src/guide/demos/mobile.vue new file mode 100644 index 000000000..a1c5685e2 --- /dev/null +++ b/src/guide/demos/mobile.vue @@ -0,0 +1,34 @@ + + + + diff --git a/src/guide/demos/my-popover.vue b/src/guide/demos/my-popover.vue new file mode 100644 index 000000000..1e77a9e6f --- /dev/null +++ b/src/guide/demos/my-popover.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/guide/demos/no-mask.vue b/src/guide/demos/no-mask.vue new file mode 100644 index 000000000..d8d63c201 --- /dev/null +++ b/src/guide/demos/no-mask.vue @@ -0,0 +1,127 @@ + + + + diff --git a/src/guide/demos/popover-dialog.vue b/src/guide/demos/popover-dialog.vue new file mode 100644 index 000000000..129af39bb --- /dev/null +++ b/src/guide/demos/popover-dialog.vue @@ -0,0 +1,127 @@ + + + + diff --git a/src/guide/guide.en-US.md b/src/guide/guide.en-US.md new file mode 100644 index 000000000..2edf24f59 --- /dev/null +++ b/src/guide/guide.en-US.md @@ -0,0 +1,55 @@ +:: BASE_DOC :: + +## API +### Guide Props + +name | type | default | description | required +-- | -- | -- | -- | -- +backButtonProps | Object | - | Typescript:`ButtonProps` | N +counter | Slot / Function | - | Typescript:`TNode<{ current: number; total: number }>`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +current | Number | - | `v-model` and `v-model:current` is supported | N +defaultCurrent | Number | - | uncontrolled property | N +finishButtonProps | Object | - | Typescript:`ButtonProps` | N +hideCounter | Boolean | false | \- | N +hideSkip | Boolean | false | \- | N +highlightPadding | Number | 8 | \- | N +mode | String | popover | options: popover/dialog | N +nextButtonProps | Object | - | Typescript:`ButtonProps`,[Button API Documents](./button?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | - | Typescript:`ButtonProps` | N +steps | Array | - | Typescript:`Array` | N +zIndex | Number | 999999 | \- | N +onBack | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N +onChange | Function | | Typescript:`(current: number, context?: { e: MouseEvent, total: number }) => void`
| N +onFinish | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N +onNextStepClick | Function | | Typescript:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
| N +onSkip | Function | | Typescript:`(context: { e: MouseEvent, current: number, total: number }) => void`
| N + +### Guide Events + +name | params | description +-- | -- | -- +back | `(context: { e: MouseEvent, current: number, total: number })` | \- +change | `(current: number, context?: { e: MouseEvent, total: number })` | \- +finish | `(context: { e: MouseEvent, current: number, total: number })` | \- +next-step-click | `(context: { e: MouseEvent, next: number, current: number, total: number })` | \- +skip | `(context: { e: MouseEvent, current: number, total: number })` | \- + +### GuideStep + +name | type | default | description | required +-- | -- | -- | -- | -- +backButtonProps | Object | - | Typescript:`ButtonProps` | N +body | String / Slot / Function | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +content | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +element | String / Function | - | required。Typescript:`AttachNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | Typescript:`TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +highlightPadding | Number | - | \- | N +mode | String | - | options: popover/dialog | N +nextButtonProps | Object | - | Typescript:`ButtonProps` | N +offset | Array | - | this api is in discussing. do not use it.。Typescript:`Array` | N +placement | String | 'top' | Typescript:`StepPopoverPlacement ` `type StepPopoverPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'\|'center'`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +popoverProps | Object | - | Popover component props if `mode = popover`。Typescript:`PopoverProps`,[Popover API Documents](./popover?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | \- | N +skipButtonProps | Object | - | Typescript:`ButtonProps` | N +title | String / Slot / Function | - | title of current step。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N diff --git a/src/guide/guide.md b/src/guide/guide.md new file mode 100644 index 000000000..ddcf279ed --- /dev/null +++ b/src/guide/guide.md @@ -0,0 +1,55 @@ +:: BASE_DOC :: + +## API +### Guide Props + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +backButtonProps | Object | - | 透传 返回 的全部属性,示例:`{ content: '返回', theme: 'default' }`。TS 类型:`ButtonProps` | N +counter | Slot / Function | - | 用于自定义渲染计数部分。TS 类型:`TNode<{ current: number; total: number }>`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +current | Number | - | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景。支持语法糖 `v-model` 或 `v-model:current` | N +defaultCurrent | Number | - | 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景。非受控属性 | N +finishButtonProps | Object | - | 透传 完成 的全部属性,示例:`{ content: '完成', theme: 'primary' }`。TS 类型:`ButtonProps` | N +hideCounter | Boolean | false | 是否隐藏计数 | N +hideSkip | Boolean | false | 是否隐藏跳过按钮 | N +highlightPadding | Number | 8 | 高亮框的内边距 | N +mode | String | popover | 引导框的类型。可选项:popover/dialog | N +nextButtonProps | Object | - | 透传 下一步按钮 的全部属性,示例:{ content: '下一步', theme: 'primary' }。TS 类型:`ButtonProps`,[Button API Documents](./button?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | - | 透传 跳过按钮 的全部属性,{ content: '跳过', theme: 'default' }。TS 类型:`ButtonProps` | N +steps | Array | - | 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。。TS 类型:`Array` | N +zIndex | Number | 999999 | 提示框的层级 | N +onBack | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击返回按钮时触发 | N +onChange | Function | | TS 类型:`(current: number, context?: { e: MouseEvent, total: number }) => void`
当前步骤发生变化时触发 | N +onFinish | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击完成按钮时触发 | N +onNextStepClick | Function | | TS 类型:`(context: { e: MouseEvent, next: number, current: number, total: number }) => void`
点击下一步时触发 | N +onSkip | Function | | TS 类型:`(context: { e: MouseEvent, current: number, total: number }) => void`
点击跳过按钮时触发 | N + +### Guide Events + +名称 | 参数 | 描述 +-- | -- | -- +back | `(context: { e: MouseEvent, current: number, total: number })` | 点击返回按钮时触发 +change | `(current: number, context?: { e: MouseEvent, total: number })` | 当前步骤发生变化时触发 +finish | `(context: { e: MouseEvent, current: number, total: number })` | 点击完成按钮时触发 +next-step-click | `(context: { e: MouseEvent, next: number, current: number, total: number })` | 点击下一步时触发 +skip | `(context: { e: MouseEvent, current: number, total: number })` | 点击跳过按钮时触发 + +### GuideStep + +名称 | 类型 | 默认值 | 说明 | 必传 +-- | -- | -- | -- | -- +backButtonProps | Object | - | 用于自定义当前引导框的返回按钮的内容。TS 类型:`ButtonProps` | N +body | String / Slot / Function | - | 当前步骤提示框的内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +content | Slot / Function | - | 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +element | String / Function | - | 必需。高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign')。TS 类型:`AttachNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | Y +highlightContent | Slot / Function | - | 用户自定义的高亮框 (仅当 `mode` 为 `popover` 时生效)。TS 类型:`TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N +highlightPadding | Number | - | 高亮框的内边距 | N +mode | String | - | 引导框的类型。可选项:popover/dialog | N +nextButtonProps | Object | - | 用于自定义当前引导框的下一步按钮的内容。TS 类型:`ButtonProps` | N +offset | Array | - | 【讨论确认中】相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px']。TS 类型:`Array` | N +placement | String | 'top' | 引导框相对于高亮元素出现的位置,(仅当 `mode` 为 `popover` 时生效)。TS 类型:`StepPopoverPlacement ` `type StepPopoverPlacement = 'top'\|'left'\|'right'\|'bottom'\|'top-left'\|'top-right'\|'bottom-left'\|'bottom-right'\|'left-top'\|'left-bottom'\|'right-top'\|'right-bottom'\|'center'`。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +popoverProps | Object | - | 透传全部属性到 Popover 组件。`mode=popover` 时有效。TS 类型:`PopoverProps`,[Popover API Documents](./popover?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-vue/tree/develop/src/guide/type.ts) | N +showOverlay | Boolean | true | 是否出现遮罩层 | N +skipButtonProps | Object | - | 用于自定义当前步骤引导框的跳过按钮的内容。TS 类型:`ButtonProps` | N +title | String / Slot / Function | - | 当前步骤的标题内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-vue/blob/develop/src/common.ts) | N diff --git a/src/guide/guide.vue b/src/guide/guide.vue new file mode 100644 index 000000000..90c8f0539 --- /dev/null +++ b/src/guide/guide.vue @@ -0,0 +1,506 @@ + + + diff --git a/src/guide/index.ts b/src/guide/index.ts new file mode 100644 index 000000000..61e1c1c8f --- /dev/null +++ b/src/guide/index.ts @@ -0,0 +1,11 @@ +import Guide from './guide.vue'; +import { withInstall, WithInstallType } from '../shared'; +import { TdGuideProps, GuideStep } from './type'; + +import './style'; + +export * from './type'; +export type GuideProps = TdGuideProps; + +const _Guide: WithInstallType = withInstall(Guide); +export default _Guide; diff --git a/src/guide/interface.ts b/src/guide/interface.ts new file mode 100644 index 000000000..a53eff0fe --- /dev/null +++ b/src/guide/interface.ts @@ -0,0 +1,15 @@ +import { GuideStep } from './type'; + +export type GuideCrossProps = Pick< + GuideStep, + 'mode' | 'skipButtonProps' | 'nextButtonProps' | 'backButtonProps' | 'showOverlay' | 'highlightPadding' +>; + +export interface ContentProps { + handleNext: (e: MouseEvent) => void; + handleSkip: (e: MouseEvent) => void; + handleBack: (e: MouseEvent) => void; + handleFinish: (e: MouseEvent) => void; + current: number; + total: number; +} diff --git a/src/guide/props.ts b/src/guide/props.ts new file mode 100644 index 000000000..9ec3e7c16 --- /dev/null +++ b/src/guide/props.ts @@ -0,0 +1,86 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdGuideProps } from './type'; +import { PropType } from 'vue'; + +export default { + /** 透传 返回 的全部属性,示例:`{ content: '返回', theme: 'default' }` */ + backButtonProps: { + type: Object as PropType, + }, + /** 用于自定义渲染计数部分 */ + counter: { + type: Function as PropType, + }, + /** 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 */ + current: { + type: Number, + default: undefined, + }, + modelValue: { + type: Number, + default: undefined, + }, + /** 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景,非受控属性 */ + defaultCurrent: { + type: Number, + }, + /** 透传 完成 的全部属性,示例:`{ content: '完成', theme: 'primary' }` */ + finishButtonProps: { + type: Object as PropType, + }, + /** 是否隐藏计数 */ + hideCounter: Boolean, + /** 是否隐藏跳过按钮 */ + hideSkip: Boolean, + /** 高亮框的内边距 */ + highlightPadding: { + type: Number, + default: 8, + }, + /** 引导框的类型 */ + mode: { + type: String as PropType, + default: 'popover' as TdGuideProps['mode'], + validator(val: TdGuideProps['mode']): boolean { + if (!val) return true; + return ['popover', 'dialog'].includes(val); + }, + }, + /** 透传 下一步按钮 的全部属性,示例:{ content: '下一步', theme: 'primary' } */ + nextButtonProps: { + type: Object as PropType, + }, + /** 是否出现遮罩层 */ + showOverlay: { + type: Boolean, + default: true, + }, + /** 透传 跳过按钮 的全部属性,{ content: '跳过', theme: 'default' } */ + skipButtonProps: { + type: Object as PropType, + }, + /** 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。 */ + steps: { + type: Array as PropType, + }, + /** 提示框的层级 */ + zIndex: { + type: Number, + default: 999999, + }, + /** 点击返回按钮时触发 */ + onBack: Function as PropType, + /** 当前步骤发生变化时触发 */ + onChange: Function as PropType, + /** 点击完成按钮时触发 */ + onFinish: Function as PropType, + /** 点击下一步时触发 */ + onNextStepClick: Function as PropType, + /** 点击跳过按钮时触发 */ + onSkip: Function as PropType, +}; diff --git a/src/guide/style/css.js b/src/guide/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/guide/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/guide/style/index.js b/src/guide/style/index.js new file mode 100644 index 000000000..e6425e1f4 --- /dev/null +++ b/src/guide/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/mobile/components/guide/_index.less'; diff --git a/src/guide/type.ts b/src/guide/type.ts new file mode 100644 index 000000000..dd29efeb7 --- /dev/null +++ b/src/guide/type.ts @@ -0,0 +1,174 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { ButtonProps } from '../button'; +import { PopoverProps } from '../popover'; +import { TNode, AttachNode } from '../common'; + +export interface TdGuideProps { + /** + * 透传 返回 的全部属性,示例:`{ content: '返回', theme: 'default' }` + */ + backButtonProps?: ButtonProps; + /** + * 用于自定义渲染计数部分 + */ + counter?: TNode<{ current: number; total: number }>; + /** + * 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + current?: number; + /** + * 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景,非受控属性 + */ + defaultCurrent?: number; + /** + * 当前步骤,即整个引导的进度。-1 则不展示,用于需要中断展示的场景 + */ + modelValue?: number; + /** + * 透传 完成 的全部属性,示例:`{ content: '完成', theme: 'primary' }` + */ + finishButtonProps?: ButtonProps; + /** + * 是否隐藏计数 + * @default false + */ + hideCounter?: boolean; + /** + * 是否隐藏跳过按钮 + * @default false + */ + hideSkip?: boolean; + /** + * 高亮框的内边距 + * @default 8 + */ + highlightPadding?: number; + /** + * 引导框的类型 + * @default popover + */ + mode?: 'popover' | 'dialog'; + /** + * 透传 下一步按钮 的全部属性,示例:{ content: '下一步', theme: 'primary' } + */ + nextButtonProps?: ButtonProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 透传 跳过按钮 的全部属性,{ content: '跳过', theme: 'default' } + */ + skipButtonProps?: ButtonProps; + /** + * 用于定义每个步骤的内容,包括高亮的节点、相对位置和具体的文案内容等。 + */ + steps?: Array; + /** + * 提示框的层级 + * @default 999999 + */ + zIndex?: number; + /** + * 点击返回按钮时触发 + */ + onBack?: (context: { e: MouseEvent; current: number; total: number }) => void; + /** + * 当前步骤发生变化时触发 + */ + onChange?: (current: number, context?: { e: MouseEvent; total: number }) => void; + /** + * 点击完成按钮时触发 + */ + onFinish?: (context: { e: MouseEvent; current: number; total: number }) => void; + /** + * 点击下一步时触发 + */ + onNextStepClick?: (context: { e: MouseEvent; next: number; current: number; total: number }) => void; + /** + * 点击跳过按钮时触发 + */ + onSkip?: (context: { e: MouseEvent; current: number; total: number }) => void; +} + +export interface GuideStep { + /** + * 用于自定义当前引导框的返回按钮的内容 + */ + backButtonProps?: ButtonProps; + /** + * 当前步骤提示框的内容 + */ + body?: string | TNode; + /** + * 用户自定义引导弹框的内容,一旦存在,此时除 `placement`、`offset`和`element` 外,其它属性全部失效) + */ + content?: TNode; + /** + * 高亮的节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'#tdesign' 或 () => document.querySelector('#tdesign') + */ + element: AttachNode; + /** + * 用户自定义的高亮框 (仅当 `mode` 为 `popover` 时生效) + */ + highlightContent?: TNode; + /** + * 高亮框的内边距 + */ + highlightPadding?: number; + /** + * 引导框的类型 + */ + mode?: 'popover' | 'dialog'; + /** + * 用于自定义当前引导框的下一步按钮的内容 + */ + nextButtonProps?: ButtonProps; + /** + * 【讨论确认中】相对于 placement 的偏移量,示例:[-10, 20] 或 ['10px', '8px'] + */ + offset?: Array; + /** + * 引导框相对于高亮元素出现的位置,(仅当 `mode` 为 `popover` 时生效) + * @default 'top' + */ + placement?: StepPopoverPlacement; + /** + * 透传全部属性到 Popover 组件。`mode=popover` 时有效 + */ + popoverProps?: PopoverProps; + /** + * 是否出现遮罩层 + * @default true + */ + showOverlay?: boolean; + /** + * 用于自定义当前步骤引导框的跳过按钮的内容 + */ + skipButtonProps?: ButtonProps; + /** + * 当前步骤的标题内容 + */ + title?: string | TNode; +} + +export type StepPopoverPlacement = + | 'top' + | 'left' + | 'right' + | 'bottom' + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right' + | 'left-top' + | 'left-bottom' + | 'right-top' + | 'right-bottom' + | 'center'; diff --git a/src/guide/utils/dom.ts b/src/guide/utils/dom.ts new file mode 100644 index 000000000..c1a8bca17 --- /dev/null +++ b/src/guide/utils/dom.ts @@ -0,0 +1,136 @@ +import isString from 'lodash/isString'; +import isFunction from 'lodash/isFunction'; +import { AttachNode } from '@/common'; +import { elementInViewport, getWindowScroll, getWindowSize } from '../../shared/dom'; +/** + * 获取元素某个 css 对应的值 + * @param element 元素 + * @param propName css 名 + * @returns string + */ +export function getElmCssPropValue(element: HTMLElement, propName: string): string { + let propValue = ''; + + if (document.defaultView && document.defaultView.getComputedStyle) { + propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); + } + + if (propValue && propValue.toLowerCase) { + return propValue.toLowerCase(); + } + + return propValue; +} + +/** + * 判断元素是否处在 position fixed 中 + * @param element 元素 + * @returns boolean + */ +export function isFixed(element: HTMLElement): boolean { + const p = element.parentNode as HTMLElement; + + if (!p || p.nodeName === 'HTML') { + return false; + } + + if (getElmCssPropValue(element, 'position') === 'fixed') { + return true; + } + + return isFixed(p); +} + +/** + * 获取元素相对于另一个元素的位置(或者说相对于 body) + * 感谢 `meouw`: http://stackoverflow.com/a/442474/375966 + */ +export function getRelativePosition(elm: HTMLElement, relativeElm: HTMLElement = document.body) { + const { scrollTop, scrollLeft } = getWindowScroll(); + const { top: elmTop, left: elmLeft } = elm.getBoundingClientRect(); + const { top: relElmTop, left: relElmLeft } = relativeElm.getBoundingClientRect(); + const relativeElmPosition = getElmCssPropValue(relativeElm, 'position'); + + if ( + (relativeElm.tagName.toLowerCase() !== 'body' && relativeElmPosition === 'relative') || + relativeElmPosition === 'sticky' + ) { + return { + top: elmTop - relElmTop, + left: elmLeft - relElmLeft, + }; + } + + if (isFixed(elm)) { + return { + top: elmTop, + left: elmLeft, + }; + } + + return { + top: elmTop + scrollTop, + left: elmLeft + scrollLeft, + }; +} + +export function getTargetElm(elm: AttachNode): HTMLElement { + if (elm) { + let targetElement: HTMLElement = null; + if (isString(elm)) { + targetElement = document.querySelector(elm); + } else if (isFunction(elm)) { + targetElement = elm() as HTMLElement; + } else { + throw new Error('elm should be string or function'); + } + if (targetElement) { + return targetElement as HTMLElement; + } + if (process?.env?.NODE_ENV !== 'test') { + throw new Error('There is no element with given.'); + } + } else { + return document.body; + } +} + +export function getScrollParent(element: HTMLElement) { + let style = window.getComputedStyle(element); + const excludeStaticParent = style.position === 'absolute'; + const overflowRegex = /(auto|scroll)/; + + if (style.position === 'fixed') return document.body; + + for (let parent = element; parent.parentElement; ) { + parent = parent.parentElement; + style = window.getComputedStyle(parent); + if (excludeStaticParent && style.position === 'static') { + continue; + } + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; +} + +export function scrollToParentVisibleArea(element: HTMLElement) { + const parent = getScrollParent(element); + if (parent === document.body) return; + // !todo 逻辑待验证 + if (elementInViewport(element, parent)) return; + parent.scrollTop = element.offsetTop - parent.offsetTop; +} + +export function scrollToElm(elm: HTMLElement) { + const rect = elm.getBoundingClientRect(); + + if (!elementInViewport(elm)) { + const winHeight = getWindowSize().height; + // const top = rect.bottom - (rect.bottom - rect.top); + window.scrollTo({ + top: rect.top - (winHeight / 2 - rect.height / 2), + behavior: 'smooth', + }); + } +} diff --git a/src/guide/utils/index.ts b/src/guide/utils/index.ts new file mode 100644 index 000000000..ae897bea5 --- /dev/null +++ b/src/guide/utils/index.ts @@ -0,0 +1,10 @@ +import { + getElmCssPropValue, + isFixed, + getRelativePosition, + getTargetElm, + scrollToParentVisibleArea, + scrollToElm, +} from './dom'; + +export { getElmCssPropValue, isFixed, getRelativePosition, getTargetElm, scrollToParentVisibleArea, scrollToElm }; diff --git a/src/locale/en_US.ts b/src/locale/en_US.ts index d122ae417..a4a69aee9 100644 --- a/src/locale/en_US.ts +++ b/src/locale/en_US.ts @@ -80,4 +80,10 @@ export default { successText: 'Success', }, }, + guide: { + next: 'Next', + skip: 'Skip', + finish: 'Finish', + back: 'Back', + }, }; diff --git a/src/locale/zh_CN.ts b/src/locale/zh_CN.ts index ed616202c..b14467446 100644 --- a/src/locale/zh_CN.ts +++ b/src/locale/zh_CN.ts @@ -67,4 +67,10 @@ export default { successText: '上传成功', }, }, + guide: { + next: '下一步', + skip: '跳过', + finish: '完成', + back: '返回', + }, }; diff --git a/src/shared/dom.ts b/src/shared/dom.ts index 5b51712cf..e3480a422 100644 --- a/src/shared/dom.ts +++ b/src/shared/dom.ts @@ -3,6 +3,8 @@ import isString from 'lodash/isString'; import { AttachNode } from '../common'; +const trim = (str: string): string => (str || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); + export function getAttach(node: AttachNode) { const attachNode = isFunction(node) ? node() : node; @@ -29,3 +31,98 @@ export function preventDefault(event: Event, isStopPropagation?: boolean) { stopPropagation(event); } } + +export function hasClass(el: Element, cls: string): any { + if (!el || !cls) return false; + if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); + if (el.classList) { + return el.classList.contains(cls); + } + return ` ${el.className} `.indexOf(` ${cls} `) > -1; +} + +export function addClass(el: Element, cls: string): any { + if (!el) return; + let curClass = el.className; + const classes = (cls || '').split(' '); + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.add(clsName); + } else if (!hasClass(el, clsName)) { + curClass += ` ${clsName}`; + } + } + if (!el.classList) { + el.className = curClass; + } +} + +export function removeClass(el: Element, cls: string): any { + if (!el || !cls) return; + const classes = cls.split(' '); + let curClass = ` ${el.className} `; + + for (let i = 0, j = classes.length; i < j; i++) { + const clsName = classes[i]; + if (!clsName) continue; + + if (el.classList) { + el.classList.remove(clsName); + } else if (hasClass(el, clsName)) { + curClass = curClass.replace(` ${clsName} `, ' '); + } + } + if (!el.classList) { + el.className = trim(curClass); + } +} + +/** + * 检查元素是否在父元素视图 + * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport + * @param elm 元素 + * @param parent + * @returns boolean + */ +export function elementInViewport(elm: HTMLElement, parent?: HTMLElement): boolean { + const rect = elm.getBoundingClientRect(); + if (parent) { + const parentRect = parent.getBoundingClientRect(); + return ( + rect.top >= parentRect.top && + rect.left >= parentRect.left && + rect.bottom <= parentRect.bottom && + rect.right <= parentRect.right + ); + } + return rect.top >= 0 && rect.left >= 0 && rect.bottom + 80 <= window.innerHeight && rect.right <= window.innerWidth; +} + +/** + * 获取当前视图滑动的距离 + * @returns { scrollTop: number, scrollLeft: number } + */ +export function getWindowScroll(): { scrollTop: number; scrollLeft: number } { + const { body } = document; + const docElm = document.documentElement; + const scrollTop = window.pageYOffset || docElm.scrollTop || body.scrollTop; + const scrollLeft = window.pageXOffset || docElm.scrollLeft || body.scrollLeft; + + return { scrollTop, scrollLeft }; +} + +/** + * 获取当前视图的大小 + * @returns { width: number, height: number } + */ +export function getWindowSize(): { width: number; height: number } { + if (window.innerWidth !== undefined) { + return { width: window.innerWidth, height: window.innerHeight }; + } + const doc = document.documentElement; + return { width: doc.clientWidth, height: doc.clientHeight }; +}