/* # EQCSS ## version 1.7.0 A JavaScript plugin to read EQCSS syntax to provide: scoped styles, element queries, container queries, meta-selectors, eval(), and element-based units. - github.com/eqcss/eqcss - elementqueries.com Authors: Tommy Hodgins, Maxime Euzière, Azareal License: MIT */ // Uses Node, AMD or browser globals to create a module (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD: Register as an anonymous module define([], factory); } else if (typeof module === 'object' && module.exports) { // Node: Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node module.exports = factory(); } else { // Browser globals (root is window) root.EQCSS = factory(); } }(this, function() { var EQCSS = { data: [] } /* * EQCSS.load() * Called automatically on page load. * Call it manually after adding EQCSS code in the page. * Loads and parses all the EQCSS code. */ EQCSS.load = function() { // Retrieve all style blocks var styles = document.getElementsByTagName('style'); for (var i = 0; i < styles.length; i++) { // Test if the style is not read yet if (styles[i].getAttribute('data-eqcss-read') === null) { // Mark the style block as read styles[i].setAttribute('data-eqcss-read', 'true'); EQCSS.process(styles[i].innerHTML); } } // Retrieve all link tags var link = document.getElementsByTagName('link'); for (i = 0; i < link.length; i++) { // Test if the link is not read yet, and has rel=stylesheet if (link[i].getAttribute('data-eqcss-read') === null && link[i].rel === 'stylesheet') { // retrieve the file content with AJAX and process it if (link[i].href) { (function() { var xhr = new XMLHttpRequest; xhr.open('GET', link[i].href, true); xhr.send(null); xhr.onreadystatechange = function() { EQCSS.process(xhr.responseText); } })(); } // Mark the link as read link[i].setAttribute('data-eqcss-read', 'true'); } } } /* * EQCSS.parse() * Called by load for each script / style / link resource. * Generates data for each Element Query found */ EQCSS.parse = function(code) { var parsed_queries = new Array(); // Cleanup code = code.replace(/\s+/g, ' '); // reduce spaces and line breaks code = code.replace(/\/\*[\w\W]*?\*\//g, ''); // remove comments code = code.replace(/@element/g, '\n@element'); // one element query per line code = code.replace(/(@element.*?\{([^}]*?\{[^}]*?\}[^}]*?)*\}).*/g, '$1'); // Keep the queries only (discard regular css written around them) // Parse // For each query code.replace(/(@element.*(?!@element))/g, function(string, query) { // Create a data entry var dataEntry = {}; // Extract the selector query.replace(/(@element)\s*(".*?"|'.*?'|.*?)\s*(and\s*\(|{)/g, function(string, atrule, selector, extra) { // Strip outer quotes if present selector = selector.replace(/^\s?['](.*)[']/, '$1'); selector = selector.replace(/^\s?["](.*)["]/, '$1'); dataEntry.selector = selector; }) // Extract the conditions (measure, value, unit) dataEntry.conditions = []; query.replace(/and ?\( ?([^:]*) ?: ?([^)]*) ?\)/g, function(string, measure, value) { // Separate value and unit if it's possible var unit = null; unit = value.replace(/^(\d*\.?\d+)(\D+)$/, '$2'); if (unit === value) { unit = null; } value = value.replace(/^(\d*\.?\d+)\D+$/, '$1'); dataEntry.conditions.push({measure: measure, value: value, unit: unit}); }); // Extract the styles query.replace(/{(.*)}/g, function(string, style) { dataEntry.style = style; }); parsed_queries.push(dataEntry); }); return parsed_queries; } /* * EQCSS.register() * Add a single object, or an array of objects to EQCSS.data * */ EQCSS.register = function(queries) { if (Object.prototype.toString.call(queries) === '[object Object]') { EQCSS.data.push(queries); EQCSS.apply(); } if (Object.prototype.toString.call(queries) === '[object Array]') { for (var i=0; i if it doesn't exist) css_block = document.querySelector('#' + element_guid); if (!css_block) { css_block = document.createElement('style'); css_block.id = element_guid; css_block.setAttribute('data-eqcss-read', 'true'); document.querySelector('head').appendChild(css_block); } css_block = document.querySelector('#' + element_guid); // Reset the query test's result (first, we assume that the selector is matched) test = true; // Loop on the conditions test_conditions: for (var k = 0; k < EQCSS.data[i].conditions.length; k++) { // Reuse element and parent's computed style instead of computing it everywhere computed_style = window.getComputedStyle(elements[j], null); parent_computed_style = null; if (elements[j] != document.documentElement) { parent_computed_style = window.getComputedStyle(elements[j].parentNode, null); } // Do we have to reconvert the size in px at each call? // This is true only for vw/vh/vmin/vmax var recomputed = false; // If the condition's unit is vw, convert current value in vw, in px if (EQCSS.data[i].conditions[k].unit === 'vw') { recomputed = true; var value = parseInt(EQCSS.data[i].conditions[k].value); EQCSS.data[i].conditions[k].recomputed_value = value * window.innerWidth / 100; } // If the condition's unit is vh, convert current value in vh, in px else if (EQCSS.data[i].conditions[k].unit === 'vh') { recomputed = true; var value = parseInt(EQCSS.data[i].conditions[k].value); EQCSS.data[i].conditions[k].recomputed_value = value * window.innerHeight / 100; } // If the condition's unit is vmin, convert current value in vmin, in px else if (EQCSS.data[i].conditions[k].unit === 'vmin') { recomputed = true; var value = parseInt(EQCSS.data[i].conditions[k].value); EQCSS.data[i].conditions[k].recomputed_value = value * Math.min(window.innerWidth, window.innerHeight) / 100; } // If the condition's unit is vmax, convert current value in vmax, in px else if (EQCSS.data[i].conditions[k].unit === 'vmax') { recomputed = true; var value = parseInt(EQCSS.data[i].conditions[k].value); EQCSS.data[i].conditions[k].recomputed_value = value * Math.max(window.innerWidth, window.innerHeight) / 100; } // If the condition's unit is set and is not px or %, convert it into pixels else if (EQCSS.data[i].conditions[k].unit != null && EQCSS.data[i].conditions[k].unit != 'px' && EQCSS.data[i].conditions[k].unit != '%') { // Create a hidden DIV, sibling of the current element (or its child, if the element is ) // Set the given measure and unit to the DIV's width // Measure the DIV's width in px // Remove the DIV var div = document.createElement('div'); div.style.visibility = 'hidden'; div.style.border = '1px solid red'; div.style.width = EQCSS.data[i].conditions[k].value + EQCSS.data[i].conditions[k].unit; var position = elements[j]; if (elements[j] != document.documentElement) { position = elements[j].parentNode; } position.appendChild(div); EQCSS.data[i].conditions[k].value = parseInt(window.getComputedStyle(div, null).getPropertyValue('width')); EQCSS.data[i].conditions[k].unit = 'px'; position.removeChild(div); } // Store the good value in final_value depending if the size is recomputed or not var final_value = recomputed ? EQCSS.data[i].conditions[k].recomputed_value : parseInt(EQCSS.data[i].conditions[k].value); // Check each condition for this query and this element // If at least one condition is false, the element selector is not matched switch (EQCSS.data[i].conditions[k].measure) { case 'min-width': // Min-width in px if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { element_width = parseInt(computed_style.getPropertyValue('width')); if (!(element_width >= final_value)) { test = false; break test_conditions; } } // Min-width in % if (EQCSS.data[i].conditions[k].unit === '%') { element_width = parseInt(computed_style.getPropertyValue('width')); parent_width = parseInt(parent_computed_style.getPropertyValue('width')); if (!(parent_width / element_width <= 100 / final_value)) { test = false; break test_conditions; } } break; case 'max-width': // Max-width in px if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { element_width = parseInt(computed_style.getPropertyValue('width')); if (!(element_width <= final_value)) { test = false; break test_conditions; } } // Max-width in % if (EQCSS.data[i].conditions[k].unit === '%') { element_width = parseInt(computed_style.getPropertyValue('width')); parent_width = parseInt(parent_computed_style.getPropertyValue('width')); if (!(parent_width / element_width >= 100 / final_value)) { test = false; break test_conditions; } } break; case 'min-height': // Min-height in px if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { element_height = parseInt(computed_style.getPropertyValue('height')); if (!(element_height >= final_value)) { test = false; break test_conditions; } } // Min-height in % if (EQCSS.data[i].conditions[k].unit === '%') { element_height = parseInt(computed_style.getPropertyValue('height')); parent_height = parseInt(parent_computed_style.getPropertyValue('height')); if (!(parent_height / element_height <= 100 / final_value)) { test = false; break test_conditions; } } break; case 'max-height': // Max-height in px if (recomputed === true || EQCSS.data[i].conditions[k].unit === 'px') { element_height = parseInt(computed_style.getPropertyValue('height')); if (!(element_height <= final_value)) { test = false; break test_conditions; } } // Max-height in % if (EQCSS.data[i].conditions[k].unit === '%') { element_height = parseInt(computed_style.getPropertyValue('height')); parent_height = parseInt(parent_computed_style.getPropertyValue('height')); if (!(parent_height / element_height >= 100 / final_value)) { test = false; break test_conditions; } } break; // Min-characters case 'min-characters': // form inputs if (elements[j].value) { if (!(elements[j].value.length >= final_value)) { test = false; break test_conditions; } } // blocks else { if (!(elements[j].textContent.length >= final_value)) { test = false; break test_conditions; } } break; // Max-characters case 'max-characters': // form inputs if (elements[j].value) { if (!(elements[j].value.length <= final_value)) { test = false; break test_conditions; } } // blocks else { if (!(elements[j].textContent.length <= final_value)) { test = false; break test_conditions; } } break; // Min-children case 'min-children': if (!(elements[j].children.length >= final_value)) { test = false; break test_conditions; } break; // Max-children case 'max-children': if (!(elements[j].children.length <= final_value)) { test = false; break test_conditions; } break; // Min-lines case 'min-lines': element_height = parseInt(computed_style.getPropertyValue('height')) - parseInt(computed_style.getPropertyValue('border-top-width')) - parseInt(computed_style.getPropertyValue('border-bottom-width')) - parseInt(computed_style.getPropertyValue('padding-top')) - parseInt(computed_style.getPropertyValue('padding-bottom')); element_line_height = computed_style.getPropertyValue('line-height'); if (element_line_height === 'normal') { var element_font_size = parseInt(computed_style.getPropertyValue('font-size')); element_line_height = element_font_size * 1.125; } else { element_line_height = parseInt(element_line_height); } if (!(element_height / element_line_height >= final_value)) { test = false; break test_conditions; } break; // Max-lines case 'max-lines': element_height = parseInt(computed_style.getPropertyValue('height')) - parseInt(computed_style.getPropertyValue('border-top-width')) - parseInt(computed_style.getPropertyValue('border-bottom-width')) - parseInt(computed_style.getPropertyValue('padding-top')) - parseInt(computed_style.getPropertyValue('padding-bottom')); element_line_height = computed_style.getPropertyValue('line-height'); if (element_line_height === 'normal') { var element_font_size = parseInt(computed_style.getPropertyValue('font-size')); element_line_height = element_font_size * 1.125; } else { element_line_height = parseInt(element_line_height); } if (!(element_height / element_line_height + 1 <= final_value)) { test = false; break test_conditions; } break; } } // Update CSS block: // If all conditions are met: copy the CSS code from the query to the corresponding CSS block if (test === true) { // Get the CSS code to apply to the element css_code = EQCSS.data[i].style; // Replace eval('xyz') with the result of try{with(element){eval(xyz)}} in JS css_code = css_code.replace( /eval\( *((".*?")|('.*?')) *\)/g, function(string, match) { return EQCSS.tryWithEval(elements[j], match); } ); // Replace '$this' or 'eq_this' with '[element_guid]' css_code = css_code.replace(/(\$|eq_)this/gi, '[' + element_guid + ']'); // Replace '$parent' or 'eq_parent' with '[element_guid_parent]' css_code = css_code.replace(/(\$|eq_)parent/gi, '[' + element_guid_parent + ']'); if(css_block.innerHTML != css_code){ css_block.innerHTML = css_code; } } // If condition is not met: empty the CSS block else if(css_block.innerHTML != '') { css_block.innerHTML = ''; } } } } /* * Eval('') and $it * (…yes with() was necessary, and eval() too!) */ EQCSS.tryWithEval = function(element, string) { var $it = element; var ret = ''; try { with ($it) { ret = eval(string.slice(1, -1)) } } catch(e) { ret = ''; } return ret; } /* * EQCSS.reset * Deletes parsed queries removes EQCSS-generated tags and attributes * To reload EQCSS again after running EQCSS.reset() use EQCSS.load() */ EQCSS.reset = function() { // Reset EQCSS.data, removing previously parsed queries EQCSS.data = []; // Remove EQCSS-generated style tags from head var style_tag = document.querySelectorAll('head style[id^="data-eqcss-"]'); for (var i = 0; i < style_tag.length; i++) { style_tag[i].parentNode.removeChild(style_tag[i]); } // Remove EQCSS-generated attributes from all tags var tag = document.querySelectorAll('*'); // For each tag in the document for (var j = 0; j < tag.length; j++) { // Loop through all attributes for (var k = 0; k < tag[j].attributes.length; k++) { // If an attribute begins with 'data-eqcss-' if (tag[j].attributes[k].name.indexOf('data-eqcss-') === 0) { // Remove the attribute from the tag tag[j].removeAttribute(tag[j].attributes[k].name) } } } } /* * 'DOM Ready' cross-browser polyfill / Diego Perini / MIT license * Forked from: https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js */ EQCSS.domReady = function(fn) { var done = false; var top = true; var doc = window.document; var root = doc.documentElement; var modern = !~navigator.userAgent.indexOf('MSIE 8'); var add = modern ? 'addEventListener' : 'attachEvent'; var rem = modern ? 'removeEventListener' : 'detachEvent'; var pre = modern ? '' : 'on'; var init = function(e) { if (e.type === 'readystatechange' && doc.readyState !== 'complete') return; (e.type === 'load' ? window : doc)[rem](pre + e.type, init, false); if (!done && (done = true)) fn.call(window, e.type || e); }, poll = function() { try { root.doScroll('left'); } catch(e) { setTimeout(poll, 50); return; } init('poll'); }; if (doc.readyState === 'complete') { fn.call(window, 'lazy'); return; } if (!modern && root.doScroll) { try { top = !window.frameElement; } catch(e) {} if (top) poll(); } doc[add](pre + 'DOMContentLoaded', init, false); doc[add](pre + 'readystatechange', init, false); window[add](pre + 'load', init, false); } /* * EQCSS.throttle * Ensures EQCSS.apply() is not called more than once every (EQCSS_timeout)ms */ var EQCSS_throttle_available = true; var EQCSS_throttle_queued = false; var EQCSS_mouse_down = false; var EQCSS_timeout = 200; EQCSS.throttle = function() { /* if (EQCSS_throttle_available) {*/ EQCSS.apply(); /*EQCSS_throttle_available = false; setTimeout(function() { EQCSS_throttle_available = true; if (EQCSS_throttle_queued) { EQCSS_throttle_queued = false; EQCSS.apply(); } }, EQCSS_timeout); } else { EQCSS_throttle_queued = true; }*/ } // Call load (and apply, indirectly) on page load EQCSS.domReady(function() { EQCSS.load(); EQCSS.throttle(); }); // On resize, click, call EQCSS.throttle. window.addEventListener('resize', EQCSS.throttle); window.addEventListener('click', EQCSS.throttle); // Debug: here's a shortcut for console.log function l(a) { console.log(a) } return EQCSS; }));