Home | History | Annotate | Download | only in src
      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