UNPKG

@skyux/core

Version:

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

1,240 lines (1,220 loc) 207 kB
import * as i0 from '@angular/core'; import { NgModule, Injectable, inject, RendererFactory2, NgZone, EventEmitter, Output, Input, Directive, EnvironmentInjector, createEnvironmentInjector, createComponent, ChangeDetectorRef, ElementRef, ViewContainerRef, ViewChild, ChangeDetectionStrategy, Component, InjectionToken, input, effect, Optional, Inject, ApplicationRef, afterNextRender, Injector, Pipe, HostBinding, Renderer2, HostListener } from '@angular/core'; import * as i1$1 from '@angular/common'; import { DOCUMENT, CommonModule } from '@angular/common'; import { Subject, Subscription, ReplaySubject, fromEvent, of, Observable, filter, map, distinctUntilChanged, shareReplay, observeOn, animationFrameScheduler, takeUntil as takeUntil$1, BehaviorSubject, combineLatestWith, switchMap, concat, debounceTime as debounceTime$1 } from 'rxjs'; import { takeUntil, debounceTime, take } from 'rxjs/operators'; import { ViewportRuler } from '@angular/cdk/overlay'; import { toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop'; import * as i1 from '@skyux/i18n'; import { SkyLibResourcesService, SkyI18nModule, SkyIntlNumberFormatStyle, SkyIntlNumberFormatter } from '@skyux/i18n'; import { Router, NavigationStart } from '@angular/router'; import * as i1$2 from '@angular/platform-browser'; /** * @deprecated The `SkyCoreAdapterService` no longer needs the `SkyCoreAdapterModule`. * The `SkyCoreAdapterModule` can be removed from your project. */ class SkyCoreAdapterModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterModule }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterModule, decorators: [{ type: NgModule, args: [{}] }] }); /** * A list of all breakpoints. * @internal */ const SKY_BREAKPOINTS = ['xs', 'sm', 'md', 'lg']; /** * Represents all available media breakpoints. * @deprecated Use `SkyBreakpoint` instead. */ var SkyMediaBreakpoints; (function (SkyMediaBreakpoints) { /** * Screen widths of 767px or less. */ SkyMediaBreakpoints[SkyMediaBreakpoints["xs"] = 1] = "xs"; /** * Screen widths of 768px to 991px. */ SkyMediaBreakpoints[SkyMediaBreakpoints["sm"] = 2] = "sm"; /** * Screen widths of 992px to 1199px. */ SkyMediaBreakpoints[SkyMediaBreakpoints["md"] = 3] = "md"; /** * Screen widths of 1200px or greater. */ SkyMediaBreakpoints[SkyMediaBreakpoints["lg"] = 4] = "lg"; })(SkyMediaBreakpoints || (SkyMediaBreakpoints = {})); const breakpointLookup = new Map([ [SkyMediaBreakpoints.xs, 'xs'], [SkyMediaBreakpoints.sm, 'sm'], [SkyMediaBreakpoints.md, 'md'], [SkyMediaBreakpoints.lg, 'lg'], ]); const legacyLookup = new Map([ ['xs', SkyMediaBreakpoints.xs], ['sm', SkyMediaBreakpoints.sm], ['md', SkyMediaBreakpoints.md], ['lg', SkyMediaBreakpoints.lg], ]); /** * Whether the value is of type `SkyBreakpoint`. * @internal */ function isSkyBreakpoint(value) { return (value !== null && value !== undefined && SKY_BREAKPOINTS.includes(value)); } /** * Transforms a `SkyMediaBreakpoints` value to `SkyBreakpoint`. * @internal */ function toSkyBreakpoint(breakpoint) { return breakpointLookup.get(breakpoint); } /** * Transforms a `SkyBreakpoint` value to `SkyMediaBreakpoints`. * @internal */ function toSkyMediaBreakpoints(breakpoint) { return legacyLookup.get(breakpoint); } const SKY_TABBABLE_SELECTOR = [ 'a[href]', 'area[href]', 'input:not([disabled])', 'button:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'iframe', 'object', 'embed', '*[contenteditable=true]:not([disabled])', '*[tabindex]:not([disabled])', ].join(', '); class SkyCoreAdapterService { #renderer; constructor(rendererFactory) { this.#renderer = rendererFactory.createRenderer(undefined, null); } /** * Set the responsive container CSS class for a given element. * * @param elementRef - The element that will receive the new CSS class. * @param breakpoint - The breakpoint to determine which class gets set. * For example a breakpoint of "xs" will set a CSS class of "sky-responsive-container-xs". * @deprecated Use the `SkyResponsiveHostDirective` instead. */ setResponsiveContainerClass(elementRef, breakpoint) { const nativeEl = elementRef.nativeElement; for (const breakpointType of SKY_BREAKPOINTS) { this.#renderer.removeClass(nativeEl, `sky-responsive-container-${breakpointType}`); } if (!isSkyBreakpoint(breakpoint)) { breakpoint = toSkyBreakpoint(breakpoint); } this.#renderer.addClass(nativeEl, `sky-responsive-container-${breakpoint}`); } /** * This method temporarily enables/disables pointer events. * This is helpful to prevent iFrames from interfering with drag events. * * @param enable - Set to `true` to enable pointer events. Set to `false` to disable. */ toggleIframePointerEvents(enable) { const iframes = Array.from(document.querySelectorAll('iframe')); for (const iframe of iframes) { this.#renderer.setStyle(iframe, 'pointer-events', enable ? '' : 'none'); } } /** * Focuses on the first element found with an `autofocus` attribute inside the supplied `elementRef`. * * @param elementRef - The element to search within. * @return Returns `true` if a child element with autofocus is found. */ applyAutoFocus(elementRef) { if (!elementRef) { return false; } const elementWithAutoFocus = elementRef.nativeElement.querySelector('[autofocus]'); // Child was found with the autofocus property. Set focus and return true. if (elementWithAutoFocus) { elementWithAutoFocus.focus(); return true; } // No children were found with autofocus property. Return false. return false; } /** * Sets focus on the first focusable child of the `elementRef` parameter. * If no focusable children are found, and `focusOnContainerIfNoChildrenFound` is `true`, * focus will be set on the container element. * * @param elementRef - The element to search within. * @param containerSelector - A CSS selector indicating the container that should * receive focus if no focusable children are found. * @param focusOnContainerIfNoChildrenFound - It set to `true`, the container will * receive focus if no focusable children are found. */ getFocusableChildrenAndApplyFocus(elementRef, containerSelector, focusOnContainerIfNoChildrenFound = false) { const containerElement = elementRef.nativeElement.querySelector(containerSelector); if (containerElement) { const focusableChildren = this.getFocusableChildren(containerElement); // Focus first focusable child if available. Otherwise, set focus on container. if (!this.#focusFirstElement(focusableChildren) && focusOnContainerIfNoChildrenFound) { containerElement.focus(); } } } /** * Returns an array of all focusable children of provided `element`. * * @param element - The HTMLElement to search within. * @param options - Options for getting focusable children. */ getFocusableChildren(element, options) { if (!element) { return []; } let elements = Array.prototype.slice.call(element.querySelectorAll(SKY_TABBABLE_SELECTOR)); // Unless ignoreTabIndex = true, filter out elements with tabindex = -1. if (!options || !options.ignoreTabIndex) { elements = elements.filter((el) => { return el.tabIndex !== -1; }); } // Unless ignoreVisibility = true, filter out elements that are not visible. if (!options || !options.ignoreVisibility) { elements = elements.filter((el) => { return this.#isVisible(el); }); } return elements; } /** * Returns the clientWidth of the provided elementRef. * @param elementRef - The element to calculate width from. */ getWidth(elementRef) { return elementRef.nativeElement.clientWidth; } /** * Checks if an event target has a higher z-index than a given element. * @param target The event target element. * @param element The element to test against. A z-index must be explicitly set for this element. */ isTargetAboveElement(target, element) { const zIndex = getComputedStyle(element).zIndex; let el = target; while (el) { // Getting the computed style only works for elements that exist in the DOM. // In certain scenarios, an element is removed after a click event; by the time the event // bubbles up to other elements, however, the element has been removed and the computed style returns empty. // In this case, we'll need to check the z-index directly, via the style property. const targetZIndex = getComputedStyle(el).zIndex || el.style.zIndex; if (targetZIndex !== '' && targetZIndex !== 'auto' && +targetZIndex > +zIndex) { return true; } el = el.parentElement; } return false; } /** * Remove inline height styles from the provided elements. * @param elementRef - The element to search within. * @param selector - The CSS selector to use when finding elements for removing height. */ resetHeight(elementRef, selector) { const children = Array.from(elementRef.nativeElement.querySelectorAll(selector)); for (const child of children) { this.#renderer.removeStyle(child, 'height'); } } /** * Sets all element heights to match the height of the tallest element. * @param elementRef - The element to search within. * @param selector - The CSS selector to use when finding elements for syncing height. */ syncMaxHeight(elementRef, selector) { const children = Array.from(elementRef.nativeElement.querySelectorAll(selector)); /* istanbul ignore else */ if (children.length > 0) { let maxHeight = 0; for (const child of children) { maxHeight = Math.max(maxHeight, child.offsetHeight); } for (const child of children) { this.#renderer.setStyle(child, 'height', `${maxHeight}px`); } } } #focusFirstElement(list) { if (list.length > 0) { list[0].focus(); return true; } return false; } #isVisible(element) { const style = window.getComputedStyle(element); const isHidden = style.display === 'none' || style.visibility === 'hidden'; if (isHidden) { return false; } const hasBounds = !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); return hasBounds; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterService, deps: [{ token: i0.RendererFactory2 }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyCoreAdapterService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: i0.RendererFactory2 }] }); var SkyAffixAutoFitContext; (function (SkyAffixAutoFitContext) { /** * Auto-fit functionality will respect the nearest overflow parent element's dimensions. */ SkyAffixAutoFitContext[SkyAffixAutoFitContext["OverflowParent"] = 0] = "OverflowParent"; /** * Auto-fit functionality will respect the browser viewport dimensions. */ SkyAffixAutoFitContext[SkyAffixAutoFitContext["Viewport"] = 1] = "Viewport"; })(SkyAffixAutoFitContext || (SkyAffixAutoFitContext = {})); function getNextPlacement(placement) { const placements = ['above', 'right', 'below', 'left']; let index = placements.indexOf(placement) + 1; if (index >= placements.length) { index = 0; } return placements[index]; } function getInversePlacement(placement) { const pairings = { above: 'below', below: 'above', right: 'left', left: 'right', }; return pairings[placement]; } function useViewportForBounds(element) { return 'BODY' === element.tagName; } /** * Returns the offset values of a given element. * @param element The HTML element. * @param bufferOffset An optional offset to add/subtract to the element's actual offset. */ function getElementOffset(element, bufferOffset) { const bufferOffsetBottom = bufferOffset?.bottom || 0; const bufferOffsetLeft = bufferOffset?.left || 0; const bufferOffsetRight = bufferOffset?.right || 0; const bufferOffsetTop = bufferOffset?.top || 0; let top; let left; let right; let bottom; const clientRect = element.getBoundingClientRect(); left = clientRect.left; top = clientRect.top; right = clientRect.right; bottom = clientRect.bottom; bottom -= bufferOffsetBottom; left += bufferOffsetLeft; right -= bufferOffsetRight; top += bufferOffsetTop; return { bottom, left, right, top, }; } /** * Returns an AffixRect that represents the outer dimensions of a given element. */ function getOuterRect(element) { const rect = element.getBoundingClientRect(); const computedStyle = window.getComputedStyle(element, undefined); const marginTop = parseFloat(computedStyle.marginTop); const marginLeft = parseFloat(computedStyle.marginLeft); const marginRight = parseFloat(computedStyle.marginRight); const marginBottom = parseFloat(computedStyle.marginBottom); return { top: rect.top - marginTop, left: rect.left - marginLeft, bottom: rect.top + rect.height + marginBottom, right: rect.left + rect.width + marginLeft + marginRight, width: rect.width + marginLeft + marginRight, height: rect.height + marginTop + marginBottom, }; } /** * Returns the visible rect for a given element. */ function getVisibleRectForElement(viewportRuler, element) { const elementRect = getOuterRect(element); const viewportRect = viewportRuler.getViewportRect(); const visibleRect = { top: Math.max(elementRect.top, 0), left: Math.max(elementRect.left, 0), bottom: Math.min(elementRect.bottom, viewportRect.height), right: Math.min(elementRect.right, viewportRect.width), }; return { ...visibleRect, width: visibleRect.right - visibleRect.left, height: visibleRect.bottom - visibleRect.top, }; } function getOverflowParents(child) { const bodyElement = window.document.body; const results = []; let parentElement = child?.parentNode; while (parentElement !== undefined && parentElement instanceof HTMLElement) { if (parentElement.matches('body')) { break; } const computedStyle = window.getComputedStyle(parentElement, undefined); const overflowY = computedStyle.overflowY.toLowerCase(); const largerThanTheDocumentElement = window.document.documentElement.scrollWidth < parentElement.scrollWidth || window.document.documentElement.scrollHeight < parentElement.scrollHeight; const hasOverflowRules = overflowY === 'auto' || overflowY === 'hidden' || overflowY === 'scroll'; if (largerThanTheDocumentElement || hasOverflowRules) { results.push(parentElement); } if (computedStyle.position === 'fixed') { break; } parentElement = parentElement.parentNode; } results.push(bodyElement); return results; } /** * Confirms offset is fully visible within a parent element. */ function isOffsetFullyVisibleWithinParent(viewportRuler, parent, offset, bufferOffset) { let parentOffset; if (useViewportForBounds(parent)) { const viewportRect = viewportRuler.getViewportRect(); parentOffset = { top: 0, left: 0, right: viewportRect.width, bottom: viewportRect.height, }; } else if (bufferOffset) { parentOffset = getElementOffset(parent, bufferOffset); } else { parentOffset = getVisibleRectForElement(viewportRuler, parent); } return (parentOffset.top <= offset.top && parentOffset.right >= offset.right && parentOffset.bottom >= offset.bottom && parentOffset.left <= offset.left); } function isOffsetPartiallyVisibleWithinParent(viewportRuler, parent, offset, bufferOffset) { let parentOffset; if (useViewportForBounds(parent)) { const viewportRect = viewportRuler.getViewportRect(); parentOffset = { top: 0, left: 0, right: viewportRect.width, bottom: viewportRect.height, }; } else if (bufferOffset) { parentOffset = getElementOffset(parent, bufferOffset); } else { parentOffset = getVisibleRectForElement(viewportRuler, parent); } return !(parentOffset.top >= offset.bottom || parentOffset.right <= offset.left || parentOffset.bottom <= offset.top || parentOffset.left >= offset.right); } const DEFAULT_AFFIX_CONFIG = { autoFitContext: SkyAffixAutoFitContext.OverflowParent, enableAutoFit: false, horizontalAlignment: 'center', isSticky: false, placement: 'above', }; class SkyAffixer { /** * Fires when the affixed element's offset changes. */ get offsetChange() { return this.#offsetChangeObs; } /** * Fires when the base element's nearest overflow parent is scrolling. This is useful if you need * to perform an additional action during the scroll event but don't want to generate another * event listener. */ get overflowScroll() { return this.#overflowScrollObs; } /** * Fires when the placement value changes. A `null` value indicates that a suitable * placement could not be found. */ get placementChange() { return this.#placementChangeObs; } get #config() { return this.#_config; } set #config(value) { const merged = { ...DEFAULT_AFFIX_CONFIG, ...value, }; // Make sure none of the values are undefined. let key; for (key in merged) { if (merged[key] === undefined) { merged[key] = DEFAULT_AFFIX_CONFIG[key]; } } this.#_config = merged; } #affixedElement; #baseElement; #currentOffset; #currentPlacement; #layoutViewport; #offsetChange; #offsetChangeObs; #overflowParents = []; #overflowScroll; #overflowScrollObs; #placementChange; #placementChangeObs; #renderer; #scrollChange = new Subject(); #viewportListeners; #viewportRuler; #zone; #_config = DEFAULT_AFFIX_CONFIG; #scrollChangeListener = () => this.#scrollChange.next(); constructor(affixedElement, renderer, viewportRuler, zone, layoutViewport) { this.#affixedElement = affixedElement; this.#renderer = renderer; this.#layoutViewport = layoutViewport; this.#viewportRuler = viewportRuler; this.#zone = zone; this.#offsetChange = new Subject(); this.#overflowScroll = new Subject(); this.#placementChange = new Subject(); this.#offsetChangeObs = this.#offsetChange.asObservable(); this.#overflowScrollObs = this.#overflowScroll.asObservable(); this.#placementChangeObs = this.#placementChange.asObservable(); } /** * Affixes an element to a base element. * @param baseElement The base element. * @param config Configuration for the affix action. */ affixTo(baseElement, config) { this.#reset(); this.#config = config; this.#baseElement = baseElement; this.#overflowParents = getOverflowParents(baseElement); this.#affix(); if (this.#config.isSticky) { this.#addViewportListeners(); } } getConfig() { return this.#config; } /** * Re-runs the affix calculation. */ reaffix() { // Reset current placement to preferred placement. this.#currentPlacement = this.#config.placement; this.#affix(); } /** * Destroys the affixer. */ destroy() { this.#reset(); this.#placementChange.complete(); this.#offsetChange.complete(); this.#overflowScroll.complete(); this.#scrollChange.complete(); } #affix() { const offset = this.#getOffset(); const offsetParentRect = this.#getOffsetParentRect(); offset.top = offset.top - offsetParentRect.top; offset.left = offset.left - offsetParentRect.left; offset.bottom = offset.bottom - offsetParentRect.top; offset.right = offset.right - offsetParentRect.left; if (this.#isNewOffset(offset)) { this.#renderer.setStyle(this.#affixedElement, 'top', `${offset.top}px`); this.#renderer.setStyle(this.#affixedElement, 'left', `${offset.left}px`); this.#offsetChange.next({ offset }); } } #getOffsetParentRect() { // Firefox sets the offsetParent to document.body if the element uses fixed positioning. if (this.#config.position === 'absolute' && this.#affixedElement.offsetParent) { return getOuterRect(this.#affixedElement.offsetParent); } else { const layoutRect = getOuterRect(this.#layoutViewport); return { top: layoutRect.top, left: layoutRect.left, height: layoutRect.height, width: layoutRect.width, bottom: layoutRect.top - layoutRect.height, right: layoutRect.left - layoutRect.width, }; } } #getOffset() { const parent = this.#getAutoFitContextParent(); const maxAttempts = 4; let attempts = 0; let isAffixedElementFullyVisible = false; let offset; let placement = this.#config.placement; do { offset = this.#getPreferredOffset(placement); isAffixedElementFullyVisible = isOffsetFullyVisibleWithinParent(this.#viewportRuler, parent, offset, this.#config.autoFitOverflowOffset); if (!this.#config.enableAutoFit) { break; } if (!isAffixedElementFullyVisible) { placement = attempts % 2 === 0 ? getInversePlacement(placement) : getNextPlacement(placement); } attempts++; } while (!isAffixedElementFullyVisible && attempts < maxAttempts); if (isAffixedElementFullyVisible) { if (this.#isBaseElementVisible()) { this.#notifyPlacementChange(placement); } else { this.#notifyPlacementChange(null); } return offset; } if (this.#config.enableAutoFit) { this.#notifyPlacementChange(null); } // No suitable placement was found, so revert to preferred placement. return this.#getPreferredOffset(this.#config.placement); } #getPreferredOffset(placement) { if (!this.#baseElement) { return { top: 0, left: 0, bottom: 0, right: 0 }; } const affixedRect = getOuterRect(this.#affixedElement); const baseRect = this.#baseElement.getBoundingClientRect(); const horizontalAlignment = this.#config.horizontalAlignment; const verticalAlignment = this.#config.verticalAlignment; const enableAutoFit = this.#config.enableAutoFit; let top; let left; if (placement === 'above' || placement === 'below') { if (placement === 'above') { top = baseRect.top - affixedRect.height; switch (verticalAlignment) { case 'top': top = top + affixedRect.height; break; case 'middle': top = top + affixedRect.height / 2; break; case 'bottom': default: break; } } else { top = baseRect.bottom; switch (verticalAlignment) { case 'top': default: break; case 'middle': top = top - affixedRect.height / 2; break; case 'bottom': top = top - affixedRect.height; break; } } switch (horizontalAlignment) { case 'left': left = baseRect.left; break; case 'center': default: left = baseRect.left + baseRect.width / 2 - affixedRect.width / 2; break; case 'right': left = baseRect.right - affixedRect.width; break; } } else { if (placement === 'left') { left = baseRect.left - affixedRect.width; } else { left = baseRect.right; } switch (verticalAlignment) { case 'top': top = baseRect.top; break; case 'middle': default: top = baseRect.top + baseRect.height / 2 - affixedRect.height / 2; break; case 'bottom': top = baseRect.bottom - affixedRect.height; break; } } const offset = { top, left, bottom: 0, right: 0 }; if (enableAutoFit) { const adjustments = this.#adjustOffsetToOverflowParent({ top, left }, placement, this.#baseElement); offset.top = adjustments.top; offset.left = adjustments.left; } offset.bottom = offset.top + affixedRect.height; offset.right = offset.left + affixedRect.width; return offset; } /** * Slightly adjust the offset to fit within the scroll parent's boundaries if * the affixed element would otherwise be clipped. */ #adjustOffsetToOverflowParent(offset, placement, baseElement) { const affixedRect = getOuterRect(this.#affixedElement); const baseRect = baseElement.getBoundingClientRect(); const parent = this.#getAutoFitContextParent(); let parentOffset; if (this.#config.autoFitOverflowOffset) { // When the config contains a specific offset. parentOffset = getElementOffset(parent, this.#config.autoFitOverflowOffset); } else if (isOffsetFullyVisibleWithinParent(this.#viewportRuler, parent, baseRect)) { // When the base element is fully visible within the parent, aim for the visible portion of the parent element. parentOffset = getVisibleRectForElement(this.#viewportRuler, parent); } else { // Anywhere in the parent element. parentOffset = getOuterRect(parent); } // A pixel value representing the leeway between the edge of the overflow parent and the edge // of the base element before it disappears from view. // If the visible portion of the base element is less than this pixel value, the auto-fit // functionality attempts to find another placement. const defaultPixelTolerance = 40; let pixelTolerance; const originalOffsetTop = offset.top; const originalOffsetLeft = offset.left; switch (placement) { case 'above': case 'below': // Keep the affixed element within the overflow parent. if (offset.left < parentOffset.left) { offset.left = parentOffset.left; } else if (offset.left + affixedRect.width > parentOffset.right) { offset.left = parentOffset.right - affixedRect.width; } // Use a smaller pixel tolerance if the base element width is less than the default. pixelTolerance = Math.min(defaultPixelTolerance, baseRect.width); // Make sure the affixed element never detaches from the base element. if (offset.left + pixelTolerance > baseRect.right || offset.left + affixedRect.width - pixelTolerance < baseRect.left) { offset.left = originalOffsetLeft; } break; case 'left': case 'right': // Keep the affixed element within the overflow parent. if (offset.top < parentOffset.top) { offset.top = parentOffset.top; } else if (offset.top + affixedRect.height > parentOffset.bottom) { offset.top = parentOffset.bottom - affixedRect.height; } // Use a smaller pixel tolerance if the base element height is less than the default. pixelTolerance = Math.min(defaultPixelTolerance, baseRect.height); // Make sure the affixed element never detaches from the base element. if (offset.top + pixelTolerance > baseRect.bottom || offset.top + affixedRect.height - pixelTolerance < baseRect.top) { offset.top = originalOffsetTop; } break; } return offset; } #getImmediateOverflowParent() { return this.#overflowParents[0]; } #getAutoFitContextParent() { const bodyElement = this.#overflowParents[this.#overflowParents.length - 1]; return this.#config.autoFitContext === SkyAffixAutoFitContext.OverflowParent ? this.#getImmediateOverflowParent() : bodyElement; } #notifyPlacementChange(placement) { if (this.#currentPlacement !== placement) { this.#currentPlacement = placement ?? undefined; this.#placementChange.next({ placement, }); } } #reset() { this.#removeViewportListeners(); this.#overflowParents = []; this.#config = this.#baseElement = this.#currentPlacement = this.#currentOffset = undefined; } #isNewOffset(offset) { if (this.#currentOffset === undefined) { this.#currentOffset = offset; return true; } if (this.#currentOffset.top === offset.top && this.#currentOffset.left === offset.left) { return false; } this.#currentOffset = offset; return true; } #isBaseElementVisible() { // Can't get here if the base element is undefined. /* istanbul ignore if */ if (!this.#baseElement) { return false; } const baseRect = this.#baseElement.getBoundingClientRect(); return isOffsetPartiallyVisibleWithinParent(this.#viewportRuler, this.#getImmediateOverflowParent(), { top: baseRect.top, left: baseRect.left, right: baseRect.right, bottom: baseRect.bottom, }, this.#config.autoFitOverflowOffset); } #addViewportListeners() { this.#viewportListeners = new Subscription(); // Resize and orientation changes. this.#viewportListeners.add(this.#viewportRuler.change().subscribe(() => { this.#affix(); })); this.#viewportListeners.add(this.#scrollChange.subscribe(() => { this.#affix(); this.#overflowScroll.next(); })); // Listen for scroll events on the window, visual viewport, and any overflow parents. // https://developer.chrome.com/blog/visual-viewport-api/#events-only-fire-when-the-visual-viewport-changes this.#zone.runOutsideAngular(() => { [window, window.visualViewport, ...this.#overflowParents].forEach((parentElement) => { parentElement?.addEventListener('scroll', this.#scrollChangeListener); }); }); } #removeViewportListeners() { this.#viewportListeners?.unsubscribe(); this.#zone.runOutsideAngular(() => { [window, window.visualViewport, ...this.#overflowParents].forEach((parentElement) => { parentElement?.removeEventListener('scroll', this.#scrollChangeListener); }); }); } } class SkyAffixService { #renderer = inject(RendererFactory2).createRenderer(undefined, null); #viewportRuler = inject(ViewportRuler); #zone = inject(NgZone); #layoutViewport = this.#createLayoutViewportShim(inject(DOCUMENT)); ngOnDestroy() { this.#renderer.removeChild(this.#layoutViewport.parentNode, this.#layoutViewport); } /** * Creates an instance of [[SkyAffixer]]. * @param affixed The element to be affixed. */ createAffixer(affixed) { return new SkyAffixer(affixed.nativeElement, this.#renderer, this.#viewportRuler, this.#zone, this.#layoutViewport); } /** * Create a layout viewport element that can be used to determine the relative position * of the visual viewport. Inspired by * https://github.com/WICG/visual-viewport/blob/gh-pages/examples/fixed-to-viewport.html */ #createLayoutViewportShim(doc) { const layoutViewportElement = this.#renderer.createElement('div'); this.#renderer.addClass(layoutViewportElement, 'sky-affix-layout-viewport-shim'); this.#renderer.setStyle(layoutViewportElement, 'width', '100%'); this.#renderer.setStyle(layoutViewportElement, 'height', '100%'); this.#renderer.setStyle(layoutViewportElement, 'position', 'fixed'); this.#renderer.setStyle(layoutViewportElement, 'top', '0'); this.#renderer.setStyle(layoutViewportElement, 'left', '0'); this.#renderer.setStyle(layoutViewportElement, 'visibility', 'hidden'); this.#renderer.setStyle(layoutViewportElement, 'pointerEvents', 'none'); this.#renderer.appendChild(doc.body, layoutViewportElement); return layoutViewportElement; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * Affixes the host element to a base element. */ class SkyAffixDirective { #affixer; #affixService; #elementRef; #ngUnsubscribe; constructor(elementRef, affixService) { /** * Fires when the affixed element's offset changes. */ this.affixOffsetChange = new EventEmitter(); /** * Fires when the affixed element's overflow container is scrolled. */ this.affixOverflowScroll = new EventEmitter(); /** * Fires when the placement value changes. */ this.affixPlacementChange = new EventEmitter(); this.#ngUnsubscribe = new Subject(); this.#elementRef = elementRef; this.#affixService = affixService; } ngOnInit() { this.#affixer = this.#affixService.createAffixer(this.#elementRef); this.#affixer.offsetChange .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((change) => this.affixOffsetChange.emit(change)); this.#affixer.overflowScroll .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((change) => this.affixOverflowScroll.emit(change)); this.#affixer.placementChange .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((change) => this.affixPlacementChange.emit(change)); this.#updateAlignment(); } ngOnChanges(changes) { /* istanbul ignore else */ if (changes['affixAutoFitContext'] || changes['affixAutoFitOverflowOffset'] || changes['affixEnableAutoFit'] || changes['affixHorizontalAlignment'] || changes['affixIsSticky'] || changes['affixPlacement'] || changes['affixPosition'] || changes['affixVerticalAlignment']) { this.#updateAlignment(); } } ngOnDestroy() { this.affixOffsetChange.complete(); this.affixOverflowScroll.complete(); this.affixPlacementChange.complete(); this.#ngUnsubscribe.next(); this.#ngUnsubscribe.complete(); /*istanbul ignore else*/ if (this.#affixer) { this.#affixer.destroy(); this.#affixer = undefined; } } #updateAlignment() { if (this.skyAffixTo && this.#affixer) { this.#affixer.affixTo(this.skyAffixTo, { autoFitContext: this.affixAutoFitContext, autoFitOverflowOffset: this.affixAutoFitOverflowOffset, enableAutoFit: this.affixEnableAutoFit, horizontalAlignment: this.affixHorizontalAlignment, isSticky: this.affixIsSticky, placement: this.affixPlacement, position: this.affixPosition, verticalAlignment: this.affixVerticalAlignment, }); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixDirective, deps: [{ token: i0.ElementRef }, { token: SkyAffixService }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.14", type: SkyAffixDirective, isStandalone: false, selector: "[skyAffixTo]", inputs: { skyAffixTo: "skyAffixTo", affixAutoFitContext: "affixAutoFitContext", affixAutoFitOverflowOffset: "affixAutoFitOverflowOffset", affixEnableAutoFit: "affixEnableAutoFit", affixHorizontalAlignment: "affixHorizontalAlignment", affixIsSticky: "affixIsSticky", affixPlacement: "affixPlacement", affixPosition: "affixPosition", affixVerticalAlignment: "affixVerticalAlignment" }, outputs: { affixOffsetChange: "affixOffsetChange", affixOverflowScroll: "affixOverflowScroll", affixPlacementChange: "affixPlacementChange" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixDirective, decorators: [{ type: Directive, args: [{ selector: '[skyAffixTo]', standalone: false, }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: SkyAffixService }], propDecorators: { skyAffixTo: [{ type: Input }], affixAutoFitContext: [{ type: Input }], affixAutoFitOverflowOffset: [{ type: Input }], affixEnableAutoFit: [{ type: Input }], affixHorizontalAlignment: [{ type: Input }], affixIsSticky: [{ type: Input }], affixPlacement: [{ type: Input }], affixPosition: [{ type: Input }], affixVerticalAlignment: [{ type: Input }], affixOffsetChange: [{ type: Output }], affixOverflowScroll: [{ type: Output }], affixPlacementChange: [{ type: Output }] } }); class SkyAffixModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixModule, declarations: [SkyAffixDirective], imports: [CommonModule], exports: [SkyAffixDirective] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixModule, imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAffixModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule], exports: [SkyAffixDirective], declarations: [SkyAffixDirective], }] }] }); /** * @internal * An API to provide information about a parent component's content to child components. * For example, toolbar can use this to provide its child components with a list * descriptor they can use to construct aria labels, or tree view can provide the node * name to its context menus. */ class SkyContentInfoProvider { #contentInfo = new ReplaySubject(1); #currentValue = {}; patchInfo(value) { const newValue = { ...this.#currentValue, ...value, }; this.#currentValue = newValue; this.#contentInfo.next(newValue); } getInfo() { return this.#contentInfo.asObservable(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyContentInfoProvider, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyContentInfoProvider }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyContentInfoProvider, decorators: [{ type: Injectable }] }); /** * @internal * An API to provide default Angular component input values to child components. */ class SkyDefaultInputProvider { #props = {}; setValue(componentName, inputName, value) { const subject = this.#getSubject(componentName, inputName); subject.next(value); } getValue(componentName, inputName) { const inputDefault = this.#getSubject(componentName, inputName); return inputDefault.asObservable(); } #getSubject(componentName, inputName) { const componentSubjects = this.#props[componentName] || {}; const inputSubject = componentSubjects[inputName]; if (!inputSubject) { componentSubjects[inputName] = new ReplaySubject(1); this.#props[componentName] = componentSubjects; } return componentSubjects[inputName]; } } /** * Represents a single item added to the dock. */ class SkyDockItem { /** * An event that emits when the item is removed from the dock. */ get destroyed() { return this.#destroyedObs; } #destroyed = new Subject(); #destroyedObs; /** * @param componentInstance The item's component instance. * @param stackOrder The assigned stack order of the docked item. */ constructor(componentInstance, stackOrder) { this.componentInstance = componentInstance; this.stackOrder = stackOrder; this.#destroyedObs = this.#destroyed.asObservable(); } /** * Removes the item from the dock. */ destroy() { this.#destroyed.next(); this.#destroyed.complete(); } } /** * The location on the page where the dock component should be rendered. */ var SkyDockLocation; (function (SkyDockLocation) { /** * Renders the dock component before a given element. */ SkyDockLocation[SkyDockLocation["BeforeElement"] = 0] = "BeforeElement"; /** * Renders the dock component as the last element inside the BODY element. */ SkyDockLocation[SkyDockLocation["BodyBottom"] = 1] = "BodyBottom"; /** * Renders the dock component as the last element inside a given element. */ SkyDockLocation[SkyDockLocation["ElementBottom"] = 2] = "ElementBottom"; })(SkyDockLocation || (SkyDockLocation = {})); /** * @deprecated The `SkyDockModule` is no longer needed and can be removed from your application. * @internal */ class SkyDockModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyDockModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.14", ngImport: i0, type: SkyDockModule }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyDockModule }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyDockModule, decorators: [{ type: NgModule, args: [{}] }] }); /** * The location on the page where the dynamic component should be rendered. */ var SkyDynamicComponentLocation; (function (SkyDynamicComponentLocation) { /** * Renders the dynamic component before a given element. */ SkyDynamicComponentLocation[SkyDynamicComponentLocation["BeforeElement"] = 0] = "BeforeElement"; /** * Renders the dynamic component as the last element inside the BODY element. */ SkyDynamicComponentLocation[SkyDynamicComponentLocation["BodyBottom"] = 1] = "BodyBottom"; /** * Renders the dynamic component as the first element inside the BODY element. */ SkyDynamicComponentLocation[SkyDynamicComponentLocation["BodyTop"] = 2] = "BodyTop"; /** * Renders the dynamic component as the last element inside a given element. */ SkyDynamicComponentLocation[SkyDynamicComponentLocation["ElementBottom"] = 3] = "ElementBottom"; /** * Renders the dynamic component as the first element inside a given element. */ SkyDynamicComponentLocation[SkyDynamicComponentLocation["ElementTop"] = 4] = "ElementTop"; })(SkyDynamicComponentLocation || (SkyDynamicComponentLocation = {})); /** * @internal */ function getWindow() { return window; } /** * The application window reference service references the global window variable. * After users inject SkyAppWindowRef into a component, they can use the service to interact with * window properties and event handlers by referencing its nativeWindow property. */ class SkyAppWindowRef { /** * The global `window` variable. */ get nativeWindow() { return getWindow(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAppWindowRef, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAppWindowRef, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: SkyAppWindowRef, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * Angular service for creating and rendering a dynamic component. * @internal */ class SkyDynamicComponentService { #applicationRef; #renderer; #windowRef; #environmentInjector = inject(EnvironmentInjector); constructor(applicationRef, windowRef, rendererFactory) { this.#applicationRef = applicationRef; this.#windowRef = windowRef; // Based on suggestions from https://github.com/angular/angular/issues/17824 // for accessing an instance of Renderer2 in a service since Renderer2 can't // be injected into a service. Passing undefined for both parameters results // in the default renderer which is what we want here. this.#renderer = rendererFactory.createRenderer(undefined, null); } /** * Creates an instance of the specified component and adds it to the specified location * on the page. */ createComponent(componentType, options