github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/public/EQCSS.js (about)

     1  /*
     2  
     3  #  EQCSS
     4  ## version 1.7.0
     5  
     6  A JavaScript plugin to read EQCSS syntax to provide:
     7  scoped styles, element queries, container queries,
     8  meta-selectors, eval(), and element-based units.
     9  
    10  - github.com/eqcss/eqcss
    11  - elementqueries.com
    12  
    13  Authors: Tommy Hodgins, Maxime Euzière, Azareal
    14  
    15  License: MIT
    16  
    17  */
    18  
    19  // Uses Node, AMD or browser globals to create a module
    20  (function (root, factory) {
    21    if (typeof define === 'function' && define.amd) {
    22      // AMD: Register as an anonymous module
    23      define([], factory);
    24    } else if (typeof module === 'object' && module.exports) {
    25      // Node: Does not work with strict CommonJS, but
    26      // only CommonJS-like environments that support module.exports,
    27      // like Node
    28      module.exports = factory();
    29    } else {
    30      // Browser globals (root is window)
    31      root.EQCSS = factory();
    32    }
    33  }(this, function() {
    34      var EQCSS = {
    35        data: []
    36      }
    37  
    38      /*
    39       * EQCSS.load()
    40       * Called automatically on page load.
    41       * Call it manually after adding EQCSS code in the page.
    42       * Loads and parses all the EQCSS code.
    43       */
    44      EQCSS.load = function() {
    45        // Retrieve all style blocks
    46        var styles = document.getElementsByTagName('style');
    47  
    48        for (var i = 0; i < styles.length; i++) {
    49          // Test if the style is not read yet
    50          if (styles[i].getAttribute('data-eqcss-read') === null) {
    51  
    52            // Mark the style block as read
    53            styles[i].setAttribute('data-eqcss-read', 'true');
    54  
    55            EQCSS.process(styles[i].innerHTML);
    56          }
    57        }
    58  
    59        // Retrieve all link tags
    60        var link = document.getElementsByTagName('link');
    61  
    62        for (i = 0; i < link.length; i++) {
    63          // Test if the link is not read yet, and has rel=stylesheet
    64          if (link[i].getAttribute('data-eqcss-read') === null && link[i].rel === 'stylesheet' && link[i].getAttribute("href").endsWith("main.css")) {
    65            // retrieve the file content with AJAX and process it
    66            if (link[i].href) {
    67              (function() {
    68                var xhr = new XMLHttpRequest;
    69                xhr.open('GET', link[i].href, true);
    70                xhr.send(null);
    71                xhr.onreadystatechange = function() {
    72                  EQCSS.process(xhr.responseText);
    73                }
    74              })();
    75            }
    76            // Mark the link as read
    77            link[i].setAttribute('data-eqcss-read', 'true');
    78          }
    79        }
    80      }
    81  
    82      /*
    83       * EQCSS.parse()
    84       * Called by load for each script / style / link resource.
    85       * Generates data for each Element Query found
    86       */
    87      EQCSS.parse = function(code) {
    88        var parsed_queries = new Array();
    89  
    90        // Cleanup
    91        code = code.replace(/\s+/g, ' '); // reduce spaces and line breaks
    92        code = code.replace(/\/\*[\w\W]*?\*\//g, ''); // remove comments
    93        code = code.replace(/@element/g, '\n@element'); // one element query per line
    94        code = code.replace(/(@element.*?\{([^}]*?\{[^}]*?\}[^}]*?)*\}).*/g, '$1'); // Keep the queries only (discard regular css written around them)
    95  
    96        // Parse
    97  
    98        // For each query
    99        code.replace(/(@element.*(?!@element))/g, function(string, query) {
   100          // Create a data entry
   101          var dataEntry = {};
   102  
   103          // Extract the selector
   104          query.replace(/(@element)\s*(".*?"|'.*?'|.*?)\s*(and\s*\(|{)/g, function(string, atrule, selector, extra) {
   105            // Strip outer quotes if present
   106            selector = selector.replace(/^\s?['](.*)[']/, '$1');
   107            selector = selector.replace(/^\s?["](.*)["]/, '$1');
   108  
   109            dataEntry.selector = selector;
   110          })
   111  
   112          // Extract the conditions (measure, value, unit)
   113          dataEntry.conditions = [];
   114          query.replace(/and ?\( ?([^:]*) ?: ?([^)]*) ?\)/g, function(string, measure, value) {
   115            // Separate value and unit if it's possible
   116            var unit = null;
   117            unit = value.replace(/^(\d*\.?\d+)(\D+)$/, '$2');
   118  
   119            if (unit === value) {
   120              unit = null;
   121            }
   122            value = value.replace(/^(\d*\.?\d+)\D+$/, '$1');
   123            dataEntry.conditions.push({measure: measure, value: value, unit: unit});
   124          });
   125  
   126          // Extract the styles
   127          query.replace(/{(.*)}/g, function(string, style) {
   128            dataEntry.style = style;
   129          });
   130  
   131          parsed_queries.push(dataEntry);
   132        });
   133  
   134        return parsed_queries;
   135      }
   136  
   137      /*
   138       * EQCSS.register()
   139       * Add a single object, or an array of objects to EQCSS.data
   140       *
   141       */
   142       EQCSS.register = function(queries) {
   143         if (Object.prototype.toString.call(queries) === '[object Object]') {
   144           EQCSS.data.push(queries);
   145           EQCSS.apply();
   146         }
   147  
   148         if (Object.prototype.toString.call(queries) === '[object Array]') {
   149          for (var i=0; i<queries.length; i++) {
   150            EQCSS.data.push(queries[i]);
   151          }
   152          EQCSS.apply();
   153         }
   154       }
   155  
   156      /*
   157       * EQCSS.process()
   158       * Parse and Register queries with `EQCSS.data`
   159       */
   160  
   161       EQCSS.process = function(code) {
   162         var queries = EQCSS.parse(code)
   163         return EQCSS.register(queries)
   164       }
   165  
   166      /*
   167       * EQCSS.apply()
   168       * Called on load, on resize and manually on DOM update
   169       * Enable the Element Queries in which the conditions are true
   170       */
   171      EQCSS.apply = function() {
   172        var elements;                     // Elements targeted by each query
   173        var element_guid;                 // GUID for current element
   174        var css_block;                    // CSS block corresponding to each targeted element
   175        var element_guid_parent;          // GUID for current element's parent
   176        var element_guid_prev;            // GUID for current element's previous sibling element
   177        var element_guid_next;            // GUID for current element's next sibling element
   178        var css_code;                     // CSS code to write in each CSS block (one per targeted element)
   179        var element_width, parent_width;  // Computed widths
   180        var element_height, parent_height;// Computed heights
   181        var element_line_height;          // Computed line-height
   182        var test;                         // Query's condition test result
   183        var computed_style;               // Each targeted element's computed style
   184        var parent_computed_style;        // Each targeted element parent's computed style
   185  
   186        // Loop on all element queries
   187        for (var i = 0; i < EQCSS.data.length; i++) {
   188          // Find all the elements targeted by the query
   189          elements = document.querySelectorAll(EQCSS.data[i].selector);
   190  
   191          // Loop on all the elements
   192          for (var j = 0; j < elements.length; j++) {
   193            // Create a guid for this element
   194            // Pattern: 'EQCSS_{element-query-index}_{matched-element-index}'
   195            element_guid = 'data-eqcss-' + i + '-' + j;
   196  
   197            // Add this guid as an attribute to the element
   198            elements[j].setAttribute(element_guid, '');
   199  
   200            // Create a guid for the parent of this element
   201            // Pattern: 'EQCSS_{element-query-index}_{matched-element-index}_parent'
   202            element_guid_parent = 'data-eqcss-' + i + '-' + j + '-parent';
   203  
   204            // Add this guid as an attribute to the element's parent (except if element is the root element)
   205            if (elements[j] != document.documentElement) {
   206              elements[j].parentNode.setAttribute(element_guid_parent, '');
   207            }
   208  
   209            // Get the CSS block associated to this element (or create one in the <HEAD> if it doesn't exist)
   210            css_block = document.querySelector('#' + element_guid);
   211  
   212            if (!css_block) {
   213              css_block = document.createElement('style');
   214              css_block.id = element_guid;
   215              css_block.setAttribute('data-eqcss-read', 'true');
   216              document.querySelector('head').appendChild(css_block);
   217            }
   218            css_block = document.querySelector('#' + element_guid);
   219  
   220            // Reset the query test's result (first, we assume that the selector is matched)
   221            test = true;
   222  
   223            // Loop on the conditions
   224            test_conditions: for (var k = 0; k < EQCSS.data[i].conditions.length; k++) {
   225              // Reuse element and parent's computed style instead of computing it everywhere
   226              computed_style = window.getComputedStyle(elements[j], null);
   227  
   228              parent_computed_style = null;
   229  
   230              if (elements[j] != document.documentElement) {
   231                parent_computed_style = window.getComputedStyle(elements[j].parentNode, null);
   232              }
   233  
   234              // Do we have to reconvert the size in px at each call?
   235              // This is true only for vw/vh/vmin/vmax
   236              var recomputed = false;
   237  
   238              // If the condition's unit is vw, convert current value in vw, in px
   239              if (EQCSS.data[i].conditions[k].unit === 'vw') {
   240                recomputed = true;
   241  
   242                var value = parseInt(EQCSS.data[i].conditions[k].value);
   243                EQCSS.data[i].conditions[k].recomputed_value = value * window.innerWidth / 100;
   244              }
   245  
   246              // If the condition's unit is vh, convert current value in vh, in px
   247              else if (EQCSS.data[i].conditions[k].unit === 'vh') {
   248                recomputed = true;
   249  
   250                var value = parseInt(EQCSS.data[i].conditions[k].value);
   251                EQCSS.data[i].conditions[k].recomputed_value = value * window.innerHeight / 100;
   252              }
   253  
   254              // If the condition's unit is vmin, convert current value in vmin, in px
   255              else if (EQCSS.data[i].conditions[k].unit === 'vmin') {
   256                recomputed = true;
   257  
   258                var value = parseInt(EQCSS.data[i].conditions[k].value);
   259                EQCSS.data[i].conditions[k].recomputed_value = value * Math.min(window.innerWidth, window.innerHeight) / 100;
   260              }
   261  
   262              // If the condition's unit is vmax, convert current value in vmax, in px
   263              else if (EQCSS.data[i].conditions[k].unit === 'vmax') {
   264                recomputed = true;
   265  
   266                var value = parseInt(EQCSS.data[i].conditions[k].value);
   267                EQCSS.data[i].conditions[k].recomputed_value = value * Math.max(window.innerWidth, window.innerHeight) / 100;
   268              }
   269  
   270              // If the condition's unit is set and is not px or %, convert it into pixels
   271              else if (EQCSS.data[i].conditions[k].unit != null && EQCSS.data[i].conditions[k].unit != 'px' && EQCSS.data[i].conditions[k].unit != '%') {
   272                // Create a hidden DIV, sibling of the current element (or its child, if the element is <html>)
   273                // Set the given measure and unit to the DIV's width
   274                // Measure the DIV's width in px
   275                // Remove the DIV
   276                var div = document.createElement('div');
   277  
   278                div.style.visibility = 'hidden';
   279                div.style.border = '1px solid red';
   280                div.style.width = EQCSS.data[i].conditions[k].value + EQCSS.data[i].conditions[k].unit;
   281  
   282                var position = elements[j];
   283                if (elements[j] != document.documentElement) {
   284                  position = elements[j].parentNode;
   285                }
   286  
   287                position.appendChild(div);
   288                EQCSS.data[i].conditions[k].value = parseInt(window.getComputedStyle(div, null).getPropertyValue('width'));
   289                EQCSS.data[i].conditions[k].unit = 'px';
   290                position.removeChild(div);
   291              }
   292  
   293              // Store the good value in final_value depending if the size is recomputed or not
   294              var final_value = recomputed ? EQCSS.data[i].conditions[k].recomputed_value : parseInt(EQCSS.data[i].conditions[k].value);
   295  
   296              // Check each condition for this query and this element
   297              // If at least one condition is false, the element selector is not matched
   298              switch (EQCSS.data[i].conditions[k].measure) {
   299               case 'min-width':
   300                  // Min-width in px
   301                  if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') {
   302                    element_width = parseInt(computed_style.getPropertyValue('width'));
   303                    if (!(element_width >= final_value)) {
   304                      test = false;
   305                      break test_conditions;
   306                    }
   307                  }
   308  
   309                  // Min-width in %
   310                  if (EQCSS.data[i].conditions[k].unit === '%') {
   311                    element_width = parseInt(computed_style.getPropertyValue('width'));
   312                    parent_width = parseInt(parent_computed_style.getPropertyValue('width'));
   313                    if (!(parent_width / element_width <= 100 / final_value)) {
   314                      test = false;
   315                      break test_conditions;
   316                    }
   317                  }
   318  
   319                break;
   320  
   321                case 'max-width':
   322                  // Max-width in px
   323                  if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') {
   324                    element_width = parseInt(computed_style.getPropertyValue('width'));
   325                    if (!(element_width <= final_value)) {
   326                      test = false;
   327                      break test_conditions;
   328                    }
   329                  }
   330  
   331                  // Max-width in %
   332                  if (EQCSS.data[i].conditions[k].unit === '%') {
   333                    element_width = parseInt(computed_style.getPropertyValue('width'));
   334                    parent_width = parseInt(parent_computed_style.getPropertyValue('width'));
   335                    if (!(parent_width / element_width >= 100 / final_value)) {
   336                      test = false;
   337                      break test_conditions;
   338                    }
   339                  }
   340                break;
   341  
   342                case 'min-height':
   343                  // Min-height in px
   344                  if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') {
   345                    element_height = parseInt(computed_style.getPropertyValue('height'));
   346                    if (!(element_height >= final_value)) {
   347                      test = false;
   348                      break test_conditions;
   349                    }
   350                  }
   351  
   352                  // Min-height in %
   353                  if (EQCSS.data[i].conditions[k].unit === '%') {
   354                    element_height = parseInt(computed_style.getPropertyValue('height'));
   355                    parent_height = parseInt(parent_computed_style.getPropertyValue('height'));
   356                    if (!(parent_height / element_height <= 100 / final_value)) {
   357                      test = false;
   358                      break test_conditions;
   359                    }
   360                  }
   361                break;
   362  
   363                case 'max-height':
   364                  // Max-height in px
   365                  if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') {
   366                    element_height = parseInt(computed_style.getPropertyValue('height'));
   367                    if (!(element_height <= final_value)) {
   368                      test = false;
   369                      break test_conditions;
   370                    }
   371                  }
   372  
   373                  // Max-height in %
   374                  if (EQCSS.data[i].conditions[k].unit === '%') {
   375                    element_height = parseInt(computed_style.getPropertyValue('height'));
   376                    parent_height = parseInt(parent_computed_style.getPropertyValue('height'));
   377                    if (!(parent_height / element_height >= 100 / final_value)) {
   378                      test = false;
   379                      break test_conditions;
   380                    }
   381                  }
   382                break;
   383                
   384                // Min-characters
   385                case 'min-characters':
   386                  // form inputs
   387                  if (elements[j].value) {
   388                    if (!(elements[j].value.length >= final_value)) {
   389                      test = false;
   390                      break test_conditions;
   391                    }
   392                  }
   393                  // blocks
   394                  else {
   395                    if (!(elements[j].textContent.length >= final_value)) {
   396                      test = false;
   397                      break test_conditions;
   398                    }
   399                  }
   400                break;
   401  
   402                // Max-characters
   403                case 'max-characters':
   404                  // form inputs
   405                  if (elements[j].value) {
   406                    if (!(elements[j].value.length <= final_value)) {
   407                      test = false;
   408                      break test_conditions;
   409                    }
   410                  }
   411                  // blocks
   412                  else {
   413                    if (!(elements[j].textContent.length <= final_value)) {
   414                      test = false;
   415                      break test_conditions;
   416                    }
   417                  }
   418                break;
   419  
   420                // Min-children
   421                case 'min-children':
   422                  if (!(elements[j].children.length >= final_value)) {
   423                    test = false;
   424                    break test_conditions;
   425                  }
   426                break;
   427  
   428                // Max-children
   429                case 'max-children':
   430                  if (!(elements[j].children.length <= final_value)) {
   431                    test = false;
   432                    break test_conditions;
   433                  }
   434                break;
   435  
   436                // Min-lines
   437                case 'min-lines':
   438                  element_height =
   439                    parseInt(computed_style.getPropertyValue('height'))
   440                    - parseInt(computed_style.getPropertyValue('border-top-width'))
   441                    - parseInt(computed_style.getPropertyValue('border-bottom-width'))
   442                    - parseInt(computed_style.getPropertyValue('padding-top'))
   443                    - parseInt(computed_style.getPropertyValue('padding-bottom'));
   444  
   445                  element_line_height = computed_style.getPropertyValue('line-height');
   446                  if (element_line_height === 'normal') {
   447                    var element_font_size = parseInt(computed_style.getPropertyValue('font-size'));
   448                    element_line_height = element_font_size * 1.125;
   449                  } else {
   450                    element_line_height = parseInt(element_line_height);
   451                  }
   452  
   453                  if (!(element_height / element_line_height >= final_value)) {
   454                    test = false;
   455                    break test_conditions;
   456                  }
   457                break;
   458  
   459                // Max-lines
   460                case 'max-lines':
   461                  element_height =
   462                    parseInt(computed_style.getPropertyValue('height'))
   463                    - parseInt(computed_style.getPropertyValue('border-top-width'))
   464                    - parseInt(computed_style.getPropertyValue('border-bottom-width'))
   465                    - parseInt(computed_style.getPropertyValue('padding-top'))
   466                    - parseInt(computed_style.getPropertyValue('padding-bottom'));
   467  
   468                  element_line_height = computed_style.getPropertyValue('line-height');
   469                  if (element_line_height === 'normal') {
   470                    var element_font_size = parseInt(computed_style.getPropertyValue('font-size'));
   471                    element_line_height = element_font_size * 1.125;
   472                  } else {
   473                    element_line_height = parseInt(element_line_height);
   474                  }
   475  
   476                  if (!(element_height / element_line_height + 1 <= final_value)) {
   477                    test = false;
   478                    break test_conditions;
   479                  }
   480                break;
   481              }
   482            }
   483  
   484            // Update CSS block:
   485            // If all conditions are met: copy the CSS code from the query to the corresponding CSS block
   486            if (test === true) {
   487              // Get the CSS code to apply to the element
   488              css_code = EQCSS.data[i].style;
   489  
   490              // Replace eval('xyz') with the result of try{with(element){eval(xyz)}} in JS
   491              css_code = css_code.replace(
   492                /eval\( *((".*?")|('.*?')) *\)/g,
   493                function(string, match) {
   494                  return EQCSS.tryWithEval(elements[j], match);
   495                }
   496              );
   497  
   498              // Replace '$this' or 'eq_this' with '[element_guid]'
   499              css_code = css_code.replace(/(\$|eq_)this/gi, '[' + element_guid + ']');
   500  
   501              // Replace '$parent' or 'eq_parent' with '[element_guid_parent]'
   502              css_code = css_code.replace(/(\$|eq_)parent/gi, '[' + element_guid_parent + ']');
   503              
   504              if(css_block.innerHTML != css_code){
   505                css_block.innerHTML = css_code;
   506              }
   507            }
   508  
   509            // If condition is not met: empty the CSS block
   510            else if(css_block.innerHTML != '') {
   511              css_block.innerHTML = '';
   512            }
   513          }
   514        }
   515      }
   516  
   517      /*
   518       * Eval('') and $it
   519       * (…yes with() was necessary, and eval() too!)
   520       */
   521      EQCSS.tryWithEval = function(element, string) {
   522        var $it = element;
   523        var ret = '';
   524        
   525        try {
   526          with ($it) { ret = eval(string.slice(1, -1)) }
   527        }
   528        catch(e) {
   529          ret = '';
   530        }
   531        return ret;
   532      }
   533  
   534      /*
   535       * EQCSS.reset
   536       * Deletes parsed queries removes EQCSS-generated tags and attributes
   537       * To reload EQCSS again after running EQCSS.reset() use EQCSS.load()
   538       */
   539      EQCSS.reset = function() {
   540        // Reset EQCSS.data, removing previously parsed queries
   541        EQCSS.data = [];
   542        
   543        // Remove EQCSS-generated style tags from head
   544        var style_tag = document.querySelectorAll('head style[id^="data-eqcss-"]');
   545        for (var i = 0; i < style_tag.length; i++) {
   546          style_tag[i].parentNode.removeChild(style_tag[i]);
   547        }
   548  
   549        // Remove EQCSS-generated attributes from all tags
   550        var tag = document.querySelectorAll('*');
   551  
   552        // For each tag in the document
   553        for (var j = 0; j < tag.length; j++) {
   554          // Loop through all attributes
   555          for (var k = 0; k < tag[j].attributes.length; k++) {
   556            // If an attribute begins with 'data-eqcss-'
   557            if (tag[j].attributes[k].name.indexOf('data-eqcss-') === 0) {
   558              // Remove the attribute from the tag
   559              tag[j].removeAttribute(tag[j].attributes[k].name)
   560            }
   561          }
   562        }
   563      }
   564  
   565      /*
   566       * 'DOM Ready' cross-browser polyfill / Diego Perini / MIT license
   567       * Forked from: https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js
   568       */
   569      EQCSS.domReady = function(fn) {
   570        var done = false;
   571        var top = true;
   572        var doc = window.document;
   573        var root = doc.documentElement;
   574        var modern = !~navigator.userAgent.indexOf('MSIE 8');
   575        var add = modern ? 'addEventListener' : 'attachEvent';
   576        var rem = modern ? 'removeEventListener' : 'detachEvent';
   577        var pre = modern ? '' : 'on';
   578        var init = function(e) {
   579          if (e.type === 'readystatechange' && doc.readyState !== 'complete') return;
   580          (e.type === 'load' ? window : doc)[rem](pre + e.type, init, false);
   581          if (!done && (done = true)) fn.call(window, e.type || e);
   582        },
   583        poll = function() {
   584          try {
   585            root.doScroll('left');
   586          }
   587          catch(e) {
   588            setTimeout(poll, 50);
   589            return;
   590          }
   591          init('poll');
   592        };
   593  
   594        if (doc.readyState === 'complete') {
   595          fn.call(window, 'lazy');
   596          return;
   597        }
   598        
   599        if (!modern && root.doScroll) {
   600            try {
   601              top = !window.frameElement;
   602            }
   603            catch(e) {}
   604            if (top) poll();
   605        }
   606        doc[add](pre + 'DOMContentLoaded', init, false);
   607        doc[add](pre + 'readystatechange', init, false);
   608        window[add](pre + 'load', init, false);
   609      }
   610  
   611      /*
   612       * EQCSS.throttle
   613       * Ensures EQCSS.apply() is not called more than once every (EQCSS_timeout)ms
   614       */
   615      var EQCSS_throttle_available = true;
   616      var EQCSS_throttle_queued = false;
   617      var EQCSS_mouse_down = false;
   618      var EQCSS_timeout = 200;
   619  
   620      EQCSS.throttle = function() {
   621       /* if (EQCSS_throttle_available) {*/
   622          EQCSS.apply();
   623          /*EQCSS_throttle_available = false;
   624  
   625          setTimeout(function() {
   626            EQCSS_throttle_available = true;
   627            if (EQCSS_throttle_queued) {
   628              EQCSS_throttle_queued = false;
   629              EQCSS.apply();
   630            }
   631          }, EQCSS_timeout);
   632        } else {
   633          EQCSS_throttle_queued = true;
   634        }*/
   635      }
   636  
   637      // Call load (and apply, indirectly) on page load
   638      EQCSS.domReady(function() {
   639        EQCSS.load();
   640        EQCSS.throttle();
   641      });
   642  
   643      // On resize, click, call EQCSS.throttle.
   644      window.addEventListener('resize', EQCSS.throttle);
   645      window.addEventListener('click', EQCSS.throttle);
   646  
   647      // Debug: here's a shortcut for console.log
   648      function l(a) { console.log(a) }
   649  
   650      return EQCSS;
   651  }));