Completed
Push — development ( f93eb8...ffa1a0 )
by Thomas
20s
created

$.extend._switchPlugins   C

Complexity

Conditions 16
Paths 9

Size

Total Lines 77

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
c 1
b 0
f 0
nc 9
nop 2
dl 0
loc 77
rs 5.1945

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like $.extend._switchPlugins often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * Global state manager
3
 *
4
 * The state manager helps to master different behaviors for different screen sizes.
5
 * It provides you with the ability to register different states that are handled
6
 * by breakpoints.
7
 *
8
 * Those Breakpoints are defined by entering and exiting points (in pixels)
9
 * based on the viewport width.
10
 * By entering the breakpoint range, the enter functions of the registered
11
 * listeners are called.
12
 * But when the defined points are exceeded, the registered listeners exit
13
 * functions will be called.
14
 *
15
 * That way you can register callbacks that will be called on entering / exiting the defined state.
16
 *
17
 * The manager provides you multiple helper methods and polyfills which help you
18
 * master responsive design.
19
 *
20
 * @example Initialize the StateManager
21
 * ```
22
 *     StateManager.init([{
23
 *         state: 'xs',
24
 *         enter: '0em',
25
 *         exit: '47.5em'
26
 *      }, {
27
 *         state: 'm',
28
 *         enter: '47.5em',
29
 *         exit: '64em'
30
 *      }]);
31
 * ```
32
 *
33
 * @example Register breakpoint listeners
34
 * ```
35
 *     StateManager.registerListener([{
36
 *        state: 'xs',
37
 *        enter: function() { console.log('onEnter'); },
38
 *        exit: function() { console.log('onExit'); }
39
 *     }]);
40
 * ```
41
 *
42
 * @example Wildcard support
43
 * ```
44
 *     StateManager.registerListener([{
45
 *         state: '*',
46
 *         enter: function() { console.log('onGlobalEnter'); },
47
 *         exit: function() { console.log('onGlobalExit'); }
48
 *     }]);
49
 * ```
50
 *
51
 * @example StateManager Events
52
 * In this example we are adding an event listener for the 'resize' event.
53
 * This event will be called independent of the original window resize event,
54
 * because the resize will be compared in a requestAnimationFrame loop.
55
 *
56
 * ```
57
 *     StateManager.on('resize', function () {
58
 *         console.log('onResize');
59
 *     });
60
 *
61
 *     StateManager.once('resize', function () {
62
 *         console.log('This resize event will only be called once');
63
 *     });
64
 * ```
65
 *
66
 * @example StateManager plugin support
67
 * In this example we register the plugin 'pluginName' on the element
68
 * matching the '.my-selector' selector.
69
 * You can also define view ports in which the plugin will be available.
70
 * When switching the view ports and the configuration isn't changed for
71
 * that state, only the 'update' function of the plugin will be called.
72
 *
73
 * ```
74
 *     // The plugin will be available on all view port states.
75
 *     // Uses the default configuration
76
 *
77
 *     StateManager.addPlugin('.my-selector', 'pluginName');
78
 *
79
 *     // The plugin will only be available for the 'xs' state.
80
 *     // Uses the default configuration.
81
 *
82
 *     StateManager.addPlugin('.my-selector', 'pluginName', 'xs');
83
 *
84
 *     // The plugin will only be available for the 'l' and 'xl' state.
85
 *     // Uses the default configuration.
86
 *
87
 *     StateManager.addPlugin('.my-selector', 'pluginName', ['l', 'xl']);
88
 *
89
 *     // The plugin will only be available for the 'xs' and 's' state.
90
 *     // For those two states, the passed config will be used.
91
 *
92
 *     StateManager.addPlugin('.my-selector', 'pluginName', {
93
 *         'configA': 'valueA',
94
 *         'configB': 'valueB',
95
 *         'configFoo': 'valueBar'
96
 *     }, ['xs', 's']);
97
 *
98
 *     // The plugin is available on all view port states.
99
 *     // We override the 'foo' config only for the 'm' state.
100
 *
101
 *     StateManager.addPlugin('.my-selector', 'pluginName', { 'foo': 'bar' })
102
 *                .addPlugin('.my-selector', 'pluginName', { 'foo': 'baz' }, 'm');
103
 * ```
104
 */
105
;(function ($, window, document) {
106
    'use strict';
107
108
    var $html = $('html'),
109
        vendorPropertyDiv = document.createElement('div'),
110
        vendorPrefixes = ['webkit', 'moz', 'ms', 'o'];
111
112
    /**
113
     * @class EventEmitter
114
     * @constructor
115
     */
116
    function EventEmitter() {
117
        var me = this;
118
119
        /**
120
         * @private
121
         * @property _events
122
         * @type {Object}
123
         */
124
        me._events = {};
125
    }
126
127
    EventEmitter.prototype = {
128
129
        constructor: EventEmitter,
130
131
        name: 'EventEmitter',
132
133
        /**
134
         * @public
135
         * @chainable
136
         * @method on
137
         * @param {String} eventName
138
         * @param {Function} callback
139
         * @param {*} context
140
         * @returns {EventEmitter}
141
         */
142
        on: function (eventName, callback, context) {
143
            var me = this,
144
                events = me._events || (me._events = {}),
145
                event = events[eventName] || (events[eventName] = []);
146
147
            event.push({
148
                callback: callback,
149
                context: context || me
150
            });
151
152
            return me;
153
        },
154
155
        /**
156
         * @public
157
         * @chainable
158
         * @method once
159
         * @param {String} eventName
160
         * @param {Function} callback
161
         * @param {*} context
162
         * @returns {EventEmitter}
163
         */
164
        once: function (eventName, callback, context) {
165
            var me = this,
166
                once = function () {
167
                    me.off(eventName, once, context);
168
                    callback.apply(me, arguments);
169
                };
170
171
            return me.on(eventName, once, context);
172
        },
173
174
        /**
175
         * @public
176
         * @chainable
177
         * @method off
178
         * @param {String} eventName
179
         * @param {Function} callback
180
         * @param {*} context
181
         * @returns {EventEmitter}
182
         */
183
        off: function (eventName, callback, context) {
184
            var me = this,
185
                events = me._events || (me._events = {}),
186
                eventNames = eventName ? [eventName] : Object.keys(events),
187
                eventList,
188
                event,
189
                name,
190
                len,
191
                i, j;
192
193
            for (i = 0, len = eventNames.length; i < len; i++) {
194
                name = eventNames[i];
195
                eventList = events[name];
196
197
                /**
198
                 * Return instead of continue because only the one passed
199
                 * event name can be wrong / not available.
200
                 */
201
                if (!eventList) {
202
                    return me;
203
                }
204
205
                if (!callback && !context) {
206
                    eventList.length = 0;
207
                    delete events[name];
208
                    continue;
209
                }
210
211
                for (j = eventList.length - 1; j >= 0; j--) {
212
                    event = eventList[j];
213
214
                    // Check if the callback and the context (if passed) is the same
215
                    if ((callback && callback !== event.callback) || (context && context !== event.context)) {
216
                        continue;
217
                    }
218
219
                    eventList.splice(j, 1);
220
                }
221
            }
222
223
            return me;
224
        },
225
226
        /**
227
         * @public
228
         * @chainable
229
         * @method trigger
230
         * @param {String} eventName
231
         * @returns {EventEmitter}
232
         */
233
        trigger: function (eventName) {
234
            var me = this,
235
                events = me._events || (me._events = {}),
236
                eventList = events[eventName],
237
                event,
238
                args,
239
                a1, a2, a3,
240
                len, i;
241
242
            if (!eventList) {
243
                return me;
244
            }
245
246
            args = Array.prototype.slice.call(arguments, 1);
247
            len = eventList.length;
248
            i = -1;
249
250
            if (args.length <= 3) {
251
                a1 = args[0];
252
                a2 = args[1];
253
                a3 = args[2];
254
            }
255
256
            /**
257
             * Using switch to improve the performance of listener calls
258
             * .call() has a much greater performance than .apply() on
259
             * many parameters.
260
             */
261
            switch (args.length) {
262
                case 0:
263
                    while (++i < len) (event = eventList[i]).callback.call(event.context);
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
264
                    return me;
265
                case 1:
266
                    while (++i < len) (event = eventList[i]).callback.call(event.context, a1);
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
Bug introduced by
The variable a1 does not seem to be initialized in case args.length <= 3 on line 250 is false. Are you sure the function call handles undefined variables?
Loading history...
267
                    return me;
268
                case 2:
269
                    while (++i < len) (event = eventList[i]).callback.call(event.context, a1, a2);
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
Bug introduced by
The variable a2 does not seem to be initialized in case args.length <= 3 on line 250 is false. Are you sure the function call handles undefined variables?
Loading history...
270
                    return me;
271
                case 3:
272
                    while (++i < len) (event = eventList[i]).callback.call(event.context, a1, a2, a3);
0 ignored issues
show
Bug introduced by
The variable a3 does not seem to be initialized in case args.length <= 3 on line 250 is false. Are you sure the function call handles undefined variables?
Loading history...
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
273
                    return me;
274
                default:
275
                    while (++i < len) (event = eventList[i]).callback.apply(event.context, args);
0 ignored issues
show
introduced by
This node falls through to the next case due to this statement. Please add a comment either directly below this line or between the cases to explain.
Loading history...
276
                    return me;
277
            }
278
        },
279
280
        /**
281
         * @public
282
         * @method destroy
283
         */
284
        destroy: function () {
285
            this.off();
286
        }
287
    };
288
289
    /**
290
     * @public
291
     * @static
292
     * @class StateManager
293
     * @extends {EventEmitter}
294
     * @type {Object}
295
     */
296
    window.StateManager = $.extend(Object.create(EventEmitter.prototype), {
297
        
298
        /**
299
         * Constructor for EventEmitter
300
         *
301
         * @public
302
         * @class EventEmitter
303
         * @constructor
304
         */
305
        EventEmitter: EventEmitter,
306
        
307
        /**
308
         * Collection of all registered breakpoints
309
         *
310
         * @private
311
         * @property _breakpoints
312
         * @type {Array}
313
         */
314
        _breakpoints: [],
315
316
        /**
317
         * Collection of all registered listeners
318
         *
319
         * @private
320
         * @property _listeners
321
         * @type {Array}
322
         */
323
        _listeners: [],
324
325
        /**
326
         * Collection of all added plugin configurations
327
         *
328
         * @private
329
         * @property _plugins
330
         * @type {Object}
331
         */
332
        _plugins: {},
333
334
        /**
335
         * Collection of all plugins that should be initialized when the DOM is ready
336
         *
337
         * @private
338
         * @property _pluginQueue
339
         * @type {Object}
340
         */
341
        _pluginQueue: {},
342
343
        /**
344
         * Flag whether the queued plugins were initialized or not
345
         *
346
         * @private
347
         * @property _pluginsInitialized
348
         * @type {Boolean}
349
         */
350
        _pluginsInitialized: false,
351
352
        /**
353
         * Current breakpoint type
354
         *
355
         * @private
356
         * @property _currentState
357
         * @type {String}
358
         */
359
        _currentState: '',
360
361
        /**
362
         * Previous breakpoint type
363
         *
364
         * @private
365
         * @property _previousState
366
         * @type {String}
367
         */
368
        _previousState: '',
369
370
        /**
371
         * Last calculated viewport width.
372
         *
373
         * @private
374
         * @property _viewportWidth
375
         * @type {Number}
376
         */
377
        _viewportWidth: 0,
378
379
        /**
380
         * Cache for all previous gathered vendor properties.
381
         *
382
         * @private
383
         * @property _vendorPropertyCache
384
         * @type {Object}
385
         */
386
        _vendorPropertyCache: {},
387
388
        /**
389
         * Initializes the StateManager with the incoming breakpoint
390
         * declaration and starts the listing of the resize of the browser window.
391
         *
392
         * @public
393
         * @chainable
394
         * @method init
395
         * @param {Object|Array} breakpoints - User defined breakpoints.
396
         * @returns {StateManager}
397
         */
398
        init: function (breakpoints) {
399
            var me = this;
400
401
            me._viewportWidth = me.getViewportWidth();
402
403
            me._baseFontSize = parseInt($html.css('font-size'));
404
405
            me.registerBreakpoint(breakpoints);
406
407
            me._checkResize();
408
            me._browserDetection();
409
            me._setDeviceCookie();
410
            $($.proxy(me.initQueuedPlugins, me, true));
411
            $.publish('StateManager/onInit', [ me ]);
412
            return me;
413
        },
414
415
        /**
416
         * Adds a breakpoint to check against, after the {@link StateManager.init} was called.
417
         *
418
         * @public
419
         * @chainable
420
         * @method registerBreakpoint
421
         * @param {Array|Object} breakpoint.
422
         * @returns {StateManager}
423
         */
424
        registerBreakpoint: function (breakpoint) {
425
            var me = this,
426
                breakpoints = breakpoint instanceof Array ? breakpoint : Array.prototype.slice.call(arguments),
427
                len = breakpoints.length,
428
                i = 0;
429
430
            for (; i < len; i++) {
431
                me._addBreakpoint(breakpoints[i]);
432
            }
433
434
            return me;
435
        },
436
437
        /**
438
         * Adds a breakpoint to check against, after the {@link StateManager.init} was called.
439
         *
440
         * @private
441
         * @chainable
442
         * @method _addBreakpoint
443
         * @param {Object} breakpoint.
444
         */
445
        _addBreakpoint: function (breakpoint) {
446
            var me = this,
447
                breakpoints = me._breakpoints,
448
                existingBreakpoint,
449
                state = breakpoint.state,
450
                enter = me._convertRemValue(breakpoint.enter),
451
                exit = me._convertRemValue(breakpoint.exit),
452
                len = breakpoints.length,
453
                i = 0;
454
455
            breakpoint.enter = enter;
456
            breakpoint.exit = exit;
457
458
            for (; i < len; i++) {
459
                existingBreakpoint = breakpoints[i];
460
461
                if (existingBreakpoint.state === state) {
462
                    throw new Error('Multiple breakpoints of state "' + state + '" detected.');
463
                }
464
465
                if (existingBreakpoint.enter <= exit && enter <= existingBreakpoint.exit) {
466
                    throw new Error('Breakpoint range of state "' + state + '" overlaps state "' + existingBreakpoint.state + '".');
467
                }
468
            }
469
470
            breakpoints.push(breakpoint);
471
472
            me._plugins[state] = {};
473
            me._checkBreakpoint(breakpoint, me._viewportWidth);
474
475
            return me;
476
        },
477
478
        _convertRemValue: function(remValue) {
479
            var me = this,
480
                baseFontSize = me._baseFontSize;
481
482
            return remValue * baseFontSize;
483
        },
484
485
        /**
486
         * Removes breakpoint by state and removes the generated getter method for the state.
487
         *
488
         * @public
489
         * @chainable
490
         * @method removeBreakpoint
491
         * @param {String} state State which should be removed
492
         * @returns {StateManager}
493
         */
494
        removeBreakpoint: function (state) {
495
            var me = this,
496
                breakpoints = me._breakpoints,
497
                len = breakpoints.length,
498
                i = 0;
499
500
            if (typeof state !== 'string') {
501
                return me;
502
            }
503
504
            for (; i < len; i++) {
505
                if (state !== breakpoints[i].state) {
506
                    continue;
507
                }
508
509
                breakpoints.splice(i, 1);
510
511
                return me._removeStatePlugins(state);
512
            }
513
514
            return me;
515
        },
516
517
        /**
518
         * @protected
519
         * @chainable
520
         * @method _removeStatePlugins
521
         * @param {String} state
522
         * @returns {StateManager}
523
         */
524
        _removeStatePlugins: function (state) {
525
            var me = this,
526
                plugins = me._plugins[state],
527
                selectors = Object.keys(plugins),
528
                selectorLen = selectors.length,
529
                pluginNames,
530
                pluginLen,
531
                i, j;
532
533
            for (i = 0; i < selectorLen; i++) {
534
                pluginNames = Object.keys(plugins[selectors[i]]);
535
536
                for (j = 0, pluginLen = pluginNames.length; j < pluginLen; j++) {
537
                    me.destroyPlugin(selectors[i], pluginNames[j]);
538
                }
539
            }
540
541
            delete plugins[state];
542
543
            return me;
544
        },
545
546
        /**
547
         * Registers one or multiple event listeners to the StateManager,
548
         * so they will be fired when the state matches the current active
549
         * state..
550
         *
551
         * @public
552
         * @chainable
553
         * @method registerListener
554
         * @param {Object|Array} listener
555
         * @returns {StateManager}
556
         */
557
        registerListener: function (listener) {
558
            var me = this,
559
                listenerArr = listener instanceof Array ? listener : Array.prototype.slice.call(arguments),
560
                len = listenerArr.length,
561
                i = 0;
562
563
            for (; i < len; i++) {
564
                me._addListener(listenerArr[i]);
565
            }
566
567
            return me;
568
        },
569
570
        /**
571
         * @private
572
         * @chainable
573
         * @method _addListener
574
         * @param {Object} listener.
575
         */
576
        _addListener: function (listener) {
577
            var me = this,
578
                listeners = me._listeners,
579
                enterFn = listener.enter;
580
581
            listeners.push(listener);
582
583
            if ((listener.state === me._currentState || listener.state === '*') && typeof enterFn === 'function') {
584
                enterFn({
585
                    'exiting': me._previousState,
586
                    'entering': me._currentState
587
                });
588
            }
589
590
            return me;
591
        },
592
593
        /**
594
         * @public
595
         * @chainable
596
         * @method addPlugin
597
         * @param {String} selector
598
         * @param {String} pluginName
599
         * @param {Object|Array|String} config
600
         * @param {Array|String} viewport
601
         * @returns {StateManager}
602
         */
603
        addPlugin: function (selector, pluginName, config, viewport) {
604
            var me = this,
605
                pluginsInitialized = me._pluginsInitialized,
606
                breakpoints = me._breakpoints,
607
                currentState = me._currentState,
608
                len,
609
                i;
610
611
            // If the third parameter are the viewport states
612
            if (typeof config === 'string' || config instanceof Array) {
613
                viewport = config;
614
                config = {};
615
            }
616
617
            if (typeof viewport === 'string') {
618
                viewport = [viewport];
619
            }
620
621
            if (!(viewport instanceof Array)) {
622
                viewport = [];
623
624
                for (i = 0, len = breakpoints.length; i < len; i++) {
625
                    viewport.push(breakpoints[i].state);
626
                }
627
            }
628
629
            for (i = 0, len = viewport.length; i < len; i++) {
630
                me._addPluginOption(viewport[i], selector, pluginName, config);
631
632
                if (currentState !== viewport[i]) {
633
                    continue;
634
                }
635
636
                if (pluginsInitialized) {
637
                    me._initPlugin(selector, pluginName);
638
                    continue;
639
                }
640
641
                me.addPluginToQueue(selector, pluginName);
642
            }
643
644
            return me;
645
        },
646
647
        /**
648
         * @public
649
         * @chainable
650
         * @method removePlugin
651
         * @param {String} selector
652
         * @param {String} pluginName
653
         * @param {Array|String} viewport
654
         * @returns {StateManager}
655
         */
656
        removePlugin: function (selector, pluginName, viewport) {
657
            var me = this,
658
                breakpoints = me._breakpoints,
659
                plugins = me._plugins,
660
                state,
661
                sel,
662
                len,
663
                i;
664
665
            if (typeof viewport === 'string') {
666
                viewport = [viewport];
667
            }
668
669
            if (!(viewport instanceof Array)) {
670
                viewport = [];
671
672
                for (i = 0, len = breakpoints.length; i < len; i++) {
673
                    viewport.push(breakpoints[i].state);
674
                }
675
            }
676
677
            for (i = 0, len = viewport.length; i < len; i++) {
678
                if (!(state = plugins[viewport[i]])) {
679
                    continue;
680
                }
681
682
                if (!(sel = state[selector])) {
683
                    continue;
684
                }
685
686
                delete sel[pluginName];
687
            }
688
689
            if (!me._pluginsInitialized) {
690
                me.removePluginFromQueue(selector, pluginName);
691
            }
692
693
            return me;
694
        },
695
696
        /**
697
         * @public
698
         * @chainable
699
         * @method updatePlugin
700
         * @param {String} selector
701
         * @param {String} pluginName
702
         * @returns {StateManager}
703
         */
704
        updatePlugin: function (selector, pluginName) {
705
            var me = this,
706
                state = me._currentState,
707
                pluginConfigs = me._plugins[state][selector] || {},
708
                pluginNames = (typeof pluginName === 'string') ? [pluginName] : Object.keys(pluginConfigs),
709
                len = pluginNames.length,
710
                i = 0;
711
712
            for (; i < len; i++) {
713
                me._initPlugin(selector, pluginNames[i]);
714
            }
715
716
            return me;
717
        },
718
719
        /**
720
         * @private
721
         * @method _addPluginOption
722
         * @param {String} state
723
         * @param {String} selector
724
         * @param {String} pluginName
725
         * @param {Object} config
726
         */
727
        _addPluginOption: function (state, selector, pluginName, config) {
728
            var me = this,
729
                plugins = me._plugins,
730
                selectors = plugins[state] || (plugins[state] = {}),
731
                configs = selectors[selector] || (selectors[selector] = {}),
732
                pluginConfig = configs[pluginName];
733
734
            configs[pluginName] = $.extend(pluginConfig || {}, config);
735
        },
736
737
        /**
738
         * @private
739
         * @method _initPlugin
740
         * @param {String} selector
741
         * @param {String} pluginName
742
         */
743
        _initPlugin: function (selector, pluginName) {
744
            var me = this,
745
                $el = $(selector);
746
747
            if ($el.length > 1) {
748
                $.each($el, function () {
749
                    me._initSinglePlugin($(this), selector, pluginName);
750
                });
751
                return;
752
            }
753
754
            me._initSinglePlugin($el, selector, pluginName);
755
        },
756
757
        /**
758
         * @public
759
         * @method addPluginToQueue
760
         * @param {String} selector
761
         * @param {String} pluginName
762
         */
763
        addPluginToQueue: function (selector, pluginName) {
764
            var me = this,
765
                queue = me._pluginQueue,
766
                pluginNames = queue[selector] || (queue[selector] = []);
767
768
            if (pluginNames.indexOf(pluginName) === -1) {
769
                pluginNames.push(pluginName);
770
            }
771
        },
772
773
        /**
774
         * @public
775
         * @method removePluginFromQueue
776
         * @param {String} selector
777
         * @param {String} pluginName
778
         */
779
        removePluginFromQueue: function (selector, pluginName) {
780
            var me = this,
781
                queue = me._pluginQueue,
782
                pluginNames = queue[selector],
783
                index;
784
785
            if (pluginNames && (index = pluginNames.indexOf(pluginName)) !== -1) {
786
                pluginNames.splice(index, 1);
787
            }
788
        },
789
790
        /**
791
         * @public
792
         * @method initQueuedPlugins
793
         * @param {Boolean} clearQueue
794
         */
795
        initQueuedPlugins: function (clearQueue) {
796
            var me = this,
797
                queue = me._pluginQueue,
798
                selectors = Object.keys(queue),
799
                selectorLen = selectors.length,
800
                i = 0,
801
                selector,
802
                plugins,
803
                pluginLen,
804
                j;
805
806
            for (; i < selectorLen; i++) {
807
                selector = selectors[i];
808
                plugins = queue[selector];
809
810
                for (j = 0, pluginLen = plugins.length; j < pluginLen; j++) {
811
                    me._initPlugin(selector, plugins[j]);
812
                }
813
814
                if (clearQueue !== false) {
815
                    delete queue[selector];
816
                }
817
            }
818
819
            me._pluginsInitialized = true;
820
        },
821
822
        /**
823
         * @private
824
         * @method _initSinglePlugin
825
         * @param {Object} element
826
         * @param {String} selector
827
         * @param {String} pluginName
828
         */
829
        _initSinglePlugin: function (element, selector, pluginName) {
830
            var me = this,
831
                currentConfig = me._getPluginConfig(me._currentState, selector, pluginName),
832
                plugin = element.data('plugin_' + pluginName);
833
834
            if (!plugin) {
835
                if (!element[pluginName]) {
836
                    console.error('Plugin "' + pluginName + '" is not a valid jQuery-plugin!');
837
                    return;
838
                }
839
840
                element[pluginName](currentConfig);
841
                return;
842
            }
843
844
            if (JSON.stringify(currentConfig) === JSON.stringify(me._getPluginConfig(me._previousState, selector, pluginName))) {
845
                if (typeof plugin.update === 'function') {
846
                    plugin.update(me._currentState, me._previousState);
847
                }
848
                return;
849
            }
850
851
            me.destroyPlugin(element, pluginName);
852
853
            element[pluginName](currentConfig);
854
        },
855
856
        /**
857
         * @private
858
         * @method _getPluginConfig
859
         * @param {String} state
860
         * @param {String} selector
861
         * @param {String} plugin
862
         */
863
        _getPluginConfig: function (state, selector, plugin) {
864
            var selectors = this._plugins[state] || {},
865
                pluginConfigs = selectors[selector] || {};
866
867
            return pluginConfigs[plugin] || {};
868
        },
869
870
        /**
871
         * @private
872
         * @method _checkResize
873
         */
874
        _checkResize: function () {
875
            var me = this,
876
                width = me.getViewportWidth();
877
878
            if (width !== me._viewportWidth) {
879
                me._checkBreakpoints(width);
880
                me.trigger('resize', width);
881
                me._setDeviceCookie();
882
            }
883
884
            me._viewportWidth = width;
885
886
            me.requestAnimationFrame(me._checkResize.bind(me));
887
        },
888
889
        /**
890
         * @private
891
         * @method _checkBreakpoints
892
         * @param {Number} width
893
         */
894
        _checkBreakpoints: function (width) {
895
            var me = this,
896
                checkWidth = width || me.getViewportWidth(),
897
                breakpoints = me._breakpoints,
898
                len = breakpoints.length,
899
                i = 0;
900
901
            for (; i < len; i++) {
902
                me._checkBreakpoint(breakpoints[i], checkWidth);
903
            }
904
905
            return me;
906
        },
907
908
        /**
909
         * @private
910
         * @method _checkBreakpoint
911
         * @param {Object} breakpoint
912
         * @param {Number} width
913
         */
914
        _checkBreakpoint: function (breakpoint, width) {
915
            var me = this,
916
                checkWidth = width || me.getViewportWidth(),
917
                enterWidth = ~~(breakpoint.enter),
918
                exitWidth = ~~(breakpoint.exit),
919
                state = breakpoint.state;
920
921
            if (state !== me._currentState && checkWidth >= enterWidth && checkWidth <= exitWidth) {
922
                me._changeBreakpoint(state);
923
            }
924
        },
925
926
        /**
927
         * @private
928
         * @chainable
929
         * @method _changeBreakpoint
930
         * @param {String} state
931
         * @returns {StateManager}
932
         */
933
        _changeBreakpoint: function (state) {
934
            var me = this,
935
                previousState = me._previousState = me._currentState,
936
                currentState = me._currentState = state;
937
938
            return me
939
                .trigger('exitBreakpoint', previousState)
940
                .trigger('changeBreakpoint', {
941
                    'entering': currentState,
942
                    'exiting': previousState
943
                })
944
                .trigger('enterBreakpoint', currentState)
945
                ._switchListener(previousState, currentState)
946
                ._switchPlugins(previousState, currentState);
947
        },
948
949
        /**
950
         * @private
951
         * @chainable
952
         * @method _switchListener
953
         * @param {String} fromState
954
         * @param {String} toState
955
         * @returns {StateManager}
956
         */
957
        _switchListener: function (fromState, toState) {
958
            var me = this,
959
                previousListeners = me._getBreakpointListeners(fromState),
960
                currentListeners = me._getBreakpointListeners(toState),
961
                eventObj = {
962
                    'exiting': fromState,
963
                    'entering': toState
964
                },
965
                callFn,
966
                len,
967
                i;
968
969
            for (i = 0, len = previousListeners.length; i < len; i++) {
970
                if (typeof (callFn = previousListeners[i].exit) === 'function') {
971
                    callFn(eventObj);
972
                }
973
            }
974
975
            for (i = 0, len = currentListeners.length; i < len; i++) {
976
                if (typeof (callFn = currentListeners[i].enter) === 'function') {
977
                    callFn(eventObj);
978
                }
979
            }
980
981
            return me;
982
        },
983
984
        /**
985
         * @private
986
         * @method _getBreakpointListeners
987
         * @param {String} state
988
         * @returns {Array}
989
         */
990
        _getBreakpointListeners: function (state) {
991
            var me = this,
992
                listeners = me._listeners,
993
                breakpointListeners = [],
994
                len = listeners.length,
995
                i = 0,
996
                listenerType;
997
998
            for (; i < len; i++) {
999
                if ((listenerType = listeners[i].state) === state || listenerType === '*') {
1000
                    breakpointListeners.push(listeners[i]);
1001
                }
1002
            }
1003
1004
            return breakpointListeners;
1005
        },
1006
1007
        /**
1008
         * @private
1009
         * @chainable
1010
         * @method _switchPlugins
1011
         * @param {String} fromState
1012
         * @param {String} toState
1013
         * @returns {StateManager}
1014
         */
1015
        _switchPlugins: function (fromState, toState) {
1016
            var me = this,
1017
                plugins = me._plugins,
1018
                fromSelectors = plugins[fromState] || {},
1019
                fromKeys = Object.keys(fromSelectors),
1020
                selector,
1021
                oldPluginConfigs,
1022
                newPluginConfigs,
1023
                configKeys,
1024
                pluginName,
1025
                plugin,
1026
                $el,
1027
                toSelectors = plugins[toState] || {},
1028
                toKeys = Object.keys(toSelectors),
1029
                lenKeys, lenConfig, lenEl,
1030
                x, y, z;
1031
1032
            for (x = 0, lenKeys = fromKeys.length; x < lenKeys; x++) {
1033
                selector = fromKeys[x];
1034
                oldPluginConfigs = fromSelectors[selector];
1035
                $el = $(selector);
1036
1037
                if (!oldPluginConfigs || !(lenEl = $el.length)) {
1038
                    continue;
1039
                }
1040
1041
                newPluginConfigs = toSelectors[selector];
1042
                configKeys = Object.keys(oldPluginConfigs);
1043
1044
                for (y = 0, lenConfig = configKeys.length; y < lenConfig; y++) {
1045
                    pluginName = configKeys[y];
1046
1047
                    // When no new state config is available, destroy the old plugin
1048
                    if (!newPluginConfigs || !(newPluginConfigs[pluginName])) {
1049
                        me.destroyPlugin($el, pluginName);
1050
                        continue;
1051
                    }
1052
1053
                    if (JSON.stringify(newPluginConfigs[pluginName]) === JSON.stringify(oldPluginConfigs[pluginName])) {
1054
                        for (z = 0; z < lenEl; z++) {
1055
                            if (!(plugin = $($el[z]).data('plugin_' + pluginName))) {
1056
                                continue;
1057
                            }
1058
1059
                            if (typeof plugin.update === 'function') {
1060
                                plugin.update(fromState, toState);
1061
                            }
1062
                        }
1063
                        continue;
1064
                    }
1065
1066
                    me.destroyPlugin($el, pluginName);
1067
                }
1068
            }
1069
1070
            for (x = 0, lenKeys = toKeys.length; x < lenKeys; x++) {
1071
                selector = toKeys[x];
1072
                newPluginConfigs = toSelectors[selector];
1073
                $el = $(selector);
1074
1075
                if (!newPluginConfigs || !$el.length) {
1076
                    continue;
1077
                }
1078
1079
                configKeys = Object.keys(newPluginConfigs);
1080
1081
                for (y = 0, lenConfig = configKeys.length; y < lenConfig; y++) {
1082
                    pluginName = configKeys[y];
1083
1084
                    if (!$el.data('plugin_' + pluginName)) {
1085
                        $el[pluginName](newPluginConfigs[pluginName]);
1086
                    }
1087
                }
1088
            }
1089
1090
            return me;
1091
        },
1092
1093
        /**
1094
         * @public
1095
         * @method destroyPlugin
1096
         * @param {String|jQuery} selector
1097
         * @param {String} pluginName
1098
         */
1099
        destroyPlugin: function (selector, pluginName) {
1100
            var $el = (typeof selector === 'string') ? $(selector) : selector,
1101
                name = 'plugin_' + pluginName,
1102
                len = $el.length,
1103
                i = 0,
1104
                $currentEl,
1105
                plugin;
1106
1107
            if (!len) {
1108
                return;
1109
            }
1110
1111
            for (; i < len; i++) {
1112
                $currentEl = $($el[i]);
1113
1114
                if ((plugin = $currentEl.data(name))) {
1115
                    plugin.destroy();
1116
                    $currentEl.removeData(name);
1117
                }
1118
            }
1119
        },
1120
1121
        /**
1122
         * Returns the current viewport width.
1123
         *
1124
         * @public
1125
         * @method getViewportWidth
1126
         * @returns {Number} The width of the viewport in pixels.
1127
         */
1128
        getViewportWidth: function () {
1129
            var width = window.innerWidth;
1130
1131
            if (typeof width === 'number') {
1132
                return width;
1133
            }
1134
1135
            return (width = document.documentElement.clientWidth) !== 0 ? width : document.body.clientWidth;
1136
        },
1137
1138
        /**
1139
         * Returns the current viewport height.
1140
         *
1141
         * @public
1142
         * @method getViewportHeight
1143
         * @returns {Number} The height of the viewport in pixels.
1144
         */
1145
        getViewportHeight: function () {
1146
            var height = window.innerHeight;
1147
1148
            if (typeof height === 'number') {
1149
                return height;
1150
            }
1151
1152
            return (height = document.documentElement.clientHeight) !== 0 ? height : document.body.clientHeight;
1153
        },
1154
1155
        /**
1156
         * Returns the current active state.
1157
         *
1158
         * @public
1159
         * @method getPrevious
1160
         * @returns {String} previous breakpoint state
1161
         */
1162
        getPreviousState: function () {
1163
            return this._previousState;
1164
        },
1165
1166
        /**
1167
         * Returns whether or not the previous active type is the passed one.
1168
         *
1169
         * @public
1170
         * @method getPrevious
1171
         * @param {String|Array} state
1172
         * @returns {Boolean}
1173
         */
1174
        isPreviousState: function (state) {
1175
            var states = state instanceof Array ? state : Array.prototype.slice.call(arguments),
1176
                previousState = this._previousState,
1177
                len = states.length,
1178
                i = 0;
1179
1180
            for (; i < len; i++) {
1181
                if (previousState === states[i]) {
1182
                    return true;
1183
                }
1184
            }
1185
1186
            return false;
1187
        },
1188
1189
        /**
1190
         * Returns the current active state.
1191
         *
1192
         * @public
1193
         * @method getCurrent
1194
         * @returns {String} current breakpoint state
1195
         */
1196
        getCurrentState: function () {
1197
            return this._currentState;
1198
        },
1199
1200
        /**
1201
         * Returns whether or not the current active state is the passed one.
1202
         *
1203
         * @public
1204
         * @method isCurrent
1205
         * @param {String | Array} state
1206
         * @returns {Boolean}
1207
         */
1208
        isCurrentState: function (state) {
1209
            var states = state instanceof Array ? state : Array.prototype.slice.call(arguments),
1210
                currentState = this._currentState,
1211
                len = states.length,
1212
                i = 0;
1213
1214
            for (; i < len; i++) {
1215
                if (currentState === states[i]) {
1216
                    return true;
1217
                }
1218
            }
1219
1220
            return false;
1221
        },
1222
1223
        /**
1224
         * Checks if the device is currently running in portrait mode.
1225
         *
1226
         * @public
1227
         * @method isPortraitMode
1228
         * @returns {Boolean} Whether or not the device is in portrait mode
1229
         */
1230
        isPortraitMode: function () {
1231
            return !!this.matchMedia('(orientation: portrait)').matches;
1232
        },
1233
1234
        /**
1235
         * Checks if the device is currently running in landscape mode.
1236
         *
1237
         * @public
1238
         * @method isLandscapeMode
1239
         * @returns {Boolean} Whether or not the device is in landscape mode
1240
         */
1241
        isLandscapeMode: function () {
1242
            return !!this.matchMedia('(orientation: landscape)').matches;
1243
        },
1244
1245
        /**
1246
         * Gets the device pixel ratio. All retina displays should return a value > 1, all standard
1247
         * displays like a desktop monitor will return 1.
1248
         *
1249
         * @public
1250
         * @method getDevicePixelRatio
1251
         * @returns {Number} The device pixel ratio.
1252
         */
1253
        getDevicePixelRatio: function () {
1254
            return window.devicePixelRatio || 1;
1255
        },
1256
1257
        /**
1258
         * Returns if the current user agent is matching the browser test.
1259
         *
1260
         * @param browser
1261
         * @returns {boolean}
1262
         */
1263
        isBrowser: function(browser) {
1264
            var regEx = new RegExp(browser.toLowerCase(), 'i');
1265
            return this._checkUserAgent(regEx);
1266
        },
1267
1268
        /**
1269
         * Checks the user agent against the given regexp.
1270
         *
1271
         * @param regEx
1272
         * @returns {boolean}
1273
         * @private
1274
         */
1275
        _checkUserAgent: function(regEx) {
1276
            return !!navigator.userAgent.toLowerCase().match(regEx);
1277
        },
1278
1279
        /**
1280
         * Detects the browser type and adds specific css classes to the html tag.
1281
         *
1282
         * @private
1283
         */
1284
        _browserDetection: function() {
1285
            var me = this,
1286
                detections = {};
1287
1288
            detections['is--edge'] = me._checkUserAgent(/edge\//);
1289
            detections['is--opera'] = me._checkUserAgent(/opera/);
1290
            detections['is--chrome'] = !detections['is--edge'] && me._checkUserAgent(/\bchrome\b/);
1291
            detections['is--firefox'] = me._checkUserAgent(/firefox/);
1292
            detections['is--webkit'] = !detections['is--edge'] && me._checkUserAgent(/webkit/);
1293
            detections['is--safari'] = !detections['is--edge'] && !detections['is--chrome'] && me._checkUserAgent(/safari/);
1294
            detections['is--ie'] = !detections['is--opera'] && (me._checkUserAgent(/msie/) || me._checkUserAgent(/trident\/7/));
1295
            detections['is--ie-touch'] = detections['is--ie'] && me._checkUserAgent(/touch/);
1296
            detections['is--gecko'] = !detections['is--webkit'] && me._checkUserAgent(/gecko/);
1297
1298
            $.each(detections, function(key, value) {
1299
                if (value) $html.addClass(key);
1300
            });
1301
        },
1302
1303
        _getCurrentDevice: function() {
1304
            var i = 0,
1305
                width = this.getViewportWidth(),
1306
                device = 'desktop',
1307
                devices = window.statisticDevices || [];
1308
1309
            for (; i < devices.length; i++) {
1310
                if (width >= ~~(devices[i].enter) && width <= ~~(devices[i].exit)) {
1311
                    device = devices[i].device;
1312
                }
1313
            }
1314
1315
            return device;
1316
        },
1317
1318
        _setDeviceCookie: function() {
1319
            var device = this._getCurrentDevice();
1320
1321
            document.cookie = 'x-ua-device=' + device + '; path=/';
1322
        },
1323
1324
        /**
1325
         * First calculates the scroll bar width and height of the browser
1326
         * and saves it to a object that can be accessed.
1327
         *
1328
         * @private
1329
         * @property _scrollBarSize
1330
         * @type {Object}
1331
         */
1332
        _scrollBarSize: (function () {
1333
            var $el = $('<div>', {
1334
                    css: {
1335
                        width: 100,
1336
                        height: 100,
1337
                        overflow: 'scroll',
1338
                        position: 'absolute',
1339
                        top: -9999
1340
                    }
1341
                }),
1342
                el = $el[0],
1343
                width,
1344
                height;
1345
1346
            $('body').append($el);
1347
1348
            width = el.offsetWidth - el.clientWidth;
1349
            height = el.offsetHeight - el.clientHeight;
1350
1351
            $($el).remove();
1352
1353
            return {
1354
                width: width,
1355
                height: height
1356
            };
1357
        }()),
1358
1359
        /**
1360
         * Returns an object containing the width and height of the default
1361
         * scroll bar sizes.
1362
         *
1363
         * @public
1364
         * @method getScrollBarSize
1365
         * @returns {Object} The width/height pair of the scroll bar size.
1366
         */
1367
        getScrollBarSize: function () {
1368
            return $.extend({}, this._scrollBarSize);
1369
        },
1370
1371
        /**
1372
         * Returns the default scroll bar width of the browser.
1373
         *
1374
         * @public
1375
         * @method getScrollBarWidth
1376
         * @returns {Number} Width of the default browser scroll bar.
1377
         */
1378
        getScrollBarWidth: function () {
1379
            return this._scrollBarSize.width;
1380
        },
1381
1382
        /**
1383
         * Returns the default scroll bar width of the browser.
1384
         *
1385
         * @public
1386
         * @method getScrollBarHeight
1387
         * @returns {Number} Height of the default browser scroll bar.
1388
         */
1389
        getScrollBarHeight: function () {
1390
            return this._scrollBarSize.height;
1391
        },
1392
1393
        /**
1394
         * matchMedia proxy
1395
         *
1396
         * @public
1397
         * @method matchMedia
1398
         * @param {String} media
1399
         */
1400
        matchMedia: window.matchMedia.bind(window),
1401
1402
        /**
1403
         * requestAnimationFrame proxy
1404
         *
1405
         * @public
1406
         * @method requestAnimationFrame
1407
         * @param {Function} callback
1408
         * @returns {Number}
1409
         */
1410
        requestAnimationFrame: window.requestAnimationFrame.bind(window),
1411
1412
        /**
1413
         * cancelAnimationFrame proxy
1414
         *
1415
         * @public
1416
         * @method cancelAnimationFrame
1417
         * @param {Number} id
1418
         */
1419
        cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
1420
1421
        /**
1422
         * Tests the given CSS style property on an empty div with all vendor
1423
         * properties. If it fails and the softError flag was not set, it
1424
         * returns null, otherwise the given property.
1425
         *
1426
         * @example
1427
         *
1428
         * // New chrome version
1429
         * StateManager.getVendorProperty('transform'); => 'transform'
1430
         *
1431
         * // IE9
1432
         * StateManager.getVendorProperty('transform'); => 'msTransform'
1433
         *
1434
         * // Property not supported, without soft error flag
1435
         * StateManager.getVendorProperty('animation'); => null
1436
         *
1437
         * // Property not supported, with soft error flag
1438
         * StateManager.getVendorProperty('animation', true); => 'animate'
1439
         *
1440
         * @public
1441
         * @method getVendorProperty
1442
         * @param {String} property
1443
         * @param {Boolean} softError
1444
         */
1445
        getVendorProperty: function (property, softError) {
1446
            var cache = this._vendorPropertyCache,
1447
                style = vendorPropertyDiv.style;
1448
1449
            if (cache[property]) {
1450
                return cache[property];
1451
            }
1452
1453
            if (property in style) {
1454
                return (cache[property] = property);
1455
            }
1456
1457
            var prop = property.charAt(0).toUpperCase() + property.substr(1),
1458
                len = vendorPrefixes.length,
1459
                i = 0,
1460
                vendorProp;
1461
1462
            for (; i < len; i++) {
1463
                vendorProp = vendorPrefixes[i] + prop;
1464
1465
                if (vendorProp in style) {
1466
                    return (cache[property] = vendorProp);
1467
                }
1468
            }
1469
1470
            return (cache[property] = (softError ? property : null));
1471
        }
1472
    });
1473
})(jQuery, window, document);
1474