fix(ImagePreview): close event triggered twice (#5411)

This commit is contained in:
neverland 2019-12-28 08:43:57 +08:00 committed by GitHub
parent 93e6c81c49
commit 8dea26db95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 234 additions and 254 deletions

View File

@ -1,7 +1,7 @@
import { createNamespace } from '../utils';
import { emit, inherit } from '../utils/functional';
import { BORDER_TOP, BORDER_BOTTOM } from '../utils/constant';
import { PopupMixin } from '../mixins/popup';
import { popupMixinProps } from '../mixins/popup';
import Icon from '../icon';
import Popup from '../popup';
import Loading from '../loading';
@ -152,7 +152,7 @@ function ActionSheet(
}
ActionSheet.props = {
...PopupMixin.props,
...popupMixinProps,
title: String,
actions: Array,
duration: Number,

View File

@ -6,7 +6,7 @@ import Button from '../button';
const [createComponent, bem, t] = createNamespace('dialog');
export default createComponent({
mixins: [PopupMixin],
mixins: [PopupMixin()],
props: {
title: String,

View File

@ -18,7 +18,12 @@ function getDistance(touches) {
}
export default createComponent({
mixins: [PopupMixin, TouchMixin],
mixins: [
PopupMixin({
skipToggleEvent: true
}),
TouchMixin
],
props: {
className: null,
@ -83,8 +88,8 @@ export default createComponent({
};
if (scale !== 1) {
style.transform = `scale3d(${scale}, ${scale}, 1) translate(${this.moveX /
scale}px, ${this.moveY / scale}px)`;
style.transform = `scale3d(${scale}, ${scale}, 1) translate(${this
.moveX / scale}px, ${this.moveY / scale}px)`;
}
return style;
@ -92,8 +97,15 @@ export default createComponent({
},
watch: {
value() {
value(val) {
this.setActive(this.startPosition);
if (!val) {
this.$emit('close', {
index: this.active,
url: this.images[this.active]
});
}
},
startPosition(active) {
@ -116,14 +128,7 @@ export default createComponent({
if (deltaTime < 300 && offsetX < 10 && offsetY < 10) {
if (!this.doubleClickTimer) {
this.doubleClickTimer = setTimeout(() => {
const index = this.active;
if (!this.asyncClose) {
this.$emit('close', {
index,
url: this.images[index]
});
this.$emit('input', false);
}
@ -248,7 +253,8 @@ export default createComponent({
if (this.showIndex) {
return (
<div class={bem('index')}>
{this.slots('index') || `${this.active + 1} / ${this.images.length}`}
{this.slots('index') ||
`${this.active + 1} / ${this.images.length}`}
</div>
);
}

View File

@ -1,37 +1,25 @@
<template>
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-button
type="primary"
@click="showImagePreview"
>
<van-button type="primary" @click="showImagePreview">
{{ $t('button1') }}
</van-button>
</demo-block>
<demo-block :title="$t('button2')">
<van-button
type="primary"
@click="showImagePreview(1)"
>
<van-button type="primary" @click="showImagePreview(1)">
{{ $t('button2') }}
</van-button>
</demo-block>
<demo-block :title="$t('button3')">
<van-button
type="primary"
@click="showImagePreview(0, 1000)"
>
<van-button type="primary" @click="showImagePreview(0, 3000)">
{{ $t('button3') }}
</van-button>
</demo-block>
<demo-block :title="$t('componentCall')">
<van-button
type="primary"
@click="componentCall"
>
<van-button type="primary" @click="componentCall">
{{ $t('componentCall') }}
</van-button>
<van-image-preview
@ -112,7 +100,7 @@ export default {
</script>
<style lang="less">
@import "../../style/var";
@import '../../style/var';
.demo-image-preview {
background-color: @white;

View File

@ -92,7 +92,7 @@ test('double click', async done => {
done();
});
test('onClose option', async done => {
test('onClose option', () => {
const onClose = jest.fn();
const instance = ImagePreview({
images,
@ -100,37 +100,10 @@ test('onClose option', async done => {
onClose
});
instance.$emit('input', true);
expect(onClose).toHaveBeenCalledTimes(0);
await later(300);
const wrapper = document.querySelector('.van-image-preview');
const swipe = wrapper.querySelector('.van-swipe__track');
triggerDrag(swipe, 0, 0);
expect(onClose).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledWith({ index: 0, url: 'https://img.yzcdn.cn/1.png' });
done();
});
test('onClose should only trigger once', async done => {
const onClose = jest.fn();
const instance = ImagePreview({
images,
startPostion: 1,
onClose
});
ImagePreview({
images,
startPostion: 1,
onClose
});
instance.close();
expect(onClose).toHaveBeenCalledTimes(1);
done();
expect(onClose).toHaveBeenCalledWith({ index: 0, url: 'https://img.yzcdn.cn/1.png' });
});
test('onChange option', async done => {

View File

@ -6,190 +6,197 @@ import { on, off, preventDefault } from '../../utils/dom/event';
import { openOverlay, closeOverlay, updateOverlay } from './overlay';
import { getScrollEventTarget } from '../../utils/dom/scroll';
export const PopupMixin = {
mixins: [
TouchMixin,
CloseOnPopstateMixin,
PortalMixin({
afterPortal() {
if (this.overlay) {
updateOverlay();
}
}
})
],
props: {
// whether to show popup
value: Boolean,
// whether to show overlay
overlay: Boolean,
// overlay custom style
overlayStyle: Object,
// overlay custom class name
overlayClass: String,
// whether to close popup when click overlay
closeOnClickOverlay: Boolean,
// z-index
zIndex: [Number, String],
// prevent body scroll
lockScroll: {
type: Boolean,
default: true
},
// whether to lazy render
lazyRender: {
type: Boolean,
default: true
}
export const popupMixinProps = {
// whether to show popup
value: Boolean,
// whether to show overlay
overlay: Boolean,
// overlay custom style
overlayStyle: Object,
// overlay custom class name
overlayClass: String,
// whether to close popup when click overlay
closeOnClickOverlay: Boolean,
// z-index
zIndex: [Number, String],
// prevent body scroll
lockScroll: {
type: Boolean,
default: true
},
data() {
return {
inited: this.value
};
},
computed: {
shouldRender() {
return this.inited || !this.lazyRender;
}
},
watch: {
value(val) {
const type = val ? 'open' : 'close';
this.inited = this.inited || this.value;
this[type]();
this.$emit(type);
},
overlay: 'renderOverlay'
},
mounted() {
if (this.value) {
this.open();
}
},
/* istanbul ignore next */
activated() {
if (this.shouldReopen) {
this.$emit('input', true);
this.shouldReopen = false;
}
},
beforeDestroy() {
this.close();
if (this.getContainer && this.$parent && this.$parent.$el) {
this.$parent.$el.appendChild(this.$el);
}
},
/* istanbul ignore next */
deactivated() {
if (this.value) {
this.close();
this.shouldReopen = true;
}
},
methods: {
open() {
/* istanbul ignore next */
if (this.$isServer || this.opened) {
return;
}
// cover default zIndex
if (this.zIndex !== undefined) {
context.zIndex = this.zIndex;
}
this.opened = true;
this.renderOverlay();
if (this.lockScroll) {
on(document, 'touchstart', this.touchStart);
on(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.add('van-overflow-hidden');
}
context.lockCount++;
}
},
close() {
if (!this.opened) {
return;
}
if (this.lockScroll) {
context.lockCount--;
off(document, 'touchstart', this.touchStart);
off(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.remove('van-overflow-hidden');
}
}
this.opened = false;
closeOverlay(this);
this.$emit('input', false);
},
onTouchMove(event) {
this.touchMove(event);
const direction = this.deltaY > 0 ? '10' : '01';
const el = getScrollEventTarget(event.target, this.$el);
const { scrollHeight, offsetHeight, scrollTop } = el;
let status = '11';
/* istanbul ignore next */
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01';
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10';
}
/* istanbul ignore next */
if (
status !== '11' &&
this.direction === 'vertical' &&
!(parseInt(status, 2) & parseInt(direction, 2))
) {
preventDefault(event, true);
}
},
renderOverlay() {
if (this.$isServer || !this.value) {
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;
}
// whether to lazy render
lazyRender: {
type: Boolean,
default: true
}
};
export function PopupMixin(options = {}) {
return {
mixins: [
TouchMixin,
CloseOnPopstateMixin,
PortalMixin({
afterPortal() {
if (this.overlay) {
updateOverlay();
}
}
})
],
props: popupMixinProps,
data() {
return {
inited: this.value
};
},
computed: {
shouldRender() {
return this.inited || !this.lazyRender;
}
},
watch: {
value(val) {
const type = val ? 'open' : 'close';
this.inited = this.inited || this.value;
this[type]();
if (!options.skipToggleEvent) {
this.$emit(type);
}
},
overlay: 'renderOverlay'
},
mounted() {
if (this.value) {
this.open();
}
},
/* istanbul ignore next */
activated() {
if (this.shouldReopen) {
this.$emit('input', true);
this.shouldReopen = false;
}
},
beforeDestroy() {
this.close();
if (this.getContainer && this.$parent && this.$parent.$el) {
this.$parent.$el.appendChild(this.$el);
}
},
/* istanbul ignore next */
deactivated() {
if (this.value) {
this.close();
this.shouldReopen = true;
}
},
methods: {
open() {
/* istanbul ignore next */
if (this.$isServer || this.opened) {
return;
}
// cover default zIndex
if (this.zIndex !== undefined) {
context.zIndex = this.zIndex;
}
this.opened = true;
this.renderOverlay();
if (this.lockScroll) {
on(document, 'touchstart', this.touchStart);
on(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.add('van-overflow-hidden');
}
context.lockCount++;
}
},
close() {
if (!this.opened) {
return;
}
if (this.lockScroll) {
context.lockCount--;
off(document, 'touchstart', this.touchStart);
off(document, 'touchmove', this.onTouchMove);
if (!context.lockCount) {
document.body.classList.remove('van-overflow-hidden');
}
}
this.opened = false;
closeOverlay(this);
this.$emit('input', false);
},
onTouchMove(event) {
this.touchMove(event);
const direction = this.deltaY > 0 ? '10' : '01';
const el = getScrollEventTarget(event.target, this.$el);
const { scrollHeight, offsetHeight, scrollTop } = el;
let status = '11';
/* istanbul ignore next */
if (scrollTop === 0) {
status = offsetHeight >= scrollHeight ? '00' : '01';
} else if (scrollTop + offsetHeight >= scrollHeight) {
status = '10';
}
/* istanbul ignore next */
if (
status !== '11' &&
this.direction === 'vertical' &&
!(parseInt(status, 2) & parseInt(direction, 2))
) {
preventDefault(event, true);
}
},
renderOverlay() {
if (this.$isServer || !this.value) {
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;
}
}
};
}

View File

@ -31,13 +31,17 @@ function onClickOverlay(): void {
}
}
function mountOverlay() {
overlay = mount(Overlay, {
on: {
click: onClickOverlay
}
});
}
export function updateOverlay(): void {
if (!overlay) {
overlay = mount(Overlay, {
on: {
click: onClickOverlay
}
});
mountOverlay();
}
if (context.top) {

View File

@ -1,7 +1,7 @@
import { createNamespace } from '../utils';
import { WHITE } from '../utils/constant';
import { inherit } from '../utils/functional';
import { PopupMixin } from '../mixins/popup';
import { popupMixinProps } from '../mixins/popup';
import Popup from '../popup';
// Types
@ -48,7 +48,7 @@ function Notify(
}
Notify.props = {
...PopupMixin.props,
...popupMixinProps,
background: String,
className: null as any,
message: [Number, String],

View File

@ -5,7 +5,7 @@ import Icon from '../icon';
const [createComponent, bem] = createNamespace('popup');
export default createComponent({
mixins: [PopupMixin],
mixins: [PopupMixin()],
props: {
round: Boolean,

View File

@ -7,7 +7,7 @@ import Loading from '../loading';
const [createComponent, bem] = createNamespace('toast');
export default createComponent({
mixins: [PopupMixin],
mixins: [PopupMixin()],
props: {
icon: String,

View File

@ -19,7 +19,9 @@ export type ImagePreviewOptions = string[] | {
export class VanImagePreview extends VanPopupMixin {
images: string[];
showIndex: boolean;
startPosition: number;
}