@skyux/core
Version:
This library was generated with [Nx](https://nx.dev).
1,240 lines (1,220 loc) • 207 kB
JavaScript
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