UNPKG

@angular/flex-layout

Version:
1,316 lines (1,295 loc) 86.8 kB
import * as i0 from '@angular/core'; import { APP_BOOTSTRAP_LISTENER, PLATFORM_ID, NgModule, Injectable, InjectionToken, Inject, inject, Directive } from '@angular/core'; import { isPlatformBrowser, DOCUMENT, isPlatformServer } from '@angular/common'; import { BehaviorSubject, Observable, merge, Subject, asapScheduler, of, fromEvent } from 'rxjs'; import { applyCssPrefixes, extendObject, buildLayoutCSS } from '@angular/flex-layout/_private-utils'; import { filter, tap, debounceTime, switchMap, map, distinctUntilChanged, takeUntil, take } from 'rxjs/operators'; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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-'; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * ***************************************************************** * Define module for common Angular Layout utilities * ***************************************************************** */ class CoreModule { } CoreModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: CoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); CoreModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "15.0.2", ngImport: i0, type: CoreModule }); CoreModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: CoreModule, providers: [BROWSER_PROVIDER] }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: CoreModule, decorators: [{ type: NgModule, args: [{ providers: [BROWSER_PROVIDER] }] }] }); /** * 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); } } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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; } } StylesheetMap.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StylesheetMap, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); StylesheetMap.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StylesheetMap, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StylesheetMap, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ 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 }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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 }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ const BREAKPOINT = new InjectionToken('Flex Layout token, collect all breakpoints into one provider', { providedIn: 'root', factory: () => null }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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; } /** 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) { } } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ 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); } } }); } } StyleUtils.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StyleUtils, deps: [{ token: StylesheetMap }, { token: SERVER_TOKEN }, { token: PLATFORM_ID }, { token: LAYOUT_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); StyleUtils.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StyleUtils, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: StyleUtils, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ 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; } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** 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; } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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) { this._zone = _zone; this._platformId = _platformId; this._document = _document; /** 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) => { 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); 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)); } } MatchMedia.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: MatchMedia, deps: [{ token: i0.NgZone }, { token: PLATFORM_ID }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); MatchMedia.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: MatchMedia, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: MatchMedia, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }]; } }); /** * 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) { 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 (!styleEl.styleSheet) { const cssText = ` /* @angular/flex-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); } /** * 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, } ]; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /* 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 } ]; 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])); } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Injection token unique to the flex-layout library. * Use this token when build a custom provider (see below). */ const BREAKPOINTS = new InjectionToken('Token (@angular/flex-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); } }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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; } } BreakPointRegistry.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: BreakPointRegistry, deps: [{ token: BREAKPOINTS }], target: i0.ɵɵFactoryTarget.Injectable }); BreakPointRegistry.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: BreakPointRegistry, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: BreakPointRegistry, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [BREAKPOINTS] }] }]; } }); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ 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)); } } } PrintHook.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: PrintHook, deps: [{ token: BreakPointRegistry }, { token: LAYOUT_CONFIG }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); PrintHook.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: PrintHook, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "15.0.2", ngImport: i0, type: PrintHook, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ 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; } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * MediaMarshaller - register responsive values from directives and * trigger them based on media query events */ class MediaMarshaller { 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(); } get activatedAlias() { return this.activatedBreakpoints[0]?.alias ?? ''; } set activatedBreakpoints(bps) { this._activatedBreakpoints = [...bps]; } get activatedBreakpoints() { return [...this._activatedBreakpoints]; } set useFallbacks(value) { this._useFallbacks = value; } /** * 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 = buil