blob: 7e96585943e0f7d201dc368e2a91bae838b6436b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { throttle, extend } from './func'
import { createEvent } from './event'
import config from '../config'
export function getParentScroller (vm) {
if (!vm) return null
if (vm._parentScroller) {
return vm._parentScroller
}
function _getParentScroller (parent) {
if (!parent) { return }
if (config.scrollableTypes.indexOf(parent.weexType) > -1) {
vm._parentScroller = parent
return parent
}
return _getParentScroller(parent.$parent)
}
return _getParentScroller(vm.$parent)
}
function horizontalBalance (rect, ctRect) {
return rect.left < ctRect.right && rect.right > ctRect.left
}
function verticalBalance (rect, ctRect) {
return rect.top < ctRect.bottom && rect.bottom > ctRect.top
}
/**
* return a data array with two boolean value, which are:
* 1. visible in current ct's viewport.
* 2. visible with offset in current ct's viewport.
*/
export function hasIntersection (rect, ctRect, dir, offset) {
dir = dir || 'up'
const isHorizontal = dir === 'left' || dir === 'right'
const isVertical = dir === 'up' || dir === 'down'
if (isHorizontal && !verticalBalance(rect, ctRect)) {
return [false, false]
}
if (isVertical && !horizontalBalance(rect, ctRect)) {
return [false, false]
}
offset = parseInt(offset || 0) * weex.config.env.scale
switch (dir) {
case 'up':
return [
rect.top < ctRect.bottom && rect.bottom > ctRect.top,
rect.top < ctRect.bottom + offset && rect.bottom > ctRect.top - offset
]
case 'down':
return [
rect.bottom > ctRect.top && rect.top < ctRect.bottom,
rect.bottom > ctRect.top - offset && rect.top < ctRect.bottom + offset
]
case 'left':
return [
rect.left < ctRect.right && rect.right > ctRect.left,
rect.left < ctRect.right + offset && rect.right > ctRect.left - offset
]
case 'right':
return [
rect.right > ctRect.left && rect.left < ctRect.right,
rect.right > ctRect.left - offset && rect.left < ctRect.right + offset
]
}
}
/**
* isElementVisible
* @param {HTMLElement} el a dom element.
* @param {HTMLElement} container optional, the container of this el.
*/
export function isElementVisible (el, container, dir, offset) {
if (!el.getBoundingClientRect) { return false }
const bodyRect = {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth
}
const ctRect = (container === document.body)
? bodyRect : container
? container.getBoundingClientRect() : bodyRect
return hasIntersection(el.getBoundingClientRect(), ctRect, dir, offset)
}
// to trigger the appear/disappear event.
function triggerEvent (elm, handlers, evt, dir) {
let listener = handlers[evt]
if (listener && listener.fn) {
listener = listener.fn
}
if (listener) {
listener(createEvent(elm, evt, {
direction: dir
}))
}
}
/**
* get all event listeners. including bound handlers in all parent vnodes.
*/
export function getEventHandlers (context) {
let vnode = context.$vnode
const handlers = {}
const attachedVnodes = []
while (vnode) {
attachedVnodes.push(vnode)
vnode = vnode.parent
}
attachedVnodes.forEach(function (vnode) {
const parentListeners = vnode.componentOptions && vnode.componentOptions.listeners
const dataOn = vnode.data && vnode.data.on
extend(handlers, parentListeners, dataOn)
})
return handlers
}
function getAppearOffset (el) {
return el && el.getAttribute('appear-offset')
}
function checkHandlers (handlers) {
return [
!!(handlers.appear || handlers.disappear),
!!(handlers.offsetAppear || handlers.offsetDisappear)
]
}
/**
* Watch element's visibility to tell whether should trigger a appear/disappear
* event in scroll handler.
*/
export function watchAppear (context, fireNow) {
const el = context && context.$el
if (!el || el.nodeType !== 1) { return }
const appearOffset = getAppearOffset(el)
const handlers = getEventHandlers(context)
const checkResults = checkHandlers(handlers)
// no appear or offsetAppear handler was bound.
if (!checkResults[0] && !checkResults[1]) {
return
}
let isWindow = false
let container = document.body
const scroller = getParentScroller(context)
if (scroller && scroller.$el) {
container = scroller.$el
}
else {
isWindow = true
}
if (fireNow) {
const visibleData = isElementVisible(el, container, null, appearOffset)
detectAppear(context, visibleData, null)
}
// add current vm to the container's appear watch list.
if (!container._watchAppearList) {
container._watchAppearList = []
}
container._watchAppearList.push(context)
/**
* Code below will only exec once for binding scroll handler for parent container.
*/
if (container._scrollWatched) {
return
}
container._scrollWatched = true
const scrollHandler = throttle(event => {
/**
* detect scrolling direction.
* direction only support up & down yet.
* TODO: direction support left & right.
*/
const scrollTop = isWindow ? window.pageYOffset : container.scrollTop
const preTop = container._lastScrollTop
container._lastScrollTop = scrollTop
const dir = (scrollTop < preTop
? 'down' : scrollTop > preTop
? 'up' : container._prevDirection) || null
container._prevDirection = dir
const watchAppearList = container._watchAppearList || []
const len = watchAppearList.length
for (let i = 0; i < len; i++) {
const vm = watchAppearList[i]
const el = vm.$el
const appearOffset = getAppearOffset(el)
const visibleData = isElementVisible(el, container, dir, appearOffset)
detectAppear(vm, visibleData, dir)
}
}, 25, true)
container.addEventListener('scroll', scrollHandler, false)
}
/**
* trigger a disappear event.
*/
export function triggerDisappear (context) {
return detectAppear(context, [false, false])
}
/**
* decide whether to trigger a appear/disappear event.
* @param {VueComponent} context
* @param {boolean} visible
* @param {string} dir
*/
export function detectAppear (context, visibleData, dir = null, appearOffset) {
const el = context && context.$el
const [visible, offsetVisible] = visibleData
if (!el) { return }
const handlers = getEventHandlers(context)
/**
* No matter it's binding appear/disappear or both of them. Always
* should test it's visibility and change the context/._visible.
* If neithor of them was bound, then just ignore it.
*/
/**
* if the component hasn't appeared for once yet, then it shouldn't trigger
* a disappear event at all.
*/
if (context._appearedOnce || visible) {
if (context._visible !== visible) {
if (!context._appearedOnce) {
context._appearedOnce = true
}
context._visible = visible
triggerEvent(el, handlers, visible ? 'appear' : 'disappear', dir)
}
}
if (context._offsetAppearedOnce || offsetVisible) {
if (context._offsetVisible !== offsetVisible) {
if (!context._offsetAppearedOnce) {
context._offsetAppearedOnce = true
}
context._offsetVisible = offsetVisible
triggerEvent(el, handlers, offsetVisible ? 'offsetAppear' : 'offsetDisappear', dir)
}
}
}