123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /**
- * Copyright Marc J. Schmidt. See the LICENSE file at the top-level
- * directory of this distribution and at
- * https://github.com/marcj/css-element-queries/blob/master/LICENSE.
- */
- ;
- (function (root, factory) {
- if (typeof define === "function" && define.amd) {
- define(['./ResizeSensor.js'], factory);
- } else if (typeof exports === "object") {
- module.exports = factory(require('./ResizeSensor.js'));
- } else {
- root.ElementQueries = factory(root.ResizeSensor);
- }
- }(this, function (ResizeSensor) {
- /**
- *
- * @type {Function}
- * @constructor
- */
- var ElementQueries = function() {
- var trackingActive = false;
- var elements = [];
- /**
- *
- * @param element
- * @returns {Number}
- */
- function getEmSize(element) {
- if (!element) {
- element = document.documentElement;
- }
- var fontSize = window.getComputedStyle(element, null).fontSize;
- return parseFloat(fontSize) || 16;
- }
- /**
- *
- * @copyright https://github.com/Mr0grog/element-query/blob/master/LICENSE
- *
- * @param {HTMLElement} element
- * @param {*} value
- * @returns {*}
- */
- function convertToPx(element, value) {
- var numbers = value.split(/\d/);
- var units = numbers[numbers.length-1];
- value = parseFloat(value);
- switch (units) {
- case "px":
- return value;
- case "em":
- return value * getEmSize(element);
- case "rem":
- return value * getEmSize();
- // Viewport units!
- // According to http://quirksmode.org/mobile/tableViewport.html
- // documentElement.clientWidth/Height gets us the most reliable info
- case "vw":
- return value * document.documentElement.clientWidth / 100;
- case "vh":
- return value * document.documentElement.clientHeight / 100;
- case "vmin":
- case "vmax":
- var vw = document.documentElement.clientWidth / 100;
- var vh = document.documentElement.clientHeight / 100;
- var chooser = Math[units === "vmin" ? "min" : "max"];
- return value * chooser(vw, vh);
- default:
- return value;
- // for now, not supporting physical units (since they are just a set number of px)
- // or ex/ch (getting accurate measurements is hard)
- }
- }
- /**
- *
- * @param {HTMLElement} element
- * @constructor
- */
- function SetupInformation(element) {
- this.element = element;
- this.options = {};
- var key, option, width = 0, height = 0, value, actualValue, attrValues, attrValue, attrName;
- /**
- * @param {Object} option {mode: 'min|max', property: 'width|height', value: '123px'}
- */
- this.addOption = function(option) {
- var idx = [option.mode, option.property, option.value].join(',');
- this.options[idx] = option;
- };
- var attributes = ['min-width', 'min-height', 'max-width', 'max-height'];
- /**
- * Extracts the computed width/height and sets to min/max- attribute.
- */
- this.call = function() {
- // extract current dimensions
- width = this.element.offsetWidth;
- height = this.element.offsetHeight;
- attrValues = {};
- for (key in this.options) {
- if (!this.options.hasOwnProperty(key)){
- continue;
- }
- option = this.options[key];
- value = convertToPx(this.element, option.value);
- actualValue = option.property == 'width' ? width : height;
- attrName = option.mode + '-' + option.property;
- attrValue = '';
- if (option.mode == 'min' && actualValue >= value) {
- attrValue += option.value;
- }
- if (option.mode == 'max' && actualValue <= value) {
- attrValue += option.value;
- }
- if (!attrValues[attrName]) attrValues[attrName] = '';
- if (attrValue && -1 === (' '+attrValues[attrName]+' ').indexOf(' ' + attrValue + ' ')) {
- attrValues[attrName] += ' ' + attrValue;
- }
- }
- for (var k in attributes) {
- if(!attributes.hasOwnProperty(k)) continue;
- if (attrValues[attributes[k]]) {
- this.element.setAttribute(attributes[k], attrValues[attributes[k]].substr(1));
- } else {
- this.element.removeAttribute(attributes[k]);
- }
- }
- };
- }
- /**
- * @param {HTMLElement} element
- * @param {Object} options
- */
- function setupElement(element, options) {
- if (element.elementQueriesSetupInformation) {
- element.elementQueriesSetupInformation.addOption(options);
- } else {
- element.elementQueriesSetupInformation = new SetupInformation(element);
- element.elementQueriesSetupInformation.addOption(options);
- element.elementQueriesSensor = new ResizeSensor(element, function() {
- element.elementQueriesSetupInformation.call();
- });
- }
- element.elementQueriesSetupInformation.call();
- if (trackingActive && elements.indexOf(element) < 0) {
- elements.push(element);
- }
- }
- /**
- * @param {String} selector
- * @param {String} mode min|max
- * @param {String} property width|height
- * @param {String} value
- */
- var allQueries = {};
- function queueQuery(selector, mode, property, value) {
- if (typeof(allQueries[mode]) == 'undefined') allQueries[mode] = {};
- if (typeof(allQueries[mode][property]) == 'undefined') allQueries[mode][property] = {};
- if (typeof(allQueries[mode][property][value]) == 'undefined') allQueries[mode][property][value] = selector;
- else allQueries[mode][property][value] += ','+selector;
- }
- function getQuery() {
- var query;
- if (document.querySelectorAll) query = document.querySelectorAll.bind(document);
- if (!query && 'undefined' !== typeof $$) query = $$;
- if (!query && 'undefined' !== typeof jQuery) query = jQuery;
- if (!query) {
- throw 'No document.querySelectorAll, jQuery or Mootools\'s $$ found.';
- }
- return query;
- }
- /**
- * Start the magic. Go through all collected rules (readRules()) and attach the resize-listener.
- */
- function findElementQueriesElements() {
- var query = getQuery();
- for (var mode in allQueries) if (allQueries.hasOwnProperty(mode)) {
- for (var property in allQueries[mode]) if (allQueries[mode].hasOwnProperty(property)) {
- for (var value in allQueries[mode][property]) if (allQueries[mode][property].hasOwnProperty(value)) {
- var elements = query(allQueries[mode][property][value]);
- for (var i = 0, j = elements.length; i < j; i++) {
- setupElement(elements[i], {
- mode: mode,
- property: property,
- value: value
- });
- }
- }
- }
- }
- }
- /**
- *
- * @param {HTMLElement} element
- */
- function attachResponsiveImage(element) {
- var children = [];
- var rules = [];
- var sources = [];
- var defaultImageId = 0;
- var lastActiveImage = -1;
- var loadedImages = [];
- for (var i in element.children) {
- if(!element.children.hasOwnProperty(i)) continue;
- if (element.children[i].tagName && element.children[i].tagName.toLowerCase() === 'img') {
- children.push(element.children[i]);
- var minWidth = element.children[i].getAttribute('min-width') || element.children[i].getAttribute('data-min-width');
- //var minHeight = element.children[i].getAttribute('min-height') || element.children[i].getAttribute('data-min-height');
- var src = element.children[i].getAttribute('data-src') || element.children[i].getAttribute('url');
- sources.push(src);
- var rule = {
- minWidth: minWidth
- };
- rules.push(rule);
- if (!minWidth) {
- defaultImageId = children.length - 1;
- element.children[i].style.display = 'block';
- } else {
- element.children[i].style.display = 'none';
- }
- }
- }
- lastActiveImage = defaultImageId;
- function check() {
- var imageToDisplay = false, i;
- for (i in children){
- if(!children.hasOwnProperty(i)) continue;
- if (rules[i].minWidth) {
- if (element.offsetWidth > rules[i].minWidth) {
- imageToDisplay = i;
- }
- }
- }
- if (!imageToDisplay) {
- //no rule matched, show default
- imageToDisplay = defaultImageId;
- }
- if (lastActiveImage != imageToDisplay) {
- //image change
- if (!loadedImages[imageToDisplay]){
- //image has not been loaded yet, we need to load the image first in memory to prevent flash of
- //no content
- var image = new Image();
- image.onload = function() {
- children[imageToDisplay].src = sources[imageToDisplay];
- children[lastActiveImage].style.display = 'none';
- children[imageToDisplay].style.display = 'block';
- loadedImages[imageToDisplay] = true;
- lastActiveImage = imageToDisplay;
- };
- image.src = sources[imageToDisplay];
- } else {
- children[lastActiveImage].style.display = 'none';
- children[imageToDisplay].style.display = 'block';
- lastActiveImage = imageToDisplay;
- }
- } else {
- //make sure for initial check call the .src is set correctly
- children[imageToDisplay].src = sources[imageToDisplay];
- }
- }
- element.resizeSensor = new ResizeSensor(element, check);
- check();
- if (trackingActive) {
- elements.push(element);
- }
- }
- function findResponsiveImages(){
- var query = getQuery();
- var elements = query('[data-responsive-image],[responsive-image]');
- for (var i = 0, j = elements.length; i < j; i++) {
- attachResponsiveImage(elements[i]);
- }
- }
- var regex = /,?[\s\t]*([^,\n]*?)((?:\[[\s\t]*?(?:min|max)-(?:width|height)[\s\t]*?[~$\^]?=[\s\t]*?"[^"]*?"[\s\t]*?])+)([^,\n\s\{]*)/mgi;
- var attrRegex = /\[[\s\t]*?(min|max)-(width|height)[\s\t]*?[~$\^]?=[\s\t]*?"([^"]*?)"[\s\t]*?]/mgi;
- /**
- * @param {String} css
- */
- function extractQuery(css) {
- var match;
- var smatch;
- css = css.replace(/'/g, '"');
- while (null !== (match = regex.exec(css))) {
- smatch = match[1] + match[3];
- attrs = match[2];
- while (null !== (attrMatch = attrRegex.exec(attrs))) {
- queueQuery(smatch, attrMatch[1], attrMatch[2], attrMatch[3]);
- }
- }
- }
- /**
- * @param {CssRule[]|String} rules
- */
- function readRules(rules) {
- var selector = '';
- if (!rules) {
- return;
- }
- if ('string' === typeof rules) {
- rules = rules.toLowerCase();
- if (-1 !== rules.indexOf('min-width') || -1 !== rules.indexOf('max-width')) {
- extractQuery(rules);
- }
- } else {
- for (var i = 0, j = rules.length; i < j; i++) {
- if (1 === rules[i].type) {
- selector = rules[i].selectorText || rules[i].cssText;
- if (-1 !== selector.indexOf('min-height') || -1 !== selector.indexOf('max-height')) {
- extractQuery(selector);
- }else if(-1 !== selector.indexOf('min-width') || -1 !== selector.indexOf('max-width')) {
- extractQuery(selector);
- }
- } else if (4 === rules[i].type) {
- readRules(rules[i].cssRules || rules[i].rules);
- }
- }
- }
- }
- var defaultCssInjected = false;
- /**
- * Searches all css rules and setups the event listener to all elements with element query rules..
- *
- * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
- * (no garbage collection possible if you don not call .detach() first)
- */
- this.init = function(withTracking) {
- trackingActive = typeof withTracking === 'undefined' ? false : withTracking;
- for (var i = 0, j = document.styleSheets.length; i < j; i++) {
- try {
- readRules(document.styleSheets[i].cssRules || document.styleSheets[i].rules || document.styleSheets[i].cssText);
- } catch(e) {
- if (e.name !== 'SecurityError') {
- throw e;
- }
- }
- }
- if (!defaultCssInjected) {
- var style = document.createElement('style');
- style.type = 'text/css';
- style.innerHTML = '[responsive-image] > img, [data-responsive-image] {overflow: hidden; padding: 0; } [responsive-image] > img, [data-responsive-image] > img { width: 100%;}';
- document.getElementsByTagName('head')[0].appendChild(style);
- defaultCssInjected = true;
- }
- findElementQueriesElements();
- findResponsiveImages();
- };
- /**
- *
- * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
- * (no garbage collection possible if you don not call .detach() first)
- */
- this.update = function(withTracking) {
- this.init(withTracking);
- };
- this.detach = function() {
- if (!this.withTracking) {
- throw 'withTracking is not enabled. We can not detach elements since we don not store it.' +
- 'Use ElementQueries.withTracking = true; before domready or call ElementQueryes.update(true).';
- }
- var element;
- while (element = elements.pop()) {
- ElementQueries.detach(element);
- }
- elements = [];
- };
- };
- /**
- *
- * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
- * (no garbage collection possible if you don not call .detach() first)
- */
- ElementQueries.update = function(withTracking) {
- ElementQueries.instance.update(withTracking);
- };
- /**
- * Removes all sensor and elementquery information from the element.
- *
- * @param {HTMLElement} element
- */
- ElementQueries.detach = function(element) {
- if (element.elementQueriesSetupInformation) {
- //element queries
- element.elementQueriesSensor.detach();
- delete element.elementQueriesSetupInformation;
- delete element.elementQueriesSensor;
- } else if (element.resizeSensor) {
- //responsive image
- element.resizeSensor.detach();
- delete element.resizeSensor;
- } else {
- //console.log('detached already', element);
- }
- };
- ElementQueries.withTracking = false;
- ElementQueries.init = function() {
- if (!ElementQueries.instance) {
- ElementQueries.instance = new ElementQueries();
- }
- ElementQueries.instance.init(ElementQueries.withTracking);
- };
- var domLoaded = function (callback) {
- /* Internet Explorer */
- /*@cc_on
- @if (@_win32 || @_win64)
- document.write('<script id="ieScriptLoad" defer src="//:"><\/script>');
- document.getElementById('ieScriptLoad').onreadystatechange = function() {
- if (this.readyState == 'complete') {
- callback();
- }
- };
- @end @*/
- /* Mozilla, Chrome, Opera */
- if (document.addEventListener) {
- document.addEventListener('DOMContentLoaded', callback, false);
- }
- /* Safari, iCab, Konqueror */
- else if (/KHTML|WebKit|iCab/i.test(navigator.userAgent)) {
- var DOMLoadTimer = setInterval(function () {
- if (/loaded|complete/i.test(document.readyState)) {
- callback();
- clearInterval(DOMLoadTimer);
- }
- }, 10);
- }
- /* Other web browsers */
- else window.onload = callback;
- };
- ElementQueries.listen = function() {
- domLoaded(ElementQueries.init);
- };
- // make available to common module loader
- if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
- module.exports = ElementQueries;
- }
- else {
- window.ElementQueries = ElementQueries;
- ElementQueries.listen();
- }
- return ElementQueries;
- }));
|