Post

Web Component: PageIndex

Harianto van Insulinde

Source

js
class PageIndex extends HTMLElement {
    constructor() {
        // Always call super first in constructor
        super()

        const d = document,
            options = () => {
                const attr = {}
                for (let { name, value } of this.attributes) {
                    attr[name] = value
                }
                return attr
            },
            rootEl = () => {
                const t = d.documentElement || d.body.parentNode
                return t && typeof t.scrollTop == 'number' ? t : d.body
            },
            setStyle = (el, style) => {
                for (let [key, value] of Object.entries(style)) {
                    el.style[key] = value
                }
            },
            pushState = scrollTop => {
                const pageIndex = { scrollTop: scrollTop || rootEl().scrollTop }
                history.pushState({ pageIndex }, d.title)
            },
            getOffsetScrollTop = (el, relativeEl) =>
                el.getBoundingClientRect().top -
                (relativeEl || d.body).getBoundingClientRect().top -
                (options().offset
                    ? options().offset
                    : d.querySelector('.site-header .outer')
                    ? d.querySelector('.site-header .outer').getBoundingClientRect()
                            .height
                    : 0)

        // Events
        const onDOMContentLoaded = event => {
            const { element } = options()
            if (!element) return
            this.classList.add('page-index')

            const headers = d
                .querySelector(element)
                .querySelectorAll('h1, h2, h3, h4, h5, h6')

            const ul = this.appendChild(d.createElement('ul'))
            ul.classList.add('page-index__list')
            Array.from(headers).forEach(el => {
                let li = ul.appendChild(d.createElement('li')),
                    a = li.appendChild(d.createElement('a'))
                li.classList.add('page-index__list-item')
                if (el.id) a.href = `#${el.id}`
                a.innerHTML = el.innerHTML
                a.title = el.innerText
                a.classList.add(`page-index--${el.tagName}`)
                a.$el = el
            })

            ul.addEventListener('click', event => {
                event.preventDefault()
                const { target } = event
                console.log({ target, closest: target.closest('a') })
                const a = target.closest('a')
                pushState(getOffsetScrollTop(a.$el))
                rootEl().scrollTop = getOffsetScrollTop(a.$el)

                a.$el.classList.add('page-index--visited')
            })
        }
        const onPopstate = event => {
            const { state } = event
            state &&
                state.pageIndex &&
                (rootEl().scrollTop = state.pageIndex.scrollTop)
        }

        window.addEventListener('popstate', onPopstate)

        // Init
        if (
            d.readyState === 'complete' ||
            d.readyState === 'loaded' ||
            d.readyState === 'interactive'
        ) {
            // document has at least been parsed
            onDOMContentLoaded()
            history.replaceState(
                { pageIndex: { scrollTop: rootEl().scrollTop } },
                d.title
            )
        }
    }
}

// Define the new element
customElements.define('page-index', PageIndex)

JavaScript

Example

Style

css
:root {
    scroll-behavior: smooth;
}

Web component

html
<page-index element=".post-full .post-content"></page-index>
  • element - The parent element that contains h1, h2, h3, h4, h5, h6 elements
  • offset (optional) - default: 0; offsetTop scroll height