/** * 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('