mirror of
https://gitee.com/vant-contrib/vant.git
synced 2026-07-16 02:21:05 +08:00
Compare commits
17 Commits
50ee584953
...
a677bee2b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a677bee2b8 | ||
|
|
05544c109f | ||
|
|
21c57caa9b | ||
|
|
66aa2906e0 | ||
|
|
61ef6b9a98 | ||
|
|
c25acb8d46 | ||
|
|
8bcb34724f | ||
|
|
cf5e7e6629 | ||
|
|
a02cfe9604 | ||
|
|
71354f7742 | ||
|
|
84b05f0668 | ||
|
|
95542bf06e | ||
|
|
8d15fa4fdf | ||
|
|
35f81fb208 | ||
|
|
da3400435f | ||
|
|
6a7376d15d | ||
|
|
2cca630808 |
@ -30,7 +30,7 @@
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body ontouchstart>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/desktop/main.js"></script>
|
||||
</body>
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body ontouchstart>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/mobile/main.js"></script>
|
||||
</body>
|
||||
|
||||
@ -13,3 +13,8 @@ window.app = createApp(App)
|
||||
setTimeout(() => {
|
||||
window.app.mount('#app');
|
||||
}, 0);
|
||||
|
||||
// https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari/33681490#33681490
|
||||
document.addEventListener('touchstart', () => {}, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
@ -212,3 +212,24 @@ import 'vant/es/image-preview/style';
|
||||
```
|
||||
|
||||
> Tip: "Full Import" and "On-demand Import" should not be used at the same time, otherwise it will lead to problems such as code duplication and style overrides.
|
||||
|
||||
## With Frameworks
|
||||
|
||||
### Use Vant in Nuxt 3
|
||||
|
||||
When using Vant in Nuxt 3, you should add `/vant/` to the `build.transpile`:
|
||||
|
||||
```ts
|
||||
import { defineNuxtConfig } from 'nuxt';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
externalVue: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Reference:
|
||||
|
||||
- [nuxt/framework#6761](https://github.com/nuxt/framework/issues/6761)
|
||||
- [nuxt/framework#4084](https://github.com/nuxt/framework/issues/4084)
|
||||
|
||||
@ -217,3 +217,24 @@ import 'vant/es/image-preview/style';
|
||||
```
|
||||
|
||||
> 提示:在单个项目中不应该同时使用「全量引入」和「按需引入」,否则会导致代码重复、样式错乱等问题。
|
||||
|
||||
## 在框架中使用
|
||||
|
||||
### 在 Nuxt 3 中使用
|
||||
|
||||
在 Nuxt 3 中使用 Vant 时,由于 Nuxt 3 框架本身的限制,需要在 `nuxt.config.ts` 中添加以下配置:
|
||||
|
||||
```ts
|
||||
import { defineNuxtConfig } from 'nuxt';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
experimental: {
|
||||
externalVue: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
关于该问题的背景,可以参考以下 issue:
|
||||
|
||||
- [nuxt/framework#6761](https://github.com/nuxt/framework/issues/6761)
|
||||
- [nuxt/framework#4084](https://github.com/nuxt/framework/issues/4084)
|
||||
|
||||
@ -134,7 +134,7 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const getInitialDate = (defaultDate = props.defaultDate) => {
|
||||
const { type, minDate, maxDate } = props;
|
||||
const { type, minDate, maxDate, allowSameDay } = props;
|
||||
|
||||
if (defaultDate === null) {
|
||||
return defaultDate;
|
||||
@ -149,9 +149,12 @@ export default defineComponent({
|
||||
const start = limitDateRange(
|
||||
defaultDate[0] || now,
|
||||
minDate,
|
||||
getPrevDay(maxDate)
|
||||
allowSameDay ? maxDate : getPrevDay(maxDate)
|
||||
);
|
||||
const end = limitDateRange(
|
||||
defaultDate[1] || now,
|
||||
allowSameDay ? minDate : getNextDay(minDate)
|
||||
);
|
||||
const end = limitDateRange(defaultDate[1] || now, getNextDay(minDate));
|
||||
return [start, end];
|
||||
}
|
||||
|
||||
|
||||
@ -270,3 +270,41 @@ test('should emit overRange when exceeded max range', async () => {
|
||||
|
||||
expect(onOverRange).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should allow default date to be minDate when using allowSameDay prop', () => {
|
||||
const minDate = new Date(1800, 0, 1);
|
||||
const maxDate = new Date(1800, 0, 29);
|
||||
const wrapper = mount(Calendar, {
|
||||
props: {
|
||||
type: 'range',
|
||||
poppable: false,
|
||||
minDate,
|
||||
maxDate,
|
||||
defaultDate: [minDate, minDate],
|
||||
lazyRender: false,
|
||||
allowSameDay: true,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.find('.van-calendar__confirm').trigger('click');
|
||||
expect(wrapper.emitted<[Date]>('confirm')![0][0]).toEqual([minDate, minDate]);
|
||||
});
|
||||
|
||||
test('should allow default date to be maxDate when using allowSameDay prop', () => {
|
||||
const minDate = new Date(1800, 0, 1);
|
||||
const maxDate = new Date(1800, 0, 29);
|
||||
const wrapper = mount(Calendar, {
|
||||
props: {
|
||||
type: 'range',
|
||||
poppable: false,
|
||||
minDate,
|
||||
maxDate,
|
||||
defaultDate: [maxDate, maxDate],
|
||||
lazyRender: false,
|
||||
allowSameDay: true,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper.find('.van-calendar__confirm').trigger('click');
|
||||
expect(wrapper.emitted<[Date]>('confirm')![0][0]).toEqual([maxDate, maxDate]);
|
||||
});
|
||||
|
||||
@ -65,7 +65,10 @@ export default defineComponent({
|
||||
);
|
||||
};
|
||||
|
||||
const isMinYear = (year: number) => year === props.minDate.getFullYear();
|
||||
const isMaxYear = (year: number) => year === props.maxDate.getFullYear();
|
||||
const isMinMonth = (month: number) =>
|
||||
month === props.minDate.getMonth() + 1;
|
||||
const isMaxMonth = (month: number) =>
|
||||
month === props.maxDate.getMonth() + 1;
|
||||
|
||||
@ -88,28 +91,30 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
const genMonthOptions = () => {
|
||||
if (isMaxYear(getValue('year'))) {
|
||||
return genOptions(
|
||||
1,
|
||||
props.maxDate.getMonth() + 1,
|
||||
'month',
|
||||
props.formatter,
|
||||
props.filter
|
||||
);
|
||||
}
|
||||
return genOptions(1, 12, 'month', props.formatter, props.filter);
|
||||
const year = getValue('year');
|
||||
const minMonth = isMinYear(year) ? props.minDate.getMonth() + 1 : 1;
|
||||
const maxMonth = isMaxYear(year) ? props.maxDate.getMonth() + 1 : 12;
|
||||
|
||||
return genOptions(
|
||||
minMonth,
|
||||
maxMonth,
|
||||
'month',
|
||||
props.formatter,
|
||||
props.filter
|
||||
);
|
||||
};
|
||||
|
||||
const genDayOptions = () => {
|
||||
const year = getValue('year');
|
||||
const month = getValue('month');
|
||||
const minDate =
|
||||
isMinYear(year) && isMinMonth(month) ? props.minDate.getDate() : 1;
|
||||
const maxDate =
|
||||
isMaxYear(year) && isMaxMonth(month)
|
||||
? props.maxDate.getDate()
|
||||
: getMonthEndDay(year, month);
|
||||
|
||||
let maxDate = getMonthEndDay(year, month);
|
||||
if (isMaxYear(year) && isMaxMonth(month)) {
|
||||
maxDate = props.maxDate.getDate();
|
||||
}
|
||||
|
||||
return genOptions(1, maxDate, 'day', props.formatter, props.filter);
|
||||
return genOptions(minDate, maxDate, 'day', props.formatter, props.filter);
|
||||
};
|
||||
|
||||
const columns = computed(() =>
|
||||
@ -130,7 +135,7 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
watch(currentValues, (newValues) => {
|
||||
if (isSameValue(newValues, props.modelValue)) {
|
||||
if (!isSameValue(newValues, props.modelValue)) {
|
||||
emit('update:modelValue', newValues);
|
||||
}
|
||||
});
|
||||
|
||||
@ -21,7 +21,12 @@ app.use(DatePicker);
|
||||
### Basic Usage
|
||||
|
||||
```html
|
||||
<van-date-picker v-model="currentDate" title="Choose Date" />
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
title="Choose Date"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
/>
|
||||
```
|
||||
|
||||
```js
|
||||
@ -191,7 +196,7 @@ export default {
|
||||
|
||||
| Name | Description | SlotProps |
|
||||
| -------------- | ---------------------------- | ---------------------- |
|
||||
| default | Custom toolbar content | - |
|
||||
| toolbar | Custom toolbar content | - |
|
||||
| title | Custom title | - |
|
||||
| confirm | Custom confirm button text | - |
|
||||
| cancel | Custom cancel button text | - |
|
||||
|
||||
@ -23,7 +23,12 @@ app.use(DatePicker);
|
||||
通过 `v-model` 绑定当前选中的日期,通过 `min-date` 和 `max-date` 属性来设定可选的时间范围。
|
||||
|
||||
```html
|
||||
<van-date-picker v-model="currentDate" title="选择日期" />
|
||||
<van-date-picker
|
||||
v-model="currentDate"
|
||||
title="选择日期"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
/>
|
||||
```
|
||||
|
||||
```js
|
||||
@ -197,7 +202,7 @@ export default {
|
||||
|
||||
| 名称 | 说明 | 参数 |
|
||||
| -------------- | ---------------------- | ---------------------- |
|
||||
| default | 自定义整个顶部栏的内容 | - |
|
||||
| toolbar | 自定义整个顶部栏的内容 | - |
|
||||
| title | 自定义标题内容 | - |
|
||||
| confirm | 自定义确认按钮内容 | - |
|
||||
| cancel | 自定义取消按钮内容 | - |
|
||||
|
||||
@ -26,6 +26,7 @@ import {
|
||||
makeStringProp,
|
||||
makeNumericProp,
|
||||
createNamespace,
|
||||
type ComponentInstance,
|
||||
} from '../utils';
|
||||
import {
|
||||
cutString,
|
||||
@ -42,7 +43,11 @@ import {
|
||||
import { cellSharedProps } from '../cell/Cell';
|
||||
|
||||
// Composables
|
||||
import { CUSTOM_FIELD_INJECTION_KEY, useParent } from '@vant/use';
|
||||
import {
|
||||
useParent,
|
||||
useEventListener,
|
||||
CUSTOM_FIELD_INJECTION_KEY,
|
||||
} from '@vant/use';
|
||||
import { useId } from '../composables/use-id';
|
||||
import { useExpose } from '../composables/use-expose';
|
||||
|
||||
@ -145,6 +150,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const inputRef = ref<HTMLInputElement>();
|
||||
const clearIconRef = ref<ComponentInstance>();
|
||||
const customValue = ref<() => unknown>();
|
||||
|
||||
const { parent: form } = useParent(FORM_KEY);
|
||||
@ -353,7 +359,7 @@ export default defineComponent({
|
||||
const onClickRightIcon = (event: MouseEvent) =>
|
||||
emit('clickRightIcon', event);
|
||||
|
||||
const onClear = (event: MouseEvent) => {
|
||||
const onClear = (event: TouchEvent) => {
|
||||
preventDefault(event);
|
||||
emit('update:modelValue', '');
|
||||
emit('clear', event);
|
||||
@ -526,9 +532,9 @@ export default defineComponent({
|
||||
{renderInput()}
|
||||
{showClear.value && (
|
||||
<Icon
|
||||
ref={clearIconRef}
|
||||
name={props.clearIcon}
|
||||
class={bem('clear')}
|
||||
onTouchstart={onClear}
|
||||
/>
|
||||
)}
|
||||
{renderRightIcon()}
|
||||
@ -568,6 +574,11 @@ export default defineComponent({
|
||||
nextTick(adjustTextareaSize);
|
||||
});
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchstart', onClear, {
|
||||
target: computed(() => clearIconRef.value?.$el),
|
||||
});
|
||||
|
||||
return () => {
|
||||
const disabled = getProp('disabled');
|
||||
const labelAlign = getProp('labelAlign');
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import VanField from '..';
|
||||
import VanForm from '../../form';
|
||||
import VanCellGroup from '../../cell-group';
|
||||
import { ref } from 'vue';
|
||||
import { useTranslate } from '../../../docs/site';
|
||||
@ -43,35 +44,39 @@ const password = ref('');
|
||||
<template>
|
||||
<demo-block :title="t('customType')">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="text"
|
||||
:label="t('text')"
|
||||
:placeholder="t('textPlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="phone"
|
||||
type="tel"
|
||||
:label="t('phone')"
|
||||
:placeholder="t('phonePlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="digit"
|
||||
type="digit"
|
||||
:label="t('digit')"
|
||||
:placeholder="t('digitPlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="number"
|
||||
type="number"
|
||||
:label="t('number')"
|
||||
:placeholder="t('numberPlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="password"
|
||||
type="password"
|
||||
:label="t('password')"
|
||||
:placeholder="t('passwordPlaceholder')"
|
||||
/>
|
||||
<van-form>
|
||||
<van-field
|
||||
v-model="text"
|
||||
:label="t('text')"
|
||||
:placeholder="t('textPlaceholder')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<van-field
|
||||
v-model="phone"
|
||||
type="tel"
|
||||
:label="t('phone')"
|
||||
:placeholder="t('phonePlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="digit"
|
||||
type="digit"
|
||||
:label="t('digit')"
|
||||
:placeholder="t('digitPlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="number"
|
||||
type="number"
|
||||
:label="t('number')"
|
||||
:placeholder="t('numberPlaceholder')"
|
||||
/>
|
||||
<van-field
|
||||
v-model="password"
|
||||
type="password"
|
||||
:label="t('password')"
|
||||
:placeholder="t('passwordPlaceholder')"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</van-form>
|
||||
</van-cell-group>
|
||||
</demo-block>
|
||||
</template>
|
||||
|
||||
@ -26,103 +26,107 @@ exports[`should render demo and match snapshot 1`] = `
|
||||
</div>
|
||||
<div>
|
||||
<div class="van-cell-group van-cell-group--inset">
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Text
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="text"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Text"
|
||||
aria-labelledby="van-field-label"
|
||||
<form class="van-form">
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Text
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="text"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Text"
|
||||
autocomplete="off"
|
||||
aria-labelledby="van-field-label"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Phone
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="tel"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Phone"
|
||||
aria-labelledby="van-field-label"
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Phone
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="tel"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Phone"
|
||||
aria-labelledby="van-field-label"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Digit
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="tel"
|
||||
inputmode="numeric"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Digit"
|
||||
aria-labelledby="van-field-label"
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Digit
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="tel"
|
||||
inputmode="numeric"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Digit"
|
||||
aria-labelledby="van-field-label"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Number
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="text"
|
||||
inputmode="decimal"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Number"
|
||||
aria-labelledby="van-field-label"
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Number
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="text"
|
||||
inputmode="decimal"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Number"
|
||||
aria-labelledby="van-field-label"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="password"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Password"
|
||||
aria-labelledby="van-field-label"
|
||||
<div class="van-cell van-field">
|
||||
<div class="van-cell__title van-field__label">
|
||||
<label id="van-field-label"
|
||||
for="van-field-input"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div class="van-cell__value van-field__value">
|
||||
<div class="van-field__body">
|
||||
<input type="password"
|
||||
id="van-field-input"
|
||||
class="van-field__control"
|
||||
placeholder="Password"
|
||||
autocomplete="off"
|
||||
aria-labelledby="van-field-label"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
reactive,
|
||||
@ -13,10 +14,12 @@ import {
|
||||
preventDefault,
|
||||
createNamespace,
|
||||
makeRequiredProp,
|
||||
type ComponentInstance,
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
import { useEventListener } from '@vant/use';
|
||||
|
||||
// Components
|
||||
import { Image } from '../image';
|
||||
@ -57,6 +60,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
const touch = useTouch();
|
||||
const swipeItem = ref<ComponentInstance>();
|
||||
|
||||
const vertical = computed(() => {
|
||||
const { rootWidth, rootHeight } = props;
|
||||
@ -271,6 +275,11 @@ export default defineComponent({
|
||||
}
|
||||
);
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: computed(() => swipeItem.value?.$el),
|
||||
});
|
||||
|
||||
return () => {
|
||||
const imageSlots = {
|
||||
loading: () => <Loading type="spinner" />,
|
||||
@ -278,9 +287,9 @@ export default defineComponent({
|
||||
|
||||
return (
|
||||
<SwipeItem
|
||||
ref={swipeItem}
|
||||
class={bem('swipe-item')}
|
||||
onTouchstartPassive={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
>
|
||||
|
||||
@ -77,6 +77,7 @@ export default defineComponent({
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const root = ref<HTMLElement>();
|
||||
const sidebar = ref<HTMLElement>();
|
||||
const activeAnchor = ref<Numeric>('');
|
||||
|
||||
const touch = useTouch();
|
||||
@ -272,11 +273,11 @@ export default defineComponent({
|
||||
|
||||
const renderSidebar = () => (
|
||||
<div
|
||||
ref={sidebar}
|
||||
class={bem('sidebar')}
|
||||
style={sidebarStyle.value}
|
||||
onClick={onClickSidebar}
|
||||
onTouchstartPassive={touch.start}
|
||||
onTouchmove={onTouchMove}
|
||||
>
|
||||
{renderIndexes()}
|
||||
</div>
|
||||
@ -284,6 +285,11 @@ export default defineComponent({
|
||||
|
||||
useExpose({ scrollTo });
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: sidebar,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div ref={root} class={bem()}>
|
||||
{props.teleport ? (
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import {
|
||||
ref,
|
||||
Transition,
|
||||
defineComponent,
|
||||
type PropType,
|
||||
type CSSProperties,
|
||||
type ExtractPropTypes,
|
||||
} from 'vue';
|
||||
|
||||
// Utils
|
||||
import {
|
||||
noop,
|
||||
isDef,
|
||||
extend,
|
||||
truthProp,
|
||||
@ -16,6 +18,9 @@ import {
|
||||
createNamespace,
|
||||
getZIndexStyle,
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { useEventListener } from '@vant/use';
|
||||
import { useLazyRender } from '../composables/use-lazy-render';
|
||||
|
||||
const [name, bem] = createNamespace('overlay');
|
||||
@ -38,10 +43,13 @@ export default defineComponent({
|
||||
props: overlayProps,
|
||||
|
||||
setup(props, { slots }) {
|
||||
const root = ref<HTMLElement>();
|
||||
const lazyRender = useLazyRender(() => props.show || !props.lazyRender);
|
||||
|
||||
const preventTouchMove = (event: TouchEvent) => {
|
||||
preventDefault(event, true);
|
||||
const onTouchMove = (event: TouchEvent) => {
|
||||
if (props.lockScroll) {
|
||||
preventDefault(event, true);
|
||||
}
|
||||
};
|
||||
|
||||
const renderOverlay = lazyRender(() => {
|
||||
@ -57,15 +65,20 @@ export default defineComponent({
|
||||
return (
|
||||
<div
|
||||
v-show={props.show}
|
||||
ref={root}
|
||||
style={style}
|
||||
class={[bem(), props.className]}
|
||||
onTouchmove={props.lockScroll ? preventTouchMove : noop}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: root,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<Transition v-slots={{ default: renderOverlay }} name="van-fade" appear />
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
|
||||
// Utils
|
||||
import {
|
||||
pick,
|
||||
extend,
|
||||
unitToPx,
|
||||
truthProp,
|
||||
@ -17,27 +18,32 @@ import {
|
||||
preventDefault,
|
||||
makeStringProp,
|
||||
makeNumericProp,
|
||||
createNamespace,
|
||||
HAPTICS_FEEDBACK,
|
||||
BORDER_UNSET_TOP_BOTTOM,
|
||||
type Numeric,
|
||||
} from '../utils';
|
||||
import {
|
||||
bem,
|
||||
name,
|
||||
isOptionExist,
|
||||
getColumnsType,
|
||||
findOptionByValue,
|
||||
assignDefaultFields,
|
||||
formatCascadeColumns,
|
||||
getFirstEnabledOption,
|
||||
assignDefaultFields,
|
||||
} from './utils';
|
||||
|
||||
// Composables
|
||||
import { useChildren } from '@vant/use';
|
||||
import { useChildren, useEventListener } from '@vant/use';
|
||||
import { useExpose } from '../composables/use-expose';
|
||||
|
||||
// Components
|
||||
import { Loading } from '../loading';
|
||||
import Column, { PICKER_KEY } from './PickerColumn';
|
||||
import Toolbar, {
|
||||
pickerToolbarPropKeys,
|
||||
pickerToolbarProps,
|
||||
pickerToolbarSlots,
|
||||
} from './PickerToolbar';
|
||||
|
||||
// Types
|
||||
import type {
|
||||
@ -48,20 +54,18 @@ import type {
|
||||
PickerToolbarPosition,
|
||||
} from './types';
|
||||
|
||||
const [name, bem, t] = createNamespace('picker');
|
||||
|
||||
export const pickerSharedProps = {
|
||||
title: String,
|
||||
loading: Boolean,
|
||||
readonly: Boolean,
|
||||
allowHtml: Boolean,
|
||||
optionHeight: makeNumericProp(44),
|
||||
showToolbar: truthProp,
|
||||
swipeDuration: makeNumericProp(1000),
|
||||
visibleOptionNum: makeNumericProp(6),
|
||||
cancelButtonText: String,
|
||||
confirmButtonText: String,
|
||||
};
|
||||
export const pickerSharedProps = extend(
|
||||
{
|
||||
loading: Boolean,
|
||||
readonly: Boolean,
|
||||
allowHtml: Boolean,
|
||||
optionHeight: makeNumericProp(44),
|
||||
showToolbar: truthProp,
|
||||
swipeDuration: makeNumericProp(1000),
|
||||
visibleOptionNum: makeNumericProp(6),
|
||||
},
|
||||
pickerToolbarProps
|
||||
);
|
||||
|
||||
const pickerProps = extend({}, pickerSharedProps, {
|
||||
columns: makeArrayProp<PickerOption | PickerColumn>(),
|
||||
@ -80,6 +84,7 @@ export default defineComponent({
|
||||
emits: ['confirm', 'cancel', 'change', 'clickOption', 'update:modelValue'],
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const columnsRef = ref<HTMLElement>();
|
||||
const selectedValues = ref(props.modelValue);
|
||||
const { children, linkChildren } = useChildren(PICKER_KEY);
|
||||
|
||||
@ -121,6 +126,11 @@ export default defineComponent({
|
||||
}
|
||||
};
|
||||
|
||||
const getEventParams = () => ({
|
||||
selectedValues: selectedValues.value,
|
||||
selectedOptions: selectedOptions.value,
|
||||
});
|
||||
|
||||
const onChange = (value: Numeric, columnIndex: number) => {
|
||||
setValue(columnIndex, value);
|
||||
|
||||
@ -137,81 +147,21 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
|
||||
emit('change', {
|
||||
columnIndex,
|
||||
selectedValues: selectedValues.value,
|
||||
selectedOptions: selectedOptions.value,
|
||||
});
|
||||
emit('change', extend({ columnIndex }, getEventParams()));
|
||||
};
|
||||
|
||||
const onClickOption = (currentOption: PickerOption, columnIndex: number) =>
|
||||
emit('clickOption', {
|
||||
columnIndex,
|
||||
currentOption,
|
||||
selectedValues: selectedValues.value,
|
||||
selectedOptions: selectedOptions.value,
|
||||
});
|
||||
emit(
|
||||
'clickOption',
|
||||
extend({ columnIndex, currentOption }, getEventParams())
|
||||
);
|
||||
|
||||
const confirm = () => {
|
||||
children.forEach((child) => child.stopMomentum());
|
||||
emit('confirm', {
|
||||
selectedValues: selectedValues.value,
|
||||
selectedOptions: selectedOptions.value,
|
||||
});
|
||||
emit('confirm', getEventParams());
|
||||
};
|
||||
|
||||
const cancel = () =>
|
||||
emit('cancel', {
|
||||
selectedValues: selectedValues.value,
|
||||
selectedOptions: selectedOptions.value,
|
||||
});
|
||||
|
||||
const renderTitle = () => {
|
||||
if (slots.title) {
|
||||
return slots.title();
|
||||
}
|
||||
if (props.title) {
|
||||
return <div class={[bem('title'), 'van-ellipsis']}>{props.title}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCancel = () => {
|
||||
const text = props.cancelButtonText || t('cancel');
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={[bem('cancel'), HAPTICS_FEEDBACK]}
|
||||
onClick={cancel}
|
||||
>
|
||||
{slots.cancel ? slots.cancel() : text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderConfirm = () => {
|
||||
const text = props.confirmButtonText || t('confirm');
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={[bem('confirm'), HAPTICS_FEEDBACK]}
|
||||
onClick={confirm}
|
||||
>
|
||||
{slots.confirm ? slots.confirm() : text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderToolbar = () => {
|
||||
if (props.showToolbar) {
|
||||
return (
|
||||
<div class={bem('toolbar')}>
|
||||
{slots.toolbar
|
||||
? slots.toolbar()
|
||||
: [renderCancel(), renderTitle(), renderConfirm()]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
const cancel = () => emit('cancel', getEventParams());
|
||||
|
||||
const renderColumnItems = () =>
|
||||
currentColumns.value.map((options, columnIndex) => (
|
||||
@ -252,17 +202,26 @@ export default defineComponent({
|
||||
const wrapHeight = optionHeight.value * +props.visibleOptionNum;
|
||||
const columnsStyle = { height: `${wrapHeight}px` };
|
||||
return (
|
||||
<div
|
||||
class={bem('columns')}
|
||||
style={columnsStyle}
|
||||
onTouchmove={preventDefault}
|
||||
>
|
||||
<div ref={columnsRef} class={bem('columns')} style={columnsStyle}>
|
||||
{renderColumnItems()}
|
||||
{renderMask(wrapHeight)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderToolbar = () => {
|
||||
if (props.showToolbar) {
|
||||
return (
|
||||
<Toolbar
|
||||
v-slots={pick(slots, pickerToolbarSlots)}
|
||||
{...pick(props, pickerToolbarPropKeys)}
|
||||
onConfirm={confirm}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
currentColumns,
|
||||
(columns) => {
|
||||
@ -300,6 +259,11 @@ export default defineComponent({
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', preventDefault, {
|
||||
target: columnsRef,
|
||||
});
|
||||
|
||||
const getSelectedOptions = () => selectedOptions.value;
|
||||
|
||||
useExpose<PickerExpose>({ confirm, getSelectedOptions });
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
import { getElementTranslateY, findIndexOfEnabledOption } from './utils';
|
||||
|
||||
// Composables
|
||||
import { useParent } from '@vant/use';
|
||||
import { useEventListener, useParent } from '@vant/use';
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
import { useExpose } from '../composables/use-expose';
|
||||
|
||||
@ -65,6 +65,7 @@ export default defineComponent({
|
||||
let momentumOffset: number;
|
||||
let transitionEndTrigger: null | (() => void);
|
||||
|
||||
const root = ref<HTMLElement>();
|
||||
const wrapper = ref<HTMLElement>();
|
||||
const currentOffset = ref(0);
|
||||
const currentDuration = ref(0);
|
||||
@ -250,11 +251,16 @@ export default defineComponent({
|
||||
currentOffset.value = offset;
|
||||
});
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: root,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div
|
||||
ref={root}
|
||||
class={bem()}
|
||||
onTouchstartPassive={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
>
|
||||
|
||||
75
packages/vant/src/picker/PickerToolbar.tsx
Normal file
75
packages/vant/src/picker/PickerToolbar.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { bem, t } from './utils';
|
||||
import { createNamespace, HAPTICS_FEEDBACK } from '../utils';
|
||||
|
||||
const [name] = createNamespace('picker-toolbar');
|
||||
|
||||
export const pickerToolbarProps = {
|
||||
title: String,
|
||||
cancelButtonText: String,
|
||||
confirmButtonText: String,
|
||||
};
|
||||
|
||||
export const pickerToolbarSlots = ['cancel', 'confirm', 'title', 'toolbar'];
|
||||
|
||||
export type PickerToolbarPropKeys = Array<keyof typeof pickerToolbarProps>;
|
||||
|
||||
export const pickerToolbarPropKeys = Object.keys(
|
||||
pickerToolbarProps
|
||||
) as PickerToolbarPropKeys;
|
||||
|
||||
export default defineComponent({
|
||||
name,
|
||||
|
||||
props: pickerToolbarProps,
|
||||
|
||||
emits: ['confirm', 'cancel'],
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const renderTitle = () => {
|
||||
if (slots.title) {
|
||||
return slots.title();
|
||||
}
|
||||
if (props.title) {
|
||||
return <div class={[bem('title'), 'van-ellipsis']}>{props.title}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const onCancel = () => emit('cancel');
|
||||
const onConfirm = () => emit('confirm');
|
||||
|
||||
const renderCancel = () => {
|
||||
const text = props.cancelButtonText || t('cancel');
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={[bem('cancel'), HAPTICS_FEEDBACK]}
|
||||
onClick={onCancel}
|
||||
>
|
||||
{slots.cancel ? slots.cancel() : text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderConfirm = () => {
|
||||
const text = props.confirmButtonText || t('confirm');
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class={[bem('confirm'), HAPTICS_FEEDBACK]}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{slots.confirm ? slots.confirm() : text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div class={bem('toolbar')}>
|
||||
{slots.toolbar
|
||||
? slots.toolbar()
|
||||
: [renderCancel(), renderTitle(), renderConfirm()]}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
@ -1,7 +1,11 @@
|
||||
import { isDef, clamp, extend, type Numeric } from '../utils';
|
||||
import { isDef, clamp, extend, createNamespace, type Numeric } from '../utils';
|
||||
import type { Ref } from 'vue';
|
||||
import type { PickerOption, PickerColumn, PickerFieldNames } from './types';
|
||||
|
||||
const [name, bem, t] = createNamespace('picker');
|
||||
|
||||
export { name, bem, t };
|
||||
|
||||
export const getFirstEnabledOption = (
|
||||
options: PickerOption[]
|
||||
): PickerOption | undefined =>
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { useRect, useCustomFieldValue } from '@vant/use';
|
||||
import { useRect, useCustomFieldValue, useEventListener } from '@vant/use';
|
||||
import { useRefs } from '../composables/use-refs';
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
|
||||
@ -268,6 +268,11 @@ export default defineComponent({
|
||||
|
||||
useCustomFieldValue(() => props.modelValue);
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: groupRef,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div
|
||||
ref={groupRef}
|
||||
@ -280,7 +285,6 @@ export default defineComponent({
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
onTouchstartPassive={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
>
|
||||
{list.value.map(renderStar)}
|
||||
</div>
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { useRect, useCustomFieldValue } from '@vant/use';
|
||||
import { useRect, useCustomFieldValue, useEventListener } from '@vant/use';
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
|
||||
const [name, bem] = createNamespace('slider');
|
||||
@ -65,6 +65,7 @@ export default defineComponent({
|
||||
let startValue: SliderValue;
|
||||
|
||||
const root = ref<HTMLElement>();
|
||||
const slider = ref<HTMLElement>();
|
||||
const dragStatus = ref<'start' | 'dragging' | ''>();
|
||||
const touch = useTouch();
|
||||
|
||||
@ -290,6 +291,7 @@ export default defineComponent({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={slider}
|
||||
role="slider"
|
||||
class={getButtonClassName(index)}
|
||||
tabindex={props.disabled ? undefined : 0}
|
||||
@ -306,7 +308,6 @@ export default defineComponent({
|
||||
}
|
||||
onTouchStart(event);
|
||||
}}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
onClick={stopPropagation}
|
||||
@ -320,6 +321,11 @@ export default defineComponent({
|
||||
updateValue(props.modelValue);
|
||||
useCustomFieldValue(() => props.modelValue);
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: slider,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div
|
||||
ref={root}
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { useRect, useClickAway } from '@vant/use';
|
||||
import { useRect, useClickAway, useEventListener } from '@vant/use';
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
import { useExpose } from '../composables/use-expose';
|
||||
|
||||
@ -209,6 +209,11 @@ export default defineComponent({
|
||||
|
||||
useClickAway(root, () => onClick('outside'), { eventName: 'touchstart' });
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: root,
|
||||
});
|
||||
|
||||
return () => {
|
||||
const wrapperStyle = {
|
||||
transform: `translate3d(${state.offset}px, 0, 0)`,
|
||||
@ -221,7 +226,6 @@ export default defineComponent({
|
||||
class={bem()}
|
||||
onClick={getClickHandler('cell', lockClick)}
|
||||
onTouchstartPassive={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
>
|
||||
|
||||
@ -28,7 +28,12 @@ import {
|
||||
} from '../utils';
|
||||
|
||||
// Composables
|
||||
import { doubleRaf, useChildren, usePageVisibility } from '@vant/use';
|
||||
import {
|
||||
doubleRaf,
|
||||
useChildren,
|
||||
useEventListener,
|
||||
usePageVisibility,
|
||||
} from '@vant/use';
|
||||
import { useTouch } from '../composables/use-touch';
|
||||
import { useExpose } from '../composables/use-expose';
|
||||
import { onPopupReopen } from '../composables/on-popup-reopen';
|
||||
@ -66,6 +71,7 @@ export default defineComponent({
|
||||
|
||||
setup(props, { emit, slots }) {
|
||||
const root = ref<HTMLElement>();
|
||||
const track = ref<HTMLElement>();
|
||||
const state = reactive<SwipeState>({
|
||||
rect: null,
|
||||
width: 0,
|
||||
@ -301,8 +307,15 @@ export default defineComponent({
|
||||
touch.move(event);
|
||||
|
||||
if (isCorrectDirection.value) {
|
||||
preventDefault(event, props.stopPropagation);
|
||||
move({ offset: delta.value });
|
||||
const isEdgeTouch =
|
||||
!props.loop &&
|
||||
((state.active === 0 && delta.value > 0) ||
|
||||
(state.active === count.value - 1 && delta.value < 0));
|
||||
|
||||
if (!isEdgeTouch) {
|
||||
preventDefault(event, props.stopPropagation);
|
||||
move({ offset: delta.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -435,13 +448,18 @@ export default defineComponent({
|
||||
onDeactivated(stopAutoplay);
|
||||
onBeforeUnmount(stopAutoplay);
|
||||
|
||||
// useEventListener will set passive to `false` to eliminate the warning of Chrome
|
||||
useEventListener('touchmove', onTouchMove, {
|
||||
target: track,
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div ref={root} class={bem()}>
|
||||
<div
|
||||
ref={track}
|
||||
style={trackStyle.value}
|
||||
class={bem('track', { vertical: props.vertical })}
|
||||
onTouchstartPassive={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
>
|
||||
|
||||
@ -197,7 +197,7 @@ export default {
|
||||
|
||||
| Name | Description | SlotProps |
|
||||
| -------------- | ---------------------------- | ---------------------- |
|
||||
| default | Custom toolbar content | - |
|
||||
| toolbar | Custom toolbar content | - |
|
||||
| title | Custom title | - |
|
||||
| confirm | Custom confirm button text | - |
|
||||
| cancel | Custom cancel button text | - |
|
||||
|
||||
@ -198,7 +198,7 @@ export default {
|
||||
|
||||
| 名称 | 说明 | 参数 |
|
||||
| -------------- | ---------------------- | ---------------------- |
|
||||
| default | 自定义整个顶部栏的内容 | - |
|
||||
| toolbar | 自定义整个顶部栏的内容 | - |
|
||||
| title | 自定义标题内容 | - |
|
||||
| confirm | 自定义确认按钮内容 | - |
|
||||
| cancel | 自定义取消按钮内容 | - |
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user