├── .gitignore ├── .jsbeautifyrc ├── LICENSE.md ├── README.md ├── bower.json ├── match-media.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "js": { 3 | "indent_char": " ", 4 | "indent_size": 2 5 | } 6 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Angular matchMedia Module Software Usage License 2 | 3 | ## Angular matchMedia Module 4 | 5 | Copyright © 2013-2014 Jack Tarantino. 6 | 7 | This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/deed.en_US . 8 | 9 | Any copies of this software, copies of significant portions of this software or derivative works should include credit or attribution to Jack Tarantino or a copy of this license. Derivative works should also be open-source and shared in a similar manner. 10 | 11 | ## matchMedia Polyfill License 12 | 13 | The matchMedia polyfill was copied from https://github.com/paulirish/matchMedia.js/ 14 | 15 | The original license was as follows: 16 | 17 | Copyright (c) 2012 Scott Jehl 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular matchMedia Module 2 | 3 | Provides an Angular service that returns true if the current screen width matches or false if not. Uses the screen widths predefined in Twitter Bootstrap 3 or a customized size you define. There is a staic method `is` which checks for a match on page load and a dynamic method `on` which checks for a match and updates the value on window resize. 4 | 5 | ## Installation 6 | 7 | Download the component via bower: 8 | ```bash 9 | bower install --save angular-media-queries 10 | ``` 11 | 12 | Include AngularJS on the page and then include this script. If possible, include these scripts in the footer of your site before the closing `` tag. 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | ## Usage 19 | 20 | Require the `matchMedia` module as a dependency in your app: 21 | ```javascript 22 | angular.module('myApp', ['matchMedia']) 23 | ``` 24 | 25 | ### In a Controller 26 | 27 | #### Get 28 | Use the service's `get` method to get the name of the currently matching media query rule. 29 | ```javascript 30 | angular.module('myApp', ['matchMedia']) 31 | .controller('mainController', ['screenSize', function (screenSize) { 32 | console.log('Your screen size at the moment matches the "'+screenSize.get()+'" media query rule.'); 33 | }]); 34 | ``` 35 | 36 | #### Is 37 | Use the service's `is` method to determine if the screensize matches the given string/array. 38 | ```javascript 39 | angular.module('myApp', ['matchMedia']) 40 | .controller('mainController', ['screenSize', function (screenSize) { 41 | var data = complicatedChartData; 42 | 43 | //Determine to either perform cpu/network-intensive actions(desktop) or retrieve a small static image(mobile). 44 | if (screenSize.is('xs, sm')) { 45 | // it's a mobile device so fetch a small image 46 | $http.post('/imageGenerator', data) 47 | .success(function (response) { 48 | document.querySelector('.chart').src = response.chartUrl; 49 | }); 50 | } 51 | else { 52 | // it's a desktop size so do the complicated calculations and render that 53 | document.querySelector('.chart') 54 | .appendCanvas() 55 | .parseData() 56 | .renderCrazyChart(); 57 | } 58 | }]); 59 | ``` 60 | 61 | #### On 62 | The callback fed to `.on` will execute on every window resize and takes the truthiness of the media query as its first argument. 63 | Be careful using this method as `resize` events fire often and can bog down your application if not handled properly. 64 | - Executes the callback function on window resize with the match truthiness as the first argument. 65 | - Returns the current match truthiness. 66 | - Passing $scope as a third parameter is optional, when not passed it will use $rootScope 67 | 68 | ```javascript 69 | angular.module('myApp', ['matchMedia']) 70 | .controller('mainController', ['screenSize', function (screenSize) { 71 | $scope.isMobile = screenSize.on('xs, sm', function(isMatch){ 72 | $scope.isMobile = isMatch; 73 | }); 74 | }]); 75 | ``` 76 | 77 | #### When 78 | If you only want the callback to fire while in the correct screensize, use the `when` method. 79 | Be careful using this method as `resize` events fire often and can bog down your application if not handled properly. 80 | ```javascript 81 | angular.module('myApp', ['matchMedia']) 82 | .controller('mainController', ['screenSize', function (screenSize) { 83 | 84 | // Will fire as long as the screen is size between 768px and 991px 85 | screenSize.when('sm', function() { 86 | console.log('Your screen size at the moment is between 768px and 991px.'); 87 | }); 88 | }]); 89 | ``` 90 | 91 | #### OnChange 92 | The callback fed to `.onChange` will execute only when a window resize causes the media query to begin matching or to 93 | stop matching, and takes the truthiness of the media query as its first argument. 94 | Be careful using this method as `resize` events fire often and can bog down your application if not handled properly. 95 | - Executes the callback function ONLY when the true/false state of the match differs from previous state. 96 | - Returns the current match truthiness. 97 | - The 'scope' parameter is required for cleanup reasons (destroy event). 98 | 99 | ```javascript 100 | angular.module('myApp', ['matchMedia']) 101 | .controller('mainController', ['$scope', 'screenSize', function ($scope, screenSize) { 102 | $scope.isMobile = screenSize.onChange($scope, 'xs, sm', function(isMatch){ 103 | $scope.isMobile = isMatch; 104 | }); 105 | }]); 106 | ``` 107 | 108 | #### OnRuleChange 109 | The callback fed to `.onRuleChange` will execute only when a window resize causes the matching media query rule to change, 110 | and takes the name of the matched rule as its first argument. Depending on your needs, using this method can be more 111 | efficient than registering multiple `.onChange` handlers. 112 | Be careful using this method as `resize` events fire often and can bog down your application if not handled properly. 113 | - Executes the callback function ONLY when the name of the matched rule differs from previous matched rule. 114 | - Returns the current rule name. 115 | - The 'scope' parameter is required for cleanup reasons (destroy event). 116 | 117 | ```javascript 118 | angular.module('myApp', ['matchMedia']) 119 | .controller('mainController', ['$scope', 'screenSize', function ($scope, screenSize) { 120 | $scope.screenSizeName = screenSize.onRuleChange($scope, function(ruleName){ 121 | switch (ruleName) { 122 | case 'xs': 123 | // do something special 124 | break; 125 | case 'sm': 126 | // do something else special 127 | break; 128 | default: 129 | // do general logic 130 | break; 131 | } 132 | }); 133 | }]); 134 | ``` 135 | 136 | #### isRetina 137 | This will return a boolean to indicate if the current screen is hi-def/retina. 138 | ```javascript 139 | angular.module('myApp', ['matchMedia']) 140 | .controller('mainController', ['screenSize', function (screenSize) { 141 | $scope.isRetina = screenSize.isRetina; 142 | }]); 143 | ``` 144 | 145 | ### Filter 146 | 147 | Operate on string values with the filter: Have the placeholder sign % replaced by the actual media query rule name. 148 | 149 | #### Example: 150 | ```html 151 |
{{'Your screen size is: ' | media }} "
152 | ``` 153 | 154 | #### Example with replace: 155 | ```html 156 |
157 | ``` 158 | 159 | #### Extended example: 160 | ```html 161 |
162 | ``` 163 | 164 | ### ngIf Example 165 | 166 | In your controller you can create variables that correspond to screen sizes. For example add the following to your controller: 167 | ```javascript 168 | // Using static method `is` 169 | angular.module('myApp', ['matchMedia']) 170 | .controller('mainController', ['screenSize', function (screenSize) { 171 | $scope.desktop = screenSize.is('md, lg'); 172 | $scope.mobile = screenSize.is('xs, sm'); 173 | }]); 174 | 175 | // Using dynamic method `on`, which will set the variables initially and then update the variable on window resize 176 | $scope.desktop = screenSize.on('md, lg', function(match){ 177 | $scope.desktop = match; 178 | }); 179 | $scope.mobile = screenSize.on('xs, sm', function(match){ 180 | $scope.mobile = match; 181 | }); 182 | ``` 183 | 184 | Then in your HTML you can show or hide content using ngIf or similar directives that take an Angular expression: 185 | ```javascript 186 | 187 | ``` 188 | This particular example is great for only loading large, unnecessary images on desktop computers. 189 | 190 | Note: It's important if you plan on using screensize.is() in directives to assign its return value to a scoped variable. If you don't, it will only be evaluated once and will not update if the window is resized or if a mobile device is turned sideways. 191 | 192 | ### Custom Screen Sizes or Media Queries 193 | 194 | You can access and therefore customize the media queries or create your own: 195 | ```javascript 196 | angular.module('myApp', ['matchMedia']) 197 | .controller('mainController', ['screenSize', function (screenSize) { 198 | screenSize.rules = { 199 | retina: 'only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx)', 200 | superJumbo: '(min-width: 2000px)', 201 | 202 | }; 203 | 204 | if (screenSize.is('retina')) { 205 | // switch out regular images for hi-dpi ones 206 | } 207 | 208 | if (screenSize.is('superJumbo')) { 209 | // do something for enormous screens 210 | } 211 | }]); 212 | ``` 213 | 214 | `screenSize.rules` contains a copy of the default rules initially, so it is straightforward to 215 | add to or change the default rules rather than replacing them, if you wish. Setting 216 | `screenSize.rules` to `null` will cause `screenSize` to use the default (bootstrap3) rules, 217 | but does not make them available as a base for further changes; to restore `screenSize.rules` 218 | to the default value, use `screenSize.restoreDefaultRules()` 219 | ```javascript 220 | angular.module('myApp', ['matchMedia']) 221 | .controller('mainController', ['screenSize', function (screenSize) { 222 | 223 | // this removes the lg media query, causing its screen sizes to be lumped in with md 224 | delete screenSize.rules.lg; 225 | 226 | // this reverts the media queries to the bootstrap3 defaults 227 | screenSize.rules = null; 228 | 229 | // this restores screenSize.rules to the default media query values 230 | screenSize.restoreDefaultRules(); 231 | 232 | // this adds a new superJumbo media query, without removing the default rules 233 | angular.extend(screenSize.rules, {superJumbo: '(min-width: 2000px)'}; 234 | }]); 235 | ``` 236 | 237 | ## License 238 | 239 | This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/deed.en_US. 240 | 241 | ## Contributors 242 | 243 | * Module roughly based on https://github.com/chrismatheson/ngMediaFilter 244 | * Polyfill from https://github.com/paulirish/matchMedia.js/ 245 | * @jacopotarantino 246 | * @thatmarvin 247 | * Matthias Max @bitflowertweets 248 | 249 | ## Todo 250 | 251 | * Write tests. 252 | * Add a simple directive wrapper for ng-if. 253 | * Add Grunt tasks. 254 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-media-queries", 3 | "version": "0.7.0", 4 | "authors": [ 5 | "Jack ", 6 | "Joe " 7 | ], 8 | "description": "Angular service to test if a given @media statment is true.", 9 | "main": "match-media.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "angular", 15 | "angularjs", 16 | "matchmedia", 17 | "@media", 18 | "css" 19 | ], 20 | "license": "Creative Commons Attribution-ShareAlike 4.0 International License", 21 | "homepage": "https://jack.ofspades.com/angular-matchmedia-module/", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ], 29 | "dependencies": { 30 | "angular": ">=1.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /match-media.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | /* 5 | * Angular matchMedia Module 6 | * Version 0.6.0 7 | * Uses Bootstrap 3 breakpoint sizes 8 | * Exposes service "screenSize" which returns true if breakpoint(s) matches. 9 | * Includes matchMedia polyfill for backward compatibility. 10 | * Copyright © Jack Tarantino . 11 | **/ 12 | 13 | 14 | var app = angular.module('matchMedia', []); 15 | 16 | 17 | app.run(function initializeNgMatchMedia() { 18 | /*! matchMedia() polyfill - Test a CSS media type/query in JS. 19 | * Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. 20 | * Dual MIT/BSD license 21 | **/ 22 | 23 | window.matchMedia || (window.matchMedia = function matchMediaPolyfill() { 24 | 25 | // For browsers that support matchMedium api such as IE 9 and webkit 26 | var styleMedia = (window.styleMedia || window.media); 27 | 28 | // For those that don't support matchMedium 29 | if (!styleMedia) { 30 | var style = document.createElement('style'), 31 | script = document.getElementsByTagName('script')[0], 32 | info = null; 33 | 34 | style.type = 'text/css'; 35 | style.id = 'matchmediajs-test'; 36 | 37 | script.parentNode.insertBefore(style, script); 38 | 39 | // 'style.currentStyle' is used by IE <= 8 40 | // 'window.getComputedStyle' for all other browsers 41 | info = ('getComputedStyle' in window) && window.getComputedStyle(style, null) || style.currentStyle; 42 | 43 | styleMedia = { 44 | matchMedium: function (media) { 45 | var text = '@media ' + media + '{ #matchmediajs-test { width: 1px; } }'; 46 | 47 | // 'style.styleSheet' is used by IE <= 8 48 | // 'style.textContent' for all other browsers 49 | if (style.styleSheet) { 50 | style.styleSheet.cssText = text; 51 | } else { 52 | style.textContent = text; 53 | } 54 | 55 | // Test if media query is true or false 56 | return info.width === '1px'; 57 | } 58 | }; 59 | } 60 | 61 | return function (media) { 62 | return { 63 | matches: styleMedia.matchMedium(media || 'all'), 64 | media: media || 'all' 65 | }; 66 | }; 67 | }()); 68 | }); 69 | 70 | //Service to use in controllers 71 | app.service('screenSize', screenSize); 72 | 73 | screenSize.$inject = ['$rootScope']; 74 | 75 | function screenSize($rootScope) { 76 | 77 | var defaultRules = { 78 | lg: '(min-width: 1200px)', 79 | md: '(min-width: 992px) and (max-width: 1199px)', 80 | sm: '(min-width: 768px) and (max-width: 991px)', 81 | xs: '(max-width: 767px)' 82 | }; 83 | 84 | this.isRetina = ( 85 | window.devicePixelRatio > 1 || 86 | (window.matchMedia && window.matchMedia('(-webkit-min-device-pixel-ratio: 1.5),(-moz-min-device-pixel-ratio: 1.5),(min-device-pixel-ratio: 1.5),(min-resolution: 192dpi),(min-resolution: 2dppx)').matches) 87 | ); 88 | 89 | var that = this; 90 | 91 | // Executes Angular $apply in a safe way 92 | var safeApply = function (fn, scope) { 93 | scope = scope || $rootScope; 94 | var phase = scope.$root.$$phase; 95 | if (phase === '$apply' || phase === '$digest') { 96 | if (fn && (typeof (fn) === 'function')) { 97 | fn(); 98 | } 99 | } else { 100 | scope.$apply(fn); 101 | } 102 | }; 103 | 104 | // Validates that we're getting a string or array. 105 | // When string: converts string(comma seperated) to an array. 106 | var assureList = function (list) { 107 | 108 | if (typeof list !== 'string' && Object.prototype.toString.call(list) !== '[object Array]') { 109 | throw new Error('screenSize requires array or comma-separated list'); 110 | } 111 | 112 | return typeof list === 'string' ? list.split(/\s*,\s*/) : list; 113 | }; 114 | 115 | var getCurrentMatch = function () { 116 | var rules = that.rules || defaultRules; 117 | 118 | for (var property in rules) { 119 | if (rules.hasOwnProperty(property)) { 120 | if (window.matchMedia(rules[property]).matches) { 121 | return property; 122 | } 123 | } 124 | } 125 | }; 126 | 127 | // Return the actual size (it's string name defined in the rules) 128 | this.get = getCurrentMatch; 129 | 130 | this.restoreDefaultRules = function () { 131 | this.rules = angular.extend({}, defaultRules); 132 | }; 133 | this.restoreDefaultRules(); 134 | 135 | this.is = function (list) { 136 | list = assureList(list); 137 | return list.indexOf(getCurrentMatch()) !== -1; 138 | }; 139 | 140 | // Executes the callback function on window resize with the match truthiness as the first argument. 141 | // Returns the current match truthiness. 142 | // The 'scope' parameter is optional. If it's not passed in, '$rootScope' is used. 143 | this.on = function (list, callback, scope) { 144 | window.addEventListener('resize', listenerFunc); 145 | 146 | if (scope) { 147 | scope.$on('$destroy', function () { 148 | window.removeEventListener('resize', listenerFunc); 149 | }); 150 | } 151 | 152 | return that.is(list); 153 | 154 | function listenerFunc() { 155 | safeApply(callback(that.is(list)), scope); 156 | } 157 | }; 158 | 159 | // Executes the callback function ONLY when the match differs from previous match. 160 | // Returns the current match truthiness. 161 | // The 'scope' parameter is required for cleanup reasons (destroy event). 162 | this.onChange = function (scope, list, callback) { 163 | var currentMatch = getCurrentMatch(); 164 | list = assureList(list); 165 | if (!scope) { 166 | throw 'scope has to be applied for cleanup reasons. (destroy)'; 167 | } 168 | 169 | window.addEventListener('resize', listenerFunc); 170 | 171 | scope.$on('$destroy', function () { 172 | window.removeEventListener('resize', listenerFunc); 173 | }); 174 | 175 | return that.is(list); 176 | 177 | function listenerFunc() { 178 | var previousMatch = currentMatch; 179 | currentMatch = getCurrentMatch(); 180 | 181 | var wasPreviousMatch = list.indexOf(previousMatch) !== -1; 182 | var doesCurrentMatch = list.indexOf(currentMatch) !== -1; 183 | if (wasPreviousMatch !== doesCurrentMatch) { 184 | safeApply(callback(doesCurrentMatch), scope); 185 | } 186 | } 187 | }; 188 | 189 | // Executes the callback function ONLY when the matched rule changes. 190 | // Returns the current match rule name. 191 | // The 'scope' parameter is required for cleanup reasons (destroy event). 192 | this.onRuleChange = function (scope, callback) { 193 | var currentMatch = getCurrentMatch(); 194 | if (!scope) { 195 | throw 'scope has to be applied for cleanup reasons. (destroy)'; 196 | } 197 | 198 | window.addEventListener('resize', listenerFunc); 199 | 200 | scope.$on('$destroy', function () { 201 | window.removeEventListener('resize', listenerFunc); 202 | }); 203 | 204 | return currentMatch; 205 | 206 | function listenerFunc() { 207 | var previousMatch = currentMatch; 208 | currentMatch = getCurrentMatch(); 209 | 210 | if (previousMatch !== currentMatch) { 211 | safeApply(callback(currentMatch), scope); 212 | } 213 | } 214 | }; 215 | 216 | // Executes the callback only when inside of the particular screensize. 217 | // The 'scope' parameter is optional. If it's not passed in, '$rootScope' is used. 218 | this.when = function (list, callback, scope) { 219 | window.addEventListener('resize', listenerFunc); 220 | 221 | if (scope) { 222 | scope.$on('$destroy', function () { 223 | window.removeEventListener('resize', listenerFunc); 224 | }); 225 | } 226 | 227 | return that.is(list); 228 | 229 | function listenerFunc() { 230 | if (that.is(list) === true) { 231 | safeApply(callback(that.is(list)), scope); 232 | } 233 | } 234 | }; 235 | } 236 | 237 | app.filter('media', ['screenSize', function (screenSize) { 238 | 239 | var mediaFilter = function (inputValue, options) { 240 | // Get actual size 241 | var size = screenSize.get(); 242 | 243 | // Variable for the value being return (either a size/rule name or a group name) 244 | var returnedName = ''; 245 | 246 | if (!options) { 247 | // Return the size/rule name 248 | return size; 249 | } 250 | 251 | // Replace placeholder with group name in input value 252 | if (options.groups) { 253 | for (var prop in options.groups) { 254 | if (options.groups.hasOwnProperty(prop)) { 255 | var index = options.groups[prop].indexOf(size); 256 | if (index >= 0) { 257 | returnedName = prop; 258 | } 259 | } 260 | } 261 | 262 | // If no group name is found for size use the size itself 263 | if (returnedName === '') { 264 | returnedName = size; 265 | } 266 | } 267 | 268 | // Replace or return size/rule name? 269 | if (options.replace && typeof options.replace === 'string' && options.replace.length > 0) { 270 | return inputValue.replace(options.replace, returnedName); 271 | } else { 272 | return returnedName; 273 | } 274 | }; 275 | 276 | // Since AngularJS 1.3, filters which are not stateless (depending at the scope) 277 | // have to explicit define this behavior. 278 | mediaFilter.$stateful = true; 279 | return mediaFilter; 280 | }]); 281 | 282 | })(); 283 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-media-queries", 3 | "version": "0.7.0", 4 | "description": "Angular service to test if a given @media statment is true.", 5 | "main": "match-media.js", 6 | "scripts": { 7 | "test": "karma start" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jacopotarantino/angular-match-media.git" 12 | }, 13 | "keywords": [ 14 | "angular", 15 | "angularjs", 16 | "matchmedia", 17 | "@media", 18 | "css" 19 | ], 20 | "author": "Jack ", 21 | "contributors": [ 22 | "Joe " 23 | ], 24 | "license": "CC-BY-SA-4.0", 25 | "bugs": { 26 | "url": "https://github.com/jacopotarantino/angular-match-media/issues" 27 | }, 28 | "homepage": "https://github.com/jacopotarantino/angular-match-media#readme" 29 | } 30 | --------------------------------------------------------------------------------