├── .editorconfig
├── .github
└── workflows
│ ├── ci.yaml
│ └── package-size-report.yaml
├── .gitignore
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── examples
├── amp.html
├── basic.html
├── embed-pym.html
├── embed.html
├── pym.html
└── server.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── src
├── auto.js
├── constants.js
├── framer.js
├── frames.js
└── index.js
└── tests
├── public
├── auto-embed.html
├── auto.html
├── embed.html
├── index.html
├── init-auto.js
├── init-framer-pym.js
├── init-framer.js
├── observe-iframe.js
├── pym-embed.html
├── pym.html
├── pym.js
├── send-frame-height-controller.html
├── send-frame-height.html
├── send-height-on-framer-init.html
├── send-height-on-load.html
├── send-height-on-poll.html
├── send-height-on-resize.html
└── style.css
├── server.js
└── test.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | tests:
7 | runs-on: ubuntu-latest
8 |
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | browser: [chromium, firefox, webkit]
13 |
14 | steps:
15 | - name: Checkout the repo
16 | uses: actions/checkout@v2
17 |
18 | - name: Setup Node.js v16
19 | uses: actions/setup-node@v2
20 | with:
21 | node-version: '16'
22 | cache: 'npm'
23 |
24 | - name: Install dependencies
25 | run: npm ci
26 |
27 | - name: Install Playwright browsers
28 | run: npx playwright install-deps ${{ matrix.browser }}
29 |
30 | - name: Run tests
31 | run: npm test
32 | env:
33 | BROWSER: ${{ matrix.browser }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/package-size-report.yaml:
--------------------------------------------------------------------------------
1 | name: Package Size Report
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | pkg-size-report:
10 | name: Package Size Report
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v2
16 |
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v2
19 | with:
20 | node-version: '16'
21 | cache: 'npm'
22 |
23 | - name: Package size report
24 | uses: pkg-size/action@v1
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | with:
28 | display-size: uncompressed, gzip
29 | hide-files: '*.{js,cjs}.map'
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless/
74 |
75 | # FuseBox cache
76 | .fusebox/
77 |
78 | # DynamoDB Local files
79 | .dynamodb/
80 |
81 | # Project files
82 | dist
83 | mangle.json
84 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package.json
3 | package-lock.json
4 | dist
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [1.0.0] - 2022-01-04
11 |
12 | ### Added
13 |
14 | - Tests! Of everything!
15 |
16 | - The act of "observing" an iframe has been broken out of the `Framer` class into its own function — `observeIframe`! This makes it possible to observe `@newswire/frames` compatible-iframes that have been created independent of this library. This means it is now possible to use your own code to create iframes (perhaps lazy load them with `IntersectionObserver`!), have them added via your CMS/templating engine, etc.
17 |
18 | It's important to remember however that this method **does not** add any attributes to the existing iframe. It just sets up the observer and stops there. This means it's on you to use CSS or other methods to style the iframe. (Set width to `100%`, etc.)
19 |
20 | ```js
21 | // grab a reference to an existing iframe, assuming there's already a "src" on this
22 | const iframe = document.getElementById('my-embed');
23 |
24 | // returns a `unobserve()` function if you need to stop listening
25 | const unobserve = observeIframe(iframe);
26 |
27 | // later, if you need to disconnect from the iframe
28 | unobserve();
29 | ```
30 |
31 | As the example shows above, you can _also_ now disable the observer using the `unobserve` function `observeIframe` returns. Unlike the `remove()` method on `Framer`, this will **not** remove the iframe from the DOM.
32 |
33 | - On the frames side there is a new method for notifying the parent `Framer` of an embed's size - `sendHeightOnFramerInit`. Once an iframe is observed (with either `observeIframe` or `Framer`), the parent page will notify the iframe it is now ready to receive height updates. In response, the iframe will send a message back to the parent `Framer` with the initial height of the iframe. This should help get the correct iframe height to the parent page sooner.
34 |
35 | `sendHeightOnFramerInit` has been added to both `initFrame` and `initFrameAndPoll`.
36 |
37 | - `@newswire/frames` now has legacy support for [Pym.js](http://blog.apps.npr.org/pym.js/) child frames. This means you can now use `@newswire/frames` to resize iframes that have been built with Pym.js. However - `@newswire/frames` only recognizes Pym.js' `height` events. All other events **will be ignored**.
38 |
39 | ### Changed
40 |
41 | - `Framer` still exists but its interface has changed. Because the `container` was never optional it is now the first expected parameter when creating a new instance. The second parameter is now an object with two optional properties - `src` and `attributes`. `src` does what you expect and sets the `src` attribute on the iframe, but the `attributes` object is the new way to configure any other attributes on the `iframe` that's created. It's now just a convienient way to loop over an object and call `setAttribute`.
42 |
43 | Why the change? The most common request to this library has been to add additional attributes that `Framer` can apply to the iframe it creates. (Or the ability to _not_ set one, [like `src`](https://github.com/rdmurphy/frames/pull/6)!) Instead of having to add support to `Framer` for every attribute you want to set on the iframe, it's now just a matter of adding a new property to the `attributes` object.
44 |
45 | - `Framer` is no longer a class and instead just a function that returns an object. It was never really intended to be subclassed and this makes it a bit more compact when bundled, but it is still compatible with `new` if you prefer that.
46 |
47 | - The auto loader now expects attributes to be set on containers using the `data-frame-attribute-` prefix. This is to match the new way of passing attributes to `Framer`.
48 |
49 | ```html
50 |
51 |
52 |
53 |
54 |
58 | ```
59 |
60 | ## [0.3.1] - 2019-02-25
61 |
62 | ### Fixed
63 |
64 | - Previous release did not actually contain changes. 😣
65 |
66 | ## [0.3.0] - 2019-02-25
67 |
68 | ### Added
69 |
70 | - Added support for `title` attribute.
71 |
72 | ### Changed
73 |
74 | - The name of the library for the UMD build is now `newswireFrames` instead of `frames`. This change was necessary to prevent a clash with the native [`Window.frames`](https://developer.mozilla.org/en-US/docs/Web/API/Window/frames).
75 |
76 | ## [0.2.0] - 2019-02-12
77 |
78 | ### Changed
79 |
80 | - We no longer use [spread in object literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals), which was adding an [`Object.assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) call in the compiled library. This breaks `@newswire/frames` in IE 11. We've moved to a tiny built-in extend implementation that restores IE 11 support.
81 |
82 | ## [0.1.0] - 2018-12-30
83 |
84 | ### Added
85 |
86 | - Initial release!
87 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ryan Murphy
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | @newswire/frames
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | `@newswire/frames` is a minimalistic take on responsive iframes in the spirit of [Pym.js](http://blog.apps.npr.org/pym.js/).
13 |
14 | ## Key features
15 |
16 | - 🐜 **~1 kilobyte** gzipped for both parent and frame code
17 | - 🌴 **Tree-shakable** - import only what you need for your use case
18 | - ⚡️ **Speaks [AMP](https://www.ampproject.org)** and is compatible with [`amp-iframe`](https://www.ampproject.org/docs/reference/components/amp-iframe)
19 | - 🕰 **Legacy [Pym.js](http://blog.apps.npr.org/pym.js/) support** - recognizes `height` updates from Pym.js iframes
20 | - 🧪 **Fully tested** in Safari, Chrome and Firefox with [Playwright](https://playwright.dev)
21 |
22 | ## Supported browsers
23 |
24 | | Browser | Supported |
25 | | ------------------------------ | --------- |
26 | | Safari | ✅ |
27 | | Mozilla Firefox | ✅ |
28 | | Google Chrome | ✅ |
29 | | Opera | ✅ |
30 | | Microsoft Edge | ✅ |
31 | | Internet Explorer 11 | ✅ |
32 | | Internet Explorer 10 and lower | ⛔️ |
33 |
34 | ## Installation
35 |
36 | `@newswire/frames` is available via `npm`.
37 |
38 | ```sh
39 | npm install @newswire/frames
40 | ```
41 |
42 | You can also use it directly via [unpkg.com](https://unpkg.com/).
43 |
44 | ```html
45 |
46 |
47 | ```
48 |
49 | You can also import it as a module via unpkg!
50 |
51 | ```html
52 |
57 | ```
58 |
59 | ## Usage
60 |
61 | ### From the **host** page (_framer_ or _parent_)
62 |
63 | The page that contains the embeds needs to use the `Framer` class to set up instances for each embed.
64 |
65 | Assume we have the following markup in our HTML:
66 |
67 | ```html
68 | Loading...
69 | ```
70 |
71 | Then, in our script:
72 |
73 | ```js
74 | import { Framer } from '@newswire/frames';
75 |
76 | const container = document.getElementById('embed-container');
77 | const src = 'https://i-am-an-embed/';
78 | const attributes = { sandbox: 'allow-scripts allow-same-origin' };
79 |
80 | const framer = new Framer(container, { src, attributes });
81 | // Now the iframe has been added to the page and is listening for height changes notifications from within the iframe
82 | ```
83 |
84 | It is also possible to observe existing iframes on a page if the content of the frames are compatible with `@newswire/frames`. This is handy if you already have your own method to dynamically add iframes to the page, or are using a custom method to lazy load them and don't need the heavy hand of `Framer`.
85 |
86 | ```js
87 | import { observeIframe } from '@newswire/frames';
88 |
89 | // grab a reference to an existing iframe
90 | const iframe = document.getElementById('my-embed');
91 |
92 | // returns a `unobserve()` function if you need to stop listening
93 | const unobserve = observeIframe(iframe);
94 |
95 | // later, if you need to disconnect from the iframe
96 | unobserve();
97 | ```
98 |
99 | Pym.js had the ability to automatically initialize embeds that had matching attibutes on their container elements — `@newswire/frames` can do this as well.
100 |
101 | Assume we have the following markup in our HTML:
102 |
103 | ```html
104 |
105 |
106 |
107 | ```
108 |
109 | Then in our script, we can skip the fanfare of setting up a `Framer` for each one and use the `data-frame-src` attribute to find them.
110 |
111 | ```js
112 | import { autoInitFrames } from '@newswire/frames';
113 |
114 | // looks for any elements with `data-frame-src` that haven't been initialized yet, and sets them up
115 | autoInitFrames();
116 | ```
117 |
118 | If you're needing to pass any of the other options to `Framer` when you're automatically creating the embeds, you can add attributes that the initializer will pick up and pass along using the `data-frame-attribute-*` prefix.
119 |
120 | ```html
121 |
125 |
126 |
127 |
129 | ```
130 |
131 | ### From the **embedded** page (_frame_ or _child_)
132 |
133 | While the code to setup the host page is similar to Pym's `Parent` class, the methods for making the iframed page communicate with the host page are a little different.
134 |
135 | Want to set it and forget it? You can import a function that sets up listeners and sends the initial height of the frame's content.
136 |
137 | ```js
138 | import { initFrame } from '@newswire/frames';
139 |
140 | // 1. Sends the initial frame's content height
141 | // 2. Sets up an one-time istener to send the height on load
142 | // 3. Sets up a listener to send the height every time the frame resizes
143 | // 4. Sets up an event listener that sends the height once the parent window begins watching
144 | initFrame();
145 | ```
146 |
147 | You can also automatically set up long polling for height changes as well.
148 |
149 | ```js
150 | import { initFrameAndPoll } from '@newswire/frames';
151 |
152 | // 1. Sends the initial frame's content height
153 | // 2. Sets up an one-time listener to send the height on load
154 | // 3. Sets up a listener to send the height every time the frame resizes
155 | // 4. Sets up an event listener that sends the height once the parent window begins watching
156 | // 5. Sets up an interval to send a new height update every 300ms
157 | initFrameAndPoll();
158 | ```
159 |
160 | Alternatively, you can set and use function independently depending on the needs of your frame's content.
161 |
162 | ```js
163 | import {
164 | sendFrameHeight,
165 | sendHeightOnLoad,
166 | sendHeightOnResize,
167 | sendHeightOnPoll,
168 | sendHeightOnFramerInit,
169 | } from '@newswire/frames';
170 |
171 | // 1. Sends the initial frame's content height
172 | sendFrameHeight();
173 |
174 | // 2. Sets up an one-time listener to send the height on load
175 | sendHeightOnLoad();
176 |
177 | // 3. Sets up a listener to send the height every time the frame resizes
178 | sendHeightOnResize();
179 |
180 | // 4. Sets up an event listener that sends the height once the parent window begins watching
181 | sendHeightOnFramerInit();
182 |
183 | // 5. Sets up an interval to send a new height update every 150ms
184 | sendHeightOnPoll(150);
185 |
186 | // 1-4 is identical to initFrame()! 1-5 is identical to initFrameAndPoll()!
187 | ```
188 |
189 | Typically using `initFrame()` will be enough, but if you have code that will potentially change the height of the frame's content (like with an ` ` or `` press) and would rather not use polling, you can use `sendFrameHeight()` to manually recalculate and send an update to the parent page.
190 |
191 | ## Legacy Pym.js support
192 |
193 | `@newswire/frames` (`1.0.0+`) can be a drop-in replacement for `Pym.js` on host/parent pages, but it only understands `height` events sent from Pym.js-powered child frames. All other events **will be ignored**. Unless you were doing something exotic or extremely bespoke with Pym.js odds are it will work!
194 |
195 | ## API
196 |
197 |
198 |
199 | #### Table of Contents
200 |
201 | - [observeIframe](#observeiframe)
202 | - [Parameters](#parameters)
203 | - [Examples](#examples)
204 | - [FramerOptions](#frameroptions)
205 | - [Properties](#properties)
206 | - [Framer](#framer)
207 | - [Parameters](#parameters-1)
208 | - [autoInitFrames](#autoinitframes)
209 | - [Examples](#examples-1)
210 | - [sendFrameHeight](#sendframeheight)
211 | - [Parameters](#parameters-2)
212 | - [Examples](#examples-2)
213 | - [sendHeightOnLoad](#sendheightonload)
214 | - [Examples](#examples-3)
215 | - [sendHeightOnResize](#sendheightonresize)
216 | - [Examples](#examples-4)
217 | - [sendHeightOnFramerInit](#sendheightonframerinit)
218 | - [Examples](#examples-5)
219 | - [sendHeightOnPoll](#sendheightonpoll)
220 | - [Parameters](#parameters-3)
221 | - [Examples](#examples-6)
222 | - [initFrame](#initframe)
223 | - [Examples](#examples-7)
224 | - [initFrameAndPoll](#initframeandpoll)
225 | - [Parameters](#parameters-4)
226 | - [Examples](#examples-8)
227 |
228 | ### observeIframe
229 |
230 | Adds an event listener to an existing iframe for receiving height change
231 | messages. Also tells the iframe that we're listening and requests the
232 | initial height. Returns an `unobserve()` function for later removing the
233 | listener.
234 |
235 | #### Parameters
236 |
237 | - `iframe` **[HTMLIFrameElement](https://developer.mozilla.org/docs/Web/API/HTMLIFrameElement)** the iframe to observe
238 |
239 | #### Examples
240 |
241 | ```javascript
242 | // grab a reference to an existing iframe
243 | const iframe = document.getElementById('my-embed');
244 |
245 | // returns a `unobserve()` function if you need to stop listening
246 | const unobserve = observeIframe(iframe);
247 |
248 | // later, if you need to disconnect from the iframe
249 | unobserve();
250 | ```
251 |
252 | ### FramerOptions
253 |
254 | Type: [object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object)
255 |
256 | #### Properties
257 |
258 | - `src` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | null)?** the URL to set as the `src` of the iframe
259 | - `attributes` **Record<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>?** any attributes to add to the iframe itself
260 |
261 | ### Framer
262 |
263 | The Framer function to be called in the host page. A wrapper around
264 | interactions with a created iframe. Returns a `remove()` function for
265 | disconnecting the event listener and removing the iframe from the DOM.
266 |
267 | #### Parameters
268 |
269 | - `container` **[Element](https://developer.mozilla.org/docs/Web/API/Element)** the containing DOM element for the iframe
270 | - `options` **[FramerOptions](#frameroptions)** (optional, default `{}`)
271 |
272 | - `options.attributes`
273 | - `options.src`
274 |
275 | ### autoInitFrames
276 |
277 | Automatically initializes any frames that have not already been
278 | auto-activated.
279 |
280 | #### Examples
281 |
282 | ```javascript
283 | // sets up all frames that have not been initialized yet
284 | autoInitFrames();
285 | ```
286 |
287 | ### sendFrameHeight
288 |
289 | Sends the current document's height or provided value to the host window
290 | using postMessage.
291 |
292 | #### Parameters
293 |
294 | - `height` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** The height to pass to the host page, is determined automatically if not passed (optional, default `getDocumentHeight()`)
295 |
296 | #### Examples
297 |
298 | ```javascript
299 | // Uses the document's height to tell the host page
300 | sendFrameHeight();
301 |
302 | // Pass a height you've determined in another way
303 | sendFrameHeight(500);
304 | ```
305 |
306 | Returns **void**
307 |
308 | ### sendHeightOnLoad
309 |
310 | Sets up an event listener for the load event that sends the new frame
311 | height to the host. Automatically removes itself once fired.
312 |
313 | #### Examples
314 |
315 | ```javascript
316 | // once the frame's load event is fired, tell the host page its new height
317 | sendHeightOnLoad();
318 | ```
319 |
320 | Returns **void**
321 |
322 | ### sendHeightOnResize
323 |
324 | Sets up an event listener for the resize event that sends the new frame
325 | height to the host.
326 |
327 | #### Examples
328 |
329 | ```javascript
330 | // every time the frame is resized, tell the host page what its new height is
331 | sendHeightOnResize();
332 | ```
333 |
334 | Returns **void**
335 |
336 | ### sendHeightOnFramerInit
337 |
338 | Sets up an event listener for a message from the parent window that it is
339 | now listening for messages from this iframe, and tells it the iframe's height
340 | at that time. This makes it possible to delay observing an iframe (e.g. when
341 | lazy loading) but trust the parent will get the current height ASAP.
342 |
343 | #### Examples
344 |
345 | ```javascript
346 | // as soon as a Framer connects, tell the host page what the current height is
347 | sendHeightOnFramerInit();
348 | ```
349 |
350 | Returns **void**
351 |
352 | ### sendHeightOnPoll
353 |
354 | Sends height updates to the host page on an interval.
355 |
356 | #### Parameters
357 |
358 | - `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** How long to set the interval (optional, default `300`)
359 |
360 | #### Examples
361 |
362 | ```javascript
363 | // will call sendFrameHeight every 300ms
364 | sendHeightOnPoll();
365 |
366 | // will call sendFrameHeight every 150ms
367 | sendHeightOnPoll(150);
368 | ```
369 |
370 | Returns **void**
371 |
372 | ### initFrame
373 |
374 | A helper for running the standard functions for setting up a frame.
375 |
376 | Automatically calls an `sendFrameHeight`, `sendHeightOnLoad`, `sendHeightOnResize`
377 | and `sendHeightOnFramerInit`.
378 |
379 | #### Examples
380 |
381 | ```javascript
382 | initFrame();
383 | ```
384 |
385 | Returns **void**
386 |
387 | ### initFrameAndPoll
388 |
389 | Calls `initFrame` to setup a frame, then initializes a poller to continue to update on an interval.
390 |
391 | #### Parameters
392 |
393 | - `delay` **[number](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number)?** An optional custom delay to pass to sendHeightOnPoll
394 |
395 | #### Examples
396 |
397 | ```javascript
398 | // calls initFrame, then calls sendHeightOnPoll
399 | initFrameAndPoll();
400 | ```
401 |
402 | Returns **void**
403 |
404 | ## License
405 |
406 | MIT
407 |
--------------------------------------------------------------------------------
/examples/amp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Hello, AMPs
7 |
11 |
15 |
24 |
72 |
82 |
87 |
88 |
89 | Welcome to the mobile web
90 |
91 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Id commodi
92 | repudiandae nisi ducimus deleniti a odio, soluta odit, laboriosam
93 | pariatur, voluptates veniam perferendis non? Tenetur, magni? Eligendi
94 | ipsum rerum error? Lorem ipsum dolor sit amet, consectetur adipisicing
95 | elit. Suscipit a dolorem atque itaque incidunt natus assumenda minus,
96 | nihil reprehenderit, tempore eum facere quia, perferendis molestias
97 | aliquam. Facilis nam ea dolores.
98 |
99 |
100 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Id commodi
101 | repudiandae nisi ducimus deleniti a odio, soluta odit, laboriosam
102 | pariatur, voluptates veniam perferendis non? Tenetur, magni? Eligendi
103 | ipsum rerum error? Lorem ipsum dolor sit amet, consectetur adipisicing
104 | elit. Suscipit a dolorem atque itaque incidunt natus assumenda minus,
105 | nihil reprehenderit, tempore eum facere quia, perferendis molestias
106 | aliquam. Facilis nam ea dolores.
107 |
108 |
109 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Id commodi
110 | repudiandae nisi ducimus deleniti a odio, soluta odit, laboriosam
111 | pariatur, voluptates veniam perferendis non? Tenetur, magni? Eligendi
112 | ipsum rerum error? Lorem ipsum dolor sit amet, consectetur adipisicing
113 | elit. Suscipit a dolorem atque itaque incidunt natus assumenda minus,
114 | nihil reprehenderit, tempore eum facere quia, perferendis molestias
115 | aliquam. Facilis nam ea dolores.
116 |
117 |
118 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Id commodi
119 | repudiandae nisi ducimus deleniti a odio, soluta odit, laboriosam
120 | pariatur, voluptates veniam perferendis non? Tenetur, magni? Eligendi
121 | ipsum rerum error? Lorem ipsum dolor sit amet, consectetur adipisicing
122 | elit. Suscipit a dolorem atque itaque incidunt natus assumenda minus,
123 | nihil reprehenderit, tempore eum facere quia, perferendis molestias
124 | aliquam. Facilis nam ea dolores.
125 |
126 |
134 |
135 | Read more!
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/examples/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Basic
8 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/examples/embed-pym.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pym Embed
8 |
26 |
27 |
28 |
29 |
30 |
48 |
49 |
--------------------------------------------------------------------------------
/examples/embed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
26 |
27 |
28 |
29 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/examples/pym.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pym
8 |
13 |
14 |
15 |
16 |
17 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/server.js:
--------------------------------------------------------------------------------
1 | // native
2 | import { createServer } from 'node:http';
3 |
4 | // packages
5 | import sirv from 'sirv';
6 |
7 | const dist = sirv('./src', { dev: true });
8 | const examples = sirv('./examples', { dev: true });
9 | const wares = [
10 | (req, res, next) => {
11 | // so AMP will shut up
12 | if (req.url.endsWith('.js')) {
13 | res.setHeader('Access-Control-Allow-Origin', 'null');
14 | }
15 |
16 | next();
17 | },
18 | dist,
19 | examples,
20 | ];
21 |
22 | createServer(function requestListener(req, res) {
23 | let index = 0;
24 |
25 | function next() {
26 | const ware = wares[index++];
27 |
28 | if (!ware) {
29 | res.statusCode = 404;
30 | res.end('404 Not Found');
31 | return;
32 | }
33 |
34 | ware(req, res, next);
35 | }
36 |
37 | next();
38 | }).listen(3000);
39 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "allowUmdGlobalAccess": true,
5 | "checkJs": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "isolatedModules": true,
8 | "jsx": "preserve",
9 | "jsxFactory": "h",
10 | "lib": ["DOM", "ES2020"],
11 | "module": "ESNext",
12 | "moduleResolution": "node",
13 | "noEmit": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "esnext",
18 | },
19 | "include": ["src/*"]
20 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@newswire/frames",
3 | "type": "module",
4 | "version": "1.0.0",
5 | "description": "A minimalistic take on responsive iframes in the spirit of Pym.js.",
6 | "source": "src/index.js",
7 | "main": "dist/frames.cjs",
8 | "exports": "./dist/frames.modern.js",
9 | "module": "dist/frames.module.js",
10 | "unpkg": "dist/frames.umd.js",
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "microbundle src/index.js --name newswireFrames --target web",
16 | "build:watch": "microbundle watch src/index.js --name newswireFrames --target web --no-compress & npm run serve",
17 | "docs": "documentation readme --readme-file README.md --section=API src/index.js && prettier --write README.md",
18 | "prerelease": "npm test && agadoo",
19 | "release": "git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish",
20 | "serve": "node server.js",
21 | "test": "node tests/test.js"
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/rdmurphy/frames.git"
26 | },
27 | "keywords": [
28 | "responsive",
29 | "iframes",
30 | "amphtml"
31 | ],
32 | "author": "Ryan Murphy",
33 | "license": "MIT",
34 | "bugs": {
35 | "url": "https://github.com/rdmurphy/frames/issues"
36 | },
37 | "homepage": "https://github.com/rdmurphy/frames#readme",
38 | "devDependencies": {
39 | "@rdm/prettier-config": "^2.0.0",
40 | "agadoo": "^2.0.0",
41 | "documentation": "^13.1.1",
42 | "microbundle": "^0.14.2",
43 | "playwright": "^1.17.1",
44 | "prettier": "^2.5.1",
45 | "sirv": "^2.0.0",
46 | "uvu": "^0.5.2"
47 | },
48 | "prettier": "@rdm/prettier-config",
49 | "mangle": {
50 | "regex": "_$"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/auto.js:
--------------------------------------------------------------------------------
1 | import { Framer } from './framer.js';
2 | import {
3 | FRAME_ATTRIBUTE_PREFIX,
4 | FRAME_AUTO_INITIALIZED,
5 | FRAME_SRC,
6 | } from './constants.js';
7 |
8 | /**
9 | * @private
10 | * @type {number}
11 | */
12 | const prefixLength = FRAME_ATTRIBUTE_PREFIX.length;
13 |
14 | /**
15 | * Searches an element's attributes and returns an Object of all the ones that
16 | * begin with our prefix. Each matching attribute name is returned
17 | * without the prefix.
18 | *
19 | * @private
20 | * @param {Element} element
21 | * @return {Record}
22 | */
23 | function getMatchingAttributes(element) {
24 | // prepare the object to return
25 | /**
26 | * @private
27 | * @type {Record}
28 | */
29 | const attrs = {};
30 |
31 | // grab all the attributes off the element
32 | const map = element.attributes;
33 |
34 | // get a count of the number of attributes
35 | const length = map.length;
36 |
37 | // loop through the attributes
38 | for (let i = 0; i < length; i++) {
39 | // get each attribute key
40 | const key = map[i].name;
41 |
42 | // continue if the key begins with supplied prefix
43 | if (key.slice(0, prefixLength) === FRAME_ATTRIBUTE_PREFIX) {
44 | // slice off the prefix to get the bare field key
45 | const field = key.slice(prefixLength);
46 |
47 | // grab the value associated with the key
48 | const value = map[i].value;
49 |
50 | // add matching key to object
51 | attrs[field] = value;
52 | }
53 | }
54 |
55 | return attrs;
56 | }
57 |
58 | /**
59 | * Automatically initializes any frames that have not already been
60 | * auto-activated.
61 | *
62 | * @example
63 | * // sets up all frames that have not been initialized yet
64 | * autoInitFrames();
65 | */
66 | export function autoInitFrames() {
67 | const elements = document.querySelectorAll(
68 | `[${FRAME_SRC}]:not([${FRAME_AUTO_INITIALIZED}])`,
69 | );
70 |
71 | for (let i = 0; i < elements.length; i++) {
72 | const container = elements[i];
73 |
74 | const src = container.getAttribute(FRAME_SRC);
75 | const attributes = getMatchingAttributes(container);
76 | container.setAttribute(FRAME_AUTO_INITIALIZED, '');
77 |
78 | Framer(container, { attributes, src });
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const HEIGHT = 'height';
2 | export const EMBED_SIZE = 'embed-size';
3 | export const INITIAL_MESSAGE = 'frames-init';
4 | export const AMP_SENTINEL = 'amp';
5 | export const PYM_SENTINEL = 'pym';
6 | export const PYM_REGEX = /xPYMx/;
7 | export const FRAME_PREFIX = 'data-frame-';
8 | export const FRAME_AUTO_INITIALIZED = `${FRAME_PREFIX}auto-initialized`;
9 | export const FRAME_SRC = `${FRAME_PREFIX}src`;
10 | export const FRAME_ATTRIBUTE_PREFIX = `${FRAME_PREFIX}attribute-`;
11 |
--------------------------------------------------------------------------------
/src/framer.js:
--------------------------------------------------------------------------------
1 | import {
2 | AMP_SENTINEL,
3 | EMBED_SIZE,
4 | HEIGHT,
5 | INITIAL_MESSAGE,
6 | PYM_REGEX,
7 | PYM_SENTINEL,
8 | } from './constants.js';
9 |
10 | /**
11 | * Adds an event listener to an existing iframe for receiving height change
12 | * messages. Also tells the iframe that we're listening and requests the
13 | * initial height. Returns an `unobserve()` function for later removing the
14 | * listener.
15 | *
16 | * @param {HTMLIFrameElement} iframe the iframe to observe
17 | * @returns {() => void}
18 | * @example
19 | *
20 | * // grab a reference to an existing iframe
21 | * const iframe = document.getElementById('my-embed');
22 | *
23 | * // returns a `unobserve()` function if you need to stop listening
24 | * const unobserve = observeIframe(iframe);
25 | *
26 | * // later, if you need to disconnect from the iframe
27 | * unobserve();
28 | */
29 | export function observeIframe(iframe) {
30 | /**
31 | * @private
32 | * @param {MessageEvent} event
33 | * @returns {void}
34 | */
35 | function processMessage(event) {
36 | // this message isn't from our created frame, stop here
37 | if (event.source !== iframe.contentWindow) {
38 | return;
39 | }
40 |
41 | const { data } = event;
42 |
43 | // if the sentinel and type matches, update our height
44 | if (data.sentinel === AMP_SENTINEL && data.type === EMBED_SIZE) {
45 | iframe.setAttribute(HEIGHT, data.height);
46 | } else if (typeof data === 'string' && data.slice(0, 3) === PYM_SENTINEL) {
47 | const [, , type, height] = data.split(PYM_REGEX);
48 |
49 | if (type === HEIGHT) {
50 | iframe.setAttribute(HEIGHT, height);
51 | }
52 | }
53 | }
54 |
55 | window.addEventListener('message', processMessage, false);
56 |
57 | // tell the iframe we've connected
58 | if (iframe.contentWindow) {
59 | iframe.contentWindow.postMessage(
60 | { sentinel: AMP_SENTINEL, type: INITIAL_MESSAGE },
61 | '*',
62 | );
63 | }
64 |
65 | return function unobserve() {
66 | window.removeEventListener('message', processMessage, false);
67 | };
68 | }
69 |
70 | /**
71 | * @typedef {object} FramerOptions
72 | * @property {string | null} [src] the URL to set as the `src` of the iframe
73 | * @property {Record} [attributes] any attributes to add to the iframe itself
74 | */
75 |
76 | /**
77 | * The Framer function to be called in the host page. A wrapper around
78 | * interactions with a created iframe. Returns a `remove()` function for
79 | * disconnecting the event listener and removing the iframe from the DOM.
80 | *
81 | * @param {Element} container the containing DOM element for the iframe
82 | * @param {FramerOptions} options
83 | * @returns {{remove: () => void}}
84 | */
85 | export function Framer(container, { attributes, src } = {}) {
86 | // create the iframe
87 | const iframe = document.createElement('iframe');
88 |
89 | // hook up our observer
90 | const unobserve = observeIframe(iframe);
91 |
92 | // set its source if provided
93 | if (src) {
94 | iframe.setAttribute('src', src);
95 | }
96 |
97 | // set some smart default attributes
98 | iframe.setAttribute('width', '100%');
99 | iframe.setAttribute('scrolling', 'no');
100 | iframe.setAttribute('marginheight', '0');
101 | iframe.setAttribute('frameborder', '0');
102 |
103 | if (attributes) {
104 | // apply any supplied attributes
105 | for (let key in attributes) {
106 | const value = attributes[key];
107 |
108 | iframe.setAttribute(key, value);
109 | }
110 | }
111 |
112 | // append it to the container
113 | container.appendChild(iframe);
114 |
115 | return {
116 | remove() {
117 | unobserve();
118 | container.removeChild(iframe);
119 | },
120 | };
121 | }
122 |
--------------------------------------------------------------------------------
/src/frames.js:
--------------------------------------------------------------------------------
1 | // internal
2 | import { AMP_SENTINEL, EMBED_SIZE, INITIAL_MESSAGE } from './constants.js';
3 |
4 | /**
5 | * Gets the height of the current document's body. Uses offsetHeight to ensure
6 | * that margins are accounted for.
7 | *
8 | * @private
9 | * @returns {number}
10 | */
11 | function getDocumentHeight() {
12 | return document.documentElement.offsetHeight;
13 | }
14 |
15 | /**
16 | * Sends the current document's height or provided value to the host window
17 | * using postMessage.
18 | *
19 | * @param {number} [height] The height to pass to the host page, is determined automatically if not passed
20 | * @returns {void}
21 | * @example
22 | *
23 | * // Uses the document's height to tell the host page
24 | * sendFrameHeight();
25 | *
26 | * // Pass a height you've determined in another way
27 | * sendFrameHeight(500);
28 | *
29 | */
30 | function sendFrameHeight(height = getDocumentHeight()) {
31 | window.parent.postMessage(
32 | { sentinel: AMP_SENTINEL, type: EMBED_SIZE, height },
33 | '*',
34 | );
35 | }
36 |
37 | /**
38 | * Sets up an event listener for the load event that sends the new frame
39 | * height to the host. Automatically removes itself once fired.
40 | *
41 | * @returns {void}
42 | * @example
43 | *
44 | * // once the frame's load event is fired, tell the host page its new height
45 | * sendHeightOnLoad();
46 | */
47 | function sendHeightOnLoad() {
48 | window.addEventListener(
49 | 'load',
50 | function load() {
51 | sendFrameHeight();
52 |
53 | window.removeEventListener('load', load, false);
54 | },
55 | false,
56 | );
57 | }
58 |
59 | /**
60 | * Sets up an event listener for the resize event that sends the new frame
61 | * height to the host.
62 | *
63 | * @returns {void}
64 | * @example
65 | *
66 | * // every time the frame is resized, tell the host page what its new height is
67 | * sendHeightOnResize();
68 | */
69 | function sendHeightOnResize() {
70 | window.addEventListener('resize', () => sendFrameHeight(), false);
71 | }
72 |
73 | /**
74 | * Sets up an event listener for a message from the parent window that it is
75 | * now listening for messages from this iframe, and tells it the iframe's height
76 | * at that time. This makes it possible to delay observing an iframe (e.g. when
77 | * lazy loading) but trust the parent will get the current height ASAP.
78 | *
79 | * @returns {void}
80 | * @example
81 | *
82 | * // as soon as a Framer connects, tell the host page what the current height is
83 | * sendHeightOnFramerInit();
84 | */
85 | function sendHeightOnFramerInit() {
86 | window.addEventListener(
87 | 'message',
88 | function onInit(event) {
89 | const { data } = event;
90 |
91 | // if the sentinel and type matches, update our height
92 | if (data.sentinel === AMP_SENTINEL && data.type === INITIAL_MESSAGE) {
93 | // don't need it anymore
94 | window.removeEventListener('message', onInit, false);
95 |
96 | // send the current frame height
97 | sendFrameHeight();
98 | }
99 | },
100 | false,
101 | );
102 | }
103 |
104 | /**
105 | * Sends height updates to the host page on an interval.
106 | *
107 | * @param {number} [delay] How long to set the interval
108 | * @returns {void}
109 | * @example
110 | *
111 | * // will call sendFrameHeight every 300ms
112 | * sendHeightOnPoll();
113 | *
114 | * // will call sendFrameHeight every 150ms
115 | * sendHeightOnPoll(150);
116 | */
117 | function sendHeightOnPoll(delay = 300) {
118 | setInterval(sendFrameHeight, delay);
119 | }
120 |
121 | /**
122 | * A helper for running the standard functions for setting up a frame.
123 | *
124 | * Automatically calls an `sendFrameHeight`, `sendHeightOnLoad`, `sendHeightOnResize`
125 | * and `sendHeightOnFramerInit`.
126 | *
127 | * @returns {void}
128 | * @example
129 | *
130 | * initFrame();
131 | */
132 | function initFrame() {
133 | sendFrameHeight();
134 | sendHeightOnLoad();
135 | sendHeightOnResize();
136 | sendHeightOnFramerInit();
137 | }
138 |
139 | /**
140 | * Calls `initFrame` to setup a frame, then initializes a poller to continue to update on an interval.
141 | *
142 | * @param {number} [delay] An optional custom delay to pass to sendHeightOnPoll
143 | * @returns {void}
144 | * @example
145 | *
146 | * // calls initFrame, then calls sendHeightOnPoll
147 | * initFrameAndPoll();
148 | */
149 | function initFrameAndPoll(delay) {
150 | initFrame();
151 | sendHeightOnPoll(delay);
152 | }
153 |
154 | export {
155 | initFrame,
156 | initFrameAndPoll,
157 | sendFrameHeight,
158 | sendHeightOnLoad,
159 | sendHeightOnPoll,
160 | sendHeightOnResize,
161 | sendHeightOnFramerInit,
162 | };
163 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // for the host or parent page
2 | export { Framer, observeIframe } from './framer.js';
3 | export { autoInitFrames } from './auto.js';
4 |
5 | // for the frame or child page
6 | export {
7 | initFrame,
8 | initFrameAndPoll,
9 | sendFrameHeight,
10 | sendHeightOnLoad,
11 | sendHeightOnPoll,
12 | sendHeightOnResize,
13 | sendHeightOnFramerInit,
14 | } from './frames.js';
15 |
--------------------------------------------------------------------------------
/tests/public/auto-embed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
13 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/tests/public/auto.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/tests/public/embed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/tests/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/public/init-auto.js:
--------------------------------------------------------------------------------
1 | import { autoInitFrames } from '/index.js';
2 |
3 | autoInitFrames();
4 |
--------------------------------------------------------------------------------
/tests/public/init-framer-pym.js:
--------------------------------------------------------------------------------
1 | import { Framer } from '/index.js';
2 |
3 | window.framer = Framer(document.getElementById('iframe-container'), {
4 | src: '/pym-embed.html',
5 | });
6 |
--------------------------------------------------------------------------------
/tests/public/init-framer.js:
--------------------------------------------------------------------------------
1 | import { Framer } from '/index.js';
2 |
3 | window.framer = Framer(document.getElementById('iframe-container'), {
4 | src: '/send-frame-height-controller.html',
5 | attributes: { sandbox: 'allow-scripts allow-same-origin' },
6 | });
7 |
--------------------------------------------------------------------------------
/tests/public/observe-iframe.js:
--------------------------------------------------------------------------------
1 | import { observeIframe } from '/index.js';
2 |
3 | window.unobserve = observeIframe(document.querySelector('iframe'));
4 |
--------------------------------------------------------------------------------
/tests/public/pym-embed.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Pym Embed
8 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/public/pym.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/tests/public/pym.js:
--------------------------------------------------------------------------------
1 | /*! pym.js - v1.3.2 - 2018-02-13 */
2 | /*
3 | * Pym.js is library that resizes an iframe based on the width of the parent and the resulting height of the child.
4 | * Check out the docs at http://blog.apps.npr.org/pym.js/ or the readme at README.md for usage.
5 | */
6 |
7 | /** @module pym */
8 | (function (factory) {
9 | if (typeof define === 'function' && define.amd) {
10 | define(factory);
11 | } else if (typeof module !== 'undefined' && module.exports) {
12 | module.exports = factory();
13 | } else {
14 | window.pym = factory.call(this);
15 | }
16 | })(function () {
17 | var MESSAGE_DELIMITER = 'xPYMx';
18 |
19 | var lib = {};
20 |
21 | /**
22 | * Create and dispatch a custom pym event
23 | *
24 | * @method _raiseCustomEvent
25 | * @inner
26 | *
27 | * @param {String} eventName
28 | */
29 | var _raiseCustomEvent = function (eventName) {
30 | var event = document.createEvent('Event');
31 | event.initEvent('pym:' + eventName, true, true);
32 | document.dispatchEvent(event);
33 | };
34 |
35 | /**
36 | * Generic function for parsing URL params.
37 | * Via http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
38 | *
39 | * @method _getParameterByName
40 | * @inner
41 | *
42 | * @param {String} name The name of the paramter to get from the URL.
43 | */
44 | var _getParameterByName = function (name) {
45 | var regex = new RegExp(
46 | '[\\?&]' +
47 | name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]') +
48 | '=([^]*)',
49 | );
50 | var results = regex.exec(location.search);
51 |
52 | if (results === null) {
53 | return '';
54 | }
55 |
56 | return decodeURIComponent(results[1].replace(/\+/g, ' '));
57 | };
58 |
59 | /**
60 | * Check the message to make sure it comes from an acceptable xdomain.
61 | * Defaults to '*' but can be overriden in config.
62 | *
63 | * @method _isSafeMessage
64 | * @inner
65 | *
66 | * @param {Event} e The message event.
67 | * @param {Object} settings Configuration.
68 | */
69 | var _isSafeMessage = function (e, settings) {
70 | if (settings.xdomain !== '*') {
71 | // If origin doesn't match our xdomain, return.
72 | if (!e.origin.match(new RegExp(settings.xdomain + '$'))) {
73 | return;
74 | }
75 | }
76 |
77 | // Ignore events that do not carry string data #151
78 | if (typeof e.data !== 'string') {
79 | return;
80 | }
81 |
82 | return true;
83 | };
84 |
85 | var _isSafeUrl = function (url) {
86 | // Adapted from angular 2 url sanitizer
87 | var SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp):|[^&:/?#]*(?:[/?#]|$))/gi;
88 | if (!url.match(SAFE_URL_PATTERN)) {
89 | return;
90 | }
91 |
92 | return true;
93 | };
94 |
95 | /**
96 | * Construct a message to send between frames.
97 | *
98 | * NB: We use string-building here because JSON message passing is
99 | * not supported in all browsers.
100 | *
101 | * @method _makeMessage
102 | * @inner
103 | *
104 | * @param {String} id The unique id of the message recipient.
105 | * @param {String} messageType The type of message to send.
106 | * @param {String} message The message to send.
107 | */
108 | var _makeMessage = function (id, messageType, message) {
109 | var bits = ['pym', id, messageType, message];
110 |
111 | return bits.join(MESSAGE_DELIMITER);
112 | };
113 |
114 | /**
115 | * Construct a regex to validate and parse messages.
116 | *
117 | * @method _makeMessageRegex
118 | * @inner
119 | *
120 | * @param {String} id The unique id of the message recipient.
121 | */
122 | var _makeMessageRegex = function (id) {
123 | var bits = ['pym', id, '(\\S+)', '(.*)'];
124 |
125 | return new RegExp('^' + bits.join(MESSAGE_DELIMITER) + '$');
126 | };
127 |
128 | /**
129 | * Underscore implementation of getNow
130 | *
131 | * @method _getNow
132 | * @inner
133 | *
134 | */
135 | var _getNow =
136 | Date.now ||
137 | function () {
138 | return new Date().getTime();
139 | };
140 |
141 | /**
142 | * Underscore implementation of throttle
143 | *
144 | * @method _throttle
145 | * @inner
146 | *
147 | * @param {function} func Throttled function
148 | * @param {number} wait Throttle wait time
149 | * @param {object} options Throttle settings
150 | */
151 |
152 | var _throttle = function (func, wait, options) {
153 | var context, args, result;
154 | var timeout = null;
155 | var previous = 0;
156 | if (!options) {
157 | options = {};
158 | }
159 | var later = function () {
160 | previous = options.leading === false ? 0 : _getNow();
161 | timeout = null;
162 | result = func.apply(context, args);
163 | if (!timeout) {
164 | context = args = null;
165 | }
166 | };
167 | return function () {
168 | var now = _getNow();
169 | if (!previous && options.leading === false) {
170 | previous = now;
171 | }
172 | var remaining = wait - (now - previous);
173 | context = this;
174 | args = arguments;
175 | if (remaining <= 0 || remaining > wait) {
176 | if (timeout) {
177 | clearTimeout(timeout);
178 | timeout = null;
179 | }
180 | previous = now;
181 | result = func.apply(context, args);
182 | if (!timeout) {
183 | context = args = null;
184 | }
185 | } else if (!timeout && options.trailing !== false) {
186 | timeout = setTimeout(later, remaining);
187 | }
188 | return result;
189 | };
190 | };
191 |
192 | /**
193 | * Clean autoInit Instances: those that point to contentless iframes
194 | * @method _cleanAutoInitInstances
195 | * @inner
196 | */
197 | var _cleanAutoInitInstances = function () {
198 | var length = lib.autoInitInstances.length;
199 |
200 | // Loop backwards to avoid index issues
201 | for (var idx = length - 1; idx >= 0; idx--) {
202 | var instance = lib.autoInitInstances[idx];
203 | // If instance has been removed or is contentless then remove it
204 | if (
205 | instance.el.getElementsByTagName('iframe').length &&
206 | instance.el.getElementsByTagName('iframe')[0].contentWindow
207 | ) {
208 | continue;
209 | } else {
210 | // Remove the reference to the removed or orphan instance
211 | lib.autoInitInstances.splice(idx, 1);
212 | }
213 | }
214 | };
215 |
216 | /**
217 | * Store auto initialized Pym instances for further reference
218 | * @name module:pym#autoInitInstances
219 | * @type Array
220 | * @default []
221 | */
222 | lib.autoInitInstances = [];
223 |
224 | /**
225 | * Initialize Pym for elements on page that have data-pym attributes.
226 | * Expose autoinit in case we need to call it from the outside
227 | * @instance
228 | * @method autoInit
229 | * @param {Boolean} doNotRaiseEvents flag to avoid sending custom events
230 | */
231 | lib.autoInit = function (doNotRaiseEvents) {
232 | var elements = document.querySelectorAll(
233 | '[data-pym-src]:not([data-pym-auto-initialized])',
234 | );
235 | var length = elements.length;
236 |
237 | // Clean stored instances in case needed
238 | _cleanAutoInitInstances();
239 | for (var idx = 0; idx < length; ++idx) {
240 | var element = elements[idx];
241 | /*
242 | * Mark automatically-initialized elements so they are not
243 | * re-initialized if the user includes pym.js more than once in the
244 | * same document.
245 | */
246 | element.setAttribute('data-pym-auto-initialized', '');
247 |
248 | // Ensure elements have an id
249 | if (element.id === '') {
250 | element.id =
251 | 'pym-' + idx + '-' + Math.random().toString(36).substr(2, 5);
252 | }
253 |
254 | var src = element.getAttribute('data-pym-src');
255 |
256 | // List of data attributes to configure the component
257 | // structure: {'attribute name': 'type'}
258 | var settings = {
259 | xdomain: 'string',
260 | title: 'string',
261 | name: 'string',
262 | id: 'string',
263 | sandbox: 'string',
264 | allowfullscreen: 'boolean',
265 | parenturlparam: 'string',
266 | parenturlvalue: 'string',
267 | optionalparams: 'boolean',
268 | trackscroll: 'boolean',
269 | scrollwait: 'number',
270 | };
271 |
272 | var config = {};
273 |
274 | for (var attribute in settings) {
275 | // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#Notes
276 | if (element.getAttribute('data-pym-' + attribute) !== null) {
277 | switch (settings[attribute]) {
278 | case 'boolean':
279 | config[attribute] = !(
280 | element.getAttribute('data-pym-' + attribute) === 'false'
281 | ); // jshint ignore:line
282 | break;
283 | case 'string':
284 | config[attribute] = element.getAttribute('data-pym-' + attribute);
285 | break;
286 | case 'number':
287 | var n = Number(element.getAttribute('data-pym-' + attribute));
288 | if (!isNaN(n)) {
289 | config[attribute] = n;
290 | }
291 | break;
292 | default:
293 | console.err('unrecognized attribute type');
294 | }
295 | }
296 | }
297 |
298 | // Store references to autoinitialized pym instances
299 | var parent = new lib.Parent(element.id, src, config);
300 | lib.autoInitInstances.push(parent);
301 | }
302 |
303 | // Fire customEvent
304 | if (!doNotRaiseEvents) {
305 | _raiseCustomEvent('pym-initialized');
306 | }
307 | // Return stored autoinitalized pym instances
308 | return lib.autoInitInstances;
309 | };
310 |
311 | /**
312 | * The Parent half of a response iframe.
313 | *
314 | * @memberof module:pym
315 | * @class Parent
316 | * @param {String} id The id of the div into which the iframe will be rendered. sets {@link module:pym.Parent~id}
317 | * @param {String} url The url of the iframe source. sets {@link module:pym.Parent~url}
318 | * @param {Object} [config] Configuration for the parent instance. sets {@link module:pym.Parent~settings}
319 | * @param {string} [config.xdomain='*'] - xdomain to validate messages received
320 | * @param {string} [config.title] - if passed it will be assigned to the iframe title attribute
321 | * @param {string} [config.name] - if passed it will be assigned to the iframe name attribute
322 | * @param {string} [config.id] - if passed it will be assigned to the iframe id attribute
323 | * @param {boolean} [config.allowfullscreen] - if passed and different than false it will be assigned to the iframe allowfullscreen attribute
324 | * @param {string} [config.sandbox] - if passed it will be assigned to the iframe sandbox attribute (we do not validate the syntax so be careful!!)
325 | * @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name passed to the iframe src
326 | * @param {string} [config.parenturlvalue] - if passed it will be override the default parentUrl query string parameter value passed to the iframe src
327 | * @param {string} [config.optionalparams] - if passed and different than false it will strip the querystring params parentUrl and parentTitle passed to the iframe src
328 | * @param {boolean} [config.trackscroll] - if passed it will activate scroll tracking on the parent
329 | * @param {number} [config.scrollwait] - if passed it will set the throttle wait in order to fire scroll messaging. Defaults to 100 ms.
330 | * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe iFrame}
331 | */
332 | lib.Parent = function (id, url, config) {
333 | /**
334 | * The id of the container element
335 | *
336 | * @memberof module:pym.Parent
337 | * @member {string} id
338 | * @inner
339 | */
340 | this.id = id;
341 | /**
342 | * The url that will be set as the iframe's src
343 | *
344 | * @memberof module:pym.Parent
345 | * @member {String} url
346 | * @inner
347 | */
348 | this.url = url;
349 |
350 | /**
351 | * The container DOM object
352 | *
353 | * @memberof module:pym.Parent
354 | * @member {HTMLElement} el
355 | * @inner
356 | */
357 | this.el = document.getElementById(id);
358 | /**
359 | * The contained child iframe
360 | *
361 | * @memberof module:pym.Parent
362 | * @member {HTMLElement} iframe
363 | * @inner
364 | * @default null
365 | */
366 | this.iframe = null;
367 | /**
368 | * The parent instance settings, updated by the values passed in the config object
369 | *
370 | * @memberof module:pym.Parent
371 | * @member {Object} settings
372 | * @inner
373 | */
374 | this.settings = {
375 | xdomain: '*',
376 | optionalparams: true,
377 | parenturlparam: 'parentUrl',
378 | parenturlvalue: window.location.href,
379 | trackscroll: false,
380 | scrollwait: 100,
381 | };
382 | /**
383 | * RegularExpression to validate the received messages
384 | *
385 | * @memberof module:pym.Parent
386 | * @member {String} messageRegex
387 | * @inner
388 | */
389 | this.messageRegex = _makeMessageRegex(this.id);
390 | /**
391 | * Stores the registered messageHandlers for each messageType
392 | *
393 | * @memberof module:pym.Parent
394 | * @member {Object} messageHandlers
395 | * @inner
396 | */
397 | this.messageHandlers = {};
398 |
399 | // ensure a config object
400 | config = config || {};
401 |
402 | /**
403 | * Construct the iframe.
404 | *
405 | * @memberof module:pym.Parent
406 | * @method _constructIframe
407 | * @inner
408 | */
409 | this._constructIframe = function () {
410 | // Calculate the width of this element.
411 | var width = this.el.offsetWidth.toString();
412 |
413 | // Create an iframe element attached to the document.
414 | this.iframe = document.createElement('iframe');
415 |
416 | // Save fragment id
417 | var hash = '';
418 | var hashIndex = this.url.indexOf('#');
419 |
420 | if (hashIndex > -1) {
421 | hash = this.url.substring(hashIndex, this.url.length);
422 | this.url = this.url.substring(0, hashIndex);
423 | }
424 |
425 | // If the URL contains querystring bits, use them.
426 | // Otherwise, just create a set of valid params.
427 | if (this.url.indexOf('?') < 0) {
428 | this.url += '?';
429 | } else {
430 | this.url += '&';
431 | }
432 |
433 | // Append the initial width as a querystring parameter
434 | // and optional params if configured to do so
435 | this.iframe.src =
436 | this.url + 'initialWidth=' + width + '&childId=' + this.id;
437 |
438 | if (this.settings.optionalparams) {
439 | this.iframe.src += '&parentTitle=' + encodeURIComponent(document.title);
440 | this.iframe.src +=
441 | '&' +
442 | this.settings.parenturlparam +
443 | '=' +
444 | encodeURIComponent(this.settings.parenturlvalue);
445 | }
446 | this.iframe.src += hash;
447 |
448 | // Set some attributes to this proto-iframe.
449 | this.iframe.setAttribute('width', '100%');
450 | this.iframe.setAttribute('scrolling', 'no');
451 | this.iframe.setAttribute('marginheight', '0');
452 | this.iframe.setAttribute('frameborder', '0');
453 |
454 | if (this.settings.title) {
455 | this.iframe.setAttribute('title', this.settings.title);
456 | }
457 |
458 | if (
459 | this.settings.allowfullscreen !== undefined &&
460 | this.settings.allowfullscreen !== false
461 | ) {
462 | this.iframe.setAttribute('allowfullscreen', '');
463 | }
464 |
465 | if (
466 | this.settings.sandbox !== undefined &&
467 | typeof this.settings.sandbox === 'string'
468 | ) {
469 | this.iframe.setAttribute('sandbox', this.settings.sandbox);
470 | }
471 |
472 | if (this.settings.id) {
473 | if (!document.getElementById(this.settings.id)) {
474 | this.iframe.setAttribute('id', this.settings.id);
475 | }
476 | }
477 |
478 | if (this.settings.name) {
479 | this.iframe.setAttribute('name', this.settings.name);
480 | }
481 |
482 | // Replace the child content if needed
483 | // (some CMSs might strip out empty elements)
484 | while (this.el.firstChild) {
485 | this.el.removeChild(this.el.firstChild);
486 | }
487 | // Append the iframe to our element.
488 | this.el.appendChild(this.iframe);
489 |
490 | // Add an event listener that will handle redrawing the child on resize.
491 | window.addEventListener('resize', this._onResize);
492 |
493 | // Add an event listener that will send the child the viewport.
494 | if (this.settings.trackscroll) {
495 | window.addEventListener('scroll', this._throttleOnScroll);
496 | }
497 | };
498 |
499 | /**
500 | * Send width on resize.
501 | *
502 | * @memberof module:pym.Parent
503 | * @method _onResize
504 | * @inner
505 | */
506 | this._onResize = function () {
507 | this.sendWidth();
508 | if (this.settings.trackscroll) {
509 | this.sendViewportAndIFramePosition();
510 | }
511 | }.bind(this);
512 |
513 | /**
514 | * Send viewport and iframe info on scroll.
515 | *
516 | * @memberof module:pym.Parent
517 | * @method _onScroll
518 | * @inner
519 | */
520 | this._onScroll = function () {
521 | this.sendViewportAndIFramePosition();
522 | }.bind(this);
523 |
524 | /**
525 | * Fire all event handlers for a given message type.
526 | *
527 | * @memberof module:pym.Parent
528 | * @method _fire
529 | * @inner
530 | *
531 | * @param {String} messageType The type of message.
532 | * @param {String} message The message data.
533 | */
534 | this._fire = function (messageType, message) {
535 | if (messageType in this.messageHandlers) {
536 | for (var i = 0; i < this.messageHandlers[messageType].length; i++) {
537 | this.messageHandlers[messageType][i].call(this, message);
538 | }
539 | }
540 | };
541 |
542 | /**
543 | * Remove this parent from the page and unbind it's event handlers.
544 | *
545 | * @memberof module:pym.Parent
546 | * @method remove
547 | * @instance
548 | */
549 | this.remove = function () {
550 | window.removeEventListener('message', this._processMessage);
551 | window.removeEventListener('resize', this._onResize);
552 |
553 | this.el.removeChild(this.iframe);
554 | // _cleanAutoInitInstances in case this parent was autoInitialized
555 | _cleanAutoInitInstances();
556 | };
557 |
558 | /**
559 | * Process a new message from the child.
560 | *
561 | * @memberof module:pym.Parent
562 | * @method _processMessage
563 | * @inner
564 | *
565 | * @param {Event} e A message event.
566 | */
567 | this._processMessage = function (e) {
568 | // First, punt if this isn't from an acceptable xdomain.
569 | if (!_isSafeMessage(e, this.settings)) {
570 | return;
571 | }
572 |
573 | // Discard object messages, we only care about strings
574 | if (typeof e.data !== 'string') {
575 | return;
576 | }
577 |
578 | // Grab the message from the child and parse it.
579 | var match = e.data.match(this.messageRegex);
580 |
581 | // If there's no match or too many matches in the message, punt.
582 | if (!match || match.length !== 3) {
583 | return false;
584 | }
585 |
586 | var messageType = match[1];
587 | var message = match[2];
588 |
589 | this._fire(messageType, message);
590 | }.bind(this);
591 |
592 | /**
593 | * Resize iframe in response to new height message from child.
594 | *
595 | * @memberof module:pym.Parent
596 | * @method _onHeightMessage
597 | * @inner
598 | *
599 | * @param {String} message The new height.
600 | */
601 | this._onHeightMessage = function (message) {
602 | /*
603 | * Handle parent height message from child.
604 | */
605 | var height = parseInt(message);
606 |
607 | this.iframe.setAttribute('height', height + 'px');
608 | };
609 |
610 | /**
611 | * Navigate parent to a new url.
612 | *
613 | * @memberof module:pym.Parent
614 | * @method _onNavigateToMessage
615 | * @inner
616 | *
617 | * @param {String} message The url to navigate to.
618 | */
619 | this._onNavigateToMessage = function (message) {
620 | /*
621 | * Handle parent scroll message from child.
622 | */
623 | if (!_isSafeUrl(message)) {
624 | return;
625 | }
626 | document.location.href = message;
627 | };
628 |
629 | /**
630 | * Scroll parent to a given child position.
631 | *
632 | * @memberof module:pym.Parent
633 | * @method _onScrollToChildPosMessage
634 | * @inner
635 | *
636 | * @param {String} message The offset inside the child page.
637 | */
638 | this._onScrollToChildPosMessage = function (message) {
639 | // Get the child container position using getBoundingClientRect + pageYOffset
640 | // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
641 | var iframePos =
642 | document.getElementById(this.id).getBoundingClientRect().top +
643 | window.pageYOffset;
644 |
645 | var totalOffset = iframePos + parseInt(message);
646 | window.scrollTo(0, totalOffset);
647 | };
648 |
649 | /**
650 | * Bind a callback to a given messageType from the child.
651 | *
652 | * Reserved message names are: "height", "scrollTo" and "navigateTo".
653 | *
654 | * @memberof module:pym.Parent
655 | * @method onMessage
656 | * @instance
657 | *
658 | * @param {String} messageType The type of message being listened for.
659 | * @param {module:pym.Parent~onMessageCallback} callback The callback to invoke when a message of the given type is received.
660 | */
661 | this.onMessage = function (messageType, callback) {
662 | if (!(messageType in this.messageHandlers)) {
663 | this.messageHandlers[messageType] = [];
664 | }
665 |
666 | this.messageHandlers[messageType].push(callback);
667 | };
668 |
669 | /**
670 | * @callback module:pym.Parent~onMessageCallback
671 | * @param {String} message The message data.
672 | */
673 |
674 | /**
675 | * Send a message to the the child.
676 | *
677 | * @memberof module:pym.Parent
678 | * @method sendMessage
679 | * @instance
680 | *
681 | * @param {String} messageType The type of message to send.
682 | * @param {String} message The message data to send.
683 | */
684 | this.sendMessage = function (messageType, message) {
685 | // When used alongside with pjax some references are lost
686 | if (this.el.getElementsByTagName('iframe').length) {
687 | if (this.el.getElementsByTagName('iframe')[0].contentWindow) {
688 | this.el
689 | .getElementsByTagName('iframe')[0]
690 | .contentWindow.postMessage(
691 | _makeMessage(this.id, messageType, message),
692 | '*',
693 | );
694 | } else {
695 | // Contentless child detected remove listeners and iframe
696 | this.remove();
697 | }
698 | }
699 | };
700 |
701 | /**
702 | * Transmit the current iframe width to the child.
703 | *
704 | * You shouldn't need to call this directly.
705 | *
706 | * @memberof module:pym.Parent
707 | * @method sendWidth
708 | * @instance
709 | */
710 | this.sendWidth = function () {
711 | var width = this.el.offsetWidth.toString();
712 | this.sendMessage('width', width);
713 | };
714 |
715 | /**
716 | * Transmit the current viewport and iframe position to the child.
717 | * Sends viewport width, viewport height
718 | * and iframe bounding rect top-left-bottom-right
719 | * all separated by spaces
720 | *
721 | * You shouldn't need to call this directly.
722 | *
723 | * @memberof module:pym.Parent
724 | * @method sendViewportAndIFramePosition
725 | * @instance
726 | */
727 | this.sendViewportAndIFramePosition = function () {
728 | var iframeRect = this.iframe.getBoundingClientRect();
729 | var vWidth = window.innerWidth || document.documentElement.clientWidth;
730 | var vHeight = window.innerHeight || document.documentElement.clientHeight;
731 | var payload = vWidth + ' ' + vHeight;
732 | payload += ' ' + iframeRect.top + ' ' + iframeRect.left;
733 | payload += ' ' + iframeRect.bottom + ' ' + iframeRect.right;
734 | this.sendMessage('viewport-iframe-position', payload);
735 | };
736 |
737 | // Add any overrides to settings coming from config.
738 | for (var key in config) {
739 | this.settings[key] = config[key];
740 | }
741 |
742 | /**
743 | * Throttled scroll function.
744 | *
745 | * @memberof module:pym.Parent
746 | * @method _throttleOnScroll
747 | * @inner
748 | */
749 | this._throttleOnScroll = _throttle(
750 | this._onScroll.bind(this),
751 | this.settings.scrollwait,
752 | );
753 |
754 | // Bind required message handlers
755 | this.onMessage('height', this._onHeightMessage);
756 | this.onMessage('navigateTo', this._onNavigateToMessage);
757 | this.onMessage('scrollToChildPos', this._onScrollToChildPosMessage);
758 | this.onMessage('parentPositionInfo', this.sendViewportAndIFramePosition);
759 |
760 | // Add a listener for processing messages from the child.
761 | window.addEventListener('message', this._processMessage, false);
762 |
763 | // Construct the iframe in the container element.
764 | this._constructIframe();
765 |
766 | return this;
767 | };
768 |
769 | /**
770 | * The Child half of a responsive iframe.
771 | *
772 | * @memberof module:pym
773 | * @class Child
774 | * @param {Object} [config] Configuration for the child instance. sets {@link module:pym.Child~settings}
775 | * @param {function} [config.renderCallback=null] Callback invoked after receiving a resize event from the parent, sets {@link module:pym.Child#settings.renderCallback}
776 | * @param {string} [config.xdomain='*'] - xdomain to validate messages received
777 | * @param {number} [config.polling=0] - polling frequency in milliseconds to send height to parent
778 | * @param {number} [config.id] - parent container id used when navigating the child iframe to a new page but we want to keep it responsive.
779 | * @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name expected on the iframe src
780 | */
781 | lib.Child = function (config) {
782 | /**
783 | * The initial width of the parent page
784 | *
785 | * @memberof module:pym.Child
786 | * @member {string} parentWidth
787 | * @inner
788 | */
789 | this.parentWidth = null;
790 | /**
791 | * The id of the parent container
792 | *
793 | * @memberof module:pym.Child
794 | * @member {String} id
795 | * @inner
796 | */
797 | this.id = null;
798 | /**
799 | * The title of the parent page from document.title.
800 | *
801 | * @memberof module:pym.Child
802 | * @member {String} parentTitle
803 | * @inner
804 | */
805 | this.parentTitle = null;
806 | /**
807 | * The URL of the parent page from window.location.href.
808 | *
809 | * @memberof module:pym.Child
810 | * @member {String} parentUrl
811 | * @inner
812 | */
813 | this.parentUrl = null;
814 | /**
815 | * The settings for the child instance. Can be overriden by passing a config object to the child constructor
816 | * i.e.: var pymChild = new pym.Child({renderCallback: render, xdomain: "\\*\.npr\.org"})
817 | *
818 | * @memberof module:pym.Child.settings
819 | * @member {Object} settings - default settings for the child instance
820 | * @inner
821 | */
822 | this.settings = {
823 | renderCallback: null,
824 | xdomain: '*',
825 | polling: 0,
826 | parenturlparam: 'parentUrl',
827 | };
828 |
829 | /**
830 | * The timerId in order to be able to stop when polling is enabled
831 | *
832 | * @memberof module:pym.Child
833 | * @member {String} timerId
834 | * @inner
835 | */
836 | this.timerId = null;
837 | /**
838 | * RegularExpression to validate the received messages
839 | *
840 | * @memberof module:pym.Child
841 | * @member {String} messageRegex
842 | * @inner
843 | */
844 | this.messageRegex = null;
845 | /**
846 | * Stores the registered messageHandlers for each messageType
847 | *
848 | * @memberof module:pym.Child
849 | * @member {Object} messageHandlers
850 | * @inner
851 | */
852 | this.messageHandlers = {};
853 |
854 | // Ensure a config object
855 | config = config || {};
856 |
857 | /**
858 | * Bind a callback to a given messageType from the child.
859 | *
860 | * Reserved message names are: "width".
861 | *
862 | * @memberof module:pym.Child
863 | * @method onMessage
864 | * @instance
865 | *
866 | * @param {String} messageType The type of message being listened for.
867 | * @param {module:pym.Child~onMessageCallback} callback The callback to invoke when a message of the given type is received.
868 | */
869 | this.onMessage = function (messageType, callback) {
870 | if (!(messageType in this.messageHandlers)) {
871 | this.messageHandlers[messageType] = [];
872 | }
873 |
874 | this.messageHandlers[messageType].push(callback);
875 | };
876 |
877 | /**
878 | * @callback module:pym.Child~onMessageCallback
879 | * @param {String} message The message data.
880 | */
881 |
882 | /**
883 | * Fire all event handlers for a given message type.
884 | *
885 | * @memberof module:pym.Child
886 | * @method _fire
887 | * @inner
888 | *
889 | * @param {String} messageType The type of message.
890 | * @param {String} message The message data.
891 | */
892 | this._fire = function (messageType, message) {
893 | /*
894 | * Fire all event handlers for a given message type.
895 | */
896 | if (messageType in this.messageHandlers) {
897 | for (var i = 0; i < this.messageHandlers[messageType].length; i++) {
898 | this.messageHandlers[messageType][i].call(this, message);
899 | }
900 | }
901 | };
902 |
903 | /**
904 | * Process a new message from the parent.
905 | *
906 | * @memberof module:pym.Child
907 | * @method _processMessage
908 | * @inner
909 | *
910 | * @param {Event} e A message event.
911 | */
912 | this._processMessage = function (e) {
913 | /*
914 | * Process a new message from parent frame.
915 | */
916 | // First, punt if this isn't from an acceptable xdomain.
917 | if (!_isSafeMessage(e, this.settings)) {
918 | return;
919 | }
920 |
921 | // Discard object messages, we only care about strings
922 | if (typeof e.data !== 'string') {
923 | return;
924 | }
925 |
926 | // Get the message from the parent.
927 | var match = e.data.match(this.messageRegex);
928 |
929 | // If there's no match or it's a bad format, punt.
930 | if (!match || match.length !== 3) {
931 | return;
932 | }
933 |
934 | var messageType = match[1];
935 | var message = match[2];
936 |
937 | this._fire(messageType, message);
938 | }.bind(this);
939 |
940 | /**
941 | * Resize iframe in response to new width message from parent.
942 | *
943 | * @memberof module:pym.Child
944 | * @method _onWidthMessage
945 | * @inner
946 | *
947 | * @param {String} message The new width.
948 | */
949 | this._onWidthMessage = function (message) {
950 | /*
951 | * Handle width message from the child.
952 | */
953 | var width = parseInt(message);
954 |
955 | // Change the width if it's different.
956 | if (width !== this.parentWidth) {
957 | this.parentWidth = width;
958 |
959 | // Call the callback function if it exists.
960 | if (this.settings.renderCallback) {
961 | this.settings.renderCallback(width);
962 | }
963 |
964 | // Send the height back to the parent.
965 | this.sendHeight();
966 | }
967 | };
968 |
969 | /**
970 | * Send a message to the the Parent.
971 | *
972 | * @memberof module:pym.Child
973 | * @method sendMessage
974 | * @instance
975 | *
976 | * @param {String} messageType The type of message to send.
977 | * @param {String} message The message data to send.
978 | */
979 | this.sendMessage = function (messageType, message) {
980 | /*
981 | * Send a message to the parent.
982 | */
983 | window.parent.postMessage(
984 | _makeMessage(this.id, messageType, message),
985 | '*',
986 | );
987 | };
988 |
989 | /**
990 | * Transmit the current iframe height to the parent.
991 | *
992 | * Call this directly in cases where you manually alter the height of the iframe contents.
993 | *
994 | * @memberof module:pym.Child
995 | * @method sendHeight
996 | * @instance
997 | */
998 | this.sendHeight = function () {
999 | // Get the child's height.
1000 | var height = document
1001 | .getElementsByTagName('body')[0]
1002 | .offsetHeight.toString();
1003 |
1004 | // Send the height to the parent.
1005 | this.sendMessage('height', height);
1006 |
1007 | return height;
1008 | }.bind(this);
1009 |
1010 | /**
1011 | * Ask parent to send the current viewport and iframe position information
1012 | *
1013 | * @memberof module:pym.Child
1014 | * @method sendHeight
1015 | * @instance
1016 | */
1017 | this.getParentPositionInfo = function () {
1018 | // Send the height to the parent.
1019 | this.sendMessage('parentPositionInfo');
1020 | };
1021 |
1022 | /**
1023 | * Scroll parent to a given element id.
1024 | *
1025 | * @memberof module:pym.Child
1026 | * @method scrollParentTo
1027 | * @instance
1028 | *
1029 | * @param {String} hash The id of the element to scroll to.
1030 | */
1031 | this.scrollParentTo = function (hash) {
1032 | this.sendMessage('navigateTo', '#' + hash);
1033 | };
1034 |
1035 | /**
1036 | * Navigate parent to a given url.
1037 | *
1038 | * @memberof module:pym.Child
1039 | * @method navigateParentTo
1040 | * @instance
1041 | *
1042 | * @param {String} url The url to navigate to.
1043 | */
1044 | this.navigateParentTo = function (url) {
1045 | this.sendMessage('navigateTo', url);
1046 | };
1047 |
1048 | /**
1049 | * Scroll parent to a given child element id.
1050 | *
1051 | * @memberof module:pym.Child
1052 | * @method scrollParentToChildEl
1053 | * @instance
1054 | *
1055 | * @param {String} id The id of the child element to scroll to.
1056 | */
1057 | this.scrollParentToChildEl = function (id) {
1058 | // Get the child element position using getBoundingClientRect + pageYOffset
1059 | // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
1060 | var topPos =
1061 | document.getElementById(id).getBoundingClientRect().top +
1062 | window.pageYOffset;
1063 | this.scrollParentToChildPos(topPos);
1064 | };
1065 |
1066 | /**
1067 | * Scroll parent to a particular child offset.
1068 | *
1069 | * @memberof module:pym.Child
1070 | * @method scrollParentToChildPos
1071 | * @instance
1072 | *
1073 | * @param {Number} pos The offset of the child element to scroll to.
1074 | */
1075 | this.scrollParentToChildPos = function (pos) {
1076 | this.sendMessage('scrollToChildPos', pos.toString());
1077 | };
1078 |
1079 | /**
1080 | * Mark Whether the child is embedded or not
1081 | * executes a callback in case it was passed to the config
1082 | *
1083 | * @memberof module:pym.Child
1084 | * @method _markWhetherEmbedded
1085 | * @inner
1086 | *
1087 | * @param {module:pym.Child~onMarkedEmbeddedStatus} The callback to execute after determining whether embedded or not.
1088 | */
1089 | var _markWhetherEmbedded = function (onMarkedEmbeddedStatus) {
1090 | var htmlElement = document.getElementsByTagName('html')[0],
1091 | newClassForHtml,
1092 | originalHtmlClasses = htmlElement.className;
1093 | try {
1094 | if (window.self !== window.top) {
1095 | newClassForHtml = 'embedded';
1096 | } else {
1097 | newClassForHtml = 'not-embedded';
1098 | }
1099 | } catch (e) {
1100 | newClassForHtml = 'embedded';
1101 | }
1102 | if (originalHtmlClasses.indexOf(newClassForHtml) < 0) {
1103 | htmlElement.className = originalHtmlClasses
1104 | ? originalHtmlClasses + ' ' + newClassForHtml
1105 | : newClassForHtml;
1106 | if (onMarkedEmbeddedStatus) {
1107 | onMarkedEmbeddedStatus(newClassForHtml);
1108 | }
1109 | _raiseCustomEvent('marked-embedded');
1110 | }
1111 | };
1112 |
1113 | /**
1114 | * @callback module:pym.Child~onMarkedEmbeddedStatus
1115 | * @param {String} classname "embedded" or "not-embedded".
1116 | */
1117 |
1118 | /**
1119 | * Unbind child event handlers and timers.
1120 | *
1121 | * @memberof module:pym.Child
1122 | * @method remove
1123 | * @instance
1124 | */
1125 | this.remove = function () {
1126 | window.removeEventListener('message', this._processMessage);
1127 | if (this.timerId) {
1128 | clearInterval(this.timerId);
1129 | }
1130 | };
1131 |
1132 | // Initialize settings with overrides.
1133 | for (var key in config) {
1134 | this.settings[key] = config[key];
1135 | }
1136 |
1137 | // Identify what ID the parent knows this child as.
1138 | this.id = _getParameterByName('childId') || config.id;
1139 | this.messageRegex = new RegExp(
1140 | '^pym' +
1141 | MESSAGE_DELIMITER +
1142 | this.id +
1143 | MESSAGE_DELIMITER +
1144 | '(\\S+)' +
1145 | MESSAGE_DELIMITER +
1146 | '(.*)$',
1147 | );
1148 |
1149 | // Get the initial width from a URL parameter.
1150 | var width = parseInt(_getParameterByName('initialWidth'));
1151 |
1152 | // Get the url of the parent frame
1153 | this.parentUrl = _getParameterByName(this.settings.parenturlparam);
1154 |
1155 | // Get the title of the parent frame
1156 | this.parentTitle = _getParameterByName('parentTitle');
1157 |
1158 | // Bind the required message handlers
1159 | this.onMessage('width', this._onWidthMessage);
1160 |
1161 | // Set up a listener to handle any incoming messages.
1162 | window.addEventListener('message', this._processMessage, false);
1163 |
1164 | // If there's a callback function, call it.
1165 | if (this.settings.renderCallback) {
1166 | this.settings.renderCallback(width);
1167 | }
1168 |
1169 | // Send the initial height to the parent.
1170 | this.sendHeight();
1171 |
1172 | // If we're configured to poll, create a setInterval to handle that.
1173 | if (this.settings.polling) {
1174 | this.timerId = window.setInterval(this.sendHeight, this.settings.polling);
1175 | }
1176 |
1177 | _markWhetherEmbedded(config.onMarkedEmbeddedStatus);
1178 |
1179 | return this;
1180 | };
1181 |
1182 | // Initialize elements with pym data attributes
1183 | // if we are not in server configuration
1184 | if (typeof document !== 'undefined') {
1185 | lib.autoInit(true);
1186 | }
1187 |
1188 | return lib;
1189 | });
1190 |
--------------------------------------------------------------------------------
/tests/public/send-frame-height-controller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/public/send-frame-height.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/public/send-height-on-framer-init.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/public/send-height-on-load.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/public/send-height-on-poll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/public/send-height-on-resize.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Embed
8 |
9 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/public/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | height: 300px;
3 | margin: 0;
4 | }
5 |
6 | iframe {
7 | width: 100%;
8 | }
9 |
--------------------------------------------------------------------------------
/tests/server.js:
--------------------------------------------------------------------------------
1 | // native
2 | import http from 'node:http';
3 | import { fileURLToPath } from 'node:url';
4 |
5 | // packages
6 | import sirv from 'sirv';
7 |
8 | export function createServer() {
9 | const src = sirv(fileURLToPath(new URL('../src', import.meta.url)), {
10 | dev: true,
11 | });
12 | const examples = sirv(fileURLToPath(new URL('./public', import.meta.url)), {
13 | dev: true,
14 | });
15 | const wares = [src, examples];
16 |
17 | const server = http
18 | .createServer(function requestListener(req, res) {
19 | let index = 0;
20 |
21 | function next() {
22 | const ware = wares[index++];
23 |
24 | if (!ware) {
25 | res.statusCode = 404;
26 | res.end('404 Not Found');
27 | return;
28 | }
29 |
30 | ware(req, res, next);
31 | }
32 |
33 | next();
34 | })
35 | .listen(3000);
36 |
37 | return {
38 | close() {
39 | server.close();
40 | },
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/tests/test.js:
--------------------------------------------------------------------------------
1 | // native
2 | import { strict as assert } from 'node:assert';
3 |
4 | // packages
5 | import { chromium, firefox, webkit } from 'playwright';
6 | import { suite } from 'uvu';
7 |
8 | // library
9 | import { createServer } from './server.js';
10 | import * as lib from '../src/index.js';
11 | import * as constants from '../src/constants.js';
12 |
13 | const browserName = process.env.BROWSER || 'chromium';
14 |
15 | const basic = suite('basic');
16 |
17 | basic('should export all public functions', () => {
18 | assert.deepEqual(Object.keys(lib), [
19 | 'Framer',
20 | 'autoInitFrames',
21 | 'initFrame',
22 | 'initFrameAndPoll',
23 | 'observeIframe',
24 | 'sendFrameHeight',
25 | 'sendHeightOnFramerInit',
26 | 'sendHeightOnLoad',
27 | 'sendHeightOnPoll',
28 | 'sendHeightOnResize',
29 | ]);
30 | });
31 |
32 | basic.run();
33 |
34 | let server;
35 | /** @type {import('playwright').Browser} */
36 | let browser;
37 | /** @type {import('playwright').Page} */
38 | let page;
39 |
40 | async function before() {
41 | browser = await { chromium, webkit, firefox }[browserName].launch();
42 | page = await browser.newPage();
43 | server = createServer();
44 | }
45 |
46 | async function beforeEach() {
47 | await page.goto('http://localhost:3000/');
48 | }
49 |
50 | async function after() {
51 | await page.close();
52 | await browser.close();
53 | server.close();
54 | }
55 |
56 | const frames = suite('frames');
57 |
58 | frames.before(before);
59 | frames.before.each(beforeEach);
60 | frames.after(after);
61 |
62 | frames('frames.sendFrameHeight()', async () => {
63 | // get iframe on page
64 | const iframe = page.locator('iframe');
65 | // assert initial height is 150
66 | assert.equal((await iframe.boundingBox()).height, 150);
67 | // inject the frame observer code
68 | await page.addScriptTag({ url: '/observe-iframe.js', type: 'module' });
69 | // set the src on the iframe and wait for it to load
70 | await iframe.evaluate((iframe) => {
71 | iframe.src = '/send-frame-height.html';
72 |
73 | return new Promise((resolve) => {
74 | iframe.onload = resolve;
75 | });
76 | });
77 | // assert the iframe now has the new height of 300
78 | assert.equal((await iframe.boundingBox()).height, 300);
79 | });
80 |
81 | frames('frames.sendHeightOnLoad()', async () => {
82 | // get iframe on page
83 | const iframe = page.locator('iframe');
84 | // assert initial height is 150
85 | assert.equal((await iframe.boundingBox()).height, 150);
86 | // inject the frame observer code
87 | await page.addScriptTag({ url: '/observe-iframe.js', type: 'module' });
88 | // set the src on the iframe and wait for it to load
89 | await iframe.evaluate((iframe) => {
90 | iframe.src = '/send-height-on-load.html';
91 |
92 | return new Promise((resolve) => {
93 | iframe.onload = resolve;
94 | });
95 | });
96 | // assert the iframe now has the new height of 300
97 | assert.equal((await iframe.boundingBox()).height, 300);
98 | });
99 |
100 | frames('frames.sendHeightOnPoll()', async () => {
101 | // get iframe on page
102 | const iframe = page.locator('iframe');
103 | // assert initial height is 150
104 | assert.equal((await iframe.boundingBox()).height, 150);
105 | // inject the frame observer code
106 | await page.addScriptTag({ url: '/observe-iframe.js', type: 'module' });
107 | // set the src on the iframe and wait for it to load, then wait for that
108 | // first poll to land
109 | await Promise.all([
110 | iframe.evaluate((iframe) => {
111 | iframe.src = '/send-height-on-poll.html';
112 |
113 | return new Promise((resolve) => {
114 | iframe.onload = resolve;
115 | });
116 | }),
117 | page.waitForFunction(() => {
118 | return new Promise((resolve) => {
119 | window.addEventListener('message', resolve);
120 | });
121 | }),
122 | ]);
123 |
124 | // assert the iframe now has the new height of 300
125 | assert.equal((await iframe.boundingBox()).height, 300);
126 | });
127 |
128 | frames('frames.sendHeightOnResize()', async () => {
129 | // get iframe on page
130 | const iframe = page.locator('iframe');
131 | // assert initial height is 150
132 | assert.equal((await iframe.boundingBox()).height, 150);
133 | // inject the frame observer code
134 | await page.addScriptTag({ url: '/observe-iframe.js', type: 'module' });
135 | // set the src on the iframe and wait for it to load
136 | await iframe.evaluate((iframe) => {
137 | iframe.src = '/send-height-on-resize.html';
138 |
139 | return new Promise((resolve) => {
140 | iframe.onload = resolve;
141 | });
142 | });
143 |
144 | // change the width of the iframe to trigger a resize
145 | await Promise.all([
146 | iframe.evaluate((iframe) => {
147 | iframe.style.width = '400px';
148 | }),
149 | page.waitForFunction(() => {
150 | return new Promise((resolve) => {
151 | window.addEventListener('message', resolve);
152 | });
153 | }),
154 | ]);
155 |
156 | // assert the iframe now has the new height of 300
157 | assert.equal((await iframe.boundingBox()).height, 300);
158 | });
159 |
160 | frames('frames.sendHeightOnFramerInit()', async () => {
161 | // get iframe on page
162 | const iframe = page.locator('iframe');
163 | // assert initial height is 150
164 | assert.equal((await iframe.boundingBox()).height, 150);
165 |
166 | // we do this in reverse — the trigger is the *observer* connecting!
167 |
168 | // set the src on the iframe and wait for it to load
169 | await iframe.evaluate((iframe) => {
170 | iframe.src = '/send-height-on-framer-init.html';
171 |
172 | return new Promise((resolve) => {
173 | iframe.onload = resolve;
174 | });
175 | });
176 |
177 | // inject the frame observer code
178 | await Promise.all([
179 | page.addScriptTag({ url: '/observe-iframe.js', type: 'module' }),
180 | page.waitForFunction(() => {
181 | return new Promise((resolve) => {
182 | window.addEventListener('message', resolve);
183 | });
184 | }),
185 | ]);
186 |
187 | // assert the iframe now has the new height of 300
188 | assert.equal((await iframe.boundingBox()).height, 300);
189 | });
190 |
191 | frames.run();
192 |
193 | const framer = suite('framer');
194 |
195 | framer.before(before);
196 | framer.before.each(beforeEach);
197 | framer.after(after);
198 |
199 | framer('framer.observeIframe()', async () => {
200 | // get iframe on page
201 | const iframe = page.locator('iframe');
202 | // assert initial height is 150
203 | assert.equal((await iframe.boundingBox()).height, 150);
204 | // inject the frame observer code
205 | await page.addScriptTag({ url: '/observe-iframe.js', type: 'module' });
206 | // set the src on the iframe and wait for it to load
207 | await iframe.evaluate((iframe) => {
208 | iframe.src = '/send-frame-height-controller.html';
209 |
210 | return new Promise((resolve) => {
211 | iframe.onload = resolve;
212 | });
213 | });
214 |
215 | // hook into the iframe's frame
216 | const iframeHandle = await iframe.elementHandle();
217 | const contentFrame = await iframeHandle.contentFrame();
218 |
219 | // update the frame's height and wait for the observer to pick it up
220 | await Promise.all(
221 | [
222 | contentFrame.evaluate(() => {
223 | window.sendFrameHeight(350);
224 | }),
225 | ],
226 | // while we're here - let's make sure the event comes over cleanly
227 | page.waitForFunction((constants) => {
228 | return new Promise((resolve) => {
229 | window.addEventListener('message', (event) => {
230 | const { data } = event;
231 |
232 | resolve(
233 | data.sentinel === constants.AMP_SENTINEL &&
234 | data.type === constants.EMBED_SIZE,
235 | );
236 | });
237 | });
238 | }, constants),
239 | );
240 |
241 | // assert the iframe now has the new height of 350
242 | assert.equal((await iframe.boundingBox()).height, 350);
243 |
244 | // update the frame's height again
245 | await contentFrame.evaluate(() => {
246 | window.sendFrameHeight(450);
247 | });
248 |
249 | // now disconnect our observer
250 | await page.evaluate(() => {
251 | window.unobserve();
252 | });
253 |
254 | // update the frame's height one more time
255 | await contentFrame.evaluate(() => {
256 | window.sendFrameHeight(550);
257 | });
258 |
259 | // the iframe should still have the height of 450
260 | assert.equal((await iframe.boundingBox()).height, 450);
261 | });
262 |
263 | framer('framer.Framer()', async () => {
264 | // prep for the eventual iframe on the page
265 | const iframe = page.locator('#iframe-container > iframe');
266 |
267 | // inject the frame observer code and wait for the height message
268 | await page.addScriptTag({ url: '/init-framer.js', type: 'module' });
269 |
270 | // assert initial height is 150
271 | assert.equal((await iframe.boundingBox()).height, 150);
272 |
273 | // hook into the iframe's frame
274 | const iframeHandle = await iframe.elementHandle();
275 | const contentFrame = await iframeHandle.contentFrame();
276 |
277 | // assert Framer set the width
278 | assert.equal(await iframe.getAttribute('width'), '100%');
279 | // assert Framer set scrolling
280 | assert.equal(await iframe.getAttribute('scrolling'), 'no');
281 | // assert Framer set marginheight
282 | assert.equal(await iframe.getAttribute('marginheight'), '0');
283 | // assert Framer set frameborder
284 | assert.equal(await iframe.getAttribute('frameborder'), '0');
285 | // assert Framer set sandbox via attributes option
286 | assert.equal(
287 | await iframe.getAttribute('sandbox'),
288 | 'allow-scripts allow-same-origin',
289 | );
290 |
291 | await contentFrame.evaluate(() => {
292 | window.sendFrameHeight(350);
293 | });
294 |
295 | // assert the height is 350
296 | assert.equal((await iframe.boundingBox()).height, 350);
297 |
298 | // update the frame's height again
299 | await contentFrame.evaluate(() => {
300 | window.sendFrameHeight(450);
301 | });
302 |
303 | // set up the next assertion
304 | assert.equal(await iframe.count(), 1);
305 |
306 | // now remove our framer instance
307 | await page.evaluate(() => {
308 | window.framer.remove();
309 | });
310 |
311 | // the iframe should be gone
312 | assert.equal(await iframe.count(), 0);
313 | });
314 |
315 | framer.run();
316 |
317 | const auto = suite('auto');
318 |
319 | auto.before(before);
320 | auto.after(after);
321 |
322 | auto('autoInitFrames()', async () => {
323 | // go to the page for auto loading
324 | await page.goto('http://localhost:3000/auto.html');
325 |
326 | // locate the three containers
327 | const containers = page.locator('[data-frame-src]');
328 |
329 | // locate the iframes
330 | const iframes = page.locator('iframe');
331 |
332 | // assert they're all there
333 | assert.equal(await containers.count(), 3);
334 | // but no iframes yet
335 | assert.equal(await iframes.count(), 0);
336 |
337 | // inject the auto load code, wait for the three to update
338 | await Promise.all([
339 | page.addScriptTag({ url: '/init-auto.js', type: 'module' }),
340 | page.waitForFunction(() => {
341 | return new Promise((resolve) => {
342 | let count = 0;
343 | window.addEventListener('message', () => {
344 | count++;
345 |
346 | if (count === 3) {
347 | resolve();
348 | }
349 | });
350 | });
351 | }),
352 | ]);
353 |
354 | // now we have iframes
355 | assert.equal(await iframes.count(), 3);
356 |
357 | // make sure they all have the right heights
358 | for (const iframe of await iframes.elementHandles()) {
359 | assert.equal((await iframe.boundingBox()).height, 300);
360 | }
361 |
362 | // make sure that the attribute matcher works
363 | assert.equal(await iframes.nth(2).getAttribute('data-id'), '3');
364 | });
365 |
366 | auto.run();
367 |
368 | const pym = suite('pym');
369 |
370 | pym.before(before);
371 | pym.after(after);
372 |
373 | pym('should support pym.js embeds', async () => {
374 | // go to the pym parent page
375 | await page.goto('http://localhost:3000/pym.html');
376 |
377 | // prep for the eventual iframe on the page
378 | const iframe = page.locator('#iframe-container > iframe');
379 |
380 | // inject the frame observer code
381 | await page.addScriptTag({ url: '/init-framer-pym.js', type: 'module' });
382 |
383 | // assert the height is 300
384 | assert.equal((await iframe.boundingBox()).height, 300);
385 | });
386 |
387 | pym.run();
388 |
--------------------------------------------------------------------------------