v-fit-children 1.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/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/vFitChildren.d.ts +16 -0
- package/dist/vFitChildren.d.ts.map +1 -0
- package/dist/vFitChildren.js +324 -0
- package/dist/vFitChildren.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ozgur Seyidoglu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# v-fit-children
|
|
2
|
+
|
|
3
|
+
## Vue directive to hide overflowing children automagically ‼️
|
|
4
|
+
|
|
5
|
+
> **Note:** Watch the [usage example video](https://github.com/ozJSey/v-fit-children-resources/blob/main/Screen%20Recording%202026-02-08%20at%2019.09.25.mov) to see the directive in action (temporary link).
|
|
6
|
+
|
|
7
|
+
A Vue 3 directive that automatically hides child elements that don't fit within a container's width. Ideal for chips, badges, tags, or any inline elements in a tight space.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Hides children that overflow the container width
|
|
12
|
+
- Emits a custom event with hidden children count and references (for "+N more" indicators)
|
|
13
|
+
- Supports `gap` / `column-gap` in parent container
|
|
14
|
+
- Accounts for margins, padding, and borders on both container and children
|
|
15
|
+
- Option to hide largest items first (`sortBySize`) to maximize visible count
|
|
16
|
+
- Pin specific children so they are never hidden (`keepVisibleEl` or `data-v-fit-keep`)
|
|
17
|
+
- Multi-row support via `rowCount`
|
|
18
|
+
- Responds to container resizes via `ResizeObserver`
|
|
19
|
+
- Monitors individual child size changes via `ResizeObserver`
|
|
20
|
+
- Detects child additions/removals via `MutationObserver`
|
|
21
|
+
- Caches child widths — only re-measures when children change
|
|
22
|
+
- Batches recalculations with `requestAnimationFrame` for performance
|
|
23
|
+
- Written in TypeScript — ships with full type declarations
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install v-fit-children
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Vue 3 is a peer dependency — it won't be bundled.
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```vue
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { ref } from "vue";
|
|
38
|
+
import { vFitChildren } from "v-fit-children";
|
|
39
|
+
|
|
40
|
+
const containerRef = ref<HTMLElement>();
|
|
41
|
+
const hiddenCount = ref(0);
|
|
42
|
+
|
|
43
|
+
function onUpdate(e: CustomEvent) {
|
|
44
|
+
hiddenCount.value = e.detail.hiddenChildrenCount;
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<div ref="containerRef">
|
|
50
|
+
<div
|
|
51
|
+
v-fit-children="{
|
|
52
|
+
widthRestrictingContainer: containerRef, // Optional: defaults to this element
|
|
53
|
+
offsetNeededInPx: 50, // Optional: defaults to 50
|
|
54
|
+
}"
|
|
55
|
+
@fit-children-updated="onUpdate"
|
|
56
|
+
>
|
|
57
|
+
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
|
|
58
|
+
</div>
|
|
59
|
+
<span v-if="hiddenCount">+{{ hiddenCount }} more</span>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The directive element and the width-restricting container can be the same element or different elements. When they differ, the directive element's own margin, border, and padding are subtracted from the available space.
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
All options are passed as the directive value:
|
|
69
|
+
|
|
70
|
+
```vue
|
|
71
|
+
<div v-fit-children="{ widthRestrictingContainer: containerRef, offsetNeededInPx: 80 }">
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Description |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| `widthRestrictingContainer` | `HTMLElement` | Directive Element | The element whose width constrains the children. Defaults to the element the directive is on. |
|
|
77
|
+
| `offsetNeededInPx` | `number` | `50` | Reserved space in px (e.g. for a "+N more" badge). Set to `0` if you don't need reserved space. |
|
|
78
|
+
| `gap` | `number` | Computed `gap` | Manually specify the gap between items in pixels. Useful if `gap` CSS is not used (e.g. inline-block margins). |
|
|
79
|
+
| `sortBySize` | `boolean` | `false` | If `true`, hides larger items first to maximize the number of visible items. If `false`, hides items from the end. |
|
|
80
|
+
| `keepVisibleEl` | `HTMLElement` | — | An element (or descendant of a child) that should never be hidden. Useful for inputs or interactive elements. |
|
|
81
|
+
| `rowCount` | `number` | `1` | Number of rows to fill before hiding. Offset is only reserved on the last row. If >1, sets `flex-wrap: wrap`. |
|
|
82
|
+
|
|
83
|
+
Options are reactive — changing them via the directive value triggers a recalculation.
|
|
84
|
+
|
|
85
|
+
## TypeScript
|
|
86
|
+
|
|
87
|
+
The package ships with full type declarations. Exported types:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { vFitChildren } from "v-fit-children";
|
|
91
|
+
import type { FitChildrenOptions, FitChildrenEventDetail } from "v-fit-children";
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `FitChildrenOptions`
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface FitChildrenOptions {
|
|
98
|
+
gap?: number;
|
|
99
|
+
keepVisibleEl?: HTMLElement;
|
|
100
|
+
offsetNeededInPx?: number;
|
|
101
|
+
rowCount?: number;
|
|
102
|
+
sortBySize?: boolean;
|
|
103
|
+
widthRestrictingContainer?: HTMLElement;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `FitChildrenEventDetail`
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
type FitChildrenEventDetail = {
|
|
111
|
+
hiddenChildren: HTMLElement[];
|
|
112
|
+
hiddenChildrenCount: number;
|
|
113
|
+
isOverflowing: boolean;
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Typing the event handler
|
|
118
|
+
|
|
119
|
+
Vue's `@fit-children-updated` handler receives a `CustomEvent`. You can type it like this:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
function onUpdate(e: CustomEvent<FitChildrenEventDetail>) {
|
|
123
|
+
console.log(e.detail.hiddenChildrenCount);
|
|
124
|
+
console.log(e.detail.hiddenChildren); // HTMLElement[]
|
|
125
|
+
console.log(e.detail.isOverflowing); // boolean
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Event
|
|
130
|
+
|
|
131
|
+
The directive dispatches a `fit-children-updated` custom event on the directive's element whenever visibility is recalculated.
|
|
132
|
+
|
|
133
|
+
```vue
|
|
134
|
+
<div
|
|
135
|
+
v-fit-children="{ widthRestrictingContainer: containerRef }"
|
|
136
|
+
@fit-children-updated="onUpdate"
|
|
137
|
+
>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The event's `detail` contains:
|
|
141
|
+
|
|
142
|
+
| Property | Type | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `hiddenChildrenCount` | `number` | Number of children that were hidden |
|
|
145
|
+
| `hiddenChildren` | `HTMLElement[]` | Direct references to the hidden DOM elements |
|
|
146
|
+
| `isOverflowing` | `boolean` | `true` if any children were hidden, `false` if all fit |
|
|
147
|
+
|
|
148
|
+
When all children fit (including the offset), `isOverflowing` is `false` and no offset space is reserved — the "+N" badge is unnecessary.
|
|
149
|
+
|
|
150
|
+
## Keeping elements visible
|
|
151
|
+
|
|
152
|
+
You can prevent specific children from being hidden. This is useful for inputs, buttons, or any interactive element that should always remain accessible.
|
|
153
|
+
|
|
154
|
+
**Option A — via directive value (`keepVisibleEl`):**
|
|
155
|
+
|
|
156
|
+
Pass a ref to the element (or a descendant of a child) that should stay visible:
|
|
157
|
+
|
|
158
|
+
```vue
|
|
159
|
+
<script setup lang="ts">
|
|
160
|
+
import { ref } from "vue";
|
|
161
|
+
import { vFitChildren } from "v-fit-children";
|
|
162
|
+
|
|
163
|
+
const containerRef = ref<HTMLElement>();
|
|
164
|
+
const inputRef = ref<HTMLElement>();
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<template>
|
|
168
|
+
<div ref="containerRef">
|
|
169
|
+
<div v-fit-children="{ widthRestrictingContainer: containerRef, keepVisibleEl: inputRef }">
|
|
170
|
+
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
|
|
171
|
+
<div class="input-wrapper">
|
|
172
|
+
<input ref="inputRef" />
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</template>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The directive walks up from `keepVisibleEl` to find the matching immediate child. So if `inputRef` points to a nested `<input>`, the parent child that contains it stays visible.
|
|
180
|
+
|
|
181
|
+
**Option B — via data attribute (`data-v-fit-keep`):**
|
|
182
|
+
|
|
183
|
+
Add the `data-v-fit-keep` attribute directly on the child element — no ref needed:
|
|
184
|
+
|
|
185
|
+
```vue
|
|
186
|
+
<div v-fit-children="{ widthRestrictingContainer: containerRef }">
|
|
187
|
+
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
|
|
188
|
+
<div data-v-fit-keep>
|
|
189
|
+
<input />
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Both methods can be used together. If a kept element is wider than the available space, it stays visible anyway — better to overflow than to hide an input the user is typing in.
|
|
195
|
+
|
|
196
|
+
## Sorting by size
|
|
197
|
+
|
|
198
|
+
By default, children are hidden from the end (last child first). Enable `sortBySize` to hide the largest children first, maximizing the number of visible items:
|
|
199
|
+
|
|
200
|
+
```vue
|
|
201
|
+
<div v-fit-children="{ widthRestrictingContainer: containerRef, sortBySize: true }">
|
|
202
|
+
<span style="width: 200px">Wide tag</span>
|
|
203
|
+
<span style="width: 60px">Small</span>
|
|
204
|
+
<span style="width: 60px">Small</span>
|
|
205
|
+
</div>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
With `sortBySize: true`, the "Wide tag" is hidden first, leaving both small tags visible.
|
|
209
|
+
|
|
210
|
+
## Multi-row layout
|
|
211
|
+
|
|
212
|
+
Use `rowCount` to allow children to fill multiple rows before hiding:
|
|
213
|
+
|
|
214
|
+
```vue
|
|
215
|
+
<div
|
|
216
|
+
v-fit-children="{ widthRestrictingContainer: containerRef, rowCount: 2 }"
|
|
217
|
+
style="flex-wrap: wrap; gap: 8px;"
|
|
218
|
+
>
|
|
219
|
+
<span v-for="tag in manyTags" :key="tag">{{ tag }}</span>
|
|
220
|
+
</div>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The offset (`offsetNeededInPx`) is only reserved on the **last** row. All preceding rows use the full container width.
|
|
224
|
+
|
|
225
|
+
## Inline "+N" badge
|
|
226
|
+
|
|
227
|
+
To keep the badge inline with the chips (instead of below), wrap both in a flex container and give the directive element `flex: 1`:
|
|
228
|
+
|
|
229
|
+
```vue
|
|
230
|
+
<template>
|
|
231
|
+
<div ref="containerRef" style="display: flex; align-items: center; gap: 8px;">
|
|
232
|
+
<div
|
|
233
|
+
style="flex: 1; overflow: hidden;"
|
|
234
|
+
v-fit-children="{ widthRestrictingContainer: containerRef, offsetNeededInPx: 0 }"
|
|
235
|
+
@fit-children-updated="onUpdate"
|
|
236
|
+
>
|
|
237
|
+
<span v-for="tag in tags" :key="tag">{{ tag }}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<span v-if="hiddenCount">+{{ hiddenCount }}</span>
|
|
240
|
+
</div>
|
|
241
|
+
</template>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Set `offsetNeededInPx: 0` since the badge lives outside the directive element.
|
|
245
|
+
|
|
246
|
+
## How it works
|
|
247
|
+
|
|
248
|
+
1. On mount, the directive observes the `widthRestrictingContainer` for resize and the directive element for child list mutations.
|
|
249
|
+
2. Individual children are also observed for size changes (e.g. an input growing as the user types).
|
|
250
|
+
3. When triggered, it measures child widths via `getBoundingClientRect` (accounting for margins and `gap`). Measurements are cached and only re-computed when children are added, removed, or resized.
|
|
251
|
+
4. If all children fit without needing the offset, everything stays visible — no "+N" badge is needed.
|
|
252
|
+
5. Otherwise, candidates are sorted by priority (`keepVisibleEl` / `data-v-fit-keep` first), then by size (if `sortBySize`), then by DOM order.
|
|
253
|
+
6. Children are placed row by row (up to `rowCount`). Offset is only reserved on the last row.
|
|
254
|
+
7. A `fit-children-updated` event is dispatched so you can render a "+N more" indicator.
|
|
255
|
+
8. Recalculations are batched via `requestAnimationFrame` + Vue's `nextTick` to avoid layout thrashing.
|
|
256
|
+
|
|
257
|
+
## Known limitations
|
|
258
|
+
|
|
259
|
+
- The directive hides children using `display: none !important`. If a child has critical `display` styles set inline, they will be overridden while hidden.
|
|
260
|
+
- `keepVisibleEl` accepts a single element. To pin multiple children, use `data-v-fit-keep` on each. We won't hide them so it may break things
|
|
261
|
+
|
|
262
|
+
## Browser support
|
|
263
|
+
|
|
264
|
+
Requires browsers that support `ResizeObserver`, `MutationObserver`, and `getBoundingClientRect`. All modern browsers (Chrome, Firefox, Safari, Edge) are supported.
|
|
265
|
+
|
|
266
|
+
## License
|
|
267
|
+
|
|
268
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Directive } from "vue";
|
|
2
|
+
export type FitChildrenEventDetail = {
|
|
3
|
+
hiddenChildren: HTMLElement[];
|
|
4
|
+
hiddenChildrenCount: number;
|
|
5
|
+
isOverflowing: boolean;
|
|
6
|
+
};
|
|
7
|
+
export interface FitChildrenOptions {
|
|
8
|
+
gap?: number;
|
|
9
|
+
keepVisibleEl?: HTMLElement;
|
|
10
|
+
offsetNeededInPx?: number;
|
|
11
|
+
rowCount?: number;
|
|
12
|
+
sortBySize?: boolean;
|
|
13
|
+
widthRestrictingContainer?: HTMLElement;
|
|
14
|
+
}
|
|
15
|
+
export declare const vFitChildren: Directive;
|
|
16
|
+
//# sourceMappingURL=vFitChildren.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vFitChildren.d.ts","sourceRoot":"","sources":["../vFitChildren.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAmC,MAAM,KAAK,CAAC;AA8CtE,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,WAAW,EAAE,CAAC;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,WAAW,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yBAAyB,CAAC,EAAE,WAAW,CAAC;CACzC;AA8YD,eAAO,MAAM,YAAY,EAAE,SAyB1B,CAAC"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { nextTick } from "vue";
|
|
2
|
+
const DEFAULT_OFFSET_PX = 50;
|
|
3
|
+
const HIDDEN_ATTR = "data-v-fit-hidden";
|
|
4
|
+
const KEEP_ATTR = "data-v-fit-keep";
|
|
5
|
+
const EVENT_NAME = "fit-children-updated";
|
|
6
|
+
const stateMap = new WeakMap();
|
|
7
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
8
|
+
/** Parse a CSS pixel value, returning 0 for invalid/missing values. */
|
|
9
|
+
const parsePx = (value) => parseFloat(value) || 0;
|
|
10
|
+
/** Total horizontal overhead of an element (margin + border + padding). */
|
|
11
|
+
const getHorizontalOverhead = (style) => parsePx(style.marginLeft) +
|
|
12
|
+
parsePx(style.marginRight) +
|
|
13
|
+
parsePx(style.borderLeftWidth) +
|
|
14
|
+
parsePx(style.borderRightWidth) +
|
|
15
|
+
parsePx(style.paddingLeft) +
|
|
16
|
+
parsePx(style.paddingRight);
|
|
17
|
+
/** Content width of an element (inner width, excluding border and padding). */
|
|
18
|
+
const getContentWidth = (el, style = window.getComputedStyle(el)) => el.getBoundingClientRect().width -
|
|
19
|
+
parsePx(style.borderLeftWidth) -
|
|
20
|
+
parsePx(style.borderRightWidth) -
|
|
21
|
+
parsePx(style.paddingLeft) -
|
|
22
|
+
parsePx(style.paddingRight);
|
|
23
|
+
/** Outer width of an element (bounding rect width + horizontal margins). */
|
|
24
|
+
const getOuterWidth = (el) => {
|
|
25
|
+
const style = window.getComputedStyle(el);
|
|
26
|
+
return (el.getBoundingClientRect().width +
|
|
27
|
+
parsePx(style.marginLeft) +
|
|
28
|
+
parsePx(style.marginRight));
|
|
29
|
+
};
|
|
30
|
+
/** Whether a child should be kept visible (pinned via attribute or option). */
|
|
31
|
+
const isKeptChild = (child, keepVisibleEl) => child.hasAttribute(KEEP_ATTR) ||
|
|
32
|
+
(!!keepVisibleEl &&
|
|
33
|
+
(child === keepVisibleEl || child.contains(keepVisibleEl)));
|
|
34
|
+
// ── Visibility ───────────────────────────────────────────────────────
|
|
35
|
+
const dispatchUpdate = (el, detail) => {
|
|
36
|
+
el.dispatchEvent(new CustomEvent(EVENT_NAME, { detail }));
|
|
37
|
+
};
|
|
38
|
+
const showChild = (child) => {
|
|
39
|
+
// If explicitly hidden by us, or currently display:none, reveal it.
|
|
40
|
+
if (child.style.display === "none" || child.hasAttribute(HIDDEN_ATTR)) {
|
|
41
|
+
child.style.removeProperty("display");
|
|
42
|
+
child.removeAttribute(HIDDEN_ATTR);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const hideChild = (child) => {
|
|
46
|
+
if (child.style.display !== "none") {
|
|
47
|
+
child.style.setProperty("display", "none", "important");
|
|
48
|
+
child.setAttribute(HIDDEN_ATTR, "true");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
// ── Measurement ──────────────────────────────────────────────────────
|
|
52
|
+
const updateMeasurements = (state) => {
|
|
53
|
+
const { targetElement: el } = state;
|
|
54
|
+
if (!el)
|
|
55
|
+
return;
|
|
56
|
+
const children = Array.from(el.children);
|
|
57
|
+
// Temporarily reveal hidden children so measurements are accurate
|
|
58
|
+
const previouslyHidden = el.querySelectorAll(`[${HIDDEN_ATTR}]`);
|
|
59
|
+
previouslyHidden.forEach((node) => {
|
|
60
|
+
const child = node;
|
|
61
|
+
child.style.display = "";
|
|
62
|
+
child.removeAttribute(HIDDEN_ATTR);
|
|
63
|
+
});
|
|
64
|
+
const style = window.getComputedStyle(el);
|
|
65
|
+
const computedGap = parsePx(style.columnGap || style.gap || "0");
|
|
66
|
+
state.gapPx =
|
|
67
|
+
state.gapFromOption !== undefined ? state.gapFromOption : computedGap;
|
|
68
|
+
state.cachedWidths = children.map(getOuterWidth);
|
|
69
|
+
state.totalChildWidth = state.cachedWidths.reduce((sum, w, i) => {
|
|
70
|
+
return sum + w + (i > 0 ? state.gapPx : 0);
|
|
71
|
+
}, 0);
|
|
72
|
+
state.cacheValid = true;
|
|
73
|
+
};
|
|
74
|
+
// ── Overflow calculation ─────────────────────────────────────────────
|
|
75
|
+
const calculateOverflow = (targetElement) => {
|
|
76
|
+
if (!targetElement)
|
|
77
|
+
return;
|
|
78
|
+
const state = stateMap.get(targetElement);
|
|
79
|
+
if (!state)
|
|
80
|
+
return;
|
|
81
|
+
const { parentContainer, targetElement: el, offsetNeededInPx, sortBySize, rowCount, keepVisibleEl, } = state;
|
|
82
|
+
if (!el || !parentContainer) {
|
|
83
|
+
state.rafId = null;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let availableSpaceForChildren = getContentWidth(parentContainer);
|
|
87
|
+
if (parentContainer !== el) {
|
|
88
|
+
availableSpaceForChildren -= getHorizontalOverhead(window.getComputedStyle(el));
|
|
89
|
+
}
|
|
90
|
+
const immediateChildren = Array.from(el.children);
|
|
91
|
+
if (!state.cacheValid ||
|
|
92
|
+
state.cachedWidths.length !== immediateChildren.length) {
|
|
93
|
+
updateMeasurements(state);
|
|
94
|
+
}
|
|
95
|
+
// If all children fit without offset, show everything (no "+N" badge needed)
|
|
96
|
+
if (state.totalChildWidth <= availableSpaceForChildren) {
|
|
97
|
+
immediateChildren.forEach(showChild);
|
|
98
|
+
dispatchUpdate(el, {
|
|
99
|
+
hiddenChildren: [],
|
|
100
|
+
hiddenChildrenCount: 0,
|
|
101
|
+
isOverflowing: false,
|
|
102
|
+
});
|
|
103
|
+
state.rafId = null;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Build candidate indices sorted by priority: kept first, then by size/DOM order
|
|
107
|
+
const candidates = Array.from(immediateChildren.keys()).sort((a, b) => {
|
|
108
|
+
const aKeep = isKeptChild(immediateChildren[a], keepVisibleEl);
|
|
109
|
+
const bKeep = isKeptChild(immediateChildren[b], keepVisibleEl);
|
|
110
|
+
if (aKeep && !bKeep)
|
|
111
|
+
return -1;
|
|
112
|
+
if (!aKeep && bKeep)
|
|
113
|
+
return 1;
|
|
114
|
+
if (sortBySize) {
|
|
115
|
+
return state.cachedWidths[a] - state.cachedWidths[b];
|
|
116
|
+
}
|
|
117
|
+
return a - b;
|
|
118
|
+
});
|
|
119
|
+
// Pack children row-by-row; offset is only reserved on the last row
|
|
120
|
+
const hiddenChildren = [];
|
|
121
|
+
const visibleIndices = new Set();
|
|
122
|
+
const strictWidthLastRow = availableSpaceForChildren - offsetNeededInPx;
|
|
123
|
+
let usedWidth = 0;
|
|
124
|
+
let currentLine = 1;
|
|
125
|
+
for (const index of candidates) {
|
|
126
|
+
const itemWidth = state.cachedWidths[index];
|
|
127
|
+
const child = immediateChildren[index];
|
|
128
|
+
let gap = usedWidth === 0 ? 0 : state.gapPx;
|
|
129
|
+
let limit = currentLine === rowCount
|
|
130
|
+
? strictWidthLastRow
|
|
131
|
+
: availableSpaceForChildren;
|
|
132
|
+
// If it doesn't fit current line, try next line
|
|
133
|
+
if (usedWidth + gap + itemWidth > limit) {
|
|
134
|
+
if (currentLine < rowCount) {
|
|
135
|
+
currentLine++;
|
|
136
|
+
usedWidth = 0;
|
|
137
|
+
gap = 0;
|
|
138
|
+
limit =
|
|
139
|
+
currentLine === rowCount
|
|
140
|
+
? strictWidthLastRow
|
|
141
|
+
: availableSpaceForChildren;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (usedWidth + gap + itemWidth <= limit) {
|
|
145
|
+
usedWidth += gap + itemWidth;
|
|
146
|
+
visibleIndices.add(index);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Does not fit on any available line
|
|
150
|
+
if (isKeptChild(child, keepVisibleEl)) {
|
|
151
|
+
usedWidth += gap + itemWidth;
|
|
152
|
+
visibleIndices.add(index);
|
|
153
|
+
}
|
|
154
|
+
else if (!sortBySize) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
immediateChildren.forEach((child, i) => {
|
|
160
|
+
if (visibleIndices.has(i)) {
|
|
161
|
+
showChild(child);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
hideChild(child);
|
|
165
|
+
hiddenChildren.push(child);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
dispatchUpdate(el, {
|
|
169
|
+
hiddenChildren,
|
|
170
|
+
hiddenChildrenCount: hiddenChildren.length,
|
|
171
|
+
isOverflowing: true,
|
|
172
|
+
});
|
|
173
|
+
state.rafId = null;
|
|
174
|
+
};
|
|
175
|
+
// ── Scheduling ───────────────────────────────────────────────────────
|
|
176
|
+
const scheduleOverflowCalculation = (state, invalidateCache = false) => {
|
|
177
|
+
if (invalidateCache)
|
|
178
|
+
state.cacheValid = false;
|
|
179
|
+
if (state.rafId || !state.targetElement)
|
|
180
|
+
return;
|
|
181
|
+
state.rafId = requestAnimationFrame(() => {
|
|
182
|
+
nextTick(() => {
|
|
183
|
+
calculateOverflow(state.targetElement);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const handleChildResize = (entries, state) => {
|
|
188
|
+
if (!state.cacheValid || !state.targetElement)
|
|
189
|
+
return;
|
|
190
|
+
const children = Array.from(state.targetElement.children);
|
|
191
|
+
const needsRecalc = entries.some((entry) => {
|
|
192
|
+
const index = children.indexOf(entry.target);
|
|
193
|
+
if (index === -1)
|
|
194
|
+
return false;
|
|
195
|
+
const currentOuterWidth = getOuterWidth(entry.target);
|
|
196
|
+
return Math.abs(currentOuterWidth - (state.cachedWidths[index] || 0)) > 1;
|
|
197
|
+
});
|
|
198
|
+
if (needsRecalc) {
|
|
199
|
+
scheduleOverflowCalculation(state, true);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
// ── Directive lifecycle ──────────────────────────────────────────────
|
|
203
|
+
function handleFitChildren(wrapperEl, binding) {
|
|
204
|
+
let state = stateMap.get(wrapperEl);
|
|
205
|
+
if (!state) {
|
|
206
|
+
state = {
|
|
207
|
+
cachedWidths: [],
|
|
208
|
+
cacheValid: false,
|
|
209
|
+
childResizeObserver: undefined,
|
|
210
|
+
gapFromOption: binding.value?.gap,
|
|
211
|
+
gapPx: 0,
|
|
212
|
+
keepVisibleEl: binding.value?.keepVisibleEl,
|
|
213
|
+
mutationObserver: undefined,
|
|
214
|
+
offsetNeededInPx: Math.max(binding.value?.offsetNeededInPx ?? DEFAULT_OFFSET_PX, 0),
|
|
215
|
+
parentContainer: undefined,
|
|
216
|
+
rafId: null,
|
|
217
|
+
resizeObserver: undefined,
|
|
218
|
+
rowCount: binding.value?.rowCount ?? 1,
|
|
219
|
+
sortBySize: binding.value?.sortBySize ?? false,
|
|
220
|
+
targetElement: undefined,
|
|
221
|
+
totalChildWidth: 0,
|
|
222
|
+
};
|
|
223
|
+
stateMap.set(wrapperEl, state);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
let needsUpdate = false;
|
|
227
|
+
if (binding.value?.gap !== state.gapFromOption) {
|
|
228
|
+
state.gapFromOption = binding.value?.gap;
|
|
229
|
+
needsUpdate = true;
|
|
230
|
+
}
|
|
231
|
+
if (binding.value?.offsetNeededInPx !== undefined &&
|
|
232
|
+
binding.value.offsetNeededInPx !== state.offsetNeededInPx) {
|
|
233
|
+
state.offsetNeededInPx = Math.max(binding.value.offsetNeededInPx, 0);
|
|
234
|
+
needsUpdate = true;
|
|
235
|
+
}
|
|
236
|
+
if (binding.value?.sortBySize !== undefined &&
|
|
237
|
+
binding.value.sortBySize !== state.sortBySize) {
|
|
238
|
+
state.sortBySize = binding.value.sortBySize;
|
|
239
|
+
needsUpdate = true;
|
|
240
|
+
}
|
|
241
|
+
if (binding.value?.rowCount !== undefined &&
|
|
242
|
+
binding.value.rowCount !== state.rowCount) {
|
|
243
|
+
state.rowCount = Math.max(binding.value.rowCount, 1);
|
|
244
|
+
needsUpdate = true;
|
|
245
|
+
}
|
|
246
|
+
if (binding.value?.keepVisibleEl !== state.keepVisibleEl) {
|
|
247
|
+
state.keepVisibleEl = binding.value?.keepVisibleEl;
|
|
248
|
+
needsUpdate = true;
|
|
249
|
+
}
|
|
250
|
+
if (needsUpdate) {
|
|
251
|
+
scheduleOverflowCalculation(state, false);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!state.targetElement && wrapperEl) {
|
|
255
|
+
state.targetElement = wrapperEl;
|
|
256
|
+
if (!state.childResizeObserver) {
|
|
257
|
+
const childResizeObserver = new ResizeObserver((entries) => handleChildResize(entries, state));
|
|
258
|
+
Array.from(state.targetElement.children).forEach((child) => childResizeObserver.observe(child));
|
|
259
|
+
state.childResizeObserver = childResizeObserver;
|
|
260
|
+
}
|
|
261
|
+
if (!state.mutationObserver) {
|
|
262
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
263
|
+
if (!state)
|
|
264
|
+
return;
|
|
265
|
+
mutations.forEach((m) => {
|
|
266
|
+
m.addedNodes.forEach((node) => {
|
|
267
|
+
if (node instanceof Element) {
|
|
268
|
+
state.childResizeObserver?.observe(node);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
m.removedNodes.forEach((node) => {
|
|
272
|
+
if (node instanceof Element) {
|
|
273
|
+
state.childResizeObserver?.unobserve(node);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
scheduleOverflowCalculation(state, true);
|
|
278
|
+
});
|
|
279
|
+
mutationObserver.observe(state.targetElement, { childList: true });
|
|
280
|
+
state.mutationObserver = mutationObserver;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Use provided container or fallback to the directive's element
|
|
284
|
+
const container = binding.value?.widthRestrictingContainer || wrapperEl;
|
|
285
|
+
if (!state.parentContainer || state.parentContainer !== container) {
|
|
286
|
+
if (state.resizeObserver) {
|
|
287
|
+
state.resizeObserver.disconnect();
|
|
288
|
+
state.resizeObserver = undefined;
|
|
289
|
+
}
|
|
290
|
+
state.parentContainer = container;
|
|
291
|
+
if (!state.resizeObserver) {
|
|
292
|
+
const resizeObserver = new ResizeObserver(() => scheduleOverflowCalculation(state));
|
|
293
|
+
resizeObserver.observe(container);
|
|
294
|
+
state.resizeObserver = resizeObserver;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Ensure flex-wrap is enabled if multiple rows are allowed
|
|
298
|
+
if (state.rowCount > 1 && wrapperEl.style.flexWrap !== "wrap") {
|
|
299
|
+
wrapperEl.style.setProperty("flex-wrap", "wrap");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
export const vFitChildren = {
|
|
303
|
+
beforeMount: handleFitChildren,
|
|
304
|
+
beforeUnmount(el) {
|
|
305
|
+
const state = stateMap.get(el);
|
|
306
|
+
if (!state)
|
|
307
|
+
return;
|
|
308
|
+
if (state.rafId) {
|
|
309
|
+
cancelAnimationFrame(state.rafId);
|
|
310
|
+
}
|
|
311
|
+
if (state.mutationObserver) {
|
|
312
|
+
state.mutationObserver.disconnect();
|
|
313
|
+
}
|
|
314
|
+
if (state.resizeObserver) {
|
|
315
|
+
state.resizeObserver.disconnect();
|
|
316
|
+
}
|
|
317
|
+
if (state.childResizeObserver) {
|
|
318
|
+
state.childResizeObserver.disconnect();
|
|
319
|
+
}
|
|
320
|
+
stateMap.delete(el);
|
|
321
|
+
},
|
|
322
|
+
updated: handleFitChildren,
|
|
323
|
+
};
|
|
324
|
+
//# sourceMappingURL=vFitChildren.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vFitChildren.js","sourceRoot":"","sources":["../vFitChildren.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Directive, type DirectiveBinding, nextTick } from \"vue\";\n\n/**\n * A Vue directive that manages visibility of child elements within a container.\n *\n * Useful for chips, badges, tags, or any list of inline elements in a tight space.\n * Children that don't fit within the container's width are automatically hidden,\n * and a custom event is emitted with details about hidden children\n * (enabling \"+N more\" indicators).\n *\n * Features:\n * - Monitors the container's width via ResizeObserver\n * - Monitors individual child sizes via ResizeObserver\n * - Detects child additions/removals via MutationObserver\n * - Uses requestAnimationFrame for batched, performant recalculation\n * - Supports multi-row layout via `rowCount`\n * - Supports priority pinning via `keepVisibleEl` / `data-v-fit-keep`\n * - Supports hiding largest items first via `sortBySize`\n *\n * Usage:\n * <div v-fit-children=\"{ widthRestrictingContainer: containerRef, offsetNeededInPx: 50 }\">\n * <chip v-for=\"item in items\" />\n * </div>\n *\n * The directive emits a 'fit-children-updated' event with details about\n * hidden children count and overflow status.\n */\n\ninterface FitChildrenState {\n cachedWidths: number[];\n cacheValid: boolean;\n childResizeObserver?: ResizeObserver;\n gapFromOption?: number;\n gapPx: number;\n keepVisibleEl?: HTMLElement;\n mutationObserver?: MutationObserver;\n offsetNeededInPx: number;\n parentContainer?: HTMLElement;\n rafId: number | null;\n resizeObserver?: ResizeObserver;\n rowCount: number;\n sortBySize: boolean;\n targetElement?: HTMLElement;\n totalChildWidth: number;\n}\n\nexport type FitChildrenEventDetail = {\n hiddenChildren: HTMLElement[];\n hiddenChildrenCount: number;\n isOverflowing: boolean;\n};\n\nexport interface FitChildrenOptions {\n gap?: number;\n keepVisibleEl?: HTMLElement;\n offsetNeededInPx?: number;\n rowCount?: number;\n sortBySize?: boolean;\n widthRestrictingContainer?: HTMLElement;\n}\n\nconst DEFAULT_OFFSET_PX = 50;\nconst HIDDEN_ATTR = \"data-v-fit-hidden\";\nconst KEEP_ATTR = \"data-v-fit-keep\";\nconst EVENT_NAME = \"fit-children-updated\";\n\nconst stateMap = new WeakMap<HTMLElement, FitChildrenState>();\n\n// ── Helpers ──────────────────────────────────────────────────────────\n\n/** Parse a CSS pixel value, returning 0 for invalid/missing values. */\nconst parsePx = (value: string): number => parseFloat(value) || 0;\n\n/** Total horizontal overhead of an element (margin + border + padding). */\nconst getHorizontalOverhead = (style: CSSStyleDeclaration): number =>\n parsePx(style.marginLeft) +\n parsePx(style.marginRight) +\n parsePx(style.borderLeftWidth) +\n parsePx(style.borderRightWidth) +\n parsePx(style.paddingLeft) +\n parsePx(style.paddingRight);\n\n/** Content width of an element (inner width, excluding border and padding). */\nconst getContentWidth = (\n el: HTMLElement,\n style: CSSStyleDeclaration = window.getComputedStyle(el),\n): number =>\n el.getBoundingClientRect().width -\n parsePx(style.borderLeftWidth) -\n parsePx(style.borderRightWidth) -\n parsePx(style.paddingLeft) -\n parsePx(style.paddingRight);\n\n/** Outer width of an element (bounding rect width + horizontal margins). */\nconst getOuterWidth = (el: HTMLElement): number => {\n const style = window.getComputedStyle(el);\n return (\n el.getBoundingClientRect().width +\n parsePx(style.marginLeft) +\n parsePx(style.marginRight)\n );\n};\n\n/** Whether a child should be kept visible (pinned via attribute or option). */\nconst isKeptChild = (\n child: HTMLElement,\n keepVisibleEl?: HTMLElement,\n): boolean =>\n child.hasAttribute(KEEP_ATTR) ||\n (!!keepVisibleEl &&\n (child === keepVisibleEl || child.contains(keepVisibleEl)));\n\n// ── Visibility ───────────────────────────────────────────────────────\n\nconst dispatchUpdate = (el: HTMLElement, detail: FitChildrenEventDetail) => {\n el.dispatchEvent(\n new CustomEvent<FitChildrenEventDetail>(EVENT_NAME, { detail }),\n );\n};\n\nconst showChild = (child: HTMLElement) => {\n // If explicitly hidden by us, or currently display:none, reveal it.\n if (child.style.display === \"none\" || child.hasAttribute(HIDDEN_ATTR)) {\n child.style.removeProperty(\"display\");\n child.removeAttribute(HIDDEN_ATTR);\n }\n};\n\nconst hideChild = (child: HTMLElement) => {\n if (child.style.display !== \"none\") {\n child.style.setProperty(\"display\", \"none\", \"important\");\n child.setAttribute(HIDDEN_ATTR, \"true\");\n }\n};\n\n// ── Measurement ──────────────────────────────────────────────────────\n\nconst updateMeasurements = (state: FitChildrenState) => {\n const { targetElement: el } = state;\n if (!el) return;\n\n const children = Array.from(el.children) as HTMLElement[];\n\n // Temporarily reveal hidden children so measurements are accurate\n const previouslyHidden = el.querySelectorAll(`[${HIDDEN_ATTR}]`);\n previouslyHidden.forEach((node) => {\n const child = node as HTMLElement;\n child.style.display = \"\";\n child.removeAttribute(HIDDEN_ATTR);\n });\n\n const style = window.getComputedStyle(el);\n const computedGap = parsePx(style.columnGap || style.gap || \"0\");\n state.gapPx =\n state.gapFromOption !== undefined ? state.gapFromOption : computedGap;\n\n state.cachedWidths = children.map(getOuterWidth);\n\n state.totalChildWidth = state.cachedWidths.reduce((sum, w, i) => {\n return sum + w + (i > 0 ? state.gapPx : 0);\n }, 0);\n\n state.cacheValid = true;\n};\n\n// ── Overflow calculation ─────────────────────────────────────────────\n\nconst calculateOverflow = (targetElement: HTMLElement | undefined) => {\n if (!targetElement) return;\n\n const state = stateMap.get(targetElement);\n if (!state) return;\n\n const {\n parentContainer,\n targetElement: el,\n offsetNeededInPx,\n sortBySize,\n rowCount,\n keepVisibleEl,\n } = state;\n\n if (!el || !parentContainer) {\n state.rafId = null;\n return;\n }\n\n let availableSpaceForChildren = getContentWidth(parentContainer);\n\n if (parentContainer !== el) {\n availableSpaceForChildren -= getHorizontalOverhead(\n window.getComputedStyle(el),\n );\n }\n\n const immediateChildren = Array.from(el.children) as HTMLElement[];\n\n if (\n !state.cacheValid ||\n state.cachedWidths.length !== immediateChildren.length\n ) {\n updateMeasurements(state);\n }\n\n // If all children fit without offset, show everything (no \"+N\" badge needed)\n if (state.totalChildWidth <= availableSpaceForChildren) {\n immediateChildren.forEach(showChild);\n dispatchUpdate(el, {\n hiddenChildren: [],\n hiddenChildrenCount: 0,\n isOverflowing: false,\n });\n state.rafId = null;\n return;\n }\n\n // Build candidate indices sorted by priority: kept first, then by size/DOM order\n const candidates = Array.from(immediateChildren.keys()).sort((a, b) => {\n const aKeep = isKeptChild(immediateChildren[a], keepVisibleEl);\n const bKeep = isKeptChild(immediateChildren[b], keepVisibleEl);\n\n if (aKeep && !bKeep) return -1;\n if (!aKeep && bKeep) return 1;\n\n if (sortBySize) {\n return state.cachedWidths[a] - state.cachedWidths[b];\n }\n return a - b;\n });\n\n // Pack children row-by-row; offset is only reserved on the last row\n const hiddenChildren: HTMLElement[] = [];\n const visibleIndices = new Set<number>();\n const strictWidthLastRow = availableSpaceForChildren - offsetNeededInPx;\n let usedWidth = 0;\n let currentLine = 1;\n\n for (const index of candidates) {\n const itemWidth = state.cachedWidths[index];\n const child = immediateChildren[index];\n\n let gap = usedWidth === 0 ? 0 : state.gapPx;\n let limit =\n currentLine === rowCount\n ? strictWidthLastRow\n : availableSpaceForChildren;\n\n // If it doesn't fit current line, try next line\n if (usedWidth + gap + itemWidth > limit) {\n if (currentLine < rowCount) {\n currentLine++;\n usedWidth = 0;\n gap = 0;\n limit =\n currentLine === rowCount\n ? strictWidthLastRow\n : availableSpaceForChildren;\n }\n }\n\n if (usedWidth + gap + itemWidth <= limit) {\n usedWidth += gap + itemWidth;\n visibleIndices.add(index);\n } else {\n // Does not fit on any available line\n if (isKeptChild(child, keepVisibleEl)) {\n usedWidth += gap + itemWidth;\n visibleIndices.add(index);\n } else if (!sortBySize) {\n break;\n }\n }\n }\n\n immediateChildren.forEach((child, i) => {\n if (visibleIndices.has(i)) {\n showChild(child);\n } else {\n hideChild(child);\n hiddenChildren.push(child);\n }\n });\n\n dispatchUpdate(el, {\n hiddenChildren,\n hiddenChildrenCount: hiddenChildren.length,\n isOverflowing: true,\n });\n state.rafId = null;\n};\n\n// ── Scheduling ───────────────────────────────────────────────────────\n\nconst scheduleOverflowCalculation = (\n state: FitChildrenState,\n invalidateCache = false,\n) => {\n if (invalidateCache) state.cacheValid = false;\n\n if (state.rafId || !state.targetElement) return;\n\n state.rafId = requestAnimationFrame(() => {\n nextTick(() => {\n calculateOverflow(state.targetElement);\n });\n });\n};\n\nconst handleChildResize = (\n entries: ResizeObserverEntry[],\n state: FitChildrenState,\n) => {\n if (!state.cacheValid || !state.targetElement) return;\n\n const children = Array.from(state.targetElement.children);\n\n const needsRecalc = entries.some((entry) => {\n const index = children.indexOf(entry.target);\n if (index === -1) return false;\n const currentOuterWidth = getOuterWidth(entry.target as HTMLElement);\n return Math.abs(currentOuterWidth - (state.cachedWidths[index] || 0)) > 1;\n });\n\n if (needsRecalc) {\n scheduleOverflowCalculation(state, true);\n }\n};\n\n// ── Directive lifecycle ──────────────────────────────────────────────\n\nfunction handleFitChildren(\n wrapperEl: HTMLElement,\n binding: DirectiveBinding<FitChildrenOptions>,\n) {\n let state = stateMap.get(wrapperEl);\n\n if (!state) {\n state = {\n cachedWidths: [],\n cacheValid: false,\n childResizeObserver: undefined,\n gapFromOption: binding.value?.gap,\n gapPx: 0,\n keepVisibleEl: binding.value?.keepVisibleEl,\n mutationObserver: undefined,\n offsetNeededInPx: Math.max(\n binding.value?.offsetNeededInPx ?? DEFAULT_OFFSET_PX,\n 0,\n ),\n parentContainer: undefined,\n rafId: null,\n resizeObserver: undefined,\n rowCount: binding.value?.rowCount ?? 1,\n sortBySize: binding.value?.sortBySize ?? false,\n targetElement: undefined,\n totalChildWidth: 0,\n };\n stateMap.set(wrapperEl, state);\n } else {\n let needsUpdate = false;\n if (binding.value?.gap !== state.gapFromOption) {\n state.gapFromOption = binding.value?.gap;\n needsUpdate = true;\n }\n if (\n binding.value?.offsetNeededInPx !== undefined &&\n binding.value.offsetNeededInPx !== state.offsetNeededInPx\n ) {\n state.offsetNeededInPx = Math.max(binding.value.offsetNeededInPx, 0);\n needsUpdate = true;\n }\n if (\n binding.value?.sortBySize !== undefined &&\n binding.value.sortBySize !== state.sortBySize\n ) {\n state.sortBySize = binding.value.sortBySize;\n needsUpdate = true;\n }\n if (\n binding.value?.rowCount !== undefined &&\n binding.value.rowCount !== state.rowCount\n ) {\n state.rowCount = Math.max(binding.value.rowCount, 1);\n needsUpdate = true;\n }\n if (binding.value?.keepVisibleEl !== state.keepVisibleEl) {\n state.keepVisibleEl = binding.value?.keepVisibleEl;\n needsUpdate = true;\n }\n\n if (needsUpdate) {\n scheduleOverflowCalculation(state, false);\n }\n }\n\n if (!state.targetElement && wrapperEl) {\n state.targetElement = wrapperEl;\n\n if (!state.childResizeObserver) {\n const childResizeObserver = new ResizeObserver((entries) =>\n handleChildResize(entries, state!), // Safe because we just set stateMap\n );\n\n Array.from(state.targetElement.children).forEach((child) =>\n childResizeObserver.observe(child),\n );\n state.childResizeObserver = childResizeObserver;\n }\n\n if (!state.mutationObserver) {\n const mutationObserver = new MutationObserver((mutations) => {\n if (!state) return;\n mutations.forEach((m) => {\n m.addedNodes.forEach((node) => {\n if (node instanceof Element) {\n state.childResizeObserver?.observe(node);\n }\n });\n\n m.removedNodes.forEach((node) => {\n if (node instanceof Element) {\n state.childResizeObserver?.unobserve(node);\n }\n });\n });\n scheduleOverflowCalculation(state, true);\n });\n mutationObserver.observe(state.targetElement, { childList: true });\n state.mutationObserver = mutationObserver;\n }\n }\n\n // Use provided container or fallback to the directive's element\n const container = binding.value?.widthRestrictingContainer || wrapperEl;\n if (!state.parentContainer || state.parentContainer !== container) {\n if (state.resizeObserver) {\n state.resizeObserver.disconnect();\n state.resizeObserver = undefined;\n }\n\n state.parentContainer = container;\n\n if (!state.resizeObserver) {\n const resizeObserver = new ResizeObserver(() =>\n scheduleOverflowCalculation(state!),\n );\n resizeObserver.observe(container);\n state.resizeObserver = resizeObserver;\n }\n }\n\n // Ensure flex-wrap is enabled if multiple rows are allowed\n if (state.rowCount > 1 && wrapperEl.style.flexWrap !== \"wrap\") {\n wrapperEl.style.setProperty(\"flex-wrap\", \"wrap\");\n }\n}\n\nexport const vFitChildren: Directive = {\n beforeMount: handleFitChildren,\n beforeUnmount(el: HTMLElement) {\n const state = stateMap.get(el);\n if (!state) return;\n\n if (state.rafId) {\n cancelAnimationFrame(state.rafId);\n }\n\n if (state.mutationObserver) {\n state.mutationObserver.disconnect();\n }\n\n if (state.resizeObserver) {\n state.resizeObserver.disconnect();\n }\n\n if (state.childResizeObserver) {\n state.childResizeObserver.disconnect();\n }\n\n stateMap.delete(el);\n },\n updated: handleFitChildren,\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "v-fit-children",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vue directive that auto-hides children that don't fit within a container's width. Useful for chips, badges, tags, and other inline elements in tight spaces.",
|
|
5
|
+
"author": "Ozgur Seyidoglu",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/AzurIce/v-fit-children.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/AzurIce/v-fit-children#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/AzurIce/v-fit-children/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "dist/vFitChildren.js",
|
|
17
|
+
"module": "dist/vFitChildren.js",
|
|
18
|
+
"types": "dist/vFitChildren.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/vFitChildren.js",
|
|
22
|
+
"types": "./dist/vFitChildren.d.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"vue",
|
|
35
|
+
"directive",
|
|
36
|
+
"overflow",
|
|
37
|
+
"hide",
|
|
38
|
+
"children",
|
|
39
|
+
"fit",
|
|
40
|
+
"chips",
|
|
41
|
+
"badges",
|
|
42
|
+
"tags",
|
|
43
|
+
"responsive",
|
|
44
|
+
"truncate",
|
|
45
|
+
"compact",
|
|
46
|
+
"vue3"
|
|
47
|
+
],
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"vue": "^3.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"jsdom": "^25.0.0",
|
|
53
|
+
"typescript": "^5.5.0",
|
|
54
|
+
"vitest": "^2.0.0",
|
|
55
|
+
"vue": "^3.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|