UNPKG

@blackbaud/skyux-lib-stache

Version:

This library was generated with [Nx](https://nx.dev).

1 lines 196 kB
{"version":3,"file":"blackbaud-skyux-lib-stache.mjs","sources":["../../../../libs/stache/src/lib/modules/auth/auth.service.ts","../../../../libs/stache/src/lib/modules/shared/input-converter.ts","../../../../libs/stache/src/lib/modules/router/route-options.ts","../../../../libs/stache/src/lib/modules/router/sort.ts","../../../../libs/stache/src/lib/modules/router/route.service.ts","../../../../libs/stache/src/lib/modules/router/router.module.ts","../../../../libs/stache/src/lib/modules/shared/sky-stache-resources.module.ts","../../../../libs/stache/src/lib/modules/shared/window-ref.ts","../../../../libs/stache/src/lib/modules/nav/nav.service.ts","../../../../libs/stache/src/lib/modules/nav/link.directive.ts","../../../../libs/stache/src/lib/modules/nav/nav.component.ts","../../../../libs/stache/src/lib/modules/nav/nav.component.html","../../../../libs/stache/src/lib/modules/nav/nav.module.ts","../../../../libs/stache/src/lib/modules/action-buttons/action-buttons.component.ts","../../../../libs/stache/src/lib/modules/action-buttons/action-buttons.component.html","../../../../libs/stache/src/lib/modules/action-buttons/action-buttons.module.ts","../../../../libs/stache/src/lib/modules/shared/omnibar-adapter.service.ts","../../../../libs/stache/src/lib/modules/affix/affix-top.directive.ts","../../../../libs/stache/src/lib/modules/affix/affix.component.ts","../../../../libs/stache/src/lib/modules/affix/affix.component.html","../../../../libs/stache/src/lib/modules/affix/affix.module.ts","../../../../libs/stache/src/lib/modules/analytics/google-analytics.directive.ts","../../../../libs/stache/src/lib/modules/analytics/analytics.module.ts","../../../../libs/stache/src/lib/modules/back-to-top/back-to-top.component.ts","../../../../libs/stache/src/lib/modules/back-to-top/back-to-top.component.html","../../../../libs/stache/src/lib/modules/back-to-top/back-to-top.module.ts","../../../../libs/stache/src/lib/modules/blockquote/blockquote.component.ts","../../../../libs/stache/src/lib/modules/blockquote/blockquote.component.html","../../../../libs/stache/src/lib/modules/blockquote/blockquote.module.ts","../../../../libs/stache/src/lib/modules/breadcrumbs/breadcrumbs.component.ts","../../../../libs/stache/src/lib/modules/breadcrumbs/breadcrumbs.component.html","../../../../libs/stache/src/lib/modules/breadcrumbs/breadcrumbs.module.ts","../../../../libs/stache/src/lib/modules/edit-button/edit-button.component.ts","../../../../libs/stache/src/lib/modules/edit-button/edit-button.component.html","../../../../libs/stache/src/lib/modules/edit-button/edit-button.module.ts","../../../../libs/stache/src/lib/modules/footer/footer.component.ts","../../../../libs/stache/src/lib/modules/footer/footer.component.html","../../../../libs/stache/src/lib/modules/footer/footer.module.ts","../../../../libs/stache/src/lib/modules/hide-from-search/hide-from-search.component.ts","../../../../libs/stache/src/lib/modules/hide-from-search/hide-from-search.component.html","../../../../libs/stache/src/lib/modules/hide-from-search/hide-from-search.module.ts","../../../../libs/stache/src/lib/modules/json-data/json-data-service-config-token.ts","../../../../libs/stache/src/lib/modules/json-data/json-data.service.ts","../../../../libs/stache/src/lib/modules/page-anchor/page-anchor.service.ts","../../../../libs/stache/src/lib/modules/page-anchor/page-anchor.component.ts","../../../../libs/stache/src/lib/modules/page-anchor/page-anchor.component.html","../../../../libs/stache/src/lib/modules/page-anchor/page-anchor.module.ts","../../../../libs/stache/src/lib/modules/page-header/page-header.component.ts","../../../../libs/stache/src/lib/modules/page-header/page-header.component.html","../../../../libs/stache/src/lib/modules/page-header/page-title.component.ts","../../../../libs/stache/src/lib/modules/page-header/page-title.component.html","../../../../libs/stache/src/lib/modules/page-header/page-header.module.ts","../../../../libs/stache/src/lib/modules/page-summary/page-summary.component.ts","../../../../libs/stache/src/lib/modules/page-summary/page-summary.component.html","../../../../libs/stache/src/lib/modules/page-summary/page-summary.module.ts","../../../../libs/stache/src/lib/modules/sidebar/sidebar.component.ts","../../../../libs/stache/src/lib/modules/sidebar/sidebar.component.html","../../../../libs/stache/src/lib/modules/sidebar/sidebar-wrapper.component.ts","../../../../libs/stache/src/lib/modules/sidebar/sidebar-wrapper.component.html","../../../../libs/stache/src/lib/modules/sidebar/sidebar.module.ts","../../../../libs/stache/src/lib/modules/table-of-contents/table-of-contents.component.ts","../../../../libs/stache/src/lib/modules/table-of-contents/table-of-contents.component.html","../../../../libs/stache/src/lib/modules/table-of-contents/table-of-contents-wrapper.component.ts","../../../../libs/stache/src/lib/modules/table-of-contents/table-of-contents-wrapper.component.html","../../../../libs/stache/src/lib/modules/table-of-contents/table-of-contents.module.ts","../../../../libs/stache/src/lib/modules/layout/container.component.ts","../../../../libs/stache/src/lib/modules/layout/container.component.html","../../../../libs/stache/src/lib/modules/layout/layout-blank.component.ts","../../../../libs/stache/src/lib/modules/layout/layout-blank.component.html","../../../../libs/stache/src/lib/modules/layout/layout-container.component.ts","../../../../libs/stache/src/lib/modules/layout/layout-container.component.html","../../../../libs/stache/src/lib/modules/layout/layout-sidebar.component.ts","../../../../libs/stache/src/lib/modules/layout/layout-sidebar.component.html","../../../../libs/stache/src/lib/modules/layout/layout.component.ts","../../../../libs/stache/src/lib/modules/layout/layout.component.html","../../../../libs/stache/src/lib/modules/layout/layout.module.ts","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step-body.component.ts","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step-body.component.html","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step-heading.component.ts","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step-heading.component.html","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step.component.ts","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step.component.html","../../../../libs/stache/src/lib/modules/tutorial-step/tutorial-step.module.ts","../../../../libs/stache/src/lib/modules/tutorial/tutorial-heading.component.ts","../../../../libs/stache/src/lib/modules/tutorial/tutorial-heading.component.html","../../../../libs/stache/src/lib/modules/tutorial/tutorial-summary.component.ts","../../../../libs/stache/src/lib/modules/tutorial/tutorial-summary.component.html","../../../../libs/stache/src/lib/modules/tutorial/tutorial.component.ts","../../../../libs/stache/src/lib/modules/tutorial/tutorial.component.html","../../../../libs/stache/src/lib/modules/tutorial/tutorial.module.ts","../../../../libs/stache/src/lib/modules/wrapper/title.service.ts","../../../../libs/stache/src/lib/modules/wrapper/wrapper.component.ts","../../../../libs/stache/src/lib/modules/wrapper/wrapper.component.html","../../../../libs/stache/src/lib/modules/wrapper/wrapper.module.ts","../../../../libs/stache/src/lib/stache.module.ts","../../../../libs/stache/src/blackbaud-skyux-lib-stache.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\nimport { of } from 'rxjs';\n\n@Injectable()\nexport class StacheAuthService {\n public readonly isAuthenticated = of<boolean>(false);\n}\n","export const stringConverter = (value: any): string | undefined => {\n if (value === undefined || typeof value === 'string') {\n return value;\n }\n\n return value.toString();\n};\n\nexport const booleanConverter = (value: any): boolean | undefined => {\n if (value === undefined || typeof value === 'boolean') {\n return value;\n }\n\n return value.toString() === 'true';\n};\n\nexport const numberConverter = (value: any): number | undefined => {\n if (value === undefined || typeof value === 'number') {\n return value;\n }\n\n return parseFloat(value.toString());\n};\n","import { InjectionToken } from '@angular/core';\n\nexport const STACHE_ROUTE_OPTIONS = new InjectionToken<StacheRouteOptions>(\n 'StacheRouteOptions',\n);\n\nexport interface StacheRouteOptions {\n basePath?: string;\n}\n","import { StacheNavLink } from '../nav/nav-link';\n\nexport function sortByName(a: StacheNavLink, b: StacheNavLink): number {\n if (a.name.toLowerCase() < b.name.toLowerCase()) {\n return -1;\n } else if (a.name.toLowerCase() > b.name.toLowerCase()) {\n return 1;\n } else {\n return 0;\n }\n}\n\nexport function sortByOrder(a: StacheNavLink, b: StacheNavLink): number {\n /*istanbul ignore if: this function is never called when order is undefined*/\n if (a.order === undefined || b.order === undefined) {\n return -1;\n }\n\n if (a.order < b.order) {\n return -1;\n } else if (a.order > b.order) {\n return 1;\n } else {\n return 0;\n }\n}\n","import { Inject, Injectable, OnDestroy, Optional } from '@angular/core';\nimport { NavigationStart, ROUTES, Router, Routes } from '@angular/router';\n\nimport { Subject } from 'rxjs';\nimport { takeUntil } from 'rxjs/operators';\n\nimport { StacheNavLink } from '../nav/nav-link';\nimport { numberConverter } from '../shared/input-converter';\n\nimport { StacheRouteMetadataConfig } from './route-metadata-config';\nimport { StacheRouteMetadataConfigJson } from './route-metadata-config-json';\nimport { STACHE_ROUTE_OPTIONS, StacheRouteOptions } from './route-options';\nimport { sortByName, sortByOrder } from './sort';\n\ntype UnformattedStacheNavLink = {\n path: string;\n segments: string[];\n children?: UnformattedStacheNavLink[];\n data?: Partial<StacheRouteMetadataConfigJson>;\n};\n\nfunction clone<T>(thing: T): T {\n return JSON.parse(JSON.stringify(thing));\n}\n\n/**\n * The StacheRouteService assumes the consuming application's routes are \"flat\", without children.\n * For example:\n * ```\n * const routes: Routes = [\n * {\n * path: '',\n * component: HomeComponent,\n * },\n * {\n * path: 'design',\n * component: DesignPlaygroundComponent,\n * data: {\n * stache: {\n * name: 'Design',\n * },\n * },\n * },\n * {\n * path: 'design/styles',\n * component: StylesPlaygroundComponent,\n * data: {\n * stache: {\n * name: 'Styles',\n * },\n * },\n * },\n * ];\n * ```\n * Import the StacheRouterModule into lazy-loaded modules to give the Stache route service the desired route config.\n * For example:\n * ```\n * @NgModule({\n * import: [\n * RouterModule.forChild({...}),\n * StacheRouterModule.forChild('my-top-level-route')\n * ]\n * })\n * export class MyLazyLoadedModule {}\n * ```\n * Lazy loaded components will lose the context provided by StacheRouterModule.forChild(...)\n * You may override the StacheRouteOptions via a route provider to wire lazy components into navigation\n * ```\n * const routes: Routes = [\n * {\n * path: 'lazy',\n * loadComponent: () => import('./path/to/lazy.component'),\n * providers: [\n * { provide: STACHE_ROUTE_OPTIONS, useValue: {basePath: 'nested'}}\n * ]\n * data: {\n * stache: {\n * name: 'Lazy',\n * },\n * },\n * },\n * ];\n * ```\n */\n@Injectable()\nexport class StacheRouteService implements OnDestroy {\n #activeRoutes: StacheNavLink[] | undefined;\n #ngUnsubscribe = new Subject<void>();\n #options: StacheRouteOptions | undefined;\n #router: Router;\n #routes: Routes = [];\n\n constructor(\n router: Router,\n @Optional() @Inject(ROUTES) routes?: Routes,\n @Optional() @Inject(STACHE_ROUTE_OPTIONS) options?: StacheRouteOptions,\n ) {\n this.#options = options;\n this.#router = router;\n this.#routes = ([] as Routes).concat(...(routes || []));\n\n router.events.pipe(takeUntil(this.#ngUnsubscribe)).subscribe((val) => {\n if (val instanceof NavigationStart) {\n this.clearActiveRoutes();\n }\n });\n }\n\n public ngOnDestroy(): void {\n this.#ngUnsubscribe.next();\n this.#ngUnsubscribe.complete();\n }\n\n public getActiveRoutes(): StacheNavLink[] {\n if (this.#activeRoutes) {\n return this.#activeRoutes;\n }\n\n const rootPath = this.getActiveUrl().replace(/^\\//, '').split('/')[0];\n\n const appRoutes = this.#options?.basePath\n ? this.#routes\n : this.#getRouteBranch(this.#routes, rootPath);\n\n const activeChildRoutes = appRoutes\n .filter((route) => {\n // If options.path is specified, it means that all routes are children\n // of the root path.\n return this.#options?.basePath || route.path?.indexOf(rootPath) === 0;\n })\n .map((route) => {\n const path = this.#prependOptionsPath(route.path);\n\n return {\n path,\n segments: path?.split('/'),\n data: route.data,\n };\n });\n\n const activeRoutes: UnformattedStacheNavLink[] = [\n {\n path: rootPath,\n segments: [rootPath],\n children: this.#assignChildren(activeChildRoutes, rootPath),\n data: activeChildRoutes.find((route) => route.path === rootPath)?.data,\n },\n ];\n\n this.#activeRoutes = this.#formatRoutes(activeRoutes);\n\n return clone(this.#activeRoutes);\n }\n\n public getActiveUrl(): string {\n return this.#router.url.split('?')[0].split('#')[0];\n }\n\n public clearActiveRoutes(): void {\n this.#activeRoutes = undefined;\n }\n\n /**\n * Recursively finds the \"branch\" of the routes tree that contains the given root path.\n */\n #getRouteBranch(routes: Routes, rootPath: string): Routes {\n const pathExistsInRoot = routes.some(\n (route) => route.path?.indexOf(rootPath) === 0,\n );\n\n if (pathExistsInRoot) {\n return routes;\n }\n\n // Path not found, look in each route's children.\n for (const route of routes) {\n if (route.children) {\n const childRoutes = this.#getRouteBranch(route.children, rootPath);\n if (childRoutes.length > 0) {\n return childRoutes;\n }\n }\n }\n\n return [];\n }\n\n #prependOptionsPath(path: string | undefined): string | undefined {\n // If options.path is specified, all routes are children of the root path,\n // so prepend it to each path when building the path for navigation.\n if (this.#options?.basePath) {\n path = path\n ? `${this.#options.basePath}/${path}`\n : this.#options.basePath;\n }\n\n return path;\n }\n\n #assignChildren(\n routes: UnformattedStacheNavLink[],\n parentPath: string,\n ): UnformattedStacheNavLink[] {\n const assignedRoutes: UnformattedStacheNavLink[] = [];\n const depth = parentPath.split('/').length + 1;\n\n routes.forEach((route) => {\n const routeDepth = route.segments.length;\n\n // Adding trailing slash to force end of parent path. Otherwise:\n // a/child, a1/child, and a2/child would have all three children displayed under a.\n const isChildRoute =\n depth === routeDepth && route.path.indexOf(parentPath + '/') > -1;\n\n if (isChildRoute) {\n route.children = this.#assignChildren(routes, route.path);\n assignedRoutes.push(route);\n }\n });\n\n return assignedRoutes;\n }\n\n #formatRoutes(routes: UnformattedStacheNavLink[]): StacheNavLink[] {\n const formatted = routes\n .map((route) => {\n const pathMetadata = this.#validateNavOrder({\n ...route.data?.['stache'],\n showInNav: (route.data?.['stache']?.showInNav ?? true) as boolean,\n });\n const formattedRoute: StacheNavLink = Object.assign(\n {},\n {\n path: route.path,\n name: this.#getNameFromPath(\n route.segments[route.segments.length - 1],\n ),\n },\n pathMetadata,\n );\n\n if (route.children) {\n formattedRoute.children = this.#formatRoutes(route.children);\n }\n\n return formattedRoute;\n })\n .filter((route) => route.showInNav !== false);\n\n return this.#sortRoutes(formatted) as StacheNavLink[];\n }\n\n #validateNavOrder(\n json: StacheRouteMetadataConfigJson,\n ): StacheRouteMetadataConfig {\n if ('order' in json) {\n const order: number = numberConverter(json.order);\n json.order = order;\n if (order === undefined || order <= 0 || Number.isNaN(order)) {\n delete json.order;\n }\n }\n\n return json as StacheRouteMetadataConfig;\n }\n\n #getNameFromPath(path: string): string {\n path = path.replace(/-/g, ' ');\n return this.#toTitleCase(path);\n }\n\n #toTitleCase(phrase: string): string {\n return phrase\n .split(' ')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n }\n\n #sortRoutes(routes: StacheNavLink[]): StacheNavLink[] {\n const sortedRoutes = routes\n .filter((route) => !('order' in route))\n .sort(sortByName);\n\n const routesWithNavOrder = routes\n .filter((route) => 'order' in route)\n .sort(sortByName)\n .sort(sortByOrder);\n\n routesWithNavOrder.forEach((route) => {\n // We know route order is defined in this loop.\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n const order = route.order!;\n let newIdx = order - 1;\n const validPosition = (): boolean => newIdx < sortedRoutes.length;\n const positionPreviouslyAssigned = (): boolean => {\n return sortedRoutes[newIdx].order === undefined\n ? false\n : sortedRoutes[newIdx].order <= order;\n };\n\n if (validPosition()) {\n while (validPosition() && positionPreviouslyAssigned()) {\n newIdx++;\n }\n sortedRoutes.splice(newIdx, 0, route);\n } else {\n sortedRoutes.push(route);\n }\n });\n\n return sortedRoutes;\n }\n}\n","import { ModuleWithProviders, NgModule } from '@angular/core';\nimport { ROUTES, Router, Routes } from '@angular/router';\n\nimport { StacheRouteOptions } from './route-options';\nimport { StacheRouteService } from './route.service';\n\n@NgModule({\n providers: [StacheRouteService],\n})\nexport class StacheRouterModule {\n public static forChild(\n basePath: string,\n ): ModuleWithProviders<StacheRouterModule> {\n return {\n ngModule: StacheRouterModule,\n providers: [\n {\n provide: StacheRouteService,\n useFactory: (router: Router, routes: Routes): StacheRouteService => {\n const options: StacheRouteOptions = {\n basePath,\n };\n\n return new StacheRouteService(router, routes, options);\n },\n deps: [Router, ROUTES],\n },\n ],\n };\n }\n}\n","/* istanbul ignore file */\n/**\n * NOTICE: DO NOT MODIFY THIS FILE!\n * The contents of this file were automatically generated by\n * the 'ng generate @skyux/i18n:lib-resources-module lib/modules/shared/sky-stache' schematic.\n * To update this file, simply rerun the command.\n */\nimport { NgModule } from '@angular/core';\nimport {\n SkyI18nModule,\n SkyLibResources,\n SkyLibResourcesService,\n} from '@skyux/i18n';\n\nconst RESOURCES: Record<string, SkyLibResources> = {\n 'EN-US': {\n stache_copyright_label: { message: 'Blackbaud, Inc. All rights reserved.' },\n stache_sidebar_label: { message: 'Sidebar Navigation' },\n stache_sidebar_toggle_button: { message: 'Toggle sidebar' },\n stache_back_to_top_button: {\n message: 'Scroll back to the top of the page',\n },\n },\n};\n\nSkyLibResourcesService.addResources(RESOURCES);\n\n/**\n * Import into any component library module that needs to use resource strings.\n */\n@NgModule({\n exports: [SkyI18nModule],\n})\nexport class SkyStacheResourcesModule {}\n","import { Injectable } from '@angular/core';\n\nimport { Observable, ReplaySubject, fromEvent } from 'rxjs';\n\nfunction getWindow(): any {\n return window;\n}\n\n/**\n * @internal\n */\n@Injectable()\nexport class StacheWindowRef {\n public get nativeWindow(): any {\n return getWindow();\n }\n\n public get onResizeStream(): Observable<Window> {\n return this.#resizeSubject.asObservable();\n }\n\n public scrollEventStream = fromEvent<Event>(this.nativeWindow, 'scroll');\n\n #resizeSubject = new ReplaySubject<Window>();\n\n constructor() {\n this.nativeWindow.addEventListener('resize', (event: UIEvent) => {\n this.#onResize(event);\n });\n }\n\n #onResize(event: UIEvent): void {\n this.#resizeSubject.next(event.target as Window);\n }\n}\n","import { Injectable } from '@angular/core';\nimport { NavigationExtras, Router } from '@angular/router';\n\nimport { StacheWindowRef } from '../shared/window-ref';\n\nimport { StacheNavLink } from './nav-link';\n\ntype StacheRoute = Omit<StacheNavLink, 'name'>;\n\n@Injectable()\nexport class StacheNavService {\n #router: Router;\n #windowRef: StacheWindowRef;\n\n constructor(router: Router, windowRef: StacheWindowRef) {\n this.#router = router;\n this.#windowRef = windowRef;\n }\n\n public navigate(route: StacheRoute): void {\n const extras: NavigationExtras = { queryParamsHandling: 'merge' };\n const currentPath = this.#router.url.split('?')[0].split('#')[0];\n\n if (this.isExternal(route)) {\n this.#windowRef.nativeWindow.location.href = route.path;\n return;\n }\n\n if (route.fragment) {\n if (this.#isCurrentRoute(route.path, currentPath)) {\n this.#navigateInPage(route.fragment);\n return;\n }\n extras.fragment = route.fragment;\n }\n\n if (Array.isArray(route.path)) {\n this.#router.navigate(route.path, extras);\n } else {\n this.#router.navigate([route.path], extras);\n }\n }\n\n public isExternal(route: StacheRoute | string): boolean {\n const routeStr =\n typeof route === 'string'\n ? route\n : typeof route.path === 'string'\n ? route.path\n : undefined;\n\n return routeStr ? /^(https?|mailto|ftp):+|^(www)/.test(routeStr) : false;\n }\n\n #isCurrentRoute(routePath: string | string[], currentPath: string): boolean {\n let path = routePath;\n\n if (Array.isArray(path)) {\n path = path.join('/');\n }\n\n return (\n path === '.' || currentPath.replace(/^\\//, '') === path.replace(/^\\//, '')\n );\n }\n\n #navigateInPage(fragment: string): void {\n const element =\n this.#windowRef.nativeWindow.document.getElementById(fragment);\n if (element) {\n element.scrollIntoView();\n this.#windowRef.nativeWindow.location.hash = fragment;\n } else {\n // The current page is the path intended, but no element with the fragment exists, scroll to\n // the top of the page.\n this.#windowRef.nativeWindow.scroll(0, 0);\n }\n }\n}\n","import { LocationStrategy } from '@angular/common';\nimport {\n AfterViewInit,\n Directive,\n ElementRef,\n HostBinding,\n HostListener,\n Input,\n OnChanges,\n Renderer2,\n} from '@angular/core';\n\nimport { StacheRouteService } from '../router/route.service';\n\nimport { StacheNavService } from './nav.service';\n\n@Directive({\n selector: '[stacheRouterLink]',\n standalone: false,\n})\nexport class StacheRouterLinkDirective implements OnChanges, AfterViewInit {\n @Input()\n public set stacheRouterLink(value: string | string[] | undefined) {\n if (value === '.') {\n this.#_stacheRouterLink = this.#routerSvc.getActiveUrl();\n } else {\n this.#_stacheRouterLink = Array.isArray(value)\n ? value.join('/')\n : (value ?? '');\n }\n }\n\n public get stacheRouterLink(): string {\n return this.#_stacheRouterLink;\n }\n\n @Input()\n public fragment: string | undefined;\n\n @HostBinding()\n public href: string | undefined;\n\n #_stacheRouterLink = '';\n #navSvc: StacheNavService;\n #routerSvc: StacheRouteService;\n #locationStrategy: LocationStrategy;\n\n constructor(\n navSvc: StacheNavService,\n routerSvc: StacheRouteService,\n elementRef: ElementRef,\n locationStrategy: LocationStrategy,\n renderer: Renderer2,\n ) {\n this.#routerSvc = routerSvc;\n this.#locationStrategy = locationStrategy;\n this.#navSvc = navSvc;\n renderer.setStyle(elementRef.nativeElement, 'cursor', 'pointer');\n }\n\n public ngOnChanges(): void {\n this.#updateTargetUrlAndHref();\n }\n\n public ngAfterViewInit(): void {\n this.#updateTargetUrlAndHref();\n }\n\n @HostListener('click', ['$event'])\n public navigate(event: MouseEvent): boolean {\n if (event.ctrlKey || event.metaKey || event.shiftKey) {\n return true;\n } else {\n event.preventDefault();\n this.#navSvc.navigate({\n path: this.stacheRouterLink,\n fragment: this.fragment,\n });\n return true;\n }\n }\n\n #updateTargetUrlAndHref(): void {\n let path = `${this.stacheRouterLink}`;\n\n if (this.fragment) {\n path += `#${this.fragment}`;\n }\n\n if (this.#navSvc.isExternal(path)) {\n this.href = path;\n } else {\n this.href = this.#locationStrategy.prepareExternalUrl(path);\n }\n }\n}\n","import { Component, Input, OnDestroy, OnInit } from '@angular/core';\n\nimport { Subject } from 'rxjs';\nimport { takeUntil } from 'rxjs/operators';\n\nimport { StacheAuthService } from '../auth/auth.service';\nimport { StacheRouteService } from '../router/route.service';\n\nimport { StacheNav } from './nav';\nimport { StacheNavLink } from './nav-link';\n\n@Component({\n selector: 'stache-nav',\n templateUrl: './nav.component.html',\n styleUrls: ['./nav.component.scss'],\n standalone: false,\n})\nexport class StacheNavComponent implements OnDestroy, OnInit, StacheNav {\n @Input()\n public set routes(value: StacheNavLink[] | undefined) {\n this.#_routes = value;\n this.filteredRoutes = this.#filterRestrictedRoutes(\n value,\n this.#isAuthenticated,\n );\n this.#assignActiveStates();\n }\n\n public get routes(): StacheNavLink[] | undefined {\n return this.#_routes;\n }\n\n @Input()\n public set navType(value: string | undefined) {\n this.#_navType = value;\n this.className = value ? `stache-nav-${value}` : undefined;\n }\n\n public get navType(): string | undefined {\n return this.#_navType;\n }\n\n set #isAuthenticated(value: boolean) {\n if (value !== this.#_isAuthenticated) {\n this.#_isAuthenticated = value;\n this.filteredRoutes = this.#filterRestrictedRoutes(this.routes, value);\n }\n }\n\n get #isAuthenticated(): boolean {\n return this.#_isAuthenticated;\n }\n\n public className: string | undefined;\n\n public filteredRoutes: StacheNavLink[] | undefined;\n\n #_isAuthenticated = false;\n #_navType: string | undefined;\n #_routes: StacheNavLink[] | undefined;\n #authSvc: StacheAuthService;\n #ngUnsubscribe = new Subject<void>();\n #routeSvc: StacheRouteService;\n\n constructor(routeSvc: StacheRouteService, authSvc: StacheAuthService) {\n this.#routeSvc = routeSvc;\n this.#authSvc = authSvc;\n }\n\n public ngOnInit(): void {\n this.#assignActiveStates();\n\n this.#authSvc.isAuthenticated\n .pipe(takeUntil(this.#ngUnsubscribe))\n .subscribe((isAuthenticated) => {\n this.#isAuthenticated = isAuthenticated;\n });\n }\n\n public ngOnDestroy(): void {\n this.#ngUnsubscribe.next();\n this.#ngUnsubscribe.complete();\n }\n\n #assignActiveStates(): void {\n const activeUrl = this.#routeSvc.getActiveUrl();\n if (this.filteredRoutes) {\n this.filteredRoutes.forEach((route) => {\n route.isActive = this.#isActive(activeUrl, route);\n route.isCurrent = this.#isCurrent(activeUrl, route);\n });\n }\n }\n\n #isActive(activeUrl: string, route: StacheNavLink): boolean {\n let path = route.path;\n let navDepth: number;\n\n if (Array.isArray(path)) {\n navDepth = path.length;\n path = path.join('/');\n } else {\n navDepth = path.split('/').length;\n }\n\n if (path.indexOf('/') !== 0) {\n path = `/${path}`;\n }\n\n const isActiveParent =\n navDepth > 1 && `${activeUrl}/`.indexOf(`${path}/`) === 0;\n\n return isActiveParent || activeUrl === path;\n }\n\n #isCurrent(activeUrl: string, route: StacheNavLink): boolean {\n let path = route.path;\n\n if (Array.isArray(path)) {\n path = path.join('/');\n }\n\n return activeUrl === `/${path}`;\n }\n\n #filterRestrictedRoutes(\n routes: StacheNavLink[] | undefined,\n isAuthenticated: boolean,\n ): StacheNavLink[] | undefined {\n if (!routes || routes.length === 0 || isAuthenticated) {\n return routes;\n }\n\n return routes.filter((route) => {\n return !route.restricted;\n });\n }\n}\n","<nav *ngIf=\"filteredRoutes?.length\" class=\"stache-nav\" [ngClass]=\"className\">\n <ol class=\"stache-nav-list\">\n <li\n class=\"stache-nav-list-item\"\n *ngFor=\"let route of filteredRoutes\"\n [attr.aria-current]=\"route.isCurrent ? 'page' : false\"\n [ngClass]=\"{ active: route.isActive, current: route.isCurrent }\"\n >\n <a\n class=\"stache-nav-anchor\"\n stacheRouterLink=\"{{ route.path }}\"\n fragment=\"{{ route.fragment }}\"\n queryParamsHandling=\"merge\"\n >{{ route.name }}</a\n >\n <stache-nav\n *ngIf=\"route.children\"\n navType=\"sidebar stache-nav-sub\"\n [routes]=\"route.children\"\n >\n </stache-nav>\n </li>\n </ol>\n</nav>\n","import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\n\nimport { StacheAuthService } from '../auth/auth.service';\nimport { StacheRouterModule } from '../router/router.module';\nimport { SkyStacheResourcesModule } from '../shared/sky-stache-resources.module';\nimport { StacheWindowRef } from '../shared/window-ref';\n\nimport { StacheRouterLinkDirective } from './link.directive';\nimport { StacheNavComponent } from './nav.component';\nimport { StacheNavService } from './nav.service';\n\n@NgModule({\n declarations: [StacheNavComponent, StacheRouterLinkDirective],\n imports: [\n CommonModule,\n RouterModule,\n SkyStacheResourcesModule,\n StacheRouterModule,\n ],\n exports: [StacheNavComponent, StacheRouterLinkDirective],\n providers: [StacheAuthService, StacheNavService, StacheWindowRef],\n})\nexport class StacheNavModule {}\n","import { Component, Input } from '@angular/core';\n\nimport { StacheNavLink } from '../nav/nav-link';\nimport { booleanConverter } from '../shared/input-converter';\n\nconst SEARCH_KEYS: (keyof StacheNavLink)[] = ['name', 'summary'];\n\n@Component({\n selector: 'stache-action-buttons',\n templateUrl: './action-buttons.component.html',\n styleUrls: ['./action-buttons.component.scss'],\n standalone: false,\n})\nexport class StacheActionButtonsComponent {\n @Input()\n public set routes(value: StacheNavLink[] | undefined) {\n this.#_routes = value;\n this.filteredRoutes = value || [];\n }\n\n public get routes(): StacheNavLink[] | undefined {\n return this.#_routes;\n }\n\n @Input()\n public set showSearch(value: boolean | string | undefined) {\n this.#_showSearch = booleanConverter(value) !== false;\n }\n\n public get showSearch(): boolean {\n return this.#_showSearch;\n }\n\n public filteredRoutes: StacheNavLink[] = [];\n\n public searchText = '';\n\n #_routes: StacheNavLink[] | undefined;\n\n #_showSearch = true;\n\n public onKeyUp(event: KeyboardEvent): void {\n const searchText = (event.target as HTMLInputElement).value;\n this.searchApplied(searchText);\n }\n\n public searchApplied(searchText: string): void {\n this.searchText = searchText;\n if (!searchText) {\n return;\n }\n\n if (this.routes) {\n const query = searchText.toLowerCase();\n this.filteredRoutes = this.routes.filter((route) => {\n const matchingFields = SEARCH_KEYS.filter((key) => {\n const value = route[key] as string | undefined;\n return value && value.toLowerCase().includes(query);\n });\n\n return matchingFields.length > 0;\n });\n }\n }\n}\n","<div class=\"stache-action-buttons\" *ngIf=\"routes?.length\">\n <div *ngIf=\"showSearch\" class=\"stache-action-buttons-search-container\">\n <sky-search\n expandMode=\"fit\"\n [searchText]=\"searchText\"\n (searchApply)=\"searchApplied($event)\"\n (keyup)=\"onKeyUp($event)\"\n >\n </sky-search>\n </div>\n <sky-row>\n <nav class=\"stache-action-buttons-nav-container\">\n <sky-column\n [screenSmall]=\"6\"\n [screenMedium]=\"4\"\n [screenLarge]=\"3\"\n *ngFor=\"let route of filteredRoutes\"\n >\n <a\n stacheRouterLink=\"{{ route.path }}\"\n fragment=\"{{ route.fragment }}\"\n class=\"stache-action-button-override sky-action-button sky-btn-default sky-rounded-corners\"\n >\n <div>\n <sky-action-button-icon\n *ngIf=\"route.icon\"\n iconType=\"{{ route.icon }}\"\n >\n </sky-action-button-icon>\n <sky-action-button-header>\n {{ route.name }}\n </sky-action-button-header>\n </div>\n <sky-action-button-details>\n {{ route.summary }}\n </sky-action-button-details>\n </a>\n </sky-column>\n </nav>\n </sky-row>\n</div>\n","import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\nimport { SkyActionButtonModule, SkyFluidGridModule } from '@skyux/layout';\nimport { SkySearchModule } from '@skyux/lookup';\n\nimport { StacheNavModule } from '../nav/nav.module';\n\nimport { StacheActionButtonsComponent } from './action-buttons.component';\n\n@NgModule({\n declarations: [StacheActionButtonsComponent],\n imports: [\n CommonModule,\n SkyActionButtonModule,\n SkyFluidGridModule,\n SkySearchModule,\n StacheNavModule,\n ],\n exports: [StacheActionButtonsComponent],\n})\nexport class StacheActionButtonsModule {}\n","import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';\n\nimport { StacheWindowRef } from './window-ref';\n\nconst HAS_OMNIBAR_CLASS_NAME = 'stache-omnibar-enabled';\nconst EXPECTED_OMNIBAR_HEIGHT = 50;\n\n@Injectable()\nexport class StacheOmnibarAdapterService {\n #element: HTMLElement;\n #renderer: Renderer2;\n #windowRef: StacheWindowRef;\n\n constructor(windowRef: StacheWindowRef, rendererFactory: RendererFactory2) {\n this.#windowRef = windowRef;\n this.#renderer = rendererFactory.createRenderer(undefined, null);\n this.#element = windowRef.nativeWindow.document.querySelector(\n '.sky-omnibar-iframe',\n );\n }\n\n public checkForOmnibar(): void {\n if (this.omnibarEnabled()) {\n this.#applyClassToBody();\n }\n }\n\n public getHeight(): number {\n if (this.omnibarEnabled()) {\n return EXPECTED_OMNIBAR_HEIGHT;\n }\n return 0;\n }\n\n public omnibarEnabled(): boolean {\n // Converts the element's existence to a boolean.\n return !!this.#element;\n }\n\n #applyClassToBody(): void {\n this.#renderer.addClass(\n this.#windowRef.nativeWindow.document.body,\n HAS_OMNIBAR_CLASS_NAME,\n );\n }\n}\n","import {\n AfterViewInit,\n Directive,\n ElementRef,\n HostListener,\n Renderer2,\n} from '@angular/core';\n\nimport { StacheOmnibarAdapterService } from '../shared/omnibar-adapter.service';\nimport { StacheWindowRef } from '../shared/window-ref';\n\nconst AFFIX_CLASS_NAME = 'stache-affix-top';\n\n@Directive({\n selector: '[stacheAffixTop]',\n standalone: false,\n})\nexport class StacheAffixTopDirective implements AfterViewInit {\n isAffixed = false;\n\n #footerWrapper: HTMLElement | undefined;\n #omnibarHeight = 0;\n #offsetTop = 0;\n #element: HTMLElement | undefined;\n\n #renderer: Renderer2;\n #elementRef: ElementRef;\n #omnibarSvc: StacheOmnibarAdapterService;\n #windowRef: StacheWindowRef;\n\n constructor(\n renderer: Renderer2,\n elementRef: ElementRef,\n omnibarSvc: StacheOmnibarAdapterService,\n windowRef: StacheWindowRef,\n ) {\n this.#renderer = renderer;\n this.#elementRef = elementRef;\n this.#omnibarSvc = omnibarSvc;\n this.#windowRef = windowRef;\n }\n\n public ngAfterViewInit(): void {\n this.#footerWrapper = this.#windowRef.nativeWindow.document.querySelector(\n '.stache-footer-wrapper',\n );\n const nativeElement = this.#elementRef.nativeElement;\n\n if (this.#isComponent(nativeElement) && nativeElement.children[0]) {\n this.#element = nativeElement.children[0];\n } else {\n this.#element = nativeElement;\n }\n }\n\n @HostListener('window:scroll')\n public onWindowScroll(): void {\n this.#omnibarHeight = this.#omnibarSvc.getHeight();\n this.#setMaxHeight();\n\n if (this.#element && !this.isAffixed) {\n this.#offsetTop = this.#getOffset(this.#element);\n }\n\n const windowIsScrolledBeyondElement =\n this.#offsetTop - this.#omnibarHeight <=\n this.#windowRef.nativeWindow.pageYOffset;\n\n if (windowIsScrolledBeyondElement) {\n this.#affixToTop();\n } else {\n this.#resetElement();\n }\n }\n\n #isComponent(element: HTMLElement): boolean {\n let isComponent = false;\n\n Array.prototype.slice.call(element.attributes).forEach((item) => {\n if (!isComponent && item.name.indexOf('_nghost') === 0) {\n isComponent = true;\n }\n });\n\n return isComponent;\n }\n\n #getOffset(element: HTMLElement): number {\n let offset = element.offsetTop;\n let el = element;\n\n while (el.offsetParent) {\n const parent = el.offsetParent as HTMLElement;\n offset += parent.offsetTop;\n el = parent;\n }\n\n return offset;\n }\n\n #affixToTop(): void {\n if (!this.isAffixed && this.#element) {\n this.isAffixed = true;\n this.#renderer.setStyle(this.#element, 'position', 'fixed');\n this.#renderer.setStyle(this.#element, 'top', '0px');\n this.#renderer.setStyle(this.#element, 'width', 'inherit');\n this.#renderer.addClass(this.#element, AFFIX_CLASS_NAME);\n }\n }\n\n #resetElement(): void {\n if (this.isAffixed) {\n this.isAffixed = false;\n this.#renderer.setStyle(this.#element, 'position', 'static');\n this.#renderer.removeClass(this.#element, AFFIX_CLASS_NAME);\n }\n }\n\n #setMaxHeight(): void {\n let maxHeight = `calc(100% - ${this.#omnibarHeight}px)`;\n\n if (this.#footerWrapper && this.#footerIsVisible()) {\n maxHeight = `${\n this.#getOffset(this.#footerWrapper) -\n this.#windowRef.nativeWindow.pageYOffset -\n this.#omnibarHeight\n }px`;\n }\n\n /* istanbul ignore else */\n if (this.#element) {\n this.#renderer.setStyle(this.#element, 'height', `${maxHeight}`);\n }\n }\n\n #footerIsVisible(): boolean {\n /*istanbul ignore else*/\n if (this.#footerWrapper) {\n return (\n this.#footerWrapper.getBoundingClientRect().top <=\n this.#windowRef.nativeWindow.innerHeight\n );\n } else {\n return false;\n }\n }\n}\n","import {\n AfterViewInit,\n ChangeDetectorRef,\n Component,\n ElementRef,\n OnDestroy,\n ViewChild,\n} from '@angular/core';\n\nimport { Subscription } from 'rxjs';\n\nimport { StacheWindowRef } from '../shared/window-ref';\n\nimport { StacheAffixTopDirective } from './affix-top.directive';\n\n@Component({\n selector: 'stache-affix',\n templateUrl: './affix.component.html',\n styleUrls: ['./affix.component.scss'],\n standalone: false,\n})\nexport class StacheAffixComponent implements AfterViewInit, OnDestroy {\n @ViewChild('stacheAffixWrapper', {\n read: ElementRef,\n static: false,\n })\n public wrapper: ElementRef | undefined;\n\n public minHeightFormatted: string | undefined;\n\n public maxWidthFormatted: string | undefined;\n\n @ViewChild(StacheAffixTopDirective, {\n read: StacheAffixTopDirective,\n static: true,\n })\n public affixTopDirective: StacheAffixTopDirective | undefined;\n\n #windowRef: StacheWindowRef;\n #windowSubscription: Subscription;\n #changeDetector: ChangeDetectorRef;\n\n public constructor(\n windowRef: StacheWindowRef,\n changeDetector: ChangeDetectorRef,\n ) {\n this.#windowRef = windowRef;\n this.#changeDetector = changeDetector;\n this.#windowSubscription = this.#windowRef.onResizeStream.subscribe(() => {\n this.#setElementRefDimensions();\n });\n }\n\n public ngAfterViewInit(): void {\n this.#setElementRefDimensions();\n this.#changeDetector.detectChanges();\n }\n\n public ngOnDestroy(): void {\n this.#windowSubscription.unsubscribe();\n }\n\n public getStyles(): Record<string, string | undefined> {\n return {\n 'min-height': this.#getCssMinHeight(),\n 'max-width': this.#getCssMaxWidth(),\n position: this.#getCssPosition(),\n };\n }\n\n #setElementRefDimensions(): void {\n /* istanbul ignore else */\n if (this.wrapper) {\n this.minHeightFormatted = `${this.wrapper.nativeElement.offsetHeight}px`;\n this.maxWidthFormatted = `${this.wrapper.nativeElement.offsetWidth}px`;\n }\n }\n\n #getCssPosition(): string {\n if (this.affixTopDirective?.isAffixed) {\n return 'relative';\n }\n\n return 'static';\n }\n\n #getCssMinHeight(): string | undefined {\n if (this.affixTopDirective?.isAffixed) {\n return this.minHeightFormatted;\n }\n\n return 'auto';\n }\n\n #getCssMaxWidth(): string | undefined {\n if (this.affixTopDirective?.isAffixed) {\n return this.maxWidthFormatted;\n }\n\n return '100%';\n }\n}\n","<div #stacheAffixWrapper>\n <div class=\"stache-affix\" [ngStyle]=\"getStyles()\">\n <div stacheAffixTop>\n <ng-content></ng-content>\n </div>\n </div>\n</div>\n","import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\n\nimport { StacheOmnibarAdapterService } from '../shared/omnibar-adapter.service';\nimport { StacheWindowRef } from '../shared/window-ref';\n\nimport { StacheAffixTopDirective } from './affix-top.directive';\nimport { StacheAffixComponent } from './affix.component';\n\n@NgModule({\n declarations: [StacheAffixComponent, StacheAffixTopDirective],\n imports: [CommonModule],\n exports: [StacheAffixComponent, StacheAffixTopDirective],\n providers: [StacheOmnibarAdapterService, StacheWindowRef],\n})\nexport class StacheAffixModule {}\n","import { CSP_NONCE, Directive, OnInit, inject } from '@angular/core';\nimport { NavigationEnd, Router } from '@angular/router';\nimport { SkyAppConfig } from '@skyux/config';\n\nimport { StacheWindowRef } from '../shared/window-ref';\n\n@Directive({\n selector: '[stacheGoogleAnalytics]',\n standalone: false,\n})\nexport class StacheGoogleAnalyticsDirective implements OnInit {\n readonly #nonce = inject(CSP_NONCE, { optional: true });\n\n private tagManagerContainerId = 'GTM-W56QP9';\n private analyticsClientId = 'UA-2418840-1';\n private isEnabled = true;\n private appName: string;\n\n public constructor(\n private windowRef: StacheWindowRef,\n private configService: SkyAppConfig,\n private router: Router,\n ) {}\n\n public ngOnInit(): void {\n const isLoaded = this.windowRef.nativeWindow.ga;\n const isProduction = this.configService.runtime.command === 'build';\n\n this.updateDefaultConfigs();\n\n if (this.isEnabled && isProduction && !isLoaded) {\n this.addGoogleTagManagerScript();\n this.initGoogleAnalytics();\n this.bindPageViewsToRouter();\n }\n }\n\n public addGoogleTagManagerScript(): void {\n const win = this.windowRef.nativeWindow as {\n dataLayer?: unknown[];\n document: Document;\n };\n\n win.dataLayer ??= [];\n win.dataLayer.push({\n 'gtm.start': new Date().getTime(),\n event: 'gtm.js',\n });\n\n const script = win.document.createElement('script');\n script.setAttribute('async', 'async');\n script.setAttribute(\n 'src',\n `https://www.googletagmanager.com/gtm.js?id=${this.tagManagerContainerId}`,\n );\n\n if (this.#nonce) {\n script.setAttribute('nonce', this.#nonce);\n }\n\n win.document.head.appendChild(script);\n }\n\n /**\n * Because GTM adds the Google Analytics script to the page outside of our control,\n * instead of loading the GA script twice, we extract the 'ga' window method out so we can\n * subscribe to our router events and track SPA page views.\n */\n public initGoogleAnalytics(): void {\n const win = this.windowRef.nativeWindow;\n\n win['GoogleAnalyticsObject'] = 'ga';\n\n win.ga =\n win.ga ||\n function (...args: unknown[]): void {\n (win.ga.q = win.ga.q || []).push(args);\n };\n\n win.ga.l = new Date().getTime();\n\n win.ga('create', this.analyticsClientId, 'auto');\n }\n\n public bindPageViewsToRouter(): void {\n this.router.events.subscribe((event) => {\n /* istanbul ignore else*/\n if (event instanceof NavigationEnd) {\n const ga = this.windowRef.nativeWindow.ga;\n\n ga('set', 'page', `${this.appName}${event.urlAfterRedirects}`);\n ga('send', 'pageview');\n }\n });\n }\n\n private updateDefaultConfigs(): void {\n // TODO: the config service should handle defaults!\n const appSettings = this.configService.skyux.appSettings || {};\n appSettings.stache = appSettings.stache || {};\n appSettings.stache.googleAnalytics =\n appSettings.stache.googleAnalytics || {};\n\n if (appSettings.stache.googleAnalytics.tagManagerContainerId) {\n this.tagManagerContainerId =\n appSettings.stache.googleAnalytics.tagManagerContainerId;\n }\n\n if (appSettings.stache.googleAnalytics.clientId) {\n this.analyticsClientId = appSettings.stache.googleAnalytics.clientId;\n }\n\n this.appName = this.configService.runtime.app.base.replace(/^\\/|\\/$/g, '');\n\n switch (appSettings.stache.googleAnalytics.enabled) {\n case false:\n case 'false':\n this.isEnabled = false;\n break;\n default:\n break;\n }\n }\n}\n","import { NgModule } from '@angular/core';\nimport { RouterModule } from '@angular/router';\n\nimport { StacheGoogleAnalyticsDirective } from './google-analytics.directive';\n\n@NgModule({\n imports: [RouterModule],\n declarations: [StacheGoogleAnalyticsDirective],\n exports: [StacheGoogleAnalyticsDirective],\n})\nexport class StacheAnalyticsModule {}\n","import { Component, HostListener, Input } from '@angular/core';\n\nimport { numberConverter } from '../shared/input-converter';\nimport { StacheWindowRef } from '../shared/window-ref';\n\nconst DEFAULT_OFFSET = 200;\n\n@Component({\n selector: 'stache-back-to-top',\n templateUrl: './back-to-top.component.html',\n styleUrls: ['./back-to-top.component.scss'],\n standalone: false,\n})\nexport class StacheBackToTopComponent {\n @Input()\n public set offset(value: number | string | undefined) {\n this.#_offset = numberConverter(value) ?? DEFAULT_OFFSET;\n }\n\n public get offset(): number {\n return this.#_offset;\n }\n\n public isHidden = true;\n\n #_offset = DEFAULT_OFFSET;\n #windowRef: StacheWindowRef;\n\n constructor(windowRef: StacheWindowRef) {\n this.#windowRef = windowRef;\n }\n\n @HostListener('window:scroll')\n public onWindowScroll(): void {\n this.isHidden = this.#windowRef.nativeWindow.pageYOffset < this.offset;\n }\n\n public scrollToTop(): void {\n this.#windowRef.nativeWindow.scroll({\n top: 0,\n left: 0,\n behavior: 'smooth',\n });\n }\n}\n","<button\n class=\"sky-btn sky-btn-primary stache-back-to-top\"\n [ngClass]=\"{ hidden: isHidden }\"\n type=\"button\"\n [attr.aria-label]=\"'stache_back_to_top_button' | skyLibResources\"\n [attr.aria-hidden]=\"isHidden\"\n (click)=\"scrollToTop()\"\n>\n <i class=\"fa fa-chevron-up\"></i>\n</button>\n","import { CommonModule } from '@angular/common';\nimport { NgModule } from '@angular/core';\n\nimport { SkyStacheResourcesModule } from '../shared/sky-stache-resources.module';\nimport { StacheWindowRef } from '../shared/window-ref';\n\nimport { StacheBackToTopComponent } from './back-to-top.component';\n\n@NgModule({\n declarations: [StacheBackToTopComponent],\n imports: [CommonModule, SkyStacheResourcesModule],\n exports: [StacheBackToTopComponent],\n providers: [StacheWindowRef],\n})\nexport class StacheBackToTopModule {}\n","import {\n ChangeDetectionStrategy,\n Component,\n Input,\n OnInit,\n} from '@angular/core';\n\n@Component({\n selector: 'stache-blockquote',\n templateUrl: './blockquote.component.html',\n styleUrls: ['./blockquote.component.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: false,\n})\nexport class StacheBlockquoteComponent implements OnInit {\n @Input()\n public author: string | undefined;\n\n @Input()\n public quoteSource: string | undefined;\n\n public ngOnInit(): void {\n if (this.quoteSource && !this.author) {\n this.author = 'Source';\n }\n }\n}\n","<div class=\"stache-blockquote\">\n <div class=\"stache-blockquote-icon\">\n <i class=\"fa fa-quote-left\" aria-hidden=\"true\"></i>\n </div>\n <blockquote class=\"stache-blockquote-content\">\n <ng-content></ng-content>\n <div *ngIf=\"author\" class=\"stache-blockquote-source\">\n &mdash;\n <a *ngIf=\"quoteSource\" href=\"{{ quoteSource }}\">\n <span>{