UNPKG

asia-weex-vue-render4light

Version:

Web renderer for weex project written in Vue DSL.

322 lines (305 loc) 9.87 kB
/* * 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 { dispatchNativeEvent } from './event' import config from '../config' /** * whether ct contains el. * @param {HTMLElement} container * @param {HTMLElement} target */ export function contains (container, target, includeSelf) { if (includeSelf && container === target) { return true } return container.contains ? container.contains(target) && (container !== target) : container.compareDocumentPosition(target) & 16 !== 0 } export function insideA (el) { if (typeof el._insideA === 'boolean') { return el._insideA } let parent = el.parentElement const parents = [] const checkParents = function (inside) { for (let i = 0, l = parents.length; i < l; i++) { parents[i]._insideA = inside } } const check = function (inside) { el._insideA = inside checkParents(inside) return inside } while (parent !== document.body) { if (parent.tagName.toLowerCase() === 'a') { return check(true) } if (typeof parent._insideA === 'boolean') { return check(parent._insideA) } parents.push(parent) parent = parent.parentElement } return check(false) } /** * get parent scroller vComponent. * return a VueComponent or null. */ 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) } /** * get scroller's element. * @param vm {HTMLElement | VueCOmponent} vm or element. * return the element or document.body. */ export function getParentScrollerElement (vm) { if (!vm) { return null } const el = vm instanceof HTMLElement ? vm : vm.$el if (!el || el.nodeType !== 1) { return } if (vm._parentScroller) { return vm._parentScroller } function _getParentScroller (parent) { if (!parent) { return } const tagName = parent.tagName.toLowerCase() if (tagName === 'body' || (tagName === 'main' && config.scrollableTypes.indexOf(parent.getAttribute('weex-type')) > -1) ) { vm._parentScroller = parent return parent } return _getParentScroller(parent.parentElement) } return _getParentScroller(el) } 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 = offset ? parseInt(offset) * weex.config.env.scale : 0 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 === window || container === document.body) ? bodyRect : container ? container.getBoundingClientRect() : bodyRect return hasIntersection(el.getBoundingClientRect(), ctRect, dir, offset) } // to trigger the appear/disappear event. function triggerAppearEvent (elm, evt, dir) { dispatchNativeEvent(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 updateWatchAppearList (container) { container._watchAppearList = Array.prototype.slice.call( container.querySelectorAll('[weex-appear]')) } /** * inject removeChild function to watch disappear and offsetDisappear events. */ if (!window._rmInjected) { window._rmInjected = true const nativeRemove = HTMLElement.prototype.removeChild HTMLElement.prototype.removeChild = function (el) { el._visible && triggerAppearEvent(el, 'disappear', null) el._offsetVisible && triggerAppearEvent(el, 'offsetDisappear', null) nativeRemove.apply(this, arguments) } } /** * 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 } let isWindow = false const container = getParentScrollerElement(context) if (!container) { return } if (container === document.body) { isWindow = true } /** * Code below will only exec once for binding scroll handler for parent container. */ let scrollHandler = container._scrollHandler if (!scrollHandler) { scrollHandler = container._scrollHandler = event => { updateWatchAppearList(container) /** * 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 el = watchAppearList[i] const appearOffset = getAppearOffset(el) const visibleData = isElementVisible(el, container, dir, appearOffset) detectAppear(el, visibleData, dir) } } container.addEventListener('scroll', throttle(scrollHandler, 100, true)) } if (fireNow) { context.$nextTick(scrollHandler) } } /** * decide whether to trigger a appear/disappear event. * @param {VueComponent} context * @param {boolean} visible * @param {string} dir */ export function detectAppear (el, visibleData, dir = null, appearOffset) { if (!el) { return } const [visible, offsetVisible] = visibleData /** * No matter it's binding appear/disappear or both of them. Always * should test it's visibility and change the el._visible. * If neigher has been bound, then ignore it. */ /** * if the component hasn't appeared for once yet, then it shouldn't trigger * a disappear event at all. */ if (el._appearedOnce || visible) { if (el._visible !== visible) { el._visible = visible if (visible && !el._appearedOnce) { el._appearedOnce = true } const evtName = visible ? 'appear' : 'disappear' if (el.getAttribute(`data-evt-${evtName}`) === '') { triggerAppearEvent(el, evtName, dir) } } } if (el._offsetAppearedOnce || offsetVisible) { if (el._offsetVisible !== offsetVisible) { el._offsetVisible = offsetVisible if (offsetVisible && !el._offsetAppearedOnce) { el._offsetAppearedOnce = true } const evt = offsetVisible ? ['offset-appear', 'offsetAppear'] : ['offset-disappear', 'offsetDisappear'] if (el.getAttribute(`data-evt-${evt[0]}`) === '') { triggerAppearEvent(el, evt[1], dir) } } } }