mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 01:43:14 +08:00
215 lines
5.9 KiB
TypeScript
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 };
|