height-harmony 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -148
- package/dist/height-harmony-min.js +12 -22
- package/dist/height-harmony.es.js +209 -0
- package/height-harmony.js +299 -29
- package/package.json +21 -8
package/README.md
CHANGED
|
@@ -1,220 +1,270 @@
|
|
|
1
1
|
# Height Harmony
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**The fastest, smartest equal-height JavaScript utility on the web.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Zero dependencies. ResizeObserver-powered. Automatically responsive. Drop it in and forget it.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/height-harmony)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](#)
|
|
6
10
|
|
|
7
11
|
**[View the interactive demo →](https://byronjohnson.github.io/height-harmony/demo/)**
|
|
8
12
|
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What's New in v2.0
|
|
16
|
+
|
|
17
|
+
| Feature | v1 | v2 |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| ResizeObserver (auto-reactive) | ❌ | ✅ |
|
|
20
|
+
| MutationObserver (dynamic content) | ❌ | ✅ |
|
|
21
|
+
| Options object | ❌ | ✅ |
|
|
22
|
+
| `destroy()` / `refresh()` methods | ❌ | ✅ |
|
|
23
|
+
| Breakpoint support | ❌ | ✅ |
|
|
24
|
+
| `data-hh-group` HTML attribute | ❌ | ✅ |
|
|
25
|
+
| ESM + UMD build | ❌ | ✅ |
|
|
26
|
+
| Zero layout thrash | partial | ✅ |
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
9
30
|
## Installation
|
|
10
31
|
|
|
11
|
-
###
|
|
32
|
+
### npm / yarn / pnpm
|
|
33
|
+
|
|
12
34
|
```bash
|
|
13
35
|
npm install height-harmony
|
|
14
36
|
```
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
// ES6 Modules (recommended)
|
|
18
|
-
import heightHarmony from 'height-harmony';
|
|
38
|
+
### CDN (UMD — no bundler needed)
|
|
19
39
|
|
|
20
|
-
|
|
21
|
-
|
|
40
|
+
```html
|
|
41
|
+
<script src="https://unpkg.com/height-harmony@2/dist/height-harmony-min.js"></script>
|
|
42
|
+
<script>
|
|
43
|
+
heightHarmony('.card');
|
|
44
|
+
</script>
|
|
22
45
|
```
|
|
23
46
|
|
|
24
|
-
### CDN
|
|
47
|
+
### CDN (ES Module)
|
|
25
48
|
|
|
26
|
-
#### ES Modules (Modern Browsers)
|
|
27
49
|
```html
|
|
28
50
|
<script type="module">
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Use immediately
|
|
32
|
-
heightHarmony('.my-elements');
|
|
51
|
+
import heightHarmony from 'https://unpkg.com/height-harmony@2/dist/height-harmony.es.js';
|
|
52
|
+
heightHarmony('.card');
|
|
33
53
|
</script>
|
|
34
54
|
```
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```javascript
|
|
61
|
+
import heightHarmony from 'height-harmony';
|
|
62
|
+
|
|
63
|
+
// Basic — equalize all matching elements
|
|
64
|
+
heightHarmony('.card');
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
66
|
+
// With options
|
|
67
|
+
heightHarmony('.card', { debounce: 100, breakpoint: 768 });
|
|
68
|
+
|
|
69
|
+
// Store the instance
|
|
70
|
+
const hh = heightHarmony('.card');
|
|
71
|
+
hh.refresh(); // manual re-trigger
|
|
72
|
+
hh.destroy(); // clean up all observers and remove inline styles
|
|
43
73
|
```
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## API Reference
|
|
78
|
+
|
|
79
|
+
### `heightHarmony(target, options?)`
|
|
80
|
+
|
|
81
|
+
**Parameters**
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Description |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `target` | `string \| NodeList \| HTMLElement[]` | CSS selector string or a collection of elements |
|
|
86
|
+
| `options` | `HeightHarmonyOptions` | *(optional)* Configuration object |
|
|
87
|
+
|
|
88
|
+
**Returns** `HeightHarmonyInstance`
|
|
89
|
+
|
|
90
|
+
---
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
### Options
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
interface HeightHarmonyOptions {
|
|
96
|
+
/**
|
|
97
|
+
* Milliseconds to debounce ResizeObserver / MutationObserver callbacks.
|
|
98
|
+
* 0 = no debounce, only requestAnimationFrame (default).
|
|
99
|
+
* @default 0
|
|
100
|
+
*/
|
|
101
|
+
debounce?: number;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Use `min-height` instead of `height`.
|
|
105
|
+
* Allows elements to grow taller than the maximum if new content is added.
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
minHeight?: boolean;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Viewport width (px) below which harmonizing is disabled.
|
|
112
|
+
* Set to 768 to let mobile layouts stack naturally.
|
|
113
|
+
* @default 0 (always on)
|
|
114
|
+
*/
|
|
115
|
+
breakpoint?: number;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Whether to auto-watch via ResizeObserver and MutationObserver.
|
|
119
|
+
* Set to false for fire-and-forget manual mode.
|
|
120
|
+
* @default true
|
|
121
|
+
*/
|
|
122
|
+
watch?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Apply a CSS `transition` on height changes for smooth animation.
|
|
126
|
+
* @default true
|
|
127
|
+
*/
|
|
128
|
+
transitions?: boolean;
|
|
129
|
+
}
|
|
52
130
|
```
|
|
53
131
|
|
|
54
|
-
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### Instance Methods
|
|
135
|
+
|
|
136
|
+
#### `.refresh()` → `this`
|
|
137
|
+
|
|
138
|
+
Manually triggers a height re-calculation. Useful after CSS transitions finish or after content changes you explicitly control.
|
|
55
139
|
|
|
56
140
|
```javascript
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
141
|
+
const hh = heightHarmony('.card');
|
|
142
|
+
// ... some time later, after a font loads or animation finishes
|
|
143
|
+
hh.refresh();
|
|
60
144
|
```
|
|
61
145
|
|
|
62
|
-
|
|
146
|
+
#### `.destroy()` → `this`
|
|
147
|
+
|
|
148
|
+
Disconnects all ResizeObserver and MutationObserver instances, removes all inline `height` / `min-height` styles set by this instance, and marks it as destroyed.
|
|
63
149
|
|
|
64
150
|
```javascript
|
|
65
|
-
|
|
66
|
-
|
|
151
|
+
const hh = heightHarmony('.card');
|
|
152
|
+
// Clean up when a component unmounts (React, Vue, etc.)
|
|
153
|
+
hh.destroy();
|
|
67
154
|
```
|
|
68
155
|
|
|
69
|
-
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `heightHarmony.autoInit(options?)`
|
|
159
|
+
|
|
160
|
+
Scans the entire document for elements with `data-hh-group` attributes and harmonizes each group automatically.
|
|
70
161
|
|
|
71
|
-
### Browser ES Modules
|
|
72
162
|
```html
|
|
73
|
-
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Initialize when DOM is ready
|
|
80
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
81
|
-
heightHarmony('.card');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Handle responsive behavior
|
|
85
|
-
window.addEventListener('resize', function() {
|
|
86
|
-
heightHarmony('.card');
|
|
87
|
-
});
|
|
88
|
-
</script>
|
|
89
|
-
</head>
|
|
90
|
-
<body>
|
|
91
|
-
<div class="card">Content 1</div>
|
|
92
|
-
<div class="card">Content 2</div>
|
|
93
|
-
<div class="card">Content 3</div>
|
|
94
|
-
</body>
|
|
95
|
-
</html>
|
|
163
|
+
<!-- HTML -->
|
|
164
|
+
<div data-hh-group="cards">Card 1 — short content</div>
|
|
165
|
+
<div data-hh-group="cards">Card 2 — a lot more content here...</div>
|
|
166
|
+
<div data-hh-group="sidebar">Widget A</div>
|
|
167
|
+
<div data-hh-group="sidebar">Widget B</div>
|
|
96
168
|
```
|
|
97
169
|
|
|
98
|
-
### Node.js / Build Tools
|
|
99
170
|
```javascript
|
|
100
|
-
// app.js
|
|
101
171
|
import heightHarmony from 'height-harmony';
|
|
102
172
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Export for use in other modules
|
|
109
|
-
export { initCards };
|
|
173
|
+
// One call handles all groups
|
|
174
|
+
const instances = heightHarmony.autoInit({ debounce: 100 });
|
|
175
|
+
// Returns an array of HeightHarmonyInstance, one per group
|
|
110
176
|
```
|
|
111
177
|
|
|
112
|
-
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
### `heightHarmony.version`
|
|
181
|
+
|
|
113
182
|
```javascript
|
|
114
|
-
//
|
|
183
|
+
console.log(heightHarmony.version); // "2.0.0"
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Framework Integration
|
|
189
|
+
|
|
190
|
+
### React
|
|
191
|
+
|
|
192
|
+
```jsx
|
|
193
|
+
import { useEffect, useRef } from 'react';
|
|
115
194
|
import heightHarmony from 'height-harmony';
|
|
116
195
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
heightHarmony('.
|
|
196
|
+
function CardGrid({ cards }) {
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
const hh = heightHarmony('.card', { debounce: 50 });
|
|
199
|
+
return () => hh.destroy();
|
|
200
|
+
}, [cards]); // re-run when cards array changes
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div className="grid">
|
|
204
|
+
{cards.map(card => <div className="card" key={card.id}>{card.content}</div>)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
120
207
|
}
|
|
121
208
|
```
|
|
122
209
|
|
|
123
|
-
|
|
210
|
+
### Vue 3
|
|
124
211
|
|
|
125
|
-
### ES Modules
|
|
126
212
|
```javascript
|
|
213
|
+
import { onMounted, onUnmounted, watch } from 'vue';
|
|
127
214
|
import heightHarmony from 'height-harmony';
|
|
128
215
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
window.addEventListener('resize', function() {
|
|
136
|
-
clearTimeout(resizeTimeout);
|
|
137
|
-
resizeTimeout = setTimeout(function() {
|
|
138
|
-
heightHarmony('.card');
|
|
139
|
-
}, 150);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Handle orientation changes on mobile
|
|
143
|
-
window.addEventListener('orientationchange', function() {
|
|
144
|
-
setTimeout(function() {
|
|
145
|
-
heightHarmony('.card');
|
|
146
|
-
}, 300);
|
|
147
|
-
});
|
|
148
|
-
});
|
|
216
|
+
export function useHeightHarmony(selector, options = {}) {
|
|
217
|
+
let instance = null;
|
|
218
|
+
onMounted(() => { instance = heightHarmony(selector, options); });
|
|
219
|
+
onUnmounted(() => { instance?.destroy(); });
|
|
220
|
+
return { refresh: () => instance?.refresh() };
|
|
221
|
+
}
|
|
149
222
|
```
|
|
150
223
|
|
|
151
|
-
###
|
|
224
|
+
### Vanilla JS — DOMContentLoaded
|
|
225
|
+
|
|
152
226
|
```javascript
|
|
153
|
-
|
|
154
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
155
|
-
heightHarmony('.card');
|
|
156
|
-
});
|
|
227
|
+
import heightHarmony from 'height-harmony';
|
|
157
228
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
window.addEventListener('resize', function() {
|
|
161
|
-
clearTimeout(resizeTimeout);
|
|
162
|
-
resizeTimeout = setTimeout(function() {
|
|
163
|
-
heightHarmony('.card');
|
|
164
|
-
}, 150);
|
|
229
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
230
|
+
heightHarmony('.card'); // ResizeObserver handles everything else
|
|
165
231
|
});
|
|
166
232
|
```
|
|
167
233
|
|
|
168
|
-
|
|
234
|
+
---
|
|
169
235
|
|
|
170
|
-
|
|
171
|
-
```javascript
|
|
172
|
-
import heightHarmony from 'height-harmony';
|
|
236
|
+
## How It Works
|
|
173
237
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
data.forEach(item => {
|
|
179
|
-
const card = document.createElement('div');
|
|
180
|
-
card.className = 'card';
|
|
181
|
-
card.textContent = item.content;
|
|
182
|
-
container.appendChild(card);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Harmonize heights after DOM update
|
|
186
|
-
setTimeout(function() {
|
|
187
|
-
heightHarmony('.card');
|
|
188
|
-
}, 10);
|
|
189
|
-
}
|
|
190
|
-
```
|
|
238
|
+
1. **Reset** — Clears inline `height` / `min-height` on all matched elements in a single write pass.
|
|
239
|
+
2. **Measure** — Reads `offsetHeight` for every element in one synchronous batch (no interleaved read/write thrashing).
|
|
240
|
+
3. **Apply** — Sets all elements to the maximum measured height.
|
|
241
|
+
4. **Watch** — `ResizeObserver` re-syncs automatically whenever any element's size changes. `MutationObserver` re-syncs when new elements are added to parent containers.
|
|
191
242
|
|
|
192
|
-
|
|
193
|
-
```javascript
|
|
194
|
-
// After adding new content
|
|
195
|
-
setTimeout(function() {
|
|
196
|
-
heightHarmony('.product-card');
|
|
197
|
-
}, 10);
|
|
198
|
-
```
|
|
243
|
+
---
|
|
199
244
|
|
|
200
|
-
##
|
|
245
|
+
## Performance
|
|
201
246
|
|
|
202
|
-
|
|
203
|
-
- Chrome 61+
|
|
204
|
-
- Firefox 60+
|
|
205
|
-
- Safari 10.1+
|
|
206
|
-
- Edge 16+
|
|
247
|
+
Height Harmony v2 is carefully engineered to avoid common causes of layout thrashing:
|
|
207
248
|
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
249
|
+
- All height reads happen **before** any writes (batch read → batch write)
|
|
250
|
+
- `ResizeObserver` is far more efficient than `window.resize` — it only fires for elements that actually changed
|
|
251
|
+
- `requestAnimationFrame` ensures writes happen at the right point in the browser rendering pipeline
|
|
252
|
+
- A built-in debounce option prevents excessive recalculations during rapid mutations
|
|
211
253
|
|
|
212
|
-
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Browser Compatibility
|
|
257
|
+
|
|
258
|
+
| Feature | Chrome | Firefox | Safari | Edge |
|
|
259
|
+
|---|---|---|---|---|
|
|
260
|
+
| ResizeObserver | 64+ | 69+ | 13.1+ | 79+ |
|
|
261
|
+
| MutationObserver | 26+ | 14+ | 7+ | 12+ |
|
|
262
|
+
| ES Modules | 61+ | 60+ | 10.1+ | 16+ |
|
|
263
|
+
|
|
264
|
+
For very old browsers, Height Harmony automatically falls back to a debounced `window.resize` listener.
|
|
213
265
|
|
|
214
|
-
|
|
215
|
-
2. Finds the tallest element in the group
|
|
216
|
-
3. Sets all elements to match the tallest height
|
|
266
|
+
---
|
|
217
267
|
|
|
218
268
|
## License
|
|
219
269
|
|
|
220
|
-
MIT
|
|
270
|
+
[MIT](LICENSE) © Byron Johnson
|
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
function heightHarmony(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
elements.forEach((element) => {
|
|
15
|
-
element.style.height = maxHeight + "px";
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
heightHarmony.version = "1.0.0";
|
|
20
|
-
export {
|
|
21
|
-
heightHarmony as default
|
|
22
|
-
};
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).heightHarmony={})}(this,function(e){"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Height Harmony v2.0.0
|
|
4
|
+
* The fastest, smartest equal-height JavaScript utility on the web.
|
|
5
|
+
*
|
|
6
|
+
* Automatically synchronizes element heights using ResizeObserver and
|
|
7
|
+
* MutationObserver — no manual resize listeners needed.
|
|
8
|
+
*
|
|
9
|
+
* @author Byron Johnson
|
|
10
|
+
* @license MIT
|
|
11
|
+
* @see https://byronjohnson.github.io/height-harmony/demo/
|
|
12
|
+
*/function t(e,t){let s=null;return function(...i){null!==s&&clearTimeout(s),t>0?s=setTimeout(()=>{s=null,requestAnimationFrame(()=>e.apply(this,i))},t):requestAnimationFrame(()=>e.apply(this,i))}}class s{constructor(e,s={}){this._target=e,this._opts=Object.assign({debounce:0,minHeight:!1,breakpoint:0,watch:!0,transitions:!0},s),this._destroyed=!1,this._resizeObserver=null,this._mutationObserver=null,this._cleanupFallback=null,this._debouncedSync=t(this._sync.bind(this),this._opts.debounce),this._sync(),this._opts.watch&&this._setupObservers()}refresh(){return this._destroyed||this._sync(),this}destroy(){if(this._destroyed)return this;this._destroyed=!0,this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._mutationObserver&&(this._mutationObserver.disconnect(),this._mutationObserver=null),this._cleanupFallback&&(this._cleanupFallback(),this._cleanupFallback=null);const e=this._opts.minHeight?"min-height":"height";return this._getElements().forEach(t=>{t.style.removeProperty(e),t.style.removeProperty("box-sizing"),this._opts.transitions&&t.style.removeProperty("transition")}),this}_getElements(){return"string"==typeof this._target?Array.from(document.querySelectorAll(this._target)):this._target instanceof NodeList||Array.isArray(this._target)?Array.from(this._target):this._target instanceof HTMLElement?[this._target]:[]}_sync(){if(this._destroyed)return;const e=this._getElements();if(0===e.length)return;if(this._opts.breakpoint>0&&window.innerWidth<this._opts.breakpoint){const t=this._opts.minHeight?"min-height":"height";return void e.forEach(e=>e.style.removeProperty(t))}const t=this._opts.minHeight?"min-height":"height";e.forEach(e=>{e.style.setProperty(t,"","important"),e.style.setProperty("box-sizing","border-box","important")});let s=0;e.forEach(e=>{const t=e.offsetHeight;t>s&&(s=t)}),0!==s&&e.forEach(e=>{this._opts.transitions&&e.style.setProperty("transition",`${t} 0.2s ease`,""),e.style.setProperty(t,`${s}px`,"important")})}_setupObservers(){if("undefined"!=typeof ResizeObserver)this._resizeObserver=new ResizeObserver(e=>{e.length>0&&this._debouncedSync()}),(()=>{this._getElements().forEach(e=>this._resizeObserver.observe(e))})();else{const e=t(this._sync.bind(this),Math.max(this._opts.debounce,150)),s=()=>setTimeout(()=>this._sync(),300);window.addEventListener("resize",e,{passive:!0}),window.addEventListener("orientationchange",s,{passive:!0}),this._cleanupFallback=()=>{window.removeEventListener("resize",e),window.removeEventListener("orientationchange",s)}}if("undefined"!=typeof MutationObserver){const e=this._getElements(),t=new Set(e.map(e=>e.parentElement).filter(Boolean));t.size>0&&(this._mutationObserver=new MutationObserver(e=>{e.some(e=>e.addedNodes.length>0||e.removedNodes.length>0)&&(this._debouncedSync(),this._resizeObserver&&this._getElements().forEach(e=>{try{this._resizeObserver.observe(e)}catch(t){}}))}),t.forEach(e=>{this._mutationObserver.observe(e,{childList:!0,subtree:!1})}))}}}function i(e,t){return new s(e,t)}i.version="2.0.0",i.autoInit=function(e={}){const t=document.querySelectorAll("[data-hh-group]");if(0===t.length)return[];const i=new Map;t.forEach(e=>{const t=e.getAttribute("data-hh-group");i.has(t)||i.set(t,[]),i.get(t).push(e)});const r=[];return i.forEach(t=>{r.push(new s(t,e))}),r},e.HeightHarmonyInstance=s,e.default=i,Object.defineProperties(e,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height Harmony v2.0.0
|
|
3
|
+
* The fastest, smartest equal-height JavaScript utility on the web.
|
|
4
|
+
*
|
|
5
|
+
* Automatically synchronizes element heights using ResizeObserver and
|
|
6
|
+
* MutationObserver — no manual resize listeners needed.
|
|
7
|
+
*
|
|
8
|
+
* @author Byron Johnson
|
|
9
|
+
* @license MIT
|
|
10
|
+
* @see https://byronjohnson.github.io/height-harmony/demo/
|
|
11
|
+
*/
|
|
12
|
+
const VERSION = "2.0.0";
|
|
13
|
+
function debounce(fn, wait) {
|
|
14
|
+
let timer = null;
|
|
15
|
+
return function debounced(...args) {
|
|
16
|
+
if (timer !== null) clearTimeout(timer);
|
|
17
|
+
if (wait > 0) {
|
|
18
|
+
timer = setTimeout(() => {
|
|
19
|
+
timer = null;
|
|
20
|
+
requestAnimationFrame(() => fn.apply(this, args));
|
|
21
|
+
}, wait);
|
|
22
|
+
} else {
|
|
23
|
+
requestAnimationFrame(() => fn.apply(this, args));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
class HeightHarmonyInstance {
|
|
28
|
+
/**
|
|
29
|
+
* @param {string|NodeList|HTMLElement[]} target CSS selector or element list
|
|
30
|
+
* @param {HeightHarmonyOptions} options
|
|
31
|
+
*/
|
|
32
|
+
constructor(target, options = {}) {
|
|
33
|
+
this._target = target;
|
|
34
|
+
this._opts = Object.assign(
|
|
35
|
+
{ debounce: 0, minHeight: false, breakpoint: 0, watch: true, transitions: true },
|
|
36
|
+
options
|
|
37
|
+
);
|
|
38
|
+
this._destroyed = false;
|
|
39
|
+
this._resizeObserver = null;
|
|
40
|
+
this._mutationObserver = null;
|
|
41
|
+
this._cleanupFallback = null;
|
|
42
|
+
this._debouncedSync = debounce(this._sync.bind(this), this._opts.debounce);
|
|
43
|
+
this._sync();
|
|
44
|
+
if (this._opts.watch) {
|
|
45
|
+
this._setupObservers();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Manually triggers a height re-calculation.
|
|
51
|
+
* Useful after CSS transitions finish or after content changes you control.
|
|
52
|
+
* @returns {this}
|
|
53
|
+
*/
|
|
54
|
+
refresh() {
|
|
55
|
+
if (this._destroyed) return this;
|
|
56
|
+
this._sync();
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Tears down all observers, removes inline height/transition styles set by
|
|
61
|
+
* this instance, and marks the instance as destroyed.
|
|
62
|
+
* @returns {this}
|
|
63
|
+
*/
|
|
64
|
+
destroy() {
|
|
65
|
+
if (this._destroyed) return this;
|
|
66
|
+
this._destroyed = true;
|
|
67
|
+
if (this._resizeObserver) {
|
|
68
|
+
this._resizeObserver.disconnect();
|
|
69
|
+
this._resizeObserver = null;
|
|
70
|
+
}
|
|
71
|
+
if (this._mutationObserver) {
|
|
72
|
+
this._mutationObserver.disconnect();
|
|
73
|
+
this._mutationObserver = null;
|
|
74
|
+
}
|
|
75
|
+
if (this._cleanupFallback) {
|
|
76
|
+
this._cleanupFallback();
|
|
77
|
+
this._cleanupFallback = null;
|
|
78
|
+
}
|
|
79
|
+
const prop = this._opts.minHeight ? "min-height" : "height";
|
|
80
|
+
this._getElements().forEach((el) => {
|
|
81
|
+
el.style.removeProperty(prop);
|
|
82
|
+
el.style.removeProperty("box-sizing");
|
|
83
|
+
if (this._opts.transitions) {
|
|
84
|
+
el.style.removeProperty("transition");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
90
|
+
/**
|
|
91
|
+
* Resolves the target into an array of HTMLElements.
|
|
92
|
+
* @returns {HTMLElement[]}
|
|
93
|
+
*/
|
|
94
|
+
_getElements() {
|
|
95
|
+
if (typeof this._target === "string") {
|
|
96
|
+
return Array.from(document.querySelectorAll(this._target));
|
|
97
|
+
}
|
|
98
|
+
if (this._target instanceof NodeList || Array.isArray(this._target)) {
|
|
99
|
+
return Array.from(this._target);
|
|
100
|
+
}
|
|
101
|
+
if (this._target instanceof HTMLElement) {
|
|
102
|
+
return [this._target];
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Core synchronization routine.
|
|
108
|
+
* Measures natural heights, finds the max, applies it to all elements.
|
|
109
|
+
*/
|
|
110
|
+
_sync() {
|
|
111
|
+
if (this._destroyed) return;
|
|
112
|
+
const elements = this._getElements();
|
|
113
|
+
if (elements.length === 0) return;
|
|
114
|
+
if (this._opts.breakpoint > 0 && window.innerWidth < this._opts.breakpoint) {
|
|
115
|
+
const prop2 = this._opts.minHeight ? "min-height" : "height";
|
|
116
|
+
elements.forEach((el) => el.style.removeProperty(prop2));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const prop = this._opts.minHeight ? "min-height" : "height";
|
|
120
|
+
elements.forEach((el) => {
|
|
121
|
+
el.style.setProperty(prop, "", "important");
|
|
122
|
+
el.style.setProperty("box-sizing", "border-box", "important");
|
|
123
|
+
});
|
|
124
|
+
let maxH = 0;
|
|
125
|
+
elements.forEach((el) => {
|
|
126
|
+
const h = el.offsetHeight;
|
|
127
|
+
if (h > maxH) maxH = h;
|
|
128
|
+
});
|
|
129
|
+
if (maxH === 0) return;
|
|
130
|
+
elements.forEach((el) => {
|
|
131
|
+
if (this._opts.transitions) {
|
|
132
|
+
el.style.setProperty("transition", `${prop} 0.2s ease`, "");
|
|
133
|
+
}
|
|
134
|
+
el.style.setProperty(prop, `${maxH}px`, "important");
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Sets up ResizeObserver to watch each element and MutationObserver to
|
|
139
|
+
* watch the parent containers for new elements being added.
|
|
140
|
+
*/
|
|
141
|
+
_setupObservers() {
|
|
142
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
143
|
+
this._resizeObserver = new ResizeObserver((entries) => {
|
|
144
|
+
if (entries.length > 0) {
|
|
145
|
+
this._debouncedSync();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const observe = () => {
|
|
149
|
+
this._getElements().forEach((el) => this._resizeObserver.observe(el));
|
|
150
|
+
};
|
|
151
|
+
observe();
|
|
152
|
+
} else {
|
|
153
|
+
const handler = debounce(this._sync.bind(this), Math.max(this._opts.debounce, 150));
|
|
154
|
+
const orientationHandler = () => setTimeout(() => this._sync(), 300);
|
|
155
|
+
window.addEventListener("resize", handler, { passive: true });
|
|
156
|
+
window.addEventListener("orientationchange", orientationHandler, { passive: true });
|
|
157
|
+
this._cleanupFallback = () => {
|
|
158
|
+
window.removeEventListener("resize", handler);
|
|
159
|
+
window.removeEventListener("orientationchange", orientationHandler);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (typeof MutationObserver !== "undefined") {
|
|
163
|
+
const elements = this._getElements();
|
|
164
|
+
const parents = new Set(elements.map((el) => el.parentElement).filter(Boolean));
|
|
165
|
+
if (parents.size > 0) {
|
|
166
|
+
this._mutationObserver = new MutationObserver((mutations) => {
|
|
167
|
+
const hasNewNodes = mutations.some((m) => m.addedNodes.length > 0 || m.removedNodes.length > 0);
|
|
168
|
+
if (hasNewNodes) {
|
|
169
|
+
this._debouncedSync();
|
|
170
|
+
if (this._resizeObserver) {
|
|
171
|
+
this._getElements().forEach((el) => {
|
|
172
|
+
try {
|
|
173
|
+
this._resizeObserver.observe(el);
|
|
174
|
+
} catch (_) {
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
parents.forEach((parent) => {
|
|
181
|
+
this._mutationObserver.observe(parent, { childList: true, subtree: false });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function heightHarmony(target, opts) {
|
|
188
|
+
return new HeightHarmonyInstance(target, opts);
|
|
189
|
+
}
|
|
190
|
+
heightHarmony.version = VERSION;
|
|
191
|
+
heightHarmony.autoInit = function autoInit(opts = {}) {
|
|
192
|
+
const all = document.querySelectorAll("[data-hh-group]");
|
|
193
|
+
if (all.length === 0) return [];
|
|
194
|
+
const groups = /* @__PURE__ */ new Map();
|
|
195
|
+
all.forEach((el) => {
|
|
196
|
+
const key = el.getAttribute("data-hh-group");
|
|
197
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
198
|
+
groups.get(key).push(el);
|
|
199
|
+
});
|
|
200
|
+
const instances = [];
|
|
201
|
+
groups.forEach((elements) => {
|
|
202
|
+
instances.push(new HeightHarmonyInstance(elements, opts));
|
|
203
|
+
});
|
|
204
|
+
return instances;
|
|
205
|
+
};
|
|
206
|
+
export {
|
|
207
|
+
HeightHarmonyInstance,
|
|
208
|
+
heightHarmony as default
|
|
209
|
+
};
|
package/height-harmony.js
CHANGED
|
@@ -1,46 +1,316 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Height Harmony v2.0.0
|
|
3
|
+
* The fastest, smartest equal-height JavaScript utility on the web.
|
|
4
|
+
*
|
|
5
|
+
* Automatically synchronizes element heights using ResizeObserver and
|
|
6
|
+
* MutationObserver — no manual resize listeners needed.
|
|
7
|
+
*
|
|
8
|
+
* @author Byron Johnson
|
|
9
|
+
* @license MIT
|
|
10
|
+
* @see https://byronjohnson.github.io/height-harmony/demo/
|
|
5
11
|
*/
|
|
6
|
-
function heightHarmony(selector) {
|
|
7
|
-
// Get all matching elements
|
|
8
|
-
const elements = document.querySelectorAll(selector);
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} HeightHarmonyOptions
|
|
15
|
+
* @property {number} [debounce=0] - Milliseconds to debounce resize/mutation callbacks (0 = rAF only)
|
|
16
|
+
* @property {boolean} [minHeight=false] - Use min-height instead of height, allowing elements to grow taller
|
|
17
|
+
* @property {number} [breakpoint=0] - Disable harmonizing below this viewport width (px); 0 = always on
|
|
18
|
+
* @property {boolean} [watch=true] - Auto-watch via ResizeObserver and MutationObserver
|
|
19
|
+
* @property {boolean} [transitions=true] - Apply CSS transition on height changes for smooth animation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const VERSION = '2.0.0';
|
|
23
|
+
|
|
24
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a debounced version of fn that fires after `wait` ms of inactivity.
|
|
28
|
+
* If wait === 0 we skip the timeout and only use requestAnimationFrame.
|
|
29
|
+
* @param {Function} fn
|
|
30
|
+
* @param {number} wait
|
|
31
|
+
* @returns {Function}
|
|
32
|
+
*/
|
|
33
|
+
function debounce(fn, wait) {
|
|
34
|
+
let timer = null;
|
|
35
|
+
return function debounced(...args) {
|
|
36
|
+
if (timer !== null) clearTimeout(timer);
|
|
37
|
+
if (wait > 0) {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
timer = null;
|
|
40
|
+
requestAnimationFrame(() => fn.apply(this, args));
|
|
41
|
+
}, wait);
|
|
42
|
+
} else {
|
|
43
|
+
requestAnimationFrame(() => fn.apply(this, args));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── HeightHarmonyInstance ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
class HeightHarmonyInstance {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string|NodeList|HTMLElement[]} target CSS selector or element list
|
|
53
|
+
* @param {HeightHarmonyOptions} options
|
|
54
|
+
*/
|
|
55
|
+
constructor(target, options = {}) {
|
|
56
|
+
this._target = target;
|
|
57
|
+
this._opts = Object.assign(
|
|
58
|
+
{ debounce: 0, minHeight: false, breakpoint: 0, watch: true, transitions: true },
|
|
59
|
+
options
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
this._destroyed = false;
|
|
63
|
+
this._resizeObserver = null;
|
|
64
|
+
this._mutationObserver = null;
|
|
65
|
+
this._cleanupFallback = null;
|
|
66
|
+
this._debouncedSync = debounce(this._sync.bind(this), this._opts.debounce);
|
|
67
|
+
|
|
68
|
+
// Run immediately
|
|
69
|
+
this._sync();
|
|
70
|
+
|
|
71
|
+
// Set up observers if watching is enabled
|
|
72
|
+
if (this._opts.watch) {
|
|
73
|
+
this._setupObservers();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Manually triggers a height re-calculation.
|
|
81
|
+
* Useful after CSS transitions finish or after content changes you control.
|
|
82
|
+
* @returns {this}
|
|
83
|
+
*/
|
|
84
|
+
refresh() {
|
|
85
|
+
if (this._destroyed) return this;
|
|
86
|
+
this._sync();
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tears down all observers, removes inline height/transition styles set by
|
|
92
|
+
* this instance, and marks the instance as destroyed.
|
|
93
|
+
* @returns {this}
|
|
94
|
+
*/
|
|
95
|
+
destroy() {
|
|
96
|
+
if (this._destroyed) return this;
|
|
97
|
+
this._destroyed = true;
|
|
98
|
+
|
|
99
|
+
if (this._resizeObserver) {
|
|
100
|
+
this._resizeObserver.disconnect();
|
|
101
|
+
this._resizeObserver = null;
|
|
102
|
+
}
|
|
103
|
+
if (this._mutationObserver) {
|
|
104
|
+
this._mutationObserver.disconnect();
|
|
105
|
+
this._mutationObserver = null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Clean up window event listeners added by the ResizeObserver fallback
|
|
109
|
+
if (this._cleanupFallback) {
|
|
110
|
+
this._cleanupFallback();
|
|
111
|
+
this._cleanupFallback = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Remove all inline styles we set
|
|
115
|
+
const prop = this._opts.minHeight ? 'min-height' : 'height';
|
|
116
|
+
this._getElements().forEach(el => {
|
|
117
|
+
el.style.removeProperty(prop);
|
|
118
|
+
el.style.removeProperty('box-sizing');
|
|
119
|
+
if (this._opts.transitions) {
|
|
120
|
+
el.style.removeProperty('transition');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Private ─────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Resolves the target into an array of HTMLElements.
|
|
131
|
+
* @returns {HTMLElement[]}
|
|
132
|
+
*/
|
|
133
|
+
_getElements() {
|
|
134
|
+
if (typeof this._target === 'string') {
|
|
135
|
+
return Array.from(document.querySelectorAll(this._target));
|
|
136
|
+
}
|
|
137
|
+
if (this._target instanceof NodeList || Array.isArray(this._target)) {
|
|
138
|
+
return Array.from(this._target);
|
|
139
|
+
}
|
|
140
|
+
if (this._target instanceof HTMLElement) {
|
|
141
|
+
return [this._target];
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Core synchronization routine.
|
|
148
|
+
* Measures natural heights, finds the max, applies it to all elements.
|
|
149
|
+
*/
|
|
150
|
+
_sync() {
|
|
151
|
+
if (this._destroyed) return;
|
|
152
|
+
|
|
153
|
+
const elements = this._getElements();
|
|
11
154
|
if (elements.length === 0) return;
|
|
12
155
|
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
156
|
+
// Check breakpoint — disable below threshold
|
|
157
|
+
if (this._opts.breakpoint > 0 && window.innerWidth < this._opts.breakpoint) {
|
|
158
|
+
const prop = this._opts.minHeight ? 'min-height' : 'height';
|
|
159
|
+
elements.forEach(el => el.style.removeProperty(prop));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const prop = this._opts.minHeight ? 'min-height' : 'height';
|
|
164
|
+
|
|
165
|
+
// Step 1: Strip current inline heights so we read natural layout heights
|
|
166
|
+
elements.forEach(el => {
|
|
167
|
+
el.style.setProperty(prop, '', 'important');
|
|
168
|
+
// Ensure border-box so our offsetHeight read is reliable
|
|
169
|
+
el.style.setProperty('box-sizing', 'border-box', 'important');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Step 2: Force a synchronous layout read (single batch)
|
|
173
|
+
// We use offsetHeight (includes padding/border, respects box model)
|
|
174
|
+
let maxH = 0;
|
|
175
|
+
elements.forEach(el => {
|
|
176
|
+
const h = el.offsetHeight;
|
|
177
|
+
if (h > maxH) maxH = h;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (maxH === 0) return;
|
|
181
|
+
|
|
182
|
+
// Step 3: Apply the max height to all elements
|
|
183
|
+
elements.forEach(el => {
|
|
184
|
+
if (this._opts.transitions) {
|
|
185
|
+
el.style.setProperty('transition', `${prop} 0.2s ease`, '');
|
|
186
|
+
}
|
|
187
|
+
el.style.setProperty(prop, `${maxH}px`, 'important');
|
|
16
188
|
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Sets up ResizeObserver to watch each element and MutationObserver to
|
|
193
|
+
* watch the parent containers for new elements being added.
|
|
194
|
+
*/
|
|
195
|
+
_setupObservers() {
|
|
196
|
+
// ResizeObserver: re-sync whenever any observed element changes size
|
|
197
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
198
|
+
this._resizeObserver = new ResizeObserver(entries => {
|
|
199
|
+
// Only fire if at least one entry has a real size change
|
|
200
|
+
if (entries.length > 0) {
|
|
201
|
+
this._debouncedSync();
|
|
202
|
+
}
|
|
203
|
+
});
|
|
17
204
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let maxHeight = 0;
|
|
205
|
+
const observe = () => {
|
|
206
|
+
this._getElements().forEach(el => this._resizeObserver.observe(el));
|
|
207
|
+
};
|
|
22
208
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
209
|
+
observe();
|
|
210
|
+
} else {
|
|
211
|
+
// Fallback for browsers without ResizeObserver (very old Safari, etc.)
|
|
212
|
+
const handler = debounce(this._sync.bind(this), Math.max(this._opts.debounce, 150));
|
|
213
|
+
const orientationHandler = () => setTimeout(() => this._sync(), 300);
|
|
27
214
|
|
|
28
|
-
|
|
29
|
-
|
|
215
|
+
window.addEventListener('resize', handler, { passive: true });
|
|
216
|
+
window.addEventListener('orientationchange', orientationHandler, { passive: true });
|
|
30
217
|
|
|
31
|
-
|
|
32
|
-
|
|
218
|
+
// Store cleanup so destroy() can remove both listeners
|
|
219
|
+
this._cleanupFallback = () => {
|
|
220
|
+
window.removeEventListener('resize', handler);
|
|
221
|
+
window.removeEventListener('orientationchange', orientationHandler);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// MutationObserver: re-sync when new children are added to parent containers
|
|
226
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
227
|
+
const elements = this._getElements();
|
|
228
|
+
const parents = new Set(elements.map(el => el.parentElement).filter(Boolean));
|
|
229
|
+
|
|
230
|
+
if (parents.size > 0) {
|
|
231
|
+
this._mutationObserver = new MutationObserver(mutations => {
|
|
232
|
+
const hasNewNodes = mutations.some(m => m.addedNodes.length > 0 || m.removedNodes.length > 0);
|
|
233
|
+
if (hasNewNodes) {
|
|
234
|
+
this._debouncedSync();
|
|
235
|
+
// Re-observe any new elements (ResizeObserver)
|
|
236
|
+
if (this._resizeObserver) {
|
|
237
|
+
this._getElements().forEach(el => {
|
|
238
|
+
try { this._resizeObserver.observe(el); } catch (_) {}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
33
242
|
});
|
|
34
243
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
element.style.height = maxHeight + 'px';
|
|
244
|
+
parents.forEach(parent => {
|
|
245
|
+
this._mutationObserver.observe(parent, { childList: true, subtree: false });
|
|
38
246
|
});
|
|
39
|
-
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
40
250
|
}
|
|
41
251
|
|
|
42
|
-
//
|
|
43
|
-
|
|
252
|
+
// ─── Public factory function ──────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* heightHarmony — equalizes the heights of all elements matching `target`.
|
|
256
|
+
*
|
|
257
|
+
* @param {string|NodeList|HTMLElement[]} target - CSS selector or element collection
|
|
258
|
+
* @param {HeightHarmonyOptions} [opts] - Configuration options
|
|
259
|
+
* @returns {HeightHarmonyInstance} - Instance with refresh() and destroy() methods
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* // Basic usage (same as v1)
|
|
263
|
+
* heightHarmony('.card');
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // With options
|
|
267
|
+
* heightHarmony('.card', { debounce: 100, breakpoint: 768 });
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* // Store instance for later control
|
|
271
|
+
* const hh = heightHarmony('.card');
|
|
272
|
+
* hh.refresh(); // manual re-trigger
|
|
273
|
+
* hh.destroy(); // clean up observers & styles
|
|
274
|
+
*/
|
|
275
|
+
function heightHarmony(target, opts) {
|
|
276
|
+
return new HeightHarmonyInstance(target, opts);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── Static metadata ──────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
heightHarmony.version = VERSION;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Auto-initializes all elements with `data-hh-group` attributes.
|
|
285
|
+
* Groups elements sharing the same data-hh-group value and harmonizes each group.
|
|
286
|
+
*
|
|
287
|
+
* Usage in HTML:
|
|
288
|
+
* <div data-hh-group="cards">...</div>
|
|
289
|
+
* <div data-hh-group="cards">...</div>
|
|
290
|
+
*
|
|
291
|
+
* @param {HeightHarmonyOptions} [opts] - Options applied to all groups
|
|
292
|
+
* @returns {HeightHarmonyInstance[]} - Array of instances, one per group
|
|
293
|
+
*/
|
|
294
|
+
heightHarmony.autoInit = function autoInit(opts = {}) {
|
|
295
|
+
const all = document.querySelectorAll('[data-hh-group]');
|
|
296
|
+
if (all.length === 0) return [];
|
|
297
|
+
|
|
298
|
+
/** @type {Map<string, HTMLElement[]>} */
|
|
299
|
+
const groups = new Map();
|
|
300
|
+
all.forEach(el => {
|
|
301
|
+
const key = el.getAttribute('data-hh-group');
|
|
302
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
303
|
+
groups.get(key).push(el);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const instances = [];
|
|
307
|
+
groups.forEach((elements) => {
|
|
308
|
+
instances.push(new HeightHarmonyInstance(elements, opts));
|
|
309
|
+
});
|
|
310
|
+
return instances;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
44
314
|
|
|
45
|
-
|
|
315
|
+
export { HeightHarmonyInstance };
|
|
46
316
|
export default heightHarmony;
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "height-harmony",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "A lightweight, zero-dependency JavaScript utility for equalizing element heights",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org/",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
5
9
|
"main": "dist/height-harmony-min.js",
|
|
6
|
-
"module": "height-harmony.js",
|
|
10
|
+
"module": "dist/height-harmony.es.js",
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
13
|
+
"browser": "./dist/height-harmony-min.js",
|
|
14
|
+
"import": "./dist/height-harmony.es.js",
|
|
15
|
+
"require": "./dist/height-harmony-min.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
14
18
|
"browser": "dist/height-harmony-min.js",
|
|
@@ -17,12 +21,15 @@
|
|
|
17
21
|
"files": [
|
|
18
22
|
"height-harmony.js",
|
|
19
23
|
"dist/height-harmony-min.js",
|
|
24
|
+
"dist/height-harmony.es.js",
|
|
20
25
|
"README.md",
|
|
21
26
|
"LICENSE"
|
|
22
27
|
],
|
|
23
28
|
"scripts": {
|
|
24
29
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
25
30
|
"build": "vite build",
|
|
31
|
+
"build:demo": "cp dist/height-harmony-min.js demo/height-harmony-min.js && vite build --config demo/vite.config.js && cp dist/height-harmony-min.js demo-dist/height-harmony-min.js",
|
|
32
|
+
"build:all": "npm run build && npm run build:demo",
|
|
26
33
|
"dev": "vite build --watch",
|
|
27
34
|
"prepublishOnly": "npm run build"
|
|
28
35
|
},
|
|
@@ -37,13 +44,19 @@
|
|
|
37
44
|
"keywords": [
|
|
38
45
|
"javascript",
|
|
39
46
|
"height",
|
|
47
|
+
"equal-height",
|
|
40
48
|
"equalize",
|
|
41
49
|
"responsive",
|
|
42
50
|
"css",
|
|
43
51
|
"utility",
|
|
44
52
|
"frontend",
|
|
45
53
|
"dom",
|
|
46
|
-
"elements"
|
|
54
|
+
"elements",
|
|
55
|
+
"resize-observer",
|
|
56
|
+
"mutation-observer",
|
|
57
|
+
"auto",
|
|
58
|
+
"layout",
|
|
59
|
+
"zero-dependency"
|
|
47
60
|
],
|
|
48
61
|
"author": "Byron Johnson",
|
|
49
62
|
"license": "MIT",
|
|
@@ -52,6 +65,6 @@
|
|
|
52
65
|
"vite": "^6.3.6"
|
|
53
66
|
},
|
|
54
67
|
"engines": {
|
|
55
|
-
"node": ">=0.
|
|
68
|
+
"node": ">=14.0.0"
|
|
56
69
|
}
|
|
57
|
-
}
|
|
70
|
+
}
|