├── .babelrc ├── .circleci └── config.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── config.json ├── dist └── mediaquerysensor.min.js ├── package.json ├── src ├── MediaQuerySensor.js └── constants │ └── Messages.js ├── tests └── MediaQuerySensor.test.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["transform-es2015-modules-commonjs"] 5 | } 6 | }, 7 | "presets": [ 8 | [ 9 | "@babel/preset-env", 10 | { 11 | "shippedProposals": true 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: circleci/node:8.12.0 7 | 8 | working_directory: ~/mediaquerysensor 9 | 10 | steps: 11 | - checkout 12 | 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - v1-dependencies-{{ checksum "package.json" }} 17 | # fallback to using the latest cache if no exact match is found 18 | - v1-dependencies- 19 | 20 | - run: yarn install 21 | 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: v1-dependencies-{{ checksum "package.json" }} 26 | 27 | # run tests! 28 | - run: yarn test-ci 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "trailing-comma": "all", 5 | "jsxBracketSameLine": true, 6 | "printWidth": 80, 7 | "overrides": [ 8 | { 9 | "files": "*.scss", 10 | "options": { 11 | "singleQuote": false, 12 | "printWidth": 80 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Enmanuel Durán 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaQuerySensor · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/enmanuelduran/mediaquerysensor/blob/master/LICENSE) [![npm version](https://img.shields.io/npm/v/mediaquerysensor.svg?style=flat)](https://www.npmjs.com/package/mediaquerysensor) [![Coverage Status](https://coveralls.io/repos/github/enmanuelduran/mediaquerysensor/badge.svg?branch=master)](https://coveralls.io/github/enmanuelduran/mediaquerysensor?branch=master) [![CircleCI](https://circleci.com/gh/enmanuelduran/mediaquerysensor/tree/master.svg?style=svg)](https://circleci.com/gh/enmanuelduran/mediaquerysensor/tree/master) 2 | 3 | _MediaQuerySensor_ (MQS) is a very simple and powerful event wrapper that allows you to add listeners to your site or app, it will basically execute functions based on media query/breakpoints specified by you. 4 | 5 | Instead of adding a listener to the window's `resize` event, MQS creates a wrapper around the window property [`window.matchMedia`](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia) which makes it fast and performant. 6 | 7 | ## Why to use MQS?, what problems does it solve? 8 | 9 | **The problem:** 10 | 11 | In today's world to be able to remove and add listeners to media queries you first need to follow some steps: 12 | 13 | 1. Create a _MediaQueryList object_. 14 | 1. Bind a function to the object created through a listener. 15 | 1. Check when the screen matches the media query to execute the function in the listener. 16 | 1. If you want to be able to remove a listener you will need to make sure that the reference to the _MediaQueryList object_ and the _function_ is still available in your current scope since _both_ are needed to achieve this. If you don't have access to the references you will have to define global variables and make changes in your code that could be avoided. 17 | 18 | **Solution:** 19 | 20 | **MQS simplifies your life when working with media query listeners** and also **solves the scoping problem**: 21 | 22 | 1. It takes care of creating _the MediaQueryList objects_. 23 | 1. It takes the responsability of binding your objects with your functions through a listener. 24 | 1. It makes the necessary checks to execute your functions when the screen size matches a media query. 25 | 1. It creates accesible references so that you can remove listeners everywhere without having to worry about the context you're currently in. 26 | 27 | ## Installation 28 | 29 | Use the package manager of your preference: 30 | 31 | **npm:** 32 | `npm install mediaquerysensor` 33 | 34 | **yarn:** 35 | `yarn add mediaquerysensor` 36 | 37 | Include it directly in the browser with a CDN: 38 | 39 | ``` 40 | https://cdn.jsdelivr.net/npm/mediaquerysensor@2.0.1/dist/mediaquerysensor.min.js 41 | ``` 42 | 43 | ## Demo 44 | 45 | A demo is available at https://enmascript.com/code/mediaquerysensor 46 | 47 | ## Usage 48 | 49 | _MediaQuerySensor exposes the `MQS` API in the window object if you include the library in your project using a script tag. If you desire to use it as a npm module, you can just import it and use it in the same way in your code._ 50 | 51 | ### Adding a sensor/listener: 52 | 53 | To add a Sensor or Listener we use the method `MQS.add({ref, mediaQuery, action})`, this method takes an object as argument with the next properties: 54 | 55 | | Property | type | Description | 56 | | ------------ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 57 | | `ref` | _Object key_ | A valid (UNIQUE) object key (preferably a String) assigned as identifier to each pair (mediaQuery and action) passed to MQS, this will allow us to remove listeners when necessary. | 58 | | `mediaQuery` | _String:MediaQueryString_ | MediaQuery in which the action is going to be executed (You can define media queries in the same way as CSS Media queries). | 59 | | `action` | _Function(Boolean)_ | Function to execute when the media query conditions are met, receives a boolean as parameter, `true` if the `mediaQuery` property above is met, `false` otherwise | 60 | 61 | #### Example 62 | 63 | ```javascript 64 | import MQS from 'mediaquerysensor'; 65 | 66 | MQS.add({ 67 | ref: 'yourRef2', 68 | mediaQuery: '(min-width: 480px) and (max-width: 990px)', 69 | action: (mediaQueryMatches) => { 70 | if (mediaQueryMatches) { 71 | console.log('Between 480px and 990px'); 72 | } else { 73 | console.log('Not in 480px and 990px'); 74 | } 75 | }, 76 | }); 77 | ``` 78 | 79 | ### Removing a sensor/listener: 80 | 81 | To remove individual sensors we use the method `MQS.remove(ref)`, this method takes the next argument: 82 | 83 | | argument | type | Description | 84 | | -------- | --------------------------------------- | ------------------------------------------------ | 85 | | `ref` | _Valid Object key, preferably a string_ | Matching UNIQUE key used when adding the sensor. | 86 | 87 | #### Example 88 | 89 | ```javascript 90 | import MQS from 'mediaquerysensor'; 91 | 92 | MQS.remove('yourRef'); 93 | ``` 94 | 95 | ### Remove all sensors/listeners 96 | 97 | MQS also provides the ability to remove all the added sensors programatically by using the method `MQS.empty()`: 98 | 99 | #### Example 100 | 101 | ```javascript 102 | import MQS from 'mediaquerysensor'; 103 | 104 | MQS.empty(); 105 | ``` 106 | 107 | ### Checking/Getting active sensors 108 | 109 | To help you make validations and find problems in your implementations there is also a `MQS.get()` method available that exposes an object with all the active sensor's properties, you can also get one specific sensor object directly to see if it is active by executing `MQS.get()['sensorRef']`. 110 | 111 | #### Example 112 | 113 | ```javascript 114 | import MQS from 'mediaquerysensor'; 115 | 116 | // Gets an object with all the active sensors 117 | MQS.get(); 118 | 119 | // Gets a sensor object identified by the key "sensorRef" 120 | MQS.get()['sensorRef']; 121 | 122 | // You may want to console.log it sometimes 123 | console.log(MQS.get()); 124 | ``` 125 | 126 | ## How it works 127 | 128 | MQS receives an object with the properties `ref`, `mediaQuery` and `action` to create _sensors_ (as previously seen on the `add` method). 129 | 130 | ### What is a Sensor? 131 | 132 | A _sensor_ is just an object created by MQS that contains the properties `mediaQuery` and `action` defined, each sensor uses the `ref` value defined by you as identifier. Once a sensor is created, a listener is added to bind your `mediaQuery` with your `action` together so that they can be executed when the screen matches the `mediaQuery` conditions. Also, you'll see that each sensor object contains two extra properties: 133 | 134 | | argument | type | Description | 135 | | ---------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | 136 | | `mediaQueryList` | _MediaQueryList Object_ | It's required to use the matchMedia API, this object allows us to add and remove listeners. | 137 | | `boundAction` | _Function_ | The final function bound to the `mediaQueryList` listeners, this function contains the validations of when an action should be executed. | 138 | 139 | So, if we call the method `add` like: 140 | 141 | ```javascript 142 | MQS.add({ 143 | ref: 'refId', 144 | mediaQuery: '(max-height: 400px)', 145 | action: (mediaQueryMatches) => console.log('maximum of 400px of height'), 146 | }); 147 | 148 | MQS.add({ 149 | ref: 'refId2', 150 | mediaQuery: '(min-width: 768px) and (max-width: 990px)', 151 | action: (mediaQueryMatches) => console.log('768px to 990px'), 152 | }); 153 | ``` 154 | 155 | It will create the following sensor objects: 156 | 157 | ```javascript 158 | { 159 | 'refId': { 160 | mediaQuery: '(max-height: 400px)', 161 | action: () => console.log('maximum of 400px of height'), 162 | mediaQueryList: MediaQueryList{}, 163 | boundAction: () => {} 164 | }, 165 | 'refId2': { 166 | mediaQuery: '(min-width: 768px) and (max-width: 990px)', 167 | action: () => console.log('768px to 990px of width'), 168 | mediaQueryList: MediaQueryList{}, 169 | boundAction: () => {} 170 | } 171 | } 172 | ``` 173 | 174 | ## Browsers support 175 | 176 | As I said before, this library is a wrapper around matchMedia [which has a great support for browsers](https://caniuse.com/#feat=matchmedia). 177 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testMatch": ["/tests/*.test.js"], 3 | "transform": { ".*": "/node_modules/babel-jest" }, 4 | "moduleFileExtensions": ["js"], 5 | "collectCoverageFrom": ["src/*.js", "!src/index.js"] 6 | } 7 | -------------------------------------------------------------------------------- /dist/mediaquerysensor.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.MQS=t():e.MQS=t()}(window,(function(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";r.r(t);var n=r(1);function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function i(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),r.push.apply(r,n)}return r}function u(e){for(var t=1;t=0||(o[r]=e[r]);return o}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,r)&&(o[r]=e[r])}return o}function a(e){var t=function(e,t){if("object"!==o(e)||null===e)return e;var r=e[Symbol.toPrimitive];if(void 0!==r){var n=r.call(e,t||"default");if("object"!==o(n))return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"===o(t)?t:String(t)}t.default=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=function(t,r,o){return window.matchMedia?r&&"function"==typeof r?t&&"string"==typeof t?!(!o||e[o])||(console.warn(n.INVALID_REF),!1):(console.warn(n.INVALID_MEDIAQUERY),!1):(console.warn(n.INVALID_ACTION),!1):(console.warn(n.UNAVAILABLE),!1)},r=function(e,t){return function(){t(e.matches)}},o=function(t,r){t.addListener(e[r].boundAction),e[r].boundAction()},i=function(n){var i=n.mediaQuery,u=n.action,c=n.ref;if(!t(i,u,c))return!1;var f=window.matchMedia(i);e[c]={mediaQuery:i,action:u,mediaQueryList:f,boundAction:r(f,u)},o(f,c)},c=function(t){if(!(t in e))return console.warn(n.REF_NOT_FOUND),!1;e[t].mediaQueryList.removeListener(e[t].boundAction);var r=e,o=(r[t],f(r,[t].map(a)));e=o},l=function(){Object.keys(e).forEach((function(t){e[t].mediaQueryList.removeListener(e[t].boundAction)})),e={}},s=function(){return Object.freeze(u({},e))};return Object.freeze({add:i,empty:l,get:s,remove:c})}()},function(e,t,r){"use strict";r.r(t),r.d(t,"UNAVAILABLE",(function(){return n})),r.d(t,"INVALID_MEDIAQUERY",(function(){return o})),r.d(t,"INVALID_ACTION",(function(){return i})),r.d(t,"INVALID_REF",(function(){return u})),r.d(t,"REF_NOT_FOUND",(function(){return c}));var n="MediaQuerySensor is not supported on this browser.",o="Invalid media query passed, it should be a valid MediaQueryString.",i="Invalid action passed, it should be a function.",u="The ref property already exists or is not valid, please, assign a new one.",c="Unable to remove the listener, the provided ref was not found."}]).default})); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediaquerysensor", 3 | "version": "3.0.0", 4 | "description": "A lightweight and performant wrapper to execute functions based on specified media queries.", 5 | "main": "dist/mediaquerysensor.min.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "test": "jest", 9 | "test-ci": "jest --config config.json --coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js", 10 | "prepublish": "yarn build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/enmanuelduran/mediaquerysensor.git" 15 | }, 16 | "keywords": [ 17 | "media", 18 | "query", 19 | "sensor", 20 | "size", 21 | "screen", 22 | "actions", 23 | "function", 24 | "handler", 25 | "event", 26 | "broadcaster", 27 | "simple", 28 | "lightweight", 29 | "performant" 30 | ], 31 | "author": "Enmanuel Durán", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/enmanuelduran/mediaquerysensor/issues" 35 | }, 36 | "homepage": "https://github.com/enmanuelduran/mediaquerysensor#readme", 37 | "devDependencies": { 38 | "@babel/core": "^7.1.2", 39 | "@babel/preset-env": "^7.1.0", 40 | "babel-jest": "^23.6.0", 41 | "babel-loader": "^8.0.4", 42 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 43 | "coveralls": "^3.0.2", 44 | "jest": "^23.6.0", 45 | "webpack": "^4.20.2", 46 | "webpack-cli": "^3.1.2" 47 | }, 48 | "jest": { 49 | "transform": { 50 | "^.+\\.js$": "babel-jest" 51 | } 52 | }, 53 | "resolutions": { 54 | "babel-core": "7.0.0-bridge.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/MediaQuerySensor.js: -------------------------------------------------------------------------------- 1 | import * as msg from './constants/Messages'; 2 | 3 | function MediaQuerySensor(data = {}) { 4 | const _validateInsertion = (mediaQuery, action, ref) => { 5 | if (!window.matchMedia) { 6 | console.warn(msg.UNAVAILABLE); 7 | 8 | return false; 9 | } 10 | 11 | if (!action || typeof action !== 'function') { 12 | console.warn(msg.INVALID_ACTION); 13 | 14 | return false; 15 | } 16 | 17 | if (!mediaQuery || typeof mediaQuery !== 'string') { 18 | console.warn(msg.INVALID_MEDIAQUERY); 19 | 20 | return false; 21 | } 22 | 23 | if (!ref || data[ref]) { 24 | console.warn(msg.INVALID_REF); 25 | 26 | return false; 27 | } 28 | 29 | return true; 30 | }; 31 | 32 | const _mediaChangeHandler = (mediaQueryList, action) => () => { 33 | action(mediaQueryList.matches); 34 | }; 35 | 36 | const _bindMediaQueries = (mediaQueryList, ref) => { 37 | mediaQueryList.addListener(data[ref].boundAction); 38 | data[ref].boundAction(); 39 | }; 40 | 41 | const add = ({ mediaQuery, action, ref }) => { 42 | if (!_validateInsertion(mediaQuery, action, ref)) { 43 | return false; 44 | } 45 | 46 | const mediaQueryList = window.matchMedia(mediaQuery); 47 | 48 | data[ref] = { 49 | mediaQuery, 50 | action, 51 | mediaQueryList, 52 | boundAction: _mediaChangeHandler(mediaQueryList, action) 53 | }; 54 | 55 | _bindMediaQueries(mediaQueryList, ref); 56 | }; 57 | 58 | const remove = ref => { 59 | if (!(ref in data)) { 60 | console.warn(msg.REF_NOT_FOUND); 61 | 62 | return false; 63 | } 64 | 65 | data[ref].mediaQueryList.removeListener(data[ref].boundAction); 66 | 67 | const { [ref]: undefined, ...newData } = data; 68 | data = newData; 69 | }; 70 | 71 | const empty = () => { 72 | Object.keys(data).forEach(elm => { 73 | data[elm].mediaQueryList.removeListener(data[elm].boundAction); 74 | }); 75 | 76 | data = {}; 77 | }; 78 | 79 | const get = () => Object.freeze({ ...data }); 80 | 81 | return Object.freeze({ 82 | add, 83 | empty, 84 | get, 85 | remove 86 | }); 87 | } 88 | 89 | export default MediaQuerySensor(); 90 | -------------------------------------------------------------------------------- /src/constants/Messages.js: -------------------------------------------------------------------------------- 1 | export const UNAVAILABLE = `MediaQuerySensor is not supported on this browser.`; 2 | export const INVALID_MEDIAQUERY = `Invalid media query passed, it should be a valid MediaQueryString.`; 3 | export const INVALID_ACTION = `Invalid action passed, it should be a function.`; 4 | export const INVALID_REF = `The ref property already exists or is not valid, please, assign a new one.`; 5 | export const REF_NOT_FOUND = `Unable to remove the listener, the provided ref was not found.`; 6 | -------------------------------------------------------------------------------- /tests/MediaQuerySensor.test.js: -------------------------------------------------------------------------------- 1 | import * as msg from '../src/constants/Messages'; 2 | import MQS from '../src/MediaQuerySensor'; 3 | 4 | describe('When processing the media query objects', () => { 5 | beforeEach(() => { 6 | global.console = { warn: jest.fn(), log: console.log }; 7 | }); 8 | 9 | afterEach(() => { 10 | MQS.empty(); 11 | jest.clearAllMocks(); 12 | }); 13 | 14 | describe('when getting the objects available', () => { 15 | beforeEach(() => { 16 | global.matchMedia = jest.fn().mockReturnValue({ 17 | addListener: jest.fn(), 18 | removeListener: jest.fn(), 19 | matches: true 20 | }); 21 | }); 22 | 23 | const mediaQueries = [ 24 | { 25 | mediaQuery: '(min-width: 600px)', 26 | action: jest.fn(), 27 | ref: 'ref' 28 | }, 29 | { 30 | mediaQuery: '(max-width: 599px)', 31 | action: jest.fn(), 32 | ref: 'ref2' 33 | } 34 | ]; 35 | 36 | it('should return an empty object if there are none', () => { 37 | expect(MQS.get()).toEqual({}); 38 | }); 39 | 40 | it('should return an object with correct elements and update', () => { 41 | MQS.add(mediaQueries[0]); 42 | 43 | expect(MQS.get()['ref']).toBeDefined(); 44 | 45 | MQS.add(mediaQueries[1]); 46 | 47 | expect(MQS.get()['ref']).toBeDefined(); 48 | expect(MQS.get()['ref2']).toBeDefined(); 49 | }); 50 | }); 51 | 52 | describe('When adding new elements', () => { 53 | const mediaQueries = { 54 | mediaQuery: '(min-width: 600px)', 55 | action: jest.fn(), 56 | ref: 'ref' 57 | }; 58 | 59 | it('should not add elements if matchMedia object is not defined', () => { 60 | global.matchMedia = undefined; 61 | 62 | expect(MQS.add(mediaQueries)).toBe(false); 63 | 64 | expect(global.console.warn).toHaveBeenCalledWith(msg.UNAVAILABLE); 65 | }); 66 | 67 | describe('when matchMedia is defined', () => { 68 | beforeEach(() => { 69 | global.matchMedia = jest.fn().mockReturnValue({ 70 | addListener: jest.fn(), 71 | removeListener: jest.fn(), 72 | matches: false 73 | }); 74 | }); 75 | 76 | it('should not process invalid values', () => { 77 | expect( 78 | MQS.add({ mediaQuery: '', action: () => {}, ref: 'ref' }) 79 | ).toBe(false); 80 | expect( 81 | MQS.add({ mediaQuery: {}, action: () => {}, ref: 'ref2' }) 82 | ).toBe(false); 83 | 84 | expect(global.matchMedia).not.toHaveBeenCalled(); 85 | expect(global.matchMedia().addListener).not.toHaveBeenCalled(); 86 | expect(global.console.warn).toHaveBeenCalledWith( 87 | msg.INVALID_MEDIAQUERY 88 | ); 89 | }); 90 | 91 | it('should not process invalid actions', () => { 92 | const mediaQueries = [ 93 | { 94 | mediaQuery: 95 | '(min-width: 991px) and (max-width: 1199px)', 96 | action: undefined, 97 | ref: 'ref' 98 | }, 99 | { 100 | mediaQuery: 101 | '(min-width: 991px) and (max-width: 1199px)', 102 | action: {}, 103 | ref: 'ref2' 104 | } 105 | ]; 106 | 107 | expect(MQS.add(mediaQueries[0])).toBe(false); 108 | expect(MQS.add(mediaQueries[1])).toBe(false); 109 | 110 | expect(global.matchMedia).not.toHaveBeenCalled(); 111 | expect(global.matchMedia().addListener).not.toHaveBeenCalled(); 112 | expect(global.console.warn).toHaveBeenCalledWith( 113 | msg.INVALID_ACTION 114 | ); 115 | }); 116 | 117 | it('should not process invalid refs', () => { 118 | const mediaQueries = [ 119 | { 120 | mediaQuery: 121 | '(min-width: 991px) and (max-width: 1199px)', 122 | action: () => {}, 123 | ref: 'ref' 124 | }, 125 | { 126 | mediaQuery: 127 | '(min-width: 991px) and (max-width: 1199px)', 128 | action: () => {}, 129 | ref: 'ref' 130 | }, 131 | { 132 | mediaQuery: 133 | '(min-width: 991px) and (max-width: 1199px)', 134 | action: () => {} 135 | } 136 | ]; 137 | 138 | expect(MQS.add(mediaQueries[0])).toBe(undefined); 139 | expect(MQS.add(mediaQueries[1])).toBe(false); 140 | expect(MQS.add(mediaQueries[2])).toBe(false); 141 | 142 | expect(global.matchMedia).toHaveBeenCalledTimes(1); 143 | expect(global.matchMedia().addListener).toHaveBeenCalledTimes( 144 | 1 145 | ); 146 | expect(global.console.warn).toHaveBeenCalledWith( 147 | msg.INVALID_REF 148 | ); 149 | }); 150 | 151 | it("should add new objects if they're valid", () => { 152 | MQS.add(mediaQueries); 153 | 154 | expect(MQS.get()['ref'].mediaQuery).toBe( 155 | mediaQueries.mediaQuery 156 | ); 157 | expect(MQS.get()['ref'].action).toEqual(mediaQueries.action); 158 | expect(MQS.get()['ref'].boundAction).toEqual( 159 | expect.any(Function) 160 | ); 161 | expect(MQS.get()['ref'].mediaQueryList).toEqual({ 162 | addListener: expect.any(Function), 163 | removeListener: expect.any(Function), 164 | matches: false 165 | }); 166 | }); 167 | 168 | it('should add the listener to the valid object', () => { 169 | MQS.add(mediaQueries); 170 | 171 | expect(global.matchMedia).toHaveBeenCalledWith( 172 | mediaQueries.mediaQuery 173 | ); 174 | expect(global.matchMedia().addListener).toHaveBeenCalledWith( 175 | MQS.get()['ref'].boundAction 176 | ); 177 | }); 178 | 179 | it('should execute the action passing false if the current screen size does not match', () => { 180 | MQS.add(mediaQueries); 181 | 182 | expect(mediaQueries.action).toHaveBeenCalledWith(false); 183 | }); 184 | 185 | it('should execute the action if the current screen size matches', () => { 186 | global.matchMedia = jest.fn().mockReturnValue({ 187 | addListener: jest.fn(), 188 | removeListener: jest.fn(), 189 | matches: true 190 | }); 191 | 192 | MQS.add(mediaQueries); 193 | 194 | expect(mediaQueries.action).toHaveBeenCalledWith(true); 195 | }); 196 | }); 197 | }); 198 | 199 | describe('When removing media queries', () => { 200 | const mediaQueries = [ 201 | { 202 | mediaQuery: '(min-width: 600px)', 203 | action: jest.fn(), 204 | ref: 'ref' 205 | }, 206 | { 207 | mediaQuery: '(max-width: 599px)', 208 | action: jest.fn(), 209 | ref: 'ref2' 210 | } 211 | ]; 212 | 213 | it('should not remove anything if the ref does not exist', () => { 214 | expect(MQS.remove('ref')).toBe(false); 215 | 216 | expect(console.warn).toHaveBeenCalledWith(msg.REF_NOT_FOUND); 217 | }); 218 | 219 | it('should remove elements correctly when the ref exist', () => { 220 | MQS.add(mediaQueries[0]); 221 | MQS.add(mediaQueries[1]); 222 | 223 | const boundAction = MQS.get()['ref'].boundAction; 224 | 225 | expect(MQS.get()['ref']).toBeDefined(); 226 | expect(MQS.get()['ref2']).toBeDefined(); 227 | 228 | MQS.remove('ref'); 229 | 230 | expect(console.warn).not.toHaveBeenCalled(); 231 | expect(MQS.get()['ref']).not.toBeDefined(); 232 | expect(MQS.get()['ref2']).toBeDefined(); 233 | expect(global.matchMedia().removeListener).toHaveBeenCalledTimes(1); 234 | expect(global.matchMedia().removeListener).toHaveBeenCalledWith( 235 | boundAction 236 | ); 237 | }); 238 | 239 | it('should be able to remove all the listeners and media queries programatically', () => { 240 | MQS.add(mediaQueries[0]); 241 | MQS.add(mediaQueries[1]); 242 | 243 | expect(MQS.get()['ref']).toBeDefined(); 244 | expect(MQS.get()['ref2']).toBeDefined(); 245 | 246 | const boundAction = MQS.get()['ref'].boundAction; 247 | const boundActionRef2 = MQS.get()['ref2'].boundAction; 248 | 249 | MQS.empty(); 250 | 251 | expect(global.matchMedia().removeListener).toHaveBeenCalledTimes(2); 252 | expect(global.matchMedia().removeListener).toHaveBeenCalledWith( 253 | boundAction 254 | ); 255 | expect(global.matchMedia().removeListener).toHaveBeenCalledWith( 256 | boundActionRef2 257 | ); 258 | expect(MQS.get()).toEqual({}); 259 | }); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/MediaQuerySensor.js', 6 | mode: 'none', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'mediaquerysensor.min.js', 10 | libraryTarget: 'umd', 11 | library: 'MQS', 12 | libraryExport: 'default' 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js)$/, 18 | use: 'babel-loader' 19 | } 20 | ] 21 | }, 22 | optimization: { 23 | minimize: true 24 | } 25 | }; 26 | --------------------------------------------------------------------------------