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 }));