mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
refactor(DropdownItem): refactor with composition api
This commit is contained in:
parent
60eef853db
commit
cd5f5bb655
@ -1,11 +1,12 @@
|
|||||||
import { Teleport } from 'vue';
|
import { reactive, Teleport } from 'vue';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { createNamespace } from '../utils';
|
import { createNamespace } from '../utils';
|
||||||
import { on, off } from '../utils/dom/event';
|
import { DROPDOWN_KEY } from '../dropdown-menu';
|
||||||
|
|
||||||
// Mixins
|
// Composition
|
||||||
import { ChildrenMixin } from '../mixins/relation';
|
import { useParent } from '../composition/use-relation';
|
||||||
|
import { usePublicApi } from '../composition/use-public-api';
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import Cell from '../cell';
|
import Cell from '../cell';
|
||||||
@ -15,8 +16,6 @@ import Popup from '../popup';
|
|||||||
const [createComponent, bem] = createNamespace('dropdown-item');
|
const [createComponent, bem] = createNamespace('dropdown-item');
|
||||||
|
|
||||||
export default createComponent({
|
export default createComponent({
|
||||||
mixins: [ChildrenMixin('vanDropdownMenu')],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
title: String,
|
title: String,
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
@ -35,93 +34,79 @@ export default createComponent({
|
|||||||
|
|
||||||
emits: ['open', 'opened', 'close', 'closed', 'change', 'update:modelValue'],
|
emits: ['open', 'opened', 'close', 'closed', 'change', 'update:modelValue'],
|
||||||
|
|
||||||
data() {
|
setup(props, { emit, slots }) {
|
||||||
return {
|
const state = reactive({
|
||||||
transition: true,
|
|
||||||
showPopup: false,
|
showPopup: false,
|
||||||
|
transition: true,
|
||||||
showWrapper: false,
|
showWrapper: false,
|
||||||
};
|
});
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
const renderTitle = () => {
|
||||||
displayTitle() {
|
if (slots.title) {
|
||||||
if (this.title) {
|
return slots.title();
|
||||||
return this.title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = this.options.filter(
|
if (props.title) {
|
||||||
(option) => option.value === this.modelValue
|
return props.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = props.options.filter(
|
||||||
|
(option) => option.value === props.modelValue
|
||||||
);
|
);
|
||||||
|
|
||||||
return match.length ? match[0].text : '';
|
return match.length ? match[0].text : '';
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
const toggle = (show = !state.showPopup, options = {}) => {
|
||||||
showPopup(val) {
|
if (show === state.showPopup) {
|
||||||
this.bindScroll(val);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeCreate() {
|
|
||||||
const createEmitter = (eventName) => () => this.$emit(eventName);
|
|
||||||
|
|
||||||
this.onOpen = createEmitter('open');
|
|
||||||
this.onClose = createEmitter('close');
|
|
||||||
this.onOpened = createEmitter('opened');
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
// @exposed-api
|
|
||||||
toggle(show = !this.showPopup, options = {}) {
|
|
||||||
if (show === this.showPopup) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transition = !options.immediate;
|
state.showPopup = show;
|
||||||
this.showPopup = show;
|
state.transition = !options.immediate;
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
this.parent.updateOffset();
|
state.showWrapper = true;
|
||||||
this.showWrapper = true;
|
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
bindScroll(bind) {
|
const { parent } = useParent(DROPDOWN_KEY, {
|
||||||
const { scroller } = this.parent;
|
props,
|
||||||
const action = bind ? on : off;
|
state,
|
||||||
action(scroller, 'scroll', this.onScroll, true);
|
toggle,
|
||||||
},
|
renderTitle,
|
||||||
|
});
|
||||||
|
|
||||||
onScroll() {
|
const createEmitter = (eventName) => () => emit(eventName);
|
||||||
this.parent.updateOffset();
|
const onOpen = createEmitter('open');
|
||||||
},
|
const onClose = createEmitter('close');
|
||||||
|
const onOpened = createEmitter('opened');
|
||||||
|
|
||||||
onClosed() {
|
const onClosed = () => {
|
||||||
this.showWrapper = false;
|
state.showWrapper = false;
|
||||||
this.$emit('closed');
|
emit('closed');
|
||||||
},
|
};
|
||||||
|
|
||||||
onClickWrapper(event) {
|
const onClickWrapper = (event) => {
|
||||||
// prevent being identified as clicking outside and closed when using teleport
|
// prevent being identified as clicking outside and closed when using teleport
|
||||||
if (this.teleport) {
|
if (props.teleport) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
const renderOption = (option) => {
|
||||||
const {
|
const { activeColor } = parent.props;
|
||||||
zIndex,
|
const active = option.value === props.modelValue;
|
||||||
offset,
|
|
||||||
overlay,
|
const onClick = () => {
|
||||||
duration,
|
state.showPopup = false;
|
||||||
direction,
|
|
||||||
activeColor,
|
if (option.value !== props.modelValue) {
|
||||||
closeOnClickOverlay,
|
emit('update:modelValue', option.value);
|
||||||
} = this.parent;
|
emit('change', option.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Options = this.options.map((option) => {
|
|
||||||
const active = option.value === this.modelValue;
|
|
||||||
return (
|
return (
|
||||||
<Cell
|
<Cell
|
||||||
clickable
|
clickable
|
||||||
@ -130,61 +115,67 @@ export default createComponent({
|
|||||||
title={option.text}
|
title={option.text}
|
||||||
class={bem('option', { active })}
|
class={bem('option', { active })}
|
||||||
style={{ color: active ? activeColor : '' }}
|
style={{ color: active ? activeColor : '' }}
|
||||||
onClick={() => {
|
onClick={onClick}
|
||||||
this.showPopup = false;
|
|
||||||
|
|
||||||
if (option.value !== this.modelValue) {
|
|
||||||
this.$emit('update:modelValue', option.value);
|
|
||||||
this.$emit('change', option.value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{active && (
|
{active && (
|
||||||
<Icon class={bem('icon')} color={activeColor} name="success" />
|
<Icon class={bem('icon')} color={activeColor} name="success" />
|
||||||
)}
|
)}
|
||||||
</Cell>
|
</Cell>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
const style = { zIndex };
|
const renderContent = () => {
|
||||||
if (direction === 'down') {
|
const { offset } = parent;
|
||||||
style.top = `${offset}px`;
|
const {
|
||||||
} else {
|
zIndex,
|
||||||
style.bottom = `${offset}px`;
|
overlay,
|
||||||
}
|
duration,
|
||||||
|
direction,
|
||||||
|
closeOnClickOverlay,
|
||||||
|
} = parent.props;
|
||||||
|
|
||||||
const Content = (
|
const style = { zIndex };
|
||||||
<div
|
if (direction === 'down') {
|
||||||
vShow={this.showWrapper}
|
style.top = `${offset.value}px`;
|
||||||
ref="wrapper"
|
} else {
|
||||||
style={style}
|
style.bottom = `${offset.value}px`;
|
||||||
class={bem([direction])}
|
}
|
||||||
onClick={this.onClickWrapper}
|
|
||||||
>
|
return (
|
||||||
<Popup
|
<div
|
||||||
vModel={[this.showPopup, 'show']}
|
vShow={state.showWrapper}
|
||||||
overlay={overlay}
|
style={style}
|
||||||
class={bem('content')}
|
class={bem([direction])}
|
||||||
position={direction === 'down' ? 'top' : 'bottom'}
|
onClick={onClickWrapper}
|
||||||
duration={this.transition ? duration : 0}
|
|
||||||
lazyRender={this.lazyRender}
|
|
||||||
overlayStyle={{ position: 'absolute' }}
|
|
||||||
closeOnClickOverlay={closeOnClickOverlay}
|
|
||||||
onOpen={this.onOpen}
|
|
||||||
onClose={this.onClose}
|
|
||||||
onOpened={this.onOpened}
|
|
||||||
onClosed={this.onClosed}
|
|
||||||
>
|
>
|
||||||
{Options}
|
<Popup
|
||||||
{this.$slots.default?.()}
|
vModel={[state.showPopup, 'show']}
|
||||||
</Popup>
|
class={bem('content')}
|
||||||
</div>
|
overlay={overlay}
|
||||||
);
|
position={direction === 'down' ? 'top' : 'bottom'}
|
||||||
|
duration={state.transition ? duration : 0}
|
||||||
|
lazyRender={props.lazyRender}
|
||||||
|
overlayStyle={{ position: 'absolute' }}
|
||||||
|
closeOnClickOverlay={closeOnClickOverlay}
|
||||||
|
onOpen={onOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
onOpened={onOpened}
|
||||||
|
onClosed={onClosed}
|
||||||
|
>
|
||||||
|
{props.options.map(renderOption)}
|
||||||
|
{slots.default?.()}
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (this.teleport) {
|
usePublicApi({ toggle });
|
||||||
return <Teleport to={this.teleport}>{Content}</Teleport>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Content;
|
return () => {
|
||||||
|
if (props.teleport) {
|
||||||
|
return <Teleport to={props.teleport}>{renderContent()}</Teleport>;
|
||||||
|
}
|
||||||
|
return renderContent();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
|
import { ref, provide, reactive, computed } from 'vue';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { createNamespace, isDef } from '../utils';
|
import { createNamespace, isDef } from '../utils';
|
||||||
import { getScroller } from '../utils/dom/scroll';
|
|
||||||
|
|
||||||
// Mixins
|
// Composition
|
||||||
import { ParentMixin } from '../mixins/relation';
|
import { useRect } from '../composition/use-rect';
|
||||||
import { ClickOutsideMixin } from '../mixins/click-outside';
|
import { useScroller } from '../composition/use-scroller';
|
||||||
|
import { useGlobalEvent } from '../composition/use-global-event';
|
||||||
|
import { useClickOutside } from '../composition/use-click-outside';
|
||||||
|
|
||||||
const [createComponent, bem] = createNamespace('dropdown-menu');
|
const [createComponent, bem] = createNamespace('dropdown-menu');
|
||||||
|
|
||||||
export default createComponent({
|
export const DROPDOWN_KEY = 'vanDropdownMenu';
|
||||||
mixins: [
|
|
||||||
ParentMixin('vanDropdownMenu'),
|
|
||||||
ClickOutsideMixin({
|
|
||||||
event: 'click',
|
|
||||||
method: 'onClickOutside',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
|
|
||||||
|
export default createComponent({
|
||||||
props: {
|
props: {
|
||||||
zIndex: [Number, String],
|
zIndex: [Number, String],
|
||||||
activeColor: String,
|
activeColor: String,
|
||||||
@ -38,101 +35,108 @@ export default createComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
setup(props, { slots }) {
|
||||||
return {
|
const offset = ref(0);
|
||||||
offset: 0,
|
const barRef = ref();
|
||||||
};
|
const rootRef = ref();
|
||||||
},
|
const children = reactive([]);
|
||||||
|
|
||||||
computed: {
|
const scroller = useScroller(rootRef);
|
||||||
scroller() {
|
|
||||||
return getScroller(this.$el);
|
|
||||||
},
|
|
||||||
|
|
||||||
opened() {
|
const opened = computed(() =>
|
||||||
return this.children.some((item) => item.showWrapper);
|
children.some((item) => item.state.showWrapper)
|
||||||
},
|
);
|
||||||
|
|
||||||
barStyle() {
|
const barStyle = computed(() => {
|
||||||
if (this.opened && isDef(this.zIndex)) {
|
if (opened.value && isDef(props.zIndex)) {
|
||||||
return {
|
return {
|
||||||
zIndex: 1 + this.zIndex,
|
zIndex: 1 + props.zIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const onClickOutside = () => {
|
||||||
updateOffset() {
|
children.forEach((item) => {
|
||||||
if (!this.$refs.bar) {
|
item.toggle(false);
|
||||||
return;
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOffset = () => {
|
||||||
|
if (barRef.value) {
|
||||||
|
const rect = useRect(barRef);
|
||||||
|
if (props.direction === 'down') {
|
||||||
|
offset.value = rect.bottom;
|
||||||
|
} else {
|
||||||
|
offset.value = window.innerHeight - rect.top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const rect = this.$refs.bar.getBoundingClientRect();
|
const toggleItem = (active) => {
|
||||||
|
children.forEach((item, index) => {
|
||||||
if (this.direction === 'down') {
|
|
||||||
this.offset = rect.bottom;
|
|
||||||
} else {
|
|
||||||
this.offset = window.innerHeight - rect.top;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleItem(active) {
|
|
||||||
this.children.forEach((item, index) => {
|
|
||||||
if (index === active) {
|
if (index === active) {
|
||||||
|
updateOffset();
|
||||||
item.toggle();
|
item.toggle();
|
||||||
} else if (item.showPopup) {
|
} else if (item.state.showPopup) {
|
||||||
item.toggle(false, { immediate: true });
|
item.toggle(false, { immediate: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
|
|
||||||
onClickOutside() {
|
const renderTitle = (item, index) => {
|
||||||
this.children.forEach((item) => {
|
const { showPopup } = item.state;
|
||||||
item.toggle(false);
|
const { disabled, titleClass } = item.props;
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const Titles = this.children.map((item, index) => (
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex={item.disabled ? -1 : 0}
|
|
||||||
class={bem('item', { disabled: item.disabled })}
|
|
||||||
onClick={() => {
|
|
||||||
if (!item.disabled) {
|
|
||||||
this.toggleItem(index);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class={[
|
|
||||||
bem('title', {
|
|
||||||
active: item.showPopup,
|
|
||||||
down: item.showPopup === (this.direction === 'down'),
|
|
||||||
}),
|
|
||||||
item.titleClass,
|
|
||||||
]}
|
|
||||||
style={{ color: item.showPopup ? this.activeColor : '' }}
|
|
||||||
>
|
|
||||||
<div class="van-ellipsis">
|
|
||||||
{item.$slots.title ? item.$slots.title() : item.displayTitle}
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={bem()}>
|
|
||||||
<div
|
<div
|
||||||
ref="bar"
|
role="button"
|
||||||
style={this.barStyle}
|
tabindex={disabled ? -1 : 0}
|
||||||
class={bem('bar', { opened: this.opened })}
|
class={bem('item', { disabled })}
|
||||||
|
onClick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
toggleItem(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{Titles}
|
<span
|
||||||
|
class={[
|
||||||
|
bem('title', {
|
||||||
|
down: showPopup === (props.direction === 'down'),
|
||||||
|
active: showPopup,
|
||||||
|
}),
|
||||||
|
titleClass,
|
||||||
|
]}
|
||||||
|
style={{ color: showPopup ? props.activeColor : '' }}
|
||||||
|
>
|
||||||
|
<div class="van-ellipsis">{item.renderTitle()}</div>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{this.$slots.default?.()}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
provide(DROPDOWN_KEY, { props, offset, children });
|
||||||
|
|
||||||
|
useClickOutside({
|
||||||
|
element: rootRef,
|
||||||
|
callback: onClickOutside,
|
||||||
|
});
|
||||||
|
|
||||||
|
useGlobalEvent(scroller, 'scroll', () => {
|
||||||
|
if (opened.value) {
|
||||||
|
updateOffset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => (
|
||||||
|
<div ref={rootRef} class={bem()}>
|
||||||
|
<div
|
||||||
|
ref={barRef}
|
||||||
|
style={barStyle.value}
|
||||||
|
class={bem('bar', { opened: opened.value })}
|
||||||
|
>
|
||||||
|
{children.map(renderTitle)}
|
||||||
|
</div>
|
||||||
|
{slots.default?.()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user