From edc437ed4a15f0148d1abad67395c70802b19823 Mon Sep 17 00:00:00 2001 From: Justin DuJardin Date: Sun, 27 Dec 2015 17:08:53 -0800 Subject: [PATCH] feat(util): add Media injectable for media query checks and events --- examples/components/dialog/basic_usage.ts | 9 +- ng2-material/all.ts | 2 + ng2-material/core/util/media.ts | 126 ++++++++++++++++++++++ 3 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 ng2-material/core/util/media.ts diff --git a/examples/components/dialog/basic_usage.ts b/examples/components/dialog/basic_usage.ts index f8fb7d18..6f5e6200 100644 --- a/examples/components/dialog/basic_usage.ts +++ b/examples/components/dialog/basic_usage.ts @@ -4,12 +4,7 @@ import {ElementRef} from "angular2/core"; import {Input} from "angular2/core"; import {DOM} from "angular2/src/platform/dom/dom_adapter"; import {MdDialogConfig, MdDialogBasic, MdDialogRef} from "ng2-material/components/dialog/dialog"; - - -function hasMedia(size: string) { - // TODO: Implement as $mdMedia - return true; -} +import {Media} from "../../../ng2-material/core/util/media"; @Component({selector: 'dialog-basic-usage'}) @View({ @@ -20,7 +15,7 @@ function hasMedia(size: string) { export default class DialogBasicUsage { status = ' '; - customFullscreen = hasMedia('xs') || hasMedia('sm'); + customFullscreen = Media.hasMedia('xs') || Media.hasMedia('sm'); constructor(public dialog: MdDialog, public element: ElementRef) { diff --git a/ng2-material/all.ts b/ng2-material/all.ts index 549be674..49021d90 100644 --- a/ng2-material/all.ts +++ b/ng2-material/all.ts @@ -50,6 +50,7 @@ export * from './components/toolbar/toolbar'; import {MdTabs, MdTab} from './components/tabs/tabs'; import {UrlResolver} from "angular2/compiler"; +import {Media} from "./core/util/media"; export * from './components/toolbar/toolbar'; /** @@ -115,6 +116,7 @@ export class MaterialTemplateResolver extends UrlResolver { */ export const MATERIAL_PROVIDERS: any[] = [ MdDialog, + Media, MdRadioDispatcher, provide(UrlResolver, {useValue: new MaterialTemplateResolver()}) ]; diff --git a/ng2-material/core/util/media.ts b/ng2-material/core/util/media.ts new file mode 100644 index 00000000..e958d26f --- /dev/null +++ b/ng2-material/core/util/media.ts @@ -0,0 +1,126 @@ +import {CONST} from "angular2/src/facade/lang"; +import {Injectable} from "angular2/core"; +import {Output} from "angular2/core"; +import {EventEmitter} from "angular2/core"; + + +/** + * As defined in core/style/variables.scss + * + * $layout-breakpoint-xs: 600px !default; + * $layout-breakpoint-sm: 960px !default; + * $layout-breakpoint-md: 1280px !default; + * $layout-breakpoint-lg: 1920px !default; + * + */ +export const MEDIA: any = { + 'xs': '(max-width: 599px)', + 'gt-xs': '(min-width: 600px)', + 'sm': '(min-width: 600px) and (max-width: 959px)', + 'gt-sm': '(min-width: 960px)', + 'md': '(min-width: 960px) and (max-width: 1279px)', + 'gt-md': '(min-width: 1280px)', + 'lg': '(min-width: 1280px) and (max-width: 1919px)', + 'gt-lg': '(min-width: 1920px)', + 'xl': '(min-width: 1920px)' +}; + +export const MEDIA_PRIORITY: any = [ + 'xl', + 'gt-lg', + 'lg', + 'gt-md', + 'md', + 'gt-sm', + 'sm', + 'gt-xs', + 'xs' +]; + +/** + * Reference to a Media query listener. When you are done with it, call the `destroy` method + * to release its reference. + */ +export class MediaListener { + + /** + * Emits when the query that this is listening for changes. + */ + @Output() onMatched: EventEmitter = new EventEmitter(); + + /** + * Determine if this query is currently matched by the viewport. + * @returns {boolean} True if the query is matched. + */ + get matches(): boolean { + return !this._destroyed && this._mql.matches; + } + + private _destroyed: boolean = false; + + private _listener: MediaQueryListListener; + + constructor(public query: string, + private _mql: MediaQueryList, + private _media: Media) { + this._listener = (mql: MediaQueryList) => this.onMatched.emit(query); + this._mql.addListener(this._listener); + } + + /** + * Destroy and unhook this listener. + */ + destroy() { + if (!this._destroyed) { + this._mql.removeListener(this._listener); + this._media.unregisterListener(this); + this._destroyed = true; + this._listener = null; + this._mql = null; + } + } + +} + +interface IMediaQueryCache { + references:number; + mql:MediaQueryList; +} + +/** + * Injectable class for being notified of changes to viewport media queries. + */ +@Injectable() +export class Media { + private _cache: {[query:string]:IMediaQueryCache} = {}; + + listen(query: string): MediaListener { + let listener = this._cache[query]; + if (!listener) { + listener = this._cache[query] = { + mql: window.matchMedia(query), + references: 0 + }; + } + listener.references++; + return new MediaListener(query, listener.mql, this); + } + + unregisterListener(listener: MediaListener): void { + let cached = this._cache[listener.query]; + if (cached) { + cached.references--; + delete this._cache[listener.query]; + } + } + + static hasMedia(size: string): boolean { + let query = MEDIA[size]; + if (!query) { + console.warn(`unknown media query size ${size}. Expected one of [${MEDIA_PRIORITY.join(',')}]`); + return false; + } + return window.matchMedia(query).matches; + } + +}