resize-iframe 0.1.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 +239 -0
- package/package.json +30 -0
- package/resize-iframe-child.js +113 -0
- package/resize-iframe.d.ts +76 -0
- package/resize-iframe.js +151 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [year] [fullname]
|
|
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,239 @@
|
|
|
1
|
+
# resize-iframe
|
|
2
|
+
|
|
3
|
+
Iframes that size themselves to their content, cross-origin included. One script on
|
|
4
|
+
each page, no build step, no dependencies, MIT.
|
|
5
|
+
|
|
6
|
+
- 📐 Auto-resizes on content, DOM, style, font, image and viewport changes
|
|
7
|
+
- ↔️ Vertical, horizontal, or both
|
|
8
|
+
- 🔒 Messages matched against the frame's own window, which cannot be forged
|
|
9
|
+
- 🍪 Storage Access API support for third-party embeds
|
|
10
|
+
- 🧩 Use `iframeResize()` or the `<resize-iframe>` element
|
|
11
|
+
- 📦 ~5KB unminified, across both files
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install resize-iframe
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or from a CDN — the parent page needs `resize-iframe.js`, the framed page needs
|
|
20
|
+
`resize-iframe-child.js`:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<script type="module" src="https://unpkg.com/resize-iframe/resize-iframe.js"></script>
|
|
24
|
+
<script src="https://unpkg.com/resize-iframe/resize-iframe-child.js"></script>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setting up the parent page
|
|
28
|
+
|
|
29
|
+
Give the iframe a percentage width and an initial height, then let the library
|
|
30
|
+
control the other dimension. Starting at `100vh` makes loading look smoother — the
|
|
31
|
+
content below the iframe only appears once it has sized itself.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<style>
|
|
35
|
+
#myIframe { width: 100%; height: 100vh; }
|
|
36
|
+
</style>
|
|
37
|
+
|
|
38
|
+
<iframe id="myIframe" src="https://anotherdomain.com/iframe.html"></iframe>
|
|
39
|
+
|
|
40
|
+
<script type="module">
|
|
41
|
+
import { iframeResize } from 'resize-iframe';
|
|
42
|
+
|
|
43
|
+
const [frame] = iframeResize({ direction: 'vertical' }, '#myIframe');
|
|
44
|
+
</script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`iframeResize(options, target)` takes a CSS selector, an element, or a list of
|
|
48
|
+
elements, and returns one handle per iframe. Both arguments are optional — with no
|
|
49
|
+
target it binds every iframe on the page.
|
|
50
|
+
|
|
51
|
+
Or use the element, which calls `iframeResize` for you:
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<script type="module" src="https://unpkg.com/resize-iframe/resize-iframe.js"></script>
|
|
55
|
+
|
|
56
|
+
<resize-iframe src="https://anotherdomain.com/iframe.html" title="Pricing table"></resize-iframe>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Setting up the child page
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<script src="https://unpkg.com/resize-iframe/resize-iframe-child.js"></script>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
That alone auto-resizes the frame. A cross-origin parent cannot measure your
|
|
66
|
+
content, so without this script nothing happens — the parent logs a warning after
|
|
67
|
+
five seconds saying exactly that.
|
|
68
|
+
|
|
69
|
+
The script tag takes two optional attributes:
|
|
70
|
+
|
|
71
|
+
| Attribute | Default | Description |
|
|
72
|
+
| -------------------- | -------------------- | ---------------------------------------------------- |
|
|
73
|
+
| `data-parent-origin` | `*` | Only report size to this embedder |
|
|
74
|
+
| `data-size-selector` | `[data-iframe-size]` | Measure these elements instead of the body's children |
|
|
75
|
+
|
|
76
|
+
Marking your content wrapper with `data-iframe-size` is the fix for a page that
|
|
77
|
+
measures larger than it looks — an overlay, a decorative element, a stray margin.
|
|
78
|
+
|
|
79
|
+
## Third-party embedding and cookies
|
|
80
|
+
|
|
81
|
+
Once your page is embedded on someone else's domain, browsers partition its
|
|
82
|
+
storage: cookies set first-party are invisible, so sessions and logins break before
|
|
83
|
+
sizing is ever the problem. The
|
|
84
|
+
[Storage Access API](https://developer.mozilla.org/en-US/docs/Web/API/Storage_Access_API)
|
|
85
|
+
is the way back, and the child script wires it up.
|
|
86
|
+
|
|
87
|
+
**Parent** — grant the permission and, if you sandbox, the matching token:
|
|
88
|
+
|
|
89
|
+
```html
|
|
90
|
+
<resize-iframe
|
|
91
|
+
src="https://embed.example.com/app"
|
|
92
|
+
allow="storage-access"
|
|
93
|
+
sandbox="allow-scripts allow-same-origin allow-storage-access-by-user-activation">
|
|
94
|
+
</resize-iframe>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Child** — probe on load and, if access is missing, ask for it from a click:
|
|
98
|
+
|
|
99
|
+
```html
|
|
100
|
+
<button id="continue" hidden>Continue</button>
|
|
101
|
+
|
|
102
|
+
<script>
|
|
103
|
+
addEventListener('load', async () => {
|
|
104
|
+
if (await parentIframe.hasStorageAccess()) return;
|
|
105
|
+
|
|
106
|
+
continue.hidden = false;
|
|
107
|
+
continue.onclick = async () => {
|
|
108
|
+
if (await parentIframe.requestStorageAccess()) location.reload();
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
</script>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Three things decide whether this works, none of them optional:
|
|
115
|
+
|
|
116
|
+
1. **A user gesture inside the frame.** The request is denied outright without a
|
|
117
|
+
click or tap in the embedded page. The parent cannot supply one for you, which
|
|
118
|
+
is why the button has to live in the child.
|
|
119
|
+
2. **A prior first-party visit.** Browsers only grant access to sites the user has
|
|
120
|
+
used directly. If your embed can be a user's first contact with your domain,
|
|
121
|
+
open it in a popup or new tab first, let them interact, then request access.
|
|
122
|
+
3. **Reload after granting.** Anything your page read at startup ran without
|
|
123
|
+
cookies. Reloading is cruder than re-fetching, and far easier to get right.
|
|
124
|
+
|
|
125
|
+
Probe the API, never the browser. Safari and Chrome both partition storage, and a
|
|
126
|
+
`/safari/i.test(navigator.userAgent)` gate silently skips the prompt everywhere
|
|
127
|
+
else — the failure is invisible, because the page loads fine and just acts logged
|
|
128
|
+
out.
|
|
129
|
+
|
|
130
|
+
The parent is told the outcome and can render its own fallback:
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
iframeResize({
|
|
134
|
+
onStorageAccess: ({ hasAccess }) => {
|
|
135
|
+
if (!hasAccess) showOpenInNewTabLink();
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Options
|
|
141
|
+
|
|
142
|
+
| Option | Default | Description |
|
|
143
|
+
| ----------------- | ------------ | --------------------------------------------------------------------- |
|
|
144
|
+
| `direction` | `'vertical'` | `'vertical'`, `'horizontal'`, `'both'`, or `'none'` |
|
|
145
|
+
| `offsetSize` | `0` | Pixels added to the computed size, positive or negative |
|
|
146
|
+
| `warningTimeout` | `5000` | Warn if the frame has not responded in this many ms; `0` to silence it |
|
|
147
|
+
| `onReady` | — | `(iframe) => void`, called on the first size message |
|
|
148
|
+
| `onResized` | — | `({ iframe, height, width }) => void` |
|
|
149
|
+
| `onMessage` | — | `({ iframe, message }) => void` |
|
|
150
|
+
| `onStorageAccess` | — | `({ iframe, hasAccess }) => void` |
|
|
151
|
+
|
|
152
|
+
With `direction: 'both'` give your content an intrinsic width, or a `width: 100%`
|
|
153
|
+
layout will chase the frame it is being measured in.
|
|
154
|
+
|
|
155
|
+
There is no `checkOrigin` option. Messages are matched against the frame's own
|
|
156
|
+
window, which the browser sets and a page cannot forge, and that holds even after
|
|
157
|
+
the frame navigates to another domain — the case an origin allowlist has to be
|
|
158
|
+
turned off for.
|
|
159
|
+
|
|
160
|
+
## Element attributes
|
|
161
|
+
|
|
162
|
+
| Attribute | Default | Description |
|
|
163
|
+
| ------------- | ------------------ | ---------------------------------------- |
|
|
164
|
+
| `src` | — | URL to embed |
|
|
165
|
+
| `title` | `Embedded content` | Accessible name for the iframe |
|
|
166
|
+
| `direction` | `vertical` | As above |
|
|
167
|
+
| `offset-size` | `0` | As above |
|
|
168
|
+
| `min-h` | — | Minimum height constraint |
|
|
169
|
+
| `max-h` | — | Maximum height constraint |
|
|
170
|
+
| `allow` | — | Passed through, e.g. `storage-access` |
|
|
171
|
+
| `sandbox` | — | Passed through |
|
|
172
|
+
|
|
173
|
+
## Methods and events
|
|
174
|
+
|
|
175
|
+
Parent, on the handle returned by `iframeResize` (also at `iframe.iframeResizer`):
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
frame.sendMessage({ hello: 'world' }, 'https://anotherdomain.com');
|
|
179
|
+
frame.disconnect(); // call before removing the iframe from the page
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Child, on `window.parentIframe`:
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
parentIframe.sendMessage('ping'); // → parent's onMessage / 'frame-message' event
|
|
186
|
+
parentIframe.autoResize(false); // pause resizing; returns the current state
|
|
187
|
+
parentIframe.resize(); // nudge, for a change neither observer sees
|
|
188
|
+
parentIframe.hasStorageAccess(); // → Promise<boolean>
|
|
189
|
+
parentIframe.requestStorageAccess(); // → Promise<boolean>, from a click handler
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Events fire on the iframe and bubble, so `<resize-iframe>` re-emits them:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
document.querySelector('resize-iframe').addEventListener('resize', (e) => {
|
|
196
|
+
console.log(`Frame is now ${e.detail.height}px tall`);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`ready`, `resize`, `frame-message`, and `storage-access` are available.
|
|
201
|
+
|
|
202
|
+
## TypeScript
|
|
203
|
+
|
|
204
|
+
Types ship with the package. JSX typing for the element is picked up automatically
|
|
205
|
+
by React 18 and below, Preact, and Solid. React 19 reads `React.JSX` instead, so
|
|
206
|
+
add this once:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import type { ResizeIframeAttributes } from 'resize-iframe';
|
|
210
|
+
|
|
211
|
+
declare module 'react' {
|
|
212
|
+
namespace JSX {
|
|
213
|
+
interface IntrinsicElements {
|
|
214
|
+
'resize-iframe': ResizeIframeAttributes;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Testing
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npx serve
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Open `resize-iframe-test.html`; it prints ten PASS lines.
|
|
227
|
+
|
|
228
|
+
## Browser Support
|
|
229
|
+
|
|
230
|
+
- Chrome/Edge 80+
|
|
231
|
+
- Firefox 75+
|
|
232
|
+
- Safari 13.1+
|
|
233
|
+
|
|
234
|
+
Storage Access API support is probed at runtime, so browsers without it simply
|
|
235
|
+
report access as granted.
|
|
236
|
+
|
|
237
|
+
## Licence
|
|
238
|
+
|
|
239
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "resize-iframe",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Iframes that size themselves to their content, cross-origin included. No build step, no dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "resize-iframe.js",
|
|
7
|
+
"module": "resize-iframe.js",
|
|
8
|
+
"types": "resize-iframe.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"resize-iframe.js",
|
|
11
|
+
"resize-iframe-child.js",
|
|
12
|
+
"resize-iframe.d.ts"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"iframe",
|
|
16
|
+
"iframe-resizer",
|
|
17
|
+
"resize",
|
|
18
|
+
"web-components",
|
|
19
|
+
"custom-element",
|
|
20
|
+
"postmessage",
|
|
21
|
+
"storage-access",
|
|
22
|
+
"third-party-cookies"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/jagreehal/resize-iframe"
|
|
27
|
+
},
|
|
28
|
+
"author": "Jag Reehal <jag@jagreehal.com>",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Load this inside the framed page:
|
|
2
|
+
// <script src=".../resize-iframe-child.js"></script>
|
|
3
|
+
// Optional attributes on that script tag:
|
|
4
|
+
// data-parent-origin="https://parent.example" report size only to that embedder
|
|
5
|
+
// data-size-selector=".content" measure these elements instead
|
|
6
|
+
// Or mark elements in the page with data-iframe-size to the same effect.
|
|
7
|
+
const config = document.currentScript?.dataset ?? {};
|
|
8
|
+
const targetOrigin = config.parentOrigin || '*';
|
|
9
|
+
const sizeSelector = config.sizeSelector || '[data-iframe-size]';
|
|
10
|
+
|
|
11
|
+
const start = () => {
|
|
12
|
+
let last = { height: 0, width: 0 };
|
|
13
|
+
let queued = false;
|
|
14
|
+
let auto = true;
|
|
15
|
+
|
|
16
|
+
// Measure the bottom and right edges of the marked elements, or of body's
|
|
17
|
+
// children. Never body itself: it stretches to fill the frame in quirks mode
|
|
18
|
+
// or under `body { height: 100% }`, which ratchets the frame larger and never
|
|
19
|
+
// lets it shrink back.
|
|
20
|
+
// ponytail: body's own bottom/right padding and margin are ignored — zero them
|
|
21
|
+
// if that gap matters, or wrap your content and mark it with data-iframe-size.
|
|
22
|
+
const measure = () => {
|
|
23
|
+
const marked = document.querySelectorAll(sizeSelector);
|
|
24
|
+
const elements = marked.length ? marked : document.body.children;
|
|
25
|
+
if (!elements.length) {
|
|
26
|
+
const body = document.body.getBoundingClientRect();
|
|
27
|
+
return { height: Math.ceil(body.bottom), width: Math.ceil(body.right) };
|
|
28
|
+
}
|
|
29
|
+
let height = 0;
|
|
30
|
+
let width = 0;
|
|
31
|
+
for (const element of elements) {
|
|
32
|
+
const box = element.getBoundingClientRect();
|
|
33
|
+
height = Math.max(height, box.bottom);
|
|
34
|
+
width = Math.max(width, box.right);
|
|
35
|
+
}
|
|
36
|
+
return { height: Math.ceil(height), width: Math.ceil(width) };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const post = () => {
|
|
40
|
+
queued = false;
|
|
41
|
+
const size = measure();
|
|
42
|
+
if (size.height === last.height && size.width === last.width) return;
|
|
43
|
+
last = size;
|
|
44
|
+
parent.postMessage({ 'resize-iframe': size }, targetOrigin);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Batch to one measurement per frame: mutations arrive in bursts and every
|
|
48
|
+
// measure() forces a layout.
|
|
49
|
+
const schedule = () => {
|
|
50
|
+
if (queued || !auto) return;
|
|
51
|
+
queued = true;
|
|
52
|
+
requestAnimationFrame(post);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ResizeObserver catches reflow (images, fonts, viewport). MutationObserver
|
|
56
|
+
// catches DOM and style changes, which the observer misses entirely whenever
|
|
57
|
+
// body is stretched to the frame and so never changes size itself.
|
|
58
|
+
new ResizeObserver(schedule).observe(document.body);
|
|
59
|
+
new MutationObserver(schedule).observe(document.body, {
|
|
60
|
+
subtree: true,
|
|
61
|
+
childList: true,
|
|
62
|
+
attributes: true,
|
|
63
|
+
characterData: true,
|
|
64
|
+
});
|
|
65
|
+
addEventListener('load', schedule); // images and fonts landing late
|
|
66
|
+
|
|
67
|
+
// Third-party cookies: an embedded page gets partitioned storage until the user
|
|
68
|
+
// grants access. Probe the API rather than sniffing the browser — Safari and
|
|
69
|
+
// Chrome both partition, and which browsers do is not a stable list.
|
|
70
|
+
const hasStorageAccess = () =>
|
|
71
|
+
document.hasStorageAccess?.().catch(() => false) ?? Promise.resolve(true);
|
|
72
|
+
|
|
73
|
+
const reportStorageAccess = async () => {
|
|
74
|
+
const access = await hasStorageAccess();
|
|
75
|
+
parent.postMessage({ 'resize-iframe-storage': { hasAccess: access } }, targetOrigin);
|
|
76
|
+
return access;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
window.parentIframe = {
|
|
80
|
+
autoResize(state) {
|
|
81
|
+
if (state !== undefined) auto = state;
|
|
82
|
+
if (auto) schedule();
|
|
83
|
+
return auto;
|
|
84
|
+
},
|
|
85
|
+
resize() {
|
|
86
|
+
post(); // nudge, for the rare change neither observer sees
|
|
87
|
+
},
|
|
88
|
+
sendMessage(message, origin = targetOrigin) {
|
|
89
|
+
parent.postMessage({ 'resize-iframe-message': message }, origin);
|
|
90
|
+
},
|
|
91
|
+
hasStorageAccess,
|
|
92
|
+
// MUST be called from a click or tap handler in this page: the browser denies
|
|
93
|
+
// the request without a user gesture here, and the parent cannot supply one.
|
|
94
|
+
// Reload afterwards if your page reads cookies during startup.
|
|
95
|
+
async requestStorageAccess() {
|
|
96
|
+
if (!document.requestStorageAccess) return true;
|
|
97
|
+
try {
|
|
98
|
+
await document.requestStorageAccess();
|
|
99
|
+
} catch {
|
|
100
|
+
// Denied, or the user has never visited this site first-party.
|
|
101
|
+
}
|
|
102
|
+
return reportStorageAccess();
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
reportStorageAccess();
|
|
107
|
+
post();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (parent !== window) {
|
|
111
|
+
if (document.body) start();
|
|
112
|
+
else addEventListener('DOMContentLoaded', start);
|
|
113
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface ResizeIframeOptions {
|
|
2
|
+
/** Which dimension the library controls. Default 'vertical'. */
|
|
3
|
+
direction?: 'vertical' | 'horizontal' | 'both' | 'none';
|
|
4
|
+
/** Pixels added to the computed size, positive or negative. Default 0. */
|
|
5
|
+
offsetSize?: number;
|
|
6
|
+
/** Warn if the frame has not responded in this many ms. 0 disables. Default 5000. */
|
|
7
|
+
warningTimeout?: number;
|
|
8
|
+
onReady?: (iframe: HTMLIFrameElement) => void;
|
|
9
|
+
onResized?: (data: {
|
|
10
|
+
iframe: HTMLIFrameElement;
|
|
11
|
+
height: number;
|
|
12
|
+
width: number;
|
|
13
|
+
}) => void;
|
|
14
|
+
onMessage?: (data: { iframe: HTMLIFrameElement; message: unknown }) => void;
|
|
15
|
+
onStorageAccess?: (data: {
|
|
16
|
+
iframe: HTMLIFrameElement;
|
|
17
|
+
hasAccess: boolean;
|
|
18
|
+
}) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResizeIframeHandle {
|
|
22
|
+
/** Call before removing the iframe from the page, or the listener leaks. */
|
|
23
|
+
disconnect(): void;
|
|
24
|
+
sendMessage(message: unknown, targetOrigin?: string): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Binds to a selector, an element, a list of elements, or every iframe on the page. */
|
|
28
|
+
export function iframeResize(
|
|
29
|
+
options?: ResizeIframeOptions,
|
|
30
|
+
target?: string | HTMLIFrameElement | HTMLIFrameElement[] | NodeList
|
|
31
|
+
): ResizeIframeHandle[];
|
|
32
|
+
|
|
33
|
+
/** Available inside the framed page once resize-iframe-child.js has loaded. */
|
|
34
|
+
export interface ParentIframe {
|
|
35
|
+
autoResize(state?: boolean): boolean;
|
|
36
|
+
resize(): void;
|
|
37
|
+
sendMessage(message: unknown, targetOrigin?: string): void;
|
|
38
|
+
hasStorageAccess(): Promise<boolean>;
|
|
39
|
+
/** Must be called from a click or tap handler in the framed page. */
|
|
40
|
+
requestStorageAccess(): Promise<boolean>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Attributes of the <resize-iframe> element. */
|
|
44
|
+
export interface ResizeIframeAttributes {
|
|
45
|
+
src?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
direction?: 'vertical' | 'horizontal' | 'both' | 'none';
|
|
48
|
+
'offset-size'?: number | string;
|
|
49
|
+
'min-h'?: string;
|
|
50
|
+
'max-h'?: string;
|
|
51
|
+
allow?: string;
|
|
52
|
+
sandbox?: string;
|
|
53
|
+
/** Whatever else the host framework puts on an element: class, ref, key. */
|
|
54
|
+
[attribute: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare global {
|
|
58
|
+
interface Window {
|
|
59
|
+
parentIframe?: ParentIframe;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface HTMLElementTagNameMap {
|
|
63
|
+
'resize-iframe': HTMLElement & {
|
|
64
|
+
readonly height: string | undefined;
|
|
65
|
+
sendMessage(message: unknown, targetOrigin?: string): void;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Picked up by React 18 and below, Preact, and Solid. React 19 moved to
|
|
70
|
+
// React.JSX — see the README for the one-line augmentation it needs.
|
|
71
|
+
namespace JSX {
|
|
72
|
+
interface IntrinsicElements {
|
|
73
|
+
'resize-iframe': ResizeIframeAttributes;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/resize-iframe.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Iframes that size themselves to their content, cross-origin included.
|
|
2
|
+
// Two ways in: iframeResize(options, target) for existing iframes, or the
|
|
3
|
+
// <resize-iframe src="..."> element. Both need resize-iframe-child.js in the
|
|
4
|
+
// framed page — a cross-origin parent cannot measure the content itself.
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
direction: 'vertical', // 'vertical' | 'horizontal' | 'both' | 'none'
|
|
7
|
+
offsetSize: 0,
|
|
8
|
+
warningTimeout: 5000,
|
|
9
|
+
onReady: null,
|
|
10
|
+
onResized: null,
|
|
11
|
+
onMessage: null,
|
|
12
|
+
onStorageAccess: null,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const toIframes = (target) => {
|
|
16
|
+
if (target == null) return [...document.querySelectorAll('iframe')];
|
|
17
|
+
if (typeof target === 'string') return [...document.querySelectorAll(target)];
|
|
18
|
+
return Array.isArray(target) || target instanceof NodeList ? [...target] : [target];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function iframeResize(options = {}, target) {
|
|
22
|
+
const settings = { ...DEFAULTS, ...options };
|
|
23
|
+
return toIframes(target).map((iframe) => connect(iframe, settings));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function connect(iframe, settings) {
|
|
27
|
+
const { direction, offsetSize } = settings;
|
|
28
|
+
let ready = false;
|
|
29
|
+
|
|
30
|
+
const warning = settings.warningTimeout
|
|
31
|
+
? setTimeout(() => {
|
|
32
|
+
if (!ready) {
|
|
33
|
+
console.warn(
|
|
34
|
+
`[resize-iframe] no response from ${iframe.src || 'iframe'} — is resize-iframe-child.js loaded in the framed page?`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}, settings.warningTimeout)
|
|
38
|
+
: 0;
|
|
39
|
+
|
|
40
|
+
const emit = (type, detail) =>
|
|
41
|
+
iframe.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
42
|
+
|
|
43
|
+
const onMessage = (event) => {
|
|
44
|
+
// The browser sets event.source, so it cannot be forged: only messages from
|
|
45
|
+
// this frame's own window get through, wherever it has navigated to. That
|
|
46
|
+
// covers what iframe-resizer's checkOrigin option does, without the config.
|
|
47
|
+
if (event.source !== iframe.contentWindow) return;
|
|
48
|
+
if (typeof event.data !== 'object' || event.data === null) return;
|
|
49
|
+
|
|
50
|
+
if ('resize-iframe-message' in event.data) {
|
|
51
|
+
const message = event.data['resize-iframe-message'];
|
|
52
|
+
settings.onMessage?.({ iframe, message });
|
|
53
|
+
emit('frame-message', message);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ('resize-iframe-storage' in event.data) {
|
|
58
|
+
const { hasAccess } = event.data['resize-iframe-storage'];
|
|
59
|
+
settings.onStorageAccess?.({ iframe, hasAccess });
|
|
60
|
+
emit('storage-access', { hasAccess });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const size = event.data['resize-iframe'];
|
|
65
|
+
if (!size) return;
|
|
66
|
+
const height = size.height + offsetSize;
|
|
67
|
+
const width = size.width + offsetSize;
|
|
68
|
+
|
|
69
|
+
if (direction === 'vertical' || direction === 'both') iframe.style.height = `${height}px`;
|
|
70
|
+
if (direction === 'horizontal' || direction === 'both') iframe.style.width = `${width}px`;
|
|
71
|
+
|
|
72
|
+
if (!ready) {
|
|
73
|
+
ready = true;
|
|
74
|
+
clearTimeout(warning);
|
|
75
|
+
settings.onReady?.(iframe);
|
|
76
|
+
emit('ready', { iframe });
|
|
77
|
+
}
|
|
78
|
+
settings.onResized?.({ iframe, height, width });
|
|
79
|
+
emit('resize', { height, width });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
addEventListener('message', onMessage);
|
|
83
|
+
|
|
84
|
+
// Call disconnect() before removing the iframe, or the listener leaks.
|
|
85
|
+
iframe.iframeResizer = {
|
|
86
|
+
disconnect() {
|
|
87
|
+
removeEventListener('message', onMessage);
|
|
88
|
+
clearTimeout(warning);
|
|
89
|
+
delete iframe.iframeResizer;
|
|
90
|
+
},
|
|
91
|
+
sendMessage(message, targetOrigin = '*') {
|
|
92
|
+
iframe.contentWindow?.postMessage(message, targetOrigin);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return iframe.iframeResizer;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class ResizeIframe extends HTMLElement {
|
|
99
|
+
static get observedAttributes() {
|
|
100
|
+
return ['src', 'min-h', 'max-h', 'allow', 'sandbox'];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
connectedCallback() {
|
|
104
|
+
if (!this.shadowRoot) {
|
|
105
|
+
this.attachShadow({ mode: 'open' }).innerHTML = `
|
|
106
|
+
<style>
|
|
107
|
+
:host { display: block; }
|
|
108
|
+
iframe { display: block; width: 100%; border: 0; }
|
|
109
|
+
</style>
|
|
110
|
+
<iframe part="frame"></iframe>`;
|
|
111
|
+
this.iframe = this.shadowRoot.querySelector('iframe');
|
|
112
|
+
// An iframe without an accessible name is a screen reader dead end.
|
|
113
|
+
this.iframe.title = this.getAttribute('title') || 'Embedded content';
|
|
114
|
+
for (const name of ResizeIframe.observedAttributes) {
|
|
115
|
+
this.attributeChangedCallback(name, null, this.getAttribute(name));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
iframeResize(
|
|
119
|
+
{
|
|
120
|
+
direction: this.getAttribute('direction') || DEFAULTS.direction,
|
|
121
|
+
offsetSize: Number(this.getAttribute('offset-size')) || 0,
|
|
122
|
+
},
|
|
123
|
+
this.iframe
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
disconnectedCallback() {
|
|
128
|
+
this.iframe?.iframeResizer?.disconnect();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
132
|
+
if (!this.iframe || newValue === null) return;
|
|
133
|
+
if (name === 'src') this.iframe.src = newValue;
|
|
134
|
+
if (name === 'min-h') this.iframe.style.minHeight = newValue;
|
|
135
|
+
if (name === 'max-h') this.iframe.style.maxHeight = newValue;
|
|
136
|
+
// Passed through because third-party embeds need them: storage access is
|
|
137
|
+
// denied outright unless the frame carries allow="storage-access", and a
|
|
138
|
+
// sandboxed frame also needs allow-storage-access-by-user-activation.
|
|
139
|
+
if (name === 'allow' || name === 'sandbox') this.iframe.setAttribute(name, newValue);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get height() {
|
|
143
|
+
return this.iframe?.style.height;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
sendMessage(message, targetOrigin) {
|
|
147
|
+
this.iframe?.iframeResizer?.sendMessage(message, targetOrigin);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
customElements.define('resize-iframe', ResizeIframe);
|