magic-grid
Version:
Super lightweight javascript library for dynamic grid layouts.
271 lines (223 loc) • 6.1 kB
JavaScript
/**
* @author emmanuelolaojo
* @since 11/10/18
*
* The MagicGrid class is an
* implementation of a flexible
* grid layout.
*/
const EventEmitter = require("./event-emitter.js");
const {checkParams, getMin} = require("./utils.js");
const {POSITIONING_COMPLETE_EVENT, REPOSITIONING_DELAY} = require("./constant.js");
class MagicGrid extends EventEmitter{
/**
* Initializes the necessary variables
* for a magic grid.
*
* @param config - configuration object
*/
constructor (config) {
super();
checkParams(config);
if (config.container instanceof HTMLElement) {
this.container = config.container;
this.containerClass = config.container.className;
}
else {
this.containerClass = config.container;
this.container = document.querySelector(config.container);
}
this.static = config.static || false;
this.size = config.items;
this.gutter = config.gutter;
this.maxColumns = config.maxColumns || false;
this.useMin = config.useMin || false;
this.useTransform = config.useTransform;
this.animate = config.animate || false;
this.center = config.center;
this.styledItems = new Set();
this.resizeObserver = null;
this.isPositioning = false;
}
/**
* Set a new container. Useful in cases where
* the container reference changes for any reason.
*
* @param container {HTMLElement}
*/
setContainer(container){
const previousContainer = this.container;
this.container = container;
this.resizeObserver.unobserve(previousContainer);
this.resizeObserver.observe(container);
}
/**
* Initializes styles
*
* @private
*/
initStyles () {
if (!this.ready()) return;
this.container.style.position = "relative";
const items = this.items();
for (let i = 0; i < items.length; i++) {
if (this.styledItems.has(items[i])) continue;
let style = items[i].style;
style.position = "absolute";
if (this.animate) {
style.transition = `${this.useTransform ? "transform" : "top, left"} 0.2s ease`;
}
this.styledItems.add(items[i]);
}
}
/**
* Gets a collection of all items in a grid.
*
* @return {HTMLCollection}
* @private
*/
items () {
return this.container.children;
}
/**
* Calculates the width of a column.
*
* @return width of a column in the grid
* @private
*/
colWidth () {
return this.items()[0].getBoundingClientRect().width + this.gutter;
}
/**
* Initializes an array of empty columns
* and calculates the leftover whitespace.
*
* @return {{cols: Array, wSpace: number}}
* @private
*/
setup () {
let width = this.container.getBoundingClientRect().width;
let colWidth = this.colWidth();
let numCols = Math.floor(width/colWidth) || 1;
let cols = [];
if (this.maxColumns && numCols > this.maxColumns) {
numCols = this.maxColumns;
}
for (let i = 0; i < numCols; i++) {
cols[i] = {height: 0, index: i};
}
let wSpace = width - numCols * colWidth + this.gutter;
return {cols, wSpace};
}
/**
* Gets the next available column.
*
* @param cols list of columns
* @param i index of dom element
*
* @return {*} next available column
* @private
*/
nextCol (cols, i) {
if (this.useMin) {
return getMin(cols);
}
return cols[i % cols.length];
}
/**
* Positions each item in the grid, based
* on their corresponding column's height
* and index then stretches the container to
* the height of the grid.
*/
positionItems () {
if(this.isPositioning){
return;
}
this.isPositioning = true;
let { cols, wSpace } = this.setup();
let maxHeight = 0;
let colWidth = this.colWidth();
let items = this.items();
wSpace = this.center ? Math.floor(wSpace / 2) : 0;
this.initStyles();
for (let i = 0; i < items.length; i++) {
let col = this.nextCol(cols, i);
let item = items[i];
let topGutter = col.height ? this.gutter : 0;
let left = col.index * colWidth + wSpace + "px";
let top = col.height + topGutter + "px";
if(this.useTransform){
item.style.transform = `translate(${left}, ${top})`;
}
else{
item.style.top = top;
item.style.left = left;
}
col.height += item.getBoundingClientRect().height + topGutter;
if(col.height > maxHeight){
maxHeight = col.height;
}
}
this.container.style.height = maxHeight + this.gutter + "px";
this.isPositioning = false;
this.emit(POSITIONING_COMPLETE_EVENT);
}
/**
* Checks if every item has been loaded
* in the dom.
*
* @return {Boolean} true if every item is present
*/
ready () {
if (this.static) return true;
return this.items().length >= this.size;
}
/**
* Periodically checks that all items
* have been loaded in the dom. Calls
* this.listen() once all the items are
* present.
*
* @private
*/
getReady () {
let interval = setInterval(() => {
this.container = document.querySelector(this.containerClass);
if (this.ready()) {
clearInterval(interval);
this.listen();
}
}, 100);
}
observeContainerResize() {
if (this.resizeObserver) return;
this.resizeObserver = new ResizeObserver(() => {
setTimeout(() => {
this.positionItems();
}, REPOSITIONING_DELAY);
});
this.resizeObserver.observe(this.container);
}
/**
* Positions all the items and
* repositions them whenever the
* window size changes.
*/
listen () {
if (this.ready()) {
window.addEventListener("resize", () => {
setTimeout(() => {
this.positionItems();
}, REPOSITIONING_DELAY);
});
this.observeContainerResize();
this.positionItems();
}
else this.getReady();
}
onPositionComplete(callback) {
return this.addListener(POSITIONING_COMPLETE_EVENT, callback);
}
}
module.exports = MagicGrid;