Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@appnest/masonry-layout

Package Overview
Dependencies
Maintainers
2
Versions
31
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@appnest/masonry-layout - npm Package Compare versions

Comparing version 1.0.2 to 2.0.0

custom-elements.json

76

masonry-helpers.d.ts

@@ -1,30 +0,19 @@

export declare const MIN_HEIGHT_PRIORITY_SLACK_PX = 10;
export declare const DISTRIBUTED_ATTR = "data-masonry-distributed";
export declare const DEFAULT_COLS: MasonryCols;
export declare const DEFAULT_MAX_COL_WIDTH = 400;
export declare const DEFAULT_GAP = 24;
export declare const DEFAULT_COLS = "auto";
export declare const DEFAULT_DEBOUNCE_MS = 300;
export declare const DEFAULT_GAP_PX = 24;
export declare const ELEMENT_NODE_TYPE = 1;
export declare type ColHeightMap = number[];
export declare type MasonryCols = number | "auto";
export declare type MasonryItemLayout = {
top: number;
left: number;
col: number;
colWidth: number;
export declare type MasonryItemCachedRead = {
height: number;
};
/**
* Returns an empty col height map.
* @param colCount
* Returns a number attribute from an element.
* @param $elem
* @param name
* @param defaultValue
*/
export declare function createEmptyColHeightMap(colCount: number): ColHeightMap;
export declare function getNumberAttribute<T>($elem: HTMLElement, name: string, defaultValue: T): number | T;
/**
* Returns the width of a col.
* The width of the column will be the total width of the element divided by the amount
* of columns, subtracted with the total amount of gap.
* @param totalWidth
* @param gap
* @param colCount
*/
export declare function getColWidth(totalWidth: number, gap: number, colCount: number): number;
/**
* Returns the amount of cols that the masonry grid should have.

@@ -37,20 +26,2 @@ * @param totalWidth

/**
* Sets the height of the component to the height of the tallest col.
* @param colHeightMap
*/
export declare function tallestColHeight(colHeightMap: ColHeightMap): number;
/**
* Computes the position of an item.
* @param i
* @param width
* @param gap
* @param colIndex
* @param colCount
* @param colHeightMap
*/
export declare function itemPosition(i: number, width: number, gap: number, colIndex: number, colCount: number, colHeightMap: ColHeightMap): {
top: number;
left: number;
};
/**
* Debounces a function.

@@ -63,26 +34,5 @@ * @param cb

/**
* Returns the shortest col from the col height map. When finding the shortest col we need to subtract
* a small offset to account for variations in the rounding.
* @param colHeightMap
* Returns the index of the column with the smallest height.
* @param colHeights
*/
export declare function getShortestCol(colHeightMap: ColHeightMap): number;
/**
* Sets a boolean attribute on an element.
* @param $elem
* @param name
* @param value
*/
export declare function setBooleanAttribute($elem: HTMLElement, name: string, value: boolean): void;
/**
* Returns a boolean attribute from an element.
* @param $elem
* @param name
*/
export declare function getBooleanAttribute($elem: HTMLElement, name: string): boolean;
/**
* Returns a number attribute from an element.
* @param $elem
* @param name
* @param defaultValue
*/
export declare function getNumberAttribute<T>($elem: HTMLElement, name: string, defaultValue: T): number | T;
export declare function findSmallestColIndex(colHeights: ColHeightMap): number;

@@ -1,27 +0,19 @@

export const MIN_HEIGHT_PRIORITY_SLACK_PX = 10;
export const DISTRIBUTED_ATTR = "data-masonry-distributed";
export const DEFAULT_MAX_COL_WIDTH = 400;
export const DEFAULT_COLS = "auto";
export const DEFAULT_MAX_COL_WIDTH = 400;
export const DEFAULT_GAP = 24;
export const DEFAULT_DEBOUNCE_MS = 300;
const DEBOUNCE_MAP = {};
export const DEFAULT_GAP_PX = 24;
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
export const ELEMENT_NODE_TYPE = 1;
const DEBOUNCE_MAP = new Map();
/**
* Returns an empty col height map.
* @param colCount
* Returns a number attribute from an element.
* @param $elem
* @param name
* @param defaultValue
*/
export function createEmptyColHeightMap(colCount) {
return Object.assign([...(new Array(colCount))].map(() => 0));
export function getNumberAttribute($elem, name, defaultValue) {
const value = parseFloat($elem.getAttribute(name) || "");
return isNaN(value) ? defaultValue : value;
}
/**
* Returns the width of a col.
* The width of the column will be the total width of the element divided by the amount
* of columns, subtracted with the total amount of gap.
* @param totalWidth
* @param gap
* @param colCount
*/
export function getColWidth(totalWidth, gap, colCount) {
return (totalWidth / colCount) - ((gap * (colCount - 1)) / colCount);
}
/**
* Returns the amount of cols that the masonry grid should have.

@@ -36,28 +28,2 @@ * @param totalWidth

/**
* Sets the height of the component to the height of the tallest col.
* @param colHeightMap
*/
export function tallestColHeight(colHeightMap) {
return Object.values(colHeightMap).reduce((acc, height) => Math.max(acc, height), 0);
}
/**
* Computes the position of an item.
* @param i
* @param width
* @param gap
* @param colIndex
* @param colCount
* @param colHeightMap
*/
export function itemPosition(i, width, gap, colIndex, colCount, colHeightMap) {
// Compute the left offset of the item. We find the left offset by first computing
// the width of the columns added together with the gap before the current element.
const left = (width * colIndex) + (gap * colIndex);
// If the element in the first row we need to treat it different by not adding before it.
const isFirstInRow = i < colCount;
// The top offset will be the height of the chosen column added together with the gap.
const top = (colHeightMap[colIndex] || 0) + (isFirstInRow ? 0 : gap);
return { top, left };
}
/**
* Debounces a function.

@@ -69,47 +35,22 @@ * @param cb

export function debounce(cb, ms, id) {
const existingTimeout = DEBOUNCE_MAP[id];
if (existingTimeout)
const existingTimeout = DEBOUNCE_MAP.get(id);
if (existingTimeout != null)
window.clearTimeout(existingTimeout);
DEBOUNCE_MAP[id] = window.setTimeout(cb, ms);
DEBOUNCE_MAP.set(id, window.setTimeout(cb, ms));
}
/**
* Returns the shortest col from the col height map. When finding the shortest col we need to subtract
* a small offset to account for variations in the rounding.
* @param colHeightMap
* Returns the index of the column with the smallest height.
* @param colHeights
*/
export function getShortestCol(colHeightMap) {
return colHeightMap.map((height, col) => [col, height]).reduce((shortestColInfo, info) => shortestColInfo[1] - MIN_HEIGHT_PRIORITY_SLACK_PX <= info[1] ? shortestColInfo : info, [0, Number.POSITIVE_INFINITY])[0];
export function findSmallestColIndex(colHeights) {
let smallestIndex = 0;
let smallestHeight = Infinity;
colHeights.forEach((height, i) => {
if (height < smallestHeight) {
smallestHeight = height;
smallestIndex = i;
}
});
return smallestIndex;
}
/**
* Sets a boolean attribute on an element.
* @param $elem
* @param name
* @param value
*/
export function setBooleanAttribute($elem, name, value) {
if (value) {
$elem.setAttribute(name, "");
}
else {
$elem.removeAttribute(name);
}
}
/**
* Returns a boolean attribute from an element.
* @param $elem
* @param name
*/
export function getBooleanAttribute($elem, name) {
return $elem.hasAttribute(name);
}
/**
* Returns a number attribute from an element.
* @param $elem
* @param name
* @param defaultValue
*/
export function getNumberAttribute($elem, name, defaultValue) {
const value = parseFloat($elem.getAttribute(name) || "");
return isNaN(value) ? defaultValue : value;
}
//# sourceMappingURL=masonry-helpers.js.map

@@ -1,2 +0,11 @@

import { MasonryCols } from "./masonry-helpers";
import { MasonryItemCachedRead } from "./masonry-helpers";
declare type ResizeObserverEntries = {
contentRect: {
width: number;
height: number;
};
}[];
/**
* Typings required for ShadyCSS.
*/
declare global {

@@ -11,11 +20,8 @@ interface Window {

* @example <masonry-layout><div class="item"></div><div class="item"></div></masonry-layout>
* @csspart column - Each column of the masonry layout.
* @csspart column-index - The specific column at the given index (eg. column-0 would target the first column and so on))
* @slot - Items that should be distributed in the layout.
* @cssprop --masonry-layout-item-transition - Transition of an item.
*/
export declare class MasonryLayout extends HTMLElement {
static get observedAttributes(): string[];
private currentColHeightMap;
private ro;
private cancelNextResizeEvent;
private layoutCache;
/**

@@ -29,8 +35,8 @@ * The maximum width of each column if cols are set to auto.

/**
* Whether the items should be locked in their columns after the have been placed.
* @attr collock
* The amount of columns.
* @attr cols
* @param v
*/
set colLock(v: boolean);
get colLock(): boolean;
set cols(v: string | number);
get cols(): number | "auto";
/**

@@ -44,16 +50,2 @@ * The gap in pixels between the columns.

/**
* The amount of columns.
* @attr cols
* @param v
*/
set cols(v: MasonryCols);
get cols(): MasonryCols;
/**
* Whether the items should have a transition.
* @attr transition
* @param v
*/
set transition(v: boolean);
get transition(): boolean;
/**
* The ms of debounce when the element resizes.

@@ -66,44 +58,56 @@ * @attr debounce

/**
* The slot element.
* The column elements.
*/
private get $slot();
private get $columns();
private debounceId;
private cachedReads;
private $unsetElementsSlot;
private ro;
/**
* All of the elements in the slot that are an Node.ELEMENT_NODE.
* https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
* Attach the shadow DOM.
*/
private get $items();
constructor();
/**
* Attaches the listeners when the element is added to the DOM.
* Hook up event listeners when added to the DOM.
*/
connectedCallback(): void;
/**
* Removes listeners when the element is removed from the DOM.
* Remove event listeners when removed from the DOM.
*/
disconnectedCallback(): void;
/**
* Updates the layout when the observed attributes changes.
* Updates the layout when one of the observed attributes changes.
*/
attributeChangedCallback(): void;
attributeChangedCallback(name: string): void;
/**
* Attaches all listeners to the element.
*
*/
private attachListeners;
onSlotChange(): void;
/**
* Detaches all listeners from the element.
* Each time the element resizes we need to schedule a layout
* if the amount available columns has has changed.
* @param entries
*/
private detachListeners;
onResize(entries?: ResizeObserverEntries): void;
/**
* Called when the element resizes and schedules a layout.
* Render X amount of columns.
* @param colCount
*/
private didResize;
renderCols(colCount: number): void;
/**
* Caches a read for an element.
* @param $elem
*/
cacheRead($elem: HTMLElement): MasonryItemCachedRead;
/**
* Schedules a layout.
* @param ms - The debounce time
* @param ms
* @param invalidateCache
*/
scheduleLayout(ms?: number): void;
scheduleLayout(ms?: number, invalidateCache?: boolean): void;
/**
* Re-distributes all of the items.
* Layouts the elements.
* @param invalidateCache
*/
layout(): void;
layout(invalidateCache?: boolean): void;
}

@@ -115,1 +119,2 @@ declare global {

}
export {};

@@ -1,33 +0,43 @@

import { createEmptyColHeightMap, debounce, DEFAULT_COLS, DEFAULT_DEBOUNCE_MS, DEFAULT_MAX_COL_WIDTH, DEFAULT_GAP, DISTRIBUTED_ATTR, getBooleanAttribute, getColCount, getColWidth, getNumberAttribute, getShortestCol, itemPosition, setBooleanAttribute, tallestColHeight } from "./masonry-helpers";
import { debounce, DEFAULT_COLS, DEFAULT_DEBOUNCE_MS, DEFAULT_GAP_PX, DEFAULT_MAX_COL_WIDTH, ELEMENT_NODE_TYPE, findSmallestColIndex, getColCount, getNumberAttribute } from "./masonry-helpers";
/**
* Template for the masonry layout.
*/
const template = document.createElement("template");
template.innerHTML = `
<style>
:host {
display: block;
position: relative;
visibility: hidden;
transform: translate3d(0, 0, 0);
}
const $template = document.createElement("template");
$template.innerHTML = `
<style>
:host {
display: flex;
align-items: flex-start;
justify-content: stretch;
}
::slotted(*) {
position: absolute;
}
/* Show the items after they have been distributed */
:host([data-masonry-distributed]) {
visibility: visible;
}
/* Apply the transition after the items have been distributed */
:host([data-masonry-distributed][transition]) ::slotted([data-masonry-distributed]) {
transition: var(--masonry-layout-item-transition, transform 200ms ease);
}
</style>
<slot id="slot"></slot>
.column {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
}
.column:not(:last-child) {
margin-right: var(--_masonry-layout-gap, ${DEFAULT_GAP_PX}px);
}
.column ::slotted(*) {
margin-bottom: var(--_masonry-layout-gap, ${DEFAULT_GAP_PX}px);
box-sizing: border-box;
}
/* Hide the items that has not yet found the correct slot */
#unset-items {
opacity: 0;
position: absolute;
pointer-events: none;
}
</style>
<div id="unset-items">
<slot></slot>
</div>
`;
// Use polyfill only in browsers that lack native Shadow DOM.
window.ShadyCSS && window.ShadyCSS.prepareTemplateStyles(template, "masonry-layout");
window.ShadyCSS && window.ShadyCSS.prepareTemplateStyles($template, "masonry-layout");
/**

@@ -37,17 +47,24 @@ * Masonry layout web component. It places the slotted elements in the optimal position based

* @example <masonry-layout><div class="item"></div><div class="item"></div></masonry-layout>
* @csspart column - Each column of the masonry layout.
* @csspart column-index - The specific column at the given index (eg. column-0 would target the first column and so on))
* @slot - Items that should be distributed in the layout.
* @cssprop --masonry-layout-item-transition - Transition of an item.
*/
export class MasonryLayout extends HTMLElement {
/**
* Attach the shadow DOM.
*/
constructor() {
super();
// A map containing the height for each col
this.currentColHeightMap = [];
// Unique debounce ID so different masonry layouts on one page won't affect eachother
this.debounceId = `layout_${Math.random()}`;
// Prepare a weakmap for the cache
this.cachedReads = new WeakMap();
// Resize observer that layouts when necessary
this.ro = undefined;
this.cancelNextResizeEvent = false;
this.layoutCache = new WeakMap();
// Bind the relevant functions to the element
this.scheduleLayout = this.scheduleLayout.bind(this);
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild($template.content.cloneNode(true));
this.onSlotChange = this.onSlotChange.bind(this);
this.onResize = this.onResize.bind(this);
this.layout = this.layout.bind(this);
this.didResize = this.didResize.bind(this);
this.$unsetElementsSlot = this.shadowRoot.querySelector("#unset-items > slot");
}

@@ -57,9 +74,3 @@ // The observed attributes.

static get observedAttributes() {
return [
"maxcolwidth",
"collock",
"gap",
"cols",
"debounce"
];
return ["maxcolwidth", "gap", "cols"];
}

@@ -78,24 +89,2 @@ /**

/**
* Whether the items should be locked in their columns after the have been placed.
* @attr collock
* @param v
*/
set colLock(v) {
setBooleanAttribute(this, "collock", v);
}
get colLock() {
return getBooleanAttribute(this, "collock");
}
/**
* The gap in pixels between the columns.
* @attr gap
* @param v
*/
set gap(v) {
this.setAttribute("gap", v.toString());
}
get gap() {
return getNumberAttribute(this, "gap", DEFAULT_GAP);
}
/**
* The amount of columns.

@@ -112,11 +101,11 @@ * @attr cols

/**
* Whether the items should have a transition.
* @attr transition
* The gap in pixels between the columns.
* @attr gap
* @param v
*/
set transition(v) {
setBooleanAttribute(this, "transition", v);
set gap(v) {
this.setAttribute("gap", v.toString());
}
get transition() {
return getBooleanAttribute(this, "transition");
get gap() {
return getNumberAttribute(this, "gap", DEFAULT_GAP_PX);
}

@@ -135,136 +124,169 @@ /**

/**
* The slot element.
* The column elements.
*/
get $slot() {
return this.shadowRoot.querySelector("slot");
get $columns() {
return Array.from(this.shadowRoot.querySelectorAll(`.column`));
}
/**
* All of the elements in the slot that are an Node.ELEMENT_NODE.
* https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
* Hook up event listeners when added to the DOM.
*/
get $items() {
return this.$slot.assignedNodes().filter(node => {
return node.nodeType === 1;
});
}
/**
* Attaches the listeners when the element is added to the DOM.
*/
connectedCallback() {
window.ShadyCSS && window.ShadyCSS.styleElement(this);
if (!this.shadowRoot) {
// Attach the shadow root
const shadow = this.attachShadow({ mode: "open" });
shadow.appendChild(template.content.cloneNode(true));
this.$unsetElementsSlot.addEventListener("slotchange", this.onSlotChange);
// Attach resize observer so we can relayout eachtime the size changes
if ("ResizeObserver" in window) {
this.ro = new ResizeObserver(this.onResize);
this.ro.observe(this);
}
this.attachListeners();
else {
window.addEventListener("resize", this.onResize);
}
}
/**
* Removes listeners when the element is removed from the DOM.
* Remove event listeners when removed from the DOM.
*/
disconnectedCallback() {
this.detachListeners();
this.$unsetElementsSlot.removeEventListener("slotchange", this.onSlotChange);
window.removeEventListener("resize", this.onResize);
if (this.ro != null) {
this.ro.unobserve(this);
}
}
/**
* Updates the layout when the observed attributes changes.
* Updates the layout when one of the observed attributes changes.
*/
attributeChangedCallback() {
attributeChangedCallback(name) {
switch (name) {
case "gap":
this.style.setProperty(`--_masonry-layout-gap`, `${this.gap}px`);
break;
}
// Recalculate the layout
this.scheduleLayout();
}
/**
* Attaches all listeners to the element.
*
*/
attachListeners() {
this.$slot.addEventListener("slotchange", this.layout);
if ("ResizeObserver" in window) {
this.ro = new ResizeObserver(this.didResize);
this.ro.observe(this);
onSlotChange() {
// Grab unset elements
const $unsetElements = this.$unsetElementsSlot.assignedNodes().filter(node => node.nodeType === ELEMENT_NODE_TYPE);
// If there are more items not yet set layout straight awy to avoid the item being delayed in its render.
if ($unsetElements.length > 0) {
this.layout();
}
else {
window.addEventListener("resize", this.didResize);
}
}
/**
* Detaches all listeners from the element.
* Each time the element resizes we need to schedule a layout
* if the amount available columns has has changed.
* @param entries
*/
detachListeners() {
this.$slot.removeEventListener("slotchange", this.layout);
window.removeEventListener("resize", this.didResize);
if (this.ro != null)
this.ro.unobserve(this);
onResize(entries) {
// Grab the width of the element. If it isn't provided by the resize observer entry
// we compute it ourselves by looking at the offset width of the element.
const { width } = entries != null && entries.length > 0
? entries[0].contentRect : { width: this.offsetWidth };
// Get the amount of columns we should have
const colCount = getColCount(width, this.cols, this.maxColWidth);
// Compare the amount of columns we should have to the current amount of columns.
// Schedule a layout that invalidates the cache if they are no longer the same.
if (colCount !== this.$columns.length) {
this.scheduleLayout(this.debounce, true);
}
}
/**
* Called when the element resizes and schedules a layout.
* Render X amount of columns.
* @param colCount
*/
didResize() {
if (this.cancelNextResizeEvent) {
this.cancelNextResizeEvent = false;
renderCols(colCount) {
// Get the current columns
const $columns = this.$columns;
// If the amount of columns is correct we don't have to add new columns.
if ($columns.length === colCount) {
return;
}
this.scheduleLayout();
// Remove all of the current columns
for (const $column of $columns) {
$column.remove();
}
// Add some new columns
for (let i = 0; i < colCount; i++) {
// Create a column element
const $column = document.createElement(`div`);
$column.classList.add(`column`);
$column.setAttribute(`part`, `column column-${i}`);
// Add a slot with the name set to the index of the column
const $slot = document.createElement(`slot`);
$slot.setAttribute(`name`, i.toString());
// Append the slot to the column an the column to the shadow root.
$column.appendChild($slot);
this.shadowRoot.appendChild($column);
}
}
/**
* Caches a read for an element.
* @param $elem
*/
cacheRead($elem) {
// Read properties of the element
const value = {
height: $elem.getBoundingClientRect().height
};
// Cache the read of the element
this.cachedReads.set($elem, value);
return value;
}
/**
* Schedules a layout.
* @param ms - The debounce time
* @param ms
* @param invalidateCache
*/
scheduleLayout(ms) {
debounce(this.layout, ms || this.debounce, "layout");
scheduleLayout(ms = this.debounce, invalidateCache = false) {
debounce(() => this.layout(invalidateCache), ms, this.debounceId);
}
/**
* Re-distributes all of the items.
* Layouts the elements.
* @param invalidateCache
*/
layout() {
layout(invalidateCache = false) {
requestAnimationFrame(() => {
const $items = this.$items;
// READ: To begin with we batch the reads to avoid layout trashing.
// The first get will most likely cause a reflow.
const totalWidth = this.offsetWidth;
const itemHeights = $items.map($item => $item.offsetHeight);
// console.time("layout");
// Compute relevant values we are going to use for layouting the elements.
const gap = this.gap;
const colLock = this.colLock;
const colCount = getColCount(totalWidth, this.cols, this.maxColWidth);
const colWidth = getColWidth(totalWidth, gap, colCount);
const colHeightMap = createEmptyColHeightMap(colCount);
// Check whether the amount of columns has changed.
// If they have changed we need to reorder everything, also if the collock is set to true!
const reorderCols = colHeightMap.length !== this.currentColHeightMap.length;
// Set the position for each item
for (const [i, $item] of $items.entries()) {
// Find the shortest col (we need to prioritize filling that one) or used the existing (locked) one
const currentLayout = this.layoutCache.get($item);
const col = colLock && !reorderCols && currentLayout != null ? currentLayout.col : getShortestCol(colHeightMap);
// Compute the position for the item
const { left, top } = itemPosition(i, colWidth, gap, col, colCount, colHeightMap);
// Check if the layout has changed
if (currentLayout == null ||
(currentLayout.colWidth !== colWidth || currentLayout.left !== left || currentLayout.top !== top || currentLayout.col !== col)) {
this.layoutCache.set($item, { left, top, col, colWidth });
// WRITE: Assign the new position.
Object.assign($item.style, {
transform: `translate(${left}px, ${top}px)`,
width: `${colWidth}px`
});
// WRITE: Tell the rest of the world that this element has been distributed
// But defer it to allow the transformation to be applied first
if (!$item.hasAttribute(DISTRIBUTED_ATTR)) {
requestAnimationFrame(() => {
$item.setAttribute(DISTRIBUTED_ATTR, "");
});
}
const $elements = Array.from(this.children).filter(node => node.nodeType === ELEMENT_NODE_TYPE);
const colCount = getColCount(this.offsetWidth, this.cols, this.maxColWidth);
// Have an array that keeps track of the highest col height.
const colHeights = Array(colCount).fill(0);
// Instead of interleaving reads and writes we create an array for all writes so we can batch them at once.
const writes = [];
// Go through all elements and figure out what column (aka slot) they should be put in.
for (const $elem of $elements) {
// Get the read data of the item (either, pick the cached value or cache it while reading it).
let { height } = invalidateCache || !this.cachedReads.has($elem)
? this.cacheRead($elem)
: this.cachedReads.get($elem);
// Find the currently smallest column
let smallestColIndex = findSmallestColIndex(colHeights);
// Add the height of the item and the gap to the column heights.
// It is very important we add the gap since the more elements we have,
// the bigger the role the margins play when computing the actual height of the columns.
colHeights[smallestColIndex] += height + gap;
// Set the slot on the element to get the element to the correct column.
// Only do it if the slot has actually changed.
const newSlot = smallestColIndex.toString();
if ($elem.slot !== newSlot) {
writes.push(() => ($elem.slot = newSlot));
}
// Add the gained height to the height map
colHeightMap[col] = top + itemHeights[i];
}
// WRITE: Set the height of the entire component to the height of the tallest col
this.style.height = `${tallestColHeight(colHeightMap)}px`;
// WRITE: Tell the rest of the world that the layout has now been distributed
if (!this.hasAttribute(DISTRIBUTED_ATTR)) {
this.setAttribute(DISTRIBUTED_ATTR, "");
// Batch all the writes at once
for (const write of writes) {
write();
}
// Store the new heights of the cols
this.currentColHeightMap = colHeightMap;
// Render the columns
this.renderCols(colCount);
// Commit the changes for ShadyCSS
window.ShadyCSS && window.ShadyCSS.styleElement(this);
// console.timeEnd("layout");
});
}
}
window.customElements.define("masonry-layout", MasonryLayout);
customElements.define("masonry-layout", MasonryLayout);
//# sourceMappingURL=masonry-layout.js.map
{
"name": "@appnest/masonry-layout",
"version": "1.0.2",
"version": "2.0.0",
"license": "MIT",

@@ -35,3 +35,3 @@ "module": "index.js",

"ncu": "ncu -u -a && npm update && npm install",
"b:lib": "node pre-build.js && tsc -p tsconfig.build.json && rollup -c rollup-build.config.ts",
"b:lib": "node pre-build.js && tsc -p tsconfig.build.json && rollup -c rollup-build.config.ts && npm run custom-elements-json",
"b:demo:dev": "rollup -c rollup.config.ts --environment NODE_ENV:dev",

@@ -47,3 +47,4 @@ "b:demo:prod": "rollup -c rollup.config.ts --environment NODE_ENV:prod",

"publish:minor": "np minor --contents=dist --no-cleanup",
"publish:major": "np major --contents=dist --no-cleanup"
"publish:major": "np major --contents=dist --no-cleanup",
"custom-elements-json": "npx wca analyze src/lib --format json --outFile dist/custom-elements.json"
},

@@ -50,0 +51,0 @@ "devDependencies": {

@@ -25,7 +25,7 @@ <h1 align="center">@appnest/masonry-layout</h1>

* **Simple:** Works right out of the box (just add it to your markup)
* **Lightweight:** Super small (1.5kb minified & gzipped)
* **Lightweight:** Super small (1kb minified & gzipped)
* **Zero dependencies:** Created using only vanilla js - no dependencies and framework agnostic!
* **Customizable:** Can customize almost everything (eg. columns, transitions, gap).
* **User friendly:** Automatically re-distribute items when the size of the grid changes or new elements are added
* **Performant:** Efficient & fast
* **Performant:** Efficient & fast (10.000 items takes 40ms for the initial layout and 10ms for the subsequent ones)

@@ -48,11 +48,11 @@

Import `@appnest/masonry-layout` somewhere in your code and you're ready to go! Simply add the masonry layout to your `html` and you'll be singing and dancing from not having to build the masonry layout yourself.
Import `@appnest/masonry-layout` somewhere in your code and you're ready to go! Simply add the `masonry-layout` element to your `html` and then add your elements in between the start and closing tags.
```html
<masonry-layout>
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
<div class="item">5</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</masonry-layout>

@@ -98,12 +98,2 @@ ```

### Lock columns
The `collock` attribute locks the columns. When the columns are locked, the layout will only distribute on the y axis when elements change their sizes. The default value is `false`.
```html
<masonry-layout collock>
...
</masonry-layout>
```
### Change debounce time

@@ -141,3 +131,3 @@

Here's a complete overview of the component.
Here's a complete overview of the element.

@@ -151,10 +141,8 @@ ### masonry-layout

| Property | Attribute | Type | Description |
|---------------|---------------|---------------|--------------------------------------------------|
| `colLock` | `collock` | `boolean` | Whether the items should be locked in their columns after the have been placed. |
| `cols` | `cols` | `MasonryCols` | The amount of columns. |
| `debounce` | `debounce` | `number` | The ms of debounce when the element resizes. |
| `gap` | `gap` | `number` | The gap in pixels between the columns. |
| `maxColWidth` | `maxcolwidth` | `number` | The maximum width of each column if cols are set to auto. |
| `transition` | `transition` | `boolean` | Whether the items should have a transition. |
| Property | Attribute | Type | Description |
|---------------|---------------|--------------------|--------------------------------------------------|
| `cols` | `cols` | `number \| "auto"` | The amount of columns. |
| `debounce` | `debounce` | `number` | The ms of debounce when the element resizes. |
| `gap` | `gap` | `number` | The gap in pixels between the columns. |
| `maxColWidth` | `maxcolwidth` | `number` | The maximum width of each column if cols are set to auto. |

@@ -167,7 +155,8 @@ #### Slots

#### CSS Custom Properties
#### CSS Shadow Parts
| Property | Description |
|------------------------------------|------------------------|
| `--masonry-layout-item-transition` | Transition of an item. |
| Part | Description |
|----------------|--------------------------------------------------|
| `column` | Each column of the masonry layout. |
| `column-index` | The specific column at the given index (eg. column-0 would target the first column and so on)) |

@@ -180,3 +169,3 @@

You might want to polyfill the `ResizeObserver`. The observer in the component makes sure to distribute the items whenever the size of the grid changes. If this is not polyfilled you will have to call the `layout()` function yourself when the height of the grid changes. If no `ResizeObserver` can be found on the `window` object it will instead re-distribute items when the size of the window changes.
You might want to polyfill the `ResizeObserver`. The observer in the element makes sure to distribute the items whenever the size of the grid changes. If this is not polyfilled you will have to call the `layout()` function yourself when the height of the grid changes. If no `ResizeObserver` can be found on the `window` object it will instead re-distribute items when the size of the window changes.

@@ -183,0 +172,0 @@

@@ -1,2 +0,10 @@

declare type MasonryCols = number | "auto";
declare type MasonryItemCachedRead = {
height: number;
};
declare type ResizeObserverEntries = {
contentRect: {
width: number;
height: number;
};
}[];
/**

@@ -6,11 +14,8 @@ * Masonry layout web component. It places the slotted elements in the optimal position based

* @example <masonry-layout><div class="item"></div><div class="item"></div></masonry-layout>
* @csspart column - Each column of the masonry layout.
* @csspart column-index - The specific column at the given index (eg. column-0 would target the first column and so on))
* @slot - Items that should be distributed in the layout.
* @cssprop --masonry-layout-item-transition - Transition of an item.
*/
declare class MasonryLayout extends HTMLElement {
static get observedAttributes(): string[];
private currentColHeightMap;
private ro;
private cancelNextResizeEvent;
private layoutCache;
/**

@@ -24,8 +29,8 @@ * The maximum width of each column if cols are set to auto.

/**
* Whether the items should be locked in their columns after the have been placed.
* @attr collock
* The amount of columns.
* @attr cols
* @param v
*/
set colLock(v: boolean);
get colLock(): boolean;
set cols(v: string | number);
get cols(): number | "auto";
/**

@@ -39,16 +44,2 @@ * The gap in pixels between the columns.

/**
* The amount of columns.
* @attr cols
* @param v
*/
set cols(v: MasonryCols);
get cols(): MasonryCols;
/**
* Whether the items should have a transition.
* @attr transition
* @param v
*/
set transition(v: boolean);
get transition(): boolean;
/**
* The ms of debounce when the element resizes.

@@ -61,45 +52,57 @@ * @attr debounce

/**
* The slot element.
* The column elements.
*/
private get $slot();
private get $columns();
private debounceId;
private cachedReads;
private $unsetElementsSlot;
private ro;
/**
* All of the elements in the slot that are an Node.ELEMENT_NODE.
* https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
* Attach the shadow DOM.
*/
private get $items();
constructor();
/**
* Attaches the listeners when the element is added to the DOM.
* Hook up event listeners when added to the DOM.
*/
connectedCallback(): void;
/**
* Removes listeners when the element is removed from the DOM.
* Remove event listeners when removed from the DOM.
*/
disconnectedCallback(): void;
/**
* Updates the layout when the observed attributes changes.
* Updates the layout when one of the observed attributes changes.
*/
attributeChangedCallback(): void;
attributeChangedCallback(name: string): void;
/**
* Attaches all listeners to the element.
*
*/
private attachListeners;
onSlotChange(): void;
/**
* Detaches all listeners from the element.
* Each time the element resizes we need to schedule a layout
* if the amount available columns has has changed.
* @param entries
*/
private detachListeners;
onResize(entries?: ResizeObserverEntries): void;
/**
* Called when the element resizes and schedules a layout.
* Render X amount of columns.
* @param colCount
*/
private didResize;
renderCols(colCount: number): void;
/**
* Caches a read for an element.
* @param $elem
*/
cacheRead($elem: HTMLElement): MasonryItemCachedRead;
/**
* Schedules a layout.
* @param ms - The debounce time
* @param ms
* @param invalidateCache
*/
scheduleLayout(ms?: number): void;
scheduleLayout(ms?: number, invalidateCache?: boolean): void;
/**
* Re-distributes all of the items.
* Layouts the elements.
* @param invalidateCache
*/
layout(): void;
layout(invalidateCache?: boolean): void;
}
export { MasonryLayout };

@@ -1,1 +0,1 @@

!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self)["masonry-layout"]={})}(this,(function(t){"use strict";const e=10,i="data-masonry-distributed",s="auto",n=400,o=24,r=300,a={};function h(t,e,i,s,n,o){const r=e*s+i*s,a=t<n;return{top:(o[s]||0)+(a?0:i),left:r}}function l(t){return t.map((t,e)=>[e,t]).reduce((t,i)=>t[1]-e<=i[1]?t:i,[0,Number.POSITIVE_INFINITY])[0]}function d(t,e,i){i?t.setAttribute(e,""):t.removeAttribute(e)}function u(t,e){return t.hasAttribute(e)}function c(t,e,i){const s=parseFloat(t.getAttribute(e)||"");return isNaN(s)?i:s}const y=document.createElement("template");y.innerHTML='\n\t<style>\n\t\t:host {\n\t\t\tdisplay: block;\n\t\t\tposition: relative;\n\t\t\tvisibility: hidden;\n\t\t\ttransform: translate3d(0, 0, 0);\n\t\t}\n\n\t\t::slotted(*) {\n\t\t\tposition: absolute;\n\t\t}\n\t\t\n\t\t/* Show the items after they have been distributed */\n\t\t:host([data-masonry-distributed]) {\n\t\t\tvisibility: visible;\n\t\t}\n\t\t\n\t\t/* Apply the transition after the items have been distributed */\n\t\t:host([data-masonry-distributed][transition]) ::slotted([data-masonry-distributed]) {\n\t\t\ttransition: var(--masonry-layout-item-transition, transform 200ms ease);\n\t\t}\n\t</style>\n\t<slot id="slot"></slot>\n',window.ShadyCSS&&window.ShadyCSS.prepareTemplateStyles(y,"masonry-layout");class b extends HTMLElement{constructor(){super(),this.currentColHeightMap=[],this.ro=void 0,this.cancelNextResizeEvent=!1,this.layoutCache=new WeakMap,this.scheduleLayout=this.scheduleLayout.bind(this),this.layout=this.layout.bind(this),this.didResize=this.didResize.bind(this)}static get observedAttributes(){return["maxcolwidth","collock","gap","cols","debounce"]}set maxColWidth(t){this.setAttribute("maxcolwidth",t.toString())}get maxColWidth(){return c(this,"maxcolwidth",n)}set colLock(t){d(this,"collock",t)}get colLock(){return u(this,"collock")}set gap(t){this.setAttribute("gap",t.toString())}get gap(){return c(this,"gap",o)}set cols(t){this.setAttribute("cols",t.toString())}get cols(){return c(this,"cols",s)}set transition(t){d(this,"transition",t)}get transition(){return u(this,"transition")}set debounce(t){this.setAttribute("debounce",t.toString())}get debounce(){return c(this,"debounce",r)}get $slot(){return this.shadowRoot.querySelector("slot")}get $items(){return this.$slot.assignedNodes().filter(t=>1===t.nodeType)}connectedCallback(){if(window.ShadyCSS&&window.ShadyCSS.styleElement(this),!this.shadowRoot){this.attachShadow({mode:"open"}).appendChild(y.content.cloneNode(!0))}this.attachListeners()}disconnectedCallback(){this.detachListeners()}attributeChangedCallback(){this.scheduleLayout()}attachListeners(){this.$slot.addEventListener("slotchange",this.layout),"ResizeObserver"in window?(this.ro=new ResizeObserver(this.didResize),this.ro.observe(this)):window.addEventListener("resize",this.didResize)}detachListeners(){this.$slot.removeEventListener("slotchange",this.layout),window.removeEventListener("resize",this.didResize),null!=this.ro&&this.ro.unobserve(this)}didResize(){this.cancelNextResizeEvent?this.cancelNextResizeEvent=!1:this.scheduleLayout()}scheduleLayout(t){!function(t,e,i){const s=a[i];s&&window.clearTimeout(s),a[i]=window.setTimeout(t,e)}(this.layout,t||this.debounce,"layout")}layout(){requestAnimationFrame(()=>{const t=this.$items,e=this.offsetWidth,s=t.map(t=>t.offsetHeight),n=this.gap,o=this.colLock,r=function(t,e,i){return isNaN(e)?Math.max(1,Math.floor(t/i)):e}(e,this.cols,this.maxColWidth),a=function(t,e,i){return t/i-e*(i-1)/i}(e,n,r),d=function(t){return Object.assign([...new Array(t)].map(()=>0))}(r),u=d.length!==this.currentColHeightMap.length;for(const[e,c]of t.entries()){const t=this.layoutCache.get(c),y=o&&!u&&null!=t?t.col:l(d),{left:b,top:m}=h(e,a,n,y,r,d);null!=t&&t.colWidth===a&&t.left===b&&t.top===m&&t.col===y||(this.layoutCache.set(c,{left:b,top:m,col:y,colWidth:a}),Object.assign(c.style,{transform:`translate(${b}px, ${m}px)`,width:`${a}px`}),c.hasAttribute(i)||requestAnimationFrame(()=>{c.setAttribute(i,"")})),d[y]=m+s[e]}this.style.height=`${function(t){return Object.values(t).reduce((t,e)=>Math.max(t,e),0)}(d)}px`,this.hasAttribute(i)||this.setAttribute(i,""),this.currentColHeightMap=d})}}window.customElements.define("masonry-layout",b),t.MasonryLayout=b,Object.defineProperty(t,"__esModule",{value:!0})}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self)["masonry-layout"]={})}(this,(function(t){"use strict";const e=400,n="auto",s=300,o=24,i=1,a=new Map;function h(t,e,n){const s=parseFloat(t.getAttribute(e)||"");return isNaN(s)?n:s}function l(t,e,n){return isNaN(e)?Math.max(1,Math.floor(t/n)):e}function r(t){let e=0,n=1/0;return t.forEach((t,s)=>{t<n&&(n=t,e=s)}),e}const d=document.createElement("template");d.innerHTML=`\n <style>\n :host {\n display: flex;\n align-items: flex-start;\n justify-content: stretch;\n }\n\n .column {\n width: 100%;\n flex: 1;\n display: flex;\n flex-direction: column;\n }\n\n .column:not(:last-child) {\n margin-right: var(--_masonry-layout-gap, ${o}px);\n }\n\n .column ::slotted(*) {\n margin-bottom: var(--_masonry-layout-gap, ${o}px);\n box-sizing: border-box;\n }\n\n /* Hide the items that has not yet found the correct slot */\n #unset-items {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n </style>\n <div id="unset-items">\n <slot></slot>\n </div>\n`,window.ShadyCSS&&window.ShadyCSS.prepareTemplateStyles(d,"masonry-layout");class c extends HTMLElement{constructor(){super(),this.debounceId=`layout_${Math.random()}`,this.cachedReads=new WeakMap,this.ro=void 0,this.attachShadow({mode:"open"}).appendChild(d.content.cloneNode(!0)),this.onSlotChange=this.onSlotChange.bind(this),this.onResize=this.onResize.bind(this),this.layout=this.layout.bind(this),this.$unsetElementsSlot=this.shadowRoot.querySelector("#unset-items > slot")}static get observedAttributes(){return["maxcolwidth","gap","cols"]}set maxColWidth(t){this.setAttribute("maxcolwidth",t.toString())}get maxColWidth(){return h(this,"maxcolwidth",e)}set cols(t){this.setAttribute("cols",t.toString())}get cols(){return h(this,"cols",n)}set gap(t){this.setAttribute("gap",t.toString())}get gap(){return h(this,"gap",o)}set debounce(t){this.setAttribute("debounce",t.toString())}get debounce(){return h(this,"debounce",s)}get $columns(){return Array.from(this.shadowRoot.querySelectorAll(".column"))}connectedCallback(){this.$unsetElementsSlot.addEventListener("slotchange",this.onSlotChange),"ResizeObserver"in window?(this.ro=new ResizeObserver(this.onResize),this.ro.observe(this)):window.addEventListener("resize",this.onResize)}disconnectedCallback(){this.$unsetElementsSlot.removeEventListener("slotchange",this.onSlotChange),window.removeEventListener("resize",this.onResize),null!=this.ro&&this.ro.unobserve(this)}attributeChangedCallback(t){switch(t){case"gap":this.style.setProperty("--_masonry-layout-gap",`${this.gap}px`)}this.scheduleLayout()}onSlotChange(){this.$unsetElementsSlot.assignedNodes().filter(t=>t.nodeType===i).length>0&&this.layout()}onResize(t){const{width:e}=null!=t&&t.length>0?t[0].contentRect:{width:this.offsetWidth};l(e,this.cols,this.maxColWidth)!==this.$columns.length&&this.scheduleLayout(this.debounce,!0)}renderCols(t){const e=this.$columns;if(e.length!==t){for(const t of e)t.remove();for(let e=0;e<t;e++){const t=document.createElement("div");t.classList.add("column"),t.setAttribute("part",`column column-${e}`);const n=document.createElement("slot");n.setAttribute("name",e.toString()),t.appendChild(n),this.shadowRoot.appendChild(t)}}}cacheRead(t){const e={height:t.getBoundingClientRect().height};return this.cachedReads.set(t,e),e}scheduleLayout(t=this.debounce,e=!1){!function(t,e,n){const s=a.get(n);null!=s&&window.clearTimeout(s),a.set(n,window.setTimeout(t,e))}(()=>this.layout(e),t,this.debounceId)}layout(t=!1){requestAnimationFrame(()=>{const e=this.gap,n=Array.from(this.children).filter(t=>t.nodeType===i),s=l(this.offsetWidth,this.cols,this.maxColWidth),o=Array(s).fill(0),a=[];for(const s of n){let{height:n}=t||!this.cachedReads.has(s)?this.cacheRead(s):this.cachedReads.get(s),i=r(o);o[i]+=n+e;const h=i.toString();s.slot!==h&&a.push(()=>s.slot=h)}for(const t of a)t();this.renderCols(s),window.ShadyCSS&&window.ShadyCSS.styleElement(this)})}}customElements.define("masonry-layout",c),t.MasonryLayout=c,Object.defineProperty(t,"__esModule",{value:!0})}));

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc