vue-sticky-element
Version:
A simple vue sticky component wrapper that will stick to screen when scrolled past it
219 lines (218 loc) • 7.16 kB
JavaScript
import { h, withDirectives as a, cloneVNode as m } from "vue";
import u from "v-scroll-threshold";
function k(e, s) {
if (m)
return m(e, { ...e.props });
const i = e.children && e.children.map((o) => k(o, s)), t = s(e.tag, e.data, i);
return t.text = e.text, t.isComment = e.isComment, t.componentOptions = e.componentOptions, t.elm = e.elm, t.context = e.context, t.ns = e.ns, t.isStatic = e.isStatic, t.key = e.key, t;
}
function v(e, s) {
return typeof a == "function" ? a(e, s) : e;
}
function S(e) {
const s = {
threshold: e.directiveThreshold,
callback: e.toggleStickiness,
scrollBackThreshold: e.scrollBackThreshold,
scrollElement: e.scrollElement
}, i = {
[e.visibleOnDirection]: !0
};
return typeof a == "function" ? [u, s, "", i] : {
name: "scroll-threshold",
value: s,
modifiers: i
};
}
function g(e) {
if (Object.is(e, -0))
return !0;
}
function w(e, s = 500) {
let i;
return (...t) => {
clearTimeout(i), i = setTimeout(() => {
e.apply(null, t);
}, s);
};
}
const y = {
passive: !0
}, b = {
directives: {
"scroll-threshold": u
},
props: {
visibleOnDirection: {
type: String,
default: "up",
validator: (e) => ["up", "down", "disabled"].includes(e)
},
stickMode: {
type: String,
default: "element-end",
validator: (e) => ["element-end", "element-start"].includes(e)
},
stuckClass: {
type: String,
default: "vue-sticky-element--stuck"
},
showClass: {
type: String,
default: "vue-sticky-element--show"
},
hideClass: {
type: String,
default: "vue-sticky-element--hide"
},
transitionClass: {
type: String,
default: "vue-sticky-element--transition"
},
transitionDuration: {
type: Number,
default: 50
},
/** how much user has to scroll back in the opposite direction before element shows again.
* this is especially important on mobile devices, when user is holding touch on screen,
* which causes element to show and hide multiple times in a row.
*
* resets on scroll in opposite direction of `visibleOnDirection`
*/
scrollBackThreshold: {
type: Number,
default: 65
},
/** When true, stops checking for scroll positions (essentially, does not do anything). this can help when you need to freeze and scroll the navbar. */
skipChecks: {
type: Boolean,
default: !1
},
/** force applies the show class */
forceShow: {
type: Boolean,
default: !1
},
/** The element to add `onscroll` event listener to instead of window. this is useful for native apps like ionic where scrolling element might not be window. this can be changed in runtime and the change will be detected, so for example you can get your element in `onMounted` [using `getScrollElement`](https://ionicframework.com/docs/api/content#getscrollelement) and you will be fine. */
scrollElement: {
type: Object,
default: void 0
}
},
emits: ["stuck", "show"],
data() {
return {
navbarStuck: !1,
navbarShow: !1,
applyTransition: !1,
height: void 0,
forceHide: !1,
observer: void 0,
lastScrollPos: void 0,
scrollBackValue: void 0
};
},
computed: {
alwaysStick() {
return this.visibleOnDirection === "disabled";
},
shouldApplyTransition() {
return !this.alwaysStick;
},
stickWithElementStart() {
return this.stickMode === "element-start";
},
directiveThreshold() {
return this.stickWithElementStart ? 0 : this.height || 0;
}
},
mounted() {
const e = () => {
var s;
this.height = (s = this.$el.firstElementChild) == null ? void 0 : s.clientHeight;
};
typeof window < "u" && ("ResizeObserver" in window ? (this.observer = new ResizeObserver(e), this.observer.observe(this.$el)) : (this.observer = w(e), window.addEventListener("resize", this.observer, y))), e();
},
// for vue 3
beforeUnmount() {
this.crossBeforeUnmount();
},
// for vue 2
beforeDestroy() {
this.crossBeforeUnmount();
},
methods: {
addHide() {
this.forceHide = !0;
},
removeHide() {
this.forceHide = !1;
},
toggleStickiness(e, s) {
this.skipChecks || (e < 0 || g(e) ? (this.navbarStuck = !1, this.$emit("stuck", !1), this.shouldApplyTransition && this.$nextTick().then(() => {
this.applyTransition = !1;
})) : e > 0 && (this.height = this.$el ? this.$el.clientHeight : this.height, this.navbarStuck = !0, this.$emit("stuck", !0), this.shouldApplyTransition && this.$nextTick().then(() => {
setTimeout(() => {
this.applyTransition = !0;
}, this.transitionDuration);
})), this.navbarStuck && (s || this.alwaysStick) ? (this.navbarShow = !0, this.$emit("show", !0)) : (this.navbarShow = !1, this.$emit("show", !1)));
},
crossBeforeUnmount() {
this.observer && ("disconnect" in this.observer ? (this.observer.disconnect(), this.observer = void 0) : window.removeEventListener("resize", this.observer, y));
}
},
render(e) {
const s = h ? h : e;
let i;
if ("$scopedSlots" in this ? i = this.$scopedSlots.default() : "$slots" in this && (i = this.$slots.default()), !(i && i[0]))
return h ? null : e();
const t = k(i[0], s), o = {
"vue-sticky-element": !0,
[this.stuckClass]: this.navbarStuck,
[this.showClass]: this.navbarShow || this.forceShow,
[this.hideClass]: this.forceHide,
[this.transitionClass]: this.applyTransition
};
t.props ? (t.props.class ? typeof t.props.class == "string" && (t.props.class = t.props.class.split(" ")) : t.props.class = [], Array.isArray(t.props.class) && (t.props.class = t.props.class.reduce(
(r, l) => (r[l] = !0, r),
{}
)), t.props.class = {
...t.props.class,
...o
}, t.props.class = Object.entries(t.props.class).map(([r, l]) => l ? r : null).filter((r) => r).join(" ")) : t.data && (t.data.class ? typeof t.data.class == "string" && (t.data.class = t.data.class.split(" ")) : t.data.class = [], Array.isArray(t.data.class) && (t.data.class = t.data.class.reduce(
(r, l) => (r[l] = !0, r),
{}
)), t.data.class = {
...t.data.class,
...o
}, t.data.class = Object.entries(t.data.class).map(([r, l]) => l ? r : null).filter((r) => r).join(" "));
const d = {}, f = {
"pointer-events": "none"
}, p = [S(this)];
return this.height && (d.height = `${this.height}px`, f.height = `${this.height}px`), v(
s(
"div",
{
style: d,
...typeof a != "function" ? { directives: p } : {}
},
[t, s("div", { style: f })]
),
p
);
}
};
const c = function(s) {
c.installed || (c.installed = !0, s.component("VueStickyElement", b), s.use(u));
}, T = {
install: c
};
let n = null;
typeof window < "u" ? n = window.Vue : typeof global < "u" && (n = global.Vue);
n && n.use && n.use(T);
b.install = c;
export {
b as VueStickyElement,
b as default,
c as install
};