1/**
2 * Copyright Marc J. Schmidt. See the LICENSE file at the top-level
3 * directory of this distribution and at
4 * https://github.com/marcj/css-element-queries/blob/master/LICENSE.
5 */
6;
7(function() {
8    /**
9     *
10     * @type {Function}
11     * @constructor
12     */
13    var ElementQueries = this.ElementQueries = function() {
14
15        this.withTracking = false;
16        var elements = [];
17
18        /**
19         *
20         * @param element
21         * @returns {Number}
22         */
23        function getEmSize(element) {
24            if (!element) {
25                element = document.documentElement;
26            }
27            var fontSize = getComputedStyle(element, 'fontSize');
28            return parseFloat(fontSize) || 16;
29        }
30
31        /**
32         *
33         * @copyright https://github.com/Mr0grog/element-query/blob/master/LICENSE
34         *
35         * @param {HTMLElement} element
36         * @param {*} value
37         * @returns {*}
38         */
39        function convertToPx(element, value) {
40            var units = value.replace(/[0-9]*/, '');
41            value = parseFloat(value);
42            switch (units) {
43                case "px":
44                    return value;
45                case "em":
46                    return value * getEmSize(element);
47                case "rem":
48                    return value * getEmSize();
49                // Viewport units!
50                // According to http://quirksmode.org/mobile/tableViewport.html
51                // documentElement.clientWidth/Height gets us the most reliable info
52                case "vw":
53                    return value * document.documentElement.clientWidth / 100;
54                case "vh":
55                    return value * document.documentElement.clientHeight / 100;
56                case "vmin":
57                case "vmax":
58                    var vw = document.documentElement.clientWidth / 100;
59                    var vh = document.documentElement.clientHeight / 100;
60                    var chooser = Math[units === "vmin" ? "min" : "max"];
61                    return value * chooser(vw, vh);
62                default:
63                    return value;
64                // for now, not supporting physical units (since they are just a set number of px)
65                // or ex/ch (getting accurate measurements is hard)
66            }
67        }
68
69        /**
70         *
71         * @param {HTMLElement} element
72         * @constructor
73         */
74        function SetupInformation(element) {
75            this.element = element;
76            this.options = {};
77            var key, option, width = 0, height = 0, value, actualValue, attrValues, attrValue, attrName;
78
79            /**
80             * @param {Object} option {mode: 'min|max', property: 'width|height', value: '123px'}
81             */
82            this.addOption = function(option) {
83                var idx = [option.mode, option.property, option.value].join(',');
84                this.options[idx] = option;
85            };
86
87            var attributes = ['min-width', 'min-height', 'max-width', 'max-height'];
88
89            /**
90             * Extracts the computed width/height and sets to min/max- attribute.
91             */
92            this.call = function() {
93                // extract current dimensions
94                width = this.element.offsetWidth;
95                height = this.element.offsetHeight;
96
97                attrValues = {};
98
99                for (key in this.options) {
100                    if (!this.options.hasOwnProperty(key)){
101                        continue;
102                    }
103                    option = this.options[key];
104
105                    value = convertToPx(this.element, option.value);
106
107                    actualValue = option.property == 'width' ? width : height;
108                    attrName = option.mode + '-' + option.property;
109                    attrValue = '';
110
111                    if (option.mode == 'min' && actualValue >= value) {
112                        attrValue += option.value;
113                    }
114
115                    if (option.mode == 'max' && actualValue <= value) {
116                        attrValue += option.value;
117                    }
118
119                    if (!attrValues[attrName]) attrValues[attrName] = '';
120                    if (attrValue && -1 === (' '+attrValues[attrName]+' ').indexOf(' ' + attrValue + ' ')) {
121                        attrValues[attrName] += ' ' + attrValue;
122                    }
123                }
124
125                for (var k in attributes) {
126                    if (attrValues[attributes[k]]) {
127                        this.element.setAttribute(attributes[k], attrValues[attributes[k]].substr(1));
128                    } else {
129                        this.element.removeAttribute(attributes[k]);
130                    }
131                }
132            };
133        }
134
135        /**
136         * @param {HTMLElement} element
137         * @param {Object}      options
138         */
139        function setupElement(element, options) {
140            if (element.elementQueriesSetupInformation) {
141                element.elementQueriesSetupInformation.addOption(options);
142            } else {
143                element.elementQueriesSetupInformation = new SetupInformation(element);
144                element.elementQueriesSetupInformation.addOption(options);
145                element.elementQueriesSensor = new ResizeSensor(element, function() {
146                    element.elementQueriesSetupInformation.call();
147                });
148            }
149            element.elementQueriesSetupInformation.call();
150
151            if (this.withTracking) {
152                elements.push(element);
153            }
154        }
155
156        /**
157         * @param {String} selector
158         * @param {String} mode min|max
159         * @param {String} property width|height
160         * @param {String} value
161         */
162        function queueQuery(selector, mode, property, value) {
163            var query;
164            if (document.querySelectorAll) query = document.querySelectorAll.bind(document);
165            if (!query && 'undefined' !== typeof $$) query = $$;
166            if (!query && 'undefined' !== typeof jQuery) query = jQuery;
167
168            if (!query) {
169                throw 'No document.querySelectorAll, jQuery or Mootools\'s $$ found.';
170            }
171
172            var elements = query(selector);
173            for (var i = 0, j = elements.length; i < j; i++) {
174                setupElement(elements[i], {
175                    mode: mode,
176                    property: property,
177                    value: value
178                });
179            }
180        }
181
182        var regex = /,?([^,\n]*)\[[\s\t]*(min|max)-(width|height)[\s\t]*[~$\^]?=[\s\t]*"([^"]*)"[\s\t]*]([^\n\s\{]*)/mgi;
183
184        /**
185         * @param {String} css
186         */
187        function extractQuery(css) {
188            var match;
189            css = css.replace(/'/g, '"');
190            while (null !== (match = regex.exec(css))) {
191                if (5 < match.length) {
192                    queueQuery(match[1] || match[5], match[2], match[3], match[4]);
193                }
194            }
195        }
196
197        /**
198         * @param {CssRule[]|String} rules
199         */
200        function readRules(rules) {
201            var selector = '';
202            if (!rules) {
203                return;
204            }
205            if ('string' === typeof rules) {
206                rules = rules.toLowerCase();
207                if (-1 !== rules.indexOf('min-width') || -1 !== rules.indexOf('max-width')) {
208                    extractQuery(rules);
209                }
210            } else {
211                for (var i = 0, j = rules.length; i < j; i++) {
212                    if (1 === rules[i].type) {
213                        selector = rules[i].selectorText || rules[i].cssText;
214                        if (-1 !== selector.indexOf('min-height') || -1 !== selector.indexOf('max-height')) {
215                            extractQuery(selector);
216                        }else if(-1 !== selector.indexOf('min-width') || -1 !== selector.indexOf('max-width')) {
217                            extractQuery(selector);
218                        }
219                    } else if (4 === rules[i].type) {
220                        readRules(rules[i].cssRules || rules[i].rules);
221                    }
222                }
223            }
224        }
225
226        /**
227         * Searches all css rules and setups the event listener to all elements with element query rules..
228         *
229         * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
230         *                               (no garbage collection possible if you don not call .detach() first)
231         */
232        this.init = function(withTracking) {
233            this.withTracking = withTracking;
234            for (var i = 0, j = document.styleSheets.length; i < j; i++) {
235                try {
236                    readRules(document.styleSheets[i].cssText || document.styleSheets[i].cssRules || document.styleSheets[i].rules);
237                } catch(e) {
238                    if (e.name !== 'SecurityError') {
239                        throw e;
240                    }
241                }
242            }
243        };
244
245        /**
246         *
247         * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
248         *                               (no garbage collection possible if you don not call .detach() first)
249         */
250        this.update = function(withTracking) {
251            this.withTracking = withTracking;
252            this.init();
253        };
254
255        this.detach = function() {
256            if (!this.withTracking) {
257                throw 'withTracking is not enabled. We can not detach elements since we don not store it.' +
258                'Use ElementQueries.withTracking = true; before domready.';
259            }
260
261            var element;
262            while (element = elements.pop()) {
263                ElementQueries.detach(element);
264            }
265
266            elements = [];
267        };
268    };
269
270    /**
271     *
272     * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
273     *                               (no garbage collection possible if you don not call .detach() first)
274     */
275    ElementQueries.update = function(withTracking) {
276        ElementQueries.instance.update(withTracking);
277    };
278
279    /**
280     * Removes all sensor and elementquery information from the element.
281     *
282     * @param {HTMLElement} element
283     */
284    ElementQueries.detach = function(element) {
285        if (element.elementQueriesSetupInformation) {
286            element.elementQueriesSensor.detach();
287            delete element.elementQueriesSetupInformation;
288            delete element.elementQueriesSensor;
289            console.log('detached');
290        } else {
291            console.log('detached already', element);
292        }
293    };
294
295    ElementQueries.withTracking = false;
296
297    ElementQueries.init = function() {
298        if (!ElementQueries.instance) {
299            ElementQueries.instance = new ElementQueries();
300        }
301
302        ElementQueries.instance.init(ElementQueries.withTracking);
303    };
304
305    var domLoaded = function (callback) {
306        /* Internet Explorer */
307        /*@cc_on
308        @if (@_win32 || @_win64)
309            document.write('<script id="ieScriptLoad" defer src="//:"><\/script>');
310            document.getElementById('ieScriptLoad').onreadystatechange = function() {
311                if (this.readyState == 'complete') {
312                    callback();
313                }
314            };
315        @end @*/
316        /* Mozilla, Chrome, Opera */
317        if (document.addEventListener) {
318            document.addEventListener('DOMContentLoaded', callback, false);
319        }
320        /* Safari, iCab, Konqueror */
321        if (/KHTML|WebKit|iCab/i.test(navigator.userAgent)) {
322            var DOMLoadTimer = setInterval(function () {
323                if (/loaded|complete/i.test(document.readyState)) {
324                    callback();
325                    clearInterval(DOMLoadTimer);
326                }
327            }, 10);
328        }
329        /* Other web browsers */
330        window.onload = callback;
331    };
332
333    if (window.addEventListener) {
334        window.addEventListener('load', ElementQueries.init, false);
335    } else {
336        window.attachEvent('onload', ElementQueries.init);
337    }
338    domLoaded(ElementQueries.init);
339
340})();
341