Created
July 20, 2017 07:57
-
-
Save mbajur/8273745f03846fd153e0f4b3083be9f3 to your computer and use it in GitHub Desktop.
vue-affix with option to delay the initialization
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<div> | |
<slot></slot> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: { | |
/** | |
* The relative element selector string. The relative element is | |
* the element you want your affix to be related to, as it will | |
* not be related to the window. | |
* | |
* @example '#contact' | |
* @type {String} | |
*/ | |
relativeElementSelector: { | |
type: String, | |
required: true | |
}, | |
/** | |
* This is the offset margin between the top/bottom of the window | |
* before the affix is applied. | |
* | |
* @type {Object} | |
*/ | |
offset: { | |
type: Object, | |
default: () => { | |
return { | |
top: 40, | |
bottom: 40 | |
} | |
} | |
}, | |
/** | |
* Checks if the plugin should be enabled/disabled based | |
* on true/false, good for conditional rendering like | |
* mobile behavior. | |
* | |
* @type {Boolean} | |
*/ | |
enabled: { | |
type: Boolean, | |
default: true | |
}, | |
/** | |
* Checks if the plugin should be enabled/disabled when component | |
* is mounted. Good when relativeElement has a dynamic height. | |
* | |
* @type {Boolean} | |
*/ | |
delayed: { | |
type: Boolean, | |
default: false | |
} | |
}, | |
computed: { | |
/** | |
* Computes the relative element selector to a selector. | |
* | |
* @return {Element} document.querySelector(this.relativeElementSelector) | |
*/ | |
relativeElement() { | |
return document.querySelector(this.relativeElementSelector); | |
} | |
}, | |
data() { | |
return { | |
affixedElmOffsetTop: null, | |
affixedElmMarginTop: null, | |
relativeElmEnd: null, | |
lastState: null, | |
currentState: null | |
} | |
}, | |
methods: { | |
start() { | |
this.$el.classList.add('vue-affix'); | |
this.update(); | |
this.onScroll(); | |
document.addEventListener('scroll', this.onScroll); | |
}, | |
update () { | |
this.affixedElmOffsetTop = this.getPosition(this.$el).y; | |
this.affixedElmMarginTop = this.affixedElmOffsetTop - this.getPosition(this.relativeElement).y + this.offset.top; | |
this.relativeElmEnd = this.relativeElement.offsetHeight + this.getPosition(this.relativeElement).y; | |
}, | |
destroy () { | |
this.lastState = null | |
this.currentState = null | |
this.removeClasses() | |
document.removeEventListener('scroll', this.onScroll); | |
}, | |
onScroll() { | |
if (!this.enabled) { | |
this.removeClasses(); | |
return; | |
} | |
let distanceFromTop = window.scrollY; | |
if (this.$el.offsetHeight + this.offset.top > this.relativeElement.offsetHeight) { | |
return; | |
} | |
if (distanceFromTop < this.getPosition(this.relativeElement).y - this.offset.top) { | |
this.currentState = 'affix-top'; | |
if (this.currentState != this.lastState) { | |
// To make sure it will not fire right after component is mounted | |
if (this.lastState) this.$emit('affixtop'); | |
this.removeClasses(); | |
this.$el.classList.add('affix-top'); | |
} | |
} else if (distanceFromTop >= this.getPosition(this.relativeElement).y - this.offset.top && distanceFromTop < this.relativeElmEnd - this.$el.offsetHeight - this.affixedElmMarginTop - this.offset.bottom) { | |
this.currentState = 'affix'; | |
this.$el.style.top = `${this.affixedElmMarginTop}px`; | |
if (this.currentState != this.lastState) { | |
// To make sure it will not fire right after component is mounted | |
if (this.lastState) this.$emit('affix'); | |
this.removeClasses(); | |
this.$el.classList.add('affix'); | |
} | |
} else if (distanceFromTop >= this.relativeElmEnd - this.$el.offsetHeight - this.affixedElmMarginTop - this.offset.bottom) { | |
this.currentState = 'affix-bottom'; | |
this.$el.style.top = `${this.relativeElmEnd - this.offset.bottom - this.$el.offsetHeight - distanceFromTop}px`; | |
if (this.currentState != this.lastState) { | |
// To make sure it will not fire right after component is mounted | |
if (this.lastState) this.$emit('affixbottom'); | |
this.removeClasses(); | |
this.$el.classList.add('affix-bottom'); | |
} | |
} | |
this.lastState = this.currentState; | |
}, | |
removeClasses() { | |
this.$el.classList.remove('affix-top'); | |
this.$el.classList.remove('affix'); | |
this.$el.classList.remove('affix-bottom'); | |
}, | |
getPosition(element) { | |
let xPosition = 0; | |
let yPosition = 0; | |
while (element) { | |
xPosition += (element.offsetLeft); | |
yPosition += (element.offsetTop); | |
element = element.offsetParent; | |
} | |
return { x: xPosition, y: yPosition }; | |
} | |
}, | |
mounted() { | |
if (!this.delayed) this.start() | |
}, | |
beforeDestroy() { | |
this.destroy() | |
} | |
} | |
</script> | |
<style> | |
.affix { | |
position: fixed; | |
} | |
.affix-bottom { | |
position: fixed; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment