UNPKG

@sandlada/mdc

Version:

@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.

14 lines 10.8 kB
import{property as e}from"../../../node_modules/@lit/reactive-element/decorators/property.js";import{state as t}from"../../../node_modules/@lit/reactive-element/decorators/state.js";import{query as n}from"../../../node_modules/@lit/reactive-element/decorators/query.js";import{queryAssignedElements as r}from"../../../node_modules/@lit/reactive-element/decorators/query-assigned-elements.js";import"../../../node_modules/lit/decorators.js";import{html as i,nothing as a}from"../../../node_modules/lit-html/lit-html.js";import{LitElement as o}from"../../../node_modules/lit-element/lit-element.js";import"../../../node_modules/lit/index.js";import{classMap as s}from"../../../node_modules/lit-html/directives/class-map.js";import"../../../node_modules/lit/directives/class-map.js";import{mixinElementInternals as c}from"../../../utils/behaviors/element-internals.js";import{composeMixin as l}from"../../../utils/compose-mixin/compose-mixin.js";import u from"../../../_virtual/_@oxc-project_runtime@0.144.0/helpers/esm/decorate.js";import{mixinDelegatesAria as d}from"../../../utils/aria/delegate.js";import"./base-on-this-page-item.js"; /** * @license * Copyright 2026 Kai-Orion & Sandlada * SPDX-License-Identifier: MIT * * @fileoverview * Internal base class for `mdc-on-this-page` — an in-page table of contents / anchor navigation. * * Automatically tracks page scroll position, highlights the active section in view, * and renders a smoothly animated outline pill indicator that matches the active item's exact bounds. */ const f=`mdc-on-this-page-item, [mdc-on-this-page-item]`;function p(e){return e.toLowerCase().trim().replace(/[\s\W-]+/g,`-`).replace(/^-+|-+$/g,``)}var m=class extends l(d,c)(o){constructor(){super(),this.caption=`On this page`,this.headline=``,this.activeHref=``,this.activeIndex=0,this.scrollTarget=`window`,this.scrollOffset=96,this.updateHash=!0,this.contentSelector=``,this.autoDiscover=!1,this.indicatorFit=`content`,this.discoveredItems=[],this.items=[],this.resizeObserver=null,this.isManualScrolling=!1,this.manualScrollTimeout=null,this.rafId=null,this.handleSlotChange=()=>{this.syncItems(),this.updateIndicatorPosition(!1)},this.handleItemClick=e=>{let t=e.detail.item;if(!(!t||t.disabled)&&(e.preventDefault(),this.scrollToTarget(t),this.activateItem(t,!0),this.updateHash&&t.href)){let e=t.href.startsWith(`#`)?t.href:`#${t.href}`;window.location.hash!==e&&history.pushState(null,``,e)}},this.onScrollThrottled=()=>{this.rafId===null&&(this.rafId=requestAnimationFrame(()=>{this.rafId=null,this.handleScroll()}))},this.handleKeyDown=e=>{if(this.items.length===0)return;let{key:t}=e,n=NaN;if(t===`ArrowDown`?n=(this.activeIndex+1)%this.items.length:t===`ArrowUp`?n=(this.activeIndex-1+this.items.length)%this.items.length:t===`Home`?n=0:t===`End`&&(n=this.items.length-1),!isNaN(n)){e.preventDefault();let t=this.items[n];t&&(t.focus(),this.scrollToTarget(t),this.activateItem(t,!0))}},this.addEventListener(`keydown`,this.handleKeyDown),this.addEventListener(`mdc-on-this-page-item-click`,this.handleItemClick)}connectedCallback(){super.connectedCallback(),this.setupScrollListener(),this.setupResizeObserver(),(this.autoDiscover||this.contentSelector)&&this.discoverHeadings(),requestAnimationFrame(()=>{this.syncItems(),this.handleScroll(),this.updateIndicatorPosition(!1)})}disconnectedCallback(){super.disconnectedCallback(),this.cleanupScrollListener(),this.resizeObserver&&=(this.resizeObserver.disconnect(),null),this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null),this.manualScrollTimeout!==null&&(window.clearTimeout(this.manualScrollTimeout),this.manualScrollTimeout=null)}updated(e){super.updated(e),e.has(`scrollTarget`)&&(this.cleanupScrollListener(),this.setupScrollListener()),(e.has(`contentSelector`)||e.has(`autoDiscover`))&&(this.autoDiscover||this.contentSelector)&&this.discoverHeadings(),(e.has(`activeIndex`)||e.has(`indicatorFit`))&&(this.applyActiveState(this.activeIndex,!1),this.updateIndicatorPosition(!0))}getRenderClasses(){return{container:!0,[`fit-${this.indicatorFit}`]:!0}}render(){return i`<nav class="${s(this.getRenderClasses())}" role="navigation" aria-label="${this.caption||`On this page`}"><header class="header"><slot name="caption">${this.caption?i`<span class="caption" part="caption">${this.caption}</span>`:a}</slot><slot name="headline">${this.headline?i`<h2 class="headline" part="headline">${this.headline}</h2>`:a}</slot></header><div class="items" role="list"><div class="indicator" aria-hidden="true" part="indicator"></div><slot @slotchange="${this.handleSlotChange}">${this.discoveredItems.map((e,t)=>i`<mdc-on-this-page-item href="#${e.id}" .label="${e.label}" .level="${e.level}" .index="${t}" .active="${t===this.activeIndex}">${e.label}</mdc-on-this-page-item>`)}</slot></div></nav>`}get activeItem(){return this.items[this.activeIndex]??null}setActiveIndex(e,t=!1){e<0||e>=this.items.length||this.activateItem(this.items[e],t)}setActiveHref(e,t=!1){let n=e.startsWith(`#`)?e.slice(1):e,r=this.items.find(t=>t.targetId===n||t.href===e||t.href===`#${n}`);r&&this.activateItem(r,t)}async updateIndicatorPosition(e=!0){if(!this.indicatorElement||!this.itemsContainerElement)return;let t=this.activeItem;if(!t||this.items.length===0){this.indicatorElement.style.opacity=`0`;return}await t.updateComplete;let n=this.itemsContainerElement.getBoundingClientRect(),r=t.getItemBounds();if(!r||r.width===0||r.height===0){this.indicatorElement.style.opacity=`0`;return}let i=r.left-n.left,a=r.top-n.top,o=r.width,s=r.height;e?this.indicatorElement.style.removeProperty(`transition`):this.indicatorElement.style.transition=`none`,this.indicatorElement.style.transform=`translate3d(${i.toFixed(2)}px, ${a.toFixed(2)}px, 0)`,this.indicatorElement.style.width=`${o.toFixed(2)}px`,this.indicatorElement.style.height=`${s.toFixed(2)}px`,this.indicatorElement.style.opacity=`1`,e||requestAnimationFrame(()=>{this.indicatorElement?.style.removeProperty(`transition`)})}syncItems(){let e=this.assignedItems;if(e.length>0)this.items=e;else{let e=Array.from(this.renderRoot.querySelectorAll(f));this.items=e}for(let[e,t]of this.items.entries())t.index=e,t.active=e===this.activeIndex,this.resizeObserver&&this.resizeObserver.observe(t);if(this.activeHref&&this.items.length>0){let e=this.items.findIndex(e=>e.href===this.activeHref||e.targetId===this.activeHref.replace(/^#/,``));e!==-1&&(this.activeIndex=e,this.applyActiveState(e,!1))}}applyActiveState(e,t){for(let[t,n]of this.items.entries())n.active=t===e;let n=this.items[e]??null;n&&(this.activeHref=n.href),this.dispatchEvent(new CustomEvent(`change`,{bubbles:!0,composed:!0,cancelable:!0,detail:{item:n,index:e,href:n?.href??``,target:n?.targetId??``,userInitiated:t}}))}activateItem(e,t){let n=e.index;n===this.activeIndex&&e.active||(this.activeIndex=n,this.activeHref=e.href,this.applyActiveState(n,t),this.updateIndicatorPosition(!0))}scrollToTarget(e){let t=e.targetId;if(!t)return;let n=document.getElementById(t)||document.querySelector(`[name="${CSS.escape(t)}"]`);if(!n)return;let r=this.getScrollContainer();if(this.isManualScrolling=!0,this.manualScrollTimeout!==null&&window.clearTimeout(this.manualScrollTimeout),this.manualScrollTimeout=window.setTimeout(()=>{this.isManualScrolling=!1,this.handleScroll()},750),r===window){let e=n.getBoundingClientRect(),t=(window.scrollY||window.pageYOffset)+e.top-this.scrollOffset;window.scrollTo({top:Math.max(0,t),behavior:`smooth`})}else if(r instanceof HTMLElement){let e=r.getBoundingClientRect(),t=n.getBoundingClientRect(),i=r.scrollTop+(t.top-e.top)-this.scrollOffset;r.scrollTo({top:Math.max(0,i),behavior:`smooth`})}}getScrollContainer(){return typeof this.scrollTarget==`string`?this.scrollTarget===`window`?window:document.querySelector(this.scrollTarget)||window:this.scrollTarget||window}setupScrollListener(){this.getScrollContainer().addEventListener(`scroll`,this.onScrollThrottled,{passive:!0})}cleanupScrollListener(){this.getScrollContainer().removeEventListener(`scroll`,this.onScrollThrottled)}setupResizeObserver(){if(!(typeof ResizeObserver>`u`)){this.resizeObserver=new ResizeObserver(()=>{this.updateIndicatorPosition(!1)}),this.itemsContainerElement&&this.resizeObserver.observe(this.itemsContainerElement);for(let e of this.items)this.resizeObserver.observe(e);this.resizeObserver.observe(this)}}handleScroll(){if(this.isManualScrolling||this.items.length===0)return;let e=this.getScrollContainer(),t=e===window,n=t?window.scrollY||window.pageYOffset:e.scrollTop,r=t?window.innerHeight:e.clientHeight,i=t?document.documentElement.scrollHeight:e.scrollHeight;if(n+r>=i-30){let e=this.items.length-1;this.activeIndex!==e&&(this.activeIndex=e,this.applyActiveState(e,!1),this.updateIndicatorPosition(!0));return}if(n<=10){this.activeIndex!==0&&(this.activeIndex=0,this.applyActiveState(0,!1),this.updateIndicatorPosition(!0));return}let a=0,o=1/0;for(let[r,i]of this.items.entries()){let s=i.targetId;if(!s)continue;let c=document.getElementById(s)||document.querySelector(`[name="${CSS.escape(s)}"]`);if(!c)continue;let l=c.getBoundingClientRect(),u=0;if(t)u=l.top+window.scrollY;else{let t=e.getBoundingClientRect();u=e.scrollTop+(l.top-t.top)}let d=n+this.scrollOffset-u;d>=-10&&d<o&&(o=d,a=r)}a!==this.activeIndex&&(this.activeIndex=a,this.applyActiveState(a,!1),this.updateIndicatorPosition(!0))}discoverHeadings(){let e=this.contentSelector?document.querySelector(this.contentSelector):document.querySelector(`main, article, [role="main"]`);if(!e)return;let t=Array.from(e.querySelectorAll(`h2, h3, h4`)),n=[];for(let e of t){let t=e.id;t||(t=p(e.textContent||``),e.id=t);let r=e.tagName===`H2`?1:e.tagName===`H3`?2:3;n.push({id:t,label:e.textContent?.trim()||``,level:r})}this.discoveredItems=n}};u([e({type:String})],m.prototype,`caption`,void 0),u([e({type:String})],m.prototype,`headline`,void 0),u([e({type:String,reflect:!0,attribute:`active-href`})],m.prototype,`activeHref`,void 0),u([e({type:Number,reflect:!0,attribute:`active-index`})],m.prototype,`activeIndex`,void 0),u([e({attribute:`scroll-target`})],m.prototype,`scrollTarget`,void 0),u([e({type:Number,attribute:`scroll-offset`})],m.prototype,`scrollOffset`,void 0),u([e({type:Boolean,attribute:`update-hash`})],m.prototype,`updateHash`,void 0),u([e({type:String,attribute:`content-selector`})],m.prototype,`contentSelector`,void 0),u([e({type:Boolean,attribute:`auto-discover`})],m.prototype,`autoDiscover`,void 0),u([e({type:String,reflect:!0,attribute:`indicator-fit`})],m.prototype,`indicatorFit`,void 0),u([n(`.items`)],m.prototype,`itemsContainerElement`,void 0),u([n(`.indicator`)],m.prototype,`indicatorElement`,void 0),u([r({selector:f,flatten:!0})],m.prototype,`assignedItems`,void 0),u([t()],m.prototype,`discoveredItems`,void 0);export{m as BaseOnThisPage}; //# sourceMappingURL=base-on-this-page.js.map