Files
2025-03-03 06:54:52 -08:00

215 lines
5.9 KiB
TypeScript

import { EventData, Observable } from '../data/observable';
import { Screen } from '../platform';
import { Application, ApplicationEventData } from '../application';
import { matchQuery, MediaQueryType } from '../css-mediaquery';
import { Trace } from '../trace';
const mediaQueryLists: MediaQueryListImpl[] = [];
const applicationEvents: string[] = [Application.orientationChangedEvent, Application.systemAppearanceChangedEvent];
// In browser, developers cannot create MediaQueryList instances without calling matchMedia
let isMediaInitializationEnabled: boolean = false;
function toggleApplicationEventListeners(toAdd: boolean) {
for (const eventName of applicationEvents) {
if (toAdd) {
Application.on(eventName, onDeviceChange);
} else {
Application.off(eventName, onDeviceChange);
}
}
}
function onDeviceChange(args: ApplicationEventData) {
for (const mql of mediaQueryLists) {
const matches = checkIfMediaQueryMatches(mql.media);
if (mql.matches !== matches) {
mql._matches = matches;
mql.notify({
eventName: MediaQueryListImpl.changeEvent,
object: mql,
matches: mql.matches,
media: mql.media,
});
}
}
}
function checkIfMediaQueryMatches(mediaQueryString: string): boolean {
const { widthPixels, heightPixels } = Screen.mainScreen;
let matches: boolean;
try {
matches = matchQuery(mediaQueryString, {
type: MediaQueryType.screen,
width: widthPixels,
height: heightPixels,
'device-width': widthPixels,
'device-height': heightPixels,
orientation: Application.orientation(),
'prefers-color-scheme': Application.systemAppearance(),
});
} catch (err) {
matches = false;
Trace.write(err, Trace.categories.MediaQuery, Trace.messageType.error);
}
return matches;
}
function matchMedia(mediaQueryString: string): MediaQueryListImpl {
isMediaInitializationEnabled = true;
const mediaQueryList = new MediaQueryListImpl();
isMediaInitializationEnabled = false;
mediaQueryList._media = mediaQueryString;
mediaQueryList._matches = checkIfMediaQueryMatches(mediaQueryString);
return mediaQueryList;
}
class MediaQueryListImpl extends Observable implements MediaQueryList {
public static readonly changeEvent = 'change';
public _media: string;
public _matches: boolean;
private _onChange: (this: MediaQueryList, ev: MediaQueryListEvent) => any;
private mediaQueryChangeListeners: Map<(this: MediaQueryList, ev: MediaQueryListEvent) => any, (data: EventData) => void>;
constructor() {
super();
if (!isMediaInitializationEnabled) {
throw new TypeError('Illegal constructor');
}
Object.defineProperties(this, {
_media: {
writable: true,
},
_matches: {
writable: true,
},
_onChange: {
writable: true,
value: null,
},
mediaQueryChangeListeners: {
value: new Map<(this: MediaQueryList, ev: MediaQueryListEvent) => any, (data: EventData) => void>(),
},
_throwInvocationError: {
value: null,
},
});
}
get media(): string {
this._throwInvocationError?.();
return this._media;
}
get matches(): boolean {
this._throwInvocationError?.();
return this._matches;
}
// @ts-ignore
public addEventListener(eventName: string, callback: (data: EventData) => void, thisArg?: any, once?: boolean): void {
this._throwInvocationError?.();
const hasChangeListeners = this.hasListeners(MediaQueryListImpl.changeEvent);
// Call super method first since it throws in the case of bad parameters
super.addEventListener(eventName, callback, thisArg, once);
if (eventName === MediaQueryListImpl.changeEvent && !hasChangeListeners) {
mediaQueryLists.push(this);
if (mediaQueryLists.length === 1) {
toggleApplicationEventListeners(true);
}
}
}
// @ts-ignore
public removeEventListener(eventName: string, callback?: (data: EventData) => void, thisArg?: any): void {
this._throwInvocationError?.();
// Call super method first since it throws in the case of bad parameters
super.removeEventListener(eventName, callback, thisArg);
if (eventName === MediaQueryListImpl.changeEvent) {
const hasChangeListeners = this.hasListeners(MediaQueryListImpl.changeEvent);
if (!hasChangeListeners) {
const index = mediaQueryLists.indexOf(this);
if (index >= 0) {
mediaQueryLists.splice(index, 1);
if (!mediaQueryLists.length) {
toggleApplicationEventListeners(false);
}
}
}
}
}
addListener(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
this._throwInvocationError?.();
// This kind of implementation helps maintain listener registration order
// regardless of using the deprecated methods or property onchange
const wrapperCallback = (data) => {
callback.call(this, <MediaQueryListEvent>{
matches: this.matches,
media: this.media,
});
};
// Call this method first since it throws in the case of bad parameters
this.addEventListener(MediaQueryListImpl.changeEvent, wrapperCallback);
this.mediaQueryChangeListeners.set(callback, wrapperCallback);
}
removeListener(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any): void {
this._throwInvocationError?.();
if (this.mediaQueryChangeListeners.has(callback)) {
// Call this method first since it throws in the case of bad parameters
this.removeEventListener(MediaQueryListImpl.changeEvent, this.mediaQueryChangeListeners.get(callback));
this.mediaQueryChangeListeners.delete(callback);
}
}
public get onchange(): (this: MediaQueryList, ev: MediaQueryListEvent) => any {
this._throwInvocationError?.();
return this._onChange;
}
public set onchange(callback: (this: MediaQueryList, ev: MediaQueryListEvent) => any) {
this._throwInvocationError?.();
// Remove old listener if any
if (this._onChange) {
this.removeListener(this._onChange);
}
if (callback) {
this.addListener(callback);
}
this._onChange = callback;
}
private _throwInvocationError() {
throw new TypeError('Illegal invocation');
}
}
export { matchMedia, MediaQueryListImpl as MediaQueryList, checkIfMediaQueryMatches };