gosora/public/EQCSS.js

651 lines
24 KiB
JavaScript
Raw Permalink Normal View History

/*
# 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<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 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 element_guid_prev; // GUID for current element's previous sibling element
var element_guid_next; // GUID for current element's next sibling element
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 element_line_height; // Computed line-height
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 (var 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 (var 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 (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 <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) {
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;
}));