@appnest/masonry-layout
Advanced tools
Comparing version
@@ -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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
14
7.69%43904
-0.72%688
-0.29%172
-6.01%1
Infinity%