Skip to content

Instantly share code, notes, and snippets.

@tomhodgins
Created August 21, 2017 15:41
Show Gist options
  • Save tomhodgins/e3a8196acc41e52849a93916c9d06e6c to your computer and use it in GitHub Desktop.
Save tomhodgins/e3a8196acc41e52849a93916c9d06e6c to your computer and use it in GitHub Desktop.
A stripped build of EQCSS v1.7.0 removing all features but $this, $parent, min-width, and max-width. May improve performance for lightweight use-cases where other features aren't required.
/*
# EQCSS lite
## version 1.7.0-lite
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
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');
// Process
EQCSS.process(styles[i].innerHTML);
}
}
// Retrieve all script blocks
var script = document.getElementsByTagName('script');
for (var i = 0; i < script.length; i++) {
// Test if the script is not read yet and has type='text/eqcss'
if (script[i].getAttribute('data-eqcss-read') === null && script[i].type === 'text/eqcss') {
// Test if they contain external EQCSS code
if (script[i].src) {
// retrieve the file content with AJAX and process it
(function() {
var xhr = new XMLHttpRequest;
xhr.open('GET', script[i].src, true);
xhr.send(null);
xhr.onreadystatechange = function() {
EQCSS.process(xhr.responseText);
}
})();
}
// or embedded EQCSS code
else {
// Process
EQCSS.process(script[i].innerHTML);
}
// Mark the script block as read
script[i].setAttribute('data-eqcss-read', 'true');
}
}
// 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;
});
// Add it to data
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<queries.length; i++) {
EQCSS.data.push(queries[i]);
}
EQCSS.apply();
}
}
/*
* EQCSS.process()
* Parse and Register queries with `EQCSS.data`
*/
EQCSS.process = function(code) {
var queries = EQCSS.parse(code)
return EQCSS.register(queries)
}
/*
* EQCSS.apply()
* Called on load, on resize and manually on DOM update
* Enable the Element Queries in which the conditions are true
*/
EQCSS.apply = function() {
var i, j, k; // Iterators
var elements; // Elements targeted by each query
var element_guid; // GUID for current element
var css_block; // CSS block corresponding to each targeted element
var element_guid_parent; // GUID for current element's parent
var css_code; // CSS code to write in each CSS block (one per targeted element)
var element_width, parent_width; // Computed widths
var element_height, parent_height;// Computed heights
var test; // Query's condition test result
var computed_style; // Each targeted element's computed style
var parent_computed_style; // Each targeted element parent's computed style
// Loop on all element queries
for (i = 0; i < EQCSS.data.length; i++) {
// Find all the elements targeted by the query
elements = document.querySelectorAll(EQCSS.data[i].selector);
// Loop on all the elements
for (j = 0; j < elements.length; j++) {
// Create a guid for this element
// Pattern: 'EQCSS_{element-query-index}_{matched-element-index}'
element_guid = 'data-eqcss-' + i + '-' + j;
// Add this guid as an attribute to the element
elements[j].setAttribute(element_guid, '');
// Create a guid for the parent of this element
// Pattern: 'EQCSS_{element-query-index}_{matched-element-index}_parent'
element_guid_parent = 'data-eqcss-' + i + '-' + j + '-parent';
// Add this guid as an attribute to the element's parent (except if element is the root element)
if (elements[j] != document.documentElement) {
elements[j].parentNode.setAttribute(element_guid_parent, '');
}
// Get the CSS block associated to this element (or create one in the <HEAD> 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 (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 <html>)
// 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) {
// Min-width
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;
// Max-width
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;
}
}
// 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 '$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 + ']');
// Replace '$root' or 'eq_root' with html
css_code = css_code.replace(/(\$|eq_)root/gi, 'html');
// good browsers
try {
css_block.innerHTML = css_code;
}
// IE8
catch(e) {
css_block.styleSheet.cssText = css_code;
}
}
// If condition is not met: empty the CSS block
else {
// Good browsers
try {
css_block.innerHTML = '';
}
// IE8
catch(e) {
css_block.styleSheet.cssText = '';
}
}
}
}
}
/*
* 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');
else {
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
window.addEventListener('resize', EQCSS.throttle);
return EQCSS;
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment