@vielzeug/floatit 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 +84 -0
- package/dist/floatit.cjs +2 -0
- package/dist/floatit.cjs.map +1 -0
- package/dist/floatit.d.ts +118 -0
- package/dist/floatit.d.ts.map +1 -0
- package/dist/floatit.js +2 -0
- package/dist/floatit.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @vielzeug/floatit
|
|
2
|
+
|
|
3
|
+
> Lightweight floating-element positioning for tooltips, dropdowns, popovers, and menus.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@vielzeug/floatit) [](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
**Floatit** is a zero-dependency DOM positioning engine used by Vielzeug components and available as a standalone package. It gives you a small, composable API for computing and applying floating positions, plus middleware for offsetting, flipping, shifting, sizing, and auto-updating.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @vielzeug/floatit
|
|
13
|
+
# npm install @vielzeug/floatit
|
|
14
|
+
# yarn add @vielzeug/floatit
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { autoUpdate, flip, offset, positionFloat, shift } from '@vielzeug/floatit';
|
|
21
|
+
|
|
22
|
+
const reference = document.querySelector('#trigger')!;
|
|
23
|
+
const floating = document.querySelector('#tooltip')!;
|
|
24
|
+
|
|
25
|
+
await positionFloat(reference, floating, {
|
|
26
|
+
placement: 'top',
|
|
27
|
+
middleware: [offset(8), flip(), shift({ padding: 6 })],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const cleanup = autoUpdate(reference, floating, () => {
|
|
31
|
+
positionFloat(reference, floating, {
|
|
32
|
+
placement: 'top',
|
|
33
|
+
middleware: [offset(8), flip(), shift({ padding: 6 })],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// later
|
|
38
|
+
cleanup();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
- ✅ **`positionFloat()`** — compute and apply `left` / `top` styles in one call
|
|
44
|
+
- ✅ **`computePosition()`** — low-level `{ x, y, placement }` API when you want to control rendering yourself
|
|
45
|
+
- ✅ **Composable middleware** — `offset`, `flip`, `shift`, and `size`
|
|
46
|
+
- ✅ **`autoUpdate()`** — keeps floating UI aligned on scroll, resize, and element resizes
|
|
47
|
+
- ✅ **Typed placement model** — `top`, `bottom-start`, `left-end`, and related helpers
|
|
48
|
+
- ✅ **Zero dependencies** — pure DOM APIs only
|
|
49
|
+
|
|
50
|
+
## API Summary
|
|
51
|
+
|
|
52
|
+
| Export | Description |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `positionFloat(reference, floating, options?)` | Compute and apply the floating position |
|
|
55
|
+
| `computePosition(reference, floating, config?)` | Return `{ x, y, placement }` without mutating the DOM |
|
|
56
|
+
| `autoUpdate(reference, floating, update, options?)` | Re-run positioning when layout conditions change |
|
|
57
|
+
| `offset(value)` | Add distance between reference and floating element |
|
|
58
|
+
| `flip(options?)` | Flip to the opposite side when the preferred side overflows |
|
|
59
|
+
| `shift(options?)` | Clamp the floating element inside the viewport |
|
|
60
|
+
| `size(options?)` | Provide available dimensions and resize hooks |
|
|
61
|
+
|
|
62
|
+
## Usage Notes
|
|
63
|
+
|
|
64
|
+
- `strategy` is part of the public option types but is not currently applied internally; position from viewport coordinates (typically with `position: fixed` CSS).
|
|
65
|
+
- Middleware runs in order; the common chain is `offset()`, `flip()`, `shift()`, then `size()`.
|
|
66
|
+
- Call the cleanup returned by `autoUpdate()` when the floating UI closes.
|
|
67
|
+
- Use `autoUpdate(..., { observeFloating: false })` when observing floating-size changes would cause unnecessary update loops.
|
|
68
|
+
- Use `computePosition()` when you want to apply transforms or animations yourself.
|
|
69
|
+
|
|
70
|
+
## Documentation
|
|
71
|
+
|
|
72
|
+
Full docs at **[vielzeug.dev/floatit](https://vielzeug.dev/floatit)**
|
|
73
|
+
|
|
74
|
+
| | |
|
|
75
|
+
|---|---|
|
|
76
|
+
| [Overview](https://vielzeug.dev/floatit/) | Installation, quick start, and feature overview |
|
|
77
|
+
| [Usage Guide](https://vielzeug.dev/floatit/usage) | Placement, middleware, and lifecycle patterns |
|
|
78
|
+
| [API Reference](https://vielzeug.dev/floatit/api) | Complete function signatures and types |
|
|
79
|
+
| [Examples](https://vielzeug.dev/floatit/examples) | Tooltips, dropdowns, and framework recipes |
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu) — Part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
|
84
|
+
|
package/dist/floatit.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={bottom:`top`,left:`right`,right:`left`,top:`bottom`};function t(e){return e.split(`-`)[0]}function n(e){return e.split(`-`)[1]??null}function r({height:e,width:t,x:n,y:r}){return{height:e,width:t,x:n,y:r}}function i(e,t,n,r){return e===`start`?t:e===`end`?t+n-r:t+(n-r)/2}function a(e,r,a){let o=t(e),s=n(e);return o===`top`?{x:i(s,r.x,r.width,a.width),y:r.y-a.height}:o===`bottom`?{x:i(s,r.x,r.width,a.width),y:r.y+r.height}:o===`left`?{x:r.x-a.width,y:i(s,r.y,r.height,a.height)}:{x:r.x+r.width,y:i(s,r.y,r.height,a.height)}}function o(e,t,n={}){let{middleware:i=[],placement:o=`bottom`}=n,s=i.filter(Boolean),c=o,l=0,u=()=>{let n=r(e.getBoundingClientRect()),i=r(t.getBoundingClientRect()),o={...a(c,n,i),elements:{floating:t,reference:e},placement:c,rects:{floating:i,reference:n}};for(let e of s)o=e.fn(o);return o.placement!==c&&l<1?(c=o.placement,l++,u()):{placement:o.placement,x:o.x,y:o.y}};return Promise.resolve(u())}function s(e){return{fn(n){let r=t(n.placement);return{...n,x:n.x+(r===`right`?e:r===`left`?-e:0),y:n.y+(r===`bottom`?e:r===`top`?-e:0)}},name:`offset`}}function c(r={}){let{padding:i=0}=r;return{fn(r){let{placement:a,rects:{floating:o},x:s,y:c}=r,l=t(a),u=window.innerWidth,d=window.innerHeight;if(!(l===`top`&&c<i||l===`bottom`&&c+o.height>d-i||l===`left`&&s<i||l===`right`&&s+o.width>u-i))return r;let f=n(a),p=e[l],m=f?`${p}-${f}`:p;return{...r,placement:m}},name:`flip`}}function l(e={}){let{padding:t=0}=e;return{fn(e){let{rects:{floating:n},x:r,y:i}=e,a=window.innerWidth,o=window.innerHeight;return{...e,x:Math.min(Math.max(r,t),a-n.width-t),y:Math.min(Math.max(i,t),o-n.height-t)}},name:`shift`}}function u(e={}){let{apply:t,padding:n=0}=e;return{fn(e){return t?.({availableHeight:window.innerHeight-n*2,availableWidth:window.innerWidth-n*2,elements:e.elements}),e},name:`size`}}function d(e,t,n,{observeFloating:r=!0,observeVisualViewport:i=!0}={}){let a=e=>{e.composedPath().includes(t)||n()};window.addEventListener(`scroll`,a,{capture:!0,passive:!0}),window.addEventListener(`resize`,n,{passive:!0});let o=i?window.visualViewport:null;o?.addEventListener(`resize`,n,{passive:!0}),o?.addEventListener(`scroll`,n,{passive:!0});let s=new ResizeObserver(n);return s.observe(e),r&&s.observe(t),()=>{window.removeEventListener(`scroll`,a,{capture:!0}),window.removeEventListener(`resize`,n),o?.removeEventListener(`resize`,n),o?.removeEventListener(`scroll`,n),s.disconnect()}}function f(e,t,n={}){return o(e,t,{strategy:`fixed`,...n}).then(({placement:e,x:n,y:r})=>(t.style.left=`${n}px`,t.style.top=`${r}px`,e))}exports.autoUpdate=d,exports.computePosition=o,exports.flip=c,exports.offset=s,exports.positionFloat=f,exports.shift=l,exports.size=u;
|
|
2
|
+
//# sourceMappingURL=floatit.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floatit.cjs","names":[],"sources":["../src/floatit.ts"],"sourcesContent":["/** @vielzeug/floatit — Lightweight floating element positioning. */\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type Side = 'top' | 'bottom' | 'left' | 'right';\nexport type Alignment = 'start' | 'end';\nexport type Placement = Side | `${Side}-${Alignment}`;\nexport type Strategy = 'fixed' | 'absolute';\n\ninterface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface MiddlewareState {\n x: number;\n y: number;\n placement: Placement;\n rects: { floating: Rect; reference: Rect };\n elements: { floating: HTMLElement; reference: Element };\n}\n\nexport interface Middleware {\n name: string;\n fn: (state: MiddlewareState) => MiddlewareState;\n}\n\nexport interface ComputePositionConfig {\n placement?: Placement;\n strategy?: Strategy;\n middleware?: Array<Middleware | null | undefined | false>;\n}\n\nexport interface ComputePositionResult {\n x: number;\n y: number;\n placement: Placement;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nconst OPPOSITE: Record<Side, Side> = { bottom: 'top', left: 'right', right: 'left', top: 'bottom' };\n\nfunction getSide(p: Placement): Side {\n return p.split('-')[0] as Side;\n}\n\nfunction getAlign(p: Placement): Alignment | null {\n return (p.split('-')[1] as Alignment) ?? null;\n}\n\nfunction toRect({ height, width, x, y }: DOMRect): Rect {\n return { height, width, x, y };\n}\n\nfunction crossCoord(align: Alignment | null, start: number, span: number, floatSpan: number): number {\n return align === 'start' ? start : align === 'end' ? start + span - floatSpan : start + (span - floatSpan) / 2;\n}\n\n/** Compute the base x/y for a floating element relative to a reference element. */\nfunction baseCoords(placement: Placement, ref: Rect, float: Rect): { x: number; y: number } {\n const side = getSide(placement);\n const align = getAlign(placement);\n\n if (side === 'top') return { x: crossCoord(align, ref.x, ref.width, float.width), y: ref.y - float.height };\n\n if (side === 'bottom') return { x: crossCoord(align, ref.x, ref.width, float.width), y: ref.y + ref.height };\n\n if (side === 'left') return { x: ref.x - float.width, y: crossCoord(align, ref.y, ref.height, float.height) };\n\n /* right */ return { x: ref.x + ref.width, y: crossCoord(align, ref.y, ref.height, float.height) };\n}\n\n// ─── computePosition ──────────────────────────────────────────────────────────\n\n/**\n * Computes the position of a floating element relative to a reference element.\n * Returns a Promise for API compatibility.\n */\nexport function computePosition(\n reference: Element,\n floating: HTMLElement,\n config: ComputePositionConfig = {},\n): Promise<ComputePositionResult> {\n const { middleware = [], placement: initial = 'bottom' } = config;\n const mws = middleware.filter(Boolean) as Middleware[];\n\n let activePlacement = initial;\n let resets = 0;\n\n const run = (): ComputePositionResult => {\n const refRect = toRect(reference.getBoundingClientRect());\n const floatRect = toRect(floating.getBoundingClientRect());\n const base = baseCoords(activePlacement, refRect, floatRect);\n\n let state: MiddlewareState = {\n ...base,\n elements: { floating, reference },\n placement: activePlacement,\n rects: { floating: floatRect, reference: refRect },\n };\n\n for (const mw of mws) state = mw.fn(state);\n\n // If a middleware (e.g. flip) changed the placement, restart once with the new one.\n if (state.placement !== activePlacement && resets < 1) {\n activePlacement = state.placement;\n resets++;\n\n return run();\n }\n\n return { placement: state.placement, x: state.x, y: state.y };\n };\n\n return Promise.resolve(run());\n}\n\n// ─── Middlewares ──────────────────────────────────────────────────────────────\n\n/** Adds a gap (in px) between the reference and the floating element. */\nexport function offset(value: number): Middleware {\n return {\n fn(state) {\n const side = getSide(state.placement);\n\n return {\n ...state,\n x: state.x + (side === 'right' ? value : side === 'left' ? -value : 0),\n y: state.y + (side === 'bottom' ? value : side === 'top' ? -value : 0),\n };\n },\n name: 'offset',\n };\n}\n\nexport interface FlipOptions {\n /** Minimum distance from the viewport edge before flipping (px). */\n padding?: number;\n}\n\n/** Flips the floating element to the opposite side when it would overflow the viewport. */\nexport function flip(options: FlipOptions = {}): Middleware {\n const { padding = 0 } = options;\n\n return {\n fn(state) {\n const {\n placement,\n rects: { floating },\n x,\n y,\n } = state;\n const side = getSide(placement);\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const overflows =\n (side === 'top' && y < padding) ||\n (side === 'bottom' && y + floating.height > vh - padding) ||\n (side === 'left' && x < padding) ||\n (side === 'right' && x + floating.width > vw - padding);\n\n if (!overflows) return state;\n\n const align = getAlign(placement);\n const opp = OPPOSITE[side];\n const flipped = (align ? `${opp}-${align}` : opp) as Placement;\n\n return { ...state, placement: flipped };\n },\n name: 'flip',\n };\n}\n\nexport interface ShiftOptions {\n /** Minimum distance to maintain from the viewport edges (px). */\n padding?: number;\n}\n\n/** Shifts the floating element along its axis to keep it within the viewport. */\nexport function shift(options: ShiftOptions = {}): Middleware {\n const { padding = 0 } = options;\n\n return {\n fn(state) {\n const {\n rects: { floating },\n x,\n y,\n } = state;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n return {\n ...state,\n x: Math.min(Math.max(x, padding), vw - floating.width - padding),\n y: Math.min(Math.max(y, padding), vh - floating.height - padding),\n };\n },\n name: 'shift',\n };\n}\n\nexport interface SizeApplyArgs {\n availableWidth: number;\n availableHeight: number;\n elements: { floating: HTMLElement; reference: Element };\n}\n\nexport interface SizeOptions {\n /** Minimum distance to maintain from the viewport edges (px). */\n padding?: number;\n /** Called with available dimensions — use to resize the floating element. */\n apply?: (args: SizeApplyArgs) => void;\n}\n\n/** Provides available width/height and optionally resizes the floating element. */\nexport function size(options: SizeOptions = {}): Middleware {\n const { apply, padding = 0 } = options;\n\n return {\n fn(state) {\n apply?.({\n availableHeight: window.innerHeight - padding * 2,\n availableWidth: window.innerWidth - padding * 2,\n elements: state.elements,\n });\n\n return state;\n },\n name: 'size',\n };\n}\n\n// ─── autoUpdate ───────────────────────────────────────────────────────────────\n\nexport interface AutoUpdateOptions {\n /**\n * Whether to observe size changes on the floating element itself.\n * Set to `false` for virtual-scroll dropdowns whose outer dimensions are\n * managed entirely by the caller (e.g. width set via `size()` middleware),\n * to avoid a ResizeObserver feedback loop.\n * Defaults to `true`.\n */\n observeFloating?: boolean;\n /**\n * Whether to observe `window.visualViewport` resize/scroll changes.\n * Helps keep floating UI aligned during pinch-zoom and virtual keyboard changes.\n * Defaults to `true`.\n */\n observeVisualViewport?: boolean;\n}\n\n/**\n * Automatically calls `update` whenever the floating element's position may have\n * changed (viewport resize, scroll events, or reference / floating element resize).\n * Returns a cleanup function.\n */\nexport function autoUpdate(\n reference: Element,\n floating: HTMLElement,\n update: () => void,\n { observeFloating = true, observeVisualViewport = true }: AutoUpdateOptions = {},\n): () => void {\n // Scroll events inside the floating element itself (e.g. a dropdown scrolling\n // its own options list) must never trigger repositioning.\n // Use composedPath() instead of e.target — shadow DOM retargets e.target to\n // the shadow host at the window listener boundary, so contains(e.target) would\n // miss scrolls that originated inside a shadow root.\n const scrollHandler = (e: Event) => {\n if (e.composedPath().includes(floating)) return;\n\n update();\n };\n\n window.addEventListener('scroll', scrollHandler, { capture: true, passive: true });\n window.addEventListener('resize', update, { passive: true });\n\n const vv = observeVisualViewport ? window.visualViewport : null;\n\n vv?.addEventListener('resize', update, { passive: true });\n vv?.addEventListener('scroll', update, { passive: true });\n\n const ro = new ResizeObserver(update);\n\n ro.observe(reference);\n\n if (observeFloating) ro.observe(floating);\n\n return () => {\n window.removeEventListener('scroll', scrollHandler, { capture: true } as EventListenerOptions);\n window.removeEventListener('resize', update);\n vv?.removeEventListener('resize', update);\n vv?.removeEventListener('scroll', update);\n ro.disconnect();\n };\n}\n\n// ─── positionFloat ────────────────────────────────────────────────────────────\n\nexport interface FloatOptions {\n /** Preferred placement relative to the reference element. */\n placement?: Placement;\n /** Positioning strategy. Defaults to `'fixed'`. */\n strategy?: Strategy;\n /** Middleware to modify positioning behavior. */\n middleware?: Array<Middleware | null | undefined | false>;\n}\n\n/**\n * Computes and applies the floating position to a floating element.\n * Sets `left`/`top` inline styles and returns the resolved placement.\n *\n * @example\n * ```ts\n * positionFloat(reference, floating, {\n * placement: 'top',\n * middleware: [offset(8), flip(), shift({ padding: 6 })],\n * }).then(placement => el.dataset.placement = placement);\n * ```\n */\nexport function positionFloat(\n reference: Element,\n floating: HTMLElement,\n options: FloatOptions = {},\n): Promise<Placement> {\n return computePosition(reference, floating, {\n strategy: 'fixed',\n ...options,\n }).then(({ placement, x, y }) => {\n floating.style.left = `${x}px`;\n floating.style.top = `${y}px`;\n\n return placement;\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** @vielzeug/floatit — Lightweight floating element positioning. */
|
|
2
|
+
export type Side = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
export type Alignment = 'start' | 'end';
|
|
4
|
+
export type Placement = Side | `${Side}-${Alignment}`;
|
|
5
|
+
export type Strategy = 'fixed' | 'absolute';
|
|
6
|
+
interface Rect {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface MiddlewareState {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
placement: Placement;
|
|
16
|
+
rects: {
|
|
17
|
+
floating: Rect;
|
|
18
|
+
reference: Rect;
|
|
19
|
+
};
|
|
20
|
+
elements: {
|
|
21
|
+
floating: HTMLElement;
|
|
22
|
+
reference: Element;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface Middleware {
|
|
26
|
+
name: string;
|
|
27
|
+
fn: (state: MiddlewareState) => MiddlewareState;
|
|
28
|
+
}
|
|
29
|
+
export interface ComputePositionConfig {
|
|
30
|
+
placement?: Placement;
|
|
31
|
+
strategy?: Strategy;
|
|
32
|
+
middleware?: Array<Middleware | null | undefined | false>;
|
|
33
|
+
}
|
|
34
|
+
export interface ComputePositionResult {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
placement: Placement;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Computes the position of a floating element relative to a reference element.
|
|
41
|
+
* Returns a Promise for API compatibility.
|
|
42
|
+
*/
|
|
43
|
+
export declare function computePosition(reference: Element, floating: HTMLElement, config?: ComputePositionConfig): Promise<ComputePositionResult>;
|
|
44
|
+
/** Adds a gap (in px) between the reference and the floating element. */
|
|
45
|
+
export declare function offset(value: number): Middleware;
|
|
46
|
+
export interface FlipOptions {
|
|
47
|
+
/** Minimum distance from the viewport edge before flipping (px). */
|
|
48
|
+
padding?: number;
|
|
49
|
+
}
|
|
50
|
+
/** Flips the floating element to the opposite side when it would overflow the viewport. */
|
|
51
|
+
export declare function flip(options?: FlipOptions): Middleware;
|
|
52
|
+
export interface ShiftOptions {
|
|
53
|
+
/** Minimum distance to maintain from the viewport edges (px). */
|
|
54
|
+
padding?: number;
|
|
55
|
+
}
|
|
56
|
+
/** Shifts the floating element along its axis to keep it within the viewport. */
|
|
57
|
+
export declare function shift(options?: ShiftOptions): Middleware;
|
|
58
|
+
export interface SizeApplyArgs {
|
|
59
|
+
availableWidth: number;
|
|
60
|
+
availableHeight: number;
|
|
61
|
+
elements: {
|
|
62
|
+
floating: HTMLElement;
|
|
63
|
+
reference: Element;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export interface SizeOptions {
|
|
67
|
+
/** Minimum distance to maintain from the viewport edges (px). */
|
|
68
|
+
padding?: number;
|
|
69
|
+
/** Called with available dimensions — use to resize the floating element. */
|
|
70
|
+
apply?: (args: SizeApplyArgs) => void;
|
|
71
|
+
}
|
|
72
|
+
/** Provides available width/height and optionally resizes the floating element. */
|
|
73
|
+
export declare function size(options?: SizeOptions): Middleware;
|
|
74
|
+
export interface AutoUpdateOptions {
|
|
75
|
+
/**
|
|
76
|
+
* Whether to observe size changes on the floating element itself.
|
|
77
|
+
* Set to `false` for virtual-scroll dropdowns whose outer dimensions are
|
|
78
|
+
* managed entirely by the caller (e.g. width set via `size()` middleware),
|
|
79
|
+
* to avoid a ResizeObserver feedback loop.
|
|
80
|
+
* Defaults to `true`.
|
|
81
|
+
*/
|
|
82
|
+
observeFloating?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether to observe `window.visualViewport` resize/scroll changes.
|
|
85
|
+
* Helps keep floating UI aligned during pinch-zoom and virtual keyboard changes.
|
|
86
|
+
* Defaults to `true`.
|
|
87
|
+
*/
|
|
88
|
+
observeVisualViewport?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Automatically calls `update` whenever the floating element's position may have
|
|
92
|
+
* changed (viewport resize, scroll events, or reference / floating element resize).
|
|
93
|
+
* Returns a cleanup function.
|
|
94
|
+
*/
|
|
95
|
+
export declare function autoUpdate(reference: Element, floating: HTMLElement, update: () => void, { observeFloating, observeVisualViewport }?: AutoUpdateOptions): () => void;
|
|
96
|
+
export interface FloatOptions {
|
|
97
|
+
/** Preferred placement relative to the reference element. */
|
|
98
|
+
placement?: Placement;
|
|
99
|
+
/** Positioning strategy. Defaults to `'fixed'`. */
|
|
100
|
+
strategy?: Strategy;
|
|
101
|
+
/** Middleware to modify positioning behavior. */
|
|
102
|
+
middleware?: Array<Middleware | null | undefined | false>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Computes and applies the floating position to a floating element.
|
|
106
|
+
* Sets `left`/`top` inline styles and returns the resolved placement.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* positionFloat(reference, floating, {
|
|
111
|
+
* placement: 'top',
|
|
112
|
+
* middleware: [offset(8), flip(), shift({ padding: 6 })],
|
|
113
|
+
* }).then(placement => el.dataset.placement = placement);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function positionFloat(reference: Element, floating: HTMLElement, options?: FloatOptions): Promise<Placement>;
|
|
117
|
+
export {};
|
|
118
|
+
//# sourceMappingURL=floatit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floatit.d.ts","sourceRoot":"","sources":["../src/floatit.ts"],"names":[],"mappings":"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"}
|
package/dist/floatit.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e={bottom:`top`,left:`right`,right:`left`,top:`bottom`};function t(e){return e.split(`-`)[0]}function n(e){return e.split(`-`)[1]??null}function r({height:e,width:t,x:n,y:r}){return{height:e,width:t,x:n,y:r}}function i(e,t,n,r){return e===`start`?t:e===`end`?t+n-r:t+(n-r)/2}function a(e,r,a){let o=t(e),s=n(e);return o===`top`?{x:i(s,r.x,r.width,a.width),y:r.y-a.height}:o===`bottom`?{x:i(s,r.x,r.width,a.width),y:r.y+r.height}:o===`left`?{x:r.x-a.width,y:i(s,r.y,r.height,a.height)}:{x:r.x+r.width,y:i(s,r.y,r.height,a.height)}}function o(e,t,n={}){let{middleware:i=[],placement:o=`bottom`}=n,s=i.filter(Boolean),c=o,l=0,u=()=>{let n=r(e.getBoundingClientRect()),i=r(t.getBoundingClientRect()),o={...a(c,n,i),elements:{floating:t,reference:e},placement:c,rects:{floating:i,reference:n}};for(let e of s)o=e.fn(o);return o.placement!==c&&l<1?(c=o.placement,l++,u()):{placement:o.placement,x:o.x,y:o.y}};return Promise.resolve(u())}function s(e){return{fn(n){let r=t(n.placement);return{...n,x:n.x+(r===`right`?e:r===`left`?-e:0),y:n.y+(r===`bottom`?e:r===`top`?-e:0)}},name:`offset`}}function c(r={}){let{padding:i=0}=r;return{fn(r){let{placement:a,rects:{floating:o},x:s,y:c}=r,l=t(a),u=window.innerWidth,d=window.innerHeight;if(!(l===`top`&&c<i||l===`bottom`&&c+o.height>d-i||l===`left`&&s<i||l===`right`&&s+o.width>u-i))return r;let f=n(a),p=e[l],m=f?`${p}-${f}`:p;return{...r,placement:m}},name:`flip`}}function l(e={}){let{padding:t=0}=e;return{fn(e){let{rects:{floating:n},x:r,y:i}=e,a=window.innerWidth,o=window.innerHeight;return{...e,x:Math.min(Math.max(r,t),a-n.width-t),y:Math.min(Math.max(i,t),o-n.height-t)}},name:`shift`}}function u(e={}){let{apply:t,padding:n=0}=e;return{fn(e){return t?.({availableHeight:window.innerHeight-n*2,availableWidth:window.innerWidth-n*2,elements:e.elements}),e},name:`size`}}function d(e,t,n,{observeFloating:r=!0,observeVisualViewport:i=!0}={}){let a=e=>{e.composedPath().includes(t)||n()};window.addEventListener(`scroll`,a,{capture:!0,passive:!0}),window.addEventListener(`resize`,n,{passive:!0});let o=i?window.visualViewport:null;o?.addEventListener(`resize`,n,{passive:!0}),o?.addEventListener(`scroll`,n,{passive:!0});let s=new ResizeObserver(n);return s.observe(e),r&&s.observe(t),()=>{window.removeEventListener(`scroll`,a,{capture:!0}),window.removeEventListener(`resize`,n),o?.removeEventListener(`resize`,n),o?.removeEventListener(`scroll`,n),s.disconnect()}}function f(e,t,n={}){return o(e,t,{strategy:`fixed`,...n}).then(({placement:e,x:n,y:r})=>(t.style.left=`${n}px`,t.style.top=`${r}px`,e))}export{d as autoUpdate,o as computePosition,c as flip,s as offset,f as positionFloat,l as shift,u as size};
|
|
2
|
+
//# sourceMappingURL=floatit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floatit.js","names":[],"sources":["../src/floatit.ts"],"sourcesContent":["/** @vielzeug/floatit — Lightweight floating element positioning. */\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport type Side = 'top' | 'bottom' | 'left' | 'right';\nexport type Alignment = 'start' | 'end';\nexport type Placement = Side | `${Side}-${Alignment}`;\nexport type Strategy = 'fixed' | 'absolute';\n\ninterface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface MiddlewareState {\n x: number;\n y: number;\n placement: Placement;\n rects: { floating: Rect; reference: Rect };\n elements: { floating: HTMLElement; reference: Element };\n}\n\nexport interface Middleware {\n name: string;\n fn: (state: MiddlewareState) => MiddlewareState;\n}\n\nexport interface ComputePositionConfig {\n placement?: Placement;\n strategy?: Strategy;\n middleware?: Array<Middleware | null | undefined | false>;\n}\n\nexport interface ComputePositionResult {\n x: number;\n y: number;\n placement: Placement;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nconst OPPOSITE: Record<Side, Side> = { bottom: 'top', left: 'right', right: 'left', top: 'bottom' };\n\nfunction getSide(p: Placement): Side {\n return p.split('-')[0] as Side;\n}\n\nfunction getAlign(p: Placement): Alignment | null {\n return (p.split('-')[1] as Alignment) ?? null;\n}\n\nfunction toRect({ height, width, x, y }: DOMRect): Rect {\n return { height, width, x, y };\n}\n\nfunction crossCoord(align: Alignment | null, start: number, span: number, floatSpan: number): number {\n return align === 'start' ? start : align === 'end' ? start + span - floatSpan : start + (span - floatSpan) / 2;\n}\n\n/** Compute the base x/y for a floating element relative to a reference element. */\nfunction baseCoords(placement: Placement, ref: Rect, float: Rect): { x: number; y: number } {\n const side = getSide(placement);\n const align = getAlign(placement);\n\n if (side === 'top') return { x: crossCoord(align, ref.x, ref.width, float.width), y: ref.y - float.height };\n\n if (side === 'bottom') return { x: crossCoord(align, ref.x, ref.width, float.width), y: ref.y + ref.height };\n\n if (side === 'left') return { x: ref.x - float.width, y: crossCoord(align, ref.y, ref.height, float.height) };\n\n /* right */ return { x: ref.x + ref.width, y: crossCoord(align, ref.y, ref.height, float.height) };\n}\n\n// ─── computePosition ──────────────────────────────────────────────────────────\n\n/**\n * Computes the position of a floating element relative to a reference element.\n * Returns a Promise for API compatibility.\n */\nexport function computePosition(\n reference: Element,\n floating: HTMLElement,\n config: ComputePositionConfig = {},\n): Promise<ComputePositionResult> {\n const { middleware = [], placement: initial = 'bottom' } = config;\n const mws = middleware.filter(Boolean) as Middleware[];\n\n let activePlacement = initial;\n let resets = 0;\n\n const run = (): ComputePositionResult => {\n const refRect = toRect(reference.getBoundingClientRect());\n const floatRect = toRect(floating.getBoundingClientRect());\n const base = baseCoords(activePlacement, refRect, floatRect);\n\n let state: MiddlewareState = {\n ...base,\n elements: { floating, reference },\n placement: activePlacement,\n rects: { floating: floatRect, reference: refRect },\n };\n\n for (const mw of mws) state = mw.fn(state);\n\n // If a middleware (e.g. flip) changed the placement, restart once with the new one.\n if (state.placement !== activePlacement && resets < 1) {\n activePlacement = state.placement;\n resets++;\n\n return run();\n }\n\n return { placement: state.placement, x: state.x, y: state.y };\n };\n\n return Promise.resolve(run());\n}\n\n// ─── Middlewares ──────────────────────────────────────────────────────────────\n\n/** Adds a gap (in px) between the reference and the floating element. */\nexport function offset(value: number): Middleware {\n return {\n fn(state) {\n const side = getSide(state.placement);\n\n return {\n ...state,\n x: state.x + (side === 'right' ? value : side === 'left' ? -value : 0),\n y: state.y + (side === 'bottom' ? value : side === 'top' ? -value : 0),\n };\n },\n name: 'offset',\n };\n}\n\nexport interface FlipOptions {\n /** Minimum distance from the viewport edge before flipping (px). */\n padding?: number;\n}\n\n/** Flips the floating element to the opposite side when it would overflow the viewport. */\nexport function flip(options: FlipOptions = {}): Middleware {\n const { padding = 0 } = options;\n\n return {\n fn(state) {\n const {\n placement,\n rects: { floating },\n x,\n y,\n } = state;\n const side = getSide(placement);\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n const overflows =\n (side === 'top' && y < padding) ||\n (side === 'bottom' && y + floating.height > vh - padding) ||\n (side === 'left' && x < padding) ||\n (side === 'right' && x + floating.width > vw - padding);\n\n if (!overflows) return state;\n\n const align = getAlign(placement);\n const opp = OPPOSITE[side];\n const flipped = (align ? `${opp}-${align}` : opp) as Placement;\n\n return { ...state, placement: flipped };\n },\n name: 'flip',\n };\n}\n\nexport interface ShiftOptions {\n /** Minimum distance to maintain from the viewport edges (px). */\n padding?: number;\n}\n\n/** Shifts the floating element along its axis to keep it within the viewport. */\nexport function shift(options: ShiftOptions = {}): Middleware {\n const { padding = 0 } = options;\n\n return {\n fn(state) {\n const {\n rects: { floating },\n x,\n y,\n } = state;\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n return {\n ...state,\n x: Math.min(Math.max(x, padding), vw - floating.width - padding),\n y: Math.min(Math.max(y, padding), vh - floating.height - padding),\n };\n },\n name: 'shift',\n };\n}\n\nexport interface SizeApplyArgs {\n availableWidth: number;\n availableHeight: number;\n elements: { floating: HTMLElement; reference: Element };\n}\n\nexport interface SizeOptions {\n /** Minimum distance to maintain from the viewport edges (px). */\n padding?: number;\n /** Called with available dimensions — use to resize the floating element. */\n apply?: (args: SizeApplyArgs) => void;\n}\n\n/** Provides available width/height and optionally resizes the floating element. */\nexport function size(options: SizeOptions = {}): Middleware {\n const { apply, padding = 0 } = options;\n\n return {\n fn(state) {\n apply?.({\n availableHeight: window.innerHeight - padding * 2,\n availableWidth: window.innerWidth - padding * 2,\n elements: state.elements,\n });\n\n return state;\n },\n name: 'size',\n };\n}\n\n// ─── autoUpdate ───────────────────────────────────────────────────────────────\n\nexport interface AutoUpdateOptions {\n /**\n * Whether to observe size changes on the floating element itself.\n * Set to `false` for virtual-scroll dropdowns whose outer dimensions are\n * managed entirely by the caller (e.g. width set via `size()` middleware),\n * to avoid a ResizeObserver feedback loop.\n * Defaults to `true`.\n */\n observeFloating?: boolean;\n /**\n * Whether to observe `window.visualViewport` resize/scroll changes.\n * Helps keep floating UI aligned during pinch-zoom and virtual keyboard changes.\n * Defaults to `true`.\n */\n observeVisualViewport?: boolean;\n}\n\n/**\n * Automatically calls `update` whenever the floating element's position may have\n * changed (viewport resize, scroll events, or reference / floating element resize).\n * Returns a cleanup function.\n */\nexport function autoUpdate(\n reference: Element,\n floating: HTMLElement,\n update: () => void,\n { observeFloating = true, observeVisualViewport = true }: AutoUpdateOptions = {},\n): () => void {\n // Scroll events inside the floating element itself (e.g. a dropdown scrolling\n // its own options list) must never trigger repositioning.\n // Use composedPath() instead of e.target — shadow DOM retargets e.target to\n // the shadow host at the window listener boundary, so contains(e.target) would\n // miss scrolls that originated inside a shadow root.\n const scrollHandler = (e: Event) => {\n if (e.composedPath().includes(floating)) return;\n\n update();\n };\n\n window.addEventListener('scroll', scrollHandler, { capture: true, passive: true });\n window.addEventListener('resize', update, { passive: true });\n\n const vv = observeVisualViewport ? window.visualViewport : null;\n\n vv?.addEventListener('resize', update, { passive: true });\n vv?.addEventListener('scroll', update, { passive: true });\n\n const ro = new ResizeObserver(update);\n\n ro.observe(reference);\n\n if (observeFloating) ro.observe(floating);\n\n return () => {\n window.removeEventListener('scroll', scrollHandler, { capture: true } as EventListenerOptions);\n window.removeEventListener('resize', update);\n vv?.removeEventListener('resize', update);\n vv?.removeEventListener('scroll', update);\n ro.disconnect();\n };\n}\n\n// ─── positionFloat ────────────────────────────────────────────────────────────\n\nexport interface FloatOptions {\n /** Preferred placement relative to the reference element. */\n placement?: Placement;\n /** Positioning strategy. Defaults to `'fixed'`. */\n strategy?: Strategy;\n /** Middleware to modify positioning behavior. */\n middleware?: Array<Middleware | null | undefined | false>;\n}\n\n/**\n * Computes and applies the floating position to a floating element.\n * Sets `left`/`top` inline styles and returns the resolved placement.\n *\n * @example\n * ```ts\n * positionFloat(reference, floating, {\n * placement: 'top',\n * middleware: [offset(8), flip(), shift({ padding: 6 })],\n * }).then(placement => el.dataset.placement = placement);\n * ```\n */\nexport function positionFloat(\n reference: Element,\n floating: HTMLElement,\n options: FloatOptions = {},\n): Promise<Placement> {\n return computePosition(reference, floating, {\n strategy: 'fixed',\n ...options,\n }).then(({ placement, x, y }) => {\n floating.style.left = `${x}px`;\n floating.style.top = `${y}px`;\n\n return placement;\n });\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`./floatit.cjs`);exports.autoUpdate=e.autoUpdate,exports.computePosition=e.computePosition,exports.flip=e.flip,exports.offset=e.offset,exports.positionFloat=e.positionFloat,exports.shift=e.shift,exports.size=e.size;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{autoUpdate as e,computePosition as t,flip as n,offset as r,positionFloat as i,shift as a,size as o}from"./floatit.js";export{e as autoUpdate,t as computePosition,n as flip,r as offset,i as positionFloat,a as shift,o as size};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vielzeug/floatit",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"source": "./src/index.ts",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build && pnpm run build:types",
|
|
21
|
+
"build:types": "tsc -p tsconfig.declarations.json",
|
|
22
|
+
"fix": "eslint --fix src",
|
|
23
|
+
"lint": "eslint src",
|
|
24
|
+
"prepublishOnly": "pnpm run build",
|
|
25
|
+
"test": "vitest"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.5.0",
|
|
34
|
+
"typescript": "~6.0.2",
|
|
35
|
+
"vite": "^8.0.2",
|
|
36
|
+
"vitest": "^4.1.1"
|
|
37
|
+
}
|
|
38
|
+
}
|