<template>
<div class="ion-content" :class="[modeClass]">
<div ref="fixedElement" class="fixed-content">
<slot name="fixed"></slot>
</div>
<div ref="scrollElement" class="scroll-content" :class="{'disable-scroll':isScrollDisabled}">
<slot></slot>
</div>
<slot name="refresher"></slot>
</div>
</template>
<script type="text/javascript">
import {removeArrayItem, parsePxUnit, cssFormat} from '../../util/util'
import {transitionEnd, registerListener} from '../../util/dom'
import ScrollView from './scroll-view'
import {updateImgs} from './img-util'
import ModeMixins from '../../themes/theme.mixins'
import throttle from 'lodash.throttle'
export default {
name: 'vm-content',
mixins: [ModeMixins],
inject: {
pageComponent: {
from: 'pageComponent',
default: null
},
tabsComponent: {
from: 'tabsComponent',
default: null
}
},
provide () {
let _this = this
return {
contentComponent: _this
}
},
data () {
return {
contentMarginTop: 0,
contentMarginBottom: 0,
headerHeight: 0,
footerHeight: 0,
scrollView: new ScrollView(),
contentTop: 0,
contentBottom: 0,
fixedTop: 0,
fixedBottom: 0,
paddingTop: 0,
paddingBottom: 0,
tabsPlacement: null,
tabsTop: 0,
resizeUnReg: null,
imgs: [],
imgReqBfr: this.$config && this.$config.getNumber('imgRequestBuffer', 400),
imgRndBfr: this.$config && this.$config.getNumber('imgRenderBuffer', 400),
imgVelMax: this.$config && this.$config.getNumber('imgVelocityMax', 3)
}
},
computed: {
contentHeight: function () {
return this.scrollView.ev.contentHeight
},
contentWidth: function () {
return this.scrollView.ev.contentWidth
},
scrollHeight: function () {
return this.scrollView.ev.scrollHeight
},
scrollWidth: function () {
return this.scrollView.ev.scrollWidth
},
scrollTop: {
get: function () {
return this.scrollView.ev.scrollTop
},
set: function (top) {
this.scrollView.setTop(top)
}
},
scrollElement () {
return this.$refs.scrollElement
},
fixedElement () {
return this.$refs.fixedElement
},
headerComponent () {
return this.pageComponent.headerComponent
},
footerComponent () {
return this.pageComponent.footerComponent
},
isScrollDisabled () {
return this.$app && this.$app.isScrollDisabled
}
},
created () {
// this.scrollView = new ScrollView()
this.imgs = []
// 窗口变化重新计算容器
this.resizeUnReg = registerListener(window, 'resize', throttle(() => { this._calculateContentDimensions() }, 200, {leading: false, trailing: true}))
const scroll = this.scrollView
scroll.onScrollStart = (ev) => {
this.$emit('onScrollStart', ev)
this.$events && this.$events.$emit('onScrollStart', ev)
}
scroll.onScroll = (ev) => {
this.$emit('onScroll', ev)
this.$events && this.$events.$emit('onScroll', ev)
this.imgsUpdate()
}
scroll.onScrollEnd = (ev) => {
this.$emit('onScrollEnd', ev)
this.$events && this.$events.$emit('onScrollEnd', ev)
this.imgsUpdate()
}
// TODO
// directive 插入的header不能被获取到pageComponent,采用监听的方式获得header的加载事件
const self = this
this.$events.$once('header:mounted', function (ele) {
let headerHeight = ele.$el.clientHeight
self._calculateContentDimensions(headerHeight)
})
},
mounted () {
if (this.$slots && this.$slots['fixed']) {
this.$slots['fixed'].forEach((item) => {
item.elm.setAttribute('fixed', '')
})
}
this._calculateContentDimensions()
},
destroyed () {
this.resizeUnReg && this.resizeUnReg()
this.scrollView && this.scrollView.destroy()
},
methods: {
getContentDimensions () {
const scrollEle = this.scrollElement
return {
contentWidth: scrollEle.clientWidth,
contentHeight: scrollEle.clientHeight - this.contentTop - this.contentBottom,
contentTop: this.contentTop,
contentBottom: this.contentBottom,
scrollHeight: scrollEle.scrollHeight,
scrollTop: scrollEle.scrollTop,
scrollWidth: scrollEle.scrollWidth,
scrollLeft: scrollEle.scrollLeft
}
},
/**
* @function scrollTo
* @description 滚动到指定位置
* @param {Number} [x=0] - 滚动到指定位置的x值
* @param {Number} [y=0] - 滚动到指定位置的y值
* @param {Number} [duration=300] - 滚动动画的时间
* @param {Function=} done - 当滚动结束时触发的回调
* @return {Promise} - 当回调done未定义的时候, 才返回Promise, 如果定义则返回undefined
*/
scrollTo (x, y, duration = 300, done) {
return this.scrollView.scrollTo(x, y, duration, done)
},
/**
* @function scrollToTop
* @description 滚动到顶部
* @param {Number} [duration=300] - 滚动动画的时间, 默认是300ms
* @return {Promise} 当滚动动画完毕后返回promise
*/
scrollToTop (duration = 300) {
/* eslint-disable no-console */
console.debug(`content, scrollToTop, duration: ${duration}`)
return this.scrollView.scrollToTop(duration)
},
/**
* @function scrollToBottom
* @description 滚动到顶部
* @param {Number} [duration=300] - 滚动动画的时间, 默认是300ms
* @return {Promise} 当滚动动画完毕后返回promise
*/
scrollToBottom (duration = 300) {
/* eslint-disable no-console */
console.debug(`content, scrollToBottom, duration: ${duration}`)
return this.scrollView.scrollToBottom(duration)
},
/**
* @function scrollBy
* @description
* 滚动到指定位置, 这个和scrollTo类似, 只不过是相对当前位置的滚动
*
* 当前位置为scrollTop为`100px`, 执行`myScroll.scrollBy(0, -10)`, 则滚动到`110px`位置
*
* @param {Number} x - 滚动到指定位置的x值
* @param {Number} y - 滚动到指定位置的y值
* @param {Number} [duration=300] - 滚动动画的时间
* @param {Function=} done - 当滚动结束时触发的回调
* @return {Promise} - 当回调done未定义的时候, 才返回Promise, 如果定义则返回undefined
*/
scrollBy (x, y, duration = 300, done) {
return this.scrollView.scrollBy(x, y, duration, done)
},
/**
* @function scrollToElement
* @description 滚动到指定元素
* @param {Element} el
* @param {Number} [duration=300] - 滚动动画的时间
* @param {Function=} done - 当滚动结束时触发的回调
* @return {Promise} - 当回调done未定义的时候, 才返回Promise, 如果定义则返回undefined
*/
scrollToElement (el, duration = 300, done) {
return this.scrollView.scrollToElement(el, duration, done)
},
/**
* @function resize
* @description
* 当动态添加Header/Footer/Tabs或者修改了他的属性时, 重新计算Content组件的尺寸.
*/
resize () {
this.$nextTick(() => {
this._calculateContentDimensions()
})
},
_calculateContentDimensions (headerHeight = 0) {
console.assert(this.fixedElement, 'fixed element was not found')
console.assert(this.scrollElement, 'scroll element was not found')
this._readDimensions(headerHeight)
this._writeDimensions()
},
_readDimensions (headerHeight = 0) {
const cachePaddingTop = this.paddingTop
const cachePaddingBottom = this.paddingBottom
const cacheHeaderHeight = this.headerHeight
const cacheTabsPlacement = this.tabsPlacement
const cacheFooterHeight = this.footerHeight
let tabsTop = 0
let scrollEvent = null
this.paddingTop = 0
this.paddingBottom = 0
this.headerHeight = headerHeight
this.footerHeight = 0
this.tabsPlacement = null
this.tabsTop = 0
this.fixedTop = 0
this.fixedBottom = 0
// In certain cases this.scrollView is undefined
// if that is the case then we should just return
if (!this.scrollView) {
console.assert(false, 'scrollView should be valid')
return
}
scrollEvent = this.scrollView.ev
if (this.headerComponent) {
let ele = this.headerComponent.getNativeElement()
this.headerHeight = parsePxUnit(window.getComputedStyle(ele).height)
}
if (this.footerComponent) {
let ele = this.footerComponent.getNativeElement()
this.footerHeight = parsePxUnit(window.getComputedStyle(ele).height)
}
// Toolbar height
this.contentTop = this.headerHeight
this.contentBottom = this.footerHeight
// In a Tabs
if (this.tabsComponent) {
let ele = this.tabsComponent.getNativeElement()
let tabbarEle = ele.firstElementChild
let tabbarHeight = tabbarEle.clientHeight
if (this.tabsPlacement === null) {
// this is the first tabbar found, remember it's position
this.tabsPlacement = ele.getAttribute('tabsplacement')
}
// Tabs height
if (this.tabsPlacement === 'top') {
this.tabsTop = this.headerHeight
tabsTop = this.tabsComponent.getTabsTop()
this.contentTop += tabbarHeight
} else {
this.contentBottom += tabbarHeight
}
}
// Fixed content shouldn't include content padding
this.fixedTop = this.contentTop
this.fixedBottom = this.contentBottom
// ******** DOM READ ****************
const contentDimensions = this.getContentDimensions()
scrollEvent.scrollHeight = contentDimensions.scrollHeight
scrollEvent.scrollWidth = contentDimensions.scrollWidth
scrollEvent.contentHeight = contentDimensions.contentHeight
scrollEvent.contentWidth = contentDimensions.contentWidth
scrollEvent.contentTop = contentDimensions.contentTop
scrollEvent.contentBottom = contentDimensions.contentBottom
this._dirty = (
cachePaddingTop !== this.paddingTop ||
cachePaddingBottom !== this.paddingBottom ||
cacheHeaderHeight !== this.headerHeight ||
cacheFooterHeight !== this.footerHeight ||
cacheTabsPlacement !== this.tabsPlacement ||
tabsTop !== this.tabsTop ||
this.contentTop !== this.contentMarginTop ||
this.contentBottom !== this.contentMarginBottom
)
this.scrollView.init(this.scrollElement)
// initial imgs refresh
this.imgsUpdate()
},
_writeDimensions () {
if (!this._dirty) {
console.debug('Skipping writeDimensions')
return
}
const scrollEle = this.scrollElement
if (!scrollEle) {
console.assert(false, 'this.scrollElement should be valid')
return
}
const fixedEle = this.fixedElement
if (!fixedEle) {
console.assert(false, 'this.fixedElement should be valid')
return
}
// Tabs height
if (this.tabsPlacement === 'bottom' && this.contentBottom > 0 && this.footerComponent) {
var footerPos = this.contentBottom - this.footerHeight
console.assert(footerPos >= 0, 'footerPos has to be positive')
this.footerComponent.getNativeElement().style.bottom = cssFormat(footerPos)
}
// Only update top margin if value changed
if (this.contentTop !== this.contentMarginTop) {
console.assert(this.contentTop >= 0, 'contentTop has to be positive')
console.assert(this.fixedTop >= 0, 'fixedTop has to be positive')
scrollEle.style.marginTop = cssFormat(this.contentTop)
fixedEle.style.marginTop = cssFormat(this.fixedTop)
this.contentMarginTop = this.contentTop
}
// Only update bottom margin if value changed
if (this.contentBottom !== this.contentMarginBottom) {
console.assert(this.contentBottom >= 0, 'contentBottom has to be positive')
console.assert(this.fixedBottom >= 0, 'fixedBottom has to be positive')
scrollEle.style.marginBottom = cssFormat(this.contentBottom)
fixedEle.style.marginBottom = cssFormat(this.fixedBottom)
this.contentMarginBottom = this.contentBottom
}
if (this.tabsComponent && this.tabsPlacement !== null) {
// set the position of the tabbar
if (this.tabsPlacement === 'top') {
this.tabsComponent.setTabbarPosition(this.tabsTop, -1)
} else {
console.assert(this.tabsPlacement === 'bottom', 'tabsPlacement should be bottom')
this.tabsComponent.setTabbarPosition(-1, 0)
}
}
},
// -------- For Refresher Component --------
getScrollElement () {
return this.scrollElement
},
onScrollElementTransitionEnd (callback) {
transitionEnd(this.getScrollElement(), callback)
},
setScrollElementStyle (prop, val) {
const scrollEle = this.scrollElement
if (scrollEle) {
this.$nextTick(() => {
(scrollEle.style)[prop] = val
})
}
},
// -------- For Img Component --------
addImg (img) {
this.imgs.push(img)
},
removeImg (img) {
removeArrayItem(this.imgs, img)
},
isImgsUpdatable () {
// an image is only "updatable" if the content isn't scrolling too fast
// if scroll speed is above the maximum velocity, then let current
// requests finish, but do not start new requets or render anything
// if scroll speed is below the maximum velocity, then it's ok
// to start new requests and render images
return Math.abs(this.scrollView.ev.velocityY) < this.imgVelMax
},
imgsUpdate () {
if (this.scrollView.initialized && this.imgs.length && this.isImgsUpdatable()) {
this.$nextTick(() => {
updateImgs(this.imgs, this.scrollTop, this.contentHeight, this.scrollView.ev.directionY, this.imgReqBfr, this.imgRndBfr)
})
}
}
}
}
</script>
<style lang="scss">
@import "content";
@import "content.ios";
@import "content.md";
</style>