Thanks to Barney Carroll and Leo Horie with issue#351
Result: http://i.imgur.com/vbij1am.png
In the above example, the panel order is set at [2, 1, 0, 3, 4, 5, 6, 7, 8, 9], therefore, the panels are intentionally out of order in the screenshot.
Thanks to Barney Carroll and Leo Horie with issue#351
Result: http://i.imgur.com/vbij1am.png
In the above example, the panel order is set at [2, 1, 0, 3, 4, 5, 6, 7, 8, 9], therefore, the panels are intentionally out of order in the screenshot.
| import Grid from './grid.js'; | |
| var grid = new Grid(); | |
| m.module(document.getElementById("grid-container"), { | |
| controller: function () { | |
| grid.init(); | |
| // Generate a random number of panels (with random | |
| // number of child tiles to create variations in height) | |
| this.panels = randRange().map(function (idx) { | |
| return { tiles: randRange(), key: idx }; | |
| }); | |
| }, | |
| view: grid.view | |
| }); | |
| function randRange () { | |
| var rand = Math.floor( Math.random() * 8 ) + 3; | |
| var arr = []; | |
| while (rand--) { | |
| arr.unshift(rand); | |
| } | |
| return arr; | |
| } |
| // This is basically lodash with some dom helpers | |
| // and shortcut to velocity.js for animation | |
| import $ from 'helpers'; | |
| import * as m from 'mithril'; | |
| function GridViewModel () { | |
| // Panels are stored in a hash by key | |
| // panelOrder preserves the order in which they are | |
| // displayed and also allows display in a arbritrary order | |
| // for instance to allow user sorting | |
| var panels = {}; | |
| var panelOrder = [2, 1, 0, 3, 4, 5, 6, 7, 8, 9]; | |
| // The containing element | |
| var container; | |
| // Setup performs some calculations based on | |
| // container width then calls render | |
| function setup () { | |
| var containerW = container.width(); | |
| // Desired panel width (in this case, we are fixing | |
| // the panel width, but the columns could also be | |
| // by modifying the calculations below) | |
| var panelW = 300; | |
| var columns = Math.floor( containerW / panelW ); | |
| var excessW = containerW - (panelW * columns); | |
| var gutterW = Math.max(0, Math.min(10, excessW / (columns + 1))); | |
| var leftGutter = Math.max(0, (excessW - (gutterW * columns - 1)) / 2); | |
| // Trigger the render method | |
| render({ columns, leftGutter, panelW, gutterW }); | |
| // Bind event listeners here | |
| } | |
| function teardown () { | |
| // Unbind event listeners here | |
| } | |
| // Render | |
| // ----------------------------------------------------- | |
| // Calculates the appropriate placement of panels | |
| // Separated from the setup method so that the container | |
| // based calculations can be cached and render can be called | |
| // indendently (ex: when a single panel's height changes) | |
| function render ({ columns, leftGutter, panelW, gutterW }) { | |
| // Get total height of the all panels | |
| var totalH = $.list.reduce(panelOrder, function (total, key) { | |
| if (panels[key]) total += panels[key].el.height(); | |
| return total; | |
| }, 0); | |
| // Calculate average height of columns | |
| var averageH = totalH / columns; | |
| // Used to check if a panel should remain in the current | |
| // columnn or move to the next | |
| var remainingH = totalH, remainingC = columns - 1; | |
| // An array of panel positions, consists of objects as follows: | |
| // { key, col, x, y, h, w } | |
| var positions = []; | |
| // Interate over the panel order and position each panel | |
| $.list.each(panelOrder, function (key) { | |
| // Only add panel if it has been | |
| // registered using the vm.panel() method | |
| if (panels[key]) { | |
| var panel = panels[key]; | |
| // Get the panel before the current panel from the positions array | |
| // If this is a first panel, place it at [leftGutter, 0] coordinates | |
| // starting from column # 1 | |
| var prev = positions.length ? positions[ positions.length - 1] : { | |
| col: 1, x: leftGutter, y: 0, h: 0 | |
| }; | |
| // Variables for current panel's position | |
| var x, y, col = prev.col, h = panel.el.height(); | |
| // The current column's height (after we add this panel) | |
| var currentH = prev.y + prev.h + h + gutterW; | |
| // Keep the panel in the current column if any | |
| // of the following cases are true, otherwise, | |
| // move it to the next column | |
| switch (true) { | |
| case (prev.y === 0): break; | |
| case (currentH < averageH * 1.33): break; | |
| case (remainingC === 0): break; | |
| case (currentH < remainingH / remainingC): break; | |
| default: col = Math.min(++col, columns); | |
| } | |
| if (col > prev.col) { | |
| // If the panel is placed in a new column | |
| remainingC = remainingC - 1; | |
| // Calculate new column's left attribute in pixels | |
| x = prev.x + panelW + gutterW; | |
| // Position at the top of the column | |
| y = gutterW; | |
| } else { | |
| // If placing the panel in the same column as previous panel, | |
| // the left value is the same as the previous panel | |
| x = prev.x; | |
| // Position just below the previous column | |
| y = prev.y + prev.h + gutterW; | |
| } | |
| // Remove current panel's height from the remaining height | |
| remainingH = remainingH - h; | |
| // Add current panel to the positions array | |
| positions.push({ key, col, x, y, h, w: panelW }); | |
| // Set the left position and width of the | |
| // current panel (we don't want it animate) | |
| panel.el.css({ | |
| left: x + 'px', | |
| width: (panelW - (gutterW * 2)) + 'px' | |
| }); | |
| // Slide each panel down from top of column | |
| // to its new y-position in the column | |
| panel.el.animate({ top: y }, { | |
| duration: 300, | |
| easing: [150, 20] | |
| }); | |
| } | |
| }); | |
| } | |
| return { | |
| container: function registerContainer () { | |
| return function (element, init, ctx) { | |
| if (!init) { | |
| container = $.el(element); | |
| setup(); | |
| ctx.onunload = teardown; | |
| } | |
| }; | |
| }, | |
| panel: function registerPanel ( panel ) { | |
| // Add panel to the panel map | |
| var key = panel.key; | |
| panels[key] = panel; | |
| return function (element, init, ctx) { | |
| if (!init) { | |
| panel.el = $.el(element); | |
| ctx.onunload = function unregister () { | |
| // Remove panel from panel map | |
| panels[key] = null; | |
| }; | |
| } | |
| }; | |
| } | |
| }; | |
| } | |
| export default function Grid () { | |
| if (!(this instanceof Grid)) return new Grid(); | |
| var grid = {}; | |
| grid.init = function () { | |
| grid.vm = new GridViewModel(); | |
| }; | |
| grid.view = function (ctrl) { | |
| return m('.viewport', { config: grid.vm.container() }, [ | |
| ctrl.panels.map(function (panel) { | |
| return m('.egrid-panel', { key: panel.key, config: grid.vm.panel(panel) }, [ | |
| m('.egrid-title', panel.key), | |
| // Add tiles to the panel | |
| m('ul', panel.tiles.map(tile => m('.egird-tile', tile))) | |
| ]); | |
| }) | |
| ]); | |
| }; | |
| return grid; | |
| } |