1 |
|
2 | ( function ( root, factory ) {
|
3 |
|
4 | if ( typeof define === 'function' && define.amd ) {
|
5 |
|
6 |
|
7 | define( [], factory );
|
8 |
|
9 | }
|
10 | else if ( typeof module === 'object' && module.exports ) {
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | module.exports = factory();
|
16 |
|
17 | }
|
18 | else {
|
19 |
|
20 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |