├── .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 | [](https://travis-ci.org/tanem/media-query-facade)
4 | [](https://coveralls.io/r/tanem/media-query-facade)
5 | [](https://www.npmjs.com/package/media-query-facade)
6 | [](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 |
--------------------------------------------------------------------------------