/* * WinJS Contrib v2.1.0.6 * licensed under MIT license (see http://opensource.org/licenses/MIT) * sources available at https://github.com/gleborgne/winjscontrib */ (function () { "use strict"; /** * Object representing a layout configuration for the grid control * @typedef {Object} WinJSContrib.UI.GridControlLayout * @property {string} layout layout algorythm to apply (horizontal | vertical | flexhorizontal | flexvertical | hbloc * @property {number} cellSpace space between grid cells * @property {number} cellWidth width of grid cells * @property {number} cellHeight height of grid cells * @property {number} itemsPerColumn number of cells per column if using a layout with a fixed number of cells * @property {number} itemsPerRow number of cells per row if using a layout with a fixed number of cells * @example * { * layout: 'horizontal', * itemsPerColumn: (options.itemsPerColumn) ? options.itemsPerColumn : undefined, * itemsPerRow: (options.itemsPerRow) ? options.itemsPerRow : undefined, * cellSpace: 10, * cellWidth: (options.cellWidth) ? options.cellWidth : undefined, * cellHeight: (options.cellHeight) ? options.cellHeight : undefined, * } */ WinJS.Namespace.define("WinJSContrib.UI", { GridControl: WinJS.Class.define( /** * @classdesc * Control that layout it's children with different algorythms. Used with {@link WinJSContrib.UI.Hub}, The Grid could rely on multipass rendering to optimize large hub pages load. * @class WinJSContrib.UI.GridControl * @param {HTMLElement} element DOM element containing the control * @param {Object} options */ function GridControl(element, options) { var grid = this; options = options || {}; grid.element = element; grid.element.className = grid.element.className + ' mcn-grid-ctrl mcn-layout-ctrl win-disposable'; grid.element.winControl = grid; /** * multipass renderer for the grid * @field * @type WinJSContrib.UI.MultiPassRenderer */ grid.renderer = new WinJSContrib.UI.MultiPassRenderer(grid.element, { multipass: options.multipass, itemClassName: options.itemClassName, itemTemplate: options.itemTemplate, itemInvoked: options.itemInvoked, }); grid.layouts = options.layouts; /** * default layout definitions for the grid * @field * @type WinJSContrib.UI.GridControlLayout */ grid.defaultLayout = { layout: options.layout || 'none', itemsPerColumn: (options.itemsPerColumn) ? options.itemsPerColumn : undefined, itemsPerRow: (options.itemsPerRow) ? options.itemsPerRow : undefined, cellSpace: (options.cellSpace) ? options.cellSpace : 10, cellWidth: (options.cellWidth) ? options.cellWidth : undefined, cellHeight: (options.cellHeight) ? options.cellHeight : undefined, }; /** * indicate if grid accept layout event from the page (if you use WinJS Contrib page events) * @field * @type boolean */ grid.autolayout = options.autolayout || true; var parent = WinJSContrib.Utils.getScopeControl(grid.element); if (parent && parent.pageLifeCycle) { parent.pageLifeCycle.steps.layout.attach(function () { if (grid.autolayout) { grid.layout(); } }); } else if (parent && parent.elementReady) { parent.elementReady.then(function () { parent.readyComplete.then(function () { if (grid.autolayout) { grid.layout(); } }); }); } }, /** * @lends WinJSContrib.UI.GridControl.prototype */ { /** * scroll element containing the grid. Required for multi pass rendering * @field * @type HTMLElement */ scrollContainer: { get: function () { return this.renderer.scrollContainer; }, set: function (val) { this.renderer.scrollContainer = val; } }, /** * indicate if grid layout itself according to the page lifecycle (default to true) * @field * @type boolean */ autolayout: { get: function () { return this._autolayout; }, set: function (val) { this._autolayout = val; } }, /** * layout definitions for the grid. It's an object containing several grid layout options. See {@link WinJSContrib.UI.GridControlLayout} * @field * @type Object */ layouts: { get: function () { return this.gridLayouts; }, set: function (val) { this.gridLayouts = val; } }, /** * indicate the kind of multipass treatment * @field * @type string */ multipass: { get: function () { return this.renderer.multipass; }, set: function (val) { this.renderer.multipass = val; } }, /** * callback triggered when clicking on an item * @field * @type HTMLElement */ itemInvoked: { get: function () { return this.renderer.itemInvoked; }, set: function (val) { this.renderer.itemInvoked = val; } }, /** * item template (WinJS Template or template function) * @field * @type Object */ itemTemplate: { get: function () { return this.renderer.itemTemplate; }, set: function (val) { this.renderer.itemTemplate = val; } }, /** * css class added on item's placeholder * @field * @type Object */ itemClassName: { get: function () { return this.renderer.itemClassName; }, set: function (val) { this.renderer.itemClassName = val; } }, /** * items to render * @field * @type Object */ items: { get: function () { return; }, set: function (val) { if (val && val.length) this.prepareItems(val); } }, /** * render HTML for items * @param {Array} items array of items to render * @param {Object} renderOptions */ prepareItems: function (items, renderOptions) { if (!this.element) return; var parent = WinJSContrib.Utils.getParentControlByClass('mcn-layout-ctrl', this.element); var parentMultipass = undefined; if (!this.renderer.multipass && parent && parent.multipass) { this.renderer.multipass = parent.multipass; } this.renderer.prepareItems(items, renderOptions); }, /** * force items content to render */ renderItemsContent: function () { if (!this.renderer) return; this.renderer.renderItemsContent(); }, resetElement: function (elt, isItem) { var style = elt.style; if (isItem && style.position) style.position = ''; if (!isItem && style.display) style.display = ''; if (style.width) style.width = ''; if (style.height) style.height = ''; if (style.minWidth) style.minWidth = ''; if (style.minHeight) style.minHeight = ''; if (style.left) style.left = ''; if (style.top) style.top = ''; }, clearContent: function () { var ctrl = this; ctrl.renderer.clear(); }, /** * Clear all layout and position styles on items */ clearLayout: function () { var ctrl = this; ctrl.resetElement(ctrl.element, false); if (ctrl.element.children.length) { for (var i = 0, l = ctrl.element.children.length; i < l; i++) { ctrl.resetElement(ctrl.element.children[i], true); } } }, fill: function (matrix, x, y, w, h) { if (matrix.length < x + w) { for (var i = matrix.length ; i < x + w ; i++) { matrix.push([]); } } for (var i = x ; i < x + w ; i++) { var col = matrix[i]; if (col.length < y + h) { for (var j = col.length ; j < y + h ; j++) { col.push(false); } } for (var j = y ; j < y + h ; j++) { col[j] = true; } } }, fit: function (matrix, x, y, w, h, maxW, maxH) { //items to big if (maxH && h > maxH && y === 0) return true; if (maxW && w > maxW && x === 0) return true; //overflow grid capacity if (maxW && (x + w) > maxW) return false; if (maxH && (y + h) > maxH) return false; for (var i = x ; i < x + w ; i++) { var col = matrix[i]; if (col) { for (var j = y ; j < y + h ; j++) { if (col[j] === true) return false; } } } return true; }, firstFit: function (matrix, w, h, maxH, numItems) { var ctrl = this; for (var i = 0 ; i < numItems * maxH * 2 ; i++) { var col = matrix[i]; if (col) { for (var j = 0 ; j < maxH ; j++) { if (col[j] !== true && ctrl.fit(matrix, i, j, w, h, undefined, maxH)) { return { x: i, y: j }; } } } else { return { x: i, y: 0 }; } } return undefined; }, visibleChilds: function () { var ctrl = this; var res = []; for (var i = 0, l = ctrl.element.children.length; i < l ; i++) { var item = ctrl.element.children[i]; var st = window.getComputedStyle(item); if (st.display != 'none' && st.visibility != 'hidden') { res.push(item); } } return res; }, /** * Layouts algorythm implementations */ GridLayoutsImpl: { none: function () { }, flexhorizontal: function () { var ctrl = this; ctrl.renderer.orientation = 'horizontal'; ctrl.element.style.position = 'relative'; ctrl.element.style.display = 'flex'; ctrl.element.style.flexFlow = 'column wrap'; ctrl.element.style.alignContent = 'flex-start'; //ctrl.element.style.alignContent = 'flex-start'; if (ctrl.element.style.width) ctrl.element.style.width = ''; ctrl.element.style.height = ''; if (ctrl.element.clientHeight) ctrl.element.style.height = ctrl.element.clientHeight + 'px'; else ctrl.element.style.height = ''; var _itemsPerColumn = Math.floor(ctrl.element.clientHeight / (ctrl.data.cellHeight + ctrl.data.cellSpace)); if (_itemsPerColumn) { var visibleitems = ctrl.visibleChilds(); var columns = Math.ceil(visibleitems.length / _itemsPerColumn); ctrl.element.style.minWidth = ((ctrl.data.cellWidth + ctrl.data.cellSpace) * columns) + 'px'; } }, flexvertical: function () { var ctrl = this; ctrl.renderer.orientation = 'vertical'; ctrl.element.style.position = 'relative'; ctrl.element.style.display = 'flex'; ctrl.element.style.flexFlow = 'row wrap'; ctrl.element.style.alignContent = 'flex-start'; ctrl.element.style.width = ''; if (ctrl.element.clientWidth) ctrl.element.style.width = ctrl.element.clientWidth + 'px'; else ctrl.element.style.width = ''; if (ctrl.element.style.height) ctrl.element.style.height = ''; }, hbloc: function () { var ctrl = this; ctrl.renderer.orientation = 'horizontal'; ctrl.element.style.position = 'relative'; ctrl.element.style.height = ''; var _containerH = ctrl.element.clientHeight; if (!_containerH) return; var cellW = ctrl.data.cellWidth; var space = ctrl.data.cellSpace; var colCount = 1; var colOffset = 0; var topOffset = 0; var childs = ctrl.visibleChilds(); childs.forEach(function (elt) { if (elt.style.position != 'absolute') elt.style.position = 'absolute'; var eltH = elt.clientHeight; if (topOffset + eltH > _containerH) { colCount++; colOffset = colOffset + space + cellW; topOffset = 0; } if (elt.style.left != colOffset + 'px') elt.style.left = colOffset + 'px'; if (elt.style.top != topOffset + 'px') elt.style.top = topOffset + 'px'; topOffset += eltH; }); colOffset = colOffset + cellW; if (ctrl.element.style.width != colOffset + 'px') ctrl.element.style.width = colOffset + 'px'; }, horizontal: function () { var ctrl = this; ctrl.renderer.orientation = 'horizontal'; ctrl.element.style.position = 'relative'; ctrl.element.style.height = ''; var _containerH = ctrl.element.clientHeight; if (!_containerH) return; var _itemsPerColumn = Math.floor(_containerH / (ctrl.data.cellHeight + ctrl.data.cellSpace)); if (_itemsPerColumn <= 0) _itemsPerColumn = 1; var cellW = ctrl.data.cellWidth; var cellH = ctrl.data.cellHeight; var space = ctrl.data.cellSpace; var aspectRatio = cellW / cellH; var ratioW = 1; var ratioH = 1; if (ctrl.data.itemsPerColumn) { _itemsPerColumn = ctrl.data.itemsPerColumn; cellH = ((_containerH - ((_itemsPerColumn - 1) * space)) / _itemsPerColumn) >> 0; cellW = (ctrl.data.cellWidth * cellH / ctrl.data.cellHeight) >> 0; ratioW = cellW / ctrl.data.cellWidth; ratioH = cellH / ctrl.data.cellHeight; } var gridCellsMatrix = [[]]; var childs = ctrl.visibleChilds(); childs.forEach(function (elt) { elt.style.position = 'absolute'; var eltW = elt.offsetWidth * ratioW; var eltH = elt.offsetHeight * ratioH; var eltColumns = (eltW / cellW) >> 0; var eltRows = (eltH / cellH) >> 0; var pos = ctrl.firstFit(gridCellsMatrix, eltColumns, eltRows, _itemsPerColumn, ctrl.element.children.length); if (!pos) pos = { x: 0, y: 0 }; ctrl.fill(gridCellsMatrix, pos.x, pos.y, eltColumns, eltRows); var left = pos.x * (cellW + space); var top = pos.y * (cellH + space); var w = (eltColumns * cellW + ((eltColumns - 1) * space)); var h = (eltRows * cellH + ((eltRows - 1) * space)); elt.style.left = left + 'px'; elt.style.top = top + 'px'; elt.style.width = w + 'px'; elt.style.height = h + 'px'; }); var elementWidth = gridCellsMatrix.length * (cellW + space); ctrl.element.style.width = elementWidth + 'px'; }, vertical: function (plugin) { var ctrl = this; ctrl.renderer.orientation = 'vertical'; ctrl.element.style.width = ''; ctrl.element.style.position = 'relative'; //Be aware that in this case, we invert the matrix to crawl data in lines var _containerW = ctrl.element.clientWidth; if (!_containerW) return; var _itemsPerLine = Math.floor(_containerW / (ctrl.data.cellWidth + ctrl.data.cellSpace)); if (_itemsPerLine <= 0) _itemsPerLine = 1; var cellW = ctrl.data.cellWidth; var cellH = ctrl.data.cellHeight; var space = ctrl.data.cellSpace; var aspectRatio = cellW / cellH; var ratioW = 1; var ratioH = 1; if (ctrl.data.itemsPerRow) { _itemsPerLine = ctrl.data.itemsPerRow; cellW = ((_containerW - ((_itemsPerLine - 1) * space)) / _itemsPerLine) >> 0; cellH = (ctrl.data.cellHeight * cellW / ctrl.data.cellWidth) >> 0; ratioW = cellW / ctrl.data.cellWidth; ratioH = cellH / ctrl.data.cellHeight; } var gridCellsMatrix = [[]]; var childs = ctrl.visibleChilds(); childs.forEach(function (elt) { elt.style.position = 'absolute'; var eltW = elt.offsetWidth * ratioW; var eltH = elt.offsetHeight * ratioH; var eltColumns = (eltW / cellW) >> 0; var eltRows = (eltH / cellH) >> 0; var pos = ctrl.firstFit(gridCellsMatrix, eltRows, eltColumns, _itemsPerLine, ctrl.element.children.length); //if (!pos) // return; ctrl.fill(gridCellsMatrix, pos.x, pos.y, eltRows, eltColumns); var left = pos.y * (cellW + space); var top = pos.x * (cellH + space); elt.style.left = left + 'px'; elt.style.top = top + 'px'; elt.style.width = (eltColumns * cellW + ((eltColumns - 1) * space)) + 'px'; elt.style.height = (eltRows * cellH + ((eltRows - 1) * space)) + 'px'; }); var elementHeight = gridCellsMatrix.length * (cellH + space); ctrl.element.style.height = elementHeight + 'px'; }, }, /** * layout content items */ layout: function () { var ctrl = this; if (!ctrl.element) return; var oldlayout = ctrl.data; ctrl.data = ctrl.getLayout(); if (ctrl.data) { //if (ctrl.data == oldlayout && ctrl.data.applyed) // return; ctrl.data.cellSpace = (ctrl.data.cellSpace != undefined ? ctrl.data.cellSpace : (ctrl.defaultLayout.cellSpace != undefined ? ctrl.defaultLayout.cellSpace : 10)); ctrl.data.cellWidth = ctrl.data.cellWidth || ctrl.defaultLayout.cellWidth || 0; ctrl.data.cellHeight = ctrl.data.cellHeight || ctrl.defaultLayout.cellHeight || 0; //if cell dimensions are not defined, take it from last child if (!ctrl.data.cellWidth || !ctrl.data.cellHeight) { if (ctrl.element && ctrl.element.childNodes && ctrl.element.children.length > 0) { var childs = ctrl.visibleChilds(); if (childs && childs.length) { var firstChild = childs[0]; var w = firstChild.clientWidth; var h = firstChild.clientHeight; var l = childs.length; if (l > 10) l = 10; for (var i = 0, l = childs.length; i < l ; i++) { var item = childs[i]; if (w == 0 || item.clientWidth < w) { w = item.clientWidth; } if (h == 0 || item.clientHeight < h) { h = item.clientHeight; } } ctrl.data.cellWidth = w; ctrl.data.cellHeight = h; } } } var layoutChanged = !oldlayout || ctrl.data.layout !== oldlayout.layout; if (ctrl.data.layout) { var layoutfunc = ctrl.GridLayoutsImpl[ctrl.data.layout.toLowerCase()]; if (layoutfunc) { if (layoutChanged) ctrl.changeLayout(); layoutfunc.bind(ctrl)(layoutChanged); ctrl.data.applyed = true; } } ctrl.renderer.checkRendering(); } }, changeLayout: function () { var ctrl = this; ctrl.clearLayout(); ctrl.renderer.updateLayout(); }, /** * update grid layout */ updateLayout: function (element, viewState, lastViewState) { var ctrl = this; if (!ctrl.element) return; ctrl.layout(); }, /** * get layout applicable to the current context */ getLayout: function () { var ctrl = this; var matchingLayout = undefined; if (ctrl.gridLayouts) { for (var name in ctrl.gridLayouts) { var layout = ctrl.gridLayouts[name]; if (ctrl.gridLayouts.hasOwnProperty(name)) { if (layout.query) { var mq = window.matchMedia(layout.query); if (mq.matches) { matchingLayout = layout; } } else if (!matchingLayout) { matchingLayout = layout; } } else if (!matchingLayout) { matchingLayout = layout; } } } if (!matchingLayout) { matchingLayout = ctrl.defaultLayout; } return JSON.parse(JSON.stringify(matchingLayout)); }, /** * Release grid resources */ dispose: function () { this.element = null; this.renderer.dispose(); if (WinJS.Utilities.disposeSubTree) WinJS.Utilities.disposeSubTree(this.element); } }) }); if (WinJSContrib.UI.WebComponents) { WinJSContrib.UI.WebComponents.register('mcn-grid', WinJSContrib.UI.GridControl, { properties: ['multipass', 'autolayout', 'layouts', 'itemInvoked', 'itemTemplate', 'itemClassName', 'items'] }); } })();