@cesarechazu/directus-extension-dynamic-iframe 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.js +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cesar
|
|
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,87 @@
|
|
|
1
|
+
# Dynamic iFrame
|
|
2
|
+
|
|
3
|
+
Embed a secure external page inside a Directus alias field using a URL template.
|
|
4
|
+
|
|
5
|
+
This interface is useful for showing dashboards, previews, reports, or external tools directly inside the item editor, based on the current form values.
|
|
6
|
+
|
|
7
|
+
This pattern is especially useful for embedded analytics and reporting dashboards, where the URL needs to change according to the record currently being edited.
|
|
8
|
+
|
|
9
|
+
## Interface
|
|
10
|
+
|
|
11
|
+
<img
|
|
12
|
+
src="https://raw.githubusercontent.com/cesarechazu/directus-extension-dynamic-iframe/main/img/interface.png"
|
|
13
|
+
alt="Dynamic iFrame interface"
|
|
14
|
+
width="720"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- Renders an external page inside an iframe
|
|
20
|
+
- Resolves placeholders from the current form values
|
|
21
|
+
- Updates the iframe URL when referenced form values change
|
|
22
|
+
- Supports configurable debounce before reloading
|
|
23
|
+
- Optional loading indicator
|
|
24
|
+
- Optional open in new tab shortcut
|
|
25
|
+
- Optional fullscreen permission
|
|
26
|
+
- Supports custom height
|
|
27
|
+
- Supports custom border styling
|
|
28
|
+
- Supports custom border radius
|
|
29
|
+
- **Accepts only `https` URLs**
|
|
30
|
+
- Shows a placeholder when the URL is missing or invalid
|
|
31
|
+
- Works as an `alias` field, so it does not create a database column
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
- `url_template`: target page template, for example `https://example.com?client={{client_id}}`
|
|
36
|
+
- `height`: iframe height, for example `800px` (default `800px`)
|
|
37
|
+
- `debounce`: delay in milliseconds before updating the iframe URL, default `250`
|
|
38
|
+
- `loading`: show a visual loading indicator while the iframe reloads, default `true`
|
|
39
|
+
- `border`: any valid CSS border value
|
|
40
|
+
- `border_radius`: any valid CSS border radius value
|
|
41
|
+
- `open_in_new_tab`: show a shortcut to open the resolved URL in a new tab, default `false`
|
|
42
|
+
- `allow_fullscreen`: enables fullscreen support on the iframe, default `false`
|
|
43
|
+
|
|
44
|
+
## Dynamic Placeholders
|
|
45
|
+
|
|
46
|
+
The `url_template` can use values from other fields in the current item form.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
|
|
50
|
+
- `https://example.com/dashboard?client={{client_id}}`
|
|
51
|
+
- `https://example.com/report/{{id}}`
|
|
52
|
+
- `https://example.com/embed?customer={{customer.id}}`
|
|
53
|
+
- `https://example.com/embed?customer={{customer.id}}&name={{customer.name}}`
|
|
54
|
+
|
|
55
|
+
Supported placeholder formats:
|
|
56
|
+
|
|
57
|
+
- `{{field_name}}`: reads a top-level field from the current form
|
|
58
|
+
- `{{relation.id}}`: reads nested values using dot notation
|
|
59
|
+
- `{{id}}`: resolves the current item primary key
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## Usage Example
|
|
63
|
+
|
|
64
|
+
One practical use case is a singleton collection used as a control panel or embedded reporting view.
|
|
65
|
+
|
|
66
|
+
Example setup:
|
|
67
|
+
|
|
68
|
+
- Create a singleton collection
|
|
69
|
+
- Add a many-to-one field such as `customer_id` related to a `customers` collection
|
|
70
|
+
- Add a second field using the `Dynamic iFrame` interface as an `alias` field
|
|
71
|
+
- Configure the iframe `url_template` like:
|
|
72
|
+
- `https://example.com/dashboard?customer={{customer_id}}`
|
|
73
|
+
|
|
74
|
+
Behavior:
|
|
75
|
+
|
|
76
|
+
- When the user changes the selected customer in the `customer_id` field
|
|
77
|
+
- Directus updates the form state immediately
|
|
78
|
+
- The iframe resolves the new `{{customer_id}}` value
|
|
79
|
+
- The iframe reloads automatically after the configured debounce
|
|
80
|
+
|
|
81
|
+
This makes it useful for filtered dashboards, contextual previews, embedded reports, or any external page that needs to react to other field values in the current form.
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- This interface only renders secure (`https`) pages.
|
|
86
|
+
- Some websites block embedding with `X-Frame-Options` or `Content-Security-Policy`.
|
|
87
|
+
- This interface works best with pages that are designed to be embedded.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineInterface as e}from"@directus/extensions-sdk";import{inject as t,ref as n,computed as a,watch as r,onBeforeUnmount as i,openBlock as l,createElementBlock as o,createElementVNode as d,createCommentVNode as u,normalizeStyle as s,createTextVNode as c}from"vue";var p=[],f=[];!function(e,t){if(e&&"undefined"!=typeof document){var n,a=!0===t.prepend?"prepend":"append",r=!0===t.singleTag,i="string"==typeof t.container?document.querySelector(t.container):document.getElementsByTagName("head")[0];if(r){var l=p.indexOf(i);-1===l&&(l=p.push(i)-1,f[l]={}),n=f[l]&&f[l][a]?f[l][a]:f[l][a]=o()}else n=o();65279===e.charCodeAt(0)&&(e=e.substring(1)),n.styleSheet?n.styleSheet.cssText+=e:n.appendChild(document.createTextNode(e))}function o(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),t.attributes)for(var n=Object.keys(t.attributes),r=0;r<n.length;r++)e.setAttribute(n[r],t.attributes[n[r]]);var l="prepend"===a?"afterbegin":"beforeend";return i.insertAdjacentElement(l,e),e}}("\n.iframe-wrapper[data-v-19686b97] {\n\tposition: relative;\n\twidth: 100%;\n}\niframe[data-v-19686b97] {\n\tborder: none;\n\tdisplay: block;\n\toverflow: auto;\n\twidth: 100%;\n\tbackground: var(--theme--background-page, #fff);\n}\n.iframe-loading[data-v-19686b97] {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tright: 0;\n\tz-index: 2;\n\theight: 3px;\n\toverflow: hidden;\n\tborder-top-left-radius: var(--theme--border-radius, 8px);\n\tborder-top-right-radius: var(--theme--border-radius, 8px);\n\tbackground: color-mix(in srgb, var(--theme--primary, #6644ff) 16%, transparent);\n}\n.iframe-loading-bar[data-v-19686b97] {\n\twidth: 35%;\n\theight: 100%;\n\tbackground: var(--theme--primary, #6644ff);\n\tanimation: iframe-loading-slide-19686b97 1s ease-in-out infinite;\n}\n.iframe-actions[data-v-19686b97] {\n\tposition: absolute;\n\ttop: 10px;\n\tright: 10px;\n\tz-index: 3;\n\tdisplay: inline-flex;\n\tgap: 8px;\n}\n.iframe-action[data-v-19686b97] {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 28px;\n\theight: 28px;\n\tborder-radius: 999px;\n\tbackground: color-mix(in srgb, var(--theme--background-page, #fff) 88%, transparent);\n\tcolor: var(--theme--foreground, #172940);\n\ttext-decoration: none;\n\tfont-size: 16px;\n\tline-height: 1;\n\tbox-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);\n}\n.iframe-action[data-v-19686b97]:hover {\n\tbackground: var(--theme--background-page, #fff);\n}\nbutton.iframe-action[data-v-19686b97] {\n\tborder: none;\n\tcursor: pointer;\n\tpadding: 0;\n}\n.iframe-placeholder[data-v-19686b97] {\n\twidth: 100%;\n\tpadding: 16px;\n\tborder: 1px dashed var(--theme--border-color-subdued, #ccc);\n\tborder-radius: var(--theme--border-radius, 8px);\n\tcolor: var(--theme--foreground-subdued, #6b7c93);\n\tfont-size: 14px;\n\tline-height: 1.5;\n}\n.iframe-placeholder code[data-v-19686b97] {\n\tpadding: 2px 6px;\n\tborder-radius: 4px;\n\tbackground: var(--theme--background-subdued, rgba(0, 0, 0, 0.05));\n\tfont-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n}\n@keyframes iframe-loading-slide-19686b97 {\n0% {\n\t\ttransform: translateX(-120%);\n}\n50% {\n\t\ttransform: translateX(120%);\n}\n100% {\n\t\ttransform: translateX(320%);\n}\n}\n",{});const m={key:0,class:"iframe-loading","aria-hidden":"true"},b={key:1,class:"iframe-actions"},v=["href"],h=["src","height","allowfullscreen","allow"],g={key:3,class:"iframe-placeholder"};var y=e({id:"dynamic-iframe",name:"Dynamic iFrame",icon:"iframe",types:["alias"],localTypes:["presentation"],group:"presentation",description:"Display a secure iframe using a URL template resolved from the current item values.",component:((e,t)=>{const n=e.__vccOpts||e;for(const[e,a]of t)n[e]=a;return n})({__name:"interface",props:{value:String,url_template:String,height:String,border:String,border_radius:String,debounce:{type:[String,Number],default:250},loading:{type:Boolean,default:!0},open_in_new_tab:{type:Boolean,default:!1},allow_fullscreen:{type:Boolean,default:!1},primaryKey:{type:[String,Number],default:null}},setup(e){const p=e,f=t("values",null),y=n(null),w=n(""),x=n(!1);let _=null;const k=a((()=>{return e=f?.value,e&&"object"==typeof e&&!Array.isArray(e)?f.value:{};var e}));const S=a((()=>{const e=p.url_template?.trim();if(!e)return"";const t={...k.value,id:p.primaryKey??k.value?.id??null};return e.replace(/{{\s*([^}]+)\s*}}/g,((e,n)=>function(e){return null==e?"":"object"==typeof e?!("id"in e)||"string"!=typeof e.id&&"number"!=typeof e.id?"":String(e.id):String(e)}(function(e,t){return String(t).split(".").reduce(((e,t)=>{if(null!=e)return e[t]}),e)}(t,n.trim()))))})),B=a((()=>{if(!S.value)return"";try{const e=new URL(S.value,window.location.origin);return"https:"!==e.protocol?"":e.toString()}catch{return""}})),T=a((()=>{const e=Number(p.debounce);return!Number.isFinite(e)||e<0?250:e})),A=a((()=>p.height?.trim()||"800px")),R=a((()=>p.border?.trim()||"1px solid var(--theme--border-color-subdued, #ccc)")),F=a((()=>p.border_radius?.trim()||"var(--theme--border-radius, 8px)")),L=a((()=>!0===p.loading&&!0===x.value)),N=a((()=>!0===p.open_in_new_tab&&Boolean(w.value))),j=a((()=>!0===p.allow_fullscreen&&Boolean(w.value))),E=a((()=>!0===N.value||!0===j.value)),O=a((()=>!0===p.allow_fullscreen||null)),q=a((()=>!0===p.allow_fullscreen?"fullscreen":null));function z(){x.value=!1}async function U(){const e=y.value;e&&("function"!=typeof e.requestFullscreen?"function"==typeof e.webkitRequestFullscreen&&e.webkitRequestFullscreen():await e.requestFullscreen())}return r(B,(e=>{if(_&&(clearTimeout(_),_=null),!e)return w.value="",void(x.value=!1);x.value=!0===p.loading,0!==T.value?_=setTimeout((()=>{w.value=e,_=null}),T.value):w.value=e}),{immediate:!0}),i((()=>{_&&clearTimeout(_)})),(e,t)=>(l(),o("div",{ref_key:"wrapperElement",ref:y,class:"iframe-wrapper"},[L.value?(l(),o("div",m,t[0]||(t[0]=[d("div",{class:"iframe-loading-bar"},null,-1)]))):u("v-if",!0),E.value?(l(),o("div",b,[N.value?(l(),o("a",{key:0,class:"iframe-action",href:w.value,target:"_blank",rel:"noopener noreferrer",title:"Open in new tab","aria-label":"Open in new tab"}," ↗ ",8,v)):u("v-if",!0),j.value?(l(),o("button",{key:1,type:"button",class:"iframe-action",title:"Enter fullscreen","aria-label":"Enter fullscreen",onClick:U}," ⛶ ")):u("v-if",!0)])):u("v-if",!0),w.value?(l(),o("iframe",{key:2,src:w.value,height:A.value,style:s({border:R.value,borderRadius:F.value}),allowfullscreen:O.value,allow:q.value,loading:"lazy",referrerpolicy:"no-referrer",onLoad:z},null,44,h)):B.value?u("v-if",!0):(l(),o("div",g,t[1]||(t[1]=[c(" Provide a valid `https` URL template. You can use placeholders like "),d("code",null,"{{ field_name }}",-1),c(". ")])))],512))}},[["__scopeId","data-v-19686b97"],["__file","interface.vue"]]),options:[{field:"url_template",type:"string",name:"URL Template",meta:{interface:"input",width:"full",options:{iconLeft:"link",placeholder:"https://example.com/dashboard?client={{client_id}}"},note:"Use placeholders like {{field_name}} or {{relation.id}} to inject current form values."}},{field:"behavior_divider",type:"alias",name:"Behavior",meta:{interface:"presentation-divider",width:"full",options:{icon:"tune",title:"Behavior"},special:["alias","no-data"]}},{field:"debounce",type:"integer",name:"Debounce (ms)",meta:{interface:"input",width:"half",note:"Delay before reloading the iframe when form values change.",options:{placeholder:"250"}},schema:{default_value:250}},{field:"loading",type:"boolean",name:"Show Loading Indicator",meta:{interface:"boolean",width:"half"},schema:{default_value:!0}},{field:"open_in_new_tab",type:"boolean",name:"Open In New Tab",meta:{interface:"boolean",width:"half"},schema:{default_value:!1}},{field:"allow_fullscreen",type:"boolean",name:"Allow Fullscreen",meta:{interface:"boolean",width:"half"},schema:{default_value:!1}},{field:"appearance_divider",type:"alias",name:"Appearance",meta:{interface:"presentation-divider",width:"full",options:{icon:"palette",title:"Appearance"},special:["alias","no-data"]}},{field:"height",type:"string",name:"Height",meta:{interface:"input",width:"half",options:{placeholder:"800px"}}},{field:"border",type:"string",name:"Border",meta:{interface:"input",width:"half",options:{placeholder:"1px solid #ccc"}}},{field:"border_radius",type:"string",name:"Border Radius",meta:{interface:"input",width:"half",options:{placeholder:"8px"}}}]});export{y as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cesarechazu/directus-extension-dynamic-iframe",
|
|
3
|
+
"description": "Embed secure pages using a URL template resolved from the current form values.",
|
|
4
|
+
"icon": "iframe",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"email": "cesarechazu@gmail.com",
|
|
9
|
+
"name": "cesar"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/cesarechazu/directus-extension-dynamic-iframe.git"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/cesarechazu/directus-extension-dynamic-iframe/issues"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/cesarechazu/directus-extension-dynamic-iframe#readme",
|
|
19
|
+
"keywords": [
|
|
20
|
+
"directus",
|
|
21
|
+
"directus-extension",
|
|
22
|
+
"directus-extension-interface",
|
|
23
|
+
"iframe",
|
|
24
|
+
"embed",
|
|
25
|
+
"template",
|
|
26
|
+
"alias"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"directus:extension": {
|
|
33
|
+
"type": "interface",
|
|
34
|
+
"path": "dist/index.js",
|
|
35
|
+
"source": "src/index.js",
|
|
36
|
+
"host": "^10.10.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "directus-extension build",
|
|
40
|
+
"dev": "directus-extension build -w --no-minify",
|
|
41
|
+
"link": "directus-extension link",
|
|
42
|
+
"validate": "directus-extension validate"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@directus/extensions-sdk": "13.0.4",
|
|
46
|
+
"vue": "^3.5.13"
|
|
47
|
+
}
|
|
48
|
+
}
|