diff --git a/src/dropdown-menu/index.js b/src/dropdown-menu/index.js index e8bdab20c..e1bf411cd 100644 --- a/src/dropdown-menu/index.js +++ b/src/dropdown-menu/index.js @@ -2,7 +2,7 @@ import { createNamespace } from '../utils'; import { BORDER_TOP_BOTTOM } from '../utils/constant'; import { ParentMixin } from '../mixins/relation'; import { ClickOutsideMixin } from '../mixins/click-outside'; -import { getScrollEventTarget } from '../utils/dom/scroll'; +import { getScroller } from '../utils/dom/scroll'; const [createComponent, bem] = createNamespace('dropdown-menu'); @@ -47,7 +47,7 @@ export default createComponent({ computed: { scroller() { - return getScrollEventTarget(this.$el); + return getScroller(this.$el); } }, diff --git a/src/hooks/use-click-outside.ts b/src/hooks/use-click-outside.ts new file mode 100644 index 000000000..db64eb2a0 --- /dev/null +++ b/src/hooks/use-click-outside.ts @@ -0,0 +1,21 @@ +import { Ref } from 'vue'; +import { useGlobalEvent } from './use-global-event'; + +export type UseClickOutsideOpitons = { + event: string; + callback: EventListener; + element: Ref; + flag?: Ref; +}; + +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); +} diff --git a/src/hooks/use-global-event.ts b/src/hooks/use-global-event.ts new file mode 100644 index 000000000..319c043bf --- /dev/null +++ b/src/hooks/use-global-event.ts @@ -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 +) { + 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); +} diff --git a/src/hooks/use-lock-scroll.ts b/src/hooks/use-lock-scroll.ts new file mode 100644 index 000000000..9783b2090 --- /dev/null +++ b/src/hooks/use-lock-scroll.ts @@ -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); + } + }; +} diff --git a/src/hooks/use-touch.ts b/src/hooks/use-touch.ts new file mode 100644 index 000000000..aaaa5fb5c --- /dev/null +++ b/src/hooks/use-touch.ts @@ -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 - this.startX; + deltaY.value = touch.clientY - this.startY; + offsetX.value = Math.abs(this.deltaX); + offsetY.value = Math.abs(this.deltaY); + + if (!direction.value) { + direction.value = getDirection(offsetX.value, offsetY.value); + } + } + + return { + move, + start, + startX, + startY, + deltaX, + deltaY, + offsetX, + offsetY, + direction + }; +} diff --git a/src/index-bar/index.js b/src/index-bar/index.js index 2085467d1..d804b147a 100644 --- a/src/index-bar/index.js +++ b/src/index-bar/index.js @@ -10,7 +10,7 @@ import { getElementTop, getRootScrollTop, setRootScrollTop, - getScrollEventTarget + getScroller } from '../utils/dom/scroll'; const [createComponent, bem] = createNamespace('index-bar'); @@ -21,7 +21,7 @@ export default createComponent({ ParentMixin('vanIndexBar'), BindEventMixin(function(bind) { if (!this.scroller) { - this.scroller = getScrollEventTarget(this.$el); + this.scroller = getScroller(this.$el); } bind(this.scroller, 'scroll', this.onScroll); diff --git a/src/list/index.js b/src/list/index.js index 4478a945c..e0ab4da67 100644 --- a/src/list/index.js +++ b/src/list/index.js @@ -1,7 +1,7 @@ import { createNamespace } from '../utils'; import { isHidden } from '../utils/dom/style'; import { BindEventMixin } from '../mixins/bind-event'; -import { getScrollEventTarget } from '../utils/dom/scroll'; +import { getScroller } from '../utils/dom/scroll'; import Loading from '../loading'; const [createComponent, bem, t] = createNamespace('list'); @@ -10,7 +10,7 @@ export default createComponent({ mixins: [ BindEventMixin(function(bind) { if (!this.scroller) { - this.scroller = getScrollEventTarget(this.$el); + this.scroller = getScroller(this.$el); } bind(this.scroller, 'scroll', this.check); diff --git a/src/mixins/popup/index.js b/src/mixins/popup/index.js index 1b64cecf7..b700bccc6 100644 --- a/src/mixins/popup/index.js +++ b/src/mixins/popup/index.js @@ -4,7 +4,7 @@ import { PortalMixin } from '../portal'; import { CloseOnPopstateMixin } from '../close-on-popstate'; import { on, off, preventDefault } from '../../utils/dom/event'; import { openOverlay, closeOverlay, updateOverlay } from './overlay'; -import { getScrollEventTarget } from '../../utils/dom/scroll'; +import { getScroller } from '../../utils/dom/scroll'; export const popupMixinProps = { // whether to show popup @@ -152,7 +152,7 @@ export function PopupMixin(options = {}) { onTouchMove(event) { this.touchMove(event); const direction = this.deltaY > 0 ? '10' : '01'; - const el = getScrollEventTarget(event.target, this.$el); + const el = getScroller(event.target, this.$el); const { scrollHeight, offsetHeight, scrollTop } = el; let status = '11'; diff --git a/src/pull-refresh/index.js b/src/pull-refresh/index.js index 7215870ea..c83f20e16 100644 --- a/src/pull-refresh/index.js +++ b/src/pull-refresh/index.js @@ -1,7 +1,7 @@ import { createNamespace } from '../utils'; import { preventDefault } from '../utils/dom/event'; import { TouchMixin } from '../mixins/touch'; -import { getScrollTop, getScrollEventTarget } from '../utils/dom/scroll'; +import { getScrollTop, getScroller } from '../utils/dom/scroll'; import Loading from '../loading'; const [createComponent, bem, t] = createNamespace('pull-refresh'); @@ -76,7 +76,7 @@ export default createComponent({ mounted() { this.bindTouchEvent(this.$refs.track); - this.scrollEl = getScrollEventTarget(this.$el); + this.scrollEl = getScroller(this.$el); }, methods: { diff --git a/src/sticky/index.js b/src/sticky/index.js index 47eefbca2..5f71fad58 100644 --- a/src/sticky/index.js +++ b/src/sticky/index.js @@ -1,6 +1,6 @@ import { createNamespace, isDef } from '../utils'; import { BindEventMixin } from '../mixins/bind-event'; -import { getScrollTop, getElementTop, getScrollEventTarget } from '../utils/dom/scroll'; +import { getScrollTop, getElementTop, getScroller } from '../utils/dom/scroll'; const [createComponent, bem] = createNamespace('sticky'); @@ -8,7 +8,7 @@ export default createComponent({ mixins: [ BindEventMixin(function(bind) { if (!this.scroller) { - this.scroller = getScrollEventTarget(this.$el); + this.scroller = getScroller(this.$el); } bind(this.scroller, 'scroll', this.onScroll, true); diff --git a/src/utils/dom/scroll.ts b/src/utils/dom/scroll.ts index 544dfae1e..44a651bc1 100644 --- a/src/utils/dom/scroll.ts +++ b/src/utils/dom/scroll.ts @@ -4,7 +4,7 @@ type ScrollElement = HTMLElement | Window; // http://w3help.org/zh-cn/causes/SD9013 // http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome const overflowScrollReg = /scroll|auto/i; -export function getScrollEventTarget(element: HTMLElement, rootParent: ScrollElement = window) { +export function getScroller(element: HTMLElement, rootParent: ScrollElement = window) { let node = element; while ( node &&