{"version":3,"file":"index.js","sources":["masonry-helpers.js","masonry-layout.js"],"sourcesContent":["export const DEFAULT_MAX_COL_WIDTH = 500;\nexport const DEFAULT_COLS = \"auto\";\nexport const DEFAULT_DEBOUNCE_MS = 300;\nexport const DEFAULT_GAP_PX = 24;\nexport const COL_COUNT_CSS_VAR_NAME = `--_masonry-layout-col-count`;\nexport const GAP_CSS_VAR_NAME = `--_masonry-layout-gap`;\n// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType\nexport const ELEMENT_NODE_TYPE = 1;\nconst DEBOUNCE_MAP = new Map();\n/**\n * Returns a number attribute from an element.\n * @param $elem\n * @param name\n * @param defaultValue\n */\nexport function getNumberAttribute($elem, name, defaultValue) {\n const value = parseFloat($elem.getAttribute(name) || \"\");\n return isNaN(value) ? defaultValue : value;\n}\n/**\n * Returns the amount of cols that the masonry grid should have.\n * @param totalWidth\n * @param cols\n * @param maxColWidth\n */\nexport function getColCount(totalWidth, cols, maxColWidth) {\n return isNaN(cols)\n ? Math.max(1, Math.ceil(totalWidth / maxColWidth))\n : cols;\n}\n/**\n * Debounces a function.\n * @param cb\n * @param ms\n * @param id\n */\nexport function debounce(cb, ms, id) {\n const existingTimeout = DEBOUNCE_MAP.get(id);\n if (existingTimeout != null)\n window.clearTimeout(existingTimeout);\n DEBOUNCE_MAP.set(id, window.setTimeout(cb, ms));\n}\n/**\n * Returns the index of the column with the smallest height.\n * @param colHeights\n */\nexport function findSmallestColIndex(colHeights) {\n let smallestIndex = 0;\n let smallestHeight = Infinity;\n colHeights.forEach((height, i) => {\n if (height < smallestHeight) {\n smallestHeight = height;\n smallestIndex = i;\n }\n });\n return smallestIndex;\n}\n//# sourceMappingURL=masonry-helpers.js.map","import { COL_COUNT_CSS_VAR_NAME, debounce, DEFAULT_COLS, DEFAULT_DEBOUNCE_MS, DEFAULT_GAP_PX, DEFAULT_MAX_COL_WIDTH, ELEMENT_NODE_TYPE, findSmallestColIndex, GAP_CSS_VAR_NAME, getColCount, getNumberAttribute } from \"./masonry-helpers\";\n/**\n * Template for the masonry layout.\n * Max width of each column is computed as the width in percentage of\n * the column minus the total with of the gaps divided between each column.\n */\nconst $template = document.createElement(\"template\");\n$template.innerHTML = `\n \n
\n \n
\n`;\n/**\n * Masonry layout web component. It places the slotted elements in the optimal position based\n * on the available vertical space, just like mason fitting stones in a wall.\n * @example
\n * @csspart column - Each column of the masonry layout.\n * @csspart column-index - The specific column at the given index (eg. column-0 would target the first column and so on)\n * @slot - Items that should be distributed in the layout.\n */\nexport class MasonryLayout extends HTMLElement {\n // The observed attributes.\n // Whenever one of these changes we need to update the layout.\n static get observedAttributes() {\n return [\"maxcolwidth\", \"gap\", \"cols\"];\n }\n /**\n * The maximum width of each column if cols are set to auto.\n * @attr maxcolwidth\n * @param v\n */\n set maxColWidth(v) {\n this.setAttribute(\"maxcolwidth\", v.toString());\n }\n get maxColWidth() {\n return getNumberAttribute(this, \"maxcolwidth\", DEFAULT_MAX_COL_WIDTH);\n }\n /**\n * The amount of columns.\n * @attr cols\n * @param v\n */\n set cols(v) {\n this.setAttribute(\"cols\", v.toString());\n }\n get cols() {\n return getNumberAttribute(this, \"cols\", DEFAULT_COLS);\n }\n /**\n * The gap in pixels between the columns.\n * @attr gap\n * @param v\n */\n set gap(v) {\n this.setAttribute(\"gap\", v.toString());\n }\n get gap() {\n return getNumberAttribute(this, \"gap\", DEFAULT_GAP_PX);\n }\n /**\n * The ms of debounce when the element resizes.\n * @attr debounce\n * @param v\n */\n set debounce(v) {\n this.setAttribute(\"debounce\", v.toString());\n }\n get debounce() {\n return getNumberAttribute(this, \"debounce\", DEFAULT_DEBOUNCE_MS);\n }\n /**\n * The column elements.\n */\n get $columns() {\n return Array.from(this.shadowRoot.querySelectorAll(`.column`));\n }\n /**\n * Attach the shadow DOM.\n */\n constructor() {\n super();\n // Unique debounce ID so different masonry layouts on one page won't affect eachother\n this.debounceId = `layout_${Math.random()}`;\n // Resize observer that layouts when necessary\n this.ro = undefined;\n // The current request animation frame callback\n this.currentRequestAnimationFrameCallback = undefined;\n const shadow = this.attachShadow({ mode: \"open\" });\n shadow.appendChild($template.content.cloneNode(true));\n this.onSlotChange = this.onSlotChange.bind(this);\n this.onResize = this.onResize.bind(this);\n this.layout = this.layout.bind(this);\n this.$unsetElementsSlot = this.shadowRoot.querySelector(\"#unset-items > slot\");\n }\n /**\n * Hook up event listeners when added to the DOM.\n */\n connectedCallback() {\n this.$unsetElementsSlot.addEventListener(\"slotchange\", this.onSlotChange);\n // Attach resize observer so we can relayout eachtime the size changes\n if (\"ResizeObserver\" in window) {\n this.ro = new ResizeObserver(this.onResize);\n this.ro.observe(this);\n }\n else {\n window.addEventListener(\"resize\", this.onResize);\n }\n }\n /**\n * Remove event listeners when removed from the DOM.\n */\n disconnectedCallback() {\n this.$unsetElementsSlot.removeEventListener(\"slotchange\", this.onSlotChange);\n window.removeEventListener(\"resize\", this.onResize);\n if (this.ro != null) {\n this.ro.unobserve(this);\n }\n }\n /**\n * Updates the layout when one of the observed attributes changes.\n */\n attributeChangedCallback(name) {\n switch (name) {\n case \"gap\":\n this.style.setProperty(GAP_CSS_VAR_NAME, `${this.gap}px`);\n break;\n }\n // Recalculate the layout\n this.scheduleLayout();\n }\n /**\n *\n */\n onSlotChange() {\n // Grab unset elements\n const $unsetElements = (this.$unsetElementsSlot.assignedNodes() || [])\n .filter(node => node.nodeType === ELEMENT_NODE_TYPE);\n // If there are more items not yet set layout straight awy to avoid the item being delayed in its render.\n if ($unsetElements.length > 0) {\n this.layout();\n }\n }\n /**\n * Each time the element resizes we need to schedule a layout\n * if the amount available columns has has changed.\n * @param entries\n */\n onResize(entries) {\n // Grab the width of the element. If it isn't provided by the resize observer entry\n // we compute it ourselves by looking at the offset width of the element.\n const { width } = entries != null && Array.isArray(entries) && entries.length > 0\n ? entries[0].contentRect : { width: this.offsetWidth };\n // Get the amount of columns we should have\n const colCount = getColCount(width, this.cols, this.maxColWidth);\n // Compare the amount of columns we should have to the current amount of columns.\n // Schedule a layout if they are no longer the same.\n if (colCount !== this.$columns.length) {\n this.scheduleLayout();\n }\n }\n /**\n * Render X amount of columns.\n * @param colCount\n */\n renderCols(colCount) {\n // Get the current columns\n const $columns = this.$columns;\n // If the amount of columns is correct we don't have to add new columns.\n if ($columns.length === colCount) {\n return;\n }\n // Remove all of the current columns\n for (const $column of $columns) {\n $column.parentNode && $column.parentNode.removeChild($column);\n }\n // Add some new columns\n for (let i = 0; i < colCount; i++) {\n // Create a column element\n const $column = document.createElement(`div`);\n $column.classList.add(`column`);\n $column.setAttribute(`part`, `column column-${i}`);\n // Add a slot with the name set to the index of the column\n const $slot = document.createElement(`slot`);\n $slot.setAttribute(`name`, i.toString());\n // Append the slot to the column an the column to the shadow root.\n $column.appendChild($slot);\n this.shadowRoot.appendChild($column);\n }\n // Set the column count so we can compute the correct width of the columns\n this.style.setProperty(COL_COUNT_CSS_VAR_NAME, colCount.toString());\n }\n /**\n * Schedules a layout.\n * @param ms\n */\n scheduleLayout(ms = this.debounce) {\n debounce(this.layout, ms, this.debounceId);\n }\n /**\n * Layouts the elements.\n */\n layout() {\n // Cancel the current animation frame callback\n if (this.currentRequestAnimationFrameCallback != null) {\n window.cancelAnimationFrame(this.currentRequestAnimationFrameCallback);\n }\n // Layout in the next animationframe\n this.currentRequestAnimationFrameCallback = requestAnimationFrame(() => {\n // console.time(\"layout\");\n // Compute relevant values we are going to use for layouting the elements.\n const gap = this.gap;\n const $elements = Array.from(this.children)\n .filter(node => node.nodeType === ELEMENT_NODE_TYPE);\n const colCount = getColCount(this.offsetWidth, this.cols, this.maxColWidth);\n // Have an array that keeps track of the highest col height.\n const colHeights = Array(colCount).fill(0);\n // Instead of interleaving reads and writes we create an array for all writes so we can batch them at once.\n const writes = [];\n // Go through all elements and figure out what column (aka slot) they should be put in.\n // We only do reads in this for loop and postpone the writes\n for (const $elem of $elements) {\n // Read the height of the element\n const height = $elem.getBoundingClientRect().height;\n // Find the currently smallest column\n let smallestColIndex = findSmallestColIndex(colHeights);\n // Add the height of the item and the gap to the column heights.\n // It is very important we add the gap since the more elements we have,\n // the bigger the role the margins play when computing the actual height of the columns.\n colHeights[smallestColIndex] += height + gap;\n // Set the slot on the element to get the element to the correct column.\n // Only do it if the slot has actually changed.\n const newSlot = smallestColIndex.toString();\n if ($elem.slot !== newSlot) {\n writes.push(() => ($elem.slot = newSlot));\n }\n }\n // Batch all the writes at once\n for (const write of writes) {\n write();\n }\n // Render the columns\n this.renderCols(colCount);\n // console.timeEnd(\"layout\");\n });\n }\n}\ncustomElements.define(\"masonry-layout\", MasonryLayout);\n//# sourceMappingURL=masonry-layout.js.map"],"names":["DEFAULT_MAX_COL_WIDTH","DEFAULT_COLS","DEFAULT_DEBOUNCE_MS","DEFAULT_GAP_PX","COL_COUNT_CSS_VAR_NAME","GAP_CSS_VAR_NAME","ELEMENT_NODE_TYPE","DEBOUNCE_MAP","Map","getNumberAttribute","$elem","name","defaultValue","value","parseFloat","getAttribute","isNaN","getColCount","totalWidth","cols","maxColWidth","Math","max","ceil","debounce","cb","ms","id","existingTimeout","get","window","clearTimeout","set","setTimeout","findSmallestColIndex","colHeights","smallestIndex","smallestHeight","Infinity","forEach","height","i","$template","document","createElement","innerHTML","MasonryLayout","HTMLElement","observedAttributes","v","this","setAttribute","toString","gap","$columns","Array","from","shadowRoot","querySelectorAll","constructor","super","debounceId","random","ro","undefined","currentRequestAnimationFrameCallback","shadow","attachShadow","mode","appendChild","content","cloneNode","onSlotChange","bind","onResize","layout","$unsetElementsSlot","querySelector","connectedCallback","addEventListener","ResizeObserver","observe","disconnectedCallback","removeEventListener","unobserve","attributeChangedCallback","style","setProperty","scheduleLayout","$unsetElements","assignedNodes","filter","node","nodeType","length","entries","width","isArray","contentRect","offsetWidth","colCount","renderCols","$column","parentNode","removeChild","classList","add","$slot","cancelAnimationFrame","requestAnimationFrame","$elements","children","fill","writes","getBoundingClientRect","smallestColIndex","newSlot","slot","push","write","customElements","define"],"mappings":"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"}