@ngbracket/ngx-layout
Version:
ngbracket/ngx-layout =======
2,150 lines • 82.1 kB
JavaScript
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import * as i0 from '@angular/core';
import { DOCUMENT, PLATFORM_ID, APP_BOOTSTRAP_LISTENER, NgModule, Injectable, InjectionToken, inject, Inject, CSP_NONCE, Optional, Directive } from '@angular/core';
import { BehaviorSubject, Observable, merge, Subject, asapScheduler, of, fromEvent } from 'rxjs';
import { extendObject, applyCssPrefixes, buildLayoutCSS } from '@ngbracket/ngx-layout/_private-utils';
import { filter, tap, debounceTime, switchMap, map, distinctUntilChanged, takeUntil, take } from 'rxjs/operators';
/**
* Class instances emitted [to observers] for each mql notification
*/
class MediaChange {
/**
* @param matches whether the mediaQuery is currently activated
* @param mediaQuery e.g. (min-width: 600px) and (max-width: 959px)
* @param mqAlias e.g. gt-sm, md, gt-lg
* @param suffix e.g. GtSM, Md, GtLg
* @param priority the priority of activation for the given breakpoint
*/
constructor(matches = false, mediaQuery = 'all', mqAlias = '', suffix = '', priority = 0) {
this.matches = matches;
this.mediaQuery = mediaQuery;
this.mqAlias = mqAlias;
this.suffix = suffix;
this.priority = priority;
this.property = '';
}
/** Create an exact copy of the MediaChange */
clone() {
return new MediaChange(this.matches, this.mediaQuery, this.mqAlias, this.suffix);
}
}
/**
* For the specified MediaChange, make sure it contains the breakpoint alias
* and suffix (if available).
*/
function mergeAlias(dest, source) {
dest = dest?.clone() ?? new MediaChange();
if (source) {
dest.mqAlias = source.alias;
dest.mediaQuery = source.mediaQuery;
dest.suffix = source.suffix;
dest.priority = source.priority;
}
return dest;
}
/**
* Find all of the server-generated stylings, if any, and remove them
* This will be in the form of inline classes and the style block in the
* head of the DOM
*/
function removeStyles(_document, platformId) {
return () => {
if (isPlatformBrowser(platformId)) {
const elements = Array.from(_document.querySelectorAll(`[class*=${CLASS_NAME}]`));
// RegExp constructor should only be used if passing a variable to the constructor.
// When using static regular expression it is more performant to use reg exp literal.
// This is also needed to provide Safari 9 compatibility, please see
// https://stackoverflow.com/questions/37919802 for more discussion.
const classRegex = /\bflex-layout-.+?\b/g;
elements.forEach((el) => {
el.classList.contains(`${CLASS_NAME}ssr`) && el.parentNode
? el.parentNode.removeChild(el)
: el.className.replace(classRegex, '');
});
}
};
}
/**
* Provider to remove SSR styles on the browser
*/
const BROWSER_PROVIDER = {
provide: APP_BOOTSTRAP_LISTENER,
useFactory: removeStyles,
deps: [DOCUMENT, PLATFORM_ID],
multi: true,
};
const CLASS_NAME = 'flex-layout-';
/**
* *****************************************************************
* Define module for common Angular Layout utilities
* *****************************************************************
*/
class CoreModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.1.2", ngImport: i0, type: CoreModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CoreModule, providers: [BROWSER_PROVIDER] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: CoreModule, decorators: [{
type: NgModule,
args: [{
providers: [BROWSER_PROVIDER],
}]
}] });
/**
* Utility to emulate a CSS stylesheet
*
* This utility class stores all of the styles for a given HTML element
* as a readonly `stylesheet` map.
*/
class StylesheetMap {
constructor() {
this.stylesheet = new Map();
}
/**
* Add an individual style to an HTML element
*/
addStyleToElement(element, style, value) {
const stylesheet = this.stylesheet.get(element);
if (stylesheet) {
stylesheet.set(style, value);
}
else {
this.stylesheet.set(element, new Map([[style, value]]));
}
}
/**
* Clear the virtual stylesheet
*/
clearStyles() {
this.stylesheet.clear();
}
/**
* Retrieve a given style for an HTML element
*/
getStyleForElement(el, styleName) {
const styles = this.stylesheet.get(el);
let value = '';
if (styles) {
const style = styles.get(styleName);
if (typeof style === 'number' || typeof style === 'string') {
value = style + '';
}
}
return value;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StylesheetMap, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StylesheetMap, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StylesheetMap, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
const BREAKPOINT = new InjectionToken('Flex Layout token, collect all breakpoints into one provider', {
providedIn: 'root',
factory: () => null,
});
const DEFAULT_CONFIG = {
addFlexToParent: true,
addOrientationBps: false,
disableDefaultBps: false,
disableVendorPrefixes: false,
serverLoaded: false,
useColumnBasisZero: true,
printWithBreakpoints: [],
mediaTriggerAutoRestore: true,
ssrObserveBreakpoints: [],
// This is disabled by default because otherwise the multiplier would
// run for all users, regardless of whether they're using this feature.
// Instead, we disable it by default, which requires this ugly cast.
multiplier: undefined,
defaultUnit: 'px',
detectLayoutDisplay: false,
};
const LAYOUT_CONFIG = new InjectionToken('Flex Layout token, config options for the library', {
providedIn: 'root',
factory: () => DEFAULT_CONFIG,
});
/**
* Token that is provided to tell whether the FlexLayoutServerModule
* has been included in the bundle
*
* NOTE: This can be manually provided to disable styles when using SSR
*/
const SERVER_TOKEN = new InjectionToken('FlexLayoutServerLoaded', {
providedIn: 'root',
factory: () => false,
});
/** HOF to sort the breakpoints by descending priority */
function sortDescendingPriority(a, b) {
const priorityA = a ? a.priority || 0 : 0;
const priorityB = b ? b.priority || 0 : 0;
return priorityB - priorityA;
}
/** HOF to sort the breakpoints by ascending priority */
function sortAscendingPriority(a, b) {
const pA = a.priority || 0;
const pB = b.priority || 0;
return pA - pB;
}
const ALIAS_DELIMITERS = /(\.|-|_)/g;
function firstUpperCase(part) {
let first = part.length > 0 ? part.charAt(0) : '';
let remainder = part.length > 1 ? part.slice(1) : '';
return first.toUpperCase() + remainder;
}
/**
* Converts snake-case to SnakeCase.
* @param name Text to UpperCamelCase
*/
function camelCase(name) {
return name
.replace(ALIAS_DELIMITERS, '|')
.split('|')
.map(firstUpperCase)
.join('');
}
/**
* For each breakpoint, ensure that a Suffix is defined;
* fallback to UpperCamelCase the unique Alias value
*/
function validateSuffixes(list) {
list.forEach((bp) => {
if (!bp.suffix) {
bp.suffix = camelCase(bp.alias); // create Suffix value based on alias
bp.overlapping = !!bp.overlapping; // ensure default value
}
});
return list;
}
/**
* Merge a custom breakpoint list with the default list based on unique alias values
* - Items are added if the alias is not in the default list
* - Items are merged with the custom override if the alias exists in the default list
*/
function mergeByAlias(defaults, custom = []) {
const dict = {};
defaults.forEach((bp) => {
dict[bp.alias] = bp;
});
// Merge custom breakpoints
custom.forEach((bp) => {
if (dict[bp.alias]) {
extendObject(dict[bp.alias], bp);
}
else {
dict[bp.alias] = bp;
}
});
return validateSuffixes(Object.keys(dict).map((k) => dict[k]));
}
/**
* NOTE: Smaller ranges have HIGHER priority since the match is more specific
*/
const DEFAULT_BREAKPOINTS = [
{
alias: 'xs',
mediaQuery: 'screen and (min-width: 0px) and (max-width: 599.98px)',
priority: 1000,
},
{
alias: 'sm',
mediaQuery: 'screen and (min-width: 600px) and (max-width: 959.98px)',
priority: 900,
},
{
alias: 'md',
mediaQuery: 'screen and (min-width: 960px) and (max-width: 1279.98px)',
priority: 800,
},
{
alias: 'lg',
mediaQuery: 'screen and (min-width: 1280px) and (max-width: 1919.98px)',
priority: 700,
},
{
alias: 'xl',
mediaQuery: 'screen and (min-width: 1920px) and (max-width: 4999.98px)',
priority: 600,
},
{
alias: 'lt-sm',
overlapping: true,
mediaQuery: 'screen and (max-width: 599.98px)',
priority: 950,
},
{
alias: 'lt-md',
overlapping: true,
mediaQuery: 'screen and (max-width: 959.98px)',
priority: 850,
},
{
alias: 'lt-lg',
overlapping: true,
mediaQuery: 'screen and (max-width: 1279.98px)',
priority: 750,
},
{
alias: 'lt-xl',
overlapping: true,
priority: 650,
mediaQuery: 'screen and (max-width: 1919.98px)',
},
{
alias: 'gt-xs',
overlapping: true,
mediaQuery: 'screen and (min-width: 600px)',
priority: -950,
},
{
alias: 'gt-sm',
overlapping: true,
mediaQuery: 'screen and (min-width: 960px)',
priority: -850,
},
{
alias: 'gt-md',
overlapping: true,
mediaQuery: 'screen and (min-width: 1280px)',
priority: -750,
},
{
alias: 'gt-lg',
overlapping: true,
mediaQuery: 'screen and (min-width: 1920px)',
priority: -650,
},
];
/* tslint:disable */
const HANDSET_PORTRAIT = '(orientation: portrait) and (max-width: 599.98px)';
const HANDSET_LANDSCAPE = '(orientation: landscape) and (max-width: 959.98px)';
const TABLET_PORTRAIT = '(orientation: portrait) and (min-width: 600px) and (max-width: 839.98px)';
const TABLET_LANDSCAPE = '(orientation: landscape) and (min-width: 960px) and (max-width: 1279.98px)';
const WEB_PORTRAIT = '(orientation: portrait) and (min-width: 840px)';
const WEB_LANDSCAPE = '(orientation: landscape) and (min-width: 1280px)';
const ScreenTypes = {
HANDSET: `${HANDSET_PORTRAIT}, ${HANDSET_LANDSCAPE}`,
TABLET: `${TABLET_PORTRAIT} , ${TABLET_LANDSCAPE}`,
WEB: `${WEB_PORTRAIT}, ${WEB_LANDSCAPE} `,
HANDSET_PORTRAIT: `${HANDSET_PORTRAIT}`,
TABLET_PORTRAIT: `${TABLET_PORTRAIT} `,
WEB_PORTRAIT: `${WEB_PORTRAIT}`,
HANDSET_LANDSCAPE: `${HANDSET_LANDSCAPE}`,
TABLET_LANDSCAPE: `${TABLET_LANDSCAPE}`,
WEB_LANDSCAPE: `${WEB_LANDSCAPE}`,
};
/**
* Extended Breakpoints for handset/tablets with landscape or portrait orientations
*/
const ORIENTATION_BREAKPOINTS = [
{ alias: 'handset', priority: 2000, mediaQuery: ScreenTypes.HANDSET },
{
alias: 'handset.landscape',
priority: 2000,
mediaQuery: ScreenTypes.HANDSET_LANDSCAPE,
},
{
alias: 'handset.portrait',
priority: 2000,
mediaQuery: ScreenTypes.HANDSET_PORTRAIT,
},
{ alias: 'tablet', priority: 2100, mediaQuery: ScreenTypes.TABLET },
{
alias: 'tablet.landscape',
priority: 2100,
mediaQuery: ScreenTypes.TABLET_LANDSCAPE,
},
{
alias: 'tablet.portrait',
priority: 2100,
mediaQuery: ScreenTypes.TABLET_PORTRAIT,
},
{
alias: 'web',
priority: 2200,
mediaQuery: ScreenTypes.WEB,
overlapping: true,
},
{
alias: 'web.landscape',
priority: 2200,
mediaQuery: ScreenTypes.WEB_LANDSCAPE,
overlapping: true,
},
{
alias: 'web.portrait',
priority: 2200,
mediaQuery: ScreenTypes.WEB_PORTRAIT,
overlapping: true,
},
];
/**
* Injection token unique to the flex-layout library.
* Use this token when build a custom provider (see below).
*/
const BREAKPOINTS = new InjectionToken('Token (@ngbracket/ngx-layout) Breakpoints', {
providedIn: 'root',
factory: () => {
const breakpoints = inject(BREAKPOINT);
const layoutConfig = inject(LAYOUT_CONFIG);
const bpFlattenArray = [].concat.apply([], (breakpoints || []).map((v) => Array.isArray(v) ? v : [v]));
const builtIns = (layoutConfig.disableDefaultBps ? [] : DEFAULT_BREAKPOINTS).concat(layoutConfig.addOrientationBps ? ORIENTATION_BREAKPOINTS : []);
return mergeByAlias(builtIns, bpFlattenArray);
},
});
/**
* Registry of 1..n MediaQuery breakpoint ranges
* This is published as a provider and may be overridden from custom, application-specific ranges
*
*/
class BreakPointRegistry {
constructor(list) {
/**
* Memoized BreakPoint Lookups
*/
this.findByMap = new Map();
this.items = [...list].sort(sortAscendingPriority);
}
/**
* Search breakpoints by alias (e.g. gt-xs)
*/
findByAlias(alias) {
return !alias
? null
: this.findWithPredicate(alias, (bp) => bp.alias === alias);
}
findByQuery(query) {
return this.findWithPredicate(query, (bp) => bp.mediaQuery === query);
}
/**
* Get all the breakpoints whose ranges could overlapping `normal` ranges;
* e.g. gt-sm overlaps md, lg, and xl
*/
get overlappings() {
return this.items.filter((it) => it.overlapping);
}
/**
* Get list of all registered (non-empty) breakpoint aliases
*/
get aliases() {
return this.items.map((it) => it.alias);
}
/**
* Aliases are mapped to properties using suffixes
* e.g. 'gt-sm' for property 'layout' uses suffix 'GtSm'
* for property layoutGtSM.
*/
get suffixes() {
return this.items.map((it) => it?.suffix ?? '');
}
/**
* Memoized lookup using custom predicate function
*/
findWithPredicate(key, searchFn) {
let response = this.findByMap.get(key);
if (!response) {
response = this.items.find(searchFn) ?? null;
this.findByMap.set(key, response);
}
return response ?? null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: BreakPointRegistry, deps: [{ token: BREAKPOINTS }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: BreakPointRegistry, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: BreakPointRegistry, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [BREAKPOINTS]
}] }] });
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* MediaMonitor configures listeners to mediaQuery changes and publishes an Observable facade to
* convert mediaQuery change callbacks to subscriber notifications. These notifications will be
* performed within the ng Zone to trigger change detections and component updates.
*
* NOTE: both mediaQuery activations and de-activations are announced in notifications
*/
class MatchMedia {
constructor(_zone, _platformId, _document, _nonce) {
this._zone = _zone;
this._platformId = _platformId;
this._document = _document;
this._nonce = _nonce;
/** Initialize source with 'all' so all non-responsive APIs trigger style updates */
this.source = new BehaviorSubject(new MediaChange(true));
this.registry = new Map();
this.pendingRemoveListenerFns = [];
this._observable$ = this.source.asObservable();
}
/**
* Publish list of all current activations
*/
get activations() {
const results = [];
this.registry.forEach((mql, key) => {
if (mql.matches) {
results.push(key);
}
});
return results;
}
/**
* For the specified mediaQuery?
*/
isActive(mediaQuery) {
const mql = this.registry.get(mediaQuery);
return (mql?.matches ?? this.registerQuery(mediaQuery).some((m) => m.matches));
}
/**
* External observers can watch for all (or a specific) mql changes.
* Typically used by the MediaQueryAdaptor; optionally available to components
* who wish to use the MediaMonitor as mediaMonitor$ observable service.
*
* Use deferred registration process to register breakpoints only on subscription
* This logic also enforces logic to register all mediaQueries BEFORE notify
* subscribers of notifications.
*/
observe(mqList, filterOthers = false) {
if (mqList && mqList.length) {
const matchMedia$ = this._observable$.pipe(filter((change) => !filterOthers ? true : mqList.indexOf(change.mediaQuery) > -1));
const registration$ = new Observable((observer) => {
// tslint:disable-line:max-line-length
const matches = this.registerQuery(mqList);
if (matches.length) {
const lastChange = matches.pop();
matches.forEach((e) => {
observer.next(e);
});
this.source.next(lastChange); // last match is cached
}
observer.complete();
});
return merge(registration$, matchMedia$);
}
return this._observable$;
}
/**
* Based on the BreakPointRegistry provider, register internal listeners for each unique
* mediaQuery. Each listener emits specific MediaChange data to observers
*/
registerQuery(mediaQuery) {
const list = Array.isArray(mediaQuery) ? mediaQuery : [mediaQuery];
const matches = [];
buildQueryCss(list, this._document, this._nonce);
list.forEach((query) => {
const onMQLEvent = (e) => {
this._zone.run(() => this.source.next(new MediaChange(e.matches, query)));
};
let mql = this.registry.get(query);
if (!mql) {
mql = this.buildMQL(query);
mql.addListener(onMQLEvent);
this.pendingRemoveListenerFns.push(() => mql.removeListener(onMQLEvent));
this.registry.set(query, mql);
}
if (mql.matches) {
matches.push(new MediaChange(true, query));
}
});
return matches;
}
ngOnDestroy() {
let fn;
while ((fn = this.pendingRemoveListenerFns.pop())) {
fn();
}
}
/**
* Call window.matchMedia() to build a MediaQueryList; which
* supports 0..n listeners for activation/deactivation
*/
buildMQL(query) {
return constructMql(query, isPlatformBrowser(this._platformId));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MatchMedia, deps: [{ token: i0.NgZone }, { token: PLATFORM_ID }, { token: DOCUMENT }, { token: CSP_NONCE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MatchMedia, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MatchMedia, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: i0.NgZone }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [CSP_NONCE]
}] }] });
/**
* Private global registry for all dynamically-created, injected style tags
* @see prepare(query)
*/
const ALL_STYLES = {};
/**
* For Webkit engines that only trigger the MediaQueryList Listener
* when there is at least one CSS selector for the respective media query.
*
* @param mediaQueries
* @param _document
*/
function buildQueryCss(mediaQueries, _document, _nonce) {
const list = mediaQueries.filter((it) => !ALL_STYLES[it]);
if (list.length > 0) {
const query = list.join(', ');
try {
const styleEl = _document.createElement('style');
styleEl.setAttribute('type', 'text/css');
if (_nonce) {
styleEl.setAttribute('nonce', _nonce);
}
if (!styleEl.styleSheet) {
const cssText = `
/*
@ngbracket/ngx-layout - workaround for possible browser quirk with mediaQuery listeners
see http://bit.ly/2sd4HMP
*/
@media ${query} {.fx-query-test{ }}
`;
styleEl.appendChild(_document.createTextNode(cssText));
}
_document.head.appendChild(styleEl);
// Store in private global registry
list.forEach((mq) => (ALL_STYLES[mq] = styleEl));
}
catch (e) {
console.error(e);
}
}
}
function buildMockMql(query) {
const et = new EventTarget();
et.matches = query === 'all' || query === '';
et.media = query;
et.addListener = () => { };
et.removeListener = () => { };
et.addEventListener = () => { };
et.dispatchEvent = () => false;
et.onchange = null;
return et;
}
function constructMql(query, isBrowser) {
const canListen = isBrowser && !!window.matchMedia('all').addListener;
return canListen ? window.matchMedia(query) : buildMockMql(query);
}
const PRINT = 'print';
const BREAKPOINT_PRINT = {
alias: PRINT,
mediaQuery: PRINT,
priority: 1000,
};
/**
* PrintHook - Use to intercept print MediaQuery activations and force
* layouts to render with the specified print alias/breakpoint
*
* Used in MediaMarshaller and MediaObserver
*/
class PrintHook {
constructor(breakpoints, layoutConfig, _document) {
this.breakpoints = breakpoints;
this.layoutConfig = layoutConfig;
this._document = _document;
// registeredBeforeAfterPrintHooks tracks if we registered the `beforeprint`
// and `afterprint` event listeners.
this.registeredBeforeAfterPrintHooks = false;
// isPrintingBeforeAfterEvent is used to track if we are printing from within
// a `beforeprint` event handler. This prevents the typical `stopPrinting`
// form `interceptEvents` so that printing is not stopped while the dialog
// is still open. This is an extension of the `isPrinting` property on
// browsers which support `beforeprint` and `afterprint` events.
this.isPrintingBeforeAfterEvent = false;
this.beforePrintEventListeners = [];
this.afterPrintEventListeners = [];
this.formerActivations = null;
// Is this service currently in print mode
this.isPrinting = false;
this.queue = new PrintQueue();
this.deactivations = [];
}
/** Add 'print' mediaQuery: to listen for matchMedia activations */
withPrintQuery(queries) {
return [...queries, PRINT];
}
/** Is the MediaChange event for any 'print' @media */
isPrintEvent(e) {
return e.mediaQuery.startsWith(PRINT);
}
/** What is the desired mqAlias to use while printing? */
get printAlias() {
return [...(this.layoutConfig.printWithBreakpoints ?? [])];
}
/** Lookup breakpoints associated with print aliases. */
get printBreakPoints() {
return this.printAlias
.map((alias) => this.breakpoints.findByAlias(alias))
.filter((bp) => bp !== null);
}
/** Lookup breakpoint associated with mediaQuery */
getEventBreakpoints({ mediaQuery }) {
const bp = this.breakpoints.findByQuery(mediaQuery);
const list = bp ? [...this.printBreakPoints, bp] : this.printBreakPoints;
return list.sort(sortDescendingPriority);
}
/** Update event with printAlias mediaQuery information */
updateEvent(event) {
let bp = this.breakpoints.findByQuery(event.mediaQuery);
if (this.isPrintEvent(event)) {
// Reset from 'print' to first (highest priority) print breakpoint
bp = this.getEventBreakpoints(event)[0];
event.mediaQuery = bp?.mediaQuery ?? '';
}
return mergeAlias(event, bp);
}
// registerBeforeAfterPrintHooks registers a `beforeprint` event hook so we can
// trigger print styles synchronously and apply proper layout styles.
// It is a noop if the hooks have already been registered or if the document's
// `defaultView` is not available.
registerBeforeAfterPrintHooks(target) {
// `defaultView` may be null when rendering on the server or in other contexts.
if (!this._document.defaultView || this.registeredBeforeAfterPrintHooks) {
return;
}
this.registeredBeforeAfterPrintHooks = true;
const beforePrintListener = () => {
// If we aren't already printing, start printing and update the styles as
// if there was a regular print `MediaChange`(from matchMedia).
if (!this.isPrinting) {
this.isPrintingBeforeAfterEvent = true;
this.startPrinting(target, this.getEventBreakpoints(new MediaChange(true, PRINT)));
target.updateStyles();
}
};
const afterPrintListener = () => {
// If we aren't already printing, start printing and update the styles as
// if there was a regular print `MediaChange`(from matchMedia).
this.isPrintingBeforeAfterEvent = false;
if (this.isPrinting) {
this.stopPrinting(target);
target.updateStyles();
}
};
// Could we have teardown logic to remove if there are no print listeners being used?
this._document.defaultView.addEventListener('beforeprint', beforePrintListener);
this._document.defaultView.addEventListener('afterprint', afterPrintListener);
this.beforePrintEventListeners.push(beforePrintListener);
this.afterPrintEventListeners.push(afterPrintListener);
}
/**
* Prepare RxJS tap operator with partial application
* @return pipeable tap predicate
*/
interceptEvents(target) {
return (event) => {
if (this.isPrintEvent(event)) {
if (event.matches && !this.isPrinting) {
this.startPrinting(target, this.getEventBreakpoints(event));
target.updateStyles();
}
else if (!event.matches &&
this.isPrinting &&
!this.isPrintingBeforeAfterEvent) {
this.stopPrinting(target);
target.updateStyles();
}
return;
}
this.collectActivations(target, event);
};
}
/** Stop mediaChange event propagation in event streams */
blockPropagation() {
return (event) => {
return !(this.isPrinting || this.isPrintEvent(event));
};
}
/**
* Save current activateBreakpoints (for later restore)
* and substitute only the printAlias breakpoint
*/
startPrinting(target, bpList) {
this.isPrinting = true;
this.formerActivations = target.activatedBreakpoints;
target.activatedBreakpoints = this.queue.addPrintBreakpoints(bpList);
}
/** For any print de-activations, reset the entire print queue */
stopPrinting(target) {
target.activatedBreakpoints = this.deactivations;
this.deactivations = [];
this.formerActivations = null;
this.queue.clear();
this.isPrinting = false;
}
/**
* To restore pre-Print Activations, we must capture the proper
* list of breakpoint activations BEFORE print starts. OnBeforePrint()
* is supported; so 'print' mediaQuery activations are used as a fallback
* in browsers without `beforeprint` support.
*
* > But activated breakpoints are deactivated BEFORE 'print' activation.
*
* Let's capture all de-activations using the following logic:
*
* When not printing:
* - clear cache when activating non-print breakpoint
* - update cache (and sort) when deactivating
*
* When printing:
* - sort and save when starting print
* - restore as activatedTargets and clear when stop printing
*/
collectActivations(target, event) {
if (!this.isPrinting || this.isPrintingBeforeAfterEvent) {
if (!this.isPrintingBeforeAfterEvent) {
// Only clear deactivations if we aren't printing from a `beforeprint` event.
// Otherwise, this will clear before `stopPrinting()` is called to restore
// the pre-Print Activations.
this.deactivations = [];
return;
}
if (!event.matches) {
const bp = this.breakpoints.findByQuery(event.mediaQuery);
// Deactivating a breakpoint
if (bp) {
const hasFormerBp = this.formerActivations && this.formerActivations.includes(bp);
const wasActivated = !this.formerActivations && target.activatedBreakpoints.includes(bp);
const shouldDeactivate = hasFormerBp || wasActivated;
if (shouldDeactivate) {
this.deactivations.push(bp);
this.deactivations.sort(sortDescendingPriority);
}
}
}
}
}
/** Teardown logic for the service. */
ngOnDestroy() {
if (this._document.defaultView) {
this.beforePrintEventListeners.forEach((l) => this._document.defaultView.removeEventListener('beforeprint', l));
this.afterPrintEventListeners.forEach((l) => this._document.defaultView.removeEventListener('afterprint', l));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: PrintHook, deps: [{ token: BreakPointRegistry }, { token: LAYOUT_CONFIG }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: PrintHook, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: PrintHook, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: BreakPointRegistry }, { type: undefined, decorators: [{
type: Inject,
args: [LAYOUT_CONFIG]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }] });
// ************************************************************************
// Internal Utility class 'PrintQueue'
// ************************************************************************
/**
* Utility class to manage print breakpoints + activatedBreakpoints
* with correct sorting WHILE printing
*/
class PrintQueue {
constructor() {
/** Sorted queue with prioritized print breakpoints */
this.printBreakpoints = [];
}
addPrintBreakpoints(bpList) {
bpList.push(BREAKPOINT_PRINT);
bpList.sort(sortDescendingPriority);
bpList.forEach((bp) => this.addBreakpoint(bp));
return this.printBreakpoints;
}
/** Add Print breakpoint to queue */
addBreakpoint(bp) {
if (!!bp) {
const bpInList = this.printBreakpoints.find((it) => it.mediaQuery === bp.mediaQuery);
if (bpInList === undefined) {
// If this is a `printAlias` breakpoint, then append. If a true 'print' breakpoint,
// register as highest priority in the queue
this.printBreakpoints = isPrintBreakPoint(bp)
? [bp, ...this.printBreakpoints]
: [...this.printBreakpoints, bp];
}
}
}
/** Restore original activated breakpoints and clear internal caches */
clear() {
this.printBreakpoints = [];
}
}
// ************************************************************************
// Internal Utility methods
// ************************************************************************
/** Only support intercept queueing if the Breakpoint is a print @media query */
function isPrintBreakPoint(bp) {
return bp?.mediaQuery.startsWith(PRINT) ?? false;
}
/**
* MediaMarshaller - register responsive values from directives and
* trigger them based on media query events
*/
class MediaMarshaller {
get activatedAlias() {
return this.activatedBreakpoints[0]?.alias ?? '';
}
set activatedBreakpoints(bps) {
this._activatedBreakpoints = [...bps];
}
get activatedBreakpoints() {
return [...this._activatedBreakpoints];
}
set useFallbacks(value) {
this._useFallbacks = value;
}
constructor(matchMedia, breakpoints, hook) {
this.matchMedia = matchMedia;
this.breakpoints = breakpoints;
this.hook = hook;
this._useFallbacks = true;
this._activatedBreakpoints = [];
this.elementMap = new Map();
this.elementKeyMap = new WeakMap();
this.watcherMap = new WeakMap(); // special triggers to update elements
this.updateMap = new WeakMap(); // callback functions to update styles
this.clearMap = new WeakMap(); // callback functions to clear styles
this.subject = new Subject();
this.observeActivations();
}
/**
* Update styles on breakpoint activates or deactivates
* @param mc
*/
onMediaChange(mc) {
const bp = this.findByQuery(mc.mediaQuery);
if (bp) {
mc = mergeAlias(mc, bp);
const bpIndex = this.activatedBreakpoints.indexOf(bp);
if (mc.matches && bpIndex === -1) {
this._activatedBreakpoints.push(bp);
this._activatedBreakpoints.sort(sortDescendingPriority);
this.updateStyles();
}
else if (!mc.matches && bpIndex !== -1) {
// Remove the breakpoint when it's deactivated
this._activatedBreakpoints.splice(bpIndex, 1);
this._activatedBreakpoints.sort(sortDescendingPriority);
this.updateStyles();
}
}
}
/**
* initialize the marshaller with necessary elements for delegation on an element
* @param element
* @param key
* @param updateFn optional callback so that custom bp directives don't have to re-provide this
* @param clearFn optional callback so that custom bp directives don't have to re-provide this
* @param extraTriggers other triggers to force style updates (e.g. layout, directionality, etc)
*/
init(element, key, updateFn, clearFn, extraTriggers = []) {
initBuilderMap(this.updateMap, element, key, updateFn);
initBuilderMap(this.clearMap, element, key, clearFn);
this.buildElementKeyMap(element, key);
this.watchExtraTriggers(element, key, extraTriggers);
}
/**
* get the value for an element and key and optionally a given breakpoint
* @param element
* @param key
* @param bp
*/
getValue(element, key, bp) {
const bpMap = this.elementMap.get(element);
if (bpMap) {
const values = bp !== undefined ? bpMap.get(bp) : this.getActivatedValues(bpMap, key);
if (values) {
return values.get(key);
}
}
return undefined;
}
/**
* whether the element has values for a given key
* @param element
* @param key
*/
hasValue(element, key) {
const bpMap = this.elementMap.get(element);
if (bpMap) {
const values = this.getActivatedValues(bpMap, key);
if (values) {
return values.get(key) !== undefined || false;
}
}
return false;
}
/**
* Set the value for an input on a directive
* @param element the element in question
* @param key the type of the directive (e.g. flex, layout-gap, etc)
* @param bp the breakpoint suffix (empty string = default)
* @param val the value for the breakpoint
*/
setValue(element, key, val, bp) {
let bpMap = this.elementMap.get(element);
if (!bpMap) {
bpMap = new Map().set(bp, new Map().set(key, val));
this.elementMap.set(element, bpMap);
}
else {
const values = (bpMap.get(bp) ?? new Map()).set(key, val);
bpMap.set(bp, values);
this.elementMap.set(element, bpMap);
}
const value = this.getValue(element, key);
if (value !== undefined) {
this.updateElement(element, key, value);
}
}
/** Track element value changes for a specific key */
trackValue(element, key) {
return this.subject
.asObservable()
.pipe(filter((v) => v.element === element && v.key === key));
}
/** update all styles for all elements on the current breakpoint */
updateStyles() {
this.elementMap.forEach((bpMap, el) => {
const keyMap = new Set(this.elementKeyMap.get(el));
let valueMap = this.getActivatedValues(bpMap);
if (valueMap) {
valueMap.forEach((v, k) => {
this.updateElement(el, k, v);
keyMap.delete(k);
});
}
keyMap.forEach((k) => {
valueMap = this.getActivatedValues(bpMap, k);
if (valueMap) {
const value = valueMap.get(k);
this.updateElement(el, k, value);
}
else {
this.clearElement(el, k);
}
});
});
}
/**
* clear the styles for a given element
* @param element
* @param key
*/
clearElement(element, key) {
const builders = this.clearMap.get(element);
if (builders) {
const clearFn = builders.get(key);
if (!!clearFn) {
clearFn();
this.subject.next({ element, key, value: '' });
}
}
}
/**
* update a given element with the activated values for a given key
* @param element
* @param key
* @param value
*/
updateElement(element, key, value) {
const builders = this.updateMap.get(element);
if (builders) {
const updateFn = builders.get(key);
if (!!updateFn) {
updateFn(value);
this.subject.next({ element, key, value });
}
}
}
/**
* release all references to a given element
* @param element
*/
releaseElement(element) {
const watcherMap = this.watcherMap.get(element);
if (watcherMap) {
watcherMap.forEach((s) => s.unsubscribe());
this.watcherMap.delete(element);
}
const elementMap = this.elementMap.get(element);
if (elementMap) {
elementMap.forEach((_, s) => elementMap.delete(s));
this.elementMap.delete(element);
}
}
/**
* trigger an update for a given element and key (e.g. layout)
* @param element
* @param key
*/
triggerUpdate(element, key) {
const bpMap = this.elementMap.get(element);
if (bpMap) {
const valueMap = this.getActivatedValues(bpMap, key);
if (valueMap) {
if (key) {
this.updateElement(element, key, valueMap.get(key));
}
else {
valueMap.forEach((v, k) => this.updateElement(element, k, v));
}
}
}
}
/** Cross-reference for HTMLElement with directive key */
buildElementKeyMap(element, key) {
let keyMap = this.elementKeyMap.get(element);
if (!keyMap) {
keyMap = new Set();
this.elementKeyMap.set(element, keyMap);
}
keyMap.add(key);
}
/**
* Other triggers that should force style updates:
* - directionality
* - layout changes
* - mutationobserver updates
*/
watchExtraTriggers(element, key, triggers) {
if (triggers && triggers.length) {
let watchers = this.watcherMap.get(element);
if (!watchers) {
watchers = new Map();
this.watcherMap.set(element, watchers);
}
const subscription = watchers.get(key);
if (!subscription) {
const newSubscription = merge(...triggers).subscribe(() => {
const currentValue = this.getValue(element, key);
this.updateElement(element, key, currentValue);
});
watchers.set(key, newSubscription);
}
}
}
/** Breakpoint locator by mediaQuery */
findByQuery(query) {
return this.breakpoints.findByQuery(query);
}
/**
* get the fallback breakpoint for a given element, starting with the current breakpoint
* @param bpMap
* @param key
*/
getActivatedValues(bpMap, key) {
for (let i = 0; i < this.activatedBreakpoints.length; i++) {
const activatedBp = this.activatedBreakpoints[i];
const valueMap = bpMap.get(activatedBp.alias);
if (valueMap) {
if (key === undefined ||
(valueMap.has(key) && valueMap.get(key) != null)) {
return valueMap;
}
}
}
// On the server, we explicitly have an "all" section filled in to begin with.
// So we don't need to aggressively find a fallback if no explicit value exists.
if (!this._useFallbacks) {
return undefined;
}
const lastHope = bpMap.get('');
return key === undefined || (lastHope && lastHope.has(key))
? lastHope
: undefined;
}
/**
* Watch for mediaQuery breakpoint activations
*/
observeActivations() {
const queries = this.breakpoints.items.map((bp) => bp.mediaQuery);
this.hook.registerBeforeAfterPrintHooks(this);
this.matchMedia
.observe(this.hook.withPrintQuery(queries))
.pipe(tap(this.hook.interceptEvents(this)), filter(this.hook.blockPropagation()))
.subscribe(this.onMediaChange.bind(this));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaMarshaller, deps: [{ token: MatchMedia }, { token: BreakPointRegistry }, { token: PrintHook }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaMarshaller, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaMarshaller, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: MatchMedia }, { type: BreakPointRegistry }, { type: PrintHook }] });
function initBuilderMap(map, element, key, input) {
if (input !== undefined) {
const oldMap = map.get(element) ?? new Map();
oldMap.set(key, input);
map.set(element, oldMap);
}
}
/** A class that encapsulates CSS style generation for common directives */
class StyleBuilder {
constructor() {
/** Whether to cache the generated output styles */
this.shouldCache = true;
}
/**
* Run a side effect computation given the input string and the computed styles
* from the build task and the host configuration object
* NOTE: This should be a no-op unless an algorithm is provided in a subclass
*/
sideEffect(_input, _styles, _parent) { }
}
class StyleUtils {
constructor(_serverStylesheet, _serverModuleLoaded, _platformId, layoutConfig) {
this._serverStylesheet = _serverStylesheet;
this._serverModuleLoaded = _serverModuleLoaded;
this._platformId = _platformId;
this.layoutConfig = layoutConfig;
}
/**
* Applies styles given via string pair or object map to the directive element
*/
applyStyleToElement(element, style, value = null) {
let styles = {};
if (typeof style === 'string') {
styles[style] = value;
style = styles;
}
styles = this.layoutConfig.disableVendorPrefixes
? style
: applyCssPrefixes(style);
this._applyMultiValueStyleToElement(styles, element);
}
/**
* Applies styles given via string pair or object map to the directive's element
*/
applyStyleToElements(style, elements = []) {
const styles = this.layoutConfig.disableVendorPrefixes
? style
: applyCssPrefixes(style);
elements.forEach((el) => {
this._applyMultiValueStyleToElement(styles, el);
});
}
/**
* Determine the DOM element's Flexbox flow (flex-direction)
*
* Check inline style first then check computed (stylesheet) style
*/
getFlowDirection(target) {
const query = 'flex-direction';
let value = this.lookupStyle(target, query);
const hasInlineValue = this.lookupInlineStyle(target, query) ||
(isPlatformServer(this._platformId) && this._serverModuleLoaded)
? value
: '';
return [value || 'row', hasInlineValue];
}
hasWrap(target) {
const query = 'flex-wrap';
return this.lookupStyle(target, query) === 'wrap';
}
/**
* Find the DOM element's raw attribute value (if any)
*/
lookupAttributeValue(element, attribute) {
return element.getAttribute(attribute) ?? '';
}
/**
* Find the DOM element's inline style value (if any)
*/
lookupInlineStyle(element, styleName) {
return isPlatformBrowser(this._platformId)
? element.style.getPropertyValue(styleName)
: getServerStyle(element, styleName);
}
/**
* Determine the inline or inherited CSS style
* NOTE: platform-server has no implementation for getComputedStyle
*/
lookupStyle(element, styleName, inlineOnly = false) {
let value = '';
if (element) {
let immediateValue = (value = this.lookupInlineStyle(element, styleName));
if (!immediateValue) {
if (isPlatformBrowser(this._platformId)) {
if (!inlineOnly) {
value = getComputedStyle(element).getPropertyValue(styleName);
}
}
else {
if (this._serverModuleLoaded) {
value = this._serverStylesheet.getStyleForElement(element, styleName);
}
}
}
}
// Note: 'inline' is the default of all elements, unless UA stylesheet overrides;
// in which case getComputedStyle() should determine a valid value.
return value ? value.trim() : '';
}
/**
* Applies the styles to the element. The styles object map may contain an array of values
* Each value will be added as element style
* Keys are sorted to add prefixed styles (like -webkit-x) first, before the standard ones
*/
_applyMultiValueStyleToElement(styles, element) {
Object.keys(styles)
.sort()
.forEach((key) => {
const el = styles[key];
const values = Array.isArray(el)
? el
: [el];
values.sort();
for (let value of values) {
value = value ? value + '' : '';
if (isPlatformBrowser(this._platformId) ||
!this._serverModuleLoaded) {
isPlatformBrowser(this._platformId)
? element.style.setProperty(key, value)
: setServerStyle(element, key, value);
}
else {
this._serverStylesheet.addStyleToElement(element, key, value);
}
}
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StyleUtils, deps: [{ token: StylesheetMap }, { token: SERVER_TOKEN }, { token: PLATFORM_ID }, { token: LAYOUT_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StyleUtils, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: StyleUtils, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: StylesheetMap }, { type: undefined, decorators: [{
type: Inject,
args: [SERVER_TOKEN]
}] }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [LAYOUT_CONFIG]
}] }] });
function getServerStyle(element, styleName) {
const styleMap = readStyleAttribute(element);
return styleMap[styleName] ?? '';
}
function setServerStyle(element, styleName, styleValue) {
styleName = styleName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
const styleMap = readStyleAttribute(element);
styleMap[styleName] = styleValue ?? '';
writeStyleAttribute(element, styleMap);
}
function writeStyleAttribute(element, styleMap) {
let styleAttrValue = '';
for (const key in styleMap) {
const newValue = styleMap[key];
if (newValue) {
styleAttrValue += `${key}:${styleMap[key]};`;
}
}
element.setAttribute('style', styleAttrValue);
}
function readStyleAttribute(element) {
const styleMap = {};
const styleAttribute = element.getAttribute('style');
if (styleAttribute) {
const styleList = styleAttribute.split(/;+/g);
for (let i = 0; i < styleList.length; i++) {
const style = styleList[i].trim();
if (style.length > 0) {
const colonIndex = style.indexOf(':');
if (colonIndex === -1) {
throw new Error(`Invalid CSS style: ${style}`);
}
const name = style.substr(0, colonIndex).trim();
styleMap[name] = style.substr(colonIndex + 1).trim();
}
}
}
return styleMap;
}
class BaseDirective2 {
/** Access to host element's parent DOM node */
get parentElement() {
return this.elementRef.nativeElement.parentElement;
}
/** Access to the HTMLElement for the directive */
get nativeElement() {
return this.elementRef.nativeElement;
}
/** Access to the activated value for the directive */
get activatedValue() {
return this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
}
set activatedValue(value) {
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, value, this.marshal.activatedAlias);
}
constructor(elementRef, styleBuilder, styler, marshal) {
this.elementRef = elementRef;
this.styleBuilder = styleBuilder;
this.styler = styler;
this.marshal = marshal;
this.DIRECTIVE_KEY = '';
this.inputs = [];
/** The most recently used styles for the builder */
this.mru = {};
this.destroySubject = new Subject();
/** Cache map for style computation */
this.styleCache = new Map();
}
/** For @Input changes */
ngOnChanges(changes) {
Object.keys(changes).forEach((key) => {
if (this.inputs.indexOf(key) !== -1) {
const bp = key.split('.').slice(1).join('.');
const val = changes[key].currentValue;
this.setValue(val, bp);
}
});
}
ngOnDestroy() {
this.destroySubject.next();
this.destroySubject.complete();
this.marshal.releaseElement(this.nativeElement);
}
/** Register with central marshaller service */
init(extraTriggers = []) {
this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, this.updateWithValue.bind(this), this.clearStyles.bind(this), extraTriggers);
}
/** Add styles to the element using predefined style builder */
addStyles(input, parent) {
const builder = this.styleBuilder;
const useCache = builder.shouldCache;
let genStyles = this.styleCache.get(input);
if (!genStyles || !useCache) {
genStyles = builder.buildStyles(input, parent);
if (useCache) {
this.styleCache.set(input, genStyles);
}
}
this.mru = { ...genStyles };
this.applyStyleToElement(genStyles);
builder.sideEffect(input, genStyles, parent);
}
/** Remove generated styles from an element using predefined style builder */
clearStyles() {
Object.keys(this.mru).forEach((k) => {
this.mru[k] = '';
});
this.applyStyleToElement(this.mru);
this.mru = {};
this.currentValue = undefined;
}
/** Force trigger style updates on DOM element */
triggerUpdate() {
this.marshal.triggerUpdate(this.nativeElement, this.DIRECTIVE_KEY);
}
/**
* Determine the DOM element's Flexbox flow (flex-direction).
*
* Check inline style first then check computed (stylesheet) style.
* And optionally add the flow value to element's inline style.
*/
getFlexFlowDirection(target, addIfMissing = false) {
if (target) {
const [value, hasInlineValue] = this.styler.getFlowDirection(target);
if (!hasInlineValue && addIfMissing) {
const style = buildLayoutCSS(value);
const elements = [target];
this.styler.applyStyleToElements(style, elements);
}
return value.trim();
}
return 'row';
}
hasWrap(target) {
return this.styler.hasWrap(target);
}
/** Applies styles given via string pair or object map to the directive element */
applyStyleToElement(style, value, element = this.nativeElement) {
this.styler.applyStyleToElement(element, style, value);
}
setValue(val, bp) {
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, val, bp);
}
updateWithValue(input) {
if (this.currentValue !== input) {
this.addStyles(input);
this.currentValue = input;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: BaseDirective2, deps: [{ token: i0.ElementRef }, { token: StyleBuilder }, { token: StyleUtils }, { token: MediaMarshaller }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.2", type: BaseDirective2, isStandalone: true, usesOnChanges: true, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: BaseDirective2, decorators: [{
type: Directive
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: StyleBuilder }, { type: StyleUtils }, { type: MediaMarshaller }] });
/**
* MockMatchMedia mocks calls to the Window API matchMedia with a build of a simulated
* MockMediaQueryListener. Methods are available to simulate an activation of a mediaQuery
* range and to clearAll mediaQuery listeners.
*/
class MockMatchMedia extends MatchMedia {
constructor(_zone, _platformId, _document, _breakpoints) {
super(_zone, _platformId, _document);
this._document = _document;
this._breakpoints = _breakpoints;
this.autoRegisterQueries = true; // Used for testing BreakPoint registrations
this.useOverlaps = false; // Allow fallback to overlapping mediaQueries
}
/** Easy method to clear all listeners for all mediaQueries */
clearAll() {
this.registry.forEach((mql) => {
mql.destroy();
});
this.registry.clear();
this.useOverlaps = false;
}
/** Feature to support manual, simulated activation of a mediaQuery. */
activate(mediaQuery, useOverlaps = this.useOverlaps) {
mediaQuery = this._validateQuery(mediaQuery);
if (useOverlaps || !this.isActive(mediaQuery)) {
this._deactivateAll();
this._registerMediaQuery(mediaQuery);
this._activateWithOverlaps(mediaQuery, useOverlaps);
}
return this.hasActivated;
}
setNonce(nonce) {
super._nonce = nonce;
}
/** Converts an optional mediaQuery alias to a specific, valid mediaQuery */
_validateQuery(queryOrAlias) {
const bp = this._breakpoints.findByAlias(queryOrAlias);
return bp?.mediaQuery ?? queryOrAlias;
}
/**
* Manually onMediaChange any overlapping mediaQueries to simulate
* similar functionality in the window.matchMedia()
*/
_activateWithOverlaps(mediaQuery, useOverlaps) {
if (useOverlaps) {
const bp = this._breakpoints.findByQuery(mediaQuery);
const alias = bp?.alias ?? 'unknown';
// Simulate activation of overlapping lt-<XXX> ranges
switch (alias) {
case 'lg':
this._activateByAlias(['lt-xl']);
break;
case 'md':
this._activateByAlias(['lt-xl', 'lt-lg']);
break;
case 'sm':
this._activateByAlias(['lt-xl', 'lt-lg', 'lt-md']);
break;
case 'xs':
this._activateByAlias(['lt-xl', 'lt-lg', 'lt-md', 'lt-sm']);
break;
}
// Simulate activation of overlapping gt-<xxxx> mediaQuery ranges
switch (alias) {
case 'xl':
this._activateByAlias(['gt-lg', 'gt-md', 'gt-sm', 'gt-xs']);
break;
case 'lg':
this._activateByAlias(['gt-md', 'gt-sm', 'gt-xs']);
break;
case 'md':
this._activateByAlias(['gt-sm', 'gt-xs']);
break;
case 'sm':
this._activateByAlias(['gt-xs']);
break;
}
}
// Activate last since the responsiveActivation is watching *this* mediaQuery
return this._activateByQuery(mediaQuery);
}
/**
*
*/
_activateByAlias(aliases) {
const activate = (alias) => {
const bp = this._breakpoints.findByAlias(alias);
this._activateByQuery(bp?.mediaQuery ?? alias);
};
aliases.forEach(activate);
}
/**
*
*/
_activateByQuery(mediaQuery) {
if (!this.registry.has(mediaQuery) && this.autoRegisterQueries) {
this._registerMediaQuery(mediaQuery);
}
const mql = this.registry.get(mediaQuery);
if (mql && !this.isActive(mediaQuery)) {
this.registry.set(mediaQuery, mql.activate());
}
return this.hasActivated;
}
/** Deactivate all current MQLs and reset the buffer */
_deactivateAll() {
this.registry.forEach((it) => {
it.deactivate();
});
return this;
}
/** Insure the mediaQuery is registered with MatchMedia */
_registerMediaQuery(mediaQuery) {
if (!this.registry.has(mediaQuery) && this.autoRegisterQueries) {
this.registerQuery(mediaQuery);
}
}
/**
* Call window.matchMedia() to build a MediaQueryList; which
* supports 0..n listeners for activation/deactivation
*/
buildMQL(query) {
return new MockMediaQueryList(query);
}
get hasActivated() {
return this.activations.length > 0;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MockMatchMedia, deps: [{ token: i0.NgZone }, { token: PLATFORM_ID }, { token: DOCUMENT }, { token: BreakPointRegistry }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MockMatchMedia }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MockMatchMedia, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i0.NgZone }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: BreakPointRegistry }] });
/**
* Special internal class to simulate a MediaQueryList and
* - supports manual activation to simulate mediaQuery matching
* - manages listeners
*/
class MockMediaQueryList extends EventTarget {
get matches() {
return this._isActive;
}
get media() {
return this._mediaQuery;
}
constructor(_mediaQuery) {
super();
this._mediaQuery = _mediaQuery;
this._isActive = false;
this._listeners = [];
this.onchange = null;
}
/**
* Destroy the current list by deactivating the
* listeners and clearing the internal list
*/
destroy() {
this.deactivate();
this._listeners = [];
}
/** Notify all listeners that 'matches === TRUE' */
activate() {
if (!this._isActive) {
this._isActive = true;
this._listeners.forEach((callback) => {
const cb = callback;
cb.call(this, {
matches: this.matches,
media: this.media,
});
});
}
return this;
}
/** Notify all listeners that 'matches === false' */
deactivate() {
if (this._isActive) {
this._isActive = false;
this._listeners.forEach((callback) => {
const cb = callback;
cb.call(this, {
matches: this.matches,
media: this.media,
});
});
}
return this;
}
/** Add a listener to our internal list to activate later */
addListener(listener) {
if (this._listeners.indexOf(listener) === -1) {
this._listeners.push(listener);
}
if (this._isActive) {
const cb = listener;
cb.call(this, {
matches: this.matches,
media: this.media,
});
}
}
/** Don't need to remove listeners in the testing environment */
removeListener(_) { }
dispatchEvent(_) {
return false;
}
}
/**
* Pre-configured provider for MockMatchMedia
*/
// tslint:disable-next-line:variable-name
const MockMatchMediaProvider = {
provide: MatchMedia,
useClass: MockMatchMedia,
};
/** Wraps the provided value in an array, unless the provided value is an array. */
function coerceArray(value) {
return Array.isArray(value) ? value : [value];
}
/**
* MediaObserver enables applications to listen for 1..n mediaQuery activations and to determine
* if a mediaQuery is currently activated.
*
* Since a breakpoint change will first deactivate 1...n mediaQueries and then possibly activate
* 1..n mediaQueries, the MediaObserver will debounce notifications and report ALL *activations*
* in 1 event notification. The reported activations will be sorted in descending priority order.
*
* This class uses the BreakPoint Registry to inject alias information into the raw MediaChange
* notification. For custom mediaQuery notifications, alias information will not be injected and
* those fields will be ''.
*
* Note: Developers should note that only mediaChange activations (not de-activations)
* are announced by the MediaObserver.
*
* @usage
*
* // RxJS
* import { filter } from 'rxjs/operators';
* import { MediaObserver } from '@ngbracket/ngx-layout';
*
* @Component({ ... })
* export class AppComponent {
* status: string = '';
*
* constructor(mediaObserver: MediaObserver) {
* const media$ = mediaObserver.asObservable().pipe(
* filter((changes: MediaChange[]) => true) // silly noop filter
* );
*
* media$.subscribe((changes: MediaChange[]) => {
* let status = '';
* changes.forEach( change => {
* status += `'${change.mqAlias}' = (${change.mediaQuery}) <br/>` ;
* });
* this.status = status;
* });
*
* }
* }
*/
class MediaObserver {
constructor(breakpoints, matchMedia, hook) {
this.breakpoints = breakpoints;
this.matchMedia = matchMedia;
this.hook = hook;
/** Filter MediaChange notifications for overlapping breakpoints */
this.filterOverlaps = false;
this.destroyed$ = new Subject();
this._media$ = this.watchActivations();
}
/**
* Completes the active subject, signalling to all complete for all
* MediaObserver subscribers
*/
ngOnDestroy() {
this.destroyed$.next();
this.destroyed$.complete();
}
// ************************************************
// Public Methods
// ************************************************
/**
* Observe changes to current activation 'list'
*/
asObservable() {
return this._media$;
}
/**
* Allow programmatic query to determine if one or more media query/alias match
* the current viewport size.
* @param value One or more media queries (or aliases) to check.
* @returns Whether any of the media queries match.
*/
isActive(value) {
const aliases = splitQueries(coerceArray(value));
return aliases.some((alias) => {
const query = toMediaQuery(alias, this.breakpoints);
return query !== null && this.matchMedia.isActive(query);
});
}
// ************************************************
// Internal Methods
// ************************************************
/**
* Register all the mediaQueries registered in the BreakPointRegistry
* This is needed so subscribers can be auto-notified of all standard, registered
* mediaQuery activations
*/
watchActivations() {
const queries = this.breakpoints.items.map((bp) => bp.mediaQuery);
return this.buildObservable(queries);
}
/**
* Only pass/announce activations (not de-activations)
*
* Since multiple-mediaQueries can be activation in a cycle,
* gather all current activations into a single list of changes to observers
*
* Inject associated (if any) alias information into the MediaChange event
* - Exclude mediaQuery activations for overlapping mQs. List bounded mQ ranges only
* - Exclude print activations that do not have an associated mediaQuery
*
* NOTE: the raw MediaChange events [from MatchMedia] do not
* contain important alias information; as such this info
* must be injected into the MediaChange
*/
buildObservable(mqList) {
const hasChanges = (changes) => {
const isValidQuery = (change) => change.mediaQuery.length > 0;
return changes.filter(isValidQuery).length > 0;
};
const excludeOverlaps = (changes) => {
return !this.filterOverlaps
? changes
: changes.filter((change) => {
const bp = this.breakpoints.findByQuery(change.mediaQuery);
return bp?.overlapping ?? true;
});
};
const ignoreDuplicates = (previous, current) => {
if (previous.length !== current.length) {
return false;
}
const previousMqs = previous.map((mc) => mc.mediaQuery);
const currentMqs = new Set(current.map((mc) => mc.mediaQuery));
const difference = new Set(previousMqs.filter((mq) => !currentMqs.has(mq)));
return difference.size === 0;
};
/**
*/
return this.matchMedia.observe(this.hook.withPrintQuery(mqList)).pipe(filter((change) => change.matches), debounceTime(0, asapScheduler), switchMap((_) => of(this.findAllActivations())), map(excludeOverlaps), filter(hasChanges), distinctUntilChanged(ignoreDuplicates), takeUntil(this.destroyed$));
}
/**
* Find all current activations and prepare single list of activations
* sorted by descending priority.
*/
findAllActivations() {
const mergeMQAlias = (change) => {
const bp = this.breakpoints.findByQuery(change.mediaQuery);
return mergeAlias(change, bp);
};
const replaceWithPrintAlias = (change) => this.hook.isPrintEvent(change) ? this.hook.updateEvent(change) : change;
return this.matchMedia.activations
.map((query) => new MediaChange(true, query))
.map(replaceWithPrintAlias)
.map(mergeMQAlias)
.sort(sortDescendingPriority);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaObserver, deps: [{ token: BreakPointRegistry }, { token: MatchMedia }, { token: PrintHook }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaObserver, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaObserver, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: BreakPointRegistry }, { type: MatchMedia }, { type: PrintHook }] });
/**
* Find associated breakpoint (if any)
*/
function toMediaQuery(query, locator) {
const bp = locator.findByAlias(query) ?? locator.findByQuery(query);
return bp?.mediaQuery ?? null;
}
/**
* Split each query string into separate query strings if two queries are provided as comma
* separated.
*/
function splitQueries(queries) {
return queries
.flatMap((query) => query.split(','))
.map((query) => query.trim());
}
/**
* Class
*/
class MediaTrigger {
constructor(breakpoints, matchMedia, layoutConfig, _platformId, _document) {
this.breakpoints = breakpoints;
this.matchMedia = matchMedia;
this.layoutConfig = layoutConfig;
this._platformId = _platformId;
this._document = _document;
this.hasCachedRegistryMatches = false;
this.originalActivations = [];
this.originalRegistry = new Map();
}
/**
* Manually activate range of breakpoints
* @param list array of mediaQuery or alias strings
*/
activate(list) {
list = list.map((it) => it.trim()); // trim queries
this.saveActivations();
this.deactivateAll();
this.setActivations(list);
this.prepareAutoRestore();
}
/**
* Restore original, 'real' breakpoints and emit events
* to trigger stream notification
*/
restore() {
if (this.hasCachedRegistryMatches) {
const extractQuery = (change) => change.mediaQuery;
const list = this.originalActivations.map(extractQuery);
try {
this.deactivateAll();
this.restoreRegistryMatches();
this.setActivations(list);
}
finally {
this.originalActivations = [];
if (this.resizeSubscription) {
this.resizeSubscription.unsubscribe();
}
}
}
}
// ************************************************
// Internal Methods
// ************************************************
/**
* Whenever window resizes, immediately auto-restore original
* activations (if we are simulating activations)
*/
prepareAutoRestore() {
const isBrowser = isPlatformBrowser(this._platformId) && this._document;
const enableAutoRestore = isBrowser && this.layoutConfig.mediaTriggerAutoRestore;
if (enableAutoRestore) {
const resize$ = fromEvent(window, 'resize').pipe(take(1));
this.resizeSubscription = resize$.subscribe(this.restore.bind(this));
}
}
/**
* Notify all matchMedia subscribers of de-activations
*
* Note: we must force 'matches' updates for
* future matchMedia::activation lookups
*/
deactivateAll() {
const list = this.currentActivations;
this.forceRegistryMatches(list, false);
this.simulateMediaChanges(list, false);
}
/**
* Cache current activations as sorted, prioritized list of MediaChanges
*/
saveActivations() {
if (!this.hasCachedRegistryMatches) {
const toMediaChange = (query) => new MediaChange(true, query);
const mergeMQAlias = (change) => {
const bp = this.breakpoints.findByQuery(change.mediaQuery);
return mergeAlias(change, bp);
};
this.originalActivations = this.currentActivations
.map(toMediaChange)
.map(mergeMQAlias)
.sort(sortDescendingPriority);
this.cacheRegistryMatches();
}
}
/**
* Force set manual activations for specified mediaQuery list
*/
setActivations(list) {
if (!!this.originalRegistry) {
this.forceRegistryMatches(list, true);
}
this.simulateMediaChanges(list);
}
/**
* For specified mediaQuery list manually simulate activations or deactivations
*/
simulateMediaChanges(queries, matches = true) {
const toMediaQuery = (query) => {
const locator = this.breakpoints;
const bp = locator.findByAlias(query) || locator.findByQuery(query);
return bp ? bp.mediaQuery : query;
};
const emitChangeEvent = (query) => this.emitChangeEvent(matches, query);
queries.map(toMediaQuery).forEach(emitChangeEvent);
}
/**
* Replace current registry with simulated registry...
* Note: this is required since MediaQueryList::matches is 'readOnly'
*/
forceRegistryMatches(queries, matches) {
const registry = new Map();
queries.forEach((query) => {
registry.set(query, { matches });
});
this.matchMedia.registry = registry;
}
/**
* Save current MatchMedia::registry items.
*/
cacheRegistryMatches() {
const target = this.originalRegistry;
target.clear();
this.matchMedia.registry.forEach((value, key) => {
target.set(key, value);
});
this.hasCachedRegistryMatches = true;
}
/**
* Restore original, 'true' registry
*/
restoreRegistryMatches() {
const target = this.matchMedia.registry;
target.clear();
this.originalRegistry.forEach((value, key) => {
target.set(key, value);
});
this.originalRegistry.clear();
this.hasCachedRegistryMatches = false;
}
/**
* Manually emit a MediaChange event via the MatchMedia to MediaMarshaller and MediaObserver
*/
emitChangeEvent(matches, query) {
this.matchMedia.source.next(new MediaChange(matches, query));
}
get currentActivations() {
return this.matchMedia.activations;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaTrigger, deps: [{ token: BreakPointRegistry }, { token: MatchMedia }, { token: LAYOUT_CONFIG }, { token: PLATFORM_ID }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaTrigger, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.2", ngImport: i0, type: MediaTrigger, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: BreakPointRegistry }, { type: MatchMedia }, { type: undefined, decorators: [{
type: Inject,
args: [LAYOUT_CONFIG]
}] }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }] });
/**
* The flex API permits 3 or 1 parts of the value:
* - `flex-grow flex-shrink flex-basis`, or
* - `flex-basis`
*/
function validateBasis(basis, grow = '1', shrink = '1') {
let parts = [grow, shrink, basis];
let j = basis.indexOf('calc');
if (j > 0) {
parts[2] = _validateCalcValue(basis.substring(j).trim());
let matches = basis.substr(0, j).trim().split(' ');
if (matches.length == 2) {
parts[0] = matches[0];
parts[1] = matches[1];
}
}
else if (j == 0) {
parts[2] = _validateCalcValue(basis.trim());
}
else {
let matches = basis.split(' ');
parts = matches.length === 3 ? matches : [grow, shrink, basis];
}
return parts;
}
/**
* Calc expressions require whitespace before & after any expression operators
* This is a simple, crude whitespace padding solution.
* - '3 3 calc(15em + 20px)'
* - calc(100% / 7 * 2)
* - 'calc(15em + 20px)'
* - 'calc(15em+20px)'
* - '37px'
* = '43%'
*/
function _validateCalcValue(calc) {
return calc.replace(/[\s]/g, '').replace(/[\/\*\+\-]/g, ' $& ');
}
const MULTIPLIER_SUFFIX = 'x';
function multiply(value, multiplier) {
if (multiplier === undefined) {
return value;
}
const transformValue = (possibleValue) => {
const numberValue = +possibleValue.slice(0, -MULTIPLIER_SUFFIX.length);
if (value.endsWith(MULTIPLIER_SUFFIX) && !isNaN(numberValue)) {
return `${numberValue * multiplier.value}${multiplier.unit}`;
}
return value;
};
return value.includes(' ')
? value.split(' ').map(transformValue).join(' ')
: transformValue(value);
}
/**
* Generated bundle index. Do not edit.
*/
export { BREAKPOINT, BREAKPOINTS, BREAKPOINT_PRINT, BROWSER_PROVIDER, BaseDirective2, BreakPointRegistry, CLASS_NAME, CoreModule, DEFAULT_BREAKPOINTS, DEFAULT_CONFIG, LAYOUT_CONFIG, MediaChange, MediaMarshaller, MediaObserver, MediaTrigger, ORIENTATION_BREAKPOINTS, PrintHook, SERVER_TOKEN, ScreenTypes, StyleBuilder, StyleUtils, StylesheetMap, coerceArray, mergeAlias, removeStyles, sortAscendingPriority, sortDescendingPriority, validateBasis, MatchMedia as ɵMatchMedia, MockMatchMedia as ɵMockMatchMedia, MockMatchMediaProvider as ɵMockMatchMediaProvider, multiply as ɵmultiply };
//# sourceMappingURL=ngbracket-ngx-layout-core.mjs.map