@angular/cdk
Version:
Angular Material Component Development Kit
180 lines (176 loc) • 8.45 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, CSP_NONCE, Injectable, NgZone } from '@angular/core';
import { Subject, combineLatest, concat, Observable } from 'rxjs';
import { take, skip, debounceTime, map, startWith, takeUntil } from 'rxjs/operators';
import { P as Platform } from './platform-20fc4de8.mjs';
import { c as coerceArray } from './array-6239d2f8.mjs';
/** Global registry for all dynamically-created, injected media queries. */
const mediaQueriesForWebkitCompatibility = new Set();
/** Style tag that holds all of the dynamically-created media queries. */
let mediaQueryStyleNode;
/** A utility for calling matchMedia queries. */
class MediaMatcher {
_platform = inject(Platform);
_nonce = inject(CSP_NONCE, { optional: true });
/** The internal matchMedia method to return back a MediaQueryList like object. */
_matchMedia;
constructor() {
this._matchMedia =
this._platform.isBrowser && window.matchMedia
? // matchMedia is bound to the window scope intentionally as it is an illegal invocation to
// call it from a different scope.
window.matchMedia.bind(window)
: noopMatchMedia;
}
/**
* Evaluates the given media query and returns the native MediaQueryList from which results
* can be retrieved.
* Confirms the layout engine will trigger for the selector query provided and returns the
* MediaQueryList for the query provided.
*/
matchMedia(query) {
if (this._platform.WEBKIT || this._platform.BLINK) {
createEmptyStyleRule(query, this._nonce);
}
return this._matchMedia(query);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MediaMatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MediaMatcher, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: MediaMatcher, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/**
* Creates an empty stylesheet that is used to work around browser inconsistencies related to
* `matchMedia`. At the time of writing, it handles the following cases:
* 1. On WebKit browsers, a media query has to have at least one rule in order for `matchMedia`
* to fire. We work around it by declaring a dummy stylesheet with a `@media` declaration.
* 2. In some cases Blink browsers will stop firing the `matchMedia` listener if none of the rules
* inside the `@media` match existing elements on the page. We work around it by having one rule
* targeting the `body`. See https://github.com/angular/components/issues/23546.
*/
function createEmptyStyleRule(query, nonce) {
if (mediaQueriesForWebkitCompatibility.has(query)) {
return;
}
try {
if (!mediaQueryStyleNode) {
mediaQueryStyleNode = document.createElement('style');
if (nonce) {
mediaQueryStyleNode.setAttribute('nonce', nonce);
}
mediaQueryStyleNode.setAttribute('type', 'text/css');
document.head.appendChild(mediaQueryStyleNode);
}
if (mediaQueryStyleNode.sheet) {
mediaQueryStyleNode.sheet.insertRule(`@media ${query} {body{ }}`, 0);
mediaQueriesForWebkitCompatibility.add(query);
}
}
catch (e) {
console.error(e);
}
}
/** No-op matchMedia replacement for non-browser platforms. */
function noopMatchMedia(query) {
// Use `as any` here to avoid adding additional necessary properties for
// the noop matcher.
return {
matches: query === 'all' || query === '',
media: query,
addListener: () => { },
removeListener: () => { },
};
}
/** Utility for checking the matching state of `@media` queries. */
class BreakpointObserver {
_mediaMatcher = inject(MediaMatcher);
_zone = inject(NgZone);
/** A map of all media queries currently being listened for. */
_queries = new Map();
/** A subject for all other observables to takeUntil based on. */
_destroySubject = new Subject();
constructor() { }
/** Completes the active subject, signalling to all other observables to complete. */
ngOnDestroy() {
this._destroySubject.next();
this._destroySubject.complete();
}
/**
* Whether one or more media queries match the current viewport size.
* @param value One or more media queries to check.
* @returns Whether any of the media queries match.
*/
isMatched(value) {
const queries = splitQueries(coerceArray(value));
return queries.some(mediaQuery => this._registerQuery(mediaQuery).mql.matches);
}
/**
* Gets an observable of results for the given queries that will emit new results for any changes
* in matching of the given queries.
* @param value One or more media queries to check.
* @returns A stream of matches for the given queries.
*/
observe(value) {
const queries = splitQueries(coerceArray(value));
const observables = queries.map(query => this._registerQuery(query).observable);
let stateObservable = combineLatest(observables);
// Emit the first state immediately, and then debounce the subsequent emissions.
stateObservable = concat(stateObservable.pipe(take(1)), stateObservable.pipe(skip(1), debounceTime(0)));
return stateObservable.pipe(map(breakpointStates => {
const response = {
matches: false,
breakpoints: {},
};
breakpointStates.forEach(({ matches, query }) => {
response.matches = response.matches || matches;
response.breakpoints[query] = matches;
});
return response;
}));
}
/** Registers a specific query to be listened for. */
_registerQuery(query) {
// Only set up a new MediaQueryList if it is not already being listened for.
if (this._queries.has(query)) {
return this._queries.get(query);
}
const mql = this._mediaMatcher.matchMedia(query);
// Create callback for match changes and add it is as a listener.
const queryObservable = new Observable((observer) => {
// Listener callback methods are wrapped to be placed back in ngZone. Callbacks must be placed
// back into the zone because matchMedia is only included in Zone.js by loading the
// webapis-media-query.js file alongside the zone.js file. Additionally, some browsers do not
// have MediaQueryList inherit from EventTarget, which causes inconsistencies in how Zone.js
// patches it.
const handler = (e) => this._zone.run(() => observer.next(e));
mql.addListener(handler);
return () => {
mql.removeListener(handler);
};
}).pipe(startWith(mql), map(({ matches }) => ({ query, matches })), takeUntil(this._destroySubject));
// Add the MediaQueryList to the set of queries.
const output = { observable: queryObservable, mql };
this._queries.set(query, output);
return output;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BreakpointObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BreakpointObserver, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BreakpointObserver, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/**
* Split each query string into separate query strings if two queries are provided as comma
* separated.
*/
function splitQueries(queries) {
return queries
.map(query => query.split(','))
.reduce((a1, a2) => a1.concat(a2))
.map(query => query.trim());
}
export { BreakpointObserver as B, MediaMatcher as M };
//# sourceMappingURL=breakpoints-observer-8e7409df.mjs.map