@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
JavaScript
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>`);