├── .babelrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin ├── build.js ├── makeWebpackConfig.js └── start.js ├── example ├── index.html └── main.js ├── package.json ├── src └── media-query-facade.js └── test └── media-query-facade.test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": [ 4 | "add-module-exports", 5 | "transform-object-rest-spread" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | insert_final_newline = true 8 | indent_style = space 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | lib 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bin 2 | src 3 | test 4 | .babelrc 5 | .editorconfig 6 | .gitignore 7 | .travis.yml 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - node 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tane Morgan 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 | # media-query-facade 2 | 3 | [![build status](https://img.shields.io/travis/tanem/media-query-facade/master.svg?style=flat-square)](https://travis-ci.org/tanem/media-query-facade) 4 | [![coverage status](https://img.shields.io/coveralls/tanem/media-query-facade.svg?style=flat-square)](https://coveralls.io/r/tanem/media-query-facade) 5 | [![npm version](https://img.shields.io/npm/v/media-query-facade.svg?style=flat-square)](https://www.npmjs.com/package/media-query-facade) 6 | [![npm downloads](https://img.shields.io/npm/dm/media-query-facade.svg?style=flat-square)](https://www.npmjs.com/package/media-query-facade) 7 | 8 | > Do stuff via JavaScript when the media queries on a document change. For efficiency it uses [window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window.matchMedia) under the hood. 9 | 10 | ## Usage 11 | 12 | ```js 13 | import MQFacade from 'media-query-facade' 14 | 15 | const mq = new MQFacade({ 16 | small: 'only screen and (max-width: 480px)', 17 | medium: 'only screen and (min-width: 480px) and (max-width: 720px)' 18 | }) 19 | 20 | const changeColour = colour => () => { 21 | document.body.style.backgroundColor = colour 22 | } 23 | 24 | mq.on('small', changeColour('blue')) 25 | mq.on('medium', changeColour('green')) 26 | mq.on('only screen and (min-width: 720px)', changeColour('red')) 27 | ``` 28 | 29 | There is a working version of the above in the `example` dir. First run `npm start`, then point a browser at `localhost:8080`. 30 | 31 | ## API 32 | 33 | ### const mq = new MQFacade(aliases) 34 | 35 | Initialise a new `MQFacade`. Media query `aliases` may also be provided up front. 36 | 37 | ### mq.registerAlias(alias, query) 38 | 39 | Register an `alias` for a `query`, or register a number of aliases at once via an object. 40 | 41 | ```js 42 | mq.registerAlias('small', '(max-width: 100px)') 43 | mq.registerAlias({ 44 | small: '(max-width: 100px)', 45 | medium: '(max-width: 200px)' 46 | }) 47 | ``` 48 | 49 | ### mq.on(query, callback, context) 50 | 51 | Register a `callback` which will be executed with the given `context` on entry of the given `query` or alias. If `context` is not specified, it will default to the `mq` instance. 52 | 53 | ```js 54 | mq.on('(max-width: 400px)', () => {}) 55 | mq.on('smartphones', () => {}, {}) 56 | ``` 57 | 58 | ### mq.off(query, callback, context) 59 | 60 | Remove all callbacks for all queries: 61 | 62 | ```js 63 | mq.off() 64 | ``` 65 | 66 | Remove all callbacks for a `query` or alias: 67 | 68 | ```js 69 | mq.off('(max-width: 400px)') 70 | ``` 71 | 72 | Remove a `callback` for a `query` or alias: 73 | 74 | ```js 75 | mq.off('(max-width: 400px)', () => {}) 76 | ``` 77 | 78 | Remove a `callback` with a `context` for a `query` or alias: 79 | 80 | ```js 81 | mq.off('(max-width: 400px)', () => {}, {}) 82 | ``` 83 | 84 | ## Install 85 | 86 | ``` 87 | $ npm install media-query-facade --save 88 | ``` 89 | 90 | There are also UMD builds available via unpkg: 91 | 92 | - https://unpkg.com/media-query-facade/dist/media-query-facade.js 93 | - https://unpkg.com/media-query-facade/dist/media-query-facade.min.js 94 | 95 | ## License 96 | 97 | MIT 98 | -------------------------------------------------------------------------------- /bin/build.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | 3 | import makeWebpackConfig from './makeWebpackConfig' 4 | 5 | const [ , , buildType ] = process.argv 6 | 7 | webpack(makeWebpackConfig(buildType), (error, stats) => { 8 | if (error) { 9 | throw new Error(error) 10 | } 11 | 12 | console.log(stats.toString({ 13 | assets: true, 14 | chunks: false, 15 | colors: true, 16 | hash: false, 17 | timings: false, 18 | version: false 19 | })) 20 | }) 21 | -------------------------------------------------------------------------------- /bin/makeWebpackConfig.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack from 'webpack' 3 | 4 | export default function makeWebpackConfig (buildType) { 5 | const baseConfig = { 6 | module: { 7 | loaders: [ 8 | { 9 | test: /\.js$/, 10 | loader: 'babel', 11 | exclude: /node_modules/ 12 | } 13 | ] 14 | } 15 | } 16 | 17 | if (buildType === 'example') { 18 | return Object.assign(baseConfig, { 19 | entry: './example/main.js', 20 | externals: {}, 21 | output: { 22 | filename: 'bundle.js', 23 | path: path.join(__dirname, '../example') 24 | } 25 | }) 26 | } 27 | 28 | if (buildType === 'umd') { 29 | return Object.assign(baseConfig, { 30 | entry: './src/media-query-facade.js', 31 | output: { 32 | filename: 'media-query-facade.js', 33 | library: 'media-query-facade', 34 | libraryTarget: 'umd', 35 | path: path.join(__dirname, '../dist') 36 | }, 37 | plugins: [ 38 | new webpack.optimize.OccurenceOrderPlugin(), 39 | new webpack.DefinePlugin({ 40 | 'process.env.NODE_ENV': JSON.stringify('production') 41 | }) 42 | ] 43 | }) 44 | } 45 | 46 | if (buildType === 'umd:min') { 47 | return Object.assign(baseConfig, { 48 | entry: './src/media-query-facade.js', 49 | output: { 50 | filename: 'media-query-facade.min.js', 51 | library: 'media-query-facade', 52 | libraryTarget: 'umd', 53 | path: path.join(__dirname, '../dist') 54 | }, 55 | plugins: [ 56 | new webpack.optimize.OccurenceOrderPlugin(), 57 | new webpack.DefinePlugin({ 58 | 'process.env.NODE_ENV': JSON.stringify('production') 59 | }), 60 | new webpack.optimize.UglifyJsPlugin({ 61 | compressor: { 62 | screw_ie8: true, 63 | warnings: false 64 | } 65 | }) 66 | ] 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /bin/start.js: -------------------------------------------------------------------------------- 1 | import WebpackDevServer from 'webpack-dev-server' 2 | import webpack from 'webpack' 3 | 4 | import makeWebpackConfig from './makeWebpackConfig' 5 | 6 | new WebpackDevServer(webpack(makeWebpackConfig('example')), { 7 | contentBase: 'example/', 8 | filename: 'bundle.js', 9 | stats: { 10 | assets: true, 11 | chunks: false, 12 | colors: true, 13 | hash: false, 14 | timings: false, 15 | version: false 16 | } 17 | }).listen(8080, 'localhost', () => { 18 | console.log('listening on localhost:8080') 19 | }) 20 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mqfacade example 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import MQFacade from '../src/media-query-facade' 2 | 3 | const mq = new MQFacade({ 4 | small: 'only screen and (max-width: 480px)', 5 | medium: 'only screen and (min-width: 480px) and (max-width: 720px)' 6 | }) 7 | 8 | const changeColour = colour => () => { 9 | document.body.style.backgroundColor = colour 10 | } 11 | 12 | mq.on('small', changeColour('blue')) 13 | mq.on('medium', changeColour('green')) 14 | mq.on('only screen and (min-width: 720px)', changeColour('red')) 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "media-query-facade", 3 | "version": "1.0.37", 4 | "description": "A nicer JavaScript media query API.", 5 | "main": "lib/media-query-facade.js", 6 | "homepage": "https://github.com/tanem/media-query-facade", 7 | "bugs": { 8 | "url": "http://github.com/tanem/media-query-facade/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/tanem/media-query-facade.git" 13 | }, 14 | "scripts": { 15 | "clean": "rimraf dist lib", 16 | "lint": "standard", 17 | "test": "jest && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js || true", 18 | "start": "babel-node ./bin/start.js", 19 | "build:lib": "babel src -d lib", 20 | "build:umd": "babel-node ./bin/build.js umd", 21 | "build:umd:min": "babel-node ./bin/build.js umd:min", 22 | "build": "npm run clean && npm run build:lib && npm run build:umd && npm run build:umd:min", 23 | "preversion": "npm run lint && npm test", 24 | "version": "npm run build", 25 | "postversion": "git push && git push --tags && npm publish", 26 | "release": "npm version -m 'Release v%s'" 27 | }, 28 | "keywords": [ 29 | "media", 30 | "query", 31 | "facade" 32 | ], 33 | "author": { 34 | "name": "Tane Morgan", 35 | "url": "http://github.com/tanem" 36 | }, 37 | "license": "MIT", 38 | "devDependencies": { 39 | "babel-cli": "^6.18.0", 40 | "babel-loader": "^6.2.10", 41 | "babel-plugin-add-module-exports": "^0.2.1", 42 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 43 | "babel-preset-env": "^1.1.4", 44 | "coveralls": "^2.11.15", 45 | "jest": "^18.0.0", 46 | "matchmedia-polyfill": "tanem/matchMedia.js#6c0ba01", 47 | "standard": "^8.0.0", 48 | "webpack": "^1.14.0", 49 | "webpack-dev-server": "^1.16.2" 50 | }, 51 | "standard": { 52 | "globals": [ 53 | "test", 54 | "expect" 55 | ] 56 | }, 57 | "jest": { 58 | "collectCoverage": true, 59 | "setupFiles": [ 60 | "./node_modules/matchmedia-polyfill/matchMedia.js", 61 | "./node_modules/matchmedia-polyfill/matchMedia.addListener.js" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/media-query-facade.js: -------------------------------------------------------------------------------- 1 | export default class MQFacade { 2 | 3 | constructor (aliases = {}) { 4 | this.aliases = aliases 5 | this.queries = {} 6 | } 7 | 8 | registerAlias (alias, query) { 9 | if (query) { 10 | this.aliases[alias] = query 11 | return 12 | } 13 | 14 | this.aliases = { 15 | ...this.aliases, 16 | ...alias 17 | } 18 | } 19 | 20 | on (query, callback, context) { 21 | let queryObject 22 | try { 23 | queryObject = this._getQueryObject(query) 24 | } catch (e) { 25 | queryObject = this._createQueryObject(query) 26 | } 27 | const handler = { callback: callback, context: context || this } 28 | queryObject.handlers.push(handler) 29 | if (queryObject.isActive) this._triggerHandler(handler) 30 | } 31 | 32 | off (query, callback, context) { 33 | if (!arguments.length) { 34 | return Object.keys(this.queries).forEach((key) => { 35 | this._removeQueryObject(this.queries[key], key) 36 | }) 37 | } 38 | 39 | if (!callback && !context) { 40 | return this._removeQueryObject(query) 41 | } 42 | 43 | this._removeHandler(query, callback, context) 44 | } 45 | 46 | _removeAlias (query) { 47 | Object.keys(this.aliases).forEach((alias) => { 48 | if (this.aliases[alias] === query) { 49 | delete this.aliases[alias] 50 | } 51 | }) 52 | } 53 | 54 | _removeQueryObject (value, query) { 55 | if (typeof value === 'string') query = value 56 | query = this.aliases[query] || query 57 | const queryObject = this._getQueryObject(query) 58 | queryObject.mql.removeListener(queryObject.listener) 59 | delete this.queries[query] 60 | this._removeAlias(query) 61 | } 62 | 63 | _removeHandler (query, callback, context) { 64 | const {handlers} = this._getQueryObject(query) 65 | handlers.some((handler, i) => { 66 | let match = handler.callback === callback 67 | if (context) match = handler.context === context 68 | if (match) return handlers.splice(i, 1) 69 | }) 70 | if (!handlers.length) this._removeQueryObject(query) 71 | } 72 | 73 | _createQueryObject (query) { 74 | query = this.aliases[query] || query 75 | const queryObject = { handlers: [] } 76 | const mql = queryObject.mql = window.matchMedia(query) 77 | const listener = queryObject.listener = () => { 78 | this._triggerHandlers(queryObject) 79 | } 80 | queryObject.isActive = mql.matches 81 | mql.addListener(listener) 82 | this.queries[query] = queryObject 83 | return queryObject 84 | } 85 | 86 | _getQueryObject (query) { 87 | const queryObject = this.queries[this.aliases[query] || query] 88 | if (!queryObject) throw new Error(`"${query}" is not registered`) 89 | return queryObject 90 | } 91 | 92 | _triggerHandlers (queryObject) { 93 | const isActive = queryObject.isActive = !queryObject.isActive 94 | if (!isActive) return 95 | queryObject.handlers.forEach(this._triggerHandler) 96 | } 97 | 98 | _triggerHandler (handler) { 99 | handler.callback.call(handler.context) 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /test/media-query-facade.test.js: -------------------------------------------------------------------------------- 1 | import MQFacade from '../src/media-query-facade' 2 | 3 | test('registration of aliases upon creation', () => { 4 | const mq = new MQFacade({ foo: '(max-width: 400px)' }) 5 | 6 | expect(mq.aliases.foo).toBe('(max-width: 400px)') 7 | }) 8 | 9 | test('registration of a single alias', () => { 10 | const mq = new MQFacade() 11 | 12 | mq.registerAlias('foo', '(max-width: 400px)') 13 | 14 | expect(mq.aliases.foo).toBe('(max-width: 400px)') 15 | }) 16 | 17 | test('registration of multiple aliases', () => { 18 | const mq = new MQFacade() 19 | 20 | mq.registerAlias({ foo: '(max-width: 400px)' }) 21 | mq.registerAlias({ bar: '(min-width: 800px)' }) 22 | 23 | expect(mq.aliases).toEqual({ 24 | foo: '(max-width: 400px)', 25 | bar: '(min-width: 800px)' 26 | }) 27 | }) 28 | 29 | test('registration of event handlers', () => { 30 | const mq = new MQFacade() 31 | const callbackOne = () => {} 32 | const callbackTwo = () => {} 33 | 34 | mq.on('foo', callbackOne) 35 | mq.on('foo', callbackTwo) 36 | 37 | expect(mq.queries.foo.handlers).toEqual([ 38 | { callback: callbackOne, context: mq }, 39 | { callback: callbackTwo, context: mq } 40 | ]) 41 | }) 42 | 43 | test('setting of the event handler callback context', () => { 44 | const mq = new MQFacade() 45 | const callbackOne = () => {} 46 | const callbackTwo = () => {} 47 | const contextOne = {} 48 | const contextTwo = {} 49 | 50 | mq.on('foo', callbackOne, contextOne) 51 | mq.on('foo', callbackTwo, contextTwo) 52 | 53 | expect(mq.queries.foo.handlers).toEqual([ 54 | { callback: callbackOne, context: contextOne }, 55 | { callback: callbackTwo, context: contextTwo } 56 | ]) 57 | }) 58 | 59 | test('removal of all event handlers', () => { 60 | const mq = new MQFacade() 61 | const callback = () => {} 62 | mq.on('foo', callback) 63 | mq.on('bar', callback) 64 | mq.on('bar', callback) 65 | 66 | mq.off() 67 | 68 | expect(mq.queries).toEqual({}) 69 | }) 70 | 71 | test('removal of all event handlers for a query', () => { 72 | const mq = new MQFacade() 73 | const callback = () => {} 74 | mq.on('foo', callback) 75 | mq.on('bar', callback) 76 | mq.on('bar', callback) 77 | 78 | mq.off('bar') 79 | 80 | expect(mq.queries.bar).toBeUndefined() 81 | }) 82 | 83 | test('removal of a single handler for a query', () => { 84 | const mq = new MQFacade() 85 | const callback = () => {} 86 | const callbackTwo = () => {} 87 | mq.on('foo', callback) 88 | mq.on('bar', callback) 89 | mq.on('bar', callbackTwo) 90 | 91 | mq.off('bar', callback) 92 | 93 | expect(mq.queries.bar.handlers).toEqual([ 94 | { callback: callbackTwo, context: mq } 95 | ]) 96 | }) 97 | 98 | test('removal of aliases if queries are removed', () => { 99 | const mq = new MQFacade({ 100 | fooAlias: 'foo', 101 | barAlias: 'bar' 102 | }) 103 | const callback = () => {} 104 | mq.on('fooAlias', callback) 105 | mq.on('barAlias', callback) 106 | mq.on('barAlias', callback) 107 | 108 | mq.off('barAlias') 109 | 110 | expect(mq.aliases.barAlias).toBeUndefined() 111 | }) 112 | 113 | test('removal of a single handler with a specific context for a query', () => { 114 | const mq = new MQFacade() 115 | const callback = () => {} 116 | const callbackTwo = () => {} 117 | const context = {} 118 | mq.on('foo', callback) 119 | mq.on('bar', callbackTwo) 120 | mq.on('bar', callback, context) 121 | 122 | mq.off('bar', callback, context) 123 | 124 | expect(mq.queries.bar.handlers).toEqual([ 125 | { callback: callbackTwo, context: mq } 126 | ]) 127 | }) 128 | 129 | test('removal of a query object if its last handler is removed', () => { 130 | const mq = new MQFacade() 131 | const callback = () => {} 132 | mq.on('foo', callback) 133 | 134 | mq.off('foo', callback) 135 | 136 | expect(mq.queries.foo).toBeUndefined() 137 | }) 138 | 139 | test(`attempting to remove a query that doesn't exist should throw an error`, () => { 140 | const mq = new MQFacade() 141 | 142 | expect(() => { 143 | mq.off('bar') 144 | }).toThrowError('"bar" is not registered') 145 | }) 146 | 147 | test('only triggering event handlers when a media query is entered', () => { 148 | const mq = new MQFacade() 149 | let callCount = 0 150 | const callback = () => { callCount++ } 151 | mq.on('foo', callback) 152 | 153 | mq.queries.foo.listener() 154 | mq.queries.foo.listener() 155 | 156 | expect(callCount).toBe(1) 157 | }) 158 | --------------------------------------------------------------------------------