UNPKG

vue-sticky-directive-ts

Version:

A powerful vue directive make element sticky.

277 lines (276 loc) 8.7 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; import { nextTick } from "vue"; const StickyNamespace = "@@vue-sticky-directive"; const events = [ "resize", "scroll", "touchstart", "touchmove", "touchend", "pageshow", "load" ]; const stylePropWithPixel = [ "width", "top", "left", "right", "bottom", "paddingTop" ]; const DefaultOptions = { topOffset: 0, bottomOffset: 0, shouldTopSticky: false, shouldBottomSticky: false, zIndex: 10, on: () => { } }; function convertProp(name, prop) { if (typeof prop === "string") { return prop; } prop = prop.toString(10); return stylePropWithPixel.includes(name) ? `${prop}px` : prop; } function batchStyle(el, style = {}, className = {}) { if (!el) { return; } for (const k in style) { el.style.setProperty(k, convertProp(k, style[k])); } for (const k in className) { if (className[k] && !el.classList.contains(k)) { el.classList.add(k); } else if (!className[k] && el.classList.contains(k)) { el.classList.remove(k); } } } class Sticky { constructor(el, options = {}) { __publicField(this, "el"); __publicField(this, "containerEl"); __publicField(this, "placeholderEl"); __publicField(this, "listeners", []); __publicField(this, "isPending", false); __publicField(this, "state"); __publicField(this, "callbackState"); __publicField(this, "options", DefaultOptions); this.el = el; this.state = { isTopSticky: false, isBottomSticky: false, height: 0, width: 0, xOffset: 0, placeholderElTop: 0, containerElTop: 0, containerElBottom: 0 }; this.callbackState = { top: false, bottom: false, sticked: false }; this.setOptions(options); this.containerEl = this.getContainerEl(); const parent = this.el.parentNode || this.containerEl; this.placeholderEl = document.createElement("div"); parent.insertBefore(this.placeholderEl, this.el); this.setListeners().then(() => { }, () => { }); } setOptions(options = {}) { const side = options.side && ["top", "bottom", "both"].includes(options.side) ? options.side : "top"; this.options = { topOffset: Number(options.topOffset) || DefaultOptions.topOffset, bottomOffset: Number(options.bottomOffset) || DefaultOptions.bottomOffset, shouldTopSticky: side === "top" || side === "both", shouldBottomSticky: side === "bottom" || side === "both", zIndex: Number(options.zIndex) || DefaultOptions.zIndex, on: options.on || DefaultOptions.on }; } async setListeners() { if (this.listeners.length > 0) { return; } const fn = this.update.bind(this); await nextTick(() => { const containers = [window, this.containerEl]; events.forEach((event) => { containers.forEach((container) => { this.listeners.push(() => container == null ? void 0 : container.removeEventListener(event, fn)); container == null ? void 0 : container.addEventListener(event, fn, { passive: true }); }); }); }); } doUnbind() { this.listeners.forEach((fn) => fn()); this.listeners = []; this.resetElement(); } updateOptions(options = {}) { this.setOptions(options); this.recomputeState(); this.updateElements(); } update() { if (!this.isPending) { requestAnimationFrame(() => { this.isPending = false; this.recomputeState(); this.updateElements(); }); this.isPending = true; } } isTopSticky() { if (!this.options.shouldTopSticky) { return false; } const fromTop = this.state.placeholderElTop; const fromBottom = this.state.containerElBottom; const topBreakpoint = this.options.topOffset; const bottomBreakpoint = this.options.bottomOffset; return fromTop <= topBreakpoint && fromBottom >= bottomBreakpoint; } isBottomSticky() { if (!this.options.shouldBottomSticky) { return false; } const fromBottom = window.innerHeight - this.state.placeholderElTop - this.state.height; const fromTop = window.innerHeight - this.state.containerElTop; const topBreakpoint = this.options.topOffset; const bottomBreakpoint = this.options.bottomOffset; return fromBottom <= bottomBreakpoint && fromTop >= topBreakpoint; } recomputeState() { const elRect = this.el.getBoundingClientRect(); const placeholderElRect = this.placeholderEl.getBoundingClientRect(); const containerElRect = this.containerEl.getBoundingClientRect(); this.state = Object.assign({}, this.state, { height: elRect.height, width: placeholderElRect.width, xOffset: placeholderElRect.left, placeholderElTop: placeholderElRect.top, containerElTop: containerElRect.top, containerElBottom: containerElRect.bottom }); this.state.isTopSticky = this.isTopSticky(); this.state.isBottomSticky = this.isBottomSticky(); } fireEvents() { if (typeof this.options.on === "function" && (this.callbackState.top !== this.state.isTopSticky || this.callbackState.bottom !== this.state.isBottomSticky || this.callbackState.sticked !== (this.state.isTopSticky || this.state.isBottomSticky))) { this.callbackState = { top: this.state.isTopSticky, bottom: this.state.isBottomSticky, sticked: this.state.isBottomSticky || this.state.isTopSticky }; this.options.on(); } } updateElements() { const placeholderStyle = { paddingTop: 0 }; const elStyle = { position: "static", top: "auto", bottom: "auto", left: "auto", width: "auto", zIndex: this.options.zIndex }; const placeholderClassName = { "vue-sticky-placeholder": true }; const elClassName = { "vue-sticky-el": true, "top-sticky": false, "bottom-sticky": false }; const limit = this.state.height + this.options.bottomOffset + this.options.topOffset; if (this.state.isTopSticky || this.state.isBottomSticky) { elStyle.position = "fixed"; elStyle.left = this.state.xOffset; elStyle.width = this.state.width; placeholderStyle.paddingTop = this.state.height; } else { placeholderStyle.paddingTop = 0; } if (this.state.isTopSticky) { elClassName["top-sticky"] = true; elStyle.top = this.options.topOffset; const bottomLimit = this.state.containerElBottom - limit; if (bottomLimit < 0) { elStyle.top += bottomLimit; } } if (this.state.isBottomSticky) { elClassName["bottom-sticky"] = true; elStyle.bottom = this.options.bottomOffset; const topLimit = window.innerHeight - this.state.containerElTop - limit; if (topLimit < 0) { elStyle.bottom += topLimit; } } batchStyle(this.el, elStyle, elClassName); batchStyle(this.placeholderEl, placeholderStyle, placeholderClassName); this.fireEvents(); } resetElement() { var _a, _b; ["position", "top", "bottom", "left", "width", "zIndex"].forEach((attr) => { this.el.style.removeProperty(attr); }); this.el.classList.remove("bottom-sticky", "top-sticky"); (_b = (_a = this.placeholderEl) == null ? void 0 : _a.parentNode) == null ? void 0 : _b.removeChild(this.placeholderEl); } getContainerEl() { var _a; let node = this.el.parentElement; while (node && node.tagName !== "HTML" && node.tagName !== "BODY" && node.nodeType === 1) { if (node.hasAttribute("sticky-container")) { return node; } node = node.parentElement; } return (_a = this.el.parentElement) != null ? _a : document.body; } } const directiveHooks = { mounted: "mounted", updated: "updated", beforeUnmount: "beforeUnmount" }; const directive = { [directiveHooks.mounted](el, bind) { if (bind.value === void 0 || bind.value) { el[StickyNamespace] = new Sticky(el, bind.value); } }, [directiveHooks.updated](el, bind) { if (el[StickyNamespace]) { el[StickyNamespace].updateOptions(bind.value); } }, [directiveHooks.beforeUnmount](el) { if (el[StickyNamespace]) { el[StickyNamespace].doUnbind(); el[StickyNamespace] = void 0; } } }; const plugin = { install: (app) => { app.directive("Sticky", directive); } }; export { Sticky, plugin as default };