UNPKG

11.1 kBJavaScriptView Raw
1/* !ViewportJS github.com/ryanfitzer/ViewportJS/blob/master/LICENSE */
2( function ( root, factory ) {
3
4 if ( typeof define === 'function' && define.amd ) {
5
6 // AMD. Register as an anonymous module.
7 define( [], factory );
8
9 }
10 else if ( typeof module === 'object' && module.exports ) {
11
12 // Node. Does not work with strict CommonJS, but
13 // only CommonJS-like environments that support module.exports,
14 // like Node.
15 module.exports = factory();
16
17 }
18 else {
19
20 // Browser globals (root is window)
21 root.viewport = factory();
22
23 }
24
25}
26( this, function () {
27
28 var exposedAPI = [
29 'state',
30 'remove',
31 'matches',
32 'current',
33 'previous'
34 ];
35
36 var undefinedVP = {
37 name: undefined,
38 matches: false,
39 current: false
40 };
41
42 function getLogMessage( label, sub ) {
43
44 sub = sub || {};
45
46 var msg = {
47 subNoSupport: '[viewportjs] Subscribing in this environment can cause memory leaks.',
48 subNoConfig: '[viewportjs] Subscriber failed to be added. Instance does not have a configuration.',
49 queryNoConfig: '[viewportjs] The `' + sub + '()` method failed. Instance does not have a configuration.',
50 noHandler: '[viewportjs] The `' + sub.method + '()` method failed. Instance for query `' + sub.query + '`, was configured without a `handler`.',
51 subNoName: '[viewportjs] Subscriber failed to be added. The name `' + sub + '` does not match any configured viewports.',
52 uniqueViewportName: '[viewportjs] Viewport configuration object overwritten. The viewport name `' + sub + '` already exists.'
53 };
54
55 return msg[ label ];
56
57 }
58
59 function copyViewportObject( vp ) {
60
61 return {
62 name: vp.name,
63 matches: vp.matches,
64 current: vp.current
65 };
66
67 }
68
69 function ensureViewportObject( name, vps ) {
70
71 if ( vps && vps[ name ] ) return copyViewportObject( vps[ name ] );
72
73 return undefinedVP;
74
75 }
76
77 function createMediaQuery( query, listener ) {
78
79 var mql = window.matchMedia( query );
80
81 mql.addEventListener( 'change', listener );
82
83 return mql;
84
85 }
86
87 function createUnsubscribe( token, channel ) {
88
89 return function () {
90
91 channel = channel.filter( function ( subscriber ) {
92
93 subscriber.token !== token;
94
95 } );
96
97 return token;
98
99 };
100
101 }
102
103 function staticSubscribe( query, handler ) {
104
105 var mql = window.matchMedia( query );
106 var api = {
107 remove: function () {
108
109 console.warn( getLogMessage( 'noHandler', {
110 query: query,
111 method: 'remove'
112 } ) );
113
114 },
115 matches: function () {
116
117 return mql.matches;
118
119 }
120 };
121
122 if ( !handler ) return api;
123
124 var listener = function ( event ) {
125
126 handler( {
127 matches: event.matches
128 }, api );
129
130 };
131
132 mql.addEventListener( 'change', listener );
133
134 if ( mql.matches ) listener( mql.matches, api );
135
136 api.remove = mql.removeEventListener.bind( mql, 'change', listener );
137
138 return api;
139
140 }
141
142 function Viewport( viewports ) {
143
144 this.api = null;
145 this.viewports = viewports;
146 this.store = {
147 vps: {},
148 tokenUid: -1,
149 channels: {},
150 channelAll: [],
151 current: undefined,
152 previous: undefined
153 };
154
155 this.viewports.forEach( function ( vp ) {
156
157 vp.listener = this.setState.bind( this );
158 vp.mql = createMediaQuery( vp.query, vp.listener );
159
160 this.store.channels[ vp.name ] = [];
161
162 console.assert( !this.store.vps[ vp.name ], getLogMessage( 'uniqueViewportName', vp.name ) );
163
164 this.store.vps[ vp.name ] = {
165 name: vp.name,
166 matches: false,
167 current: false
168 };
169
170 }, this );
171
172 this.setState();
173
174 }
175
176 Viewport.prototype = {
177
178 getMatches: function () {
179
180 return ( this.viewports || [] ).filter( function ( vp ) {
181
182 return vp.mql.matches;
183
184 } ).map( copyViewportObject );
185
186 },
187
188 getCurrent: function () {
189
190 var match = this.getMatches().pop();
191
192 return match ? match.name : undefined;
193
194 },
195
196 getChanges: function ( viewport, current ) {
197
198 var name = viewport.name;
199 var state = this.store.vps[ name ];
200 var props = {
201 matches: viewport.mql.matches,
202 current: current.name === name
203 };
204
205 return Object.keys( props ).reduce( function ( accum, label ) {
206
207 if ( state[ label ] !== props[ label ] ) {
208
209 accum.push( {
210 key: label,
211 value: props[ label ]
212 } );
213
214 }
215
216 return accum;
217
218 }, [] );
219
220 },
221
222 setState: function () {
223
224 var changed = [];
225 var current = ensureViewportObject( this.getCurrent(), this.store.vps );
226
227 this.viewports.forEach( function ( viewport ) {
228
229 var vp = this.store.vps[ viewport.name ];
230 var changes = this.getChanges( viewport, current );
231
232 changes.forEach( function ( change ) {
233
234 vp[ change.key ] = change.value;
235
236 } );
237
238 if ( changes.length ) changed.push( vp.name );
239
240 }, this );
241
242 if ( current.name !== this.store.current ) {
243
244 this.store.previous = this.store.current;
245 this.store.current = current.name;
246
247 }
248
249 changed.forEach( this.publish, this );
250
251 },
252
253 addSubscriber: function ( opts ) {
254
255 var token = this.store.tokenUid = this.store.tokenUid + 1;
256
257 opts.channel.push( {
258 token: token,
259 handler: opts.handler
260 } );
261
262 opts.handler( opts.vp, this.api );
263
264 return createUnsubscribe( token, opts.channel );
265
266 },
267
268 subscribe: function ( name, handler ) {
269
270 return this.addSubscriber( {
271 handler: handler,
272 channel: this.store.channels[ name ],
273 vp: this.store.vps[ name ]
274 } );
275
276 },
277
278 subscribeAll: function ( handler ) {
279
280 return this.addSubscriber( {
281 handler: handler,
282 channel: this.store.channelAll,
283 vp: this.current()
284 } );
285
286 },
287
288 publish: function ( name ) {
289
290 this.store.channels[ name ].forEach( function ( subscriber ) {
291
292 subscriber.handler( this.store.vps[ name ], this.api );
293
294 }, this );
295
296 this.store.channelAll.forEach( function ( subscriber ) {
297
298 subscriber.handler( this.store.vps[ name ], this.api );
299
300 }, this );
301
302 },
303
304 state: function ( name ) {
305
306 console.assert( this.viewports, getLogMessage( 'queryNoConfig', 'state' ) );
307
308 if ( name ) return ensureViewportObject( name, this.store.vps );
309
310 return Object.keys( this.store.vps || {} ).map( function ( label ) {
311
312 return ensureViewportObject( label, this.store.vps );
313
314 }, this );
315
316 },
317
318 matches: function ( name ) {
319
320 console.assert( this.viewports, getLogMessage( 'queryNoConfig', 'matches' ) );
321
322 if ( name ) return ensureViewportObject( name, this.store.vps ).matches;
323
324 return this.getMatches();
325
326 },
327
328 current: function ( name ) {
329
330 console.assert( this.viewports, getLogMessage( 'queryNoConfig', 'current' ) );
331
332 var current = ensureViewportObject( this.store.current, this.store.vps );
333
334 if ( name ) return current.name === name;
335
336 return current;
337
338 },
339
340 previous: function ( name ) {
341
342 console.assert( this.viewports, getLogMessage( 'queryNoConfig', 'previous' ) );
343
344 if ( name ) return this.store.previous === name;
345
346 return ensureViewportObject( this.store.previous, this.store.vps );
347
348 },
349
350 remove: function () {
351
352 console.assert( this.viewports, getLogMessage( 'queryNoConfig', 'remove' ) );
353
354 this.viewports.forEach( function ( viewport ) {
355
356 viewport.mql.removeEventListener( 'change', viewport.listener );
357
358 } );
359
360 return this.viewports = this.store.vps = this.store.current = this.store.previous = null;
361
362 }
363
364 };
365
366 function module( config, handler ) {
367
368 // Subscribe to a single media query
369 if ( typeof config === 'string' ) return staticSubscribe( config, handler );
370
371 var instance = new Viewport( config );
372
373 instance.api = function ( first, second ) {
374
375 console.assert( instance.viewports, getLogMessage( 'subNoConfig' ) );
376
377 // Subscribe to a single configured viewport
378 if ( typeof first === 'string' ) {
379
380 console.assert( instance.store.vps[ first ], getLogMessage( 'subNoName', first ) );
381
382 return instance.subscribe.call( instance, first, second );
383
384 }
385
386 // Subscribe to all configured viewports
387 return instance.subscribeAll.call( instance, first );
388
389 };
390
391 return exposedAPI.reduce( function ( accum, method ) {
392
393 accum[ method ] = instance[ method ].bind( instance );
394
395 return accum;
396
397 }, instance.api );
398
399 }
400
401 // Create noop API for use in Node
402 if ( typeof window === 'undefined' || typeof window.matchMedia === 'undefined' ) {
403
404 return function ( config ) {
405
406 console.assert( Array.isArray( config ), getLogMessage( 'subNoSupport' ) );
407
408 var noop = function () {};
409
410 if ( typeof config === 'string' ) {
411
412 console.warn( getLogMessage( 'subNoSupport' ) );
413
414 return {
415 remove: noop,
416 matches: noop
417 };
418
419 }
420
421 var instance = function () {
422
423 console.error( getLogMessage( 'subNoSupport' ) );
424
425 };
426
427 return exposedAPI.reduce( function ( api, method ) {
428
429 if ( /remove/.test( method ) ) {
430
431 api[ method ] = function () {};
432
433 }
434 else {
435
436 api[ method ] = function ( arg ) {
437
438 if ( typeof arg === 'string' ) return false;
439
440 if ( /state|matches/.test( method ) ) return [];
441
442 return undefinedVP;
443
444 };
445
446 }
447
448 return api;
449
450 }, instance );
451
452 };
453
454 }
455
456 return module;
457
458} ) );
\No newline at end of file