diff --git a/index.php b/index.php index d97b217592b667eff36f60633da34778479e8a30..5acaa201c74795489eee6bfb70b3bd3cd739c3a5 100644 --- a/index.php +++ b/index.php @@ -13,6 +13,8 @@ <script type="text/javascript" src="js/jquery-2.2.4.min.js"></script> <script type="text/javascript" src="materialize/js/materialize.min.js"></script> <script type="text/javascript" src="js/main.js"></script> + <script type="text/javascript" src="js/ResizeSensor.js"></script> + <script type="text/javascript" src="js/ElementQueries.js"></script> <!--Let browser know website is optimized for mobile--> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> @@ -37,7 +39,7 @@ <div class="train-circle blue"></div> </div> <div class="col s10 m7 l6"> - <div class="card padding white version-entry"> + <div class="card padding white version-entry" id="version-entry-1"> <div class="card-content"> <div class="blue lighten-2 center-align margin-bottom"> <span class="card-title bold version-entry-title truncate">v1.6.0 - Next Version</span> diff --git a/js/ElementQueries.js b/js/ElementQueries.js new file mode 100644 index 0000000000000000000000000000000000000000..b71d6c9d726fe48d604b5bfc79de687b29d87000 --- /dev/null +++ b/js/ElementQueries.js @@ -0,0 +1,515 @@ +/** + * 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; + +})); diff --git a/js/ResizeSensor.js b/js/ResizeSensor.js new file mode 100644 index 0000000000000000000000000000000000000000..af38c6ecff973fbb70697f95af601d73814b23c2 --- /dev/null +++ b/js/ResizeSensor.js @@ -0,0 +1,220 @@ +/** + * 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(factory); + } else if (typeof exports === "object") { + module.exports = factory(); + } else { + root.ResizeSensor = factory(); + } +}(this, function () { + + // Only used for the dirty checking, so the event callback count is limted to max 1 call per fps per sensor. + // In combination with the event based resize sensor this saves cpu time, because the sensor is too fast and + // would generate too many unnecessary events. + var requestAnimationFrame = window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + function (fn) { + return window.setTimeout(fn, 20); + }; + + /** + * Iterate over each of the provided element(s). + * + * @param {HTMLElement|HTMLElement[]} elements + * @param {Function} callback + */ + function forEachElement(elements, callback){ + var elementsType = Object.prototype.toString.call(elements); + var isCollectionTyped = ('[object Array]' === elementsType + || ('[object NodeList]' === elementsType) + || ('[object HTMLCollection]' === elementsType) + || ('undefined' !== typeof jQuery && elements instanceof jQuery) //jquery + || ('undefined' !== typeof Elements && elements instanceof Elements) //mootools + ); + var i = 0, j = elements.length; + if (isCollectionTyped) { + for (; i < j; i++) { + callback(elements[i]); + } + } else { + callback(elements); + } + } + + /** + * Class for dimension change detection. + * + * @param {Element|Element[]|Elements|jQuery} element + * @param {Function} callback + * + * @constructor + */ + var ResizeSensor = function(element, callback) { + /** + * + * @constructor + */ + function EventQueue() { + var q = []; + this.add = function(ev) { + q.push(ev); + }; + + var i, j; + this.call = function() { + for (i = 0, j = q.length; i < j; i++) { + q[i].call(); + } + }; + + this.remove = function(ev) { + var newQueue = []; + for(i = 0, j = q.length; i < j; i++) { + if(q[i] !== ev) newQueue.push(q[i]); + } + q = newQueue; + } + + this.length = function() { + return q.length; + } + } + + /** + * @param {HTMLElement} element + * @param {String} prop + * @returns {String|Number} + */ + function getComputedStyle(element, prop) { + if (element.currentStyle) { + return element.currentStyle[prop]; + } else if (window.getComputedStyle) { + return window.getComputedStyle(element, null).getPropertyValue(prop); + } else { + return element.style[prop]; + } + } + + /** + * + * @param {HTMLElement} element + * @param {Function} resized + */ + function attachResizeEvent(element, resized) { + if (!element.resizedAttached) { + element.resizedAttached = new EventQueue(); + element.resizedAttached.add(resized); + } else if (element.resizedAttached) { + element.resizedAttached.add(resized); + return; + } + + element.resizeSensor = document.createElement('div'); + element.resizeSensor.className = 'resize-sensor'; + var style = 'position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;'; + var styleChild = 'position: absolute; left: 0; top: 0; transition: 0s;'; + + element.resizeSensor.style.cssText = style; + element.resizeSensor.innerHTML = + '<div class="resize-sensor-expand" style="' + style + '">' + + '<div style="' + styleChild + '"></div>' + + '</div>' + + '<div class="resize-sensor-shrink" style="' + style + '">' + + '<div style="' + styleChild + ' width: 200%; height: 200%"></div>' + + '</div>'; + element.appendChild(element.resizeSensor); + + if (getComputedStyle(element, 'position') == 'static') { + element.style.position = 'relative'; + } + + var expand = element.resizeSensor.childNodes[0]; + var expandChild = expand.childNodes[0]; + var shrink = element.resizeSensor.childNodes[1]; + + var reset = function() { + expandChild.style.width = 100000 + 'px'; + expandChild.style.height = 100000 + 'px'; + + expand.scrollLeft = 100000; + expand.scrollTop = 100000; + + shrink.scrollLeft = 100000; + shrink.scrollTop = 100000; + }; + + reset(); + var dirty = false; + + var dirtyChecking = function() { + if (!element.resizedAttached) return; + + if (dirty) { + element.resizedAttached.call(); + dirty = false; + } + + requestAnimationFrame(dirtyChecking); + }; + + requestAnimationFrame(dirtyChecking); + var lastWidth, lastHeight; + var cachedWidth, cachedHeight; //useful to not query offsetWidth twice + + var onScroll = function() { + if ((cachedWidth = element.offsetWidth) != lastWidth || (cachedHeight = element.offsetHeight) != lastHeight) { + dirty = true; + + lastWidth = cachedWidth; + lastHeight = cachedHeight; + } + reset(); + }; + + var addEvent = function(el, name, cb) { + if (el.attachEvent) { + el.attachEvent('on' + name, cb); + } else { + el.addEventListener(name, cb); + } + }; + + addEvent(expand, 'scroll', onScroll); + addEvent(shrink, 'scroll', onScroll); + } + + forEachElement(element, function(elem){ + attachResizeEvent(elem, callback); + }); + + this.detach = function(ev) { + ResizeSensor.detach(element, ev); + }; + }; + + ResizeSensor.detach = function(element, ev) { + forEachElement(element, function(elem){ + if(elem.resizedAttached && typeof ev == "function"){ + elem.resizedAttached.remove(ev); + if(elem.resizedAttached.length()) return; + } + if (elem.resizeSensor) { + if (elem.contains(elem.resizeSensor)) { + elem.removeChild(elem.resizeSensor); + } + delete elem.resizeSensor; + delete elem.resizedAttached; + } + }); + }; + + return ResizeSensor; + +})); diff --git a/js/main.js b/js/main.js index a82aca8f3154d05e2e3de583ba9f21fd9dea6d4a..57005f8aa0d282343914aafb20c09062e2caf541 100644 --- a/js/main.js +++ b/js/main.js @@ -7,17 +7,18 @@ $(document).ready(function() $('.version-entry-title').click(function() { toggleDetail($(this).get(0)); - createTrainMap(); }); - $('.collapsible-header').click(function() + //reacts to resize event of card and calls createTrainMap to adjust circles + //https://github.com/marcj/css-element-queries + var entries = document.getElementsByClassName('version-entry'); + for(var i = 0; i < entries.length - 1; i++) { - setTimeout(function() + new ResizeSensor(entries[i], function() { createTrainMap(); - }, 300); - - }); + }); + } createTrainMap(); });