UNPKG

@exadel/esl

Version:

Exadel Smart Library (ESL) is the lightweight custom elements library that provide a set of super-flexible components

232 lines (231 loc) 8.08 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ESLAnchornav_1; import { ExportNs } from '../../esl-utils/environment/export-ns'; import { ESLBaseElement } from '../../esl-base-element/core'; import { attr, decorate, listen, memoize, prop, ready } from '../../esl-utils/decorators'; import { debounce, microtask } from '../../esl-utils/async'; import { getViewportForEl } from '../../esl-utils/dom/scroll'; import { ESLEventUtils, ESLIntersectionTarget } from '../../esl-event-listener/core'; import { ESLAnchor } from './esl-anchor'; /** * ESLAnchornav * @author Dmytro Shovchko * * ESLAnchornav is a component that collects content anchors from the page and provides anchor navigation */ let ESLAnchornav = ESLAnchornav_1 = class ESLAnchornav extends ESLBaseElement { constructor() { super(...arguments); this._anchors = []; this._items = new Map(); } /** Gets renderer by name */ static getRenderer(name) { return this._renderers.get(name); } static setRenderer(name, renderer) { if (typeof name !== 'string') return this.setRenderer('default', name); if (typeof name === 'string' && renderer) this._renderers.set(name, renderer); } /** Active anchor */ get active() { return this._active; } set active(value) { if (this._active === value) return; this._active = value; this._onActiveChange(); } /** Anchors list */ get $anchors() { return this._anchors.map(({ $anchor }) => $anchor); } /** Anchornav offset */ get offset() { return this._offset || 0; } set offset(value) { if (this._offset === value) return; this._offset = value; memoize.clear(this, '$viewport'); this.$$on(this._onAnchorIntersection); } /** Anchornav items container */ get $itemsArea() { const $provided = this.querySelector(`[${this.baseTagName}-items]`); if ($provided) return $provided; const $container = document.createElement('div'); $container.setAttribute(this.baseTagName + '-items', ''); this.appendChild($container); return $container; } /** Anchornav viewport (root element for IntersectionObservers checking visibility) */ get $viewport() { return getViewportForEl(this); } /** Permanent anchors to prepend to the list */ get anchorsToPrepend() { return []; } /** Permanent anchors to append to the list */ get anchorsToAppend() { return []; } connectedCallback() { super.connectedCallback(); this._onAnchornavRequest(); } /** Updates the component */ update() { memoize.clear(this, '$viewport'); this.rerender(); this.$$on(this._onAnchorIntersection); this.updateActiveAnchor(); this._onUpdateEvent(); } /** Builds the component anchors list markup */ rerender() { const { $itemsArea } = this; const anchors = this.renderAnchors(); $itemsArea.replaceChildren(...anchors); } // TODO: move to esl-utils helpers /** Converts html string to Element */ htmlToElement(html) { return (new DOMParser()).parseFromString(html, 'text/html').body.children[0]; } /** Renders the component anchors list */ renderAnchors() { const itemRenderer = ESLAnchornav_1.getRenderer(this.rendererName); this._items.clear(); return itemRenderer ? this._anchors.map((anchor, index) => { let item = itemRenderer(anchor, index, this); if (typeof item === 'string') item = this.htmlToElement(item); this._items.set(anchor.id, item); return item; }) : []; } /** Gets anchor data from the anchor element */ getDataFrom($anchor, index) { return { id: $anchor.id, title: $anchor.title, $anchor }; } /** Gets initial active anchor */ getInitialActive() { return this._anchors[0]; } /** Updates the active anchor */ updateActiveAnchor() { let active = this.getInitialActive(); const topBoundary = (this.$viewport ? this.$viewport.getBoundingClientRect().y : 0) + this.offset + 1; this._anchors.forEach((item) => { const { y } = item.$anchor.getBoundingClientRect(); if (y <= topBoundary) active = item; }); if (active) { this._items.forEach(($item, id) => $item.classList.toggle(this.activeClass, id === active.id)); this.active = active; } } /** Handles changing the active anchor */ _onActiveChange() { const detail = { id: this.active.id }; ESLEventUtils.dispatch(this, this.ACTIVECHANGED_EVENT, { detail }); } /** Handles updating the component */ _onUpdateEvent() { ESLEventUtils.dispatch(this, this.UPDATED_EVENT); } _onAnchornavRequest() { this._anchors = [...document.querySelectorAll(this.ANCHOR_SELECTOR)].map(this.getDataFrom); this._anchors.unshift(...this.anchorsToPrepend); this._anchors.push(...this.anchorsToAppend); this.update(); } _onAnchorIntersection(e) { this.updateActiveAnchor(); } _onAnchorClick(event) { this.updateActiveAnchor(); } }; ESLAnchornav.is = 'esl-anchornav'; ESLAnchornav._renderers = new Map(); __decorate([ prop('esl:anchornav:activechanged') ], ESLAnchornav.prototype, "ACTIVECHANGED_EVENT", void 0); __decorate([ prop('esl:anchornav:updated') ], ESLAnchornav.prototype, "UPDATED_EVENT", void 0); __decorate([ prop('[esl-anchor]') ], ESLAnchornav.prototype, "ANCHOR_SELECTOR", void 0); __decorate([ prop([0, 0.01, 0.99, 1]) ], ESLAnchornav.prototype, "INTERSECTION_THRESHOLD", void 0); __decorate([ attr({ defaultValue: 'default', name: 'renderer' }) ], ESLAnchornav.prototype, "rendererName", void 0); __decorate([ attr({ defaultValue: 'active' }) ], ESLAnchornav.prototype, "activeClass", void 0); __decorate([ memoize() ], ESLAnchornav.prototype, "$itemsArea", null); __decorate([ memoize() ], ESLAnchornav.prototype, "$viewport", null); __decorate([ ready ], ESLAnchornav.prototype, "connectedCallback", null); __decorate([ decorate(debounce, 50) ], ESLAnchornav.prototype, "updateActiveAnchor", null); __decorate([ decorate(microtask) ], ESLAnchornav.prototype, "_onActiveChange", null); __decorate([ decorate(microtask) ], ESLAnchornav.prototype, "_onUpdateEvent", null); __decorate([ listen({ event: ESLAnchor.prototype.CHANGE_EVENT, target: document.body }) ], ESLAnchornav.prototype, "_onAnchornavRequest", null); __decorate([ listen({ event: 'intersects', target: (that) => ESLIntersectionTarget.for(that.$anchors, { root: that.$viewport, threshold: that.INTERSECTION_THRESHOLD, rootMargin: `-${that.offset + 1}px 0px 0px 0px` }) }) ], ESLAnchornav.prototype, "_onAnchorIntersection", null); __decorate([ listen({ event: 'click', selector: 'a' }) ], ESLAnchornav.prototype, "_onAnchorClick", null); ESLAnchornav = ESLAnchornav_1 = __decorate([ ExportNs('Anchornav') ], ESLAnchornav); export { ESLAnchornav }; ESLAnchornav.setRenderer((data) => `<a class="esl-anchornav-item" href="#${data.id}">${data.title}</a>`);