chore: merge src and src-next

This commit is contained in:
chenjiahan
2020-07-15 20:02:00 +08:00
parent 6672b34618
commit 0304fcb6fa
382 changed files with 464 additions and 24746 deletions
+5 -5
View File
@@ -23,13 +23,13 @@
</demo-block>
<van-action-sheet
v-model="show.basic"
v-model:show="show.basic"
:actions="simpleActions"
@select="onSelect"
/>
<van-action-sheet
v-model="show.cancel"
v-model:show="show.cancel"
:actions="simpleActions"
close-on-click-action
:cancel-text="t('cancel')"
@@ -37,7 +37,7 @@
/>
<van-action-sheet
v-model="show.description"
v-model:show="show.description"
:actions="actionsWithDescription"
close-on-click-action
:cancel-text="t('cancel')"
@@ -45,13 +45,13 @@
/>
<van-action-sheet
v-model="show.status"
v-model:show="show.status"
close-on-click-action
:actions="statusActions"
:cancel-text="t('cancel')"
/>
<van-action-sheet v-model="show.title" :title="t('title')">
<van-action-sheet v-model:show="show.title" :title="t('title')">
<div class="demo-action-sheet-content">{{ t('content') }}</div>
</van-action-sheet>
</demo-section>
+184
View File
@@ -0,0 +1,184 @@
// Utils
import { createNamespace } from '../utils';
// Mixins
import { popupMixinProps } from '../mixins/popup';
// Components
import Icon from '../icon';
import Popup from '../popup';
import Loading from '../loading';
const [createComponent, bem] = createNamespace('action-sheet');
export default createComponent({
props: {
...popupMixinProps,
title: String,
actions: Array,
duration: [Number, String],
cancelText: String,
description: String,
getContainer: [String, Function],
closeOnPopstate: Boolean,
closeOnClickAction: Boolean,
round: {
type: Boolean,
default: true,
},
closeIcon: {
type: String,
default: 'cross',
},
safeAreaInsetBottom: {
type: Boolean,
default: true,
},
overlay: {
type: Boolean,
default: true,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
},
emits: [
'open',
'close',
'opened',
'closed',
'cancel',
'select',
'update:show',
'click-overlay',
],
setup(props, { slots, emit }) {
function onCancel() {
emit('update:show', false);
emit('cancel');
}
const createEmitter = (name) => () => emit(name);
const listeners = {
onOpen: createEmitter('open'),
onClose: createEmitter('close'),
onOpened: createEmitter('opened'),
onClosed: createEmitter('closed'),
'onClick-overlay': createEmitter('click-overlay'),
'onUpdate:show': (show) => {
emit('update:show', show);
},
};
return function () {
const { title, cancelText } = props;
function Header() {
if (title) {
return (
<div class={bem('header')}>
{title}
<Icon
name={props.closeIcon}
class={bem('close')}
onClick={onCancel}
/>
</div>
);
}
}
function Content() {
if (slots.default) {
return <div class={bem('content')}>{slots.default()}</div>;
}
}
function Option(item, index) {
const { disabled, loading, callback } = item;
function onClickOption(event) {
event.stopPropagation();
if (disabled || loading) {
return;
}
if (callback) {
callback(item);
}
emit('select', item, index);
if (props.closeOnClickAction) {
emit('update:show', false);
}
}
function OptionContent() {
if (loading) {
return <Loading size="20px" />;
}
return [
<span class={bem('name')}>{item.name}</span>,
item.subname && <div class={bem('subname')}>{item.subname}</div>,
];
}
return (
<button
type="button"
class={[bem('item', { disabled, loading }), item.className]}
style={{ color: item.color }}
onClick={onClickOption}
>
{OptionContent()}
</button>
);
}
function CancelText() {
if (cancelText) {
return [
<div class={bem('gap')} />,
<button type="button" class={bem('cancel')} onClick={onCancel}>
{cancelText}
</button>,
];
}
}
const Description = props.description && (
<div class={bem('description')}>{props.description}</div>
);
return (
<Popup
class={bem()}
position="bottom"
show={props.show}
round={props.round}
overlay={props.overlay}
duration={props.duration}
lazyRender={props.lazyRender}
lockScroll={props.lockScroll}
getContainer={props.getContainer}
closeOnPopstate={props.closeOnPopstate}
closeOnClickOverlay={props.closeOnClickOverlay}
safeAreaInsetBottom={props.safeAreaInsetBottom}
{...listeners}
>
{Header()}
{Description}
{props.actions && props.actions.map(Option)}
{Content()}
{CancelText()}
</Popup>
);
};
},
});
-193
View File
@@ -1,193 +0,0 @@
// Utils
import { createNamespace } from '../utils';
import { emit, inherit } from '../utils/functional';
// Mixins
import { popupMixinProps } from '../mixins/popup';
// Components
import Icon from '../icon';
import Popup from '../popup';
import Loading from '../loading';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
import { PopupMixinProps } from '../mixins/popup/type';
export type ActionSheetItem = {
name: string;
color?: string;
subname?: string;
loading?: boolean;
disabled?: boolean;
className?: string;
callback?: (item: ActionSheetItem) => void;
};
export type ActionSheetProps = PopupMixinProps & {
round: boolean;
title?: string;
actions?: ActionSheetItem[];
duration: number | string;
closeIcon: string;
cancelText?: string;
description?: string;
closeOnPopstate?: boolean;
closeOnClickAction?: boolean;
safeAreaInsetBottom?: boolean;
};
const [createComponent, bem] = createNamespace('action-sheet');
function ActionSheet(
h: CreateElement,
props: ActionSheetProps,
slots: DefaultSlots,
ctx: RenderContext<ActionSheetProps>
) {
const { title, cancelText } = props;
function onCancel() {
emit(ctx, 'input', false);
emit(ctx, 'cancel');
}
function Header() {
if (title) {
return (
<div class={bem('header')}>
{title}
<Icon
name={props.closeIcon}
class={bem('close')}
onClick={onCancel}
/>
</div>
);
}
}
function Content() {
if (slots.default) {
return <div class={bem('content')}>{slots.default()}</div>;
}
}
function Option(item: ActionSheetItem, index: number) {
const { disabled, loading, callback } = item;
function onClickOption(event: MouseEvent) {
event.stopPropagation();
if (disabled || loading) {
return;
}
if (callback) {
callback(item);
}
emit(ctx, 'select', item, index);
if (props.closeOnClickAction) {
emit(ctx, 'input', false);
}
}
function OptionContent() {
if (loading) {
return <Loading size="20px" />;
}
return [
<span class={bem('name')}>{item.name}</span>,
item.subname && <div class={bem('subname')}>{item.subname}</div>,
];
}
return (
<button
type="button"
class={[bem('item', { disabled, loading }), item.className]}
style={{ color: item.color }}
onClick={onClickOption}
>
{OptionContent()}
</button>
);
}
function CancelText() {
if (cancelText) {
return [
<div class={bem('gap')} />,
<button type="button" class={bem('cancel')} onClick={onCancel}>
{cancelText}
</button>,
];
}
}
const Description = props.description && (
<div class={bem('description')}>{props.description}</div>
);
return (
<Popup
class={bem()}
position="bottom"
round={props.round}
value={props.value}
overlay={props.overlay}
duration={props.duration}
lazyRender={props.lazyRender}
lockScroll={props.lockScroll}
getContainer={props.getContainer}
closeOnPopstate={props.closeOnPopstate}
closeOnClickOverlay={props.closeOnClickOverlay}
safeAreaInsetBottom={props.safeAreaInsetBottom}
{...inherit(ctx, true)}
>
{Header()}
{Description}
{props.actions && props.actions.map(Option)}
{Content()}
{CancelText()}
</Popup>
);
}
ActionSheet.props = {
...popupMixinProps,
title: String,
actions: Array,
duration: [Number, String],
cancelText: String,
description: String,
getContainer: [String, Function],
closeOnPopstate: Boolean,
closeOnClickAction: Boolean,
round: {
type: Boolean,
default: true,
},
closeIcon: {
type: String,
default: 'cross',
},
safeAreaInsetBottom: {
type: Boolean,
default: true,
},
overlay: {
type: Boolean,
default: true,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
};
export default createComponent<ActionSheetProps>(ActionSheet);
+149
View File
@@ -0,0 +1,149 @@
// Utils
import { createNamespace } from '../utils';
import { BORDER_SURROUND, WHITE } from '../utils/constant';
import { routeProps, route } from '../utils/router';
// Components
import Icon from '../icon';
import Loading from '../loading';
const [createComponent, bem] = createNamespace('button');
export default createComponent({
props: {
...routeProps,
text: String,
icon: String,
color: String,
block: Boolean,
plain: Boolean,
round: Boolean,
square: Boolean,
loading: Boolean,
hairline: Boolean,
disabled: Boolean,
iconPrefix: String,
nativeType: String,
loadingText: String,
loadingType: String,
tag: {
type: String,
default: 'button',
},
type: {
type: String,
default: 'default',
},
size: {
type: String,
default: 'normal',
},
loadingSize: {
type: String,
default: '20px',
},
},
emits: ['click', 'touchstart'],
methods: {
onClick() {
if (!this.loading && !this.disabled) {
this.$emit('click', event);
route(this.$router, this);
}
},
onTouchstart(event) {
this.$emit('touchstart', event);
},
genContent() {
const Content = [];
if (this.loading) {
Content.push(
<Loading
class={bem('loading')}
size={this.loadingSize}
type={this.loadingType}
color="currentColor"
/>
);
} else if (this.icon) {
Content.push(
<Icon
name={this.icon}
class={bem('icon')}
classPrefix={this.iconPrefix}
/>
);
}
let text;
if (this.loading) {
text = this.loadingText;
} else {
text = this.$slots.default ? this.$slots.default() : this.text;
}
if (text) {
Content.push(<span class={bem('text')}>{text}</span>);
}
return Content;
},
},
render() {
const { tag, type, color, plain, disabled, loading, hairline } = this;
const style = {};
if (color) {
style.color = plain ? color : WHITE;
if (!plain) {
// Use background instead of backgroundColor to make linear-gradient work
style.background = color;
}
// hide border when color is linear-gradient
if (color.indexOf('gradient') !== -1) {
style.border = 0;
} else {
style.borderColor = color;
}
}
const classes = [
bem([
type,
this.size,
{
plain,
loading,
disabled,
hairline,
block: this.block,
round: this.round,
square: this.square,
},
]),
{ [BORDER_SURROUND]: hairline },
];
return (
<tag
style={style}
class={classes}
type={this.nativeType}
disabled={disabled}
onClick={this.onClick}
onTouchstart={this.onTouchstart}
>
<div class={bem('content')}>{this.genContent()}</div>
</tag>
);
},
});
-191
View File
@@ -1,191 +0,0 @@
// Utils
import { createNamespace } from '../utils';
import { emit, inherit } from '../utils/functional';
import { BORDER_SURROUND, WHITE } from '../utils/constant';
import { routeProps, RouteProps, functionalRoute } from '../utils/router';
// Components
import Icon from '../icon';
import Loading, { LoadingType } from '../loading';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type ButtonType = 'default' | 'primary' | 'info' | 'warning' | 'danger';
export type ButtonSize = 'large' | 'normal' | 'small' | 'mini';
export type ButtonProps = RouteProps & {
tag: keyof HTMLElementTagNameMap | string;
type: ButtonType;
size: ButtonSize;
text?: string;
icon?: string;
color?: string;
block?: boolean;
plain?: boolean;
round?: boolean;
square?: boolean;
loading?: boolean;
hairline?: boolean;
disabled?: boolean;
nativeType?: string;
iconPrefix?: string;
loadingSize: string;
loadingType?: LoadingType;
loadingText?: string;
};
export type ButtonEvents = {
onClick?(event: Event): void;
};
const [createComponent, bem] = createNamespace('button');
function Button(
h: CreateElement,
props: ButtonProps,
slots: DefaultSlots,
ctx: RenderContext<ButtonProps>
) {
const {
tag,
icon,
type,
color,
plain,
disabled,
loading,
hairline,
loadingText,
} = props;
const style: Record<string, string | number> = {};
if (color) {
style.color = plain ? color : WHITE;
if (!plain) {
// Use background instead of backgroundColor to make linear-gradient work
style.background = color;
}
// hide border when color is linear-gradient
if (color.indexOf('gradient') !== -1) {
style.border = 0;
} else {
style.borderColor = color;
}
}
function onClick(event: Event) {
if (!loading && !disabled) {
emit(ctx, 'click', event);
functionalRoute(ctx);
}
}
function onTouchstart(event: TouchEvent) {
emit(ctx, 'touchstart', event);
}
const classes = [
bem([
type,
props.size,
{
plain,
loading,
disabled,
hairline,
block: props.block,
round: props.round,
square: props.square,
},
]),
{ [BORDER_SURROUND]: hairline },
];
function Content() {
const content = [];
if (loading) {
content.push(
<Loading
class={bem('loading')}
size={props.loadingSize}
type={props.loadingType}
color="currentColor"
/>
);
} else if (icon) {
content.push(
<Icon name={icon} class={bem('icon')} classPrefix={props.iconPrefix} />
);
}
let text;
if (loading) {
text = loadingText;
} else {
text = slots.default ? slots.default() : props.text;
}
if (text) {
content.push(<span class={bem('text')}>{text}</span>);
}
return content;
}
return (
<tag
style={style}
class={classes}
type={props.nativeType}
disabled={disabled}
onClick={onClick}
onTouchstart={onTouchstart}
{...inherit(ctx)}
>
<div class={bem('content')}>{Content()}</div>
</tag>
);
}
Button.props = {
...routeProps,
text: String,
icon: String,
color: String,
block: Boolean,
plain: Boolean,
round: Boolean,
square: Boolean,
loading: Boolean,
hairline: Boolean,
disabled: Boolean,
iconPrefix: String,
nativeType: String,
loadingText: String,
loadingType: String,
tag: {
type: String,
default: 'button',
},
type: {
type: String,
default: 'default',
},
size: {
type: String,
default: 'normal',
},
loadingSize: {
type: String,
default: '20px',
},
};
export default createComponent<ButtonProps, ButtonEvents>(Button);
+38
View File
@@ -0,0 +1,38 @@
// Utils
import { createNamespace } from '../utils';
import { BORDER_TOP_BOTTOM } from '../utils/constant';
const [createComponent, bem] = createNamespace('cell-group');
export default createComponent({
props: {
title: String,
border: {
type: Boolean,
default: true,
},
},
setup(props, { slots }) {
return function () {
const Group = (
<div class={[bem(), { [BORDER_TOP_BOTTOM]: props.border }]}>
{slots.default?.()}
</div>
);
if (props.title || slots.title) {
return (
<>
<div class={bem('title')} {...this.$attrs}>
{slots.title ? slots.title() : props.title}
</div>
{Group}
</>
);
}
return Group;
};
},
});
-58
View File
@@ -1,58 +0,0 @@
// Utils
import { createNamespace } from '../utils';
import { inherit } from '../utils/functional';
import { BORDER_TOP_BOTTOM } from '../utils/constant';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots, ScopedSlot } from '../utils/types';
export type CellGroupProps = {
title?: string;
border: boolean;
};
export type CellGroupSlots = DefaultSlots & {
title?: ScopedSlot;
};
const [createComponent, bem] = createNamespace('cell-group');
function CellGroup(
h: CreateElement,
props: CellGroupProps,
slots: CellGroupSlots,
ctx: RenderContext<CellGroupProps>
) {
const Group = (
<div
class={[bem(), { [BORDER_TOP_BOTTOM]: props.border }]}
{...inherit(ctx, true)}
>
{slots.default?.()}
</div>
);
if (props.title || slots.title) {
return (
<div>
<div class={bem('title')}>
{slots.title ? slots.title() : props.title}
</div>
{Group}
</div>
);
}
return Group;
}
CellGroup.props = {
title: String,
border: {
type: Boolean,
default: true,
},
};
export default createComponent<CellGroupProps>(CellGroup);
+133
View File
@@ -0,0 +1,133 @@
// Utils
import { createNamespace, isDef } from '../utils';
import { route, routeProps } from '../utils/router';
import { cellProps } from './shared';
// Components
import Icon from '../icon';
const [createComponent, bem] = createNamespace('cell');
export default createComponent({
props: {
...cellProps,
...routeProps,
},
emits: ['click'],
setup(props, { slots, emit }) {
return function () {
const { icon, size, title, label, value, isLink } = props;
const showTitle = slots.title || isDef(title);
function Label() {
const showLabel = slots.label || isDef(label);
if (showLabel) {
return (
<div class={[bem('label'), props.labelClass]}>
{slots.label ? slots.label() : label}
</div>
);
}
}
function Title() {
if (showTitle) {
return (
<div
class={[bem('title'), props.titleClass]}
style={props.titleStyle}
>
{slots.title ? slots.title() : <span>{title}</span>}
{Label()}
</div>
);
}
}
function Value() {
const showValue = slots.default || isDef(value);
if (showValue) {
return (
<div
class={[bem('value', { alone: !showTitle }), props.valueClass]}
>
{slots.default ? slots.default() : <span>{value}</span>}
</div>
);
}
}
function LeftIcon() {
if (slots.icon) {
return slots.icon();
}
if (icon) {
return (
<Icon
class={bem('left-icon')}
name={icon}
classPrefix={props.iconPrefix}
/>
);
}
}
function RightIcon() {
const rightIconSlot = slots['right-icon'];
if (rightIconSlot) {
return rightIconSlot();
}
if (isLink) {
const { arrowDirection } = props;
return (
<Icon
class={bem('right-icon')}
name={arrowDirection ? `arrow-${arrowDirection}` : 'arrow'}
/>
);
}
}
const onClick = (event) => {
emit('click', event);
route(this.$router, this);
};
const clickable = isLink || props.clickable;
const classes = {
clickable,
center: props.center,
required: props.required,
borderless: !props.border,
};
if (size) {
classes[size] = size;
}
return (
<div
class={bem(classes)}
role={clickable ? 'button' : null}
tabindex={clickable ? 0 : null}
onClick={onClick}
>
{LeftIcon()}
{Title()}
{Value()}
{RightIcon()}
{slots.extra?.()}
</div>
);
};
},
});
-150
View File
@@ -1,150 +0,0 @@
// Utils
import { createNamespace, isDef } from '../utils';
import { emit, inherit } from '../utils/functional';
import { routeProps, RouteProps, functionalRoute } from '../utils/router';
import { cellProps, SharedCellProps } from './shared';
// Components
import Icon from '../icon';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { ScopedSlot, DefaultSlots } from '../utils/types';
import { Mods } from '../utils/create/bem';
export type CellProps = RouteProps & SharedCellProps;
export type CellSlots = DefaultSlots & {
icon?: ScopedSlot;
title?: ScopedSlot;
label?: ScopedSlot;
extra?: ScopedSlot;
'right-icon'?: ScopedSlot;
};
export type CellEvents = {
onClick?(event: Event): void;
};
const [createComponent, bem] = createNamespace('cell');
function Cell(
h: CreateElement,
props: CellProps,
slots: CellSlots,
ctx: RenderContext<CellProps>
) {
const { icon, size, title, label, value, isLink } = props;
const showTitle = slots.title || isDef(title);
function Label() {
const showLabel = slots.label || isDef(label);
if (showLabel) {
return (
<div class={[bem('label'), props.labelClass]}>
{slots.label ? slots.label() : label}
</div>
);
}
}
function Title() {
if (showTitle) {
return (
<div class={[bem('title'), props.titleClass]} style={props.titleStyle}>
{slots.title ? slots.title() : <span>{title}</span>}
{Label()}
</div>
);
}
}
function Value() {
const showValue = slots.default || isDef(value);
if (showValue) {
return (
<div class={[bem('value', { alone: !showTitle }), props.valueClass]}>
{slots.default ? slots.default() : <span>{value}</span>}
</div>
);
}
}
function LeftIcon() {
if (slots.icon) {
return slots.icon();
}
if (icon) {
return (
<Icon
class={bem('left-icon')}
name={icon}
classPrefix={props.iconPrefix}
/>
);
}
}
function RightIcon() {
const rightIconSlot = slots['right-icon'];
if (rightIconSlot) {
return rightIconSlot();
}
if (isLink) {
const { arrowDirection } = props;
return (
<Icon
class={bem('right-icon')}
name={arrowDirection ? `arrow-${arrowDirection}` : 'arrow'}
/>
);
}
}
function onClick(event: Event) {
emit(ctx, 'click', event);
functionalRoute(ctx);
}
const clickable = isLink || props.clickable;
const classes: Mods = {
clickable,
center: props.center,
required: props.required,
borderless: !props.border,
};
if (size) {
classes[size] = size;
}
return (
<div
class={bem(classes)}
role={clickable ? 'button' : null}
tabindex={clickable ? 0 : null}
onClick={onClick}
{...inherit(ctx)}
>
{LeftIcon()}
{Title()}
{Value()}
{RightIcon()}
{slots.extra?.()}
</div>
);
}
Cell.props = {
...cellProps,
...routeProps,
};
export default createComponent<CellProps, CellEvents, CellSlots>(Cell);
+7 -7
View File
@@ -14,7 +14,7 @@ Vue.use(Circle);
### Basic Usage
```html
<van-circle v-model="currentRate" :rate="30" :speed="100" :text="text" />
<van-circle v-model:currentRate="currentRate" :rate="30" :speed="100" :text="text" />
```
```js
@@ -36,7 +36,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:stroke-width="60"
text="Custom Width"
@@ -47,7 +47,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
layer-color="#ebedf0"
text="Custom Color"
@@ -58,7 +58,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:color="gradientColor"
text="Gradient"
@@ -83,7 +83,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:clockwise="false"
text="Counter Clockwise"
@@ -94,7 +94,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
size="120px"
text="Custom Size"
@@ -107,7 +107,7 @@ export default {
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| v-model | Current rate | _number_ | - |
| v-model:currentRate | Current rate | _number_ | - |
| rate | Target rate | _number \| string_ | `100` |
| size | Circle size | _number \| string_ | `100px` |
| color `v2.1.4` | Progress color, passing object to render gradient | _string \| object_ | `#1989fa` |
+18 -8
View File
@@ -13,10 +13,15 @@ Vue.use(Circle);
### 基础用法
`rate`属性表示进度条的目标进度,`v-model`表示动画过程中的实时进度。当`rate`发生变化时,`v-model`会以`speed`的速度变化,直至达到`rate`设定的值。
`rate`属性表示进度条的目标进度,`v-model:currentRate`表示动画过程中的实时进度。当`rate`发生变化时,`v-model:currentRate`会以`speed`的速度变化,直至达到`rate`设定的值。
```html
<van-circle v-model="currentRate" :rate="30" :speed="100" :text="text" />
<van-circle
v-model:currentRate="currentRate"
:rate="30"
:speed="100"
:text="text"
/>
```
```js
@@ -40,7 +45,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:stroke-width="60"
text="宽度定制"
@@ -53,7 +58,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
layer-color="#ebedf0"
text="颜色定制"
@@ -66,7 +71,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:color="gradientColor"
text="渐变色"
@@ -93,7 +98,7 @@ export default {
```html
<van-circle
v-model="currentRate"
v-model:currentRate="currentRate"
:rate="rate"
:clockwise="false"
text="逆时针方向"
@@ -105,7 +110,12 @@ export default {
通过`size`属性设置圆环直径
```html
<van-circle v-model="currentRate" :rate="rate" size="120px" text="大小定制" />
<van-circle
v-model:currentRate="currentRate"
:rate="rate"
size="120px"
text="大小定制"
/>
```
## API
@@ -114,7 +124,7 @@ export default {
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| v-model | 当前进度 | _number_ | - |
| v-model:currentRate | 当前进度 | _number_ | - |
| rate | 目标进度 | _number \| string_ | `100` |
| size | 圆环直径,默认单位为 `px` | _number \| string_ | `100px` |
| color `v2.1.4` | 进度条颜色,传入对象格式可以定义渐变色 | _string \| object_ | `#1989fa` |
+6 -6
View File
@@ -2,7 +2,7 @@
<demo-section>
<demo-block :title="t('basicUsage')">
<van-circle
v-model="currentRate1"
v-model:currentRate="currentRate1"
:rate="rate"
:speed="100"
:text="currentRate1.toFixed(0) + '%'"
@@ -11,7 +11,7 @@
<demo-block :title="t('customStyle')">
<van-circle
v-model="currentRate3"
v-model:currentRate="currentRate3"
:rate="rate"
:speed="100"
:stroke-width="60"
@@ -19,7 +19,7 @@
/>
<van-circle
v-model="currentRate3"
v-model:currentRate="currentRate3"
color="#ee0a24"
:rate="rate"
layer-color="#ebedf0"
@@ -28,7 +28,7 @@
/>
<van-circle
v-model="currentRate2"
v-model:currentRate="currentRate2"
:rate="rate"
:speed="100"
:color="gradientColor"
@@ -36,7 +36,7 @@
/>
<van-circle
v-model="currentRate4"
v-model:currentRate="currentRate4"
color="#07c160"
:rate="rate"
:speed="100"
@@ -46,7 +46,7 @@
/>
<van-circle
v-model="currentRate4"
v-model:currentRate="currentRate4"
color="#7232dd"
:rate="rate"
:speed="100"
+10 -7
View File
@@ -23,7 +23,7 @@ export default createComponent({
props: {
text: String,
strokeLinecap: String,
value: {
currentRate: {
type: Number,
default: 0,
},
@@ -61,6 +61,8 @@ export default createComponent({
},
},
emits: ['update:currentRate'],
beforeCreate() {
this.uid = `van-circle-gradient-${uid++}`;
},
@@ -83,7 +85,7 @@ export default createComponent({
},
layerStyle() {
const offset = (PERIMETER * this.value) / 100;
const offset = (PERIMETER * this.currentRate) / 100;
return {
stroke: `${this.color}`,
@@ -130,7 +132,7 @@ export default createComponent({
rate: {
handler(rate) {
this.startTime = Date.now();
this.startRate = this.value;
this.startRate = this.currentRate;
this.endRate = format(rate);
this.increase = this.endRate > this.startRate;
this.duration = Math.abs(
@@ -141,7 +143,7 @@ export default createComponent({
cancelRaf(this.rafId);
this.rafId = raf(this.animate);
} else {
this.$emit('input', this.endRate);
this.$emit('update:currentRate', this.endRate);
}
},
immediate: true,
@@ -154,7 +156,7 @@ export default createComponent({
const progress = Math.min((now - this.startTime) / this.duration, 1);
const rate = progress * (this.endRate - this.startRate) + this.startRate;
this.$emit('input', format(parseFloat(rate.toFixed(1))));
this.$emit('update:currentRate', format(parseFloat(rate.toFixed(1))));
if (this.increase ? rate < this.endRate : rate > this.endRate) {
this.rafId = raf(this.animate);
@@ -175,8 +177,9 @@ export default createComponent({
stroke={this.gradient ? `url(#${this.uid})` : this.color}
/>
</svg>
{this.slots() ||
(this.text && <div class={bem('text')}>{this.text}</div>)}
{this.$slots.default
? this.$slots.default()
: this.text && <div class={bem('text')}>{this.text}</div>}
</div>
);
},
+3 -1
View File
@@ -15,6 +15,8 @@ export default createComponent({
},
},
emits: ['click'],
computed: {
style() {
const { index } = this;
@@ -44,7 +46,7 @@ export default createComponent({
class={bem({ [span]: span, [`offset-${offset}`]: offset })}
onClick={this.onClick}
>
{this.slots()}
{this.$slots.default?.()}
</this.tag>
);
},
+8 -2
View File
@@ -21,6 +21,8 @@ export default createComponent({
},
},
emits: ['change', 'finish'],
data() {
return {
remain: 0,
@@ -40,7 +42,9 @@ export default createComponent({
watch: {
time: {
immediate: true,
handler: 'reset',
handler() {
this.reset();
},
},
},
@@ -153,7 +157,9 @@ export default createComponent({
render() {
return (
<div class={bem()}>
{this.slots('default', this.timeData) || this.formattedTime}
{this.$slots.default
? this.$slots.default(this.timeData)
: this.formattedTime}
</div>
);
},
+3 -3
View File
@@ -1,6 +1,6 @@
import Vue from 'vue';
// import Vue from 'vue';
import VanDialog from './Dialog';
import { isServer } from '../utils';
import { inBrowser } from '../utils';
let instance;
@@ -28,7 +28,7 @@ function initInstance() {
function Dialog(options) {
/* istanbul ignore if */
if (isServer) {
if (!inBrowser) {
return Promise.resolve();
}
+33
View File
@@ -0,0 +1,33 @@
import { createNamespace } from '../utils';
const [createComponent, bem] = createNamespace('divider');
export default createComponent({
props: {
dashed: Boolean,
hairline: {
type: Boolean,
default: true,
},
contentPosition: {
type: String,
default: 'center',
},
},
render() {
const Content = this.$slots.default?.();
return (
<div
role="separator"
class={bem({
dashed: this.dashed,
hairline: this.hairline,
[`content-${this.contentPosition}`]: !!Content,
})}
>
{Content}
</div>
);
},
});
-52
View File
@@ -1,52 +0,0 @@
// Utils
import { createNamespace } from '../utils';
import { inherit } from '../utils/functional';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type DividerProps = {
dashed?: boolean;
hairline: boolean;
borderColor?: string;
contentPosition: 'left' | 'center' | 'right';
};
const [createComponent, bem] = createNamespace('divider');
function Divider(
h: CreateElement,
props: DividerProps,
slots: DefaultSlots,
ctx: RenderContext
) {
return (
<div
role="separator"
style={{ borderColor: props.borderColor }}
class={bem({
dashed: props.dashed,
hairline: props.hairline,
[`content-${props.contentPosition}`]: slots.default,
})}
{...inherit(ctx, true)}
>
{slots.default && slots.default()}
</div>
);
}
Divider.props = {
dashed: Boolean,
hairline: {
type: Boolean,
default: true,
},
contentPosition: {
type: String,
default: 'center',
},
};
export default createComponent<DividerProps>(Divider);
@@ -1,6 +1,6 @@
export default {
render() {
const genStop = (color: string, offset: number, opacity?: number) => (
const genStop = (color, offset, opacity) => (
<stop stop-color={color} offset={`${offset}%`} stop-opacity={opacity} />
);
+5 -3
View File
@@ -16,7 +16,7 @@ export default createComponent({
methods: {
genImageContent() {
const slots = this.slots('image');
const slots = this.$slots.image?.();
if (slots) {
return slots;
@@ -40,7 +40,9 @@ export default createComponent({
},
genDescription() {
const description = this.slots('description') || this.description;
const description = this.$slots.description
? this.slot.description()
: this.description;
if (description) {
return <p class={bem('description')}>{description}</p>;
@@ -48,7 +50,7 @@ export default createComponent({
},
genBottom() {
const slot = this.slots();
const slot = this.$slots.default?.();
if (slot) {
return <div class={bem('bottom')}>{slot}</div>;
+11 -17
View File
@@ -1,5 +1,5 @@
// Utils
import { createNamespace, addUnit, isDef } from '../utils';
import { createNamespace, addUnit } from '../utils';
import { BORDER } from '../utils/constant';
import { route, routeProps } from '../utils/router';
@@ -21,10 +21,11 @@ export default createComponent({
text: String,
icon: String,
iconPrefix: String,
info: [Number, String],
badge: [Number, String],
},
emits: ['click'],
computed: {
style() {
const { square, gutter, columnNum } = this.parent;
@@ -70,14 +71,11 @@ export default createComponent({
},
genIcon() {
const iconSlot = this.slots('icon');
const info = isDef(this.badge) ? this.badge : this.info;
if (iconSlot) {
if (this.$slots.icon) {
return (
<div class={bem('icon-wrapper')}>
{iconSlot}
<Info dot={this.dot} info={info} />
{this.$slots.icon()}
<Info dot={this.dot} info={this.badge} />
</div>
);
}
@@ -87,7 +85,7 @@ export default createComponent({
<Icon
name={this.icon}
dot={this.dot}
info={info}
info={this.badge}
size={this.parent.iconSize}
class={bem('icon')}
classPrefix={this.iconPrefix}
@@ -97,10 +95,8 @@ export default createComponent({
},
getText() {
const textSlot = this.slots('text');
if (textSlot) {
return textSlot;
if (this.$slots.text) {
return this.$slots.text();
}
if (this.text) {
@@ -109,10 +105,8 @@ export default createComponent({
},
genContent() {
const slot = this.slots();
if (slot) {
return slot;
if (this.$slots.default) {
return this.$slots.default();
}
return [this.genIcon(), this.getText()];
+1 -1
View File
@@ -45,7 +45,7 @@ export default createComponent({
style={this.style}
class={[bem(), { [BORDER_TOP]: this.border && !this.gutter }]}
>
{this.slots()}
{this.$slots.default?.()}
</div>
);
},
+21
View File
@@ -0,0 +1,21 @@
import { Ref } from 'vue';
import { useGlobalEvent } from './use-global-event';
export type UseClickOutsideOpitons = {
event: string;
callback: EventListener;
element: Ref<Element>;
flag?: Ref<boolean>;
};
export function useClickOutside(options: UseClickOutsideOpitons) {
const { event = 'click', callback, element, flag } = options;
function onClick(event: Event) {
if (!element.value.contains(event.target as Node)) {
callback(event);
}
}
useGlobalEvent(document, event, onClick, false, flag);
}
+46
View File
@@ -0,0 +1,46 @@
import { on, off } from '../utils/dom/event';
import {
Ref,
watch,
onMounted,
onActivated,
onUnmounted,
onDeactivated
} from 'vue';
export function useGlobalEvent(
target: EventTarget,
event: string,
handler: EventListener,
passive = false,
flag?: Ref<boolean>
) {
let binded: boolean;
function add() {
if (binded || (flag && !flag.value)) {
return;
}
on(target, event, handler, passive);
binded = true;
}
function remove() {
if (binded) {
off(target, event, handler);
binded = false;
}
}
if (flag) {
watch(() => {
flag.value ? add() : remove();
});
}
onMounted(add);
onActivated(add);
onUnmounted(remove);
onDeactivated(remove);
}
+55
View File
@@ -0,0 +1,55 @@
import { useTouch } from './use-touch';
import { getScroller } from '../utils/dom/scroll';
import { on, off, preventDefault } from '../utils/dom/event';
let count = 0;
const CLASSNAME = 'van-overflow-hidden';
export function useLockScroll(element: HTMLElement) {
const { start, move, deltaY, direction } = useTouch();
function onTouchMove(event: TouchEvent) {
move(event);
if (direction.value !== 'vertical') {
return;
}
let prevent = false;
const up = deltaY.value < 0;
const scroller = getScroller(event.target as HTMLElement, element);
const { scrollTop, scrollHeight, offsetHeight } = scroller as HTMLElement;
if (scrollTop === 0) {
prevent = up && offsetHeight < scrollHeight;
} else if (scrollTop + offsetHeight >= scrollHeight) {
prevent = !up;
}
if (prevent) {
preventDefault(event, true);
}
}
function lock() {
if (!count) {
document.body.classList.add(CLASSNAME);
}
count++;
on(document, 'touchstart', start);
on(document, 'touchmove', onTouchMove);
}
lock();
return function unlock() {
count--;
off(document, 'touchstart', start);
off(document, 'touchmove', onTouchMove);
if (!count) {
document.body.classList.remove(CLASSNAME);
}
};
}
+63
View File
@@ -0,0 +1,63 @@
import { ref } from 'vue';
const MIN_DISTANCE = 10;
function getDirection(x: number, y: number) {
if (x > y && x > MIN_DISTANCE) {
return 'horizontal';
}
if (y > x && y > MIN_DISTANCE) {
return 'vertical';
}
return '';
}
export function useTouch() {
const startX = ref(0);
const startY = ref(0);
const deltaX = ref(0);
const deltaY = ref(0);
const offsetX = ref(0);
const offsetY = ref(0);
const direction = ref('');
function reset() {
direction.value = '';
deltaX.value = 0;
deltaY.value = 0;
offsetX.value = 0;
offsetY.value = 0;
}
function start(event: TouchEvent) {
reset();
startX.value = event.touches[0].clientX;
startY.value = event.touches[0].clientY;
}
function move(event: TouchEvent) {
const touch = event.touches[0];
deltaX.value = touch.clientX - startX.value;
deltaY.value = touch.clientY - startY.value;
offsetX.value = Math.abs(deltaX.value);
offsetY.value = Math.abs(deltaY.value);
if (!direction.value) {
direction.value = getDirection(offsetX.value, offsetY.value);
}
}
return {
move,
start,
startX,
startY,
deltaX,
deltaY,
offsetX,
offsetY,
direction
};
}
+54
View File
@@ -0,0 +1,54 @@
// Utils
import { addUnit, createNamespace } from '../utils';
// Components
import Info from '../info';
const [createComponent, bem] = createNamespace('icon');
function isImage(name) {
return name ? name.indexOf('/') !== -1 : false;
}
export default createComponent({
props: {
dot: Boolean,
name: String,
size: [Number, String],
badge: [Number, String],
color: String,
tag: {
type: String,
default: 'i',
},
classPrefix: {
type: String,
default: bem(),
},
},
render() {
const { name } = this;
const imageIcon = isImage(name);
return (
<this.tag
class={[
this.classPrefix,
imageIcon ? '' : `${this.classPrefix}-${name}`,
]}
style={{
color: this.color,
fontSize: addUnit(this.size),
}}
>
{this.$slots.default?.()}
{imageIcon && <img class={bem('image')} src={name} />}
<Info
dot={this.dot}
info={this.badge}
/>
</this.tag>
);
}
});
-93
View File
@@ -1,93 +0,0 @@
// Utils
import { createNamespace, addUnit, isDef } from '../utils';
import { inherit } from '../utils/functional';
// Components
import Info from '../info';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type IconProps = {
dot?: boolean;
tag: keyof HTMLElementTagNameMap | string;
name?: string;
size?: string | number;
info?: string | number;
badge?: string | number;
color?: string;
classPrefix: string;
};
export type IconEvents = {
onClick?(event: Event): void;
};
const [createComponent, bem] = createNamespace('icon');
function isImage(name?: string): boolean {
return name ? name.indexOf('/') !== -1 : false;
}
// compatible with legacy usage, should be removed in next major version
const LEGACY_MAP: Record<string, string> = {
medel: 'medal',
'medel-o': 'medal-o',
};
function correctName(name?: string) {
return (name && LEGACY_MAP[name]) || name;
}
function Icon(
h: CreateElement,
props: IconProps,
slots: DefaultSlots,
ctx: RenderContext<IconProps>
) {
const name = correctName(props.name);
const imageIcon = isImage(name);
return (
<props.tag
class={[
props.classPrefix,
imageIcon ? '' : `${props.classPrefix}-${name}`,
]}
style={{
color: props.color,
fontSize: addUnit(props.size),
}}
{...inherit(ctx, true)}
>
{slots.default && slots.default()}
{imageIcon && <img class={bem('image')} src={name} />}
<Info
dot={props.dot}
info={isDef(props.badge) ? props.badge : props.info}
/>
</props.tag>
);
}
Icon.props = {
dot: Boolean,
name: String,
size: [Number, String],
// @deprecated
// should be removed in next major version
info: [Number, String],
badge: [Number, String],
color: String,
tag: {
type: String,
default: 'i',
},
classPrefix: {
type: String,
default: bem(),
},
};
export default createComponent<IconProps, IconEvents>(Icon);
+3 -3
View File
@@ -1,6 +1,6 @@
import Vue from 'vue';
// import Vue from 'vue';
import VueImagePreview from './ImagePreview';
import { isServer } from '../utils';
import { inBrowser } from '../utils';
let instance;
@@ -46,7 +46,7 @@ const initInstance = () => {
const ImagePreview = (images, startPosition = 0) => {
/* istanbul ignore if */
if (isServer) {
if (!inBrowser) {
return;
}
+19 -11
View File
@@ -31,6 +31,8 @@ export default createComponent({
},
},
emits: ['load', 'error', 'click'],
data() {
return {
loading: true,
@@ -116,7 +118,9 @@ export default createComponent({
if (this.loading && this.showLoading) {
return (
<div class={bem('loading')}>
{this.slots('loading') || (
{this.$slots.loading ? (
this.$slots.loading()
) : (
<Icon name={this.loadingIcon} class={bem('loading-icon')} />
)}
</div>
@@ -126,7 +130,9 @@ export default createComponent({
if (this.error && this.showError) {
return (
<div class={bem('error')}>
{this.slots('error') || (
{this.$slots.error ? (
this.$slots.error()
) : (
<Icon name={this.errorIcon} class={bem('error-icon')} />
)}
</div>
@@ -153,14 +159,16 @@ export default createComponent({
return <img ref="image" vLazy={this.src} {...imgData} />;
}
return (
<img
src={this.src}
onLoad={this.onLoad}
onError={this.onError}
{...imgData}
/>
);
if (this.src) {
return (
<img
src={this.src}
onLoad={this.onLoad}
onError={this.onError}
{...imgData}
/>
);
}
},
},
@@ -173,7 +181,7 @@ export default createComponent({
>
{this.genImage()}
{this.genPlaceholder()}
{this.slots()}
{this.$slots.default?.()}
</div>
);
},
+22
View File
@@ -0,0 +1,22 @@
// Utils
import { isDef, createNamespace } from '../utils';
const [createComponent, bem] = createNamespace('info');
export default createComponent({
props: {
dot: Boolean,
info: [Number, String],
},
render() {
const { dot, info } = this;
const showInfo = isDef(info) && info !== '';
if (!dot && !showInfo) {
return;
}
return <div class={bem({ dot })}>{dot ? '' : info}</div>;
},
});
-42
View File
@@ -1,42 +0,0 @@
// Utils
import { createNamespace, isDef } from '../utils';
import { inherit } from '../utils/functional';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type InfoProps = {
dot?: boolean;
info?: string | number;
badge?: string | number;
};
const [createComponent, bem] = createNamespace('info');
function Info(
h: CreateElement,
props: InfoProps,
slots: DefaultSlots,
ctx: RenderContext<InfoProps>
) {
const { dot, info } = props;
const showInfo = isDef(info) && info !== '';
if (!dot && !showInfo) {
return;
}
return (
<div class={bem({ dot })} {...inherit(ctx, true)}>
{dot ? '' : props.info}
</div>
);
}
Info.props = {
dot: Boolean,
info: [Number, String],
};
export default createComponent<InfoProps>(Info);
+64
View File
@@ -0,0 +1,64 @@
// Utils
import { createNamespace, addUnit } from '../utils';
const [createComponent, bem] = createNamespace('loading');
const SpinIcon = [];
for (let i = 0; i < 12; i++) {
SpinIcon.push(<i />);
}
const CircularIcon = (
<svg class={bem('circular')} viewBox="25 25 50 50">
<circle cx="50" cy="50" r="20" fill="none" />
</svg>
);
export default createComponent({
props: {
color: String,
size: [Number, String],
vertical: Boolean,
textSize: [Number, String],
type: {
type: String,
default: 'circular',
},
},
methods: {
genLoadingText() {
if (this.$slots.default) {
const style = this.textSize && {
fontSize: addUnit(this.textSize),
};
return (
<span class={bem('text')} style={style}>
{this.$slots.default()}
</span>
);
}
},
},
render() {
const { color, size, type, vertical } = this;
const style = { color };
if (size) {
const iconSize = addUnit(size);
style.width = iconSize;
style.height = iconSize;
}
return (
<div class={bem([type, { vertical }])}>
<span class={bem('spinner', type)} style={style}>
{type === 'spinner' ? SpinIcon : CircularIcon}
</span>
{this.genLoadingText()}
</div>
);
},
});
-94
View File
@@ -1,94 +0,0 @@
// Utils
import { createNamespace, addUnit } from '../utils';
import { inherit } from '../utils/functional';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type LoadingType = 'circular' | 'spinner';
export type LoadingProps = {
type: LoadingType;
size?: string | number;
color: string;
vertical?: boolean;
textSize?: string | number;
};
const [createComponent, bem] = createNamespace('loading');
function LoadingIcon(h: CreateElement, props: LoadingProps) {
if (props.type === 'spinner') {
const Spin = [];
for (let i = 0; i < 12; i++) {
Spin.push(<i />);
}
return Spin;
}
return (
<svg class={bem('circular')} viewBox="25 25 50 50">
<circle cx="50" cy="50" r="20" fill="none" />
</svg>
);
}
function LoadingText(
h: CreateElement,
props: LoadingProps,
slots: DefaultSlots
) {
if (slots.default) {
const style = props.textSize && {
fontSize: addUnit(props.textSize),
};
return (
<span class={bem('text')} style={style}>
{slots.default()}
</span>
);
}
}
function Loading(
h: CreateElement,
props: LoadingProps,
slots: DefaultSlots,
ctx: RenderContext<LoadingProps>
) {
const { color, size, type } = props;
const style: { [key: string]: string } = { color };
if (size) {
const iconSize = addUnit(size) as string;
style.width = iconSize;
style.height = iconSize;
}
return (
<div
class={bem([type, { vertical: props.vertical }])}
{...inherit(ctx, true)}
>
<span class={bem('spinner', type)} style={style}>
{LoadingIcon(h, props)}
</span>
{LoadingText(h, props, slots)}
</div>
);
}
Loading.props = {
color: String,
size: [Number, String],
vertical: Boolean,
textSize: [Number, String],
type: {
type: String,
default: 'circular',
},
};
export default createComponent<LoadingProps>(Loading);
+10 -19
View File
@@ -1,34 +1,25 @@
import Vue from 'vue';
import { ref } from 'vue';
import { deepAssign } from '../utils/deep-assign';
import defaultMessages from './lang/zh-CN';
declare module 'vue' {
interface VueConstructor {
util: {
defineReactive(obj: object, key: string, value: any): void;
};
}
}
type Messages = Record<string, Record<string, any>>;
const proto = Vue.prototype;
const { defineReactive } = Vue.util;
defineReactive(proto, '$vantLang', 'zh-CN');
defineReactive(proto, '$vantMessages', {
const lang = ref('zh-CN');
const messages = ref<Messages>({
'zh-CN': defaultMessages,
});
export default {
messages() {
return proto.$vantMessages[proto.$vantLang];
return messages.value[lang.value];
},
use(lang: string, messages?: object) {
proto.$vantLang = lang;
this.add({ [lang]: messages });
use(newLang: string, newMessages?: object) {
lang.value = newLang;
this.add({ [newLang]: newMessages });
},
add(messages = {}) {
deepAssign(proto.$vantMessages, messages);
add(newMessages = {}) {
deepAssign(messages.value, newMessages);
},
};
-31
View File
@@ -1,31 +0,0 @@
/**
* Bind event when mounted or activated
*/
import { on, off } from '../utils/dom/event';
let uid = 0;
export function BindEventMixin(handler) {
const key = `binded_${uid++}`;
function bind() {
if (!this[key]) {
handler.call(this, on, true);
this[key] = true;
}
}
function unbind() {
if (this[key]) {
handler.call(this, off, false);
this[key] = false;
}
}
return {
mounted: bind,
activated: bind,
deactivated: unbind,
beforeDestroy: unbind,
};
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Bind event when mounted or activated
*/
import { on, off } from '../utils/dom/event';
type BindEventMixinThis = {
binded: boolean;
};
type BindEventHandler = (bind: Function, isBind: boolean) => void;
export function BindEventMixin(handler: BindEventHandler) {
function bind(this: BindEventMixinThis) {
if (!this.binded) {
handler.call(this, on, true);
this.binded = true;
}
}
function unbind(this: BindEventMixinThis) {
if (this.binded) {
handler.call(this, off, false);
this.binded = false;
}
}
return {
mounted: bind,
activated: bind,
deactivated: unbind,
beforeDestroy: unbind,
};
}
-3
View File
@@ -1,9 +1,6 @@
import { OverlayConfig } from './overlay';
export type StackItem = {
vm: any;
overlay: any;
config: OverlayConfig;
};
export const context = {
+29 -43
View File
@@ -1,11 +1,5 @@
// Context
import { context } from './context';
import {
openOverlay,
closeOverlay,
updateOverlay,
removeOverlay,
} from './overlay';
// Utils
import { on, off, preventDefault } from '../../utils/dom/event';
@@ -14,18 +8,19 @@ import { getScroller } from '../../utils/dom/scroll';
// Mixins
import { TouchMixin } from '../touch';
import { PortalMixin } from '../portal';
import { CloseOnPopstateMixin } from '../close-on-popstate';
export const popupMixinProps = {
// whether to show popup
value: Boolean,
show: Boolean,
// whether to show overlay
overlay: Boolean,
// overlay custom style
overlayStyle: Object,
// overlay custom class name
overlayClass: String,
// teleport
getContainer: [String, Function],
// whether to close popup when click overlay
closeOnClickOverlay: Boolean,
// z-index
@@ -44,23 +39,13 @@ export const popupMixinProps = {
export function PopupMixin(options = {}) {
return {
mixins: [
TouchMixin,
CloseOnPopstateMixin,
PortalMixin({
afterPortal() {
if (this.overlay) {
updateOverlay();
}
},
}),
],
mixins: [TouchMixin, CloseOnPopstateMixin],
props: popupMixinProps,
data() {
return {
inited: this.value,
inited: this.show,
};
},
@@ -71,9 +56,9 @@ export function PopupMixin(options = {}) {
},
watch: {
value(val) {
show(val) {
const type = val ? 'open' : 'close';
this.inited = this.inited || this.value;
this.inited = this.inited || this.show;
this[type]();
if (!options.skipToggleEvent) {
@@ -85,7 +70,7 @@ export function PopupMixin(options = {}) {
},
mounted() {
if (this.value) {
if (this.show) {
this.open();
}
},
@@ -93,23 +78,22 @@ export function PopupMixin(options = {}) {
/* istanbul ignore next */
activated() {
if (this.shouldReopen) {
this.$emit('input', true);
this.$emit('update:show', true);
this.shouldReopen = false;
}
},
beforeDestroy() {
this.removeLock();
removeOverlay(this);
if (this.getContainer) {
removeNode(this.$el);
removeNode(this.$refs.root);
}
},
/* istanbul ignore next */
deactivated() {
if (this.value) {
if (this.show) {
this.close();
this.shouldReopen = true;
}
@@ -161,16 +145,15 @@ export function PopupMixin(options = {}) {
return;
}
closeOverlay(this);
this.opened = false;
this.removeLock();
this.$emit('input', false);
this.$emit('update:show', false);
},
onTouchMove(event) {
this.touchMove(event);
const direction = this.deltaY > 0 ? '10' : '01';
const el = getScroller(event.target, this.$el);
const el = getScroller(event.target, this.$refs.root);
const { scrollHeight, offsetHeight, scrollTop } = el;
let status = '11';
@@ -191,29 +174,32 @@ export function PopupMixin(options = {}) {
}
},
onClickOverlay() {
this.$emit('click-overlay');
if (this.closeOnClickOverlay) {
// TODO
// if (this.onClickOverlay) {
// this.onClickOverlay();
// } else {
// this.close();
// }
this.close();
}
},
renderOverlay() {
if (this.$isServer || !this.value) {
if (this.$isServer || !this.show) {
return;
}
this.$nextTick(() => {
this.updateZIndex(this.overlay ? 1 : 0);
if (this.overlay) {
openOverlay(this, {
zIndex: context.zIndex++,
duration: this.duration,
className: this.overlayClass,
customStyle: this.overlayStyle,
});
} else {
closeOverlay(this);
}
});
},
updateZIndex(value = 0) {
this.$el.style.zIndex = ++context.zIndex + value;
this.$refs.root.style.zIndex = ++context.zIndex + value;
},
},
};
-77
View File
@@ -1,77 +0,0 @@
import Overlay from '../../overlay';
import { context } from './context';
import { mount } from '../../utils/functional';
import { removeNode } from '../../utils/dom/node';
export type OverlayConfig = {
zIndex?: number;
className?: string;
customStyle?: string | object[] | object;
};
const defaultConfig: OverlayConfig = {
className: '',
customStyle: {},
};
function mountOverlay(vm: any) {
return mount(Overlay, {
on: {
// close popup when overlay clicked & closeOnClickOverlay is true
click() {
vm.$emit('click-overlay');
if (vm.closeOnClickOverlay) {
if (vm.onClickOverlay) {
vm.onClickOverlay();
} else {
vm.close();
}
}
},
},
});
}
export function updateOverlay(vm: any): void {
const item = context.find(vm);
if (item) {
const el = vm.$el;
const { config, overlay } = item;
if (el && el.parentNode) {
el.parentNode.insertBefore(overlay.$el, el);
}
Object.assign(overlay, defaultConfig, config, {
show: true,
});
}
}
export function openOverlay(vm: any, config: OverlayConfig): void {
const item = context.find(vm);
if (item) {
item.config = config;
} else {
const overlay = mountOverlay(vm);
context.stack.push({ vm, config, overlay });
}
updateOverlay(vm);
}
export function closeOverlay(vm: any): void {
const item = context.find(vm);
if (item) {
item.overlay.show = false;
}
}
export function removeOverlay(vm: any) {
const item = context.find(vm);
if (item) {
removeNode(item.overlay.$el);
}
}
-47
View File
@@ -1,47 +0,0 @@
function getElement(selector) {
if (typeof selector === 'string') {
return document.querySelector(selector);
}
return selector();
}
export function PortalMixin({ ref, afterPortal }) {
return {
props: {
getContainer: [String, Function],
},
watch: {
getContainer: 'portal',
},
mounted() {
if (this.getContainer) {
this.portal();
}
},
methods: {
portal() {
const { getContainer } = this;
const el = ref ? this.$refs[ref] : this.$el;
let container;
if (getContainer) {
container = getElement(getContainer);
} else if (this.$parent) {
container = this.$parent.$el;
}
if (container && container !== el.parentNode) {
container.appendChild(el);
}
if (afterPortal) {
afterPortal.call(this);
}
},
},
};
}
+3 -21
View File
@@ -1,24 +1,16 @@
import { sortChildren } from '../utils/vnodes';
export function ChildrenMixin(parent, options = {}) {
const indexKey = options.indexKey || 'index';
return {
inject: {
[parent]: {
// TODO: disableBindRelation
parent: {
from: parent,
default: null,
},
},
computed: {
parent() {
if (this.disableBindRelation) {
return null;
}
return this[parent];
},
[indexKey]() {
this.bindRelation();
@@ -30,14 +22,6 @@ export function ChildrenMixin(parent, options = {}) {
},
},
watch: {
disableBindRelation(val) {
if (!val) {
this.bindRelation();
}
},
},
mounted() {
this.bindRelation();
},
@@ -58,8 +42,6 @@ export function ChildrenMixin(parent, options = {}) {
const children = [...this.parent.children, this];
sortChildren(children, this.parent);
this.parent.children = children;
},
},
+5 -3
View File
@@ -22,6 +22,8 @@ export default createComponent({
},
},
emits: ['click-left', 'click-right'],
data() {
return {
height: null,
@@ -36,7 +38,7 @@ export default createComponent({
methods: {
genLeft() {
const leftSlot = this.slots('left');
const leftSlot = this.$slots.left?.();
if (leftSlot) {
return leftSlot;
@@ -49,7 +51,7 @@ export default createComponent({
},
genRight() {
const rightSlot = this.slots('right');
const rightSlot = this.$slots.right?.();
if (rightSlot) {
return rightSlot;
@@ -71,7 +73,7 @@ export default createComponent({
{this.genLeft()}
</div>
<div class={[bem('title'), 'van-ellipsis']}>
{this.slots('title') || this.title}
{this.$slots.title ? this.$slots.title() : this.title}
</div>
<div class={bem('right')} onClick={this.onClickRight}>
{this.genRight()}
+49
View File
@@ -0,0 +1,49 @@
import { Transition } from 'vue';
import { createNamespace, isDef, noop } from '../utils';
import { preventDefault } from '../utils/dom/event';
const [createComponent, bem] = createNamespace('overlay');
function preventTouchMove(event) {
preventDefault(event, true);
}
export default createComponent({
props: {
show: Boolean,
zIndex: [Number, String],
duration: [Number, String],
className: null,
customStyle: Object,
lockScroll: {
type: Boolean,
default: true,
},
},
setup(props, { slots }) {
return function () {
const style = {
zIndex: props.zIndex,
...props.customStyle,
};
if (isDef(props.duration)) {
style.animationDuration = `${props.duration}s`;
}
return (
<Transition name="van-fade">
<div
vShow={props.show}
style={style}
class={[bem(), props.className]}
onTouchmove={props.lockScroll ? preventTouchMove : noop}
>
{slots.default?.()}
</div>
</Transition>
);
};
},
});
-71
View File
@@ -1,71 +0,0 @@
// Utils
import { createNamespace, isDef, noop } from '../utils';
import { inherit } from '../utils/functional';
import { preventDefault } from '../utils/dom/event';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type OverlayProps = {
show?: boolean;
zIndex?: number | string;
duration: number | string | null;
className?: any;
lockScroll?: boolean;
customStyle?: object;
};
export type OverlayEvents = {
click(event: Event): void;
};
const [createComponent, bem] = createNamespace('overlay');
function preventTouchMove(event: TouchEvent) {
preventDefault(event, true);
}
function Overlay(
h: CreateElement,
props: OverlayProps,
slots: DefaultSlots,
ctx: RenderContext<OverlayProps>
) {
const style: { [key: string]: any } = {
zIndex: props.zIndex,
...props.customStyle,
};
if (isDef(props.duration)) {
style.animationDuration = `${props.duration}s`;
}
return (
<transition name="van-fade">
<div
vShow={props.show}
style={style}
class={[bem(), props.className]}
onTouchmove={props.lockScroll ? preventTouchMove : noop}
{...inherit(ctx, true)}
>
{slots.default?.()}
</div>
</transition>
);
}
Overlay.props = {
show: Boolean,
zIndex: [Number, String],
duration: [Number, String],
className: null as any,
customStyle: Object,
lockScroll: {
type: Boolean,
default: true,
},
};
export default createComponent<OverlayProps, OverlayEvents>(Overlay);
+1 -1
View File
@@ -23,8 +23,8 @@
<demo-block :title="t('title3')">
<van-pagination
force-ellipses
v-model="currentPage3"
force-ellipses
:total-items="125"
:show-page-size="3"
:prev-text="t('prevText')"
+13 -9
View File
@@ -16,7 +16,7 @@ export default createComponent({
type: String,
default: 'multi',
},
value: {
modelValue: {
type: Number,
default: 0,
},
@@ -38,6 +38,8 @@ export default createComponent({
},
},
emits: ['change', 'update:modelValue'],
computed: {
count() {
const count =
@@ -62,7 +64,7 @@ export default createComponent({
// recompute if showPageSize
if (isMaxSized) {
// Current page is displayed in the middle of the visible ones
startPage = Math.max(this.value - Math.floor(showPageSize / 2), 1);
startPage = Math.max(this.modelValue - Math.floor(showPageSize / 2), 1);
endPage = startPage + showPageSize - 1;
// Adjust if limit is exceeded
@@ -74,7 +76,7 @@ export default createComponent({
// Add page number links
for (let number = startPage; number <= endPage; number++) {
const page = makePage(number, number, number === this.value);
const page = makePage(number, number, number === this.modelValue);
pages.push(page);
}
@@ -96,9 +98,9 @@ export default createComponent({
},
watch: {
value: {
modelValue: {
handler(page) {
this.select(page || this.value);
this.select(page);
},
immediate: true,
},
@@ -107,8 +109,8 @@ export default createComponent({
methods: {
select(page, emitChange) {
page = Math.min(this.count, Math.max(1, page));
if (this.value !== page) {
this.$emit('input', page);
if (this.modelValue !== page) {
this.$emit('update:modelValue', page);
if (emitChange) {
this.$emit('change', page);
@@ -118,7 +120,7 @@ export default createComponent({
},
render() {
const { value } = this;
const value = this.modelValue;
const simple = this.mode !== 'multi';
const onSelect = (value) => () => {
@@ -143,7 +145,9 @@ export default createComponent({
))}
{simple && (
<li class={bem('page-desc')}>
{this.slots('pageDesc') || `${value}/${this.count}`}
{this.$slots.pageDesc
? this.$slots.pageDesc()
: `${value}/${this.count}`}
</li>
)}
<li
+10 -10
View File
@@ -15,7 +15,7 @@ Vue.use(Popup);
```html
<van-cell is-link @click="showPopup">Show Popup</van-cell>
<van-popup v-model="show">Content</van-popup>
<van-popup v-model:show="show">Content</van-popup>
```
```js
@@ -39,21 +39,21 @@ export default {
Use `position` prop to set popup display position
```html
<van-popup v-model="show" position="top" :style="{ height: '30%' }" />
<van-popup v-model:show="show" position="top" :style="{ height: '30%' }" />
```
### Close Icon
```html
<van-popup
v-model="show"
v-model:show="show"
closeable
position="bottom"
:style="{ height: '30%' }"
/>
<!-- Custom Icon -->
<van-popup
v-model="show"
v-model:show="show"
closeable
close-icon="close"
position="bottom"
@@ -61,7 +61,7 @@ Use `position` prop to set popup display position
/>
<!-- Icon Position -->
<van-popup
v-model="show"
v-model:show="show"
closeable
close-icon-position="top-left"
position="bottom"
@@ -72,7 +72,7 @@ Use `position` prop to set popup display position
### Round Corner
```html
<van-popup v-model="show" round position="bottom" :style="{ height: '30%' }" />
<van-popup v-model:show="show" round position="bottom" :style="{ height: '30%' }" />
```
### Get Container
@@ -81,13 +81,13 @@ Use `get-container` prop to specify mount location
```html
<!-- mount to body -->
<van-popup v-model="show" get-container="body" />
<van-popup v-model:show="show" get-container="body" />
<!-- mount to #app -->
<van-popup v-model="show" get-container="#app" />
<van-popup v-model:show="show" get-container="#app" />
<!-- Specify the mount location by function -->
<van-popup v-model="show" :get-container="getContainer" />
<van-popup v-model:show="show" :get-container="getContainer" />
```
```js
@@ -108,7 +108,7 @@ export default {
| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| v-model (value) | Whether to show popup | _boolean_ | `false` |
| v-model:show | Whether to show popup | _boolean_ | `false` |
| overlay | Whether to show overlay | _boolean_ | `true` |
| position | Can be set to `top` `bottom` `right` `left` | _string_ | `center` |
| overlay-class | Custom overlay class | _string_ | - |
+11 -11
View File
@@ -17,11 +17,11 @@ Vue.use(Popup);
### 基础用法
通过`v-model`控制弹出层是否展示
通过 `v-model:show` 控制弹出层是否展示
```html
<van-cell is-link @click="showPopup">展示弹出层</van-cell>
<van-popup v-model="show">内容</van-popup>
<van-popup v-model:show="show">内容</van-popup>
```
```js
@@ -45,7 +45,7 @@ export default {
通过`position`属性设置弹出位置,默认居中弹出,可以设置为`top``bottom``left``right`
```html
<van-popup v-model="show" position="top" :style="{ height: '30%' }" />
<van-popup v-model:show="show" position="top" :style="{ height: '30%' }" />
```
### 关闭图标
@@ -54,14 +54,14 @@ export default {
```html
<van-popup
v-model="show"
v-model:show="show"
closeable
position="bottom"
:style="{ height: '30%' }"
/>
<!-- 自定义图标 -->
<van-popup
v-model="show"
v-model:show="show"
closeable
close-icon="close"
position="bottom"
@@ -69,7 +69,7 @@ export default {
/>
<!-- 图标位置 -->
<van-popup
v-model="show"
v-model:show="show"
closeable
close-icon-position="top-left"
position="bottom"
@@ -82,7 +82,7 @@ export default {
设置`round`属性后,弹窗会根据弹出位置添加不同的圆角样式
```html
<van-popup v-model="show" round position="bottom" :style="{ height: '30%' }" />
<van-popup v-model:show="show" round position="bottom" :style="{ height: '30%' }" />
```
### 指定挂载位置
@@ -91,13 +91,13 @@ export default {
```html
<!-- 挂载到 body 节点下 -->
<van-popup v-model="show" get-container="body" />
<van-popup v-model:show="show" get-container="body" />
<!-- 挂载到 #app 节点下 -->
<van-popup v-model="show" get-container="#app" />
<van-popup v-model:show="show" get-container="#app" />
<!-- 通过函数指定挂载位置 -->
<van-popup v-model="show" :get-container="getContainer" />
<van-popup v-model:show="show" :get-container="getContainer" />
```
```js
@@ -119,7 +119,7 @@ export default {
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| v-model (value) | 是否显示弹出层 | _boolean_ | `false` |
| v-model:show | 是否显示弹出层 | _boolean_ | `false` |
| overlay | 是否显示遮罩层 | _boolean_ | `true` |
| position | 弹出位置,可选值为 `top` `bottom` `right` `left` | _string_ | `center` |
| overlay-class | 自定义遮罩层类名 | _string_ | - |
+14 -10
View File
@@ -2,7 +2,7 @@
<demo-section>
<demo-block card :title="t('basicUsage')">
<van-cell :title="t('buttonBasic')" is-link @click="showBasic = true" />
<van-popup v-model="showBasic" :style="{ padding: '30px 50px' }">
<van-popup v-model:show="showBasic" :style="{ padding: '30px 50px' }">
{{ t('content') }}
</van-popup>
</demo-block>
@@ -13,19 +13,23 @@
<van-cell :title="t('buttonLeft')" is-link @click="showLeft = true" />
<van-cell :title="t('buttonRight')" is-link @click="showRight = true" />
<van-popup v-model="showTop" position="top" :style="{ height: '30%' }" />
<van-popup
v-model="showBottom"
v-model:show="showTop"
position="top"
:style="{ height: '30%' }"
/>
<van-popup
v-model:show="showBottom"
position="bottom"
:style="{ height: '30%' }"
/>
<van-popup
v-model="showLeft"
v-model:show="showLeft"
position="left"
:style="{ width: '30%', height: '100%' }"
/>
<van-popup
v-model="showRight"
v-model:show="showRight"
position="right"
:style="{ width: '30%', height: '100%' }"
/>
@@ -45,20 +49,20 @@
/>
<van-popup
v-model="showCloseIcon"
v-model:show="showCloseIcon"
closeable
position="bottom"
:style="{ height: '30%' }"
/>
<van-popup
v-model="showCustomCloseIcon"
v-model:show="showCustomCloseIcon"
closeable
close-icon="close"
position="bottom"
:style="{ height: '30%' }"
/>
<van-popup
v-model="showCustomIconPosition"
v-model:show="showCustomIconPosition"
closeable
close-icon-position="top-left"
position="bottom"
@@ -73,7 +77,7 @@
@click="showRoundCorner = true"
/>
<van-popup
v-model="showRoundCorner"
v-model:show="showRoundCorner"
round
position="bottom"
:style="{ height: '30%' }"
@@ -87,7 +91,7 @@
@click="showGetContainer = true"
/>
<van-popup
v-model="showGetContainer"
v-model:show="showGetContainer"
get-container="body"
:style="{ padding: '30px 50px' }"
/>
+85 -43
View File
@@ -1,12 +1,16 @@
import { createNamespace, isDef } from '../utils';
import { Teleport, Transition } from 'vue';
import { createNamespace, isDef, isFunction } from '../utils';
import { PopupMixin } from '../mixins/popup';
import Icon from '../icon';
import Overlay from '../overlay';
const [createComponent, bem] = createNamespace('popup');
export default createComponent({
mixins: [PopupMixin()],
inheritAttrs: false,
props: {
round: Boolean,
duration: [Number, String],
@@ -35,6 +39,16 @@ export default createComponent({
},
},
emits: [
'open',
'close',
'click',
'opened',
'closed',
'update:show',
'click-overlay',
],
beforeCreate() {
const createEmitter = (eventName) => (event) =>
this.$emit(eventName, event);
@@ -44,52 +58,80 @@ export default createComponent({
this.onClosed = createEmitter('closed');
},
methods: {
genOverlay() {
if (this.overlay) {
return <Overlay show={this.show} onClick={this.onClickOverlay} />;
}
},
genPopup() {
const { round, position, duration } = this;
const isCenter = position === 'center';
const transitionName =
this.transition ||
(isCenter ? 'van-fade' : `van-popup-slide-${position}`);
const style = {};
if (isDef(duration)) {
const key = isCenter ? 'animationDuration' : 'transitionDuration';
style[key] = `${duration}s`;
}
return (
<Transition
name={transitionName}
onAfterEnter={this.onOpened}
onAfterLeave={this.onClosed}
>
{this.shouldRender ? (
<div
vShow={this.show}
ref="root"
style={style}
class={bem({
round,
[position]: position,
'safe-area-inset-bottom': this.safeAreaInsetBottom,
})}
onClick={this.onClick}
{...this.$attrs}
>
{this.$slots.default?.()}
{this.closeable && (
<Icon
role="button"
tabindex="0"
name={this.closeIcon}
class={bem('close-icon', this.closeIconPosition)}
onClick={this.close}
/>
)}
</div>
) : null}
</Transition>
);
},
},
render() {
if (!this.shouldRender) {
return;
}
const { round, position, duration } = this;
const isCenter = position === 'center';
const transitionName =
this.transition ||
(isCenter ? 'van-fade' : `van-popup-slide-${position}`);
const style = {};
if (isDef(duration)) {
const key = isCenter ? 'animationDuration' : 'transitionDuration';
style[key] = `${duration}s`;
const { getContainer } = this;
if (getContainer) {
const to = isFunction(getContainer) ? getContainer() : getContainer;
return (
<Teleport to={to}>
{this.genOverlay()}
{this.genPopup()}
</Teleport>
);
}
return (
<transition
name={transitionName}
onAfterEnter={this.onOpened}
onAfterLeave={this.onClosed}
>
<div
vShow={this.value}
style={style}
class={bem({
round,
[position]: position,
'safe-area-inset-bottom': this.safeAreaInsetBottom,
})}
onClick={this.onClick}
>
{this.slots()}
{this.closeable && (
<Icon
role="button"
tabindex="0"
name={this.closeIcon}
class={bem('close-icon', this.closeIconPosition)}
onClick={this.close}
/>
)}
</div>
</transition>
<>
{this.genOverlay()}
{this.genPopup()}
</>
);
},
});
+4 -4
View File
@@ -82,22 +82,22 @@
transition-timing-function: ease-in;
}
&-slide-top-enter,
&-slide-top-enter-from,
&-slide-top-leave-active {
transform: translate3d(0, -100%, 0);
}
&-slide-right-enter,
&-slide-right-enter-from,
&-slide-right-leave-active {
transform: translate3d(100%, -50%, 0);
}
&-slide-bottom-enter,
&-slide-bottom-enter-from,
&-slide-bottom-leave-active {
transform: translate3d(0, 100%, 0);
}
&-slide-left-enter,
&-slide-left-enter-from,
&-slide-left-leave-active {
transform: translate3d(-100%, -50%, 0);
}
+12 -9
View File
@@ -36,7 +36,7 @@ export default createComponent({
voidColor: String,
iconPrefix: String,
disabledColor: String,
value: {
modelValue: {
type: Number,
default: 0,
},
@@ -58,11 +58,15 @@ export default createComponent({
},
},
created() {
this.itemRefs = [];
},
computed: {
list() {
const list = [];
for (let i = 1; i <= this.count; i++) {
list.push(getRateStatus(this.value, i, this.allowHalf));
list.push(getRateStatus(this.modelValue, i, this.allowHalf));
}
return list;
@@ -83,8 +87,8 @@ export default createComponent({
methods: {
select(index) {
if (!this.disabled && !this.readonly && index !== this.value) {
this.$emit('input', index);
if (!this.disabled && !this.readonly && index !== this.modelValue) {
this.$emit('update:modelValue', index);
this.$emit('change', index);
}
},
@@ -96,9 +100,7 @@ export default createComponent({
this.touchStart(event);
const rects = this.$refs.items.map((item) =>
item.getBoundingClientRect()
);
const rects = this.itemRefs.map((item) => item.getBoundingClientRect());
const ranges = [];
rects.forEach((rect, index) => {
@@ -161,9 +163,10 @@ export default createComponent({
return (
<div
ref="items"
refInFor
key={index}
ref={(val) => {
this.itemRefs[index] = val;
}}
role="radio"
style={style}
tabindex="0"
+3 -1
View File
@@ -20,6 +20,8 @@ export default createComponent({
},
},
emits: ['click'],
computed: {
spaces() {
const gutter = Number(this.gutter);
@@ -80,7 +82,7 @@ export default createComponent({
})}
onClick={this.onClick}
>
{this.slots()}
{this.$slots.default?.()}
</this.tag>
);
},
+6 -9
View File
@@ -1,4 +1,4 @@
import { createNamespace, isDef } from '../utils';
import { createNamespace } from '../utils';
import { ChildrenMixin } from '../mixins/relation';
import { route, routeProps } from '../utils/router';
import Info from '../info';
@@ -8,10 +8,11 @@ const [createComponent, bem] = createNamespace('sidebar-item');
export default createComponent({
mixins: [ChildrenMixin('vanSidebar')],
emits: ['click'],
props: {
...routeProps,
dot: Boolean,
info: [Number, String],
badge: [Number, String],
title: String,
disabled: Boolean,
@@ -19,7 +20,7 @@ export default createComponent({
computed: {
select() {
return this.index === +this.parent.activeKey;
return this.index === +this.parent.modelValue;
},
},
@@ -30,7 +31,7 @@ export default createComponent({
}
this.$emit('click', this.index);
this.parent.$emit('input', this.index);
this.parent.$emit('update:modelValue', this.index);
this.parent.setIndex(this.index);
route(this.$router, this);
},
@@ -44,11 +45,7 @@ export default createComponent({
>
<div class={bem('text')}>
{this.title}
<Info
dot={this.dot}
info={isDef(this.badge) ? this.badge : this.info}
class={bem('info')}
/>
<Info dot={this.dot} info={this.badge} class={bem('info')} />
</div>
</a>
);
+6 -8
View File
@@ -6,12 +6,10 @@ const [createComponent, bem] = createNamespace('sidebar');
export default createComponent({
mixins: [ParentMixin('vanSidebar')],
model: {
prop: 'activeKey',
},
emits: ['change', 'update:modelValue'],
props: {
activeKey: {
modelValue: {
type: [Number, String],
default: 0,
},
@@ -19,13 +17,13 @@ export default createComponent({
data() {
return {
index: +this.activeKey,
index: +this.modelValue,
};
},
watch: {
activeKey() {
this.setIndex(+this.activeKey);
modelValue() {
this.setIndex(+this.modelValue);
},
},
@@ -39,6 +37,6 @@ export default createComponent({
},
render() {
return <div class={bem()}>{this.slots()}</div>;
return <div class={bem()}>{this.$slots.default?.()}</div>;
},
});
+110
View File
@@ -0,0 +1,110 @@
import { createNamespace, addUnit } from '../utils';
const [createComponent, bem] = createNamespace('skeleton');
const DEFAULT_ROW_WIDTH = '100%';
const DEFAULT_LAST_ROW_WIDTH = '60%';
export default createComponent({
props: {
title: Boolean,
round: Boolean,
avatar: Boolean,
row: {
type: [Number, String],
default: 0,
},
loading: {
type: Boolean,
default: true,
},
animate: {
type: Boolean,
default: true,
},
avatarSize: {
type: String,
default: '32px',
},
avatarShape: {
type: String,
default: 'round',
},
titleWidth: {
type: [Number, String],
default: '40%',
},
rowWidth: {
type: [Number, String, Array],
default: DEFAULT_ROW_WIDTH,
},
},
setup(props, { slots }) {
return function () {
if (!props.loading) {
return slots.default && slots.default();
}
function Title() {
if (props.title) {
return (
<h3
class={bem('title')}
style={{ width: addUnit(props.titleWidth) }}
/>
);
}
}
function Rows() {
const Rows = [];
const { rowWidth } = props;
function getRowWidth(index) {
if (rowWidth === DEFAULT_ROW_WIDTH && index === +props.row - 1) {
return DEFAULT_LAST_ROW_WIDTH;
}
if (Array.isArray(rowWidth)) {
return rowWidth[index];
}
return rowWidth;
}
for (let i = 0; i < props.row; i++) {
Rows.push(
<div
class={bem('row')}
style={{ width: addUnit(getRowWidth(i)) }}
/>
);
}
return Rows;
}
function Avatar() {
if (props.avatar) {
const size = addUnit(props.avatarSize);
return (
<div
class={bem('avatar', props.avatarShape)}
style={{ width: size, height: size }}
/>
);
}
}
return (
<div class={bem({ animate: props.animate, round: props.round })}>
{Avatar()}
<div class={bem('content')}>
{Title()}
{Rows()}
</div>
</div>
);
};
},
});
-129
View File
@@ -1,129 +0,0 @@
// Utils
import { createNamespace, addUnit } from '../utils';
import { inherit } from '../utils/functional';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type SkeletonProps = {
row: number | string;
title?: boolean;
round?: boolean;
avatar?: boolean;
loading: boolean;
animate: boolean;
avatarSize: string;
avatarShape: 'square' | 'round';
titleWidth: number | string;
rowWidth: number | string | (number | string)[];
};
const [createComponent, bem] = createNamespace('skeleton');
const DEFAULT_ROW_WIDTH = '100%';
const DEFAULT_LAST_ROW_WIDTH = '60%';
function Skeleton(
h: CreateElement,
props: SkeletonProps,
slots: DefaultSlots,
ctx: RenderContext
) {
if (!props.loading) {
return slots.default && slots.default();
}
function Title() {
if (props.title) {
return (
<h3 class={bem('title')} style={{ width: addUnit(props.titleWidth) }} />
);
}
}
function Rows() {
const Rows = [];
const { rowWidth } = props;
function getRowWidth(index: number) {
if (rowWidth === DEFAULT_ROW_WIDTH && index === +props.row - 1) {
return DEFAULT_LAST_ROW_WIDTH;
}
if (Array.isArray(rowWidth)) {
return rowWidth[index];
}
return rowWidth;
}
for (let i = 0; i < props.row; i++) {
Rows.push(
<div class={bem('row')} style={{ width: addUnit(getRowWidth(i)) }} />
);
}
return Rows;
}
function Avatar() {
if (props.avatar) {
const size = addUnit(props.avatarSize);
return (
<div
class={bem('avatar', props.avatarShape)}
style={{ width: size, height: size }}
/>
);
}
}
return (
<div
class={bem({ animate: props.animate, round: props.round })}
{...inherit(ctx)}
>
{Avatar()}
<div class={bem('content')}>
{Title()}
{Rows()}
</div>
</div>
);
}
Skeleton.props = {
title: Boolean,
round: Boolean,
avatar: Boolean,
row: {
type: [Number, String],
default: 0,
},
loading: {
type: Boolean,
default: true,
},
animate: {
type: Boolean,
default: true,
},
avatarSize: {
type: String,
default: '32px',
},
avatarShape: {
type: String,
default: 'round',
},
titleWidth: {
type: [Number, String],
default: '40%',
},
rowWidth: {
type: [Number, String, Array],
default: DEFAULT_ROW_WIDTH,
},
};
export default createComponent<SkeletonProps>(Skeleton);
+13 -9
View File
@@ -27,12 +27,14 @@ export default createComponent({
type: [Number, String],
default: 1,
},
value: {
modelValue: {
type: Number,
default: 0,
},
},
emits: ['change', 'drag-end', 'drag-start', 'update:modelValue'],
data() {
return {
dragStatus: '',
@@ -57,7 +59,7 @@ export default createComponent({
created() {
// format initial value
this.updateValue(this.value);
this.updateValue(this.modelValue);
},
mounted() {
@@ -71,7 +73,7 @@ export default createComponent({
}
this.touchStart(event);
this.startValue = this.format(this.value);
this.startValue = this.format(this.modelValue);
this.dragStatus = 'start';
},
@@ -122,15 +124,15 @@ export default createComponent({
const total = this.vertical ? rect.height : rect.width;
const value = +this.min + (delta / total) * this.range;
this.startValue = this.value;
this.startValue = this.modelValue;
this.updateValue(value, true);
},
updateValue(value, end) {
value = this.format(value);
if (value !== this.value) {
this.$emit('input', value);
if (value !== this.modelValue) {
this.$emit('update:modelValue', value);
}
if (end && value !== this.startValue) {
@@ -157,7 +159,7 @@ export default createComponent({
};
const barStyle = {
[mainAxis]: `${((this.value - this.min) * 100) / this.range}%`,
[mainAxis]: `${((this.modelValue - this.min) * 100) / this.range}%`,
background: this.activeColor,
};
@@ -177,12 +179,14 @@ export default createComponent({
role="slider"
tabindex={this.disabled ? -1 : 0}
aria-valuemin={this.min}
aria-valuenow={this.value}
aria-valuenow={this.modelValue}
aria-valuemax={this.max}
aria-orientation={this.vertical ? 'vertical' : 'horizontal'}
class={bem('button-wrapper')}
>
{this.slots('button') || (
{this.$slots.button ? (
this.$slots.button()
) : (
<div class={bem('button')} style={this.buttonStyle} />
)}
</div>
+14 -14
View File
@@ -44,22 +44,22 @@ export default createComponent({
const { activeIcon, activeColor, inactiveIcon } = this.parent;
if (this.active) {
return (
this.slots('active-icon') || (
<Icon
class={bem('icon', 'active')}
name={activeIcon}
color={activeColor}
/>
)
return this.$slots['active-icon'] ? (
this.$slots['active-icon']()
) : (
<Icon
class={bem('icon', 'active')}
name={activeIcon}
color={activeColor}
/>
);
}
const inactiveIconSlot = this.slots('inactive-icon');
if (inactiveIcon || inactiveIconSlot) {
return (
inactiveIconSlot || <Icon class={bem('icon')} name={inactiveIcon} />
if (inactiveIcon || this.$slots['inactive-icon']) {
return this.$slots['inactive-icon'] ? (
this.$slots['inactive-icon']()
) : (
<Icon class={bem('icon')} name={inactiveIcon} />
);
}
@@ -82,7 +82,7 @@ export default createComponent({
style={this.titleStyle}
onClick={this.onClickStep}
>
{this.slots()}
{this.$slots.default?.()}
</div>
<div class={bem('circle-container')} onClick={this.onClickStep}>
{this.genCircle()}
+3 -1
View File
@@ -24,10 +24,12 @@ export default createComponent({
},
},
emits: ['click-step'],
render() {
return (
<div class={bem([this.direction])}>
<div class={bem('items')}>{this.slots()}</div>
<div class={bem('items')}>{this.$slots.default?.()}</div>
</div>
);
},
+3 -3
View File
@@ -195,7 +195,7 @@ export default createComponent({
},
genLeftPart() {
const content = this.slots('left');
const content = this.$slots.left?.();
if (content) {
return (
@@ -211,7 +211,7 @@ export default createComponent({
},
genRightPart() {
const content = this.slots('right');
const content = this.$slots.right?.();
if (content) {
return (
@@ -237,7 +237,7 @@ export default createComponent({
<div class={bem()} onClick={this.getClickHandler('cell')}>
<div class={bem('wrapper')} style={wrapperStyle}>
{this.genLeftPart()}
{this.slots()}
{this.$slots.default?.()}
{this.genRightPart()}
</div>
</div>
+2 -2
View File
@@ -54,7 +54,7 @@ export default {
### Async Control
```html
<van-switch :value="checked" @input="onInput" />
<van-switch :model-value="checked" @update:model:value="onUpdateValue" />
```
```js
@@ -65,7 +65,7 @@ export default {
};
},
methods: {
onInput(checked) {
onUpdateValue(checked) {
Dialog.confirm({
title: 'Confirm',
message: 'Are you sure to toggle switch?',
+3 -3
View File
@@ -63,10 +63,10 @@ export default {
### 异步控制
需要异步控制开关时,可以使用`value`属性和`input`事件代替`v-model`,并在`input`事件回调函数中手动处理开关状态
需要异步控制开关时,可以使用 `modelValue` 属性和 `update:model-value` 事件代替 `v-model`,并在事件回调函数中手动处理开关状态
```html
<van-switch :value="checked" @input="onInput" />
<van-switch :model-value="checked" @update:model:value="onUpdateValue" />
```
```js
@@ -77,7 +77,7 @@ export default {
};
},
methods: {
onInput(checked) {
onUpdateValue(checked) {
Dialog.confirm({
title: '提醒',
message: '是否切换开关?',
+4 -2
View File
@@ -25,12 +25,14 @@
</demo-block>
<demo-block :title="t('asyncControl')">
<van-switch :value="checked4" @input="onInput" />
<van-switch :model-value="checked4" @update:model-value="onInput" />
</demo-block>
<demo-block :title="t('withCell')">
<van-cell center :title="t('title')">
<van-switch v-model="checked5" slot="right-icon" size="24" />
<template #right-icon>
<van-switch v-model="checked5" size="24" />
</template>
</van-cell>
</demo-block>
</demo-section>
+4 -2
View File
@@ -15,9 +15,11 @@ export default createComponent({
props: switchProps,
emits: ['click', 'change', 'update:modelValue'],
computed: {
checked() {
return this.value === this.activeValue;
return this.modelValue === this.activeValue;
},
style() {
@@ -34,7 +36,7 @@ export default createComponent({
if (!this.disabled && !this.loading) {
const newValue = this.checked ? this.inactiveValue : this.activeValue;
this.$emit('input', newValue);
this.$emit('update:modelValue', newValue);
this.$emit('change', newValue);
}
},
+2 -2
View File
@@ -4,9 +4,9 @@
export type SharedSwitchProps = {
size?: string | number;
value?: any;
loading?: boolean;
disabled?: boolean;
modelValue?: any;
activeValue: any;
inactiveValue: any;
activeColor?: string;
@@ -15,9 +15,9 @@ export type SharedSwitchProps = {
export const switchProps = {
size: [Number, String],
value: null as any,
loading: Boolean,
disabled: Boolean,
modelValue: null as any,
activeColor: String,
inactiveColor: String,
activeValue: {
+68
View File
@@ -0,0 +1,68 @@
// Utils
import { createNamespace } from '../utils';
import { BORDER_SURROUND } from '../utils/constant';
// Components
import Icon from '../icon';
const [createComponent, bem] = createNamespace('tag');
export default createComponent({
props: {
size: String,
mark: Boolean,
color: String,
plain: Boolean,
round: Boolean,
textColor: String,
closeable: Boolean,
type: {
type: String,
default: 'default',
},
},
emits: ['close'],
setup(props, { slots, emit }) {
return function () {
const { type, mark, plain, color, round, size } = props;
const key = plain ? 'color' : 'backgroundColor';
const style = { [key]: color };
if (props.textColor) {
style.color = props.textColor;
}
const classes = { mark, plain, round };
if (size) {
classes[size] = size;
}
const CloseIcon = props.closeable && (
<Icon
name="cross"
class={bem('close')}
onClick={(event) => {
event.stopPropagation();
emit('close');
}}
/>
);
return (
<transition name={props.closeable ? 'van-fade' : null}>
<span
key="content"
style={style}
class={[bem([classes, type]), { [BORDER_SURROUND]: plain }]}
>
{slots.default?.()}
{CloseIcon}
</span>
</transition>
);
};
},
});
-90
View File
@@ -1,90 +0,0 @@
// Utils
import { createNamespace } from '../utils';
import { inherit, emit } from '../utils/functional';
import { BORDER_SURROUND } from '../utils/constant';
// Components
import Icon from '../icon';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots } from '../utils/types';
export type TagType = 'default' | 'primary' | 'success' | 'danger';
export type TagSize = 'large' | 'medium';
export type TagProps = {
type: TagType;
size?: TagSize;
mark?: boolean;
color?: string;
plain?: boolean;
round?: boolean;
textColor?: string;
closeable?: boolean;
};
const [createComponent, bem] = createNamespace('tag');
function Tag(
h: CreateElement,
props: TagProps,
slots: DefaultSlots,
ctx: RenderContext<TagProps>
) {
const { type, mark, plain, color, round, size } = props;
const key = plain ? 'color' : 'backgroundColor';
const style = { [key]: color };
if (props.textColor) {
style.color = props.textColor;
}
const classes: { [key: string]: any } = { mark, plain, round };
if (size) {
classes[size] = size;
}
const CloseIcon = props.closeable && (
<Icon
name="cross"
class={bem('close')}
onClick={(event: PointerEvent) => {
event.stopPropagation();
emit(ctx, 'close');
}}
/>
);
return (
<transition name={props.closeable ? 'van-fade' : null}>
<span
key="content"
style={style}
class={[bem([classes, type]), { [BORDER_SURROUND]: plain }]}
{...inherit(ctx, true)}
>
{slots.default?.()}
{CloseIcon}
</span>
</transition>
);
}
Tag.props = {
size: String,
mark: Boolean,
color: String,
plain: Boolean,
round: Boolean,
textColor: String,
closeable: Boolean,
type: {
type: String,
default: 'default',
},
};
export default createComponent<TagProps>(Tag);
+6 -6
View File
@@ -15,9 +15,9 @@ Vue.use(TreeSelect);
```html
<van-tree-select
v-model:active-id="activeId"
v-model:main-active-index="activeIndex"
:items="items"
:active-id.sync="activeId"
:main-active-index.sync="activeIndex"
/>
```
@@ -37,9 +37,9 @@ export default {
```html
<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="items"
:active-id.sync="activeIds"
:main-active-index.sync="activeIndex"
/>
```
@@ -58,7 +58,7 @@ export default {
### Custom Content
```html
<van-tree-select height="55vw" :items="items" :main-active-index.sync="active">
<van-tree-select v-model:main-active-index="active" height="55vw" :items="items">
<template #content>
<van-image
v-if="active === 0"
@@ -87,9 +87,9 @@ export default {
```html
<van-tree-select
v-model:main-active-index="activeIndex"
height="55vw"
:items="items"
:main-active-index.sync="activeIndex"
/>
```
+10 -6
View File
@@ -17,9 +17,9 @@ Vue.use(TreeSelect);
```html
<van-tree-select
v-model:active-id="activeId"
v-model:main-active-index="activeIndex"
:items="items"
:active-id.sync="activeId"
:main-active-index.sync="activeIndex"
/>
```
@@ -41,9 +41,9 @@ export default {
```html
<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex"
:items="items"
:active-id.sync="activeIds"
:main-active-index.sync="activeIndex"
/>
```
@@ -64,7 +64,11 @@ export default {
通过`content`插槽可以自定义右侧区域的内容
```html
<van-tree-select height="55vw" :items="items" :main-active-index.sync="active">
<van-tree-select
v-model:main-active-index="active"
height="55vw"
:items="items"
>
<template #content>
<van-image
v-if="active === 0"
@@ -95,9 +99,9 @@ export default {
```html
<van-tree-select
v-model:main-active-index="activeIndex"
height="55vw"
:items="items"
:main-active-index.sync="activeIndex"
/>
```
+7 -7
View File
@@ -2,25 +2,25 @@
<demo-section>
<demo-block :title="t('radioMode')">
<van-tree-select
v-model:active-id="activeId"
v-model:main-active-index="activeIndex"
:items="items"
:active-id.sync="activeId"
:main-active-index.sync="activeIndex"
/>
</demo-block>
<demo-block :title="t('multipleMode')">
<van-tree-select
v-model:active-id="activeIds"
v-model:main-active-index="activeIndex2"
:items="items"
:active-id.sync="activeIds"
:main-active-index.sync="activeIndex2"
/>
</demo-block>
<demo-block :title="t('customContent')">
<van-tree-select
v-model:main-active-index="activeIndex3"
height="55vw"
:items="simpleItems"
:main-active-index.sync="activeIndex3"
>
<template slot="content">
<van-image
@@ -39,10 +39,10 @@
<demo-block :title="t('showBadge')">
<van-tree-select
v-model:active-id="activeId2"
v-model:main-active-index="activeIndex4"
height="55vw"
:items="badgeItems"
:active-id.sync="activeId2"
:main-active-index.sync="activeIndex4"
/>
</demo-block>
</demo-section>
+128
View File
@@ -0,0 +1,128 @@
// Utils
import { createNamespace, addUnit, isDef } from '../utils';
// Components
import Icon from '../icon';
import Sidebar from '../sidebar';
import SidebarItem from '../sidebar-item';
const [createComponent, bem] = createNamespace('tree-select');
export default createComponent({
props: {
max: {
type: [Number, String],
default: Infinity,
},
items: {
type: Array,
default: () => [],
},
height: {
type: [Number, String],
default: 300,
},
activeId: {
type: [Number, String, Array],
default: 0,
},
selectedIcon: {
type: String,
default: 'success',
},
mainActiveIndex: {
type: [Number, String],
default: 0,
},
},
emits: [
'click-nav',
'click-item',
'update:activeId',
'update:mainActiveIndex',
],
setup(props, { emit, slots }) {
return function () {
const { items, height, activeId, selectedIcon, mainActiveIndex } = props;
const selectedItem = items[+mainActiveIndex] || {};
const subItems = selectedItem.children || [];
const isMultiple = Array.isArray(activeId);
function isActiveItem(id) {
return isMultiple ? activeId.indexOf(id) !== -1 : activeId === id;
}
const Navs = items.map((item) => (
<SidebarItem
dot={item.dot}
info={isDef(item.badge) ? item.badge : item.info}
title={item.text}
disabled={item.disabled}
class={[bem('nav-item'), item.className]}
/>
));
function Content() {
if (slots.content) {
return slots.content();
}
return subItems.map((item) => (
<div
key={item.id}
class={[
'van-ellipsis',
bem('item', {
active: isActiveItem(item.id),
disabled: item.disabled,
}),
]}
onClick={() => {
if (!item.disabled) {
let newActiveId = item.id;
if (isMultiple) {
newActiveId = activeId.slice();
const index = newActiveId.indexOf(item.id);
if (index !== -1) {
newActiveId.splice(index, 1);
} else if (newActiveId.length < props.max) {
newActiveId.push(item.id);
}
}
emit('update:activeId', newActiveId);
emit('click-item', item);
}
}}
>
{item.text}
{isActiveItem(item.id) && (
<Icon name={selectedIcon} class={bem('selected')} />
)}
</div>
));
}
return (
<div class={bem()} style={{ height: addUnit(height) }}>
<Sidebar
class={bem('nav')}
modelValue={mainActiveIndex}
onChange={(index) => {
emit('update:mainActiveIndex', index);
emit('click-nav', index);
}}
>
{Navs}
</Sidebar>
<div class={bem('content')}>{Content()}</div>
</div>
);
};
},
});
-166
View File
@@ -1,166 +0,0 @@
// Utils
import { createNamespace, addUnit, isDef } from '../utils';
import { emit, inherit } from '../utils/functional';
// Components
import Icon from '../icon';
import Sidebar from '../sidebar';
import SidebarItem from '../sidebar-item';
// Types
import { CreateElement, RenderContext } from 'vue/types';
import { DefaultSlots, ScopedSlot } from '../utils/types';
export type TreeSelectItem = {
text: string;
dot?: boolean;
info?: string | number;
badge?: string | number;
disabled?: boolean;
className?: any;
children: TreeSelectChildren[];
};
export type TreeSelectChildren = {
id: number;
text: string;
disabled?: boolean;
};
export type TreeSelectActiveId = number | string | (number | string)[];
export type TreeSelectProps = {
max: number | string;
height: number | string;
items: TreeSelectItem[];
activeId: TreeSelectActiveId;
selectedIcon: string;
mainActiveIndex: number | string;
};
export type TreeSelectSlots = DefaultSlots & {
content?: ScopedSlot;
};
const [createComponent, bem] = createNamespace('tree-select');
function TreeSelect(
h: CreateElement,
props: TreeSelectProps,
slots: TreeSelectSlots,
ctx: RenderContext<TreeSelectProps>
) {
const { items, height, activeId, selectedIcon, mainActiveIndex } = props;
const selectedItem: Partial<TreeSelectItem> = items[+mainActiveIndex] || {};
const subItems = selectedItem.children || [];
const isMultiple = Array.isArray(activeId);
function isActiveItem(id: number | string) {
return isMultiple
? (activeId as (number | string)[]).indexOf(id) !== -1
: activeId === id;
}
const Navs = items.map((item) => (
<SidebarItem
dot={item.dot}
info={isDef(item.badge) ? item.badge : item.info}
title={item.text}
disabled={item.disabled}
class={[bem('nav-item'), item.className]}
/>
));
function Content() {
if (slots.content) {
return slots.content();
}
return subItems.map((item) => (
<div
key={item.id}
class={[
'van-ellipsis',
bem('item', {
active: isActiveItem(item.id),
disabled: item.disabled,
}),
]}
onClick={() => {
if (!item.disabled) {
let newActiveId: TreeSelectActiveId = item.id;
if (isMultiple) {
newActiveId = (activeId as (string | number)[]).slice();
const index = newActiveId.indexOf(item.id);
if (index !== -1) {
newActiveId.splice(index, 1);
} else if (newActiveId.length < props.max) {
newActiveId.push(item.id);
}
}
emit(ctx, 'update:active-id', newActiveId);
emit(ctx, 'click-item', item);
// compatible with legacy usage, should be removed in next major version
emit(ctx, 'itemclick', item);
}
}}
>
{item.text}
{isActiveItem(item.id) && (
<Icon name={selectedIcon} class={bem('selected')} />
)}
</div>
));
}
return (
<div class={bem()} style={{ height: addUnit(height) }} {...inherit(ctx)}>
<Sidebar
class={bem('nav')}
activeKey={mainActiveIndex}
onChange={(index: number) => {
emit(ctx, 'update:main-active-index', index);
emit(ctx, 'click-nav', index);
// compatible with legacy usage, should be removed in next major version
emit(ctx, 'navclick', index);
}}
>
{Navs}
</Sidebar>
<div class={bem('content')}>{Content()}</div>
</div>
);
}
TreeSelect.props = {
max: {
type: [Number, String],
default: Infinity,
},
items: {
type: Array,
default: () => [],
},
height: {
type: [Number, String],
default: 300,
},
activeId: {
type: [Number, String, Array],
default: 0,
},
selectedIcon: {
type: String,
default: 'success',
},
mainActiveIndex: {
type: [Number, String],
default: 0,
},
};
export default createComponent<TreeSelectProps>(TreeSelect);
+8 -76
View File
@@ -1,86 +1,18 @@
/**
* Create a basic component with common options
*/
import '../../locale';
import { isFunction } from '..';
import { camelize } from '../format/string';
import { SlotsMixin } from '../../mixins/slots';
import Vue, {
VNode,
VueConstructor,
ComponentOptions,
RenderContext,
} from 'vue';
import { DefaultProps, FunctionComponent } from '../types';
export interface VantComponentOptions extends ComponentOptions<Vue> {
functional?: boolean;
install?: (Vue: VueConstructor) => void;
}
export type TsxBaseProps<Slots> = {
key: string | number;
// hack for jsx prop spread
props: any;
class: any;
style: string | object[] | object;
scopedSlots: Slots;
};
export type TsxComponent<Props, Events, Slots> = (
props: Partial<Props & Events & TsxBaseProps<Slots>>
) => VNode;
function install(this: ComponentOptions<Vue>, Vue: VueConstructor) {
const { name } = this;
Vue.component(name as string, this);
Vue.component(camelize(`-${name}`), this);
}
// unify slots & scopedSlots
export function unifySlots(context: RenderContext) {
// use data.scopedSlots in lower Vue version
const scopedSlots = context.scopedSlots || context.data.scopedSlots || {};
const slots = context.slots();
Object.keys(slots).forEach((key) => {
if (!scopedSlots[key]) {
scopedSlots[key] = () => slots[key];
}
});
return scopedSlots;
}
// should be removed after Vue 3
function transformFunctionComponent(
pure: FunctionComponent
): VantComponentOptions {
return {
functional: true,
props: pure.props,
model: pure.model,
render: (h, context): any =>
pure(h, context.props, unifySlots(context), context),
};
}
// function install(this: any, Vue: VueConstructor) {
// const { name } = this;
// Vue.component(name as string, this);
// Vue.component(camelize(`-${name}`), this);
// }
export function createComponent(name: string) {
return function <Props = DefaultProps, Events = {}, Slots = {}>(
sfc: VantComponentOptions | FunctionComponent
): TsxComponent<Props, Events, Slots> {
if (isFunction(sfc)) {
sfc = transformFunctionComponent(sfc);
}
if (!sfc.functional) {
sfc.mixins = sfc.mixins || [];
sfc.mixins.push(SlotsMixin);
}
return function (sfc: any) {
sfc.name = name;
sfc.install = install;
// sfc.install = install;
return sfc as TsxComponent<Props, Events, Slots>;
return sfc;
};
}
+5 -1
View File
@@ -10,5 +10,9 @@ type CreateNamespaceReturn = [
export function createNamespace(name: string): CreateNamespaceReturn {
name = 'van-' + name;
return [createComponent(name), createBEM(name), createI18N(name)];
return [
createComponent(name),
createBEM(name),
createI18N(name)
];
}
+4 -4
View File
@@ -1,10 +1,10 @@
import { isServer } from '..';
import { inBrowser } from '..';
import { EventHandler } from '../types';
// eslint-disable-next-line import/no-mutable-exports
export let supportsPassive = false;
if (!isServer) {
if (inBrowser) {
try {
const opts = {};
Object.defineProperty(opts, 'passive', {
@@ -25,7 +25,7 @@ export function on(
handler: EventHandler,
passive = false
) {
if (!isServer) {
if (inBrowser) {
target.addEventListener(
event,
handler,
@@ -35,7 +35,7 @@ export function on(
}
export function off(target: EventTarget, event: string, handler: EventHandler) {
if (!isServer) {
if (inBrowser) {
target.removeEventListener(event, handler);
}
}
+2 -2
View File
@@ -2,7 +2,7 @@
* requestAnimationFrame polyfill
*/
import { isServer } from '..';
import { inBrowser } from '..';
let prev = Date.now();
@@ -16,7 +16,7 @@ function fallback(fn: FrameRequestCallback): number {
}
/* istanbul ignore next */
const root = (isServer ? global : window) as Window;
const root = (inBrowser ? window : global) as Window;
/* istanbul ignore next */
const iRaf = root.requestAnimationFrame || fallback;
-71
View File
@@ -1,71 +0,0 @@
import Vue, { RenderContext, VNodeData } from 'vue';
import { ObjectIndex } from './types';
type Context = RenderContext & { data: VNodeData & ObjectIndex };
type InheritContext = Partial<VNodeData> & ObjectIndex;
const inheritKey = [
'ref',
'style',
'class',
'attrs',
'nativeOn',
'directives',
'staticClass',
'staticStyle',
];
const mapInheritKey: ObjectIndex = { nativeOn: 'on' };
// inherit partial context, map nativeOn to on
export function inherit(
context: Context,
inheritListeners?: boolean
): InheritContext {
const result = inheritKey.reduce((obj, key) => {
if (context.data[key]) {
obj[mapInheritKey[key] || key] = context.data[key];
}
return obj;
}, {} as InheritContext);
if (inheritListeners) {
result.on = result.on || {};
Object.assign(result.on, context.data.on);
}
return result;
}
// emit event
export function emit(context: Context, eventName: string, ...args: any[]) {
const listeners = context.listeners[eventName];
if (listeners) {
if (Array.isArray(listeners)) {
listeners.forEach((listener) => {
listener(...args);
});
} else {
listeners(...args);
}
}
}
// mount functional component
export function mount(Component: any, data?: VNodeData) {
const instance = new Vue({
el: document.createElement('div'),
props: Component.props,
render(h) {
return h(Component, {
props: this.$props,
...data,
});
},
});
document.body.appendChild(instance.$el);
return instance;
}
+3 -6
View File
@@ -1,14 +1,11 @@
import Vue from 'vue';
export { createNamespace } from './create';
export { addUnit } from './format/unit';
export const inBrowser = typeof window !== 'undefined';
export const isServer: boolean = Vue.prototype.$isServer;
export { createNamespace } from './create';
// eslint-disable-next-line @typescript-eslint/no-empty-function
export function noop() {}
export const inBrowser = typeof window !== 'undefined'
export function isDef(val: unknown): boolean {
return val !== undefined && val !== null;
}
+4 -9
View File
@@ -2,12 +2,11 @@
* Vue Router support
*/
import { RenderContext } from 'vue/types';
import VueRouter, { RawLocation } from 'vue-router/types';
import type { Router, RouteLocation } from 'vue-router';
export type RouteConfig = {
url?: string;
to?: RawLocation;
to?: RouteLocation;
replace?: boolean;
};
@@ -19,7 +18,7 @@ function isRedundantNavigation(err: Error) {
);
}
export function route(router: VueRouter, config: RouteConfig) {
export function route(router: Router, config: RouteConfig) {
const { to, url, replace } = config;
if (to && router) {
const promise = router[replace ? 'replace' : 'push'](to);
@@ -37,14 +36,10 @@ export function route(router: VueRouter, config: RouteConfig) {
}
}
export function functionalRoute(context: RenderContext) {
route(context.parent && context.parent.$router, context.props);
}
export type RouteProps = {
url?: string;
replace?: boolean;
to?: RawLocation;
to?: RouteLocation;
};
export const routeProps = {
+5 -5
View File
@@ -1,13 +1,13 @@
import { isServer } from '..';
import { inBrowser } from '..';
export function isAndroid(): boolean {
/* istanbul ignore next */
return isServer ? false : /android/.test(navigator.userAgent.toLowerCase());
return inBrowser ? /android/.test(navigator.userAgent.toLowerCase()) : false;
}
export function isIOS(): boolean {
/* istanbul ignore next */
return isServer
? false
: /ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase());
return inBrowser
? /ios|iphone|ipad|ipod/.test(navigator.userAgent.toLowerCase())
: false;
}
-33
View File
@@ -1,33 +0,0 @@
import { VNode } from 'vue';
function flattenVNodes(vnodes: VNode[]) {
const result: VNode[] = [];
function traverse(vnodes: VNode[]) {
vnodes.forEach((vnode) => {
result.push(vnode);
if (vnode.componentInstance) {
traverse(vnode.componentInstance.$children.map((item) => item.$vnode));
}
if (vnode.children) {
traverse(vnode.children);
}
});
}
traverse(vnodes);
return result;
}
// sort children instances by vnodes order
export function sortChildren(children: Vue[], parent: Vue) {
const { componentOptions } = parent.$vnode;
if (!componentOptions || !componentOptions.children) {
return;
}
const vnodes = flattenVNodes(componentOptions.children);
children.sort((a, b) => vnodes.indexOf(a.$vnode) - vnodes.indexOf(b.$vnode));
}