@data-fair/frame 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/d-frame-content.min.js +2 -0
- package/dist/d-frame.min.js +12 -0
- package/package.json +44 -0
- package/src/DFrameContent.d.ts +20 -0
- package/src/DFrameContent.js +148 -0
- package/src/DFrameElement.d.ts +45 -0
- package/src/DFrameElement.js +249 -0
- package/src/d-frame-content.d.ts +3 -0
- package/src/d-frame-content.js +2 -0
- package/src/d-frame.d.ts +1 -0
- package/src/d-frame.js +2 -0
- package/src/messages.d.ts +17 -0
- package/src/messages.js +15 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Alban Mouton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# d-frame
|
|
2
|
+
An iframe wrapper for powerful web app integrations.
|
|
3
|
+
|
|
4
|
+
Lightweight, vanilla JS, no dependency.
|
|
5
|
+
|
|
6
|
+
Supports:
|
|
7
|
+
- navigation syncing
|
|
8
|
+
- aspect ratio management
|
|
9
|
+
- dynamic resize
|
|
10
|
+
|
|
11
|
+
See [the documentation](https://data-fair.github.io/frame/latest/).
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var DFrameContent=function(){"use strict";var u=Object.defineProperty;var g=(n,i,a)=>i in n?u(n,i,{enumerable:!0,configurable:!0,writable:!0,value:a}):n[i]=a;var c=(n,i,a)=>g(n,typeof i!="symbol"?i+"":i,a);function n(s){return s[0]==="df-parent"&&s[1]==="init"}function i(s){return s[0]==="df-parent"&&s[1]==="updateSrc"}const a=["animationstart","webkitAnimationStart","animationiteration","webkitAnimationIteration","animationend","webkitAnimationEnd","input","mouseup","mousedown","orientationchange","afterprint","beforeprint","readystatechange","touchstart","touchend","touchcancel","transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart","transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration","transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend","resize"];class d{constructor(e){c(this,"options");c(this,"pendingCheckHeight",!1);c(this,"debug",!1);c(this,"throttledCheckHeight");c(this,"lastHeight",0);this.options=e??{};const t=()=>{this.checkHeight(),this.pendingCheckHeight=!1};this.throttledCheckHeight=()=>{this.pendingCheckHeight||(this.pendingCheckHeight=!0,requestAnimationFrame(t))},!(typeof window>"u")&&(window.addEventListener("message",o=>this.onMessage(o)),this.postMessageToParent(["df-child","init"]))}log(e,...t){e==="debug"&&!this.debug||(e==="debug"&&console.debug("d-frame-content",...t),e==="info"&&console.info("d-frame-content",...t))}onMessage(e){if(e.source===window.parent&&Array.isArray(e.data)){const t=e.data;n(t)&&(this.debug=!!t[2].debug,t[2].resize!=="no"&&this.initResize(),(t[2].syncParams||t[2].syncPath)&&this.initSyncParams()),i(t)&&(this.options.updateSrc?this.options.updateSrc(t[2].startsWith("/")?window.location.origin+t[2]:t[2]):window.location.href=t[2])}}postMessageToParent(e){window.parent.postMessage(e)}initResize(){this.log("debug","initResize"),this.checkHeight(),this.createMutationObserver(),this.createWindowEventListeners()}initSyncParams(){this.log("debug","initSyncParams");const e=window.history.replaceState,t=(...r)=>{const h=e.apply(window.history,r);return this.postMessageToParent(["df-child","stateChange","push",window.location.href]),h};window.history.pushState=t;const o=(...r)=>{const h=e.apply(window.history,r);return this.postMessageToParent(["df-child","stateChange","replace",window.location.href]),h};window.history.replaceState=o}createMutationObserver(){const e=document.querySelector("body");if(!e)throw new Error("DFrameContentManager was initialized before the HTML body");new window.MutationObserver(this.throttledCheckHeight).observe(e,{attributes:!1,attributeOldValue:!1,characterData:!1,characterDataOldValue:!1,childList:!0,subtree:!0})}createWindowEventListeners(){for(const e of a)window.addEventListener(e,this.throttledCheckHeight,{passive:!0})}checkHeight(){const e=document.querySelectorAll("[data-iframe-height]");let t=0;for(const o of e){const r=o.getAttribute("data-iframe-height"),h=o.getBoundingClientRect().bottom+parseFloat(getComputedStyle(o).getPropertyValue("margin-bottom"))+(r?parseFloat(r):0);h>t&&(t=h)}t!==this.lastHeight&&(this.postMessageToParent(["df-child","height",t]),this.lastHeight=t)}}return new d}();
|
|
2
|
+
//# sourceMappingURL=d-frame-content.min.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
var E=Object.defineProperty;var R=(u,c,p)=>c in u?E(u,c,{enumerable:!0,configurable:!0,writable:!0,value:p}):u[c]=p;var o=(u,c,p)=>R(u,typeof c!="symbol"?c+"":c,p);(function(){"use strict";function u(e){return e[0]==="df-child"&&e[1]==="init"}function c(e){return e[0]==="df-child"&&e[1]==="height"}function p(e){return e[0]==="df-child"&&e[1]==="stateChange"}function m(e){return e.endsWith("/")?e:e===""?"/":e.slice(0,e.lastIndexOf("/")+1)}function b(e,r){if(r.origin!==e.origin)return r.href;if(r.pathname===e.pathname)return"";const t=m(e.pathname);return r.pathname.startsWith(t)?r.pathname.replace(t,"./"):r.pathname}const y=e=>e.replace(/[-[\]/{}()*+?.\\^$|]/g,"\\$&");function w(e){const r=[];for(const t of e.split(",")){const[i="*",s]=t.split(":"),a="^"+y(i).replace(/\\\*/g,"(.*)")+"$";r.push({regexp:new RegExp(a),append:s})}return r}function A(e,r,t,i){if(!t&&!i)return e.href;const s=new URL(r);if(t&&s.searchParams.forEach((a,d)=>{for(const n of t){if(n.append&&!d.startsWith(n.append)||d===i)continue;const h=n.append?d.replace(n.append,""):d;if(h.match(n.regexp)){e.searchParams.set(h,a);break}}}),i!==null){const a=s.searchParams.get(i);a&&(e.pathname=new URL(a,e).pathname)}return e.href}function S(e,r,t,i,s){const a=new URL(t),d=new URL(r);if(i&&(a.searchParams.forEach((n,h)=>{for(const l of i){if(l.append&&!h.startsWith(l.append))continue;const f=l.append?h.replace(l.append,""):h;if(f.match(l.regexp)){d.searchParams.get(f)===null&&a.searchParams.delete(h);break}}}),d.searchParams.forEach((n,h)=>{for(const l of i){if(!h.match(l.regexp)||(e==null?void 0:e.searchParams.get(h))===n)continue;const f=l.append?l.append+h:h;a.searchParams.set(f,n);break}})),s!==null&&e){const n=b(e,d);n?a.searchParams.set(s,n):a.searchParams.delete(s)}return a}class P{stateChange(r,t){r==="replace"&&window.history.replaceState(null,"",t),r==="push"&&window.history.pushState(null,"",t)}}const g=document.createElement("template");g.innerHTML=`<style>
|
|
2
|
+
:host {
|
|
3
|
+
display: block;
|
|
4
|
+
}
|
|
5
|
+
iframe.d-frame-iframe {
|
|
6
|
+
display: block;
|
|
7
|
+
width: 100%;
|
|
8
|
+
border: none;
|
|
9
|
+
max-height: 100%;
|
|
10
|
+
}
|
|
11
|
+
</style><slot name="loader"></slot><iframe class="d-frame-iframe" frameborder="0">`;class z extends HTMLElement{constructor(){super();o(this,"adapter");o(this,"connected",!1);o(this,"initialSrc");o(this,"srcUrl");o(this,"parsedSyncParams");o(this,"iframeElement");o(this,"slotElement");o(this,"width");o(this,"aspectRatioHeight");o(this,"resizedHeight");const t=g.content.cloneNode(!0);this.slotElement=t.childNodes[1],this.resize!=="yes"&&(this.slotElement.style.display="none"),this.iframeElement=t.childNodes[2],this.iframeElement.setAttribute("scrolling",this.resize==="yes"?"no":"auto"),this.iframeElement.setAttribute("loading",this.lazy?"lazy":"eager"),this.attachShadow({mode:"open"}).appendChild(t),new ResizeObserver(a=>{this.width!==a[0].contentRect.width&&(this.width=a[0].contentRect.width,this.updateAspectRatioHeight())}).observe(this),this.adapter=new P,window.addEventListener("message",a=>this.onMessage(a))}get debug(){return this.hasAttribute("debug")}set debug(t){t?this.setAttribute("debug",""):this.removeAttribute("debug")}get lazy(){return this.hasAttribute("lazy")}set lazy(t){t?this.setAttribute("lazy",""):this.removeAttribute("lazy")}get src(){return this.getAttribute("src")}set src(t){this.setAttribute("src",t)}get aspectRatio(){return this.getAttribute("aspect-ratio")??"auto"}set aspectRatio(t){this.setAttribute("aspect-ratio",t)}get height(){return this.getAttribute("height")}set height(t){t?this.setAttribute("height",t):this.removeAttribute("height")}set resize(t){this.setAttribute("resize",t)}get resize(){const t=this.getAttribute("resize");return t===null?"auto":t===""?"yes":t}get syncParams(){return this.getAttribute("sync-params")}set syncParams(t){t!==null?this.setAttribute("sync-params",t):this.removeAttribute("sync-params")}get syncPath(){const t=this.getAttribute("sync-path");return t===null?null:t||"p"}set syncPath(t){t!==null?this.setAttribute("sync-params",t):this.removeAttribute("sync-params")}get actualAspectRatio(){return this.aspectRatio!=="auto"?Number(this.aspectRatio):!this.width||this.width<500?1:this.width<800?4/3:this.width<1200?16/9:21/9}onMessage(t){var i;if(t.source===((i=this.iframeElement)==null?void 0:i.contentWindow)&&Array.isArray(t.data)){const s=t.data;if(this.log("debug","received message from child",s),u(s)&&(this.postMessageToChild(["df-parent","init",{debug:this.debug,resize:this.resize,syncParams:this.syncParams!==null,syncPath:this.syncPath!==null}]),this.init()),c(s)&&(this.resizedHeight=s[2],this.updateStyle()),p(s)&&(this.parsedSyncParams||this.syncPath)){const a=S(this.srcUrl,s[3],window.location.href,this.parsedSyncParams,this.syncPath);a.href!==window.location.href&&this.adapter.stateChange(s[2],a)}}}init(){this.initialSrc=this.src}postMessageToChild(t){var i,s;this.log("debug","send message to child",t),(s=(i=this.iframeElement)==null?void 0:i.contentWindow)==null||s.postMessage(t)}updateSrc(){if(!this.connected)return;this.srcUrl=new URL(this.src.startsWith("/")?window.location.origin+this.src:this.src);const t=A(this.srcUrl,window.location.href,this.parsedSyncParams,this.syncPath);this.initialSrc?this.postMessageToChild(["df-parent","updateSrc",t]):this.iframeElement.setAttribute("src",t)}updateStyle(){let t="";(this.resize==="yes"||this.resize==="auto"&&this.resizedHeight)&&(this.iframeElement.setAttribute("scrolling","no"),this.resizedHeight?(this.slotElement.style.display="none",t+=`height:${this.resizedHeight}px;`):this.height?(this.slotElement.style.display="none",t+=`height:${this.height};`):this.aspectRatio!=="auto"?(this.slotElement.style.display="none",t+=`height:${this.aspectRatioHeight}px;`):(this.slotElement.style.display="block",t+="height:0;")),(this.resize==="no"||this.resize==="auto"&&!this.resizedHeight)&&(this.iframeElement.setAttribute("scrolling","auto"),this.height?t+=`height:${this.height};`:t+=`height:${this.aspectRatioHeight}px;`),this.iframeElement.setAttribute("style",t)}updateAspectRatioHeight(){this.width&&(this.resizedHeight||this.height||(this.aspectRatioHeight=this.width/this.actualAspectRatio,this.updateStyle()))}log(t,...i){t==="debug"&&!this.debug||(t==="debug"&&console.debug("d-frame",...i),t==="info"&&console.info("d-frame",...i))}connectedCallback(){if(!this.hasAttribute("src"))throw new Error("src is a required attribute");this.log("debug","connected"),this.connected=!0,this.adapter.onStateChange&&this.adapter.onStateChange(()=>{this.updateSrc()}),this.syncParams!==null&&(this.parsedSyncParams=w(this.syncParams||"*")),this.updateStyle(),this.updateSrc()}static get observedAttributes(){return["src","aspect-ratio","height","sync-params","sync-path"]}attributeChangedCallback(t,i,s){this.log("debug","attribute change",t,i,s),t==="aspect-ratio"&&this.updateAspectRatioHeight(),t==="src"&&this.updateSrc(),t==="height"&&this.updateStyle(),t==="sync-params"&&this.updateSrc(),t==="sync-path"&&this.updateSrc()}}customElements.define("d-frame",z)})();
|
|
12
|
+
//# sourceMappingURL=d-frame.min.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-fair/frame",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "An iframe wrapper for powerful web app integrations.",
|
|
5
|
+
"main": "dist/frame.min.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"doc": "doc"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@commitlint/config-conventional": "^19.6.0",
|
|
11
|
+
"@koumoul/gh-pages-multi": "^0.7.2",
|
|
12
|
+
"commitlint": "^19.6.0",
|
|
13
|
+
"eslint": "^9.16.0",
|
|
14
|
+
"husky": "^9.1.7",
|
|
15
|
+
"neostandard": "^0.11.9",
|
|
16
|
+
"typescript": "^5.7.2",
|
|
17
|
+
"vite": "^6.0.3",
|
|
18
|
+
"vue-router": "^4.5.0"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"dev-zellij": "zellij --layout .zellij.kdl",
|
|
24
|
+
"prepare": "husky",
|
|
25
|
+
"test": "node --test tests/*.js",
|
|
26
|
+
"build": "rm -f src/*.d.ts && rm -f src/*.js && tsc -p tsconfig.json && rm -rf dist && vite -c vite.config.d-frame.js build && vite -c vite.config.d-frame-content.js build",
|
|
27
|
+
"prepublishOnly": "npm run lint && npm run test && npm run build",
|
|
28
|
+
"publish:main": "cd doc && npm run build:main && cd .. && gh-pages-multi deploy -v -s doc/.output/public -t main",
|
|
29
|
+
"publish:latest": "cd doc && npm run build:latest && cd .. && gh-pages-multi deploy -v -s doc/.output/public -t latest"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"data-fair",
|
|
33
|
+
"iframe",
|
|
34
|
+
"integration"
|
|
35
|
+
],
|
|
36
|
+
"author": "Alban Mouton <alban.mouton@gmail.com>",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"type": "module",
|
|
39
|
+
"files": [
|
|
40
|
+
"src/*.js",
|
|
41
|
+
"src/*.d.ts",
|
|
42
|
+
"dist/*.js"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ChildMessage } from './messages.js';
|
|
2
|
+
export type DFrameContentOptions = {
|
|
3
|
+
updateSrc?: (src: string) => void;
|
|
4
|
+
};
|
|
5
|
+
export default class DFrameContent {
|
|
6
|
+
private options;
|
|
7
|
+
private pendingCheckHeight;
|
|
8
|
+
private debug;
|
|
9
|
+
private throttledCheckHeight;
|
|
10
|
+
private lastHeight;
|
|
11
|
+
constructor(options?: DFrameContentOptions);
|
|
12
|
+
log(level: 'debug' | 'info', ...args: any[]): void;
|
|
13
|
+
onMessage(e: MessageEvent): void;
|
|
14
|
+
postMessageToParent(message: ChildMessage): void;
|
|
15
|
+
initResize(): void;
|
|
16
|
+
initSyncParams(): void;
|
|
17
|
+
createMutationObserver(): void;
|
|
18
|
+
createWindowEventListeners(): void;
|
|
19
|
+
checkHeight(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { isInitParentMessage, isUpdateSrcMessage } from './messages.js';
|
|
2
|
+
const windowEventTypes = [
|
|
3
|
+
'animationstart',
|
|
4
|
+
'webkitAnimationStart',
|
|
5
|
+
'animationiteration',
|
|
6
|
+
'webkitAnimationIteration',
|
|
7
|
+
'animationend',
|
|
8
|
+
'webkitAnimationEnd',
|
|
9
|
+
'input',
|
|
10
|
+
'mouseup',
|
|
11
|
+
'mousedown',
|
|
12
|
+
'orientationchange',
|
|
13
|
+
'afterprint',
|
|
14
|
+
'beforeprint',
|
|
15
|
+
'readystatechange',
|
|
16
|
+
'touchstart',
|
|
17
|
+
'touchend',
|
|
18
|
+
'touchcancel',
|
|
19
|
+
'transitionstart',
|
|
20
|
+
'webkitTransitionStart',
|
|
21
|
+
'MSTransitionStart',
|
|
22
|
+
'oTransitionStart',
|
|
23
|
+
'otransitionstart', 'transitioniteration',
|
|
24
|
+
'webkitTransitionIteration',
|
|
25
|
+
'MSTransitionIteration',
|
|
26
|
+
'oTransitionIteration',
|
|
27
|
+
'otransitioniteration', 'transitionend',
|
|
28
|
+
'webkitTransitionEnd',
|
|
29
|
+
'MSTransitionEnd',
|
|
30
|
+
'oTransitionEnd',
|
|
31
|
+
'otransitionend', 'resize'
|
|
32
|
+
];
|
|
33
|
+
export default class DFrameContent {
|
|
34
|
+
options;
|
|
35
|
+
pendingCheckHeight = false;
|
|
36
|
+
debug = false;
|
|
37
|
+
throttledCheckHeight;
|
|
38
|
+
lastHeight = 0;
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.options = options ?? {};
|
|
41
|
+
const afCallback = () => {
|
|
42
|
+
this.checkHeight();
|
|
43
|
+
this.pendingCheckHeight = false;
|
|
44
|
+
};
|
|
45
|
+
this.throttledCheckHeight = () => {
|
|
46
|
+
if (this.pendingCheckHeight)
|
|
47
|
+
return;
|
|
48
|
+
this.pendingCheckHeight = true;
|
|
49
|
+
requestAnimationFrame(afCallback);
|
|
50
|
+
};
|
|
51
|
+
if (typeof window === 'undefined')
|
|
52
|
+
return;
|
|
53
|
+
window.addEventListener('message', (e) => this.onMessage(e));
|
|
54
|
+
// simple handshake for initialization
|
|
55
|
+
this.postMessageToParent(['df-child', 'init']);
|
|
56
|
+
}
|
|
57
|
+
log(level, ...args) {
|
|
58
|
+
if (level === 'debug' && !this.debug)
|
|
59
|
+
return;
|
|
60
|
+
if (level === 'debug')
|
|
61
|
+
console.debug('d-frame-content', ...args);
|
|
62
|
+
if (level === 'info')
|
|
63
|
+
console.info('d-frame-content', ...args);
|
|
64
|
+
}
|
|
65
|
+
onMessage(e) {
|
|
66
|
+
if (e.source === window.parent && Array.isArray(e.data)) {
|
|
67
|
+
const message = e.data;
|
|
68
|
+
if (isInitParentMessage(message)) {
|
|
69
|
+
this.debug = !!message[2].debug;
|
|
70
|
+
if (message[2].resize !== 'no')
|
|
71
|
+
this.initResize();
|
|
72
|
+
if (message[2].syncParams || message[2].syncPath)
|
|
73
|
+
this.initSyncParams();
|
|
74
|
+
}
|
|
75
|
+
if (isUpdateSrcMessage(message)) {
|
|
76
|
+
if (this.options.updateSrc) {
|
|
77
|
+
this.options.updateSrc(message[2].startsWith('/') ? window.location.origin + message[2] : message[2]);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
window.location.href = message[2];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
postMessageToParent(message) {
|
|
86
|
+
window.parent.postMessage(message);
|
|
87
|
+
}
|
|
88
|
+
initResize() {
|
|
89
|
+
this.log('debug', 'initResize');
|
|
90
|
+
this.checkHeight();
|
|
91
|
+
this.createMutationObserver();
|
|
92
|
+
this.createWindowEventListeners();
|
|
93
|
+
}
|
|
94
|
+
initSyncParams() {
|
|
95
|
+
this.log('debug', 'initSyncParams');
|
|
96
|
+
// monkey patch pushState and replaceState to send all state change info to the parent window
|
|
97
|
+
// this is to compensate the lack of event emission by window.history
|
|
98
|
+
const oldReplaceState = window.history.replaceState;
|
|
99
|
+
const newPushState = (...args) => {
|
|
100
|
+
// do a replace instead of a push, the push will be done in the parent window if sync-state is activated
|
|
101
|
+
const ret = oldReplaceState.apply(window.history, args);
|
|
102
|
+
this.postMessageToParent(['df-child', 'stateChange', 'push', window.location.href]);
|
|
103
|
+
return ret;
|
|
104
|
+
};
|
|
105
|
+
window.history.pushState = newPushState;
|
|
106
|
+
const newReplaceState = (...args) => {
|
|
107
|
+
const ret = oldReplaceState.apply(window.history, args);
|
|
108
|
+
this.postMessageToParent(['df-child', 'stateChange', 'replace', window.location.href]);
|
|
109
|
+
return ret;
|
|
110
|
+
};
|
|
111
|
+
window.history.replaceState = newReplaceState;
|
|
112
|
+
}
|
|
113
|
+
createMutationObserver() {
|
|
114
|
+
const body = document.querySelector('body');
|
|
115
|
+
if (!body)
|
|
116
|
+
throw new Error('DFrameContentManager was initialized before the HTML body');
|
|
117
|
+
const observer = new window.MutationObserver(this.throttledCheckHeight);
|
|
118
|
+
observer.observe(body, {
|
|
119
|
+
attributes: false,
|
|
120
|
+
attributeOldValue: false,
|
|
121
|
+
characterData: false,
|
|
122
|
+
characterDataOldValue: false,
|
|
123
|
+
childList: true,
|
|
124
|
+
subtree: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
createWindowEventListeners() {
|
|
128
|
+
for (const eventType of windowEventTypes) {
|
|
129
|
+
window.addEventListener(eventType, this.throttledCheckHeight, { passive: true });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
checkHeight() {
|
|
133
|
+
const elements = document.querySelectorAll('[data-iframe-height]');
|
|
134
|
+
let max = 0;
|
|
135
|
+
for (const element of elements) {
|
|
136
|
+
const dataAttribute = element.getAttribute('data-iframe-height');
|
|
137
|
+
const bottom = element.getBoundingClientRect().bottom +
|
|
138
|
+
parseFloat(getComputedStyle(element).getPropertyValue('margin-bottom')) +
|
|
139
|
+
(dataAttribute ? parseFloat(dataAttribute) : 0);
|
|
140
|
+
if (bottom > max)
|
|
141
|
+
max = bottom;
|
|
142
|
+
}
|
|
143
|
+
if (max !== this.lastHeight) {
|
|
144
|
+
this.postMessageToParent(['df-child', 'height', max]);
|
|
145
|
+
this.lastHeight = max;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ParentMessage } from './messages.js';
|
|
2
|
+
export interface StateChangeAdapter {
|
|
3
|
+
stateChange(action: 'push' | 'replace', newUrl: URL): void;
|
|
4
|
+
onStateChange?(callback: () => void): void;
|
|
5
|
+
}
|
|
6
|
+
export default class DFrameElement extends HTMLElement {
|
|
7
|
+
get debug(): boolean;
|
|
8
|
+
set debug(value: boolean);
|
|
9
|
+
get lazy(): boolean;
|
|
10
|
+
set lazy(value: boolean);
|
|
11
|
+
get src(): string;
|
|
12
|
+
set src(value: string);
|
|
13
|
+
get aspectRatio(): string;
|
|
14
|
+
set aspectRatio(value: string);
|
|
15
|
+
get height(): string | null;
|
|
16
|
+
set height(value: string | null);
|
|
17
|
+
set resize(value: "auto" | "yes" | "no");
|
|
18
|
+
get resize(): 'auto' | 'yes' | 'no';
|
|
19
|
+
get syncParams(): string | null;
|
|
20
|
+
set syncParams(value: string | null);
|
|
21
|
+
get syncPath(): string | null;
|
|
22
|
+
set syncPath(value: string | null);
|
|
23
|
+
adapter: StateChangeAdapter;
|
|
24
|
+
private connected;
|
|
25
|
+
private initialSrc;
|
|
26
|
+
private srcUrl;
|
|
27
|
+
private parsedSyncParams;
|
|
28
|
+
private iframeElement;
|
|
29
|
+
private slotElement;
|
|
30
|
+
private width;
|
|
31
|
+
private aspectRatioHeight;
|
|
32
|
+
private resizedHeight;
|
|
33
|
+
get actualAspectRatio(): number;
|
|
34
|
+
constructor();
|
|
35
|
+
onMessage(e: MessageEvent): void;
|
|
36
|
+
init(): void;
|
|
37
|
+
postMessageToChild(message: ParentMessage): void;
|
|
38
|
+
updateSrc(): void;
|
|
39
|
+
updateStyle(): void;
|
|
40
|
+
updateAspectRatioHeight(): void;
|
|
41
|
+
log(level: 'debug' | 'info', ...args: any[]): void;
|
|
42
|
+
connectedCallback(): void;
|
|
43
|
+
static get observedAttributes(): string[];
|
|
44
|
+
attributeChangedCallback(name: string, oldValue: any, newValue: any): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// this is a good doc about writing web components https://web.dev/articles/custom-elements-best-practices
|
|
2
|
+
import { isHeightMessage, isInitChildMessage, isStateChangeMessage } from './messages.js';
|
|
3
|
+
import { parseSyncParams, getChildSrc, getParentUrl } from './utils/sync-params.js';
|
|
4
|
+
class WindowStateChangeAdapter {
|
|
5
|
+
stateChange(action, newUrl) {
|
|
6
|
+
if (action === 'replace')
|
|
7
|
+
window.history.replaceState(null, '', newUrl);
|
|
8
|
+
if (action === 'push')
|
|
9
|
+
window.history.pushState(null, '', newUrl);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const template = document.createElement('template');
|
|
13
|
+
template.innerHTML = `<style>
|
|
14
|
+
:host {
|
|
15
|
+
display: block;
|
|
16
|
+
}
|
|
17
|
+
iframe.d-frame-iframe {
|
|
18
|
+
display: block;
|
|
19
|
+
width: 100%;
|
|
20
|
+
border: none;
|
|
21
|
+
max-height: 100%;
|
|
22
|
+
}
|
|
23
|
+
</style><slot name="loader"></slot><iframe class="d-frame-iframe" frameborder="0">`;
|
|
24
|
+
export default class DFrameElement extends HTMLElement {
|
|
25
|
+
get debug() { return this.hasAttribute('debug'); }
|
|
26
|
+
set debug(value) {
|
|
27
|
+
if (value)
|
|
28
|
+
this.setAttribute('debug', '');
|
|
29
|
+
else
|
|
30
|
+
this.removeAttribute('debug');
|
|
31
|
+
}
|
|
32
|
+
get lazy() { return this.hasAttribute('lazy'); }
|
|
33
|
+
set lazy(value) {
|
|
34
|
+
if (value)
|
|
35
|
+
this.setAttribute('lazy', '');
|
|
36
|
+
else
|
|
37
|
+
this.removeAttribute('lazy');
|
|
38
|
+
}
|
|
39
|
+
get src() { return this.getAttribute('src'); }
|
|
40
|
+
set src(value) { this.setAttribute('src', value); }
|
|
41
|
+
get aspectRatio() { return this.getAttribute('aspect-ratio') ?? 'auto'; }
|
|
42
|
+
set aspectRatio(value) { this.setAttribute('aspect-ratio', value); }
|
|
43
|
+
get height() { return this.getAttribute('height'); }
|
|
44
|
+
set height(value) {
|
|
45
|
+
if (value)
|
|
46
|
+
this.setAttribute('height', value);
|
|
47
|
+
else
|
|
48
|
+
this.removeAttribute('height');
|
|
49
|
+
}
|
|
50
|
+
set resize(value) { this.setAttribute('resize', value); }
|
|
51
|
+
get resize() {
|
|
52
|
+
const value = this.getAttribute('resize');
|
|
53
|
+
if (value === null)
|
|
54
|
+
return 'auto';
|
|
55
|
+
if (value === '')
|
|
56
|
+
return 'yes';
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
get syncParams() { return this.getAttribute('sync-params'); }
|
|
60
|
+
set syncParams(value) {
|
|
61
|
+
if (value !== null)
|
|
62
|
+
this.setAttribute('sync-params', value);
|
|
63
|
+
else
|
|
64
|
+
this.removeAttribute('sync-params');
|
|
65
|
+
}
|
|
66
|
+
get syncPath() {
|
|
67
|
+
const value = this.getAttribute('sync-path');
|
|
68
|
+
if (value === null)
|
|
69
|
+
return null;
|
|
70
|
+
return value || 'p';
|
|
71
|
+
}
|
|
72
|
+
set syncPath(value) {
|
|
73
|
+
if (value !== null)
|
|
74
|
+
this.setAttribute('sync-params', value);
|
|
75
|
+
else
|
|
76
|
+
this.removeAttribute('sync-params');
|
|
77
|
+
}
|
|
78
|
+
adapter;
|
|
79
|
+
/* internal state */
|
|
80
|
+
connected = false;
|
|
81
|
+
initialSrc;
|
|
82
|
+
srcUrl;
|
|
83
|
+
parsedSyncParams;
|
|
84
|
+
iframeElement;
|
|
85
|
+
slotElement;
|
|
86
|
+
width;
|
|
87
|
+
aspectRatioHeight;
|
|
88
|
+
resizedHeight;
|
|
89
|
+
get actualAspectRatio() {
|
|
90
|
+
if (this.aspectRatio !== 'auto')
|
|
91
|
+
return Number(this.aspectRatio);
|
|
92
|
+
if (!this.width || this.width < 500)
|
|
93
|
+
return 1;
|
|
94
|
+
if (this.width < 800)
|
|
95
|
+
return 4 / 3;
|
|
96
|
+
if (this.width < 1200)
|
|
97
|
+
return 16 / 9;
|
|
98
|
+
return 21 / 9;
|
|
99
|
+
}
|
|
100
|
+
constructor() {
|
|
101
|
+
super();
|
|
102
|
+
const root = template.content.cloneNode(true);
|
|
103
|
+
this.slotElement = root.childNodes[1];
|
|
104
|
+
if (this.resize !== 'yes')
|
|
105
|
+
this.slotElement.style.display = 'none';
|
|
106
|
+
this.iframeElement = root.childNodes[2];
|
|
107
|
+
this.iframeElement.setAttribute('scrolling', this.resize === 'yes' ? 'no' : 'auto');
|
|
108
|
+
this.iframeElement.setAttribute('loading', this.lazy ? 'lazy' : 'eager');
|
|
109
|
+
const shadowRoot = this.attachShadow({ mode: 'open' });
|
|
110
|
+
shadowRoot.appendChild(root);
|
|
111
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
112
|
+
if (this.width !== entries[0].contentRect.width) {
|
|
113
|
+
this.width = entries[0].contentRect.width;
|
|
114
|
+
this.updateAspectRatioHeight();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// TODO: should we use unobserve or disconnect on cleanup ?
|
|
118
|
+
resizeObserver.observe(this);
|
|
119
|
+
this.adapter = new WindowStateChangeAdapter();
|
|
120
|
+
window.addEventListener('message', (e) => this.onMessage(e));
|
|
121
|
+
}
|
|
122
|
+
onMessage(e) {
|
|
123
|
+
if (e.source === this.iframeElement?.contentWindow && Array.isArray(e.data)) {
|
|
124
|
+
const message = e.data;
|
|
125
|
+
this.log('debug', 'received message from child', message);
|
|
126
|
+
if (isInitChildMessage(message)) {
|
|
127
|
+
// simple handshake for initialization
|
|
128
|
+
this.postMessageToChild(['df-parent', 'init', {
|
|
129
|
+
debug: this.debug,
|
|
130
|
+
resize: this.resize,
|
|
131
|
+
syncParams: this.syncParams !== null,
|
|
132
|
+
syncPath: this.syncPath !== null
|
|
133
|
+
}]);
|
|
134
|
+
this.init();
|
|
135
|
+
}
|
|
136
|
+
if (isHeightMessage(message)) {
|
|
137
|
+
this.resizedHeight = message[2];
|
|
138
|
+
this.updateStyle();
|
|
139
|
+
}
|
|
140
|
+
if (isStateChangeMessage(message) && (this.parsedSyncParams || this.syncPath)) {
|
|
141
|
+
const newParentHUrl = getParentUrl(this.srcUrl, message[3], window.location.href, this.parsedSyncParams, this.syncPath);
|
|
142
|
+
if (newParentHUrl.href !== window.location.href) {
|
|
143
|
+
this.adapter.stateChange(message[2], newParentHUrl);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
init() {
|
|
149
|
+
// called after initial handshake with child
|
|
150
|
+
this.initialSrc = this.src;
|
|
151
|
+
}
|
|
152
|
+
postMessageToChild(message) {
|
|
153
|
+
this.log('debug', 'send message to child', message);
|
|
154
|
+
this.iframeElement?.contentWindow?.postMessage(message);
|
|
155
|
+
}
|
|
156
|
+
updateSrc() {
|
|
157
|
+
if (!this.connected)
|
|
158
|
+
return;
|
|
159
|
+
this.srcUrl = new URL(this.src.startsWith('/') ? (window.location.origin + this.src) : this.src);
|
|
160
|
+
const fullSrc = getChildSrc(this.srcUrl, window.location.href, this.parsedSyncParams, this.syncPath);
|
|
161
|
+
if (this.initialSrc) {
|
|
162
|
+
this.postMessageToChild(['df-parent', 'updateSrc', fullSrc]);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
this.iframeElement.setAttribute('src', fullSrc);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
updateStyle() {
|
|
169
|
+
let style = '';
|
|
170
|
+
if (this.resize === 'yes' || (this.resize === 'auto' && this.resizedHeight)) {
|
|
171
|
+
this.iframeElement.setAttribute('scrolling', 'no');
|
|
172
|
+
if (this.resizedHeight) {
|
|
173
|
+
this.slotElement.style.display = 'none';
|
|
174
|
+
style += `height:${this.resizedHeight}px;`;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (this.height) {
|
|
178
|
+
this.slotElement.style.display = 'none';
|
|
179
|
+
style += `height:${this.height};`;
|
|
180
|
+
}
|
|
181
|
+
else if (this.aspectRatio !== 'auto') {
|
|
182
|
+
this.slotElement.style.display = 'none';
|
|
183
|
+
style += `height:${this.aspectRatioHeight}px;`;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.slotElement.style.display = 'block';
|
|
187
|
+
style += 'height:0;';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (this.resize === 'no' || (this.resize === 'auto' && !this.resizedHeight)) {
|
|
192
|
+
this.iframeElement.setAttribute('scrolling', 'auto');
|
|
193
|
+
if (this.height) {
|
|
194
|
+
style += `height:${this.height};`;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
style += `height:${this.aspectRatioHeight}px;`;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.iframeElement.setAttribute('style', style);
|
|
201
|
+
}
|
|
202
|
+
updateAspectRatioHeight() {
|
|
203
|
+
if (!this.width)
|
|
204
|
+
return;
|
|
205
|
+
if (this.resizedHeight)
|
|
206
|
+
return;
|
|
207
|
+
if (this.height)
|
|
208
|
+
return;
|
|
209
|
+
this.aspectRatioHeight = this.width / this.actualAspectRatio;
|
|
210
|
+
this.updateStyle();
|
|
211
|
+
}
|
|
212
|
+
log(level, ...args) {
|
|
213
|
+
if (level === 'debug' && !this.debug)
|
|
214
|
+
return;
|
|
215
|
+
if (level === 'debug')
|
|
216
|
+
console.debug('d-frame', ...args);
|
|
217
|
+
if (level === 'info')
|
|
218
|
+
console.info('d-frame', ...args);
|
|
219
|
+
}
|
|
220
|
+
/* standard custom element callbacks */
|
|
221
|
+
connectedCallback() {
|
|
222
|
+
if (!this.hasAttribute('src'))
|
|
223
|
+
throw new Error('src is a required attribute');
|
|
224
|
+
this.log('debug', 'connected');
|
|
225
|
+
this.connected = true;
|
|
226
|
+
if (this.adapter.onStateChange) {
|
|
227
|
+
this.adapter.onStateChange(() => { this.updateSrc(); });
|
|
228
|
+
}
|
|
229
|
+
if (this.syncParams !== null)
|
|
230
|
+
this.parsedSyncParams = parseSyncParams(this.syncParams || '*');
|
|
231
|
+
this.updateStyle();
|
|
232
|
+
this.updateSrc();
|
|
233
|
+
}
|
|
234
|
+
/* reflected attributes */
|
|
235
|
+
static get observedAttributes() { return ['src', 'aspect-ratio', 'height', 'sync-params', 'sync-path']; }
|
|
236
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
237
|
+
this.log('debug', 'attribute change', name, oldValue, newValue);
|
|
238
|
+
if (name === 'aspect-ratio')
|
|
239
|
+
this.updateAspectRatioHeight();
|
|
240
|
+
if (name === 'src')
|
|
241
|
+
this.updateSrc();
|
|
242
|
+
if (name === 'height')
|
|
243
|
+
this.updateStyle();
|
|
244
|
+
if (name === 'sync-params')
|
|
245
|
+
this.updateSrc();
|
|
246
|
+
if (name === 'sync-path')
|
|
247
|
+
this.updateSrc();
|
|
248
|
+
}
|
|
249
|
+
}
|
package/src/d-frame.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/d-frame.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type InitChildMessage = ['df-child', 'init'];
|
|
2
|
+
export type HeightMessage = ['df-child', 'height', number];
|
|
3
|
+
export type StateChangeMessage = ['df-child', 'stateChange', 'replace' | 'push', string];
|
|
4
|
+
export type ChildMessage = InitChildMessage | HeightMessage | StateChangeMessage;
|
|
5
|
+
export declare function isInitChildMessage(message: any[]): message is InitChildMessage;
|
|
6
|
+
export declare function isHeightMessage(message: any[]): message is HeightMessage;
|
|
7
|
+
export declare function isStateChangeMessage(message: any[]): message is StateChangeMessage;
|
|
8
|
+
export type InitParentMessage = ['df-parent', 'init', {
|
|
9
|
+
debug?: boolean;
|
|
10
|
+
resize: 'yes' | 'no' | 'auto';
|
|
11
|
+
syncParams: boolean;
|
|
12
|
+
syncPath: boolean;
|
|
13
|
+
}];
|
|
14
|
+
export type UpdateSrcMessage = ['df-parent', 'updateSrc', string];
|
|
15
|
+
export type ParentMessage = InitParentMessage | UpdateSrcMessage;
|
|
16
|
+
export declare function isInitParentMessage(message: any[]): message is InitParentMessage;
|
|
17
|
+
export declare function isUpdateSrcMessage(message: any[]): message is UpdateSrcMessage;
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isInitChildMessage(message) {
|
|
2
|
+
return message[0] === 'df-child' && message[1] === 'init';
|
|
3
|
+
}
|
|
4
|
+
export function isHeightMessage(message) {
|
|
5
|
+
return message[0] === 'df-child' && message[1] === 'height';
|
|
6
|
+
}
|
|
7
|
+
export function isStateChangeMessage(message) {
|
|
8
|
+
return message[0] === 'df-child' && message[1] === 'stateChange';
|
|
9
|
+
}
|
|
10
|
+
export function isInitParentMessage(message) {
|
|
11
|
+
return message[0] === 'df-parent' && message[1] === 'init';
|
|
12
|
+
}
|
|
13
|
+
export function isUpdateSrcMessage(message) {
|
|
14
|
+
return message[0] === 'df-parent' && message[1] === 'updateSrc';
|
|
15
|
+
}
|