vuikit
Version:
A Vuejs component library based on UIkit
290 lines (254 loc) • 7.72 kB
JavaScript
/**
* Vuikit 0.7.0
* (c) 2018 Miljan Aleksic
* @license MIT
*/
// import { Animation } from 'vuikit/core/helpers/dom/animation'
import css from 'vuikit/core/helpers/css'
import { warn } from 'vuikit/core/helpers/debug'
import { isString, isInteger } from 'vuikit/core/util'
import { on } from 'vuikit/core/helpers/dom/event'
import { addClass, removeClass, toggleClass } from 'vuikit/core/helpers/dom/class'
// let dir
let scroll = 0
on(window, 'scroll', () => {
// dir = scroll < window.pageYOffset
// ? 'down'
// : 'up'
scroll = window.pageYOffset
})
function offsetTop (element) {
return element.getBoundingClientRect().top + window.pageYOffset
}
export default {
name: 'Sticky',
abstract: true,
props: {
top: {
type: [Number, String],
default: 0
},
bottom: {
type: [Number, String],
default: 0
},
offset: {
type: Number,
default: 0
},
widthElement: {
// dom ref
default: false
},
animation: {
type: String,
default: ''
},
showOnUp: {
type: Boolean,
default: false
}
},
data: () => ({
isActive: false,
topOffset: 0,
outerHeight: 0,
clsFixed: 'uk-sticky-fixed',
clsBelow: 'uk-sticky-below',
clsActive: 'uk-active',
clsInactive: ''
}),
render (h) {
let children = this.$options._renderChildren
if (!children) {
return
}
// filter out possible whitespaces
children = children.filter(n => n.tag)
if (!children.length) {
return
}
// warn multiple elements
if (process.env.NODE_ENV !== 'production' && children.length > 1) {
warn('<vk-sticky> can only be used on a single element.', this.$parent)
}
const rawChild = children[0]
on(window, 'scroll', () => {
this.offsetTop = offsetTop(this.$el)
this.visible = isVisible(this.$el)
this.onScroll()
}, this._uid)
return rawChild
},
computed: {
stickyStartPoint () {
let top = this.top
if (isInteger(top) && this.topOffset) {
top = this.topOffset + parseFloat(top)
} else if (isString(top) && top.match(/^-?\d+vh$/)) {
top = getViewportHeightOffset(top)
} else {
top = this.getElementOffset(top)
}
return Math.max(parseFloat(top), this.topOffset) - this.offset
},
stickyEndPoint () {
let bottom = this.bottom
// get element
bottom = this.getElementOffset(bottom === true
? this.$el.parent()
: bottom
)
return bottom && bottom - this.outerHeight
},
inactive () {
return this.media && !window.matchMedia(this.media).matches
},
$widthElement () {
return this.widthElement || this.$el
},
bottomOffset () {
return this.topOffset + this.outerHeight
}
},
methods: {
show () {
this.isActive = true
this.update()
this.placeholder.removeAttribute('hidden')
},
hide () {
addClass(this.$el, this.clsInactive)
removeClass(this.$el, `${this.clsFixed} ${this.clsActive} ${this.clsBelow}`)
css(this.$el, 'position', '')
css(this.$el, 'width', '')
css(this.$el, 'top', '')
this.placeholder.setAttribute('hidden', 'hidden')
},
update () {
let top = Math.max(0, this.offset)
const active = scroll > this.stickyStartPoint
if (this.stickyEndPoint && scroll > this.stickyEndPoint - this.offset) {
top = this.stickyEndPoint - scroll
}
addClass(this.$el, this.clsFixed)
css(this.$el, 'width', `${this.$widthElement.offsetWidth}px`)
css(this.$el, 'position', 'fixed')
css(this.$el, 'top', `${top}px`)
toggleClass(this.$el, this.clsActive, active)
toggleClass(this.$el, this.clsInactive, !active)
toggleClass(this.$el, this.clsBelow, scroll > this.bottomOffset)
},
// ready () {
// if (!(this.target && window.location.hash && window.pageYOffset > 0)) {
// return
// }
//
// var target = query(window.location.hash)
//
// if (target) {
// window.requestAnimationFrame(() => {
// var top = offsetTop(target)
// var elTop = offsetTop(this.$el)
// var elHeight = this.$el[0].offsetHeight
//
// if (elTop + elHeight >= top && elTop <= top + target[0].offsetHeight) {
// window.scrollTo(0, top - elHeight - this.target - this.offset)
// }
// })
// }
// },
onScroll () {
// if (scroll < 0 || !this.visible || this.disabled || (this.showOnUp && !dir)) {
// return
// }
const scrollNotReachedStartPoint = scroll < this.stickyStartPoint
// const scrollIsBehindStartPoint = scroll <= this.stickyStartPoint
// const scrollNotReachedEndPoint = scroll <= this.bottomOffset
// const uikitComplexEval = scrollIsBehindStartPoint || dir === 'down' || (dir === 'up' && !this.isActive && scrollNotReachedEndPoint)
if (this.inactive || scrollNotReachedStartPoint) {
if (!this.isActive) {
return
}
this.isActive = false
if (this.animation && scroll > this.topOffset) {
Animation.cancel(this.$el).then(() =>
Animation.out(this.$el, this.animation).then(() => this.hide())
)
} else {
this.hide()
}
} else if (this.isActive) {
this.update()
} else if (this.animation) {
Animation.cancel(this.$el).then(() => {
this.show()
Animation.in(this.$el, this.animation)
})
} else {
this.show()
}
},
createPlaceholder () {
this.placeholder = document.createElement('div')
addClass(this.placeholder, 'uk-sticky-placeholder')
this.placeholder.setAttribute('hidden', 'hidden')
if (!this.$el.parentNode.contains(this.placeholder)) {
this.$el.parentNode.appendChild(this.placeholder)
}
},
updatePlaceholder () {
css(this.placeholder, 'height', `${this.outerHeight}px`)
css(this.placeholder, 'marginTop', css(this.$el, 'marginTop'))
css(this.placeholder, 'marginBottom', css(this.$el, 'marginBottom'))
css(this.placeholder, 'marginLeft', css(this.$el, 'marginLeft'))
css(this.placeholder, 'marginRight', css(this.$el, 'marginRight'))
},
getElementOffset (el) {
el = isString(el)
? this.$vnode.context.$refs[el]
: el
if (el) {
return offsetTop(el) + el.offsetHeight
}
}
},
mounted () {
// add sticky class
addClass(this.$el, 'uk-sticky')
// calculate offset on load and resize
// this.topOffset = this.isActive
// ? offsetTop(this.placeholder)
// : offsetTop(this.$el)
this.topOffset = offsetTop(this.$el)
// calculate outerHeight
// const outerElement = active
// ? this.placeholder
// : this.$el
// this.outerHeight = css(this.$el, 'position') !== 'absolute'
// ? outerElement.offsetHeight
// : ''
this.outerHeight = this.$el.offsetHeight
this.createPlaceholder()
this.updatePlaceholder()
const active = scroll > this.stickyStartPoint
if (active) {
this.isActive = true
this.update()
} else {
addClass(this.$el, this.clsInactive)
}
}
}
function isVisible (el) {
if (!el) {
return false
}
const elemTop = el.getBoundingClientRect().top
const elemBottom = el.getBoundingClientRect().bottom
const isVisible = (elemTop >= 0) && (elemBottom <= window.innerHeight)
return isVisible
}
function getViewportHeightOffset (height) {
return window.innerHeight * parseFloat(height) / 100
}