diff --git a/README.md b/README.md index 02e4a5c..87cbfbc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Rectpackr Layout +[![NPM Version](https://img.shields.io/npm/v/rectpackr-layout)](https://www.npmjs.com/package/rectpackr-layout) +[![Coverage Status](https://img.shields.io/coverallsCoverage/github/styiannis/rectpackr-layout)](https://coveralls.io/github/styiannis/rectpackr-layout?branch=main) +[![CodePen Demos](https://img.shields.io/badge/CodePen-Demos-blue)](https://codepen.io/collection/dGpeLa) + A web component that creates layouts by treating your HTML elements as rectangles and packing them using a best-fit 2D strip-packing algorithm. ## ⚙️ Why a Packing Algorithm for Web Layouts? @@ -68,6 +72,39 @@ Or directly in your HTML: ``` +### Using a CDN (No Build Step Needed) + +Include it directly in your HTML via CDN: + +#### unpkg + +```html + +``` + +#### jsDelivr + +```html + +``` + +#### esm.sh + +```html + +``` + +### Once installed, use the web component anywhere in your HTML: + +```html + +
Your content here
+
+``` + ## 📖 API Reference ### Attributes @@ -151,6 +188,26 @@ The `x-direction` and `y-direction` attributes control visual placement, which m ``` +## 🎯 Live Demos + +### Consistent Width Gallery + +See predictable masonry-style layouts with equal-width elements + +[View on CodePen](https://codepen.io/styiannis/pen/ogbzBXg) + +### Mixed Dimension Gallery + +Explore optimal packing of variably-sized elements and aspect ratios + +[View on CodePen](https://codepen.io/styiannis/pen/XJXjayR) + +### Interactive Playground + +Experiment with real-time controls and dynamic content manipulation + +[View on CodePen](https://codepen.io/styiannis/pen/qEbqMBZ) + ## ✅ Browser Support Modern browsers with Web Components support. diff --git a/package.json b/package.json index c8f9006..e57f5f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rectpackr-layout", - "version": "0.9.0", + "version": "1.0.0", "description": "A web component that creates layouts by packing HTML elements as rectangles using a best-fit 2D strip-packing algorithm. Automatically measures dimensions and handles mixed content.", "keywords": [ "best-fit", @@ -12,7 +12,7 @@ "masonry-layout", "responsive", "responsive-layout", - "rectangle-packer", + "rectangle-packing", "strip-packing", "web-component", "web-components" @@ -63,22 +63,22 @@ "validate-exports": "node ./scripts/validate-exports.js" }, "dependencies": { - "best-fit-strip-pack": "^1.0.1" + "best-fit-strip-pack": "^1.0.2" }, "devDependencies": { - "@rollup/plugin-node-resolve": "^16.0.1", + "@rollup/plugin-node-resolve": "^16.0.2", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^12.1.4", "@tsconfig/node22": "^22.0.2", "@types/jest": "^30.0.0", - "@types/node": "^24.5.2", - "jest": "^30.1.3", - "jest-environment-jsdom": "^30.1.2", - "rollup": "^4.52.2", + "@types/node": "^24.7.0", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "rollup": "^4.52.4", "rollup-plugin-dts": "^6.2.3", "ts-jest": "^29.4.4", "tslib": "^2.8.1", - "typescript": "^5.9.2" + "typescript": "^5.9.3" }, "exports": { ".": { diff --git a/src/core/rectpackr.ts b/src/core/rectpackr.ts index a3bad43..7f6afa2 100644 --- a/src/core/rectpackr.ts +++ b/src/core/rectpackr.ts @@ -33,9 +33,9 @@ export function create( container, children: [] as R['children'], childrenContainer, + isPendingStartObservingChildren: false, loadingImages: new Map(), observers: { childrenContainerMutation, childrenResize, containerResize }, - pendingStartObservingChildren: false, stripPack, } as R; @@ -47,5 +47,6 @@ export function create( export function clear(instance: R) { stopObserving(instance); resetStyle(instance); + instance.children.length = 0; instance.stripPack.reset(); } diff --git a/src/core/types.ts b/src/core/types.ts index bdcd821..52fc006 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -17,12 +17,12 @@ export interface IRectpackr { width: number; }[]; childrenContainer: HTMLElement; + isPendingStartObservingChildren: boolean; loadingImages: Map void>; observers: { childrenContainerMutation: MutationObserver; childrenResize: ResizeObserver; containerResize: ResizeObserver; }; - pendingStartObservingChildren: boolean; stripPack: BestFitStripPack; } diff --git a/src/core/util.ts b/src/core/util.ts index cb34a48..ddb4ba4 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -13,15 +13,15 @@ function render(instance: IRectpackr) { } function restartObservingChildren(instance: IRectpackr) { - if (instance.pendingStartObservingChildren) { + if (instance.isPendingStartObservingChildren) { return; } - instance.pendingStartObservingChildren = true; + instance.isPendingStartObservingChildren = true; stopObservingChildren(instance); requestAnimationFrame(() => { - instance.pendingStartObservingChildren = false; + instance.isPendingStartObservingChildren = false; startObservingChildren(instance); }); } @@ -53,15 +53,15 @@ function startObservingContainer(instance: IRectpackr) { } function startObservingImages(instance: IRectpackr) { - function callback(this: HTMLImageElement) { + function onImgLoad(this: HTMLImageElement) { instance.loadingImages.delete(this); restartObservingChildren(instance); } for (const img of instance.childrenContainer.querySelectorAll('img')) { if (!img.complete && !instance.loadingImages.get(img)) { - instance.loadingImages.set(img, callback); - img.addEventListener('load', callback, { once: true, passive: true }); + instance.loadingImages.set(img, onImgLoad); + img.addEventListener('load', onImgLoad, { once: true, passive: true }); } } } @@ -80,8 +80,8 @@ function stopObservingContainer(instance: IRectpackr) { } function stopObservingImages(instance: IRectpackr) { - for (const [img, callback] of instance.loadingImages) { - img.removeEventListener('load', callback); + for (const [img, onImgLoad] of instance.loadingImages) { + img.removeEventListener('load', onImgLoad); instance.loadingImages.delete(img); } } @@ -112,9 +112,7 @@ function updateStyle( instance: IRectpackr, children: { element: IRectpackrChildElement; point: [number, number] }[] ) { - /* - * Update children style. - */ + // Update children style for (const { element, point } of children) { const xVal = point[0] * @@ -143,9 +141,7 @@ function updateStyle( } } - /* - * Update container style. - */ + // Update container style instance.container.style.height = `${instance.stripPack.packedHeight}px`; } @@ -154,6 +150,10 @@ function updateStyle( /* ------------------------------------------------------------------------- */ export function onChildrenContainerMutation(instance: IRectpackr) { + if (instance.childrenContainer.children.length === 0) { + onChildResize(instance, []); + } + restartObservingChildren(instance); restartObservingImages(instance); } @@ -196,9 +196,7 @@ export function onContainerResize(instance: IRectpackr) { } export function resetStyle(instance: IRectpackr) { - /* - * Reset children style. - */ + // Reset children style for (const { element } of instance.children) { if (instance.config.positioning === 'offset') { element.style.inset = ''; @@ -207,9 +205,7 @@ export function resetStyle(instance: IRectpackr) { } } - /* - * Reset container style. - */ + // Reset container style instance.container.style.height = ''; } diff --git a/tests/rectpackr-layout-attributes.test.ts b/tests/rectpackr-layout-attributes.test.ts index 1b1f4c3..112b268 100644 --- a/tests/rectpackr-layout-attributes.test.ts +++ b/tests/rectpackr-layout-attributes.test.ts @@ -6,12 +6,8 @@ import { } from './util/types'; describe('Change attributes', () => { - const shadowRootOffsetChildrenStyle = Object.freeze({ - ltr: '::slotted(:not([slot])){ position: absolute !important }', - rtl: '::slotted(:not([slot])){ position: absolute !important }', - ttb: '::slotted(:not([slot])){ position: absolute !important }', - btt: '::slotted(:not([slot])){ position: absolute !important }', - }); + const shadowRootOffsetChildrenStyle = + '::slotted(:not([slot])){ position: absolute !important }'; const shadowRootTransformChildrenStyle = Object.freeze({ ltr: '::slotted(:not([slot])){ position: absolute !important; inset: 0 auto auto 0 !important }', @@ -102,7 +98,7 @@ describe('Change attributes', () => { changeAttributeAndValidate( { positioning: 'offset' }, - shadowRootOffsetChildrenStyle.rtl, + shadowRootOffsetChildrenStyle, [ { element: children[0]!, inset: '0 0 auto auto' }, { element: children[1]!, inset: `0 ${1 * childWidth}px auto auto` }, @@ -113,7 +109,7 @@ describe('Change attributes', () => { changeAttributeAndValidate( { 'x-direction': 'ltr' }, - shadowRootOffsetChildrenStyle.ltr, + shadowRootOffsetChildrenStyle, [ { element: children[0]!, inset: '0 auto auto 0' }, { element: children[1]!, inset: `0 auto auto ${1 * childWidth}px` }, @@ -223,7 +219,7 @@ describe('Change attributes', () => { changeAttributeAndValidate( { positioning: 'offset' }, - shadowRootOffsetChildrenStyle.btt, + shadowRootOffsetChildrenStyle, [ { element: children[0]!, inset: 'auto auto 0 0' }, { element: children[1]!, inset: `auto auto ${1 * childHeight}px 0` }, @@ -234,7 +230,7 @@ describe('Change attributes', () => { changeAttributeAndValidate( { 'y-direction': 'ttb' }, - shadowRootOffsetChildrenStyle.ttb, + shadowRootOffsetChildrenStyle, [ { element: children[0]!, inset: '0 auto auto 0' }, { element: children[1]!, inset: `${1 * childHeight}px auto auto 0` },