Merge branch 'main' into docs

This commit is contained in:
07akioni 2024-05-03 23:36:29 +08:00
commit da4a6ec969
117 changed files with 2556 additions and 755 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
extends: ['plugin:markdown/recommended', 'prettier'],
extends: ['plugin:markdown/recommended-legacy', 'prettier'],
overrides: [
{
files: '*.mjs',
@ -39,7 +39,7 @@ module.exports = {
},
{
files: ['*.ts', '*.tsx'],
extends: ['standard-with-typescript', 'plugin:import/typescript'],
extends: ['love', 'plugin:import/typescript'],
parserOptions: {
project: './tsconfig.json',
ecmaFeatures: {

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ yarn.lock
.DS_Store
.vscode
.idea
.pulsar
*.swp
*.tgz
coverage

View File

@ -1,5 +1,50 @@
# CHANGELOG
## 2.38.2
`2024-05-03`
### Fixes
- Fix `n-menu` Submenu's wai-aria role is not correct, closes [#5729](https://github.com/tusen-ai/naive-ui/issues/5729).
- Fix `n-tabs` style bug with type is `segment`closes [#5728](https://github.com/tusen-ai/naive-ui/issues/5728).
- Fix the get\*String() methods for UTC/locale mismatch, closes [#5702](closes https://github.com/tusen-ai/naive-ui/issues/5702).
- Fix `n-dialog` / `n-modal` calling `destroy` method may throw error.
- Fix `useModal` setting `card` preset without corresponding props in `n-card` slots, closes [#5746](https://github.com/tusen-ai/naive-ui/issues/5746).
- Fix `Submenu` component's wai-aria role setting error of `n-menu`closes [#5729](https://github.com/tusen-ai/naive-ui/issues/5729).
- Fix the `common` type error in the `theme-overrides` prop when modifying components' themes.
- Fix `n-split` may emit value less than `0`.
### Features
- `n-watermark` support multi-lines in content.
- Adds `n-infinite-scroll` component.
- `n-watermark` adds `text-align` prop.
- `n-qr-code` adds `type` prop, Customize rendering output by setting `type`, providing two options: `canvas` and `svg`.
- `n-card` adds `action`, `content`, `cover`, `footer` and `header-extra` props.
- `n-card`'s `title` prop supports render function.
- `n-upload` expose the `index` arg in `on-remove` function, closes [#5747](https://github.com/tusen-ai/naive-ui/issues/5747).
- `n-upload` exports `UploadOnDownload`, `UploadOnRemove`, `UploadOnFinish` and `UploadOnChange` types.
- `n-dialog` adds `action-class`, `action-style`, `content-class`, `content-style`, `title-class` and `title-style` props.
- `n-split` adds `pane1-class`, `pane1-style`, `pane2-class` and `pane2-style` props.
- `n-mention` adds `filter` method, closes [#5721](https://github.com/tusen-ai/naive-ui/pull/5721).
- `n-slider` adds wai-aria support.
- `n-date-picker` adds `time-picker-format` prop.
- `n-form-item` adds `feedback-class` and `feedback-style` props.
- `n-split` supports using pixel unit string as `value`.
- `n-scrollbar` adds `content-style` and `content-class` props, closes [#4497](https://github.com/tusen-ai/naive-ui/issues/4497).
- `n-image` adds `render-toolbar` prop.
- `n-cascader` adds `get-column-style` prop.
- `n-cascader` adds `get-render-prefix` prop.
- `n-cascader` adds `get-render-suffix` prop.
- `n-image` optimizes download icon style.
- `n-scrollbar` adds `height`, `width`, `radius`, `railInsetHorizontal`, `railInsetVertical` and `railColor` theme variables.
### i18n
- Add csCZ locale.
- Add missing itIT locale translations
## 2.38.1
`2024-02-26`

View File

@ -1,5 +1,49 @@
# CHANGELOG
## 2.38.2
`2024-05-03`
### Fixes
- 修复 `n-menu` 中 Submenu 组件的 wai-aria role 设置错误,关闭 [#5729](https://github.com/tusen-ai/naive-ui/issues/5729)
- 修复 `n-tabs` type 为 `segment` 时样式存在问题,关闭 [#5728](https://github.com/tusen-ai/naive-ui/issues/5728)
- 修复 get\*String() 方法中 UTC/区域设置不匹配的问题,关闭 [#5702](https://github.com/tusen-ai/naive-ui/issues/5702)
- 修复 `n-dialog` / `n-modal` 调用 `destroy` 方法时可能会报错
- 修复 `useModal` 设置 `card` 预设时 `n-card` 插槽缺少相应属性,关闭 [#5746](https://github.com/tusen-ai/naive-ui/issues/5746)
- 修复组件调整主题时 `theme-overrides` 属性中的 `common` 类型报错
- 修复 `n-split` 可能产生小与 `0` 的值
### Features
- `n-watermark` 支持多行文本
- 新增 `n-infinite-scroll` 组件
- `n-watermark` 新增 `text-align` 属性
- `n-qr-code` 新增 `type` 属性,设置 `type` 自定义渲染结果,提供 `canvas``svg` 两个选项
- `n-card` 新增 `action``content``cover``footer``header-extra` 属性
- `n-card``title` 属性支持渲染函数
- `n-upload` 导出 `on-remove` 方法的 `index` 属性,关闭 [#5747](https://github.com/tusen-ai/naive-ui/issues/5747)
- `n-upload` 导出 `UploadOnDownload``UploadOnRemove``UploadOnFinish``UploadOnChange` 类型
- `n-dialog` 新增 `action-class``action-style``content-class``content-style``title-class``title-style` 属性
- `n-split` 新增 `pane1-class``pane1-style``pane2-class``pane2-style` 属性
- `n-mention` 新增 `filter` 方法,关闭 [#5721](https://github.com/tusen-ai/naive-ui/pull/5721)
- `n-slider` 新增 wai-aria 支持
- `n-date-picker` 新增 `time-picker-format` 属性
- `n-form-item` 新增 `feedback-class``feedback-style` 属性
- `n-split` 支持设置像素值大小
- `n-scrollbar` 新增 `content-style``content-class` 属性,关闭 [#4497](https://github.com/tusen-ai/naive-ui/issues/4497)
- `n-image` 新增 `render-toolbar` 属性
- `n-cascader` 新增 `get-column-width` 属性
- `n-cascader` 新增 `render-prefix` 属性
- `n-cascader` 新增 `render-suffix` 属性
- `n-image` 优化下载按钮图标
- `n-scrollbar` 新增 `height``width``radius``railInsetHorizontal``railInsetVertical``railColor` 主题变量
### i18n
- 新增 csCZ locale
- 增加缺少的 itIT locale
## 2.38.1
`2024-02-26`

View File

@ -552,6 +552,11 @@ export const enComponentRoutes = [
path: 'split',
component: () => import('../../src/split/demos/enUS/index.demo-entry.md')
},
{
path: 'infinite-scroll',
component: () =>
import('../../src/infinite-scroll/demos/enUS/index.demo-entry.md')
},
{
path: 'float-button',
component: () =>
@ -951,6 +956,11 @@ export const zhComponentRoutes = [
path: 'split',
component: () => import('../../src/split/demos/zhCN/index.demo-entry.md')
},
{
path: 'infinite-scroll',
component: () =>
import('../../src/infinite-scroll/demos/zhCN/index.demo-entry.md')
},
{
path: 'float-button',
component: () =>

View File

@ -539,6 +539,13 @@ export function createComponentMenuOptions ({ lang, theme, mode }) {
zh: '树',
enSuffix: true,
path: '/tree'
},
{
en: 'Infinite Scroll',
zh: '无限滚动',
enSuffix: true,
path: '/infinite-scroll',
isNew: true
}
]
}),

View File

@ -1,6 +1,6 @@
{
"name": "naive-ui",
"version": "2.38.1",
"version": "2.38.2",
"description": "A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast",
"main": "lib/index.js",
"module": "es/index.mjs",
@ -88,15 +88,15 @@
"@rollup/plugin-terser": "^0.4.3",
"@types/estree": "^1.0.1",
"@types/jest": "^29.5.4",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"@typescript-eslint/parser": "^7.0.2",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vicons/fluent": "^0.12.0",
"@vicons/ionicons4": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/compiler-sfc": "^3.4.15",
"@vue/eslint-config-standard": "^8.0.1",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/server-renderer": "~3.4.15",
"@vue/test-utils": "^2.4.1",
"autoprefixer": "^10.4.15",
@ -106,11 +106,11 @@
"deepmerge": "^4.3.1",
"esbuild": "0.20.1",
"eslint": "^8.48.0",
"eslint-config-love": "^44.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^43.0.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-markdown": "^4.0.1",
"eslint-plugin-n": "^16.0.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.1.1",
@ -133,12 +133,12 @@
"rollup-plugin-esbuild": "^6.1.0",
"superagent": "^8.1.2",
"ts-jest": "^29.1.1",
"typescript": "5.3.3",
"typescript": "5.4.2",
"vfonts": "^0.0.3",
"vite": "^5.0.4",
"vue": "~3.4.15",
"vue-router": "^4.2.4",
"vue-tsc": "^1.8.27"
"vue-tsc": "^2.0.6"
},
"peerDependencies": {
"vue": "^3.0.0"

View File

@ -14,14 +14,14 @@ import type { PropType, CSSProperties, VNode, HTMLAttributes } from 'vue'
import { on, off } from 'evtd'
import { VResizeObserver } from 'vueuc'
import { useIsIos } from 'vooks'
import { getPreciseEventTarget } from 'seemly'
import { depx, getPreciseEventTarget } from 'seemly'
import { useConfig, useTheme, useThemeClass, useRtl } from '../../../_mixins'
import type { ThemeProps } from '../../../_mixins'
import type {
ExtractInternalPropTypes,
ExtractPublicPropTypes
} from '../../../_utils'
import { useReactivated, Wrapper } from '../../../_utils'
import { rtlInset, useReactivated, Wrapper } from '../../../_utils'
import { scrollbarLight } from '../styles'
import type { ScrollbarTheme } from '../styles'
import style from './styles/index.cssr'
@ -75,10 +75,6 @@ export interface ScrollbarInst extends ScrollbarInstMethods {
const scrollbarProps = {
...(useTheme.props as ThemeProps<ScrollbarTheme>),
size: {
type: Number,
default: 5
},
duration: {
type: Number,
default: 0
@ -155,6 +151,15 @@ const Scrollbar = defineComponent({
let memoMouseY: number = 0
const isIos = useIsIos()
const themeRef = useTheme(
'Scrollbar',
'-scrollbar',
style,
scrollbarLight,
props,
mergedClsPrefixRef
)
const yBarSizeRef = computed(() => {
const { value: containerHeight } = containerHeightRef
const { value: contentHeight } = contentHeightRef
@ -168,7 +173,8 @@ const Scrollbar = defineComponent({
} else {
return Math.min(
containerHeight,
(yRailSize * containerHeight) / contentHeight + props.size * 1.5
(yRailSize * containerHeight) / contentHeight +
depx(themeRef.value.self.width) * 1.5
)
}
})
@ -186,7 +192,10 @@ const Scrollbar = defineComponent({
) {
return 0
} else {
return (xRailSize * containerWidth) / contentWidth + props.size * 1.5
return (
(xRailSize * containerWidth) / contentWidth +
depx(themeRef.value.self.height) * 1.5
)
}
})
const xBarSizePxRef = computed(() => {
@ -636,31 +645,32 @@ const Scrollbar = defineComponent({
off('mousemove', window, handleYScrollMouseMove, true)
off('mouseup', window, handleYScrollMouseUp, true)
})
const themeRef = useTheme(
'Scrollbar',
'-scrollbar',
style,
scrollbarLight,
props,
mergedClsPrefixRef
)
const cssVarsRef = computed(() => {
const {
common: {
cubicBezierEaseInOut,
scrollbarBorderRadius,
scrollbarHeight,
scrollbarWidth
},
self: { color, colorHover }
common: { cubicBezierEaseInOut },
self: {
color,
colorHover,
height,
width,
borderRadius,
railInsetHorizontal,
railInsetVertical,
railColor
}
} = themeRef.value
return {
'--n-scrollbar-bezier': cubicBezierEaseInOut,
'--n-scrollbar-color': color,
'--n-scrollbar-color-hover': colorHover,
'--n-scrollbar-border-radius': scrollbarBorderRadius,
'--n-scrollbar-width': scrollbarWidth,
'--n-scrollbar-height': scrollbarHeight
'--n-scrollbar-border-radius': borderRadius,
'--n-scrollbar-width': width,
'--n-scrollbar-height': height,
'--n-scrollbar-rail-inset-horizontal': railInsetHorizontal,
'--n-scrollbar-rail-inset-vertical': rtlEnabledRef?.value
? rtlInset(railInsetVertical)
: railInsetVertical,
'--n-scrollbar-rail-color': railColor
}
})
const themeClassHandle = inlineThemeDisabled

View File

@ -8,6 +8,9 @@ import { fadeInTransition } from '../../../../_styles/transitions/fade-in.cssr'
// --n-scrollbar-width
// --n-scrollbar-height
// --n-scrollbar-border-radius
// --n-scrollbar-rail-inset-horizontal
// --n-scrollbar-rail-inset-vertical
// --n-scrollbar-rail-color
export default cB('scrollbar', `
overflow: hidden;
position: relative;
@ -30,6 +33,7 @@ export default cB('scrollbar', `
display: none;
`),
c('>', [
// We can't set overflow hidden since it affects positioning.
cB('scrollbar-content', `
box-sizing: border-box;
min-width: 100%;
@ -42,12 +46,11 @@ export default cB('scrollbar', `
position: absolute;
pointer-events: none;
user-select: none;
background: var(--n-scrollbar-rail-color);
-webkit-user-select: none;
`, [
cM('horizontal', `
left: 2px;
right: 2px;
bottom: 4px;
inset: var(--n-scrollbar-rail-inset-horizontal);
height: var(--n-scrollbar-height);
`, [
c('>', [
@ -59,9 +62,7 @@ export default cB('scrollbar', `
])
]),
cM('vertical', `
right: 4px;
top: 2px;
bottom: 2px;
inset: var(--n-scrollbar-rail-inset-vertical);
width: var(--n-scrollbar-width);
`, [
c('>', [

View File

@ -13,11 +13,7 @@ export default cB('scrollbar', [
right: unset;
`)
])
]),
cM('vertical', `
left: 4px;
right: unset;
`)
])
])
])
])

View File

@ -0,0 +1,5 @@
export const commonVars = {
railInsetHorizontal: 'auto 2px 4px 2px',
railInsetVertical: '2px 4px 2px auto',
railColor: 'transparent'
}

View File

@ -1,10 +1,21 @@
import { commonLight } from '../../../_styles/common'
import type { ThemeCommonVars } from '../../../_styles/common'
import type { Theme } from '../../../_mixins'
import { commonVars } from './common'
export const self = (vars: ThemeCommonVars) => {
const { scrollbarColor, scrollbarColorHover } = vars
const {
scrollbarColor,
scrollbarColorHover,
scrollbarHeight,
scrollbarWidth,
scrollbarBorderRadius
} = vars
return {
...commonVars,
height: scrollbarHeight,
width: scrollbarWidth,
borderRadius: scrollbarBorderRadius,
color: scrollbarColor,
colorHover: scrollbarColorHover
}

View File

@ -35,33 +35,32 @@ export interface ThemePropsReactive<T> {
builtinThemeOverrides?: ExtractThemeOverrides<T>
}
export type ExtractThemeVars<T> = T extends Theme<unknown, infer U, unknown>
? unknown extends U // self is undefined, ThemeVars is unknown
? Record<string, unknown>
: U
: Record<string, unknown>
export type ExtractThemeVars<T> =
T extends Theme<unknown, infer U, unknown>
? unknown extends U // self is undefined, ThemeVars is unknown
? Record<string, unknown>
: U
: Record<string, unknown>
export type ExtractPeerOverrides<T> = T extends Theme<unknown, unknown, infer V>
? {
peers?: {
[k in keyof V]?: ExtractThemeOverrides<V[k]>
export type ExtractPeerOverrides<T> =
T extends Theme<unknown, unknown, infer V>
? {
peers?: {
[k in keyof V]?: ExtractThemeOverrides<V[k]>
}
}
}
: T
: T
// V is peers theme
export type ExtractMergedPeerOverrides<T> = T extends Theme<
unknown,
unknown,
infer V
>
? {
[k in keyof V]?: ExtractPeerOverrides<V[k]>
}
: T
export type ExtractMergedPeerOverrides<T> =
T extends Theme<unknown, unknown, infer V>
? {
[k in keyof V]?: ExtractPeerOverrides<V[k]>
}
: T
export type ExtractThemeOverrides<T> = Partial<ExtractThemeVars<T>> &
ExtractPeerOverrides<T> & { common?: ThemeCommonVars }
ExtractPeerOverrides<T> & { common?: Partial<ThemeCommonVars> }
export function createTheme<N extends string, T, R> (
theme: Theme<N, T, R>
@ -75,14 +74,15 @@ type UseThemeProps<T> = Readonly<{
builtinThemeOverrides?: ExtractThemeOverrides<T>
}>
export type MergedTheme<T> = T extends Theme<unknown, infer V, infer W>
? {
common: ThemeCommonVars
self: V
peers: W
peerOverrides: ExtractMergedPeerOverrides<T>
}
: T
export type MergedTheme<T> =
T extends Theme<unknown, infer V, infer W>
? {
common: ThemeCommonVars
self: V
peers: W
peerOverrides: ExtractMergedPeerOverrides<T>
}
: T
function useTheme<N, T, R> (
resolveId: Exclude<keyof GlobalTheme, 'common' | 'name'>,

View File

@ -7,7 +7,7 @@ import {
type VNodeChild
} from 'vue'
function ensureValidVNode (
export function ensureValidVNode (
vnodes: VNodeArrayChildren
): VNodeArrayChildren | null {
return vnodes.some((child) => {

View File

@ -114,10 +114,7 @@ describe('n-auto-complete', () => {
const wrapper = mount(NAutoComplete)
await wrapper.setProps({
getShow: (value: string | null) => {
if (value && value.endsWith('@')) {
return true
}
return false
return !!value?.endsWith('@')
},
options
})

View File

@ -25,22 +25,27 @@ embedded.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| action | `() => VNodeChild` | `undefined` | Operating area content, must be a render function. | NEXT_VERION |
| bordered | `boolean` | `true` | Whether to show the card border. | |
| closable | `boolean` | `false` | Is it allowed to close. | |
| content | `string \| (() => VNodeChild)` | `undefined` | Card content, can be a render function. | NEXT_VERION |
| content-class | `string` | `undefined` | The class of the card content area. | 2.36.0 |
| content-style | `Object \| string` | `undefined` | The style of the card content area. | |
| cover | `() => VNodeChild` | `undefined` | Cover content, must be a render function. | NEXT_VERION |
| embedded | `boolean` | `false` | Use a darker background color to show the embedding effect (only for bright themes) | |
| footer | `() => VNodeChild` | `undefined` | Footer content, must be a render function. | NEXT_VERION |
| footer-class | `string` | `undefined` | The class of the bottom area of the card. | 2.36.0 |
| footer-style | `Object \| string` | `undefined` | The style of the bottom area of the card. | |
| header-class | `string` | `undefined` | The class of the card head area. | 2.36.0 |
| header-style | `Object \| string` | `undefined` | The style of the card head area. | |
| header-extra | `() => VNodeChild` | `undefined` | Header extra content, must be a render function. | NEXT_VERION |
| header-extra-class | `string` | `undefined` | The class of the card head extra area. | 2.36.0 |
| header-extra-style | `Object \| string` | `undefined` | The style of the card head extra area. | 2.25.0 |
| hoverable | `boolean` | `false` | Whether to show shadow when hovering on the card. | |
| segmented | `boolean \| { [part in 'content' \| 'footer' \| 'action']?: boolean \| 'soft' }` | `false` | Segment divider settings of the card. | |
| size | `'small' \| 'medium' \| 'large' \| 'huge'` | `'medium'` | Card size. | |
| tag | `string` | `'div'` | What tag need the card be rendered as. | 2.34.3 |
| title | `string` | `undefined` | Card title. | |
| title | `string \| (() => VNodeChild)` | `undefined` | Card title. | Render function since 2.38.2 |
| on-close | `() => void` | `undefined` | Callback function triggered upon closing the card. | |
### Card Slots

View File

@ -27,22 +27,27 @@ embedded-debug.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| action | `() => VNodeChild` | `undefined` | 操作区域内容。,需要是 render 函数 | NEXT_VERION |
| bordered | `boolean` | `true` | 是否显示卡片边框 | |
| closable | `boolean` | `false` | 是否允许关闭 | |
| content | `string \| (() => VNodeChild)` | `undefined` | 卡片内容,,可以是 render 函数 | NEXT_VERION |
| content-class | `string` | `undefined` | 卡片内容区域的类名 | 2.36.0 |
| content-style | `Object \| string` | `undefined` | 卡片内容区域的样式 | |
| cover | `() => VNodeChild` | `undefined` | 覆盖内容,需要是 render 函数 | NEXT_VERION |
| embedded | `boolean` | `false` | 使用更深的背景色展现嵌入效果,只对亮色主题生效 | |
| footer | `() => VNodeChild` | `undefined` | 底部内容 | NEXT_VERION |
| footer-class | `string` | `undefined` | 卡片底部区域的类名 | 2.36.0 |
| footer-style | `Object \| string` | `undefined` | 卡片底部区域的样式 | |
| header-class | `string` | `undefined` | 卡片头部区域的类名 | 2.36.0 |
| header-style | `Object \| string` | `undefined` | 卡片头部区域的样式 | |
| header-extra | `() => VNodeChild` | `undefined` | 头部额外内容,需要是 render 函数 | NEXT_VERION |
| header-extra-class | `string` | `undefined` | 卡片头部额外内容的类名 | 2.36.0 |
| header-extra-style | `Object \| string` | `undefined` | 卡片头部额外内容的样式 | 2.25.0 |
| hoverable | `boolean` | `false` | 卡片是否可悬浮 | |
| segmented | `boolean \| { [part in 'content' \| 'footer' \| 'action']?: boolean \| 'soft' }` | `false` | 卡片的分段区域设置 | |
| size | `'small' \| 'medium' \| 'large' \| 'huge'` | `'medium'` | 卡片的尺寸 | |
| tag | `string` | `'div'` | 卡片组件要渲染为什么标签 | 2.34.3 |
| title | `string` | `undefined` | 卡片的标题 | |
| title | `string \| (() => VNodeChild)` | `undefined` | 卡片的标题,,可以是 render 函数 | 2.38.2 支持 render 函数 |
| on-close | `() => void` | `undefined` | 点击卡片关闭图标时的回调 | |
### Card Slots

View File

@ -3,7 +3,8 @@ import {
defineComponent,
computed,
type PropType,
type CSSProperties
type CSSProperties,
type VNodeChild
} from 'vue'
import { getPadding } from 'seemly'
import { useRtl } from '../../_mixins/use-rtl'
@ -15,6 +16,7 @@ import { NBaseClose } from '../../_internal'
import { cardLight } from '../styles'
import type { CardTheme } from '../styles'
import style from './styles/index.cssr'
import { ensureValidVNode } from '../../_utils/vue/resolve-slot'
export interface CardSegmented {
content?: boolean | 'soft'
@ -23,7 +25,7 @@ export interface CardSegmented {
}
export const cardBaseProps = {
title: String,
title: [String, Function] as PropType<string | (() => VNodeChild)>,
contentClass: String,
contentStyle: [Object, String] as PropType<CSSProperties | string>,
headerClass: String,
@ -52,7 +54,12 @@ export const cardBaseProps = {
tag: {
type: String as PropType<keyof HTMLElementTagNameMap>,
default: 'div'
}
},
cover: Function as PropType<() => VNodeChild>,
content: [String, Function] as PropType<string | (() => VNodeChild)>,
footer: Function as PropType<() => VNodeChild>,
action: Function as PropType<() => VNodeChild>,
headerExtra: Function as PropType<() => VNodeChild>
} as const
export const cardBasePropKeys = keysOf(cardBaseProps)
@ -216,31 +223,43 @@ export default defineComponent({
style={this.cssVars as CSSProperties}
role={this.role}
>
{resolveWrappedSlot(
$slots.cover,
(children) =>
children && (
{resolveWrappedSlot($slots.cover, (children) => {
const mergedChildren = this.cover
? ensureValidVNode([this.cover()])
: children
return (
mergedChildren && (
<div class={`${mergedClsPrefix}-card-cover`} role="none">
{children}
{mergedChildren}
</div>
)
)}
)
})}
{resolveWrappedSlot($slots.header, (children) => {
return children || this.title || this.closable ? (
const { title } = this
const mergedChildren = title
? ensureValidVNode(
typeof title === 'function' ? [title()] : [title]
)
: children
return mergedChildren || this.closable ? (
<div
class={[`${mergedClsPrefix}-card-header`, this.headerClass]}
style={this.headerStyle}
role="heading"
>
<div
class={`${mergedClsPrefix}-card-header__main`}
role="heading"
>
{children || this.title}
{mergedChildren}
</div>
{resolveWrappedSlot(
$slots['header-extra'],
(children) =>
children && (
{resolveWrappedSlot($slots['header-extra'], (children) => {
const mergedChildren = this.headerExtra
? ensureValidVNode([this.headerExtra()])
: children
return (
mergedChildren && (
<div
class={[
`${mergedClsPrefix}-card-header__extra`,
@ -248,56 +267,69 @@ export default defineComponent({
]}
style={this.headerExtraStyle}
>
{children}
{mergedChildren}
</div>
)
)}
{this.closable ? (
)
})}
{this.closable && (
<NBaseClose
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-card-header__close`}
onClick={this.handleCloseClick}
absolute
/>
) : null}
)}
</div>
) : null
})}
{resolveWrappedSlot(
$slots.default,
(children) =>
children && (
{resolveWrappedSlot($slots.default, (children) => {
const { content } = this
const mergedChildren = content
? ensureValidVNode(
typeof content === 'function' ? [content()] : [content]
)
: children
return (
mergedChildren && (
<div
class={[`${mergedClsPrefix}-card__content`, this.contentClass]}
style={this.contentStyle}
role="none"
>
{children}
{mergedChildren}
</div>
)
)}
{resolveWrappedSlot(
$slots.footer,
(children) =>
children && [
)
})}
{resolveWrappedSlot($slots.footer, (children) => {
const mergedChildren = this.footer
? ensureValidVNode([this.footer()])
: children
return (
mergedChildren && (
<div
class={[`${mergedClsPrefix}-card__footer`, this.footerClass]}
style={this.footerStyle}
role="none"
>
{children}
</div>
]
)}
{resolveWrappedSlot(
$slots.action,
(children) =>
children && (
<div class={`${mergedClsPrefix}-card__action`} role="none">
{children}
{mergedChildren}
</div>
)
)}
)
})}
{resolveWrappedSlot($slots.action, (children) => {
const mergedChildren = this.action
? ensureValidVNode([this.action()])
: children
return (
mergedChildren && (
<div class={`${mergedClsPrefix}-card__action`} role="none">
{mergedChildren}
</div>
)
)
})}
</Component>
)
}

View File

@ -39,6 +39,7 @@ status.vue
| filterable | `boolean` | `false` | Note: If `remote` is set, this won't have any effect. | |
| filter | `(pattern: string, option: CascaderOption, path: CascaderOption[]) => boolean` | A string based filter algorithm. | Filter function of the cascader. | |
| filter-menu-props | `HTMLAttributes` | `undefined` | The filter menu's dom props. | 2.27.0 |
| get-column-style | `(detail: { level: number }) => string \| object` | `undefined` | Function that resolves column style. `level` starts from `0`. | 2.38.2 |
| value-field | `string` | `'value'` | The value field in `CascaderOption`. | |
| label-field | `string` | `'label'` | The label field in `CascaderOption`. | |
| max-tag-count | `number \| 'responsive'` | `undefined` | Max tag count in multiple select mode. `responsive` will keep all the tags in single line. | |
@ -48,7 +49,9 @@ status.vue
| placeholder | `string` | `'Please Select'` | Placeholder text. | |
| placement | `'top-start' \| 'top' \| 'top-end' \| 'right-start' \| 'right' \| 'right-end' \| 'bottom-start' \| 'bottom' \| 'bottom-end' \| 'left-start' \| 'left' \| 'left-end'` | `'bottom-start'` | Cascader placement. | 2.25.0 |
| remote | `boolean` | `false` | Whether to obtain data remotely. | |
| render-prefix | `(info: { option: CascaderOption, node: VNode \| null, checked: boolean }) => VNodeChild` | `undefined` | Render function of all the options' prefix. | 2.38.2 |
| render-label | `(option: CascaderOption, checked: boolean) => VNodeChild` | `undefined` | Render function for cascader menu option label. | 2.24.0 |
| render-suffix | `(info: { option: CascaderOption, node: VNode \| null, checked: boolean }) => VNodeChild` | `undefined` | Render function of all the options' suffix. | 2.38.2 |
| separator | `string` | `' / '` | Selected option path value separator (used with `show-path`). | |
| show | `boolean` | `undefined` | Whether to show the menu. | |
| show-path | `boolean` | `true` | Whether to show the selected options as a path. | |

View File

@ -40,6 +40,7 @@ default-value-debug.vue
| filterable | `boolean` | `false` | `remote` 被设定时不生效 | |
| filter | `(pattern: string, option: CascaderOption, path: CascaderOption[]) => boolean` | 一个基于字符串的过滤算法 | 过滤选项的函数 | |
| filter-menu-props | `HTMLAttributes` | `undefined` | 可过滤菜单的 DOM 属性 | 2.27.0 |
| get-column-style | `(detail: { level: number }) => string \| object` | `undefined` | 获取列样式的函数,`level``0` 开始 | 2.38.2 |
| value-field | `string` | `'value'` | 替代 `CascaderOption` 中的 value 字段名 | |
| label-field | `string` | `'label'` | 替代 `CascaderOption` 中的 label 字段名 | |
| max-tag-count | `number \| 'responsive'` | `undefined` | 多选标签的最大显示数量,`responsive` 会将所有标签保持在一行 | |
@ -49,7 +50,9 @@ default-value-debug.vue
| placeholder | `string` | `'请选择'` | 提示信息 | |
| placement | `'top-start' \| 'top' \| 'top-end' \| 'right-start' \| 'right' \| 'right-end' \| 'bottom-start' \| 'bottom' \| 'bottom-end' \| 'left-start' \| 'left' \| 'left-end'` | `'bottom-start'` | 弹出位置 | 2.25.0 |
| remote | `boolean` | `false` | 是否远程获取数据 | |
| render-prefix | `(info: { option: CascaderOption, node: VNode \| null, checked: boolean }) => VNodeChild` | `undefined` | 节点前缀的渲染函数 | 2.38.2 |
| render-label | `(option: CascaderOption, checked: boolean) => VNodeChild` | `undefined` | Cascader 菜单选项标签渲染函数 | 2.24.0 |
| render-suffix | `(info: { option: CascaderOption, checked: boolean }) => VNodeChild` | `undefined` | 节点后缀的渲染函数 | 2.38.2 |
| separator | `string` | `' / '` | 数据分隔符 | |
| show | `boolean` | `undefined` | 是否打开菜单 | |
| show-path | `boolean` | `true` | 是否在选择器中显示选项路径 | |

View File

@ -12,7 +12,8 @@ import {
watchEffect,
type VNodeChild,
type HTMLAttributes,
nextTick
nextTick,
type VNode
} from 'vue'
import {
createTreeMate,
@ -169,6 +170,23 @@ export const cascaderProps = {
>,
onBlur: Function as PropType<(e: FocusEvent) => void>,
onFocus: Function as PropType<(e: FocusEvent) => void>,
getColumnStyle: Function as PropType<
(detail: { level: number }) => string | CSSProperties
>,
renderPrefix: Function as PropType<
(props: {
option: CascaderOption
checked: boolean
node: VNode | null
}) => VNodeChild
>,
renderSuffix: Function as PropType<
(props: {
option: CascaderOption
checked: boolean
node: VNode | null
}) => VNodeChild
>,
// deprecated
onChange: [Function, Array] as PropType<MaybeArray<OnUpdateValue> | undefined>
} as const
@ -873,6 +891,9 @@ export default defineComponent({
localeRef,
labelFieldRef: toRef(props, 'labelField'),
renderLabelRef: toRef(props, 'renderLabel'),
getColumnStyleRef: toRef(props, 'getColumnStyle'),
renderPrefixRef: toRef(props, 'renderPrefix'),
renderSuffixRef: toRef(props, 'renderSuffix'),
syncCascaderMenuPosition,
syncSelectMenuPosition,
updateKeyboardKey,

View File

@ -65,7 +65,8 @@ export default defineComponent({
mergedClsPrefixRef,
syncCascaderMenuPosition,
handleCascaderMenuClickOutside,
mergedThemeRef
mergedThemeRef,
getColumnStyleRef
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} = inject(cascaderInjectionKey)!
const submenuInstRefs: CascaderSubmenuInstance[] = []
@ -114,6 +115,7 @@ export default defineComponent({
submenuInstRefs,
maskInstRef,
mergedTheme: mergedThemeRef,
getColumnStyle: getColumnStyleRef,
handleFocusin,
handleFocusout,
handleClickOutside,
@ -141,6 +143,7 @@ export default defineComponent({
<div class={`${mergedClsPrefix}-cascader-submenu-wrapper`}>
{this.menuModel.map((submenuOptions, index) => (
<NCascaderSubmenu
style={this.getColumnStyle?.({ level: index })}
ref={
((instance: CascaderSubmenuInstance) => {
if (instance) {

View File

@ -4,7 +4,8 @@ import {
inject,
defineComponent,
type PropType,
Transition
Transition,
type VNode
} from 'vue'
import { useMemo } from 'vooks'
import { NCheckbox } from '../../checkbox'
@ -39,6 +40,8 @@ export default defineComponent({
mergedThemeRef,
labelFieldRef,
showCheckboxRef,
renderPrefixRef,
renderSuffixRef,
updateHoverKey,
updateKeyboardKey,
addLoadingKey,
@ -171,88 +174,121 @@ export default defineComponent({
handleCheckboxUpdateValue,
mergedHandleMouseEnter: mergedHandleMouseEnterRef,
mergedHandleMouseMove: mergedHandleMouseMoveRef,
renderLabel: renderLabelRef
renderLabel: renderLabelRef,
renderPrefix: renderPrefixRef,
renderSuffix: renderSuffixRef
}
},
render () {
const { mergedClsPrefix, renderLabel } = this
const {
mergedClsPrefix,
showCheckbox,
renderLabel,
renderPrefix,
renderSuffix
} = this
let prefixNode: VNode | null = null
if (showCheckbox || renderPrefix) {
const originalNode = this.showCheckbox ? (
<NCheckbox
focusable={false}
data-checkbox
disabled={this.disabled}
checked={this.checked}
indeterminate={this.indeterminate}
theme={this.mergedTheme.peers.Checkbox}
themeOverrides={this.mergedTheme.peerOverrides.Checkbox}
onUpdateChecked={this.handleCheckboxUpdateValue}
/>
) : null
prefixNode = (
<div class={`${mergedClsPrefix}-cascader-option__prefix`}>
{renderPrefix
? renderPrefix({
option: this.tmNode.rawNode,
checked: this.checked,
node: originalNode
})
: originalNode}
</div>
)
}
let suffixNode: VNode | null = null
const originalSuffixChild = (
<div class={`${mergedClsPrefix}-cascader-option-icon-placeholder`}>
{!this.isLeaf ? (
<NBaseLoading
clsPrefix={mergedClsPrefix}
scale={0.85}
strokeWidth={24}
show={this.isLoading}
class={`${mergedClsPrefix}-cascader-option-icon`}
>
{{
default: () => (
<NBaseIcon
clsPrefix={mergedClsPrefix}
key="arrow"
class={`${mergedClsPrefix}-cascader-option-icon ${mergedClsPrefix}-cascader-option-icon--arrow`}
>
{{
default: () => <ChevronRightIcon />
}}
</NBaseIcon>
)
}}
</NBaseLoading>
) : this.checkStrategy === 'child' &&
!(this.multiple && this.cascade) ? (
<Transition name="fade-in-scale-up-transition">
{{
default: () =>
this.checked ? (
<NBaseIcon
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-cascader-option-icon ${mergedClsPrefix}-cascader-option-icon--checkmark`}
>
{{ default: () => <CheckmarkIcon /> }}
</NBaseIcon>
) : null
}}
</Transition>
) : null}
</div>
)
suffixNode = (
<div class={`${mergedClsPrefix}-cascader-option__suffix`}>
{renderSuffix
? renderSuffix({
option: this.tmNode.rawNode,
checked: this.checked,
node: originalSuffixChild
})
: originalSuffixChild}
</div>
)
return (
<div
class={[
`${mergedClsPrefix}-cascader-option`,
{
[`${mergedClsPrefix}-cascader-option--pending`]:
this.keyboardPending || this.hoverPending,
[`${mergedClsPrefix}-cascader-option--disabled`]: this.disabled,
[`${mergedClsPrefix}-cascader-option--show-prefix`]:
this.showCheckbox
}
this.keyboardPending ||
(this.hoverPending &&
`${mergedClsPrefix}-cascader-option--pending`),
this.disabled && `${mergedClsPrefix}-cascader-option--disabled`,
this.showCheckbox && `${mergedClsPrefix}-cascader-option--show-prefix`
]}
onMouseenter={this.mergedHandleMouseEnter}
onMousemove={this.mergedHandleMouseMove}
onClick={this.handleClick}
>
{this.showCheckbox ? (
<div class={`${mergedClsPrefix}-cascader-option__prefix`}>
<NCheckbox
focusable={false}
data-checkbox
disabled={this.disabled}
checked={this.checked}
indeterminate={this.indeterminate}
theme={this.mergedTheme.peers.Checkbox}
themeOverrides={this.mergedTheme.peerOverrides.Checkbox}
onUpdateChecked={this.handleCheckboxUpdateValue}
/>
</div>
) : null}
{prefixNode}
<span class={`${mergedClsPrefix}-cascader-option__label`}>
{renderLabel
? renderLabel(this.tmNode.rawNode, this.checked)
: this.label}
</span>
<div class={`${mergedClsPrefix}-cascader-option__suffix`}>
<div class={`${mergedClsPrefix}-cascader-option-icon-placeholder`}>
{!this.isLeaf ? (
<NBaseLoading
clsPrefix={mergedClsPrefix}
scale={0.85}
strokeWidth={24}
show={this.isLoading}
class={`${mergedClsPrefix}-cascader-option-icon`}
>
{{
default: () => (
<NBaseIcon
clsPrefix={mergedClsPrefix}
key="arrow"
class={`${mergedClsPrefix}-cascader-option-icon ${mergedClsPrefix}-cascader-option-icon--arrow`}
>
{{
default: () => <ChevronRightIcon />
}}
</NBaseIcon>
)
}}
</NBaseLoading>
) : this.checkStrategy === 'child' &&
!(this.multiple && this.cascade) ? (
<Transition name="fade-in-scale-up-transition">
{{
default: () =>
this.checked ? (
<NBaseIcon
clsPrefix={mergedClsPrefix}
class={`${mergedClsPrefix}-cascader-option-icon ${mergedClsPrefix}-cascader-option-icon--checkmark`}
>
{{ default: () => <CheckmarkIcon /> }}
</NBaseIcon>
) : null
}}
</Transition>
) : null}
</div>
</div>
{suffixNode}
</div>
)
}

View File

@ -2,7 +2,7 @@ import type { CheckStrategy, TreeNode } from 'treemate'
import type { MergedTheme } from '../../_mixins'
import type { NLocale } from '../../locales'
import type { CascaderTheme } from '../styles'
import type { Ref, Slots, VNodeChild } from 'vue'
import type { CSSProperties, Ref, Slots, VNode, VNodeChild } from 'vue'
import { createInjectionKey } from '../../_utils'
export type ValueAtom = string | number
@ -79,6 +79,25 @@ export interface CascaderInjection {
optionHeightRef: Ref<string>
labelFieldRef: Ref<string>
showCheckboxRef: Ref<boolean>
getColumnStyleRef: Ref<
((detail: { level: number }) => string | CSSProperties) | undefined
>
renderPrefixRef: Ref<
| ((info: {
option: CascaderOption
checked: boolean
node: VNode | null
}) => VNodeChild)
| undefined
>
renderSuffixRef: Ref<
| ((info: {
option: CascaderOption
checked: boolean
node: VNode | null
}) => VNodeChild)
| undefined
>
syncCascaderMenuPosition: () => void
syncSelectMenuPosition: () => void
updateKeyboardKey: (value: Key | null) => void

View File

@ -37,17 +37,17 @@ export default c([
flex: 1;
justify-content: center;
`),
cB('scrollbar', {
// if width not set, cascader select menu's inner scroll area's width is
// not correct, which won't change after select menu width is set
width: '100%'
}),
cB('base-menu-mask', {
backgroundColor: 'var(--n-menu-mask-color)'
}),
cB('base-loading', {
color: 'var(--n-loading-color)'
}),
// if width not set, cascader select menu's inner scroll area's width is
// not correct, which won't change after select menu width is set
cB('scrollbar', `
width: 100%;
`),
cB('base-menu-mask', `
background-color: var(--n-menu-mask-color);
`),
cB('base-loading', `
color: var(--n-loading-color);
`),
cB('cascader-submenu-wrapper', `
position: relative;
display: flex;
@ -61,9 +61,9 @@ export default c([
cM('virtual', `
width: var(--n-column-width);
`),
cB('scrollbar-content', {
position: 'relative'
}),
cB('scrollbar-content', `
position: relative;
`),
c('&:first-child', `
border-top-left-radius: var(--n-menu-border-radius);
border-bottom-left-radius: var(--n-menu-border-radius);
@ -98,68 +98,68 @@ export default c([
background-color .2s var(--n-bezier),
color 0.2s var(--n-bezier);
`, [
cM('show-prefix', {
paddingLeft: 0
}),
cM('show-prefix', `
padding-left: 0;
`),
cE('label', `
flex: 1 0 0;
overflow: hidden;
text-overflow: ellipsis;
`),
cE('prefix', {
width: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}),
cE('suffix', {
width: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}),
cB('cascader-option-icon-placeholder', {
lineHeight: 0,
position: 'relative',
width: '16px',
height: '16px',
fontSize: '16px'
}, [
cE('prefix', `
min-width: 32px;
display: flex;
align-items: center;
justify-content: center;
`),
cE('suffix', `
min-width: 32px;
display: flex;
align-items: center;
justify-content: center;
`),
cB('cascader-option-icon-placeholder', `
line-height: 0;
position: relative;
width: 16px;
height: 16px;
font-size: 16px;
`, [
cB('cascader-option-icon', [
cM('checkmark', {
color: 'var(--n-option-check-mark-color)'
}, [
cM('checkmark', `
color: var(--n-option-check-mark-color);
`, [
fadeInScaleUpTransition({
originalTransition: 'background-color .3s var(--n-bezier), box-shadow .3s var(--n-bezier)'
})
]),
cM('arrow', {
color: 'var(--n-option-arrow-color)'
})
cM('arrow', `
color: var(--n-option-arrow-color);
`)
])
]),
cM('selected', {
color: 'var(--n-option-text-color-active)'
}),
cM('active', {
color: 'var(--n-option-text-color-active)',
backgroundColor: 'var(--n-option-color-hover)'
}),
cM('pending', {
backgroundColor: 'var(--n-option-color-hover)'
}),
c('&:hover', {
backgroundColor: 'var(--n-option-color-hover)'
}),
cM('selected', `
color: var(--n-option-text-color-active);
`),
cM('active', `
color: var(--n-option-text-color-active);
background-color: var(--n-option-color-hover);
`),
cM('pending', `
background-color: var(--n-option-color-hover);
`),
c('&:hover', `
background-color: var(--n-option-color-hover);
`),
cM('disabled', `
color: var(--n-option-text-color-disabled);
background-color: #0000;
cursor: not-allowed;
`, [
cB('cascader-option-icon', [
cM('arrow', {
color: 'var(--n-option-text-color-disabled)'
})
cM('arrow', `
color: var(--n-option-text-color-disabled);
`)
])
])
])

View File

@ -1,3 +1,4 @@
import { toRaw, h } from 'vue'
import { mount } from '@vue/test-utils'
import { NCode } from '../index'
import hljs from 'highlight.js/lib/core'
@ -21,23 +22,19 @@ describe('n-code', () => {
wrapper.unmount()
})
it('should work with `language` prop', () => {
const wrapper = mount(NCode, {
props: {
code: 'console.log(a)',
language: 'javascript',
hljs
}
const wrapper = mount(() => {
return (
<NCode code="console.log(a)" language="javascript" hljs={toRaw(hljs)} />
)
})
expect(wrapper.find('.hljs-variable').text()).toBe('console')
wrapper.unmount()
})
it('should work with `hljs` prop', () => {
const wrapper = mount(NCode, {
props: {
code: 'console.log(a)',
language: 'javascript',
hljs
}
const wrapper = mount(() => {
return (
<NCode code="console.log(a)" language="javascript" hljs={toRaw(hljs)} />
)
})
expect(wrapper.find('.function_').text()).toBe('log')
wrapper.unmount()

View File

@ -51,6 +51,7 @@ export * from './legacy-transfer'
export * from './list'
export * from './loading-bar'
export * from './log'
export * from './infinite-scroll'
export * from './menu'
export * from './mention'
export * from './message'

View File

@ -53,6 +53,7 @@ panel.vue
| show | `boolean` | `undefined` | Whether to show panel. | 2.28.3 |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Date picker size. | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | Validation status. | 2.27.0 |
| time-picker-format | `string \| undefined` | `undefined` | Format of the binding value in time picker inside date picker of type `'datetime'` and `'datetimerange'`. See [format](https://date-fns.org/v2.23.0/docs/format). | 2.38.2 |
| to | `string \| HTMLElement \| false` | `body` | Container node of the panel. `false` will keep it not detached. | |
| type | `'date' \| 'datetime' \| 'daterange' \| 'datetimerange' \| 'month' \| 'monthrange' \| 'year' \| 'yearrange' \| 'quarter' \| 'quarterrange' \| 'week'` | `'date'` | Date picker type. | `'quarter'` v2.22.0, `'monthrange'` 2.28.3 |
| value | `number \| [number, number] \| null` | `undefined` | Value of the date picker when being manually set. | |

View File

@ -54,6 +54,7 @@ form-debug.vue
| show | `boolean` | `undefined` | 是否展示面板 | 2.28.3 |
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 尺寸 | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | 验证状态 | 2.27.0 |
| time-picker-format | `string \| undefined` | `undefined` | 日期面板内时间的显示方式,详情见 [format](https://date-fns.org/v2.23.0/docs/format) | 2.38.2 |
| to | `string \| HTMLElement \| false` | `body` | 面板的容器节点,`false` 会待在原地 | |
| type | `'date' \| 'datetime' \| 'daterange' \| 'datetimerange' \| 'month' \| 'monthrange' \| 'year' \| 'yearrange' \| 'quarter' \| 'quarterrange' \| 'week'` | `'date'` | Date Picker 的类型 | `'quarter'` v2.22.0, `'monthrange'` 2.28.3 |
| value | `number \| [number, number] \| null` | `undefined` | Date Picker 的值 | |

View File

@ -113,7 +113,7 @@ export const datePickerProps = {
endPlaceholder: String,
format: String,
dateFormat: String,
timeFormat: String,
timerPickerFormat: String,
actions: Array as PropType<Array<'clear' | 'confirm' | 'now'> | null>,
shortcuts: Object as PropType<Shortcuts>,
isDateDisabled: Function as PropType<IsDateDisabled>,
@ -1022,7 +1022,8 @@ export default defineComponent({
onNextMonth: this.onNextMonth,
onPrevMonth: this.onPrevMonth,
onNextYear: this.onNextYear,
onPrevYear: this.onPrevYear
onPrevYear: this.onPrevYear,
timerPickerFormat: this.timerPickerFormat
}
const renderPanel = (): VNode => {
const { type } = this

View File

@ -64,7 +64,7 @@ export default defineComponent({
<NTimePicker
size={this.timePickerSize}
placeholder={this.locale.selectTime}
format={this.timeFormat}
format={this.timerPickerFormat}
{...(Array.isArray(timePickerProps) ? undefined : timePickerProps)}
showIcon={false}
to={false}

View File

@ -67,7 +67,7 @@ export default defineComponent({
/>
<NTimePicker
placeholder={this.locale.selectTime}
format={this.timeFormat}
format={this.timerPickerFormat}
size={this.timePickerSize}
{...(Array.isArray(timePickerProps)
? timePickerProps[0]
@ -98,7 +98,7 @@ export default defineComponent({
/>
<NTimePicker
placeholder={this.locale.selectTime}
format={this.timeFormat}
format={this.timerPickerFormat}
size={this.timePickerSize}
{...(Array.isArray(timePickerProps)
? timePickerProps[1]

View File

@ -22,7 +22,7 @@ const TIME_FORMAT = 'HH:mm:ss'
const usePanelCommonProps = {
active: Boolean,
dateFormat: String,
timeFormat: {
timerPickerFormat: {
type: String,
value: TIME_FORMAT
},

View File

@ -195,7 +195,7 @@ function getMonthString (
monthFormat: string,
locale: NDateLocale['locale']
): string {
const date = Date.UTC(2000, month, 1)
const date = new Date(2000, month, 1).getTime()
return format(date, monthFormat, { locale })
}
@ -204,7 +204,7 @@ function getYearString (
yearFormat: string,
locale: NDateLocale['locale']
): string {
const date = Date.UTC(year, 1, 1)
const date = new Date(year, 1, 1).getTime()
return format(date, yearFormat, { locale })
}
@ -213,7 +213,7 @@ function getQuarterString (
quarterFormat: string,
locale: NDateLocale['locale']
): string {
const date = Date.UTC(2000, quarter * 3 - 2, 1)
const date = new Date(2000, quarter * 3 - 2, 1).getTime()
return format(date, quarterFormat, { locale })
}

View File

@ -64,14 +64,18 @@ use-dialog-reactive-list.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| action | `() => VNodeChild` | `undefined` | Content of the operation area, must be a `render` function. | |
| action | `() => VNodeChild` | `undefined` | Content of the operation area, must be a render function. | |
| actionClass | `string` | The class name of the action area. | 2.38.2 |
| actionStyle | `Object \| string` | The style of the action area. | 2.38.2 |
| autoFocus | `boolean` | `true` | Whether to focus the first focusable element inside modal. | 2.28.3 |
| blockScroll | `boolean` | `true` | Whether to disabled body scrolling when it's active. | 2.28.3 |
| bordered | `boolean` | `false` | Whether to show `border`. | |
| class | `any` | `undefined` | Class name of the dialog. | 2.33.0 |
| closable | `boolean` | `true` | Whether to show `close` icon. | |
| closeOnEsc | `boolean` | `true` | Whether to close the dialog when the Esc key is pressed | 2.26.4 |
| content | `string \| (() => VNodeChild)` | `undefined` | Content, can be a `render` function. | |
| content | `string \| (() => VNodeChild)` | `undefined` | Content, can be a render function. | |
| contentClass | `string` | The class name of the content. | 2.38.2 |
| contentStyle | `Object \| string` | The style of the content. | 2.38.2 |
| iconPlacement | `'left' \| 'top'` | `'left'` | Icon placement. | |
| icon | `() => VNodeChild` | `undefined` | `Render` function of `icon`. | |
| loading | `boolean` | `false` | Whether to display `loading` status. | |
@ -82,7 +86,9 @@ use-dialog-reactive-list.vue
| positiveText | `string` | `undefined` | Confirm button text. Corresponding button won't show if not set. | |
| showIcon | `boolean` | `true` | Whether to show `icon`. | |
| style | `string \| Object` | `undefined` | Style of the dialog. | |
| title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a `render` function. | |
| title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a render function. | |
| titleClass | `string` | The class name of the content. | 2.38.2 |
| titleStyle | `Object \| string` | The style of the content. | 2.38.2 |
| transformOrigin | `'mouse' \| 'center'` | `'mouse'` | The transform origin of the dialog's display animation. | 2.34.0 |
| type | `'error \| 'success' \| 'warning'` | `'warning'` | Dialog type. | |
| onAfterEnter | `() => void` | `undefined` | Callback on enter animation ends. | 2.33.0 |
@ -100,11 +106,15 @@ All the properties can be modified dynamically.
| Name | Type | Description | Version |
| --- | --- | --- | --- |
| actionClass | `string` | The class name of the action area. | 2.38.2 |
| actionStyle | `Object \| string` | The style of the action area. | 2.38.2 |
| bordered | `boolean` | Whether to show `border`. | |
| class | `any` | Class name of the dialog. | 2.33.0 |
| closable | `boolean` | Whether to show `close` icon. | |
| closeOnEsc | `boolean` | Whether to close dialog on Esc is pressed. | 2.26.4 |
| content | `string \| (() => VNodeChild)` | Content, can be a `render` function. | |
| content | `string \| (() => VNodeChild)` | Content, can be a render function. | |
| contentClass | `string` | The class name of the content. | 2.38.2 |
| contentStyle | `Object \| string` | The style of the content. | 2.38.2 |
| iconPlacement | `'left' \| 'top'` | Icon placement. | |
| icon | `() => VNodeChild` | `Render` function of `icon`. | |
| loading | `boolean` | Whether to display `loading` status. | |
@ -115,7 +125,9 @@ All the properties can be modified dynamically.
| positiveText | `string` | Corresponding button won't show if not set. | |
| show-icon | `boolean` | Whether to show `icon`. | |
| style | `string \| Object` | Style of the dialog. | |
| title | `string \| (() => VNodeChild)` | Can be a `render` function. | |
| title | `string \| (() => VNodeChild)` | Can be a render function. | |
| titleClass | `string` | The class name of the content. | 2.38.2 |
| titleStyle | `Object \| string` | The style of the content. | 2.38.2 |
| transformOrigin | `'mouse' \| 'center'` | The transform origin of the dialog's display animation. | 2.34.0 |
| type | `'error \| 'success' \| 'warning'` | Dialog type. | |
| onAfterEnter | `() => void \| undefined` | Callback on enter animation ends. | 2.33.0 |
@ -135,9 +147,13 @@ All the properties can be modified dynamically.
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| action-class | `string` | `undefined` | The class name of the action area. | 2.38.2 |
| action-style | `Object \| string` | `undefined` | The style of the action area. | 2.38.2 |
| bordered | `boolean` | `false` | Whether to show `border`. | |
| closable | `boolean` | `true` | Whether to show `close` icon. | |
| content | `string \| (() => VNodeChild)` | `undefined` | Can be a `render` function. | |
| content | `string \| (() => VNodeChild)` | `undefined` | Can be a render function. | |
| content-class | `string` | `undefined` | The class name of the content. | 2.38.2 |
| content-style | `Object \| string` | `undefined` | The style of the content. | 2.38.2 |
| icon-placement | `'left' \| 'top'` | `'left'` | Icon placement. | |
| icon | `() => VNodeChild` | `undefined` | `Render` function of icon. | |
| loading | `boolean` | `false` | Whether to display `loading` status. | |
@ -146,7 +162,9 @@ All the properties can be modified dynamically.
| positive-button-props | `ButtonProps` | `undefined` | Confirm button's DOM props | 2.27.0 |
| positive-text | `string` | `undefined` | Corresponding button won't show if not set. | |
| show-icon | `boolean` | `true` | Whether to display the `icon`. | |
| title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a `render` function. | |
| title | `string \| (() => VNodeChild)` | `undefined` | Title, can be a render function. | |
| title-class | `string` | `undefined` | The class name of the content. | 2.38.2 |
| title-style | `Object \| string` | `undefined` | The style of the content. | 2.38.2 |
| type | `'error \| 'success' \| 'warning' \| 'info'` | `'warning'` | Dialog type. | |
| on-close | `() => void` | `undefined` | Calback on close button clicked. | |
| on-negative-click | `(e: MouseEvent) => void` | `undefined` | Callback on positive button clicked. | |

View File

@ -66,16 +66,20 @@ rtl-debug.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| action | `() => VNodeChild` | `undefined` | 操作区域的内容,需要是 `render` 函数 | |
| action | `() => VNodeChild` | `undefined` | 操作区域的内容,需要是渲染函数 | |
| actionClass | `string` | 操作区域的类名 | 2.38.2 |
| actionStyle | `Object \| string` | 操作区域的样式 | 2.38.2 |
| autoFocus | `boolean` | `true` | 是否自动聚焦 Modal 第一个可聚焦的元素 | 2.28.3 |
| blockScroll | `boolean` | `true` | 是否在打开时禁用 body 滚动 | 2.28.3 |
| bordered | `boolean` | `false` | 是否显示 `border` | |
| class | `any` | `undefined` | 类名 | 2.33.0 |
| closable | `boolean` | `true` | 是否显示 `close` 图标 | |
| closeOnEsc | `boolean` | `true` | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 |
| content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是 `render` 函数 | |
| content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是渲染函数 | |
| contentClass | `string` | 内容的类名 | 2.38.2 |
| contentStyle | `Object \| string` | 内容的样式 | 2.38.2 |
| iconPlacement | `'left' \| 'top'` | `'left'` | 图标的位置 | |
| icon | `() => VNodeChild` | `undefined` | 对话框 `icon`, 需要是 `render` 函数 | |
| icon | `() => VNodeChild` | `undefined` | 对话框 `icon`, 需要是渲染函数 | |
| loading | `boolean` | `false` | 是否显示 `loading` 状态 | |
| maskClosable | `boolean` | `true` | 是否可以通过点击 `mask` 关闭对话框 | |
| negativeButtonProps | `ButtonProps` | `undefined` | 取消按钮的属性 | 2.27.0 |
@ -84,7 +88,9 @@ rtl-debug.vue
| positiveText | `string` | `undefined` | 确认按钮的文字,不填对应的按钮不会出现 | |
| showIcon | `boolean` | `true` | 是否显示 `icon` | |
| style | `string \| Object` | `undefined` | 样式 | |
| title | `string \| (() => VNodeChild)` | `undefined` | 标题,可以是 `render` 函数 | |
| title | `string \| (() => VNodeChild)` | `undefined` | 标题,可以是渲染函数 | |
| titleClass | `string` | 标题的类名 | 2.38.2 |
| titleStyle | `Object \| string` | 标题的样式 | 2.38.2 |
| transformOrigin | `'mouse' \| 'center'` | `'mouse'` | 对话框动画出现的位置 | 2.34.0 |
| type | `'error \| 'success' \| 'warning'` | `'warning'` | 对话框类型 | |
| onAfterEnter | `() => void` | `undefined` | 出现动画完成执行的回调 | 2.33.0 |
@ -102,13 +108,17 @@ rtl-debug.vue
| 名称 | 类型 | 说明 | 版本 |
| --- | --- | --- | --- |
| actionClass | `string` | 操作区域的类名 | 2.38.2 |
| actionStyle | `Object \| string` | 操作区域的样式 | 2.38.2 |
| bordered | `boolean` | 是否显示 `border` | |
| class | `any` | 类名 | 2.33.0 |
| closable | `boolean` | 是否显示 `close` 图标 | |
| closeOnEsc | `boolean` | 是否在摁下 Esc 键的时候关闭对话框 | 2.26.4 |
| content | `string \| (() => VNodeChild)` | 对话框内容,可以是 `render` 函数 | |
| content | `string \| (() => VNodeChild)` | 对话框内容,可以是渲染函数 | |
| contentClass | `string` | 内容的类名 | 2.38.2 |
| contentStyle | `Object \| string` | 内容的样式 | 2.38.2 |
| iconPlacement | `'left' \| 'top'` | 图标的位置 | |
| icon | `() => VNodeChild` | 对话框 `icon`,需要是 `render` 函数 | |
| icon | `() => VNodeChild` | 对话框 `icon`,需要是渲染函数 | |
| loading | `boolean` | 是否显示 `loading` 状态 | |
| maskClosable | `boolean` | 是否可以通过点击 `mask` 关闭对话框 | |
| negativeButtonProps | `ButtonProps` | 取消按钮的属性 | 2.27.0 |
@ -117,7 +127,9 @@ rtl-debug.vue
| positiveText | `string` | 确认按钮的文字,不填对应的按钮不会出现 | |
| showIcon | `boolean` | 是否显示 `icon` | |
| style | `string \| Object` | 样式 | |
| title | `string \| (() => VNodeChild)` | 可以是 `render` 函数 | |
| title | `string \| (() => VNodeChild)` | 可以是渲染函数 | |
| titleClass | `string` | 标题的类名 | 2.38.2 |
| titleStyle | `Object \| string` | 标题的样式 | 2.38.2 |
| transformOrigin | `'mouse' \| 'center'` | 对话框动画出现的位置 | 2.34.0 |
| type | `'error \| 'success' \| 'warning'` | 对话框类型 | |
| onAfterEnter | `() => void \| undefined` | 出现动画完成执行的回调 | 2.33.0 |
@ -137,18 +149,24 @@ rtl-debug.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| action-class | `string` | `undefined` | 操作区域的类名 | 2.38.2 |
| action-style | `Object \| string` | `undefined` | 操作区域的样式 | 2.38.2 |
| bordered | `boolean` | `false` | 是否显示 `border` | |
| closable | `boolean` | `true` | 是否显示 `close` 图标 | |
| content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是 `render` 函数 | |
| content | `string \| (() => VNodeChild)` | `undefined` | 对话框内容,可以是渲染函数 | |
| content-class | `string` | `undefined` | 内容的类名 | 2.38.2 |
| content-style | `Object \| string` | `undefined` | 内容的样式 | 2.38.2 |
| icon-placement | `'left' \| 'top'` | `'left'` | 图标放置的位置 | |
| icon | `() => VNodeChild` | `undefined` | 需要是 `render` 函数 | |
| icon | `() => VNodeChild` | `undefined` | 需要是渲染函数 | |
| loading | `boolean` | `false` | 是否显示 `loading` 状态 | |
| negative-button-props | `ButtonProps` | `undefined` | 取消按钮的属性 | 2.27.0 |
| negative-text | `string` | `undefined` | 取消按钮的文字,不填对应的按钮不会出现 | |
| positive-button-props | `ButtonProps` | `undefined` | 确认按钮的属性 | 2.27.0 |
| positive-text | `string` | `undefined` | 确认按钮的文字,不填对应的按钮不会出现 | |
| show-icon | `boolean` | `true` | 是否显示 `icon` | |
| title | `string \| (() => VNodeChild)` | `undefined` | 对话框标题,可以是 `render` 函数 | |
| title | `string \| (() => VNodeChild)` | `undefined` | 对话框标题,可以是渲染函数 | |
| title-class | `string` | `undefined` | 标题的类名 | 2.38.2 |
| title-style | `Object \| string` | `undefined` | 标题的样式 | 2.38.2 |
| type | `'error \| 'success' \| 'warning' \| 'info'` | `'warning'` | 对话框类型 | |
| on-close | `() => void` | `undefined` | 点击关闭时执行的回调函数 | |
| on-negative-click | `(e: MouseEvent) => void` | `undefined` | 执行 `negative` 时执行的回调函数 | |

View File

@ -1,4 +1,5 @@
import { h, defineComponent, computed, type CSSProperties } from 'vue'
import { getMargin } from 'seemly'
import {
InfoIcon,
SuccessIcon,
@ -19,7 +20,6 @@ import { dialogLight } from '../styles'
import type { DialogTheme } from '../styles'
import { dialogProps } from './dialogProps'
import style from './styles/index.cssr'
import { getMargin } from 'seemly'
const iconRenderMap = {
default: () => <InfoIcon />,
@ -205,7 +205,10 @@ export const NDialog = defineComponent({
const actionNode = resolveWrappedSlot(this.$slots.action, (children) =>
children || positiveText || negativeText || action ? (
<div class={`${mergedClsPrefix}-dialog__action`}>
<div
class={[`${mergedClsPrefix}-dialog__action`, this.actionClass]}
style={this.actionStyle}
>
{children ||
(action
? [render(action)]
@ -278,15 +281,20 @@ export const NDialog = defineComponent({
{showIcon && mergedIconPlacement === 'top' ? (
<div class={`${mergedClsPrefix}-dialog-icon-container`}>{icon}</div>
) : null}
<div class={`${mergedClsPrefix}-dialog__title`}>
<div
class={[`${mergedClsPrefix}-dialog__title`, this.titleClass]}
style={this.titleStyle}
>
{showIcon && mergedIconPlacement === 'left' ? icon : null}
{resolveSlot(this.$slots.header, () => [render(title)])}
</div>
<div
class={[
`${mergedClsPrefix}-dialog__content`,
actionNode ? '' : `${mergedClsPrefix}-dialog__content--last`
actionNode ? '' : `${mergedClsPrefix}-dialog__content--last`,
this.contentClass
]}
style={this.contentStyle}
>
{resolveSlot(this.$slots.default, () => [render(content)])}
</div>

View File

@ -83,14 +83,14 @@ export const NDialogProvider = defineComponent({
props: dialogProviderProps,
setup () {
const dialogListRef = ref<TypeSafeDialogReactive[]>([])
const dialogInstRefs: Record<string, DialogInst> = {}
const dialogInstRefs: Record<string, DialogInst | undefined> = {}
function create (options: DialogOptions = {}): DialogReactive {
const key = createId()
const dialogReactive = reactive({
...options,
key,
destroy: () => {
dialogInstRefs[`n-dialog-${key}`].hide()
dialogInstRefs[`n-dialog-${key}`]?.hide()
}
})
dialogListRef.value.push(dialogReactive)
@ -114,7 +114,7 @@ export const NDialogProvider = defineComponent({
function destroyAll (): void {
Object.values(dialogInstRefs).forEach((dialogInstRef) => {
dialogInstRef.hide()
dialogInstRef?.hide()
})
}

View File

@ -1,4 +1,4 @@
import type { PropType, VNodeChild } from 'vue'
import type { CSSProperties, PropType, VNodeChild } from 'vue'
import type { ButtonProps } from '../../button'
import type { ExtractPublicPropTypes } from '../../_utils'
import { keysOf } from '../../_utils'
@ -30,6 +30,12 @@ const dialogProps = {
loading: Boolean,
bordered: Boolean,
iconPlacement: String as PropType<IconPlacement>,
titleClass: [String, Array] as PropType<string | Array<string | undefined>>,
titleStyle: [String, Object] as PropType<string | CSSProperties>,
contentClass: [String, Array] as PropType<string | Array<string | undefined>>,
contentStyle: [String, Object] as PropType<string | CSSProperties>,
actionClass: [String, Array] as PropType<string | Array<string | undefined>>,
actionStyle: [String, Object] as PropType<string | CSSProperties>,
onPositiveClick: Function as PropType<(e: MouseEvent) => void>,
onNegativeClick: Function as PropType<(e: MouseEvent) => void>,
onClose: Function as PropType<() => void>

View File

@ -0,0 +1,35 @@
<markdown>
# Custom feedback style
Using `feedback-style` and `feedback-class` to custom feedback.
</markdown>
<template>
<n-form :model="formValue">
<n-form-item
:rule="{
required: true,
message: 'Centered feedback',
type: 'string',
trigger: ['input', 'blur']
}"
label="Centered feedback"
path="input"
feedback-style="text-align: center;"
>
<n-input v-model:value="formValue.input" />
</n-form-item>
</n-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
type FormValue = {
input: string | null
}
const formValue: FormValue = reactive({
input: null
})
</script>

View File

@ -25,6 +25,7 @@ show-label.vue
partially-apply-rules.vue
custom-messages.vue
dynamic.vue
feedback-style.vue
```
## API
@ -73,6 +74,8 @@ dynamic.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| feedback | `string` | `undefined` | The feedback message of the form item. If set, it will replace any result of rule-based validation. | |
| feedback-class | `string` | `undefined` | Feedback check vertical display positioning | 2.38.2 |
| feedback-style | `string \| object` | `undefined` | Feedback check horizontal display positioning | 2.38.2 |
| first | `boolean` | `false` | Whether to only show the first validation error message. | |
| ignore-path-change | `boolean` | `false` | Usually, changing `path` will cause a re-render and naive-ui will clear the validation result. Setting `ignore-path-change` to `true` will disable that behavior. | |
| label | `string` | `undefined` | Label. | |

View File

@ -0,0 +1,35 @@
<markdown>
# 自定义反馈样式
使用 `feedback-style` `feedback-class` 可以自定义反馈信息的样式
</markdown>
<template>
<n-form :model="formValue">
<n-form-item
:rule="{
required: true,
message: '居中的 feedback',
type: 'string',
trigger: ['input', 'blur']
}"
label="Feedback 居中"
path="input"
feedback-style="text-align: center;"
>
<n-input v-model:value="formValue.input" />
</n-form-item>
</n-form>
</template>
<script lang="ts" setup>
import { reactive } from 'vue'
type FormValue = {
input: string | null
}
const formValue: FormValue = reactive({
input: null
})
</script>

View File

@ -25,6 +25,7 @@ show-label.vue
partially-apply-rules.vue
custom-messages.vue
dynamic.vue
feedback-style.vue
```
## API
@ -67,6 +68,8 @@ dynamic.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| feedback | `string` | `undefined` | 表项的反馈信息。不设为 `undefined` 时,会覆盖规则验证的结果 | |
| feedback-class | `string` | `undefined` | 反馈校验竖向展示定位 | 2.38.2 |
| feedback-style | `string \| object` | `undefined` | 反馈校验横向展示定位 | 2.38.2 |
| first | `boolean` | `false` | 是否只展示首个出错信息 | |
| ignore-path-change | `boolean` | `false` | 通常 `path` 的改变会导致数据来源的变化,所以 naive-ui 会清空验证信息。如果不期望这个行为,可以将其置为 `true` | |
| label | `string` | `undefined` | 标签信息 | |

View File

@ -124,6 +124,7 @@ export default defineComponent({
})
}
if (formInvalid) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(errors.length ? errors : undefined)
} else {
resolve({

View File

@ -8,6 +8,7 @@ import {
type ExtractPropTypes,
ref,
provide,
type Slot,
inject,
watch,
Transition,
@ -78,6 +79,8 @@ export const formItemProps = {
ignorePathChange: Boolean,
validationStatus: String as PropType<'error' | 'warning' | 'success'>,
feedback: String,
feedbackClass: String,
feedbackStyle: [String, Object] as PropType<string | CSSProperties>,
showLabel: {
type: Boolean as PropType<boolean | undefined>,
default: undefined
@ -255,6 +258,7 @@ export default defineComponent({
if (validateCallback) {
validateCallback(errors, { warnings })
}
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(errors)
}
})
@ -604,64 +608,71 @@ export default defineComponent({
{this.mergedShowFeedback ? (
<div
key={this.feedbackId}
class={`${mergedClsPrefix}-form-item-feedback-wrapper`}
style={this.feedbackStyle}
class={[
`${mergedClsPrefix}-form-item-feedback-wrapper`,
this.feedbackClass
]}
>
<Transition name="fade-down-transition" mode="out-in">
{{
default: () => {
const { mergedValidationStatus } = this
return resolveWrappedSlot($slots.feedback, (children) => {
const { feedback } = this
const feedbackNodes =
children || feedback ? (
<div
key="__feedback__"
class={`${mergedClsPrefix}-form-item-feedback__line`}
>
{children || feedback}
</div>
) : this.renderExplains.length ? (
this.renderExplains?.map(({ key, render }) => (
return resolveWrappedSlot(
$slots.feedback as Slot | undefined,
(children) => {
const { feedback } = this
const feedbackNodes =
children || feedback ? (
<div
key={key}
key="__feedback__"
class={`${mergedClsPrefix}-form-item-feedback__line`}
>
{render()}
{children || feedback}
</div>
))
) : this.renderExplains.length ? (
this.renderExplains?.map(({ key, render }) => (
<div
key={key}
class={`${mergedClsPrefix}-form-item-feedback__line`}
>
{render()}
</div>
))
) : null
return feedbackNodes ? (
mergedValidationStatus === 'warning' ? (
<div
key="controlled-warning"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--warning`}
>
{feedbackNodes}
</div>
) : mergedValidationStatus === 'error' ? (
<div
key="controlled-error"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--error`}
>
{feedbackNodes}
</div>
) : mergedValidationStatus === 'success' ? (
<div
key="controlled-success"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--success`}
>
{feedbackNodes}
</div>
) : (
<div
key="controlled-default"
class={`${mergedClsPrefix}-form-item-feedback`}
>
{feedbackNodes}
</div>
)
) : null
return feedbackNodes ? (
mergedValidationStatus === 'warning' ? (
<div
key="controlled-warning"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--warning`}
>
{feedbackNodes}
</div>
) : mergedValidationStatus === 'error' ? (
<div
key="controlled-error"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--error`}
>
{feedbackNodes}
</div>
) : mergedValidationStatus === 'success' ? (
<div
key="controlled-success"
class={`${mergedClsPrefix}-form-item-feedback ${mergedClsPrefix}-form-item-feedback--success`}
>
{feedbackNodes}
</div>
) : (
<div
key="controlled-default"
class={`${mergedClsPrefix}-form-item-feedback`}
>
{feedbackNodes}
</div>
)
) : null
})
}
)
}
}}
</Transition>

View File

@ -0,0 +1,68 @@
<markdown>
# Custom Toolbar
You can customize the toolbar using `render-toolbar`.
</markdown>
<template>
<n-image
width="100"
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
:render-toolbar="renderToolbar"
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { OpenOutline, ClipboardOutline } from '@vicons/ionicons5'
import { useMessage, ImageRenderToolbarProps, NButton } from 'naive-ui'
export default defineComponent({
setup () {
const message = useMessage()
const url = ref('https://picsum.photos/id/10/100/100')
const renderToolbar = ({ nodes }: ImageRenderToolbarProps) => {
return [
nodes.prev,
nodes.next,
h(
NButton,
{
circle: true,
type: 'primary',
style: { marginLeft: '12px' },
onClick: () => {
window.open(url.value)
}
},
{
icon: () => h(OpenOutline)
}
),
h(
NButton,
{
circle: true,
type: 'primary',
style: { marginLeft: '12px' },
onClick: async () => {
await navigator.clipboard.writeText(url.value)
message.success('Copied to clipboard')
}
},
{
icon: () => h(ClipboardOutline)
}
)
]
}
return {
url,
renderToolbar
}
}
})
</script>

View File

@ -9,6 +9,7 @@ basic.vue
group.vue
error.vue
preview-disabled.vue
custom-toolbar.vue
custom.vue
tooltip.vue
lazy.vue
@ -32,6 +33,7 @@ previewed-img-props.vue
| preview-src | `string` | `undefined` | Source of preview image. | |
| preview-disabled | `boolean` | `false` | Whether clicking image preview is disabled. | |
| previewed-img-props | `HTMLAttributes` | `undefined` | DOM attributes of img element in preview mode. | 2.34.0 |
| render-toolbar | `(props: { nodes: { prev: VNode, next: VNode, rotateCounterclockwise: VNode, rotateClockwise: VNode, resizeToOriginalSize: VNode, zoomOut: VNode, zoomIn: VNode, download: VNode, close: VNode } }) => VNodeChild` | `undefined` | Toolbar rendering function. | `2.38.2` |
| show-toolbar | `boolean` | `true` | Whether to show the bottom toolbar when the image enlarge. | |
| show-toolbar-tooltip | `boolean` | `false` | Whether to show toolbar buttons' tooltip. | 2.24.0 |
| src | `string` | `undefined` | Image source. | |
@ -43,10 +45,11 @@ previewed-img-props.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| on-preview-prev | `() => void` | `undefined` | Click the callback from the previous slide | |
| on-preview-next | `() => void` | `undefined` | Click the callback on the next slide |
| render-toolbar | `(props: { nodes: { prev: VNode, next: VNode, rotateCounterclockwise: VNode, rotateClockwise: VNode, resizeToOriginalSize: VNode, zoomOut: VNode, zoomIn: VNode, download: VNode, close: VNode } }) => VNodeChild` | `undefined` | Toolbar rendering function. | `2.38.2` |
| show-toolbar | `boolean` | `true` | Whether to show the bottom toolbar when the image enlarge. | |
| show-toolbar-tooltip | `boolean` | `false` | Whether to show toolbar buttons' tooltip. | 2.24.0 |
| on-preview-prev | `() => void` | `undefined` | Click the callback from the previous slide | |
| on-preview-next | `() => void` | `undefined` | Click the callback on the next slide |
### Image Slots

View File

@ -0,0 +1,67 @@
<markdown>
# 自定义工具栏
你可以使用 `render-toolbar` 来自定义工具栏
</markdown>
<template>
<n-image
width="100"
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
:render-toolbar="renderToolbar"
/>
</template>
<script lang="ts">
import { defineComponent, ref, h } from 'vue'
import { OpenOutline, ClipboardOutline } from '@vicons/ionicons5'
import { useMessage, ImageRenderToolbarProps, NButton } from 'naive-ui'
export default defineComponent({
setup () {
const message = useMessage()
const url = ref('https://picsum.photos/id/10/100/100')
const renderToolbar = ({ nodes }: ImageRenderToolbarProps) => {
return [
nodes.prev,
nodes.next,
h(
NButton,
{
circle: true,
type: 'primary',
style: { marginLeft: '12px' },
onClick: () => {
window.open(url.value)
}
},
{
icon: () => h(OpenOutline)
}
),
h(
NButton,
{
circle: true,
type: 'primary',
style: { marginLeft: '12px' },
onClick: async () => {
await navigator.clipboard.writeText(url.value)
message.success('已复制到剪贴板')
}
},
{
icon: () => h(ClipboardOutline)
}
)
]
}
return {
url,
renderToolbar
}
}
})
</script>

View File

@ -9,6 +9,7 @@ basic.vue
group.vue
error.vue
preview-disabled.vue
custom-toolbar.vue
custom.vue
tooltip.vue
full-debug.vue
@ -32,6 +33,7 @@ previewed-img-props.vue
| preview-src | `string` | `undefined` | 预览图片的图片地址 | |
| preview-disabled | `boolean` | `false` | 是否禁用单击图像预览 | |
| previewed-img-props | `HTMLAttributes` | `undefined` | 预览图片时 img 元素的属性 | 2.34.0 |
| render-toolbar | `(props: { nodes: { prev: VNode, next: VNode, rotateCounterclockwise: VNode, rotateClockwise: VNode, resizeToOriginalSize: VNode, zoomOut: VNode, zoomIn: VNode, download: VNode, close: VNode } }) => VNodeChild` | `undefined` | 工具栏的渲染函数 | `2.38.2` |
| show-toolbar | `boolean` | `true` | 图片放大后是否展示底部工具栏 | |
| show-toolbar-tooltip | `boolean` | `false` | 是否展示工具栏的提示 | 2.24.0 |
| src | `string` | `undefined` | 图片来源 | |
@ -43,10 +45,11 @@ previewed-img-props.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| on-preview-prev | `() => void` | `undefined` | 点击上一张的回调 | |
| on-preview-next | `() => void` | `undefined` | 点击下一张的回调 | |
| render-toolbar | `(props: { nodes: { prev: VNode, next: VNode, rotateCounterclockwise: VNode, rotateClockwise: VNode, resizeToOriginalSize: VNode, zoomOut: VNode, zoomIn: VNode, download: VNode, close: VNode } }) => VNodeChild` | `undefined` | 工具栏的渲染函数 | `2.38.2` |
| show-toolbar | `boolean` | `true` | 图片放大后是否展示底部工具栏 | |
| show-toolbar-tooltip | `boolean` | `false` | 是否展示工具栏的提示 | 2.24.0 |
| on-preview-prev | `() => void` | `undefined` | 点击上一张的回调 | |
| on-preview-next | `() => void` | `undefined` | 点击下一张的回调 | |
### Image Slots

View File

@ -2,3 +2,4 @@ export { default as NImage, imageProps } from './src/Image'
export type { ImageProps } from './src/Image'
export { default as NImageGroup, imageGroupProps } from './src/ImageGroup'
export type { ImageGroupProps } from './src/ImageGroup'
export type * from './src/public-types'

View File

@ -206,9 +206,11 @@ export default defineComponent({
ref="previewInstRef"
showToolbar={this.showToolbar}
showToolbarTooltip={this.showToolbarTooltip}
renderToolbar={this.renderToolbar}
>
{{
default: () => imgNode
default: () => imgNode,
toolbar: () => this.$slots.toolbar?.()
}}
</NImagePreview>
)}

View File

@ -4,7 +4,8 @@ import {
ref,
provide,
getCurrentInstance,
type Ref
type Ref,
toRef
} from 'vue'
import { createId } from 'seemly'
import { createInjectionKey, type ExtractPublicPropTypes } from '../../_utils'
@ -12,11 +13,13 @@ import { useConfig } from '../../_mixins'
import NImagePreview from './ImagePreview'
import type { ImagePreviewInst } from './ImagePreview'
import { imagePreviewSharedProps } from './interface'
import type { ImageRenderToolbar } from './public-types'
export const imageGroupInjectionKey = createInjectionKey<
ImagePreviewInst & {
groupId: string
mergedClsPrefixRef: Ref<string>
renderToolbarRef: Ref<ImageRenderToolbar | undefined>
}
>('n-image-group')
@ -67,7 +70,8 @@ export default defineComponent({
toggleShow: () => {
previewInstRef.value?.toggleShow()
},
groupId
groupId,
renderToolbarRef: toRef(props, 'renderToolbar')
})
const previewInstRef = ref<ImagePreviewInst | null>(null)
return {
@ -92,6 +96,7 @@ export default defineComponent({
onNext={this.next}
showToolbar={this.showToolbar}
showToolbarTooltip={this.showToolbarTooltip}
renderToolbar={this.renderToolbar}
>
{this.$slots}
</NImagePreview>

View File

@ -27,14 +27,15 @@ import {
RotateCounterclockwiseIcon,
ZoomInIcon,
ZoomOutIcon,
ResizeSmallIcon
ResizeSmallIcon,
DownloadIcon
} from '../../_internal/icons'
import { useConfig, useLocale, useTheme, useThemeClass } from '../../_mixins'
import { NBaseIcon } from '../../_internal'
import { download } from '../../_utils'
import { NTooltip } from '../../tooltip'
import { imageLight } from '../styles'
import { prevIcon, nextIcon, closeIcon, downloadIcon } from './icons'
import { prevIcon, nextIcon, closeIcon } from './icons'
import {
imageContextKey,
type MoveStrategy,
@ -485,7 +486,75 @@ export default defineComponent({
}
},
render () {
const { clsPrefix } = this
const { clsPrefix, renderToolbar, withTooltip } = this
const prevNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.handleSwitchPrev}>
{{ default: () => prevIcon }}
</NBaseIcon>,
'tipPrevious'
)
const nextNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.handleSwitchNext}>
{{ default: () => nextIcon }}
</NBaseIcon>,
'tipNext'
)
const rotateCounterclockwiseNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.rotateCounterclockwise}>
{{
default: () => <RotateCounterclockwiseIcon />
}}
</NBaseIcon>,
'tipCounterclockwise'
)
const rotateClockwiseNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.rotateClockwise}>
{{
default: () => <RotateClockwiseIcon />
}}
</NBaseIcon>,
'tipClockwise'
)
const originalSizeNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.resizeToOrignalImageSize}>
{{
default: () => {
return <ResizeSmallIcon />
}
}}
</NBaseIcon>,
'tipOriginalSize'
)
const zoomOutNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.zoomOut}>
{{ default: () => <ZoomOutIcon /> }}
</NBaseIcon>,
'tipZoomOut'
)
const downloadNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.handleDownloadClick}>
{{ default: () => <DownloadIcon /> }}
</NBaseIcon>,
'tipDownload'
)
const closeNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.toggleShow}>
{{ default: () => closeIcon }}
</NBaseIcon>,
'tipClose'
)
const zoomInNode = withTooltip(
<NBaseIcon clsPrefix={clsPrefix} onClick={this.zoomIn}>
{{ default: () => <ZoomInIcon /> }}
</NBaseIcon>,
'tipZoomIn'
)
return (
<>
{this.$slots.default?.()}
@ -521,103 +590,39 @@ export default defineComponent({
{{
default: () => {
if (!this.show) return null
const { withTooltip } = this
return (
<div class={`${clsPrefix}-image-preview-toolbar`}>
{this.onPrev ? (
{renderToolbar ? (
renderToolbar({
nodes: {
prev: prevNode,
next: nextNode,
rotateCounterclockwise:
rotateCounterclockwiseNode,
rotateClockwise: rotateClockwiseNode,
resizeToOriginalSize: originalSizeNode,
zoomOut: zoomOutNode,
zoomIn: zoomInNode,
download: downloadNode,
close: closeNode
}
})
) : (
<>
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.handleSwitchPrev}
>
{{ default: () => prevIcon }}
</NBaseIcon>,
'tipPrevious'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.handleSwitchNext}
>
{{ default: () => nextIcon }}
</NBaseIcon>,
'tipNext'
)}
{this.onPrev ? (
<>
{prevNode}
{nextNode}
</>
) : null}
{rotateCounterclockwiseNode}
{rotateClockwiseNode}
{originalSizeNode}
{zoomOutNode}
{zoomInNode}
{downloadNode}
{closeNode}
</>
) : null}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.rotateCounterclockwise}
>
{{
default: () => (
<RotateCounterclockwiseIcon />
)
}}
</NBaseIcon>,
'tipCounterclockwise'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.rotateClockwise}
>
{{
default: () => <RotateClockwiseIcon />
}}
</NBaseIcon>,
'tipClockwise'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.resizeToOrignalImageSize}
>
{{
default: () => {
return <ResizeSmallIcon />
}
}}
</NBaseIcon>,
'tipOriginalSize'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.zoomOut}
>
{{ default: () => <ZoomOutIcon /> }}
</NBaseIcon>,
'tipZoomOut'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.zoomIn}
>
{{ default: () => <ZoomInIcon /> }}
</NBaseIcon>,
'tipZoomIn'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.handleDownloadClick}
>
{{ default: () => downloadIcon }}
</NBaseIcon>,
'tipDownload'
)}
{withTooltip(
<NBaseIcon
clsPrefix={clsPrefix}
onClick={this.toggleShow}
>
{{ default: () => closeIcon }}
</NBaseIcon>,
'tipClose'
)}
</div>
)

View File

@ -26,17 +26,3 @@ export const closeIcon = (
/>
</svg>
)
export const downloadIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
viewBox="0 0 1024 1024"
>
<path
fill="currentColor"
d="M505.7 661a8 8 0 0 0 12.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"
/>
</svg>
)

View File

@ -3,6 +3,7 @@ import type { ThemeProps } from '../../_mixins'
import { useTheme } from '../../_mixins'
import { createInjectionKey } from '../../_utils'
import type { ImageTheme } from '../styles'
import type { ImageRenderToolbar } from './public-types'
export interface MoveStrategy {
moveVerticalDirection: 'verticalTop' | 'verticalBottom'
@ -16,7 +17,8 @@ export const imagePreviewSharedProps = {
onPreviewPrev: Function as PropType<() => void>,
onPreviewNext: Function as PropType<() => void>,
showToolbar: { type: Boolean, default: true },
showToolbarTooltip: Boolean
showToolbarTooltip: Boolean,
renderToolbar: Function as PropType<ImageRenderToolbar>
}
export interface ImageContext {

View File

@ -0,0 +1,19 @@
import type { VNode, VNodeChild } from 'vue'
export interface ImageRenderToolbarProps {
nodes: {
prev: VNode
next: VNode
rotateCounterclockwise: VNode
rotateClockwise: VNode
resizeToOriginalSize: VNode
zoomOut: VNode
zoomIn: VNode
download: VNode
close: VNode
}
}
export type ImageRenderToolbar = (props: ImageRenderToolbarProps) => VNodeChild
export type ImageGroupRenderToolbarProps = ImageRenderToolbarProps
export type ImageGroupRenderToolbar = ImageRenderToolbar

View File

@ -0,0 +1,43 @@
<markdown>
# Basic
</markdown>
<template>
<n-infinite-scroll style="height: 240px" :distance="10" @load="handleLoad">
<div v-for="i in count" :key="i" class="item">
{{ i }}
</div>
</n-infinite-scroll>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const count = ref(6)
const handleLoad = () => {
count.value += 1
}
return {
count,
handleLoad
}
}
})
</script>
<style>
.item {
display: flex;
align-items: center;
height: 46px;
justify-content: center;
margin-bottom: 10px;
background-color: #e7f5ee;
}
.item:last-child {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,102 @@
<markdown>
# A bit complex example
</markdown>
<template>
<n-infinite-scroll style="height: 240px" :distance="10" @load="handleLoad">
<div
v-for="(item, index) in items"
:key="item.key"
class="message"
:class="{ reverse: index % 5 === 0 }"
>
<img class="avatar" :src="item.avatar" alt="">
<span> {{ item.message }} {{ index % 5 === 0 ? '?' : '' }}</span>
</div>
<div v-if="loading" class="text">
Loading...
</div>
<div v-if="noMore" class="text">
No More 🤪
</div>
</n-infinite-scroll>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const loading = ref(false)
const noMore = computed(() => items.value.length > 16)
const avatars = [
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
'https://avatars.githubusercontent.com/u/20943608?s=60&v=4',
'https://avatars.githubusercontent.com/u/46394163?s=60&v=4',
'https://avatars.githubusercontent.com/u/39197136?s=60&v=4',
'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
]
const messages = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
const mock = (i: number) => ({
key: `${i}`,
value: i,
avatar: avatars[i % avatars.length],
message: messages[Math.floor(Math.random() * messages.length)]
})
const items = ref(Array.from({ length: 10 }, (_, i) => mock(i)))
const handleLoad = async () => {
if (loading.value || noMore.value) return
loading.value = true
await new Promise((resolve) => setTimeout(resolve, 1000))
items.value.push(
...[mock(items.value.length), mock(items.value.length + 1)]
)
loading.value = false
}
return {
items,
noMore,
loading,
handleLoad
}
}
})
</script>
<style>
.message {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 10px;
}
.message:last-child {
margin-bottom: 0;
}
.reverse {
flex-direction: row-reverse;
}
.text {
text-align: center;
}
.reverse .avatar {
margin-left: 10px;
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,22 @@
# Infinite Scroll
Scroll, scroll, scroll, scroll...
Available since `2.38.2`.
## Demos
```demo
basic.vue
chat.vue
```
## API
### Infinite Scroll Props
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| distance | `number` | `0` | Distance threshold that triggers loading. | 2.38.2 |
| scrollbar-props | `Object` | `undefined` | Attribute reference [Scrollbar props](scrollbar#Scrollbar-Props). | 2.38.2 |
| on-load | `() => Promise<void> \| void` | `undefined` | The callback function when scrolling to the bottom. | 2.38.2 |

View File

@ -0,0 +1,43 @@
<markdown>
# 基础
</markdown>
<template>
<n-infinite-scroll style="height: 240px" :distance="10" @load="handleLoad">
<div v-for="i in count" :key="i" class="item">
{{ i }}
</div>
</n-infinite-scroll>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const count = ref(6)
const handleLoad = () => {
count.value += 1
}
return {
count,
handleLoad
}
}
})
</script>
<style>
.item {
display: flex;
align-items: center;
height: 46px;
justify-content: center;
margin-bottom: 10px;
background-color: #e7f5ee;
}
.item:last-child {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,102 @@
<markdown>
# 稍微复杂的例子
</markdown>
<template>
<n-infinite-scroll style="height: 240px" :distance="10" @load="handleLoad">
<div
v-for="(item, index) in items"
:key="item.key"
class="message"
:class="{ reverse: index % 5 === 0 }"
>
<img class="avatar" :src="item.avatar" alt="">
<span> {{ item.message }} {{ index % 5 === 0 ? '?' : '' }}</span>
</div>
<div v-if="loading" class="text">
加载中...
</div>
<div v-if="noMore" class="text">
没有更多了 🤪
</div>
</n-infinite-scroll>
</template>
<script lang="ts">
import { computed, defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const loading = ref(false)
const noMore = computed(() => items.value.length > 16)
const avatars = [
'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
'https://avatars.githubusercontent.com/u/20943608?s=60&v=4',
'https://avatars.githubusercontent.com/u/46394163?s=60&v=4',
'https://avatars.githubusercontent.com/u/39197136?s=60&v=4',
'https://avatars.githubusercontent.com/u/19239641?s=60&v=4'
]
const messages = ['星期一', '星期二', '星期三', '星期四', '星期五']
const mock = (i: number) => ({
key: `${i}`,
value: i,
avatar: avatars[i % avatars.length],
message: messages[Math.floor(Math.random() * messages.length)]
})
const items = ref(Array.from({ length: 10 }, (_, i) => mock(i)))
const handleLoad = async () => {
if (loading.value || noMore.value) return
loading.value = true
await new Promise((resolve) => setTimeout(resolve, 1000))
items.value.push(
...[mock(items.value.length), mock(items.value.length + 1)]
)
loading.value = false
}
return {
items,
noMore,
loading,
handleLoad
}
}
})
</script>
<style>
.message {
display: flex;
align-items: center;
margin-bottom: 10px;
padding: 10px;
}
.message:last-child {
margin-bottom: 0;
}
.reverse {
flex-direction: row-reverse;
}
.text {
text-align: center;
}
.reverse .avatar {
margin-left: 10px;
}
.avatar {
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,22 @@
# 无限滚动
滚雪球,滚啊滚,内容越来越多,停不下来。
`2.38.2` 版本开始提供该组件。
## 演示
```demo
basic.vue
chat.vue
```
## API
### Infinite Scroll Props
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| distance | `number` | `0` | 触发加载的距离阈值 | 2.38.2 |
| scrollbar-props | `Object` | `undefined` | 属性参考 [Scrollbar props](scrollbar#Scrollbar-Props) | 2.38.2 |
| on-load | `() => Promise<void> \| void` | `undefined` | 滚动到底部时的回调函数 | 2.38.2 |

View File

@ -0,0 +1,5 @@
export {
default as NInfiniteScroll,
infiniteScrollProps
} from './src/InfiniteScroll'
export type { InfiniteScrollProps } from './src/InfiniteScroll'

View File

@ -0,0 +1,86 @@
import { h, defineComponent, type PropType, ref } from 'vue'
import type { ExtractPublicPropTypes } from '../../_utils'
import { resolveSlot } from '../../_utils'
import { type ScrollbarProps } from '../../scrollbar/src/Scrollbar'
import { NxScrollbar, type ScrollbarInst } from '../../_internal'
export const infiniteScrollProps = {
distance: {
type: Number,
default: 0
},
onLoad: Function as PropType<() => Promise<void> | void>,
scrollbarProps: Object as PropType<ScrollbarProps>
} as const
export type InfiniteScrollProps = ExtractPublicPropTypes<
typeof infiniteScrollProps
>
export default defineComponent({
name: 'InfiniteScroll',
props: infiniteScrollProps,
setup (props) {
const scrollbarInstRef = ref<ScrollbarInst | null>(null)
let loading = false
const handleCheckBottom = async (): Promise<void> => {
const { value: scrollbarInst } = scrollbarInstRef
if (scrollbarInst) {
const { containerRef, containerScrollTop } = scrollbarInst
const scrollHeight = containerRef?.scrollHeight
const clientHeight = containerRef?.clientHeight
if (
containerRef &&
scrollHeight !== undefined &&
clientHeight !== undefined
) {
if (
containerScrollTop + clientHeight >=
scrollHeight - props.distance
) {
loading = true
try {
await props.onLoad?.()
} catch {}
loading = false
}
}
}
}
const handleScroll = (): void => {
if (loading) return
void handleCheckBottom()
}
const handleWheel = (e: WheelEvent): void => {
if (e.deltaY <= 0) return
if (loading) return
void handleCheckBottom()
}
return {
scrollbarInstRef,
handleScroll,
handleWheel
}
},
render () {
return (
<NxScrollbar
{...this.scrollbarProps}
ref="scrollbarInstRef"
onWheel={this.handleWheel}
onScroll={this.handleScroll}
>
{{
default: () => {
return resolveSlot(this.$slots.default, () => [])
}
}}
</NxScrollbar>
)
}
})

View File

@ -2161,6 +2161,150 @@ exports[`locale works 15`] = `
`;
exports[`locale works 16`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
<div class="n-input-wrapper">
<!---->
<div class="n-input__input"><input type="text" class="n-input__input-el" placeholder="Zadejte" size="20">
<div class="n-input__placeholder"><span>Zadejte</span></div>
<!---->
</div>
<!---->
</div>
<!---->
<!---->
<div class="n-input__border"></div>
<div class="n-input__state-border"></div>
<!---->
</div>
<div>
<div class="n-date-picker" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-icon-color-override: rgba(194, 194, 194, 1); --n-icon-color-disabled-override: rgba(209, 209, 209, 1);">
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);" tabindex="0">
<div class="n-input-wrapper">
<!---->
<div class="n-input__input"><input type="text" class="n-input__input-el" tabindex="-1" placeholder="Vyberte čas" size="20">
<!---->
<!---->
</div>
<div class="n-input__suffix">
<!---->
<!---->
<!----><i class="n-base-icon n-date-picker-icon"><svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M21.75,3 C23.5449254,3 25,4.45507456 25,6.25 L25,21.75 C25,23.5449254 23.5449254,25 21.75,25 L6.25,25 C4.45507456,25 3,23.5449254 3,21.75 L3,6.25 C3,4.45507456 4.45507456,3 6.25,3 L21.75,3 Z M23.5,9.503 L4.5,9.503 L4.5,21.75 C4.5,22.7164983 5.28350169,23.5 6.25,23.5 L21.75,23.5 C22.7164983,23.5 23.5,22.7164983 23.5,21.75 L23.5,9.503 Z M21.75,4.5 L6.25,4.5 C5.28350169,4.5 4.5,5.28350169 4.5,6.25 L4.5,8.003 L23.5,8.003 L23.5,6.25 C23.5,5.28350169 22.7164983,4.5 21.75,4.5 Z"></path>
</g>
</g>
</svg></i>
<!---->
<!---->
</div>
</div>
<!---->
<!---->
<div class="n-input__border"></div>
<div class="n-input__state-border"></div>
<!---->
</div>
<!---->
</div>
<div class="n-date-picker" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-icon-color-override: rgba(194, 194, 194, 1); --n-icon-color-disabled-override: rgba(209, 209, 209, 1);">
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);" tabindex="0">
<div class="n-input-wrapper">
<!---->
<div class="n-input__input"><input type="text" class="n-input__input-el" tabindex="-1" placeholder="Vyberte datum a čas" size="20">
<!---->
<!---->
</div>
<div class="n-input__suffix">
<!---->
<!---->
<!----><i class="n-base-icon n-date-picker-icon"><svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M21.75,3 C23.5449254,3 25,4.45507456 25,6.25 L25,21.75 C25,23.5449254 23.5449254,25 21.75,25 L6.25,25 C4.45507456,25 3,23.5449254 3,21.75 L3,6.25 C3,4.45507456 4.45507456,3 6.25,3 L21.75,3 Z M23.5,9.503 L4.5,9.503 L4.5,21.75 C4.5,22.7164983 5.28350169,23.5 6.25,23.5 L21.75,23.5 C22.7164983,23.5 23.5,22.7164983 23.5,21.75 L23.5,9.503 Z M21.75,4.5 L6.25,4.5 C5.28350169,4.5 4.5,5.28350169 4.5,6.25 L4.5,8.003 L23.5,8.003 L23.5,6.25 C23.5,5.28350169 22.7164983,4.5 21.75,4.5 Z"></path>
</g>
</g>
</svg></i>
<!---->
<!---->
</div>
</div>
<!---->
<!---->
<div class="n-input__border"></div>
<div class="n-input__state-border"></div>
<!---->
</div>
<!---->
</div>
<div class="n-date-picker" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-icon-color-override: rgba(194, 194, 194, 1); --n-icon-color-disabled-override: rgba(209, 209, 209, 1);">
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);" tabindex="0">
<div class="n-input-wrapper">
<!---->
<div class="n-input__input"><input type="text" class="n-input__input-el" tabindex="-1" placeholder="Vyberte rok" size="20">
<!---->
<!---->
</div>
<div class="n-input__suffix">
<!---->
<!---->
<!----><i class="n-base-icon n-date-picker-icon"><svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M21.75,3 C23.5449254,3 25,4.45507456 25,6.25 L25,21.75 C25,23.5449254 23.5449254,25 21.75,25 L6.25,25 C4.45507456,25 3,23.5449254 3,21.75 L3,6.25 C3,4.45507456 4.45507456,3 6.25,3 L21.75,3 Z M23.5,9.503 L4.5,9.503 L4.5,21.75 C4.5,22.7164983 5.28350169,23.5 6.25,23.5 L21.75,23.5 C22.7164983,23.5 23.5,22.7164983 23.5,21.75 L23.5,9.503 Z M21.75,4.5 L6.25,4.5 C5.28350169,4.5 4.5,5.28350169 4.5,6.25 L4.5,8.003 L23.5,8.003 L23.5,6.25 C23.5,5.28350169 22.7164983,4.5 21.75,4.5 Z"></path>
</g>
</g>
</svg></i>
<!---->
<!---->
</div>
</div>
<!---->
<!---->
<div class="n-input__border"></div>
<div class="n-input__state-border"></div>
<!---->
</div>
<!---->
</div>
<div class="n-date-picker" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-icon-color-override: rgba(194, 194, 194, 1); --n-icon-color-disabled-override: rgba(209, 209, 209, 1);">
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);" tabindex="0">
<div class="n-input-wrapper">
<!---->
<div class="n-input__input"><input type="text" class="n-input__input-el" tabindex="-1" placeholder="Vyberte měsíc" size="20">
<!---->
<!---->
</div>
<div class="n-input__suffix">
<!---->
<!---->
<!----><i class="n-base-icon n-date-picker-icon"><svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g fill-rule="nonzero">
<path d="M21.75,3 C23.5449254,3 25,4.45507456 25,6.25 L25,21.75 C25,23.5449254 23.5449254,25 21.75,25 L6.25,25 C4.45507456,25 3,23.5449254 3,21.75 L3,6.25 C3,4.45507456 4.45507456,3 6.25,3 L21.75,3 Z M23.5,9.503 L4.5,9.503 L4.5,21.75 C4.5,22.7164983 5.28350169,23.5 6.25,23.5 L21.75,23.5 C22.7164983,23.5 23.5,22.7164983 23.5,21.75 L23.5,9.503 Z M21.75,4.5 L6.25,4.5 C5.28350169,4.5 4.5,5.28350169 4.5,6.25 L4.5,8.003 L23.5,8.003 L23.5,6.25 C23.5,5.28350169 22.7164983,4.5 21.75,4.5 Z"></path>
</g>
</g>
</svg></i>
<!---->
<!---->
</div>
</div>
<!---->
<!---->
<div class="n-input__border"></div>
<div class="n-input__state-border"></div>
<!---->
</div>
<!---->
</div>
</div>
</div>
</div>"
`;
exports[`locale works 17`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -2304,7 +2448,7 @@ exports[`locale works 16`] = `
</div>"
`;
exports[`locale works 17`] = `
exports[`locale works 18`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -2448,7 +2592,7 @@ exports[`locale works 17`] = `
</div>"
`;
exports[`locale works 18`] = `
exports[`locale works 19`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -2592,7 +2736,7 @@ exports[`locale works 18`] = `
</div>"
`;
exports[`locale works 19`] = `
exports[`locale works 20`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -2736,7 +2880,7 @@ exports[`locale works 19`] = `
</div>"
`;
exports[`locale works 20`] = `
exports[`locale works 21`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -2880,7 +3024,7 @@ exports[`locale works 20`] = `
</div>"
`;
exports[`locale works 21`] = `
exports[`locale works 22`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -3024,7 +3168,7 @@ exports[`locale works 21`] = `
</div>"
`;
exports[`locale works 22`] = `
exports[`locale works 23`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -3168,7 +3312,7 @@ exports[`locale works 22`] = `
</div>"
`;
exports[`locale works 23`] = `
exports[`locale works 24`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -3312,7 +3456,7 @@ exports[`locale works 23`] = `
</div>"
`;
exports[`locale works 24`] = `
exports[`locale works 25`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">
@ -3456,7 +3600,7 @@ exports[`locale works 24`] = `
</div>"
`;
exports[`locale works 25`] = `
exports[`locale works 26`] = `
"<div class="n-config-provider">
<div>
<div class="n-input n-input--resizable n-input--stateful" style="--n-bezier: cubic-bezier(.4, 0, .2, 1); --n-count-text-color: rgb(118, 124, 130); --n-count-text-color-disabled: rgba(194, 194, 194, 1); --n-color: rgba(255, 255, 255, 1); --n-font-size: 14px; --n-border-radius: 3px; --n-height: 34px; --n-padding-left: 12px; --n-padding-right: 12px; --n-text-color: rgb(51, 54, 57); --n-caret-color: #18a058; --n-text-decoration-color: rgb(51, 54, 57); --n-border: 1px solid rgb(224, 224, 230); --n-border-disabled: 1px solid rgb(224, 224, 230); --n-border-hover: 1px solid #36ad6a; --n-border-focus: 1px solid #36ad6a; --n-placeholder-color: rgba(194, 194, 194, 1); --n-placeholder-color-disabled: rgba(209, 209, 209, 1); --n-icon-size: 16px; --n-line-height-textarea: 1.6; --n-color-disabled: rgb(250, 250, 252); --n-color-focus: rgba(255, 255, 255, 1); --n-text-color-disabled: rgba(194, 194, 194, 1); --n-box-shadow-focus: 0 0 0 2px rgba(24, 160, 88, 0.2); --n-loading-color: #18a058; --n-caret-color-warning: #f0a020; --n-color-focus-warning: rgba(255, 255, 255, 1); --n-box-shadow-focus-warning: 0 0 0 2px rgba(240, 160, 32, 0.2); --n-border-warning: 1px solid #f0a020; --n-border-focus-warning: 1px solid #fcb040; --n-border-hover-warning: 1px solid #fcb040; --n-loading-color-warning: #f0a020; --n-caret-color-error: #d03050; --n-color-focus-error: rgba(255, 255, 255, 1); --n-box-shadow-focus-error: 0 0 0 2px rgba(208, 48, 80, 0.2); --n-border-error: 1px solid #d03050; --n-border-focus-error: 1px solid #de576d; --n-border-hover-error: 1px solid #de576d; --n-loading-color-error: #d03050; --n-clear-color: rgba(194, 194, 194, 1); --n-clear-size: 16px; --n-clear-color-hover: rgba(146, 146, 146, 1); --n-clear-color-pressed: rgba(175, 175, 175, 1); --n-icon-color: rgba(194, 194, 194, 1); --n-icon-color-hover: rgba(146, 146, 146, 1); --n-icon-color-pressed: rgba(175, 175, 175, 1); --n-icon-color-disabled: rgba(209, 209, 209, 1); --n-suffix-text-color: rgb(51, 54, 57);">

128
src/locales/common/csCZ.ts Normal file
View File

@ -0,0 +1,128 @@
import type { NLocale } from './enUS'
const csCZ: NLocale = {
name: 'cs-CZ',
global: {
undo: 'Zpět',
redo: 'Obnovit',
confirm: 'Potvrdit',
clear: 'Vyčistit'
},
Popconfirm: {
positiveText: 'Potvrdit',
negativeText: 'Zrušit'
},
Cascader: {
placeholder: 'Prosím vyberte',
loading: 'Načítání',
loadingRequiredMessage: (label: string): string =>
`Prosím načtěte před kontrolou všechny potomky pro ${label}.`
},
Time: {
dateFormat: 'd-M-yyyy',
dateTimeFormat: 'd-M-yyyy HH:mm:ss'
},
DatePicker: {
yearFormat: 'yyyy',
monthFormat: 'MMM',
dayFormat: 'EEEE',
yearTypeFormat: 'yyyy',
monthTypeFormat: 'MMM-yyyy',
dateFormat: 'd-M-yyyy',
dateTimeFormat: 'd-M-yyyy HH:mm:ss',
quarterFormat: 'qqq-yyyy',
weekFormat: 'yyyy-w',
clear: 'Vyčistit',
now: 'Teď',
confirm: 'Potvrdit',
selectTime: 'Vybrat čas',
selectDate: 'Vybrat datum',
datePlaceholder: 'Vyberte čas',
datetimePlaceholder: 'Vyberte datum a čas',
monthPlaceholder: 'Vyberte měsíc',
yearPlaceholder: 'Vyberte rok',
quarterPlaceholder: 'Vyberte čtvrtletí',
weekPlaceholder: 'Vyberte týden',
startDatePlaceholder: 'Datum začátku',
endDatePlaceholder: 'Datum ukončení',
startDatetimePlaceholder: 'Datum a čas začátku',
endDatetimePlaceholder: 'Datum a čas ukončení ',
startMonthPlaceholder: 'Začátek měsíce',
endMonthPlaceholder: 'Konec měsíce',
monthBeforeYear: true,
firstDayOfWeek: 6 as 0 | 1 | 2 | 3 | 4 | 5 | 6,
today: 'Dnes'
},
DataTable: {
checkTableAll: 'Vybrat vše v tabulce',
uncheckTableAll: 'Zrušit výběr všeho v tabulce ',
confirm: 'Potvrdit',
clear: 'Vyčistit'
},
LegacyTransfer: {
sourceTitle: 'Zdroj',
targetTitle: 'Cíl'
},
Transfer: {
selectAll: 'Vybrat vše',
unselectAll: 'Odznačit vše',
clearAll: 'Vyčistit',
total: (num: number): string => `Celkem ${num} položek`,
selected: (num: number): string => `${num} položek vybráno`
},
Empty: {
description: 'Žádná data'
},
Select: {
placeholder: 'Prosím vyberte'
},
TimePicker: {
placeholder: 'Vybrat čas',
positiveText: 'OK',
negativeText: 'Zrušit',
now: 'Teď',
clear: 'Vyčistit'
},
Pagination: {
goto: 'Jít na',
selectionSuffix: 'Strana'
},
DynamicTags: {
add: 'Přidat'
},
Log: {
loading: 'Načítání'
},
Input: {
placeholder: 'Zadejte'
},
InputNumber: {
placeholder: 'Zadejte'
},
DynamicInput: {
create: 'Vytvořit'
},
ThemeEditor: {
title: 'Editor témat',
clearAllVars: 'Vymazat všechny proměnné',
clearSearch: 'Vymazat vyhledávání',
filterCompName: 'Filtrovat název komponenty',
filterVarName: 'Filztrovat název proměnné',
import: 'Importovat',
export: 'Exportovat',
restore: 'Obnovit původní nastavení'
},
Image: {
tipPrevious: 'Předchozí obrázek (←)',
tipNext: 'Další obrázek (→)',
tipCounterclockwise: 'Proti směru hodinových ručiček',
tipClockwise: 'Ve směru hodinových ručiček',
tipZoomOut: 'Oddálit',
tipZoomIn: 'Přiblížit',
tipDownload: 'Stáhnout',
tipClose: 'Zavřít (Esc)',
tipOriginalSize: 'Přiblížit na původní velikost'
}
}
export default csCZ

View File

@ -47,9 +47,8 @@ const itIT: NLocale = {
endDatePlaceholder: 'Data fine',
startDatetimePlaceholder: 'Data e ora di inizio',
endDatetimePlaceholder: 'Data e ora di fine',
// FIXME: translation needed
startMonthPlaceholder: 'Start Month',
endMonthPlaceholder: 'End Month',
startMonthPlaceholder: 'Mese di inizio',
endMonthPlaceholder: 'Mese di fine',
monthBeforeYear: true,
firstDayOfWeek: 0 as 0 | 1 | 2 | 3 | 4 | 5 | 6,
today: 'Oggi'
@ -64,13 +63,18 @@ const itIT: NLocale = {
sourceTitle: 'Fonte',
targetTitle: 'Destinazione'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
selectAll: 'Seleziona tutto',
unselectAll: 'Deseleziona tutto',
clearAll: 'Pulisci',
total: (num: number): string => {
if (num !== 1) return `${num} elementi in totale`
return '1 elemento in totale'
},
selected: (num: number): string => {
if (num !== 1) return `${num} elementi selezionati`
return '1 elemento selezionato'
}
},
Empty: {
description: 'Nessun Dato'
@ -123,8 +127,7 @@ const itIT: NLocale = {
tipZoomIn: 'Riduci',
tipDownload: 'Download',
tipClose: 'Chiudi (Esc)',
// TODO: translation
tipOriginalSize: 'Zoom to original size'
tipOriginalSize: 'Torna alla dimensione originale'
}
}

View File

@ -42,14 +42,13 @@ const skSK: NLocale = {
monthPlaceholder: 'Vyberte mesiac',
yearPlaceholder: 'Vyberte rok',
quarterPlaceholder: 'Vyberte štvrťrok',
weekPlaceholder: 'Select Week',
weekPlaceholder: 'Vyberte týždeň',
startDatePlaceholder: 'Dátum začiatku',
endDatePlaceholder: 'Dátum ukončenia',
startDatetimePlaceholder: 'Dátum a čas začiatku',
endDatetimePlaceholder: 'Dátum a čas ukončenia ',
// FIXME: translation needed
startMonthPlaceholder: 'Start Month',
endMonthPlaceholder: 'End Month',
startMonthPlaceholder: 'Začiatok mesiaca',
endMonthPlaceholder: 'Koniec mesiaca',
monthBeforeYear: true,
firstDayOfWeek: 6 as 0 | 1 | 2 | 3 | 4 | 5 | 6,
today: 'Dnes'
@ -64,13 +63,12 @@ const skSK: NLocale = {
sourceTitle: 'Zdroj',
targetTitle: 'Cieľ'
},
// TODO: translation
Transfer: {
selectAll: 'Select all',
unselectAll: 'Unselect all',
clearAll: 'Clear',
total: (num: number): string => `Total ${num} items`,
selected: (num: number): string => `${num} items selected`
selectAll: 'Vybrať všetko',
unselectAll: 'odznačiť všetko',
clearAll: 'Vyčistiť',
total: (num: number): string => `Celkom ${num} položiek`,
selected: (num: number): string => `Vybratých ${num} položiek`
},
Empty: {
description: 'Žiadne dáta'
@ -86,7 +84,7 @@ const skSK: NLocale = {
clear: 'Vyčistiť'
},
Pagination: {
goto: 'Ísť',
goto: 'Ísť na',
selectionSuffix: 'Strana'
},
DynamicTags: {
@ -108,13 +106,12 @@ const skSK: NLocale = {
title: 'Editor tém',
clearAllVars: 'Vymazať všetky premenné',
clearSearch: 'Vymazať vyhľadávanie',
filterCompName: 'Názov komponentu filtra',
filterVarName: 'Názov premennej filtra',
filterCompName: 'Filtrovať názov komponentu',
filterVarName: 'Filtrovať názov premennej',
import: 'Importovať',
export: 'Exportovať',
restore: 'Obnoviť pôvodné nastavenia'
},
// TODO: translation
Image: {
tipPrevious: 'Predchádzajúci obrázok (←)',
tipNext: 'Ďalší obrázok (→)',
@ -124,8 +121,7 @@ const skSK: NLocale = {
tipZoomIn: 'Priblížiť',
tipDownload: 'Sťahovať',
tipClose: 'Zavrieť (Esc)',
// TODO: translation
tipOriginalSize: 'Zoom to original size'
tipOriginalSize: 'Priblížiť na pôvodnú veľkosť'
}
}

9
src/locales/date/csCZ.ts Normal file
View File

@ -0,0 +1,9 @@
import cs from 'date-fns/esm/locale/cs'
import { type NDateLocale } from './enUS'
const dateCsCZ: NDateLocale = {
name: 'cs-CZ',
locale: cs
}
export default dateCsCZ

View File

@ -16,6 +16,7 @@ import {
esAR,
itIT,
skSK,
csCZ,
enGB,
plPL,
ptBR,
@ -41,6 +42,7 @@ import {
dateEsAR,
dateItIT,
dateSkSK,
dateCsCZ,
dateEnGB,
datePlPL,
datePtBR,
@ -274,6 +276,14 @@ describe('locale', () => {
}
}).html()
).toMatchSnapshot()
expect(
mount(Wrapper, {
props: {
dateLocale: dateCsCZ,
locale: csCZ
}
}).html()
).toMatchSnapshot()
expect(
mount(Wrapper, {
props: {

View File

@ -13,6 +13,7 @@ export { default as frFR } from './common/frFR'
export { default as esAR } from './common/esAR'
export { default as itIT } from './common/itIT'
export { default as skSK } from './common/skSK'
export { default as csCZ } from './common/csCZ'
export { default as enGB } from './common/enGB'
export { default as plPL } from './common/plPL'
export { default as ptBR } from './common/ptBR'
@ -39,6 +40,7 @@ export { default as dateFrFR } from './date/frFR'
export { default as dateEsAR } from './date/esAR'
export { default as dateItIT } from './date/itIT'
export { default as dateSkSK } from './date/skSK'
export { default as dateCsCZ } from './date/csCZ'
export { default as dateEnGB } from './date/enGB'
export { default as datePlPL } from './date/plPL'
export { default as datePtBR } from './date/ptBR'

View File

@ -30,8 +30,8 @@ Mention requires `v2.2.0` and above.
| separator | `string` | `' '` | Character to split mentions. The string length must be exactly 1. | |
| bordered | `boolean` | `true` | Whether to display the border of the input element. | |
| disabled | `boolean` | `false` | Whether to disable the input element. | |
| value | `string \| null` | `undefined` | Manually set input value. | |
| default-value | `string` | `''` | Default value when the value is not manually set. | |
| filter | `(pattern: string, option: MentionOption) => boolean` | Default filter method | Method to filter options corresponding to `pattern`. | 2.38.2 |
| loading | `boolean` | `false` | Whether the selection panel of mentions is in a loading state. | |
| prefix | `string \| string[]` | `'@'` | Prefix character(s) to trigger mentions. The string length(s) must be exactly 1. | |
| placeholder | `string` | `''` | Placeholder. | |
@ -40,6 +40,7 @@ Mention requires `v2.2.0` and above.
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | Input size. | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | Validation status. | 2.27.0 |
| to | `string \| HTMLElement \| false` | `body` | Container node of the menu. `false` will keep it not detached. | |
| value | `string \| null` | `undefined` | Manually set input value. | |
| on-update:show | `(show: boolean) => void` | `undefined` | Callback when the selection panel of mentions is shown or hidden. | 2.34.0 |
| on-update:value | `(value: string) => void` | `undefined` | Triggered when the input box value is updated. | |
| on-select | `(option: MentionOption, prefix: string) => void` | `undefined` | Triggered when the input box is selected. | |

View File

@ -30,8 +30,8 @@ Mention 在 `v2.2.0` 及以后可用。
| separator | `string` | `' '` | 切分提及使用的字符,长度必须为 1 | |
| bordered | `boolean` | `true` | 是否显示输入框边框 | |
| disabled | `boolean` | `false` | 是否设置输入框为禁用状态 | |
| value | `string \| null` | `undefined` | 输入框的值 | |
| default-value | `string` | `''` | 输入框的默认值 | |
| filter | `(pattern: string, option: MentionOption) => boolean` | 内置的过滤函数 | 根据 `pattern` 决定显示那些选项的过滤函数 | 2.38.2 |
| loading | `boolean` | `false` | 选择面板是否显示加载状态 | |
| prefix | `string \| string[]` | `'@'` | 触发提及的前缀,长度必须为 1 | |
| placeholder | `string` | `''` | 输入框的占位符 | |
@ -40,6 +40,7 @@ Mention 在 `v2.2.0` 及以后可用。
| size | `'small' \| 'medium' \| 'large'` | `'medium'` | 输入框的大小 | |
| status | `'success' \| 'warning' \| 'error'` | `undefined` | 验证状态 | 2.27.0 |
| to | `string \| HTMLElement \| false` | `body` | 菜单的容器节点,`false` 会待在原地 | |
| value | `string \| null` | `undefined` | 输入框的值 | |
| on-update:show | `(show: boolean) => void` | `undefined` | 选择面板显示状态发生变化时触发 | 2.34.0 |
| on-update:value | `(value: string) => void` | `undefined` | 输入框值发生更新时触发 | |
| on-select | `(option: MentionOption, prefix: string) => void` | `undefined` | 输入框的选中时触发 | |

View File

@ -51,6 +51,21 @@ export const mentionProps = {
type: Array as PropType<MentionOption[]>,
default: []
},
filter: {
type: Function as PropType<
(pattern: string, option: MentionOption) => boolean
>,
default: (pattern: string, option: MentionOption) => {
if (!pattern) return true
if (typeof option.label === 'string') {
return option.label.startsWith(pattern)
}
if (typeof option.value === 'string') {
return option.value.startsWith(pattern)
}
return false
}
},
type: {
type: String as PropType<'text' | 'textarea'>,
default: 'text'
@ -147,16 +162,7 @@ export default defineComponent({
let cachedPartialPatternEnd: number | null = null
const filteredOptionsRef = computed(() => {
const { value: pattern } = partialPatternRef
return props.options.filter((option) => {
if (!pattern) return true
if (typeof option.label === 'string') {
return option.label.startsWith(pattern)
}
if (typeof option.value === 'string') {
return option.value.startsWith(pattern)
}
return false
})
return props.options.filter((option) => props.filter(pattern, option))
})
const treeMateRef = computed(() => {
return createTreeMate<

View File

@ -213,7 +213,7 @@ export const NSubmenu = defineComponent({
default: () => (
<div
class={`${mergedClsPrefix}-submenu`}
role="menuitem"
role="menu"
aria-expanded={!this.collapsed}
id={this.domId}
>
@ -226,7 +226,7 @@ export const NSubmenu = defineComponent({
) : (
<div
class={`${mergedClsPrefix}-submenu`}
role="menuitem"
role="menu"
aria-expanded={!this.collapsed}
id={this.domId}
>

View File

@ -7,34 +7,55 @@ You can use `useModal.create` to create a modal. (Please make sure this API is c
</markdown>
<template>
<n-button @click="handleClick">
Start me up
</n-button>
<n-flex>
<n-button @click="showDialogPreset">
Start me up Dialog
</n-button>
<n-button @click="showCardPreset">
Start me up Card
</n-button>
</n-flex>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useModal, useMessage } from 'naive-ui'
import { defineComponent, h } from 'vue'
import { useModal, useMessage, NButton } from 'naive-ui'
export default defineComponent({
setup () {
const modal = useModal()
const message = useMessage()
const handleClick = () => {
const modalInst = modal.create({
title: 'Modal',
content: 'Content',
preset: 'dialog'
const showDialogPreset = () => {
const m = modal.create({
title: 'Dialog perset',
preset: 'dialog',
content: 'Content'
})
message.info('Shut down in three seconds')
setTimeout(() => {
modalInst.destroy()
m.destroy()
}, 3000)
}
const showCardPreset = () => {
const m = modal.create({
title: 'Card preset',
preset: 'card',
style: {
width: '400px'
},
content: 'Content',
footer: () =>
h(
NButton,
{ type: 'primary', onClick: () => m.destroy() },
() => 'Close'
)
})
}
return {
handleClick
showDialogPreset,
showCardPreset
}
}
})

View File

@ -7,34 +7,55 @@
</markdown>
<template>
<n-button @click="handleClick">
来吧
</n-button>
<n-flex>
<n-button @click="showDialogPreset">
来吧 Dialog
</n-button>
<n-button @click="showCardPreset">
来吧 Card
</n-button>
</n-flex>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useModal, useMessage } from 'naive-ui'
import { defineComponent, h } from 'vue'
import { useModal, useMessage, NButton } from 'naive-ui'
export default defineComponent({
setup () {
const modal = useModal()
const message = useMessage()
const handleClick = () => {
const modalInst = modal.create({
title: '模态框',
content: '内容',
preset: 'dialog'
const showDialogPreset = () => {
const m = modal.create({
title: 'Dialog 预设',
preset: 'dialog',
content: '内容'
})
message.info('三秒钟后关闭')
setTimeout(() => {
modalInst.destroy()
m.destroy()
}, 3000)
}
const showCardPreset = () => {
const m = modal.create({
title: 'Card 预设',
preset: 'card',
style: {
width: '400px'
},
content: '内容',
footer: () =>
h(
NButton,
{ type: 'primary', onClick: () => m.destroy() },
() => '关闭'
)
})
}
return {
handleClick
showDialogPreset,
showCardPreset
}
}
})

View File

@ -76,14 +76,14 @@ export const NModalProvider = defineComponent({
const clickedPositionRef = useClickPosition()
const modalListRef = ref<TypeSafeModalReactive[]>([])
const modalInstRefs: Record<string, ModalInst> = {}
const modalInstRefs: Record<string, ModalInst | undefined> = {}
function create (options: ModalOptions = {}): ModalReactive {
const key = createId()
const modalReactive = reactive({
...options,
key,
destroy: () => {
modalInstRefs[`n-modal-${key}`].hide()
modalInstRefs[`n-modal-${key}`]?.hide()
}
})
modalListRef.value.push(modalReactive)
@ -100,7 +100,7 @@ export const NModalProvider = defineComponent({
function destroyAll (): void {
Object.values(modalInstRefs).forEach((modalInstRef) => {
modalInstRef.hide()
modalInstRef?.hide()
})
}
@ -131,8 +131,7 @@ export const NModalProvider = defineComponent({
this.modalList.map((modal) =>
h(
NModalEnvironment,
omit(modal, ['destroy', 'style'], {
internalStyle: modal.style,
omit(modal, ['destroy'], {
to: modal.to ?? this.to,
ref: ((inst: ModalInst | null) => {
if (inst === null) {

View File

@ -13,6 +13,7 @@ size.vue
color.vue
error-correction.vue
download.vue
type.vue
```
## API
@ -31,6 +32,7 @@ download.vue
| padding | `number \| string` | `12` | Padding size of the QR Code. | 2.36.0 |
| value | `string` | `''` | Text information. | 2.36.0 |
| size | `number` | `100` | Size of the qrcode. | 2.36.0 |
| type | `'canvas'` \| `'svg'` | `'canvas'` | Customize Render Type. | 2.38.2 |
### About QR code error correction level

View File

@ -0,0 +1,26 @@
<markdown>
# Customize Render Type
Customize rendering output by setting `type`, providing two options: `canvas` and `svg`.
</markdown>
<template>
<n-space>
<n-qr-code :value="text" type="canvas" />
<n-qr-code :value="text" type="svg" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('The rain dampened the sky')
return {
text
}
}
})
</script>

View File

@ -20,6 +20,7 @@
<n-qr-code
value="https://www.naiveui.com/"
icon-src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
type="svg"
:icon-size="32"
error-correction-level="H"
/>

View File

@ -13,6 +13,7 @@ size.vue
color.vue
error-correction.vue
download.vue
type.vue
```
## API
@ -31,6 +32,7 @@ download.vue
| padding | `number \| string` | `12` | 二维码填充大小 | 2.36.0 |
| value | `string` | `''` | 文本信息 | 2.36.0 |
| size | `number` | `100` | 二维码大小 | 2.36.0 |
| type | `'canvas'` \| `'svg'` | `'canvas'` | 自定义二维码渲染类型 | 2.38.2 |
### 关于二维码纠错级别

View File

@ -0,0 +1,26 @@
<markdown>
# 自定义渲染类型
通过设置 `type` 自定义渲染结果提供 `canvas` `svg` 两个选项
</markdown>
<template>
<n-space>
<n-qr-code :value="text" type="canvas" />
<n-qr-code :value="text" type="svg" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
const text = ref('雨淋湿了天空')
return {
text
}
}
})
</script>

View File

@ -14,6 +14,8 @@ import style from './styles/index.cssr'
import { type QrCodeTheme, qrcodeLight } from '../styles'
import qrcodegen from './qrcodegen'
type Modules = ReturnType<qrcodegen.QrCode['getModules']>
const ERROR_CORRECTION_LEVEL: Record<string, qrcodegen.QrCode.Ecc> = {
L: qrcodegen.QrCode.Ecc.LOW,
M: qrcodegen.QrCode.Ecc.MEDIUM,
@ -56,6 +58,10 @@ export const qrCodeProps = {
errorCorrectionLevel: {
type: String,
default: 'M'
},
type: {
type: String,
default: 'canvas'
}
} as const
@ -104,6 +110,7 @@ export default defineComponent({
const imageLoadedTrigger = ref(0)
let loadedIcon: HTMLImageElement | null = null
watchEffect(() => {
if (props.type === 'svg') return
void imageLoadedTrigger.value
drawCanvas(
qr.value,
@ -120,7 +127,9 @@ export default defineComponent({
: null
)
})
watchEffect(() => {
if (props.type === 'svg') return
const { iconSrc } = props
if (iconSrc) {
let aborted = false
@ -197,11 +206,112 @@ export default defineComponent({
}
}
function generatePath (modules: Modules, margin: number = 0): string {
const ops: string[] = []
modules.forEach(function (row, y) {
let start: number | null = null
row.forEach(function (cell, x) {
if (!cell && start !== null) {
// M0 0h7v1H0z injects the space with the move and drops the comma,
// saving a char per operation
ops.push(
`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`
)
start = null
return
}
// end of row, clean up or skip
if (x === row.length - 1) {
if (!cell) {
// We would have closed the op above already so this can only mean
// 2+ light modules in a row.
return
}
if (start === null) {
// Just a single dark module.
ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`)
} else {
// Otherwise finish the current line.
ops.push(
`M${start + margin},${y + margin} h${x + 1 - start}v1H${
start + margin
}z`
)
}
return
}
if (cell && start === null) {
start = x
}
})
})
return ops.join('')
}
function svgInfo (
qr: qrcodegen.QrCode,
size: number,
iconConfig: {
iconSrc: string
iconBorderRadius: number
iconSize: number
iconBackgroundColor: string
} | null
): {
innerHtml: string
numCells: number
} {
const cells = qr.getModules()
const numCells = cells.length
const cellsToDraw = cells
let svgInnerHtml = ''
const path1Html = `<path fill="transparent" d="M0,0 h${numCells}v${numCells}H0z" shape-rendering="crispEdges"></path>`
const path2Html = `<path fill="${props.color}" d="${generatePath(cellsToDraw, 0)}" shape-rendering="crispEdges"></path>`
let iconHtml = ''
if (iconConfig) {
const { iconSrc, iconSize } = iconConfig
const DEFAULT_IMG_SCALE = 0.1
const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE)
const scale = numCells / size
const h = (iconSize || defaultSize) * scale
const w = (iconSize || defaultSize) * scale
const x = cells.length / 2 - w / 2
const y = cells.length / 2 - h / 2
iconHtml += `<image href="${iconSrc}" width="${w}" height="${h}" x="${x}" y="${y}" preserveAspectRatio="none"></image>`
}
svgInnerHtml += path1Html
svgInnerHtml += path2Html
svgInnerHtml += iconHtml
return {
innerHtml: svgInnerHtml,
numCells
}
}
const svgInfoRef = computed(() =>
svgInfo(
qr.value,
props.size,
props.iconSrc
? {
iconSrc: props.iconSrc,
iconBorderRadius: props.iconBorderRadius,
iconSize: props.iconSize,
iconBackgroundColor: props.iconBackgroundColor
}
: null
)
)
return {
canvasRef,
mergedClsPrefix: mergedClsPrefixRef,
cssVars: inlineThemeDisabled ? undefined : cssVarsRef,
themeClass: themeClassHandle?.themeClass
themeClass: themeClassHandle?.themeClass,
svgInfo: svgInfoRef
}
},
render () {
@ -211,8 +321,10 @@ export default defineComponent({
padding,
cssVars,
themeClass,
size
size,
type
} = this
return (
<div
class={[`${mergedClsPrefix}-qr-code`, themeClass]}
@ -224,13 +336,23 @@ export default defineComponent({
...cssVars
}}
>
<canvas
ref="canvasRef"
style={{
width: `${size}px`,
height: `${size}px`
}}
/>
{type === 'canvas' ? (
<canvas
ref="canvasRef"
style={{
width: `${size}px`,
height: `${size}px`
}}
/>
) : (
<svg
height={size}
width={size}
viewBox={`0 0 ${this.svgInfo.numCells} ${this.svgInfo.numCells}`}
role="img"
innerHTML={this.svgInfo.innerHtml}
/>
)}
</div>
)
}

View File

@ -265,6 +265,11 @@ namespace qrcodegen {
)
}
// Modified to expose modules for easy access
public getModules (): boolean[][] {
return this.modules
}
/* -- Private helper methods for constructor: Drawing function modules -- */
// Reads this object's version field, and draws and marks all function modules.

View File

@ -0,0 +1,37 @@
<markdown>
# Custom scrollbar style
You can use `theme-overrides` to control the style of the scrollbar.
</markdown>
<template>
<n-config-provider
:theme-overrides="{
Scrollbar: {
width: '8px',
railInsetHorizontal: '4px 4px 4px auto',
borderRadius: 0
}
}"
>
<n-scrollbar style="max-height: 120px">
我们在田野上面找猪<br>
想象中已找到了三只<br>
小鸟在白云上面追逐<br>
它们在树底下跳舞<br>
啦啦啦啦啦啦啦啦咧<br>
啦啦啦啦咧<br>
我们在想象中度过了许多年<br>
想象中我们是如此的疯狂<br>
我们在城市里面找猪<br>
想象中已找到了几百万只<br>
小鸟在公园里面唱歌<br>
它们独自在想象里跳舞<br>
啦啦啦啦啦啦啦啦咧<br>
啦啦啦啦咧<br>
我们在想象中度过了许多年<br>
许多年之后我们又开始想象<br>
啦啦啦啦啦啦啦啦咧
</n-scrollbar>
</n-config-provider>
</template>

View File

@ -8,6 +8,8 @@ It looks better but I'm sure it's not as reliable as native scrollbar.
basic.vue
x.vue
trigger.vue
no-sync.vue
custom.vue
```
## API
@ -16,10 +18,12 @@ trigger.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| content-class | `string` | `undefined` | Class name of content div. | 2.38.2 |
| content-style | `string \| object` | `undefined` | Style of content div. | 2.38.2 |
| size | `number` | `undefined` | Size of scrollbar. | 2.34.4 |
| trigger | `'hover' \| 'none'` | `'hover'` | Trigger of show scrollbar. `'none'` means always show it. | 2.29.1 |
| x-scrollable | `boolean` | `false` | Whether it can scroll horizontally. | |
| on-scroll | `(e: Event) => void` | `undefined` | Callback on scroll | |
| size | `number` | `undefined` | Size of scrollbar. | 2.34.4 |
### Scrollbar Slots

View File

@ -0,0 +1,27 @@
<markdown>
# Mouse dragging cannot scroll to the bottom
When the last element has a `margin-bottom`, dragging the scrollbar with the mouse cannot scroll to the bottom. This is caused by the native scrolling behavior of the browser. `n-scrollbar` cannot automatically handle this issue. You can solve this problem by setting `content-style` to `overflow: hidden;`.
</markdown>
<template>
<n-scrollbar style="max-height: 120px">
Without content-style
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
</n-scrollbar>
<n-divider />
<n-scrollbar style="max-height: 120px" content-style="overflow: hidden;">
content-style="overflow: hidden;"
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
</n-scrollbar>
</template>

View File

@ -0,0 +1,37 @@
<markdown>
# 自定义样式
你可以通过 `theme-overrides` 去控制滚动条的样式
</markdown>
<template>
<n-config-provider
:theme-overrides="{
Scrollbar: {
width: '8px',
railInsetHorizontal: '4px 4px 4px auto',
borderRadius: 0
}
}"
>
<n-scrollbar style="max-height: 120px">
我们在田野上面找猪<br>
想象中已找到了三只<br>
小鸟在白云上面追逐<br>
它们在树底下跳舞<br>
啦啦啦啦啦啦啦啦咧<br>
啦啦啦啦咧<br>
我们在想象中度过了许多年<br>
想象中我们是如此的疯狂<br>
我们在城市里面找猪<br>
想象中已找到了几百万只<br>
小鸟在公园里面唱歌<br>
它们独自在想象里跳舞<br>
啦啦啦啦啦啦啦啦咧<br>
啦啦啦啦咧<br>
我们在想象中度过了许多年<br>
许多年之后我们又开始想象<br>
啦啦啦啦啦啦啦啦咧
</n-scrollbar>
</n-config-provider>
</template>

View File

@ -8,7 +8,9 @@
basic.vue
x.vue
trigger.vue
no-sync.vue
rtl-debug.vue
custom.vue
```
## API
@ -17,10 +19,12 @@ rtl-debug.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| content-class | `string` | `undefined` | 内容 div 的类名 | 2.38.2 |
| content-style | `string \| object` | `undefined` | 内容 div 的 style | 2.38.2 |
| size | `number` | `undefined` | 滚动条的大小 | 2.34.4 |
| trigger | `'hover' \| 'none'` | `'hover'` | 显示滚动条的时机,`'none'` 表示一直显示 | 2.29.1 |
| x-scrollable | `boolean` | `false` | 是否可以横向滚动 | |
| on-scroll | `(e: Event) => void` | `undefined` | 滚动的回调 | |
| size | `number` | `undefined` | 滚动条的大小 | 2.34.4 |
### Scrollbar Slots

View File

@ -0,0 +1,27 @@
<markdown>
# 鼠标拖动滚不到最下面
在最后一个元素含有 `margin-bottom` 的情况下通过鼠标拖动滚动条滚动不到最下面这是浏览器的原生滚动行为导致的`n-scrollbar` 无法自动的处理这种问题你可以通过设定 `content-style` `'overflow: hidden;'` 来解决这个问题
</markdown>
<template>
<n-scrollbar style="max-height: 120px">
不设定 content-style
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
</n-scrollbar>
<n-divider />
<n-scrollbar style="max-height: 120px" content-style="overflow: hidden;">
content-style="overflow: hidden;"
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
<p style="margin-bottom: 90px">
margin-bottom: 90px
</p>
</n-scrollbar>
</template>

View File

@ -24,6 +24,8 @@ export const scrollbarProps = {
trigger: String as PropType<'none' | 'hover'>,
xScrollable: Boolean,
onScroll: Function as PropType<(e: Event) => void>,
contentClass: String,
contentStyle: [Object, String] as PropType<string | Record<string, any>>,
size: Number
} as const

View File

@ -723,6 +723,14 @@ export default defineComponent({
ref={this.setHandleRefs(index)}
class={`${mergedClsPrefix}-slider-handle-wrapper`}
tabindex={this.mergedDisabled ? -1 : 0}
role="slider"
aria-valuenow={value}
aria-valuemin={this.min}
aria-valuemax={this.max}
aria-orientation={
this.vertical ? 'vertical' : 'horizontal'
}
aria-disabled={this.disabled}
style={this.getHandleStyle(value, index)}
onFocus={() => {
this.handleHandleFocus(index)

View File

@ -211,4 +211,28 @@ describe('n-slider', () => {
expect((handle.element as HTMLElement).style.left).toEqual('30%')
wrapper.unmount()
})
it('should have the aria role of "slider"', () => {
const wrapper = mount(NSlider)
expect(wrapper.find('.n-slider-handle-wrapper').attributes('role')).toBe(
'slider'
)
wrapper.unmount()
})
it('should be some aria properties for "slider"', () => {
const wrapper = mount(NSlider, {
props: {
defaultValue: 50,
disabled: true
}
})
const handle = wrapper.find('.n-slider-handle-wrapper')
expect(handle.attributes('aria-valuenow')).toBe('50')
expect(handle.attributes('aria-valuemin')).toBe('0')
expect(handle.attributes('aria-valuemax')).toBe('100')
expect(handle.attributes('aria-orientation')).toBe('horizontal')
expect(handle.attributes('aria-disabled')).toBe('true')
wrapper.unmount()
})
})

View File

@ -4,7 +4,13 @@
<template>
<n-flex vertical>
<n-input-number v-model:value="split" :step="0.1" clearable />
<n-input-number
v-model:value="split"
:step="0.1"
clearable
:max="1"
:min="0"
/>
<NSplit v-model:size="split" style="height: 200px">
<template #1>
<div style="width: 100%; background-color: black" />

View File

@ -21,23 +21,27 @@ controlled.vue
| Name | Type | Default | Description | Version |
| --- | --- | --- | --- | --- |
| default-size | `number` | `0.5` | Default split size, 0~1 is a percentage. | 2.36.0 |
| size | `number` | `undefined` | Split is the controlled split size, with 0~1 representing the percentage. | 2.38.0 |
| disabled | `boolean` | `false` | Whether to disable the split. | 2.36.0 |
| default-size | `number` | `0.5` | Default split size, when it's `number` it should in 0 ~ 1, when it's `string` it should be formatted in `${number}px`. | 2.36.0, `string` 2.38.2 |
| direction | `'horizontal' \| 'vertical'` | `'horizontal'` | The direction of the split. | 2.36.0 |
| min | `number` | `0` | The minimum threshold for splitting, 0~1 is a percentage. | 2.36.0 |
| max | `number` | `1` | The maximum split threshold, 0~1 is a percentage. | 2.36.0 |
| disabled | `boolean` | `false` | Whether to disable the split. | 2.36.0 |
| max | `string \| number` | `1` | The maximum split threshold, when it's `number` it should in 0 ~ 1, when it's `string` it should be formatted in `${number}px`. | 2.36.0, `string` 2.38.2 |
| min | `string \| number` | `0` | The minimum threshold for splitting, when it's `number` it should in 0 ~ 1, when it's `string` it should be formatted in `${number}px`. | 2.36.0, `string` 2.38.2 |
| pane1-class | `string` | `undefined` | The class name of the first pane. | 2.38.2 |
| pane1-style | `Object \| string` | `undefined` | The Style of the first pane | 2.38.2 |
| pane2-class | `string` | `undefined` | The class name of the second pane. | 2.38.2 |
| pane2-style | `Object \| string` | `undefined` | The Style of the second pane | 2.38.2 |
| resize-trigger-size | `number` | `3` | Size of the resize trigger. | 2.36.0 |
| size | `string \| number` | `undefined` | Split is the controlled split size, when it's `number` it should in 0 ~ 1, when it's `string` it should be formatted in `${number}px`. | 2.38.0, `string` 2.38.2 |
| watch-props | `Array<'defaultSize'>` | `undefined` | Default prop names that needed to be watched. Components will be updated after the prop is changed. Note: the `watch-props` itself is not reactive. | 2.38.0 |
| on-drag-start | `(e: Event) => void` | `undefined` | Callback function when drag start. | 2.36.0 |
| on-drag-move | `(e: Event) => void` | `undefined` | Callback function when dragging. | 2.36.0 |
| on-drag-end | `(e: Event) => void` | `undefined` | Callback function when drag end. | 2.36.0 |
| on-update:size | `(value: number) => void` | `undefined` | Callback fired on size changes. | 2.38.0 |
| on-update:size | `(value: string \| number) => void` | `undefined` | Callback fired on `size` changes. If `props.value` or `props.defaultValue` is `string`, `value` is `string`. | 2.38.0, `string` 2.38.2 |
### Split Slots
| Name | Parameters | Description | Version |
| -------------- | ---------- | ------------------------- | ------- |
| 1 | `()` | The first panel content. | 2.36.0 |
| 2 | `()` | The Second panel content. | 2.36.0 |
| resize-trigger | `()` | Split bar content. | 2.36.0 |
| Name | Parameters | Description | Version |
| -------------- | ---------- | ------------------------ | ------- |
| 1 | `()` | The first pane content. | 2.36.0 |
| 2 | `()` | The Second pane content. | 2.36.0 |
| resize-trigger | `()` | Split bar content. | 2.36.0 |

View File

@ -4,7 +4,13 @@
<template>
<n-flex vertical>
<n-input-number v-model:value="split" :step="0.1" clearable />
<n-input-number
v-model:value="split"
:step="0.1"
clearable
:max="1"
:min="0"
/>
<NSplit v-model:size="split" style="height: 200px">
<template #1>
<div style="width: 100%; background-color: black" />

View File

@ -13,6 +13,7 @@ nest.vue
event.vue
slot.vue
controlled.vue
pixel-value.vue
```
## API
@ -21,18 +22,22 @@ controlled.vue
| 名称 | 类型 | 默认值 | 说明 | 版本 |
| --- | --- | --- | --- | --- |
| default-size | `number` | `0.5` | Split 的默认分割大小0~1 代表百分比 | 2.36.0 |
| size | `number` | `undefined` | Split 的受控分割大小0~1 代表百分比 | 2.38.0 |
| disabled | `boolean` | `false` | 是否禁用 | 2.36.0 |
| default-size | `string \| number` | `0.5` | Split 的默认分割大小,为 `number` 类型时应该为 0 ~ 1 之间的值,为 `string` 类型时应该为 `${number}px` 格式 | 2.36.0, `string` 2.38.2 |
| direction | `'horizontal' \| 'vertical'` | `'horizontal'` | Split 的分割方向 | 2.36.0 |
| min | `number` | `0` | Split 的分割最小阈值0~1 代表百分比 | 2.36.0 |
| max | `number` | `1` | Split 的分割最大阈值0~1 代表百分比 | 2.36.0 |
| disabled | `boolean` | `false` | 是否禁用 | 2.36.0 |
| max | `string \| number` | `1` | Split 的分割最大阈值,为 `number` 类型时应该为 0 ~ 1 之间的值,为 `string` 类型时应该为 `${number}px` 格式 | 2.36.0, `string` 2.38.2 |
| min | `string \| number` | `0` | Split 的分割最小阈值,为 `number` 类型时应该为 0 ~ 1 之间的值,为 `string` 类型时应该为 `${number}px` 格式 | 2.36.0, `string` 2.38.2 |
| pane1-class | `string` | `undefined` | 第一个面板的类名 | 2.38.2 |
| pane1-style | `Object \| string` | `undefined` | 第一个面板的样式 | 2.38.2 |
| pane2-class | `string` | `undefined` | 第二个面板的类名 | 2.38.2 |
| pane2-style | `Object \| string` | `undefined` | 第二个面板的样式 | 2.38.2 |
| resize-trigger-size | `number` | `3` | Split 的分隔条大小 | 2.36.0 |
| size | `string \| number` | `undefined` | Split 的受控分割大小,为 `number` 类型时应该为 0 ~ 1 之间的值,为 `string` 类型时应该为 `${number}px` 格式 | 2.38.0, `string` 2.38.2 |
| watch-props | `Array<'defaultSize'>` | `undefined` | 需要检测变更的默认属性,检测后组件状态会更新。注意:`watch-props` 本身不是响应式的 | 2.38.0 |
| on-drag-start | `(e: Event) => void` | `undefined` | 开始拖拽的回调函数 | 2.36.0 |
| on-drag-move | `(e: Event) => void` | `undefined` | 拖拽中的回调函数 | 2.36.0 |
| on-drag-end | `(e: Event) => void` | `undefined` | 结束拖拽的回调函数 | 2.36.0 |
| on-update:size | `(value: number) => void` | `undefined` | 组件 size 属性变化时触发的回调 | 2.38.0 |
| on-update:size | `(value: string \| number) => void` | `undefined` | 组件 `size` 属性变化时触发的回调,如果 `props.value``props.defaultValue``string``value``string` | 2.38.0 |
### Split Slots

View File

@ -0,0 +1,25 @@
<markdown>
# 使用像素值控制尺寸
`2.38.2` 开始`min``max``size` `default-size` 属性可以接受像素值
</markdown>
<template>
<n-split
direction="horizontal"
style="height: 200px"
max="300px"
min="100px"
default-size="200px"
>
<template #1>
Pane 1:<br>
min 100px<br>
default 200px<br>
max 300px
</template>
<template #2>
Pane 2
</template>
</n-split>
</template>

View File

@ -16,6 +16,7 @@ import { type ThemeProps, useTheme, useThemeClass } from '../../_mixins'
import style from './styles/index.cssr'
import { type SplitTheme, splitLight } from '../styles'
import { type SplitOnUpdateSize } from './types'
import { depx } from 'seemly'
export const splitProps = {
...(useTheme.props as ThemeProps<SplitTheme>),
@ -29,7 +30,7 @@ export const splitProps = {
},
disabled: Boolean,
defaultSize: {
type: Number,
type: [String, Number] as PropType<string | number>,
default: 0.5
},
'onUpdate:size': [Function, Array] as PropType<
@ -38,15 +39,19 @@ export const splitProps = {
onUpdateSize: [Function, Array] as PropType<
SplitOnUpdateSize | SplitOnUpdateSize[]
>,
size: Number,
size: [String, Number] as PropType<string | number>,
min: {
type: Number,
type: [String, Number] as PropType<string | number>,
default: 0
},
max: {
type: Number,
type: [String, Number] as PropType<string | number>,
default: 1
},
pane1Class: String,
pane1Style: [Object, String] as PropType<CSSProperties | string>,
pane2Class: String,
pane2Style: [Object, String] as PropType<CSSProperties | string>,
onDragStart: Function as PropType<(e: Event) => void>,
onDragMove: Function as PropType<(e: Event) => void>,
onDragEnd: Function as PropType<(e: Event) => void>,
@ -88,17 +93,27 @@ export default defineComponent({
watchEffect(() => (uncontrolledSizeRef.value = props.defaultSize))
}
// use to update controlled or uncontrolled values
const doUpdateSize = (size: number): void => {
const doUpdateSize = (size: number | string): void => {
const _onUpdateSize = props['onUpdate:size']
if (props.onUpdateSize) call(props.onUpdateSize, size)
if (_onUpdateSize) call(_onUpdateSize, size)
if (props.onUpdateSize) call(props.onUpdateSize, size as string & number)
if (_onUpdateSize) call(_onUpdateSize, size as string & number)
uncontrolledSizeRef.value = size
}
const mergedSizeRef = useMergedState(controlledSizeRef, uncontrolledSizeRef)
const firstPaneStyle = computed(() => {
const size = mergedSizeRef.value * 100
return {
flex: `0 0 calc(${size}% - ${(props.resizeTriggerSize * size) / 100}px)`
const sizeValue = mergedSizeRef.value
if (typeof sizeValue === 'string') {
return {
flex: `0 0 ${sizeValue}`
}
} else if (typeof sizeValue === 'number') {
const size = sizeValue * 100
return {
flex: `0 0 calc(${size}% - ${
(props.resizeTriggerSize * size) / 100
}px)`
}
}
})
@ -154,32 +169,50 @@ export default defineComponent({
offset = elRect.top - e.clientY
}
}
updateSize(e)
}
const updateSize = (event: MouseEvent): void => {
const parentRect =
const containerRect =
resizeTriggerElRef.value?.parentElement?.getBoundingClientRect()
if (!parentRect) return
const newSize =
props.direction === 'horizontal'
? (event.clientX - parentRect.left - offset) /
(parentRect.width - props.resizeTriggerSize)
: (event.clientY - parentRect.top + offset) /
(parentRect.height - props.resizeTriggerSize)
let nextSize = newSize
if (props.min) {
nextSize = Math.max(newSize, props.min)
if (!containerRect) return
const { direction } = props
const containerUsableWidth = containerRect.width - props.resizeTriggerSize
const containerUsableHeight =
containerRect.height - props.resizeTriggerSize
const containerUsableSize =
direction === 'horizontal'
? containerUsableWidth
: containerUsableHeight
const newPxSize =
direction === 'horizontal'
? event.clientX - containerRect.left - offset
: event.clientY - containerRect.top + offset
const { min, max } = props
const pxMin =
typeof min === 'string' ? depx(min) : min * containerUsableSize
const pxMax =
typeof max === 'string' ? depx(max) : max * containerUsableSize
let nextPxSize = newPxSize
nextPxSize = Math.max(nextPxSize, pxMin)
nextPxSize = Math.min(nextPxSize, pxMax, containerUsableSize)
// in pixel mode
if (typeof mergedSizeRef.value === 'string') {
doUpdateSize(`${nextPxSize}px`)
} else {
// in percentage mode
doUpdateSize(nextPxSize / containerUsableSize)
}
if (props.max) {
nextSize = Math.min(nextSize, props.max)
}
doUpdateSize(nextSize)
}
const themeClassHandle = inlineThemeDisabled
? useThemeClass('float-button', undefined, cssVarsRef, props)
? useThemeClass('split', undefined, cssVarsRef, props)
: undefined
return {
@ -207,8 +240,8 @@ export default defineComponent({
style={this.cssVars as CSSProperties}
>
<div
class={`${this.mergedClsPrefix}-split-pane-1`}
style={this.firstPaneStyle}
class={[`${this.mergedClsPrefix}-split-pane-1`, this.pane1Class]}
style={[this.firstPaneStyle, this.pane1Style]}
>
{this.$slots[1]?.()}
</div>
@ -231,7 +264,10 @@ export default defineComponent({
])}
</div>
)}
<div class={`${this.mergedClsPrefix}-split-pane-2`}>
<div
class={[`${this.mergedClsPrefix}-split-pane-2`, this.pane2Class]}
style={this.pane2Style}
>
{this.$slots[2]?.()}
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More