/** * Gridzy 2.3.0 * * Copyright 2020 * Helmut Wandl * https://ehtmlu.com/ */ (function () { 'use strict'; var version = '2.3.0'; var defaultOptionsTpl = { animate: true, layout: 'justified', filter: '*', filterControls: '', autoConsiderScrollBars: true, onBeforeOptionsChanged: function () {}, onOptionsChanged: function () {}, onBeforeRender: function () {}, onRender: function () {}, autoInitOnDomReady: true, autoInitOnDomMutation: true, autoSyncChildListMutation: true, useOptionAttributes: true, autoSyncAttributesMutation: true, autoSyncChildClassMutation: true }; var defaultOptionsLayoutTpl = {}; var defaultOptionsCompleted = null; var idAutoIncrement = 0; var lazyLoadImagesQueue = null; var helper = { isTruly: function (i) { return !!i && (typeof i !== 'string' || i.toLowerCase() !== 'false'); }, expectArray: function (i) { return helper.isArrayLike(i) ? i : [i]; }, isArrayLike: function (i) { return i.length === +i.length && typeof i !== 'string'; }, inArray: function (arr, elt) { return arr.indexOf(elt) !== -1; }, objectWalk: function (obj, func) { var a; func(obj); if (typeof obj === 'object') { for (a in obj) { helper.objectWalk(obj[a], func); } } }, objectAssign: function (target) { var pos, name; for (pos = 1; pos < arguments.length; pos++) { for (name in arguments[pos]) { target[name] = arguments[pos][name]; } } return target; }, optionsMerge: function (target, add) { var name; for (name in add) { if (typeof target[name] !== 'undefined') { target[name] = add[name]; } } return target; }, isValidCssSelector: function (selector) { var result = false; if (typeof selector === 'string') { try { helper.matches(document.body, selector); result = true; } catch (e) {} } return result; }, matches: Element.prototype.matches ? function (element, selector) { return element.matches(selector); } : (Element.prototype.msMatchesSelector ? function (element, selector) { return element.msMatchesSelector(selector); } : (Element.prototype.webkitMatchesSelector ? function (element, selector) { return element.webkitMatchesSelector(selector); } : function (element, selector) { var matches = (element.document || element.ownerDocument).querySelectorAll(selector), i = matches.length; while (--i >= 0 && matches.item(i) !== element) {} return i > -1; })), changeMultipleClassNames: (function () { // IE11 support for adding multiple css classes var list = document.createElement('div').classList; list.add('a', 'b'); return list.contains('b'); })() ? function (element, add, classNames) { element.classList[add ? 'add' : 'remove'].apply(element.classList, classNames); } : function (element, add, classNames) { var length = classNames.length, change = add ? 'add' : 'remove', pos = length; while (pos--) { element.classList[change](classNames[length - pos - 1]); } }, eventListener: (function () { var eventListeners = []; return { add: function(relatedGridzyInstance, relatedItem, element, type, handler, options, removeWhenOccurred) { eventListeners.push([relatedGridzyInstance, relatedItem, element, type, handler, options]); element.addEventListener(type, removeWhenOccurred ? function(event) { helper.eventListener.remove(relatedGridzyInstance, relatedItem, element, type, handler, options); return handler(event); } : handler, options); }, remove: function(relatedGridzyInstance, relatedItem, element, type, handler, options) { var pos = eventListeners.length; var eventListener; while (pos--) { eventListener = eventListeners[pos]; if (relatedGridzyInstance === undefined || relatedGridzyInstance === eventListener[0]) { if (relatedItem === undefined || relatedItem === eventListener[1]) { if (element === undefined || element === eventListener[2]) { if (type === undefined || type === eventListener[3]) { if (handler === undefined || handler === eventListener[4]) { if (options === undefined || options === eventListener[5]) { eventListener[2].removeEventListener(eventListener[3], eventListener[4], eventListener[5]); eventListeners.splice(pos, 1); } } } } } } } } }; })(), expectNaturalImageSize: (function () { var interval, images = [], update; update = function () { var a; for (a = 0; a < images.length; a++) { if (images[a].img.naturalWidth && images[a].img.naturalHeight || images[a].ready) { images[a].callback(images[a].img, !images[a].error); images.splice(a, 1); a--; } } if (!images.length) { clearInterval(interval); interval = null; } }; return function (gridzyInstance, item, img, onReadyCallback) { var obj = { img: img, callback: onReadyCallback || function () { }, imgB: document.createElement('img'), ready: false, error: false }; var onLoad = function () { obj.ready = true; helper.eventListener.remove(gridzyInstance, item, obj.imgB, 'error', onError, false); }; var onError = function () { obj.ready = true; obj.error = true; helper.eventListener.remove(gridzyInstance, item, obj.imgB, 'load', onLoad, false); }; helper.eventListener.add(gridzyInstance, item, obj.imgB, 'load', onLoad, false, true); helper.eventListener.add(gridzyInstance, item, obj.imgB, 'error', onError, false, true); obj.imgB.src = obj.img.src; images.push(obj); if (!interval) interval = setInterval(update, 0); }; })(), processLazyLoading: function () { if (lazyLoadImagesQueue === null) { return; } // temporarily disable observations pauseMutationObservation(); var pos = lazyLoadImagesQueue.length, viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0), viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0), imagesToLoad = [], rect, xPos, yPos, edge = 1.5; // 1 is exactly the viewport size; 2 is the space of one viewport more in each direction and so on while (pos--) { rect = lazyLoadImagesQueue[pos].image.getBoundingClientRect(); // calculate visibility of the image - the values will be between 0 and 1 if the image is visible xPos = rect.right / (viewportWidth + (rect.right - rect.left)); yPos = rect.bottom / (viewportHeight + (rect.bottom - rect.top)); if (xPos > 1 - edge && xPos < edge && yPos > 1 - edge && yPos < edge) { imagesToLoad.push(lazyLoadImagesQueue.splice(pos, 1)[0]); } } // start loading images // we use "for" instead of "while", because we need the correct order. (The loading order should be from top to bottom) for (pos = 0; pos < imagesToLoad.length; pos++) { imagesToLoad[pos].item.contentElement.classList.add('gridzyItemLoading'); if (imagesToLoad[pos].src) { imagesToLoad[pos].image.src = imagesToLoad[pos].src; } if (imagesToLoad[pos].srcset) { imagesToLoad[pos].image.srcset = imagesToLoad[pos].srcset; } } }, registerReadOnlyObjectParameters: function (obj, parameters) { var key; for (key in parameters) { Object.defineProperty(obj, key, { value: parameters[key], enumerable: true }); } }, completeDefaultOptionsObject: function () { if (defaultOptionsCompleted !== Gridzy._defaultOptions.layout && typeof Gridzy._layouts[Gridzy._defaultOptions.layout] !== 'undefined') { Gridzy._defaultOptions = helper.optionsMerge(helper.cloneDefaultOptionsTemplate(Gridzy._defaultOptions.layout), Gridzy._defaultOptions); defaultOptionsCompleted = Gridzy._defaultOptions.layout; } }, cloneDefaultOptionsTemplate: function (layout) { if (typeof defaultOptionsLayoutTpl[layout] === 'undefined' && typeof Gridzy._layouts[layout] !== 'undefined') { defaultOptionsLayoutTpl[layout] = helper.objectAssign({}, Gridzy._layouts[layout]._defaultOptions, defaultOptionsTpl); } return typeof defaultOptionsLayoutTpl[layout] !== 'undefined' ? helper.objectAssign({}, defaultOptionsLayoutTpl[layout]) : {}; } }; var mutationObservation = true; function pauseMutationObservation () { if (mutationObservation) { mutationObservation = false; setTimeout(function() { mutationObservation = true; }, 0); } } var initMutationObserver = new function() { var active = false; var mutationObserver = new MutationObserver(function () { if (mutationObservation) { initAll(); } }); this.reset = function () { var value = Gridzy._defaultOptions.autoInitOnDomMutation; if (active !== !!value) { if (value) { initAll(); mutationObserver.observe(document.body, { attributes: true, attributeFilter: ['class'], subtree: true, childList: true }); } else { mutationObserver.disconnect(); } active = !!value; } }; }; function GridzyRenderObject(gridzyInstance) { var style = getComputedStyle(gridzyInstance.element), dimensions = { actualBox: null, borderBox: { width: gridzyInstance.element.offsetWidth, height: gridzyInstance.element.offsetHeight }, paddingBox: { width: gridzyInstance.element.clientWidth, height: gridzyInstance.element.clientHeight }, contentBox: null, border: { top: parseInt(style.borderTopWidth), right: parseInt(style.borderRightWidth), bottom: parseInt(style.borderBottomWidth), left: parseInt(style.borderLeftWidth) }, padding: { top: parseInt(style.paddingTop), right: parseInt(style.paddingRight), bottom: parseInt(style.paddingBottom), left: parseInt(style.paddingLeft) } }; dimensions.contentBox = { width: dimensions.paddingBox.width - dimensions.padding.left - dimensions.padding.right, height: dimensions.paddingBox.height - dimensions.padding.top - dimensions.padding.bottom }; dimensions.actualBox = style.boxSizing === 'border-box' ? dimensions.borderBox : dimensions.contentBox; helper.registerReadOnlyObjectParameters(this, { gridzyInstance: gridzyInstance, dimensions: dimensions, items: [], layoutStorage: gridzyInstance._layoutStorage }); this.styleToApply = ''; var pos = gridzyInstance._allItems.length; while (pos--) { this.items.unshift(new GridzyRenderObjectItem(gridzyInstance._allItems[pos])); } } function GridzyRenderObjectItem(item) { helper.registerReadOnlyObjectParameters(this, { element: item.contentElement, valid: item.valid, imageError: item.failures, matchFilter: item.matchFilter, width: item.size[0], height: item.size[1], aspectRatio: item.ratio, layoutStorage: item.layoutStorage }); this.visible = item.visible; this.styleToApply = ''; } window.GridzyLayout = function (parameter) { if (typeof parameter.name === 'string' && typeof Gridzy._layouts[parameter.name] === 'undefined') { this.name = parameter.name; this._defaultOptions = parameter && typeof parameter.options === 'object' ? parameter.options : {}; this._handler = parameter && typeof parameter.handler === 'function' ? parameter.handler : function() {}; Gridzy._layouts[parameter.name] = this; } }; GridzyLayout.prototype = { constructor: GridzyLayout, set handler(value) { }, get handler() { return this._handler; } }; window.Gridzy = function (element, options) { var isNewInstance = typeof element.gridzy === 'undefined', self = isNewInstance ? this : element.gridzy, visibilityClassName, prevContainerWidth = 0; options = options || {}; // initialize default options self._options = Gridzy.getDefaultOptions(); // merge options from element and options object if (helper.objectAssign({}, self._options, options).useOptionAttributes) { options = helper.objectAssign({}, self._getOptionParametersFromElement(element), options); } if (isNewInstance) { // bind instance to container element element.gridzy = self; // register several parameters helper.registerReadOnlyObjectParameters(self, { element: element, id: ++idAutoIncrement, version: version, _allItems: [], _layoutStorage: {}, _filterControls: [], _itemVisibilityClasses: { null_true: { add: ['gridzyItemInitialToVisible'], remove: [] }, null_false: { add: ['gridzyItemHidden'], remove: [] }, false_true: { add: ['gridzyItemHiddenToVisible'], remove: [] }, true_false: { add: ['gridzyItemVisibleToHidden'], remove: [] }, true_true: { add: ['gridzyItemVisibleToVisible'], remove: [] }, true: { add: ['gridzyItemVisible'], remove: [] }, false: { add: ['gridzyItemHidden'], remove: [] }, null: { add: [], remove: [] } }, _itemVisibilityClassesArray: [], _insertQueue: { maxQueueTime: 1000, maxQuietTime: 400, items: [], lastProcess: null, timer: null }, _elementMutationObserverConfig: { attributes: false, childList: false }, _elementMutationObserver: new MutationObserver(function(mutations) { if (mutationObservation) { var options = {}, setOptions = false, render = false, syncElements = false; mutations.forEach(function(mutation) { switch (mutation.type) { case 'attributes': if (mutation.attributeName === 'class' && self._options.useOptionAttributes) { if (self._options.animate !== mutation.target.classList.contains('gridzyAnimated') && typeof options.animate === 'undefined') { options.animate = !self._options.animate; setOptions = true; } } else if (mutation.attributeName === 'style') { render = true; } else if (mutation.attributeName.indexOf('data-gridzy-') === 0 && self._options.useOptionAttributes) { var optionData = self._getOptionParametersFromElement(null, null, mutation.attributeName); options[optionData.name] = optionData.value; setOptions = true; } break; case 'childList': syncElements = true; render = true; break; } }); if (syncElements) { self._syncElements(); } if (setOptions) { self.setOptions(options); } else if (render) { self.render(); } } }), _childMutationObserverConfig: { attributes: false, attributeFilter: ["class"] }, _childMutationObserver: new MutationObserver(function(mutations) { if (mutationObservation) { var filter = self._options.filter, render = false; mutations.forEach(function(mutation) { switch (mutation.type) { case 'attributes': if (typeof mutation.target.gridzyItem !== 'undefined') { if (self._setMatchFilter(mutation.target.gridzyItem, filter)) { render = true; } } } }); if (render) { self.render(); } } }) }); self._itemIdAutoIncrement = 0; // fill _itemVisibilityClassesArray helper.objectWalk(self._itemVisibilityClasses, function(obj) { if (typeof obj === 'string' && self._itemVisibilityClassesArray.indexOf(obj) === -1) { self._itemVisibilityClassesArray.push(obj); } }); // set remove arrays of _itemVisibilityClasses for (visibilityClassName in self._itemVisibilityClasses) { self._itemVisibilityClasses[visibilityClassName].remove = self._itemVisibilityClassesArray.filter(function (el) { return self._itemVisibilityClasses[visibilityClassName].add.indexOf(el) < 0; }); } // initialize user specific options self._updateOptionParameters(options); // temporarily disable observations pauseMutationObservation(); // set gridzy id and version as element attributes (so it's easy recognizable outside) self.element.setAttribute('data-gridzyid', self.id); self.element.setAttribute('data-gridzyversion', self.version); // initialize animationend event listener helper.eventListener.add(self, null, self.element, "animationend", function (event) { if (event.target.gridzyItem) { // temporarily disable observations pauseMutationObservation(); helper.changeMultipleClassNames(event.target, true, self._itemVisibilityClasses[JSON.stringify(event.target.gridzyItem.visible)].add); helper.changeMultipleClassNames(event.target, false, self._itemVisibilityClasses[JSON.stringify(event.target.gridzyItem.visible)].remove); } }); // initialize child elements self._syncElements(); // force rendering on window resize prevContainerWidth = self.element.clientWidth; helper.eventListener.add(self, null, window, 'resize', function () { if (self.element.clientWidth !== prevContainerWidth) { prevContainerWidth = self.element.clientWidth; self.render(); } }, false); // to permit css hover state on mobile devices helper.eventListener.add(self, null, document, "touchstart", function(){}, true); } else { self.setOptions(options); } return self; }; Gridzy._layouts = {}; Gridzy._defaultOptions = helper.objectAssign({}, defaultOptionsTpl); Gridzy.setDefaultOptions = function (options) { if (typeof options.layout !== 'undefined' && Gridzy._defaultOptions.layout !== options.layout && typeof Gridzy._layouts[options.layout] !== 'undefined') { Gridzy._defaultOptions.layout = options.layout; } helper.completeDefaultOptionsObject(); Gridzy._defaultOptions = helper.optionsMerge(Gridzy._defaultOptions, options); initMutationObserver.reset(); }; Gridzy.getDefaultOptions = function (layout) { if (layout && layout !== Gridzy._defaultOptions.layout && typeof Gridzy._layouts[layout] !== 'undefiend') { return helper.optionsMerge(helper.cloneDefaultOptionsTemplate(layout), Gridzy._defaultOptions); } else { helper.completeDefaultOptionsObject(); return helper.objectAssign({}, Gridzy._defaultOptions); } }; Gridzy.getDefaultOption = function (name) { helper.completeDefaultOptionsObject(); return typeof Gridzy._defaultOptions[name] !== 'undefined' ? Gridzy._defaultOptions[name] : null; }; Gridzy.prototype = { IMG_FAILURE: 0, IMG_LOADING: 1, IMG_LOADING_READY: 2, // the image is loading but we have width and height attributes for calculation IMG_READY: 3, // the image is still loading but we have naturalWidth and naturalHeight for calculation IMG_FINISHED: 4, ITEM_REGISTERED: 1, ITEM_INSERT_QUEUE: 2, ITEM_INSERTED: 3, setOptions: function (options) { this._updateOptionParameters(options); this.render(); helper.processLazyLoading(); }, getOptions: function () { return helper.objectAssign({}, this._options); }, getOption: function (optionName) { return typeof this._options[optionName] !== 'undefined' ? this._options[optionName] : null; }, syncAttributes: function () { if (this._options.useOptionAttributes) { this.setOptions(this._getOptionParametersFromElement()); } }, syncChild: function(childElement) { this._syncElements(childElement); this.render(); }, syncChildList: function () { this._syncElements(); this.render(); }, destroy: function () { var items; // remove event listeners helper.eventListener.remove(this); // remove items from internal list items = this._allItems.splice(0, this._allItems.length); // remove reference from DOM element delete this.element.gridzy; pauseMutationObservation(); // prepare removing styles this.element.setAttribute('style', 'transition: none; animation: none; '); // reset item DOM elements this._resetItemsDOM(items); // remove attributes this.element.removeAttribute('data-gridzyid'); this.element.removeAttribute('data-gridzyversion'); this.element.removeAttribute('style'); // trigger layout engine document.body.offsetWidth; }, render: function () { var pos, item, itemStyle, changeClasses, renderObject = this._getRenderObject(), filter = this._options.filter; // execute user defined call back function this._options.onBeforeRender(this); // temporarily disable observations pauseMutationObservation(); // preset all new values to the style attribute of hidden item elements (to start animations from there) pos = renderObject.items.length; while (pos--) { item = renderObject.items[pos]; if (item.valid && !item.element.gridzyItem.visible) { item.element.style.cssText = item.element.style.cssText + '; ' + item.styleToApply + '; transition: none; '; } } // force painting document.body.offsetWidth; // set container element style renderObject.gridzyInstance.element.style.cssText = renderObject.gridzyInstance.element.style.cssText + '; ' + renderObject.styleToApply; // set all new values to the style attribute and to the class attribute of item elements pos = renderObject.items.length; while (pos--) { item = renderObject.items[pos]; if (item.valid) { if (item.element.gridzyItem.visible) { item.element.style.cssText = item.element.style.cssText + '; ' + item.styleToApply; } else { item.element.style.transition = ''; } changeClasses = this._itemVisibilityClasses[JSON.stringify(item.element.gridzyItem.visible) + '_' + JSON.stringify(item.visible)]; if (changeClasses) { helper.changeMultipleClassNames(item.element, true, changeClasses.add || []); helper.changeMultipleClassNames(item.element, false, changeClasses.remove || []); } item.element.gridzyItem.visible = item.visible; } } // reset matchFilter attribute // set class attribute of item elements which have no animation pos = renderObject.items.length; while (pos--) { item = renderObject.items[pos]; if (item.valid) { this._setMatchFilter(item.element.gridzyItem, filter); itemStyle = getComputedStyle(item.element); if (itemStyle.animationName === 'none') { changeClasses = this._itemVisibilityClasses[JSON.stringify(item.visible)]; helper.changeMultipleClassNames(item.element, true, changeClasses.add || []); helper.changeMultipleClassNames(item.element, false, changeClasses.remove || []); } } } // execute user defined call back function this._options.onRender(this); }, _resetElementMutationObserver: function () { var config = this._elementMutationObserverConfig, changed = false, observed = config.attributes || config.childList, observe; if (config.attributes !== this._options.autoSyncAttributesMutation) { config.attributes = !config.attributes; changed = true; } if (config.childList !== this._options.autoSyncChildListMutation) { config.childList = !config.childList; changed = true; } observe = config.attributes || config.childList; if (changed) { if (observed) { this._elementMutationObserver.disconnect(); } if (observe) { this._elementMutationObserver.observe(this.element, helper.objectAssign({}, config)); } } }, _resetChildMutationObserver: function (force) { var config = this._childMutationObserverConfig, configCopy, changed = false, observed = config.attributes, observe, item, filter, render = false, pos; if (config.attributes !== this._options.autoSyncChildClassMutation) { config.attributes = !config.attributes; changed = true; } observe = config.attributes; if (changed || force) { if (observed) { this._childMutationObserver.disconnect(); } if (observe) { configCopy = helper.objectAssign({}, config); filter = this._options.filter; pos = this._allItems.length; while (pos--) { item = this._allItems[pos]; this._childMutationObserver.observe(item.contentElement, configCopy); if (this._setMatchFilter(item, filter)) { render = true; } } } } if (render) { this.render(); } }, _setMatchFilter: function (item, filter) { if (item.status === this.ITEM_INSERTED) { var matchFilter = helper.matches(item.contentElement, filter); if (matchFilter !== item.matchFilter) { item.matchFilter = matchFilter; return true; } } return false; }, _filterControlEventHandler: function (event) { var pos = this._filterControls.length, pos2, filter = [], controlElement, targetElement = event.target; // get the select element if an option element is clicked if (targetElement.nodeName === 'OPTION') { targetElement = targetElement.parentNode; } // collect filter selectors of all control elements while (pos--) { controlElement = this._filterControls[pos]; if (targetElement === controlElement) { if (controlElement.nodeName === 'SELECT') { // get filters from selected options of select field pos2 = controlElement.options.length; while (pos2--) { if (controlElement.options[pos2].selected) { filter = filter.concat(controlElement.options[pos2].value); } } } else if (controlElement.checked || controlElement.type !== 'checkbox') { // get filters of focused elements which are not unchecked checkboxes filter = filter.concat(controlElement.value); } } else if (controlElement.checked) { if (targetElement.type === 'checkbox' && controlElement.type === 'checkbox') { // get filters from active checkboxes which are not clicked filter = filter.concat(controlElement.value); } else { // uncheck active radio buttons which are not clicked controlElement.checked = false; } } else if (controlElement.nodeName === 'SELECT') { // deselect options of select fields which are not clicked pos2 = controlElement.options.length; while (pos2--) { controlElement.options[pos2].selected = false; } } } this.setOptions({filter:(filter.join(', ') || ':not(*)')}); }, _getRenderObject: function () { var autoConsiderScrollBars = this._options.autoConsiderScrollBars, renderObject = new GridzyRenderObject(this), renderObjectChanged = false, ancestor = renderObject.gridzyInstance.element, ancestorOverflowY = null, ancestorHeight = null, width = renderObject.gridzyInstance.element.offsetWidth, position = renderObject.gridzyInstance.element.style.position, style = renderObject.gridzyInstance.element.style.cssText, newHeight, testRequired = false; this._prepareRenderObject(renderObject); // test whether we need a recalculation because of appearing or disappearing scrollbars if (autoConsiderScrollBars) { // test whether we really need an expensive test while (ancestor.parentNode && ancestor.parentNode.nodeName !== '#document') { ancestor = ancestor.parentNode; ancestorOverflowY = getComputedStyle(ancestor).overflowY; if (ancestorOverflowY === 'auto' || ancestor.nodeName === 'BODY' && ancestorOverflowY === 'visible') { testRequired = true; ancestorHeight = ancestor.nodeName === 'BODY' ? Math.max(document.documentElement.clientHeight, window.innerHeight || 0) : ancestor.offsetHeight; if (ancestorHeight < renderObject.gridzyInstance.element.offsetHeight) { newHeight = renderObject.styleToApply.match(/height:\s*([0-9]+)px;/); newHeight = newHeight ? parseInt(newHeight[1]) : null; if (ancestorHeight < newHeight) { testRequired = false; } } break; } } // if we really need the expensive test let's do it if (testRequired) { renderObject.gridzyInstance.element.style.cssText = style + '; ' + renderObject.styleToApply + '; ' + 'transition: none; ' + 'animation: none; ' + 'overflow: hidden; '; if (width !== renderObject.gridzyInstance.element.offsetWidth) { renderObject.gridzyInstance.element.style.position = position; renderObject = new GridzyRenderObject(this); renderObjectChanged = true; } renderObject.gridzyInstance.element.style.cssText = style + '; transition: none; animation: none; '; document.body.offsetWidth; renderObject.gridzyInstance.element.style.cssText = style; document.body.offsetWidth; // if there is a new renderObject let's prepare it if (renderObjectChanged) { this._prepareRenderObject(renderObject); } } } return renderObject; }, _prepareRenderObject: function (renderObject) { // insert the new calculated values Gridzy._layouts[this._options.layout].handler(renderObject); // complete container element style if (this.element.children.length && this.element.children[0].offsetParent !== this.element) { renderObject.styleToApply += '; position: relative; '; } if (this.element.style.display !== 'block') { renderObject.styleToApply += '; display: block; '; } }, _syncElements: function (childElement) { var children = this.element.children, pos, item, filter = this.getOption('filter'); // clear items array this._allItems.length = 0; // rebuild items array for (pos = 0; pos < children.length; pos++) { item = false; if (children[pos].gridzyItem !== undefined) { item = children[pos].gridzyItem; } else if (childElement === undefined || childElement === children[pos]) { item = this._createItem(children[pos]); } if (item) { this._setMatchFilter(item, filter); this._allItems.push(item); } } this._resetChildMutationObserver(true); }, _resetItemsDOM: function (items) { var pos; var item; pauseMutationObservation(); pos = items.length; while (pos--) { item = items[pos]; // prepare removing styles item.contentElement.setAttribute('style', 'transition: none; animation: none; '); } // trigger layout engine document.body.offsetWidth; pos = items.length; while (pos--) { item = items[pos]; // remove item reference from item DOM element delete item.contentElement.gridzyItem; // remove existing gridzy classes from element helper.changeMultipleClassNames(item.contentElement, false, this._itemVisibilityClassesArray.concat(['gridzyItem', 'gridzyItemLoading', 'gridzyItemInitializing', 'gridzyItemReady', 'gridzyItemComplete'])); // remove attributes if (item.contentElement.className === '') { item.contentElement.removeAttribute('class'); } item.contentElement.removeAttribute('style'); item.contentElement.removeAttribute('data-gridzyitemid'); } }, _createItem: function (contentElement) { if (this._insertQueue.items.length === 0) { this._insertQueue.lastProcess = (new Date()).getTime(); } // temporarily disable observations pauseMutationObservation(); // remove existing gridzy classes from element helper.changeMultipleClassNames(contentElement, false, this._itemVisibilityClassesArray.concat(['gridzyItem', 'gridzyItemLoading', 'gridzyItemInitializing', 'gridzyItemReady', 'gridzyItemComplete'])); // generate and insert the item object var item = { id: ++this._itemIdAutoIncrement, contentElement: contentElement, size: [0, 0], ratio: 0, failures: false, status: this.ITEM_REGISTERED, images: [], imagesStatus: [], imagesInfo: [], matchFilter: null, layoutStorage: {}, visible: null, destroy: function() { // remove event listeners helper.eventListener.remove(this, item); // remove item from internal list this._allItems.splice(this._allItems.indexOf(item), 1); // reset item DOM element this._resetItemsDOM([item]); // trigger layout engine document.body.offsetWidth; }.bind(this) }; contentElement.gridzyItem = item; // set attributes, classes and styles item.contentElement.setAttribute('data-gridzyitemid', item.id); item.contentElement.style.visibility = 'hidden'; item.contentElement.style.position = 'absolute'; helper.changeMultipleClassNames(item.contentElement, true, ['gridzyItem', 'gridzyItemLoading', 'gridzyItemInitializing']); // search for images to get natural image sizes (they are needed to get the element size) var images = contentElement.nodeName === 'IMG' ? [contentElement] : contentElement.querySelectorAll('img'), pos = images.length; while (pos--) { (function (e) { item.images.push(e); var attributeWidth = e.getAttribute('width'); var attributeHeight = e.getAttribute('height'); var lazySrc = e.getAttribute('data-gridzylazysrc'); var lazySrcset = e.getAttribute('data-gridzylazysrcset'); var src = e.getAttribute('src'); if (!src && (lazySrc || lazySrcset)) { // if there is a data-gridzylazysrc or a data-gridzylazysrcset attribute but there is no width or height attribute, we deactivate lazy loading for this image if (!attributeWidth || !attributeHeight) { e.src = lazySrc; e.srcset = lazySrcset; lazySrc = null; lazySrcset = null; } else { // set a transparent pixel as an image if there is no src e.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEHAAEALAAAAAABAAEAAAICTAEAOw=='; } } item.imagesInfo.push({ styleWidth: e.style.width, styleHeight: e.style.height, attributeWidth: attributeWidth, attributeHeight: attributeHeight, lazySrc: lazySrc, lazySrcset: lazySrcset }); if (attributeWidth !== null && attributeHeight !== null && parseInt(attributeWidth) > 0 && parseInt(attributeHeight) > 0) { item.imagesStatus.push(this.IMG_LOADING_READY); } else { item.imagesStatus.push(this.IMG_LOADING); } }).bind(this)(images[pos]); } // force painting document.body.offsetWidth; item.images.forEach(function (image, index) { var onLoad = function () { item.imagesStatus[index] = image.naturalWidth ? this.IMG_FINISHED : this.IMG_FAILURE; this._onImageFinished(item); helper.eventListener.remove(this, item, image, 'error', onError, false); }.bind(this); var onError = function () { item.imagesStatus[index] = this.IMG_FAILURE; this._onImageFinished(item); helper.eventListener.remove(this, item, image, 'load', onLoad, false); }.bind(this); helper.eventListener.add(this, item, image, 'load', onLoad, false, true); helper.eventListener.add(this, item, image, 'error', onError, false, true); if (image.complete) { onLoad(); } if (!item.imagesInfo[index].lazySrc && !item.imagesInfo[index].lazySrcset) { helper.expectNaturalImageSize(this, item, image, function (img, success) { item.imagesStatus[index] = success && image.naturalWidth ? (image.complete ? this.IMG_FINISHED : this.IMG_READY) : this.IMG_FAILURE; this._onImageReady(item); }.bind(this)); } }.bind(this)); this._onImageReady(item); this._onImageFinished(item); return item; }, _processInsertQueue: function (maxQuietTimeReached) { var queueItem; var maxQueueTimeReached = this._insertQueue.lastProcess < (new Date()).getTime() - this._insertQueue.maxQueueTime; var imagesStylesToReset = []; var imagesStylesToResetPos; var imagesInfo; var imagePos; var imageStyle; var queueItems = []; var queueItemsPos; var filter = this._options.filter; var lazyLoadImages = []; if (this._insertQueue.timer !== null) { clearTimeout(this._insertQueue.timer); this._insertQueue.timer = null; } if (maxQuietTimeReached || maxQueueTimeReached) { // temporarily disable observations pauseMutationObservation(); // insert item elements queueItem = this._insertQueue.items.shift(); while (queueItem) { queueItems.push(queueItem); queueItem = this._insertQueue.items.shift(); } // get images where to set width and height style to force image sizes from attributes queueItemsPos = queueItems.length; while (queueItemsPos--) { queueItem = queueItems[queueItemsPos]; imagePos = queueItem.item.images.length; while (imagePos--) { imagesInfo = queueItem.item.imagesInfo[imagePos]; // in some cases google chrome and firefox don't have the correct offset size even though the natural size is known if (!imagesInfo.attributeWidth && queueItem.item.images[imagePos].offsetWidth < 40 && queueItem.item.images[imagePos].naturalWidth) { imagesInfo.attributeWidth = queueItem.item.images[imagePos].naturalWidth; } if (!imagesInfo.attributeHeight && queueItem.item.images[imagePos].offsetHeight < 40 && queueItem.item.images[imagePos].naturalHeight) { imagesInfo.attributeHeight = queueItem.item.images[imagePos].naturalHeight; } if (imagesInfo.attributeWidth || imagesInfo.attributeHeight) { imageStyle = getComputedStyle(queueItem.item.images[imagePos]); if ((parseInt(imageStyle.width) < 40 || imageStyle.width === 'auto' || imagesInfo.lazySrc || imagesInfo.lazySrcset) && imagesInfo.attributeWidth && !imagesInfo.styleWidth) { imagesStylesToReset.push({ image: queueItem.item.images[imagePos], definition: 'width', value: imagesInfo.attributeWidth + 'px' }); } if ((parseInt(imageStyle.height) < 40 || imageStyle.height === 'auto' || imagesInfo.lazySrc || imagesInfo.lazySrcset) && imagesInfo.attributeHeight && !imagesInfo.styleHeight) { imagesStylesToReset.push({ image: queueItem.item.images[imagePos], definition: 'height', value: imagesInfo.attributeHeight + 'px' }); } } // get images to lazy load if (imagesInfo.lazySrc || imagesInfo.lazySrcset) { lazyLoadImages.push({ item: queueItem.item, image: queueItem.item.images[imagePos], src: imagesInfo.lazySrc, srcset: imagesInfo.lazySrcset }); } } } // set width and height style to force image sizes from attributes imagesStylesToResetPos = imagesStylesToReset.length; while (imagesStylesToResetPos--) { imagesStylesToReset[imagesStylesToResetPos].image.style[imagesStylesToReset[imagesStylesToResetPos].definition] = imagesStylesToReset[imagesStylesToResetPos].value; // set display to "none" because FF need more than only a browser rerendering to apply the sizes to elements imagesStylesToReset[imagesStylesToResetPos].image.style.display = 'none'; } // force browser rendering and display images again after hiding it because of FF document.body.offsetWidth; imagesStylesToResetPos = imagesStylesToReset.length; while (imagesStylesToResetPos--) { imagesStylesToReset[imagesStylesToResetPos].image.style.display = ''; } // get size of item element queueItemsPos = queueItems.length; while (queueItemsPos--) { queueItem = queueItems[queueItemsPos]; queueItem.item.size = [queueItem.contentElement.offsetWidth, queueItem.contentElement.offsetHeight]; queueItem.item.ratio = queueItem.item.size[0] / queueItem.item.size[1]; } // remove width and height style where it was set before getting size of item element imagesStylesToResetPos = imagesStylesToReset.length; while (imagesStylesToResetPos--) { imagesStylesToReset[imagesStylesToResetPos].image.style[imagesStylesToReset[imagesStylesToResetPos].definition] = ''; } // set styles for item element queueItemsPos = queueItems.length; while (queueItemsPos--) { queueItem = queueItems[queueItemsPos]; queueItem.item.status = this.ITEM_INSERTED; queueItem.item.valid = !!(queueItem.item.size[0] && queueItem.item.size[1]); this._setMatchFilter(queueItem.item, filter); queueItem.item.contentElement.classList.remove('gridzyItemInitializing'); queueItem.item.contentElement.classList.add('gridzyItemReady'); } // calculate and render layout this.render(); // make item elements visible queueItemsPos = queueItems.length; while (queueItemsPos--) { queueItem = queueItems[queueItemsPos]; queueItem.item.contentElement.style.visibility = ''; } // register lazy loading images if (lazyLoadImages.length) { this._addImagesToLazyLoadQueue(lazyLoadImages); } this._insertQueue.lastProcess = (new Date()).getTime(); } else { this._insertQueue.timer = setTimeout(function () { this._processInsertQueue(true); }.bind(this), this._insertQueue.maxQuietTime); } }, _addImagesToLazyLoadQueue: function (images) { // activate lazy loading if is not already activated if (lazyLoadImagesQueue === null && images.length) { lazyLoadImagesQueue = []; helper.eventListener.add(this, null, window, 'resize', helper.processLazyLoading, false); helper.eventListener.add(this, null, window, 'scroll', helper.processLazyLoading, true); } // add images to lazy loading queue lazyLoadImagesQueue = lazyLoadImagesQueue.concat(images); helper.processLazyLoading(); }, _addToInsertQueue: function (contentElement, item) { this._insertQueue.items.push({contentElement: contentElement, item: item}); this._processInsertQueue(false); }, _onImageFinished: function (item) { var failures = item.failures; if (!helper.inArray(item.imagesStatus, this.IMG_LOADING) && !helper.inArray(item.imagesStatus, this.IMG_LOADING_READY) && !helper.inArray(item.imagesStatus, this.IMG_READY)) { // temporarily disable observations pauseMutationObservation(); if (helper.inArray(item.imagesStatus, this.IMG_FAILURE)) { item.failures = true; } if (item.status === this.ITEM_INSERTED && failures !== item.failures) { this.render(); } item.contentElement.classList.remove('gridzyItemLoading'); item.contentElement.classList.add('gridzyItemComplete'); } }, _onImageReady: function (item) { var failures = item.failures; // register image loading failures item.failures = helper.inArray(item.imagesStatus, this.IMG_FAILURE); if (item.status === this.ITEM_INSERTED && failures !== item.failures) { this.render(); } // insert the element if the sizes of all contained images are known (because the size is important to calculate the layout) if (item.status === this.ITEM_REGISTERED && !helper.inArray(item.imagesStatus, this.IMG_LOADING)) { item.status = this.ITEM_INSERT_QUEUE; this._addToInsertQueue(item.contentElement, item); } }, _getOptionParametersFromElement: function (element, layout, attributeName) { var defaultOptions, defaultOptionsNormalized = {}, options = {}, optionName, attributes, pos; element = element || this.element; layout = layout || element.getAttribute('data-gridzy-layout') || this._options.layout; attributes = element.attributes; defaultOptions = Gridzy.getDefaultOptions(layout); for (optionName in defaultOptions) { defaultOptionsNormalized[optionName.toLowerCase()] = {name: optionName, value: defaultOptions[optionName]}; } if (attributeName) { optionName = attributeName.substr(12).toLowerCase(); if (typeof defaultOptionsNormalized[optionName] !== 'undefined') { options.name = defaultOptionsNormalized[optionName].name; options.value = typeof attributes[attributeName] !== 'undefined' ? attributes[attributeName].value : null; } } else { if (element.classList.contains('gridzyAnimated')) { options.animate = true; } pos = attributes.length; while (pos--) { if (attributes[pos].name.substr(0, 12) === 'data-gridzy-') { optionName = attributes[pos].name.substr(12).toLowerCase(); if (typeof defaultOptionsNormalized[optionName] !== 'undefined') { options[defaultOptionsNormalized[optionName].name] = attributes[pos].value; } } } } return options; }, _updateOptionParameters: function (options) { var name, layoutChanged = false, oldOptions, defaultOptions, optionParametersFromElement, pos, value, filterControls, attributeName; options = options || {}; this._options.onBeforeOptionsChanged(this); oldOptions = helper.objectAssign({}, this._options); // set layout if (typeof options.layout === 'object' && options.layout === null && this._options.layout !== Gridzy._defaultOptions.layout) { this._options.layout = Gridzy._defaultOptions.layout; layoutChanged = true; } if (typeof options.layout === 'string' && typeof Gridzy._layouts[options.layout] === 'object' && Gridzy._layouts[options.layout].constructor === GridzyLayout && this._options.layout !== options.layout) { this._options.layout = options.layout; layoutChanged = true; } defaultOptions = Gridzy.getDefaultOptions(this._options.layout); // sync layout options if layout has changed if (layoutChanged) { // remove unused layout options for (name in this._options) { if (typeof defaultOptions[name] === 'undefined') { delete this._options[name]; } } // set default layout options for (name in defaultOptions) { if (typeof this._options[name] === 'undefined') { this._options[name] = defaultOptions[name]; } } // restore layout options from element attributes if (this._options.useOptionAttributes) { optionParametersFromElement = this._getOptionParametersFromElement(null, this._options.layout); for (name in optionParametersFromElement) { if (typeof defaultOptions[name] !== 'undefined' && typeof options[name] === 'undefined' && this._options[name] !== optionParametersFromElement[name]) { options[name] = optionParametersFromElement[name]; } } } } // set layout options for (name in defaultOptions) { if (typeof options[name] !== 'undefined' && options[name] !== null) { switch (typeof defaultOptions[name]) { case 'boolean': this._options[name] = helper.isTruly(options[name]); break; case 'number': this._options[name] = +options[name]; break; default: this._options[name] = options[name]; } } } // set filter if (typeof options.filter === 'string' && this._options.filter !== options.filter && helper.isValidCssSelector(options.filter)) this._options.filter = options.filter; // set filterControls if (typeof options.filterControls === 'string' && (options.filterControls === '' || helper.isValidCssSelector(options.filterControls))) this._options.filterControls = options.filterControls; // set animate if (typeof options.animate !== 'undefined' && options.animate !== null) this._options.animate = helper.isTruly(options.animate); // set autoConsiderScrollBars if (typeof options.autoConsiderScrollBars !== 'undefined' && options.autoConsiderScrollBars !== null) this._options.autoConsiderScrollBars = helper.isTruly(options.autoConsiderScrollBars); // set event handlers if (typeof options.onBeforeOptionsChanged === 'function') this._options.onBeforeOptionsChanged = options.onBeforeOptionsChanged; if (typeof options.onOptionsChanged === 'function') this._options.onOptionsChanged = options.onOptionsChanged; if (typeof options.onBeforeRender === 'function') this._options.onBeforeRender = options.onBeforeRender; if (typeof options.onRender === 'function') this._options.onRender = options.onRender; // set autoSyncChildListMutation if (typeof options.autoSyncChildListMutation !== 'undefined' && options.autoSyncChildListMutation !== null) this._options.autoSyncChildListMutation = helper.isTruly(options.autoSyncChildListMutation); // set autoSyncAttributesMutation if (typeof options.autoSyncAttributesMutation !== 'undefined' && options.autoSyncAttributesMutation !== null) this._options.autoSyncAttributesMutation = helper.isTruly(options.autoSyncAttributesMutation); // set autoSyncChildClassMutation if (typeof options.autoSyncChildClassMutation !== 'undefined' && options.autoSyncChildClassMutation !== null) this._options.autoSyncChildClassMutation = helper.isTruly(options.autoSyncChildClassMutation); // reset options with value null for (name in options) { if (options[name] === null && typeof defaultOptions[name] !== 'undefined') { this._options[name] = defaultOptions[name]; } } // reset matchFilter parameter of items if (oldOptions.filter !== this._options.filter) { pos = this._allItems.length; while (pos--) { this._setMatchFilter(this._allItems[pos], this._options.filter); } } // set event listeners for filterControls if (oldOptions.filterControls !== this._options.filterControls || this._options.filterControls && this._filterControls.length === 0) { filterControls = [].slice.call(this._options.filterControls ? document.querySelectorAll(this._options.filterControls) : []); pos = this._filterControls.length; while (pos--) { if (!helper.inArray(filterControls, this._filterControls[pos])) { switch (this._filterControls[pos].nodeName + this._filterControls[pos].type) { case 'SELECT': case 'INPUTcheckbox': case 'INPUTradio': helper.eventListener.remove(this, null, this._filterControls[pos], 'change', this._filterControls[pos].gridzyFilterControlHandler); break; case 'INPUTbutton': case 'INPUTsubmit': case 'BUTTONbutton': case 'BUTTONsubmit': case 'BUTTONreset': helper.eventListener.remove(this, null, this._filterControls[pos], 'click', this._filterControls[pos].gridzyFilterControlHandler); break; default: helper.eventListener.remove(this, null, this._filterControls[pos], 'input', this._filterControls[pos].gridzyFilterControlHandler); } this._filterControls.splice(pos, 1); } } pos = filterControls.length; while (pos--) { if (!helper.inArray(this._filterControls, filterControls[pos])) { filterControls[pos].gridzyFilterControlHandler = this._filterControlEventHandler.bind(this); switch (filterControls[pos].nodeName + filterControls[pos].type) { case 'SELECT': case 'INPUTcheckbox': case 'INPUTradio': helper.eventListener.add(this, null, filterControls[pos], 'change', filterControls[pos].gridzyFilterControlHandler); break; case 'INPUTbutton': case 'INPUTsubmit': case 'BUTTONbutton': case 'BUTTONsubmit': case 'BUTTONreset': helper.eventListener.add(this, null, filterControls[pos], 'click', filterControls[pos].gridzyFilterControlHandler); break; default: helper.eventListener.add(this, null, filterControls[pos], 'input', filterControls[pos].gridzyFilterControlHandler); } this._filterControls.push(filterControls[pos]); } } } // temporarily disable observations pauseMutationObservation(); // reset element mutation observer to new options this._resetElementMutationObserver(); // update "class" element attribute this.element.classList[this._options.animate ? 'add' : 'remove']('gridzyAnimated'); // update "data-gridzy-" element attributes if (this._options.useOptionAttributes) { for (name in this._options) { attributeName = 'data-gridzy-' + name.toLowerCase(); value = this._options[name]; if (typeof options[name] !== 'undefined' && options[name] === null && this.element.hasAttribute(attributeName)) { this.element.removeAttribute(attributeName); } else if (defaultOptions[name] !== this._options[name] || this.element.hasAttribute(attributeName)) { switch (typeof value) { case 'boolean': value = value ? 'true' : 'false'; break; case 'number': value = value + ''; } if (typeof value === 'string') { this.element.setAttribute(attributeName, value); } } } } // reset child mutation observer to new options this._resetChildMutationObserver(); this._options.onOptionsChanged(this); } }; function initAll() { // find all elements with class 'gridzy' var elements = document.getElementsByClassName('gridzy'), pos = elements.length; // initialize them automatically if they aren't yet while (pos--) { if (typeof elements[pos].gridzy === 'undefined') { new Gridzy(elements[pos]); } } } // as soon as content is loaded helper.eventListener.add(null, null, document, 'DOMContentLoaded', function() { if (Gridzy._defaultOptions.autoInitOnDomReady) { initAll(); } initMutationObserver.reset(); }, false, true); // if jQuery is available register Gridzy as a plugin if (typeof jQuery !== 'undefined' && typeof jQuery.fn !== 'undefined') { jQuery.fn.gridzy = function (options) { return this.each(function () { jQuery(this).data('gridzy', new Gridzy(this, options)); }); }; } })(); new GridzyLayout({ name: 'justified', options: { autoFontSize: false, desiredHeight: 190, hideOnMissingImage: true, spaceBetween: 4, fillLastRow: true, // deprecated since v2.3.0 fillSingleRow: true, // deprecated since v2.3.0 lastRowAlign: 'justified', singleRowAlign: 'justified' }, handler: function(renderObject) { var // get layout options autoFontSize = !!renderObject.gridzyInstance.getOption('autoFontSize'), desiredHeight = parseInt(renderObject.gridzyInstance.getOption('desiredHeight')), hideOnMissingImage = !!renderObject.gridzyInstance.getOption('hideOnMissingImage'), spaceBetween = parseInt(renderObject.gridzyInstance.getOption('spaceBetween')), fillLastRow = !!renderObject.gridzyInstance.getOption('fillLastRow'), fillSingleRow = !!renderObject.gridzyInstance.getOption('fillSingleRow'), lastRowAlign = renderObject.gridzyInstance.getOption('lastRowAlign'), singleRowAlign = renderObject.gridzyInstance.getOption('singleRowAlign'), loopPos, loopPos2, items = renderObject.items, itemsVisible = [], itemsHidden = [], itemsLength, item, containerWidth = renderObject.dimensions.contentBox.width, optWidthAll = 0, optWidthRow, curWidthRow = 0, rows = [], rowsLength, row, rowLength, prevRow, displaySizeAllX = 0, displaySizeAllY = 0, containerWidthWithoutSpaces, fillRow; lastRowAlign = fillLastRow === false && lastRowAlign === 'justified' ? 'left' : lastRowAlign; singleRowAlign = fillSingleRow === false && singleRowAlign === 'justified' ? 'left' : singleRowAlign; // get optimal width of all items loopPos = items.length; while (loopPos--) { item = items[loopPos]; if (item.valid && item.matchFilter && !(hideOnMissingImage && item.imageError)) { itemsVisible.unshift(item); item.optWidth = desiredHeight * item.width / item.height; optWidthAll += item.optWidth; } else { itemsHidden.unshift(item); } } itemsLength = itemsVisible.length; // find the optimal length (pixels) of a row optWidthRow = lastRowAlign === 'justified' ? optWidthAll / (Math.round((optWidthAll + (spaceBetween * itemsLength)) / (containerWidth + spaceBetween)) || 1) : containerWidth; // attach items to rows loopPos = itemsLength; while (loopPos--) { item = itemsVisible[itemsLength - 1 - loopPos]; if (row === undefined || curWidthRow + (item.optWidth / 2) > optWidthRow * rows.length) { rows.push(row = { displaySizeY: 0, displayPosY: 0, ratioAll: 0, optWidth: 0, items: [] }); } curWidthRow += item.optWidth; row.optWidth += item.optWidth; row.ratioAll += item.aspectRatio; row.items.push(item); } // calculate sizes and positions of elements renderObject.height = 0; rowsLength = rows.length; prevRow = null; for (loopPos = 0; loopPos < rowsLength; loopPos++) { row = rows[loopPos]; displaySizeAllX = 0; displaySizeAllY = 0; rowLength = row.items.length; containerWidthWithoutSpaces = containerWidth - ((rowLength - 1) * spaceBetween); fillRow = rowsLength === 1 && singleRowAlign === 'justified' || rowsLength > 1 && lastRowAlign === 'justified' || loopPos < rowsLength - 1 || row.optWidth >= containerWidthWithoutSpaces; if (!fillRow && (rowsLength === 1 && singleRowAlign !== 'left' || rowsLength > 1 && lastRowAlign !== 'left')) { displaySizeAllX += Math.round((containerWidth - (row.optWidth + ((rowLength - 1) * spaceBetween))) * ((rowsLength > 1 ? lastRowAlign : singleRowAlign) === 'right' ? 1 : .5)); } for (loopPos2 = 0; loopPos2 < rowLength; loopPos2++) { item = row.items[loopPos2]; item.displayPosX = displaySizeAllX + (loopPos2 * spaceBetween); item.displaySizeX = fillRow ? Math.round((item.aspectRatio / row.ratioAll) * containerWidthWithoutSpaces) : Math.round(item.optWidth); displaySizeAllX += item.displaySizeX; if (fillRow && loopPos2 === rowLength - 1 && displaySizeAllX !== containerWidthWithoutSpaces) { item.displaySizeX += containerWidthWithoutSpaces - displaySizeAllX; } displaySizeAllY += item.displaySizeX / item.aspectRatio; } row.displaySizeY = Math.round(displaySizeAllY / rowLength); row.displayPosY = prevRow ? prevRow.displayPosY + prevRow.displaySizeY + spaceBetween : 0; for (loopPos2 = 0; loopPos2 < rowLength; loopPos2++) { item = row.items[loopPos2]; item.displaySizeY = row.displaySizeY; item.displayPosY = row.displayPosY; // set visible parameter and styles for current item item.visible = true; item.styleToApply = '' + 'position: absolute; ' + 'width: ' + item.displaySizeX + 'px; ' + 'height: ' + item.displaySizeY + 'px; ' + 'left: ' + (renderObject.dimensions.padding.left + item.displayPosX) + 'px; ' + 'top: ' + (renderObject.dimensions.padding.top + item.displayPosY) + 'px; ' + 'font-size: ' + (autoFontSize ? (item.displaySizeX * 100 / item.width) + '%' : '100%') + '; '; } renderObject.height += row.displaySizeY + (loopPos ? spaceBetween : 0); prevRow = row; } // set styles for container renderObject.styleToApply = 'height: ' + (renderObject.dimensions.actualBox.height - renderObject.dimensions.contentBox.height + renderObject.height) + 'px; '; // set visible parameter for elements which get hidden loopPos = itemsHidden.length; while (loopPos--) { itemsHidden[loopPos].visible = false; } } }); new GridzyLayout({ name: 'waterfall', options: { autoFontSize: false, desiredWidth: 250, hideOnMissingImage: true, horizontalOrder: false, spaceBetween: 4 }, handler: function(renderObject) { var // get layout options autoFontSize = !!renderObject.gridzyInstance.getOption('autoFontSize'), desiredWidth = parseInt(renderObject.gridzyInstance.getOption('desiredWidth')), hideOnMissingImage = !!renderObject.gridzyInstance.getOption('hideOnMissingImage'), horizontalOrder = !!renderObject.gridzyInstance.getOption('horizontalOrder'), spaceBetween = parseInt(renderObject.gridzyInstance.getOption('spaceBetween')), columnCount, availableContainerWidth, usedContainerWidth = 0, columnWidth, grid = [], currentColumn, largestColumnHeight = 0, loopPos, loopPos2, items = renderObject.items, itemsVisible = [], itemsHidden = [], itemsLength, containerWidth = renderObject.dimensions.contentBox.width, item; // separate visible versus hidden items loopPos = items.length; while (loopPos--) { item = items[loopPos]; if (item.valid && item.matchFilter && !(hideOnMissingImage && item.imageError)) { itemsVisible.push(item); } else { itemsHidden.push(item); } } itemsLength = itemsVisible.length; // get optimal count of columns columnCount = Math.round((containerWidth + spaceBetween) / (desiredWidth + spaceBetween)) || 1; columnCount = columnCount > itemsLength ? itemsLength : columnCount; availableContainerWidth = containerWidth - ((columnCount - 1) * spaceBetween); // get width of columns loopPos = columnCount; while (loopPos--) { columnWidth = availableContainerWidth - Math.round(availableContainerWidth * loopPos / columnCount) - usedContainerWidth; usedContainerWidth += columnWidth; grid.push({ index: grid.length, width: columnWidth, left: availableContainerWidth - usedContainerWidth + (loopPos * spaceBetween), height: 0 }); } //attach items to columns loopPos = itemsLength; while (loopPos--) { // get current column if (horizontalOrder) { currentColumn = grid[currentColumn && currentColumn.index > 0 ? currentColumn.index - 1 : columnCount - 1]; } else { loopPos2 = columnCount - 1; currentColumn = grid[loopPos2]; while (loopPos2--) { if (grid[loopPos2].height < currentColumn.height - 2) { // minus 2 to prevent to many jumps if there are many images with same size on top currentColumn = grid[loopPos2]; } } } // calculate size and position item = itemsVisible[loopPos]; item.displaySizeX = currentColumn.width; item.displaySizeY = Math.round((item.displaySizeX / item.width) * item.height); item.displayPosX = currentColumn.left; item.displayPosY = currentColumn.height + (currentColumn.height ? spaceBetween : 0); currentColumn.height += item.displaySizeY + (currentColumn.height ? spaceBetween : 0); if (largestColumnHeight < currentColumn.height) { largestColumnHeight = currentColumn.height; } // set visible parameter and styles for current item item.visible = true; item.styleToApply = '' + 'position: absolute; ' + 'width: ' + item.displaySizeX + 'px; ' + 'height: ' + item.displaySizeY + 'px; ' + 'left: ' + (renderObject.dimensions.padding.left + item.displayPosX) + 'px; ' + 'top: ' + (renderObject.dimensions.padding.top + item.displayPosY) + 'px; ' + 'font-size: ' + (autoFontSize ? (item.displaySizeX * 100 / item.width) + '%' : '100%') + '; '; } // set styles for container renderObject.styleToApply = 'height: ' + (renderObject.dimensions.actualBox.height - renderObject.dimensions.contentBox.height + largestColumnHeight) + 'px; '; // set visible parameter for elements which get hidden loopPos = itemsHidden.length; while (loopPos--) { itemsHidden[loopPos].visible = false; } } });