From ab0b11cc92ea53a26f773d2f82ca92394ba9996f Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:33:26 +0100 Subject: [PATCH] Add masonry layout functionality --- internal/assets/static/js/masonry.js | 52 ++++++++++++++++++++++++++++ internal/assets/static/js/utils.js | 4 +++ internal/assets/static/main.css | 11 ++++++ 3 files changed, 67 insertions(+) create mode 100644 internal/assets/static/js/masonry.js diff --git a/internal/assets/static/js/masonry.js b/internal/assets/static/js/masonry.js new file mode 100644 index 0000000..f2e1535 --- /dev/null +++ b/internal/assets/static/js/masonry.js @@ -0,0 +1,52 @@ +import { clamp } from "./utils.js"; + +export function setupMasonries(options = {}, selector = ".masonry") { + options = { + minColumnWidth: 300, + maxColumns: 6, + ...options + }; + + const masonryContainers = document.querySelectorAll(selector); + + for (let i = 0; i < masonryContainers.length; i++) { + const container = masonryContainers[i]; + const items = Array.from(container.children); + let previousColumnsCount = 0; + + const render = function() { + const columnsCount = clamp( + Math.floor(container.offsetWidth / options.minColumnWidth), + 1, + Math.min(options.maxColumns, items.length) + ); + + if (columnsCount === previousColumnsCount) { + return; + } else { + container.textContent = ""; + previousColumnsCount = columnsCount; + } + + const columnsFragment = document.createDocumentFragment(); + + for (let i = 0; i < columnsCount; i++) { + const column = document.createElement("div"); + column.className = "masonry-column"; + columnsFragment.append(column); + } + + // poor man's masonry + // TODO: add an option that allows placing items in the + // shortest column instead of iterating the columns in order + for (let i = 0; i < items.length; i++) { + columnsFragment.children[i % columnsCount].appendChild(items[i]); + } + + container.append(columnsFragment); + }; + + const observer = new ResizeObserver(() => requestAnimationFrame(render)); + observer.observe(container); + } +} diff --git a/internal/assets/static/js/utils.js b/internal/assets/static/js/utils.js index af02086..ddf7e4f 100644 --- a/internal/assets/static/js/utils.js +++ b/internal/assets/static/js/utils.js @@ -23,3 +23,7 @@ export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { export function isElementVisible(element) { return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } + +export function clamp(value, min, max) { + return Math.min(Math.max(value, min), max); +} diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index d5ab9bb..362e257 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -440,6 +440,17 @@ kbd:active { box-shadow: 0 0 0 0 var(--color-widget-background-highlight); } +.masonry { + display: flex; + gap: var(--widget-gap); +} + +.masonry-column { + flex: 1; + display: flex; + flex-direction: column; +} + .popover-container, [data-popover-html] { display: none; }