@gjsify/iframe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ import { EventTarget } from '@gjsify/dom-events';
2
+ import type { MessageBridge } from './message-bridge.js';
3
+ /**
4
+ * Lightweight Window-like proxy returned by `HTMLIFrameElement.contentWindow`.
5
+ *
6
+ * Supports the subset of the Window API needed for cross-origin iframe communication:
7
+ * - `postMessage()` for sending messages to the iframe content
8
+ * - `addEventListener('message', ...)` for receiving messages from the iframe content
9
+ * - `location` (read-only) reflecting the current URI
10
+ * - `parent` reference to the host window
11
+ * - `closed` status
12
+ *
13
+ * This is intentionally NOT a full BrowserWindow — just enough for standard
14
+ * postMessage-based communication patterns.
15
+ */
16
+ export declare class IFrameWindowProxy extends EventTarget {
17
+ private _bridge;
18
+ private _closed;
19
+ constructor(bridge: MessageBridge);
20
+ /**
21
+ * Send a message to the iframe content.
22
+ *
23
+ * @param message - Data to send (must be JSON-serializable)
24
+ * @param targetOrigin - Target origin for the message. Default: '*'
25
+ */
26
+ postMessage(message: unknown, targetOrigin?: string): void;
27
+ /**
28
+ * Read-only location reflecting the current WebView URI.
29
+ */
30
+ get location(): {
31
+ href: string;
32
+ origin: string;
33
+ };
34
+ /**
35
+ * Reference to the host (parent) window — in GJS this is globalThis.
36
+ */
37
+ get parent(): typeof globalThis;
38
+ /**
39
+ * Reference to the top-level window — in GJS this is globalThis.
40
+ */
41
+ get top(): typeof globalThis;
42
+ /**
43
+ * The window itself (self-reference per spec).
44
+ */
45
+ get self(): IFrameWindowProxy;
46
+ get window(): IFrameWindowProxy;
47
+ get closed(): boolean;
48
+ /** @internal Mark as closed when the WebView is destroyed */
49
+ _close(): void;
50
+ get [Symbol.toStringTag](): string;
51
+ }
@@ -0,0 +1,5 @@
1
+ export { HTMLIFrameElement } from './html-iframe-element.js';
2
+ export { IFrameWidget } from './iframe-widget.js';
3
+ export { IFrameWindowProxy } from './iframe-window-proxy.js';
4
+ export { MessageBridge } from './message-bridge.js';
5
+ export type { IFrameWidgetOptions, IFrameReadyCallback, IFrameMessageData } from './types/index.js';
@@ -0,0 +1,47 @@
1
+ import WebKit from 'gi://WebKit?version=6.0';
2
+ import type { IFrameWindowProxy } from './iframe-window-proxy.js';
3
+ /**
4
+ * Manages bidirectional postMessage communication between GJS and a WebKit.WebView.
5
+ *
6
+ * Direction 1 — GJS → WebView:
7
+ * Uses webView.evaluate_javascript() to dispatch a MessageEvent on the WebView's window.
8
+ *
9
+ * Direction 2 — WebView → GJS:
10
+ * Bootstrap script overrides window.parent.postMessage to call
11
+ * webkit.messageHandlers[CHANNEL_NAME].postMessage(), which triggers
12
+ * the UserContentManager 'script-message-received' signal in GJS.
13
+ */
14
+ export declare class MessageBridge {
15
+ private _webView;
16
+ private _userContentManager;
17
+ private _windowProxy;
18
+ private _currentUri;
19
+ private _signalId;
20
+ constructor(webView: WebKit.WebView);
21
+ /** Connect the IFrameWindowProxy that will receive messages from the WebView */
22
+ setWindowProxy(proxy: IFrameWindowProxy): void;
23
+ /** Update current URI (called by IFrameWidget on load-changed) */
24
+ updateUri(uri: string): void;
25
+ /** Get current location info for the IFrameWindowProxy */
26
+ getLocation(): {
27
+ href: string;
28
+ origin: string;
29
+ };
30
+ /**
31
+ * Send a message from GJS to the WebView content.
32
+ * Dispatches a standard MessageEvent on the WebView's window object.
33
+ */
34
+ sendToWebView(data: unknown, _targetOrigin: string): void;
35
+ /** Clean up signal handlers */
36
+ destroy(): void;
37
+ /**
38
+ * Set up the receiver for messages coming from the WebView.
39
+ * Registers a script message handler and connects to the signal.
40
+ */
41
+ private _setupReceiver;
42
+ /**
43
+ * Inject the bootstrap script into the WebView so that
44
+ * window.parent.postMessage() bridges back to GJS.
45
+ */
46
+ private _injectBootstrapScript;
47
+ }
@@ -0,0 +1,3 @@
1
+ export declare const iframeWidget: unique symbol;
2
+ export declare const windowProxy: unique symbol;
3
+ export declare const loaded: unique symbol;
@@ -0,0 +1,15 @@
1
+ /** Options passed to IFrameWidget constructor */
2
+ export interface IFrameWidgetOptions {
3
+ /** Enable developer extras (Web Inspector). Default: true */
4
+ enableDeveloperExtras?: boolean;
5
+ /** Enable JavaScript execution in the WebView. Default: true */
6
+ enableJavascript?: boolean;
7
+ }
8
+ /** Data structure for messages crossing the GJS/WebView boundary */
9
+ export interface IFrameMessageData {
10
+ data: unknown;
11
+ targetOrigin: string;
12
+ origin: string;
13
+ }
14
+ /** Callback for when the IFrameWidget is ready */
15
+ export type IFrameReadyCallback = (iframe: globalThis.HTMLIFrameElement) => void;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@gjsify/iframe",
3
+ "version": "0.1.0",
4
+ "description": "HTMLIFrameElement for GJS, backed by WebKit.WebView",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "clear": "rm -rf lib tmp tsconfig.tsbuildinfo test.gjs.mjs || exit 0",
16
+ "check": "tsc --noEmit",
17
+ "build": "yarn build:gjsify && yarn build:types",
18
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
19
+ "build:types": "tsc",
20
+ "build:test": "yarn build:test:gjs",
21
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
22
+ "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
23
+ "test:gjs": "gjs -m test.gjs.mjs"
24
+ },
25
+ "keywords": [
26
+ "gjs",
27
+ "iframe",
28
+ "webview",
29
+ "webkit",
30
+ "dom"
31
+ ],
32
+ "dependencies": {
33
+ "@girs/gjs": "^4.0.0-beta.42",
34
+ "@girs/gtk-4.0": "^4.22.1-4.0.0-beta.42",
35
+ "@girs/javascriptcore-6.0": "2.51.93-4.0.0-beta.42",
36
+ "@girs/webkit-6.0": "2.51.93-4.0.0-beta.42",
37
+ "@gjsify/dom-elements": "^0.1.0",
38
+ "@gjsify/dom-events": "^0.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@gjsify/cli": "^0.1.0",
42
+ "@gjsify/unit": "^0.1.0",
43
+ "@types/node": "^25.5.0",
44
+ "typescript": "^6.0.2"
45
+ }
46
+ }
@@ -0,0 +1,177 @@
1
+ // HTMLIFrameElement for GJS — original implementation using WebKit.WebView
2
+ // Reference: refs/happy-dom/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts
3
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement
4
+
5
+ import { HTMLElement, PropertySymbol } from '@gjsify/dom-elements';
6
+ import { Event } from '@gjsify/dom-events';
7
+ import * as PS from './property-symbol.js';
8
+
9
+ import type { IFrameWindowProxy } from './iframe-window-proxy.js';
10
+
11
+ const { tagName, localName, namespaceURI } = PropertySymbol;
12
+
13
+ /**
14
+ * HTML IFrame Element.
15
+ *
16
+ * Backed by WebKit.WebView when connected to an IFrameWidget.
17
+ * Without a backing widget, behaves as a pure DOM element with attribute storage.
18
+ *
19
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement
20
+ */
21
+ export class HTMLIFrameElement extends HTMLElement {
22
+ /** @internal The backing IFrameWidget (set by IFrameWidget when it creates this element) */
23
+ [PS.iframeWidget]: import('./iframe-widget.js').IFrameWidget | null = null;
24
+
25
+ /** @internal The contentWindow proxy */
26
+ [PS.windowProxy]: IFrameWindowProxy | null = null;
27
+
28
+ /** @internal Whether content has been loaded */
29
+ [PS.loaded]: boolean = false;
30
+
31
+ constructor() {
32
+ super();
33
+ this[tagName] = 'IFRAME';
34
+ this[localName] = 'iframe';
35
+ this[namespaceURI] = 'http://www.w3.org/1999/xhtml';
36
+ }
37
+
38
+ // -- Attribute-backed string properties --
39
+
40
+ get src(): string {
41
+ return this.getAttribute('src') ?? '';
42
+ }
43
+
44
+ set src(value: string) {
45
+ const old = this.getAttribute('src');
46
+ this.setAttribute('src', value);
47
+ if (value !== old && value && this[PS.iframeWidget]) {
48
+ this[PS.iframeWidget].loadUri(value);
49
+ }
50
+ }
51
+
52
+ get srcdoc(): string {
53
+ return this.getAttribute('srcdoc') ?? '';
54
+ }
55
+
56
+ set srcdoc(value: string) {
57
+ const old = this.getAttribute('srcdoc');
58
+ this.setAttribute('srcdoc', value);
59
+ if (value !== old && value && this[PS.iframeWidget]) {
60
+ this[PS.iframeWidget].loadHtml(value);
61
+ }
62
+ }
63
+
64
+ get name(): string {
65
+ return this.getAttribute('name') ?? '';
66
+ }
67
+
68
+ set name(value: string) {
69
+ this.setAttribute('name', value);
70
+ }
71
+
72
+ get sandbox(): string {
73
+ return this.getAttribute('sandbox') ?? '';
74
+ }
75
+
76
+ set sandbox(value: string) {
77
+ this.setAttribute('sandbox', value);
78
+ }
79
+
80
+ get allow(): string {
81
+ return this.getAttribute('allow') ?? '';
82
+ }
83
+
84
+ set allow(value: string) {
85
+ this.setAttribute('allow', value);
86
+ }
87
+
88
+ get referrerPolicy(): string {
89
+ return this.getAttribute('referrerpolicy') ?? '';
90
+ }
91
+
92
+ set referrerPolicy(value: string) {
93
+ this.setAttribute('referrerpolicy', value);
94
+ }
95
+
96
+ get loading(): string {
97
+ const value = this.getAttribute('loading');
98
+ if (value === 'lazy' || value === 'eager') return value;
99
+ return 'eager';
100
+ }
101
+
102
+ set loading(value: string) {
103
+ this.setAttribute('loading', value);
104
+ }
105
+
106
+ // -- Attribute-backed string properties (width/height are strings per spec) --
107
+
108
+ get width(): string {
109
+ return this.getAttribute('width') ?? '';
110
+ }
111
+
112
+ set width(value: string) {
113
+ this.setAttribute('width', value);
114
+ }
115
+
116
+ get height(): string {
117
+ return this.getAttribute('height') ?? '';
118
+ }
119
+
120
+ set height(value: string) {
121
+ this.setAttribute('height', value);
122
+ }
123
+
124
+ // -- Content access --
125
+
126
+ /**
127
+ * Returns the window proxy for the iframe's content.
128
+ * Available after the IFrameWidget has loaded content.
129
+ */
130
+ get contentWindow(): IFrameWindowProxy | null {
131
+ return this[PS.windowProxy];
132
+ }
133
+
134
+ /**
135
+ * Always returns null — cross-context boundary.
136
+ * The WebView content runs in a separate process; direct document access
137
+ * is not feasible. Use postMessage() for communication.
138
+ */
139
+ get contentDocument(): null {
140
+ return null;
141
+ }
142
+
143
+ // -- Methods --
144
+
145
+ /**
146
+ * Returns a promise that resolves to the iframe's src URL.
147
+ */
148
+ getSVGDocument(): null {
149
+ return null;
150
+ }
151
+
152
+ cloneNode(deep = false): HTMLIFrameElement {
153
+ const clone = super.cloneNode(deep) as HTMLIFrameElement;
154
+ // Cloned iframes are not connected to any widget
155
+ clone[PS.iframeWidget] = null;
156
+ clone[PS.windowProxy] = null;
157
+ clone[PS.loaded] = false;
158
+ return clone;
159
+ }
160
+
161
+ get [Symbol.toStringTag](): string {
162
+ return 'HTMLIFrameElement';
163
+ }
164
+
165
+ // -- Internal: called by IFrameWidget --
166
+
167
+ /** @internal Fire load event */
168
+ _onLoad(): void {
169
+ this[PS.loaded] = true;
170
+ this.dispatchEvent(new Event('load'));
171
+ }
172
+
173
+ /** @internal Fire error event */
174
+ _onError(): void {
175
+ this.dispatchEvent(new Event('error'));
176
+ }
177
+ }
@@ -0,0 +1,158 @@
1
+ // IFrameWidget GTK widget for GJS — original implementation using WebKit.WebView
2
+ // Provides a WebKit.WebView subclass that bundles all iframe bootstrapping.
3
+ // Pattern follows packages/dom/webgl/src/ts/webgl-area.ts (WebGLArea)
4
+
5
+ import GObject from 'gi://GObject';
6
+ import WebKit from 'gi://WebKit?version=6.0';
7
+
8
+ import { HTMLIFrameElement } from './html-iframe-element.js';
9
+ import { IFrameWindowProxy } from './iframe-window-proxy.js';
10
+ import { MessageBridge } from './message-bridge.js';
11
+ import * as PS from './property-symbol.js';
12
+
13
+ import type { IFrameWidgetOptions, IFrameReadyCallback } from './types/index.js';
14
+
15
+ /**
16
+ * A `WebKit.WebView` subclass that handles iframe bootstrapping:
17
+ * - Sets up WebKit settings (JavaScript, developer extras)
18
+ * - Creates an `HTMLIFrameElement` wrapping this WebView
19
+ * - Sets up postMessage bridge for GJS ↔ WebView communication
20
+ * - Fires `onReady()` callbacks with the iframe element once loaded
21
+ * - `installGlobals()` sets `globalThis.HTMLIFrameElement`
22
+ *
23
+ * Usage:
24
+ * ```ts
25
+ * const iframeWidget = new IFrameWidget();
26
+ * iframeWidget.installGlobals();
27
+ * iframeWidget.onReady((iframe) => {
28
+ * iframe.contentWindow?.addEventListener('message', (e) => {
29
+ * console.log('Message from iframe:', e.data);
30
+ * });
31
+ * });
32
+ * iframeWidget.iframeElement.src = 'https://example.com';
33
+ * window.set_child(iframeWidget);
34
+ * ```
35
+ */
36
+ export const IFrameWidget = GObject.registerClass(
37
+ { GTypeName: 'GjsifyIFrameWidget' },
38
+ class IFrameWidget extends WebKit.WebView {
39
+ _iframe: HTMLIFrameElement;
40
+ _messageBridge: MessageBridge;
41
+ _readyCallbacks: IFrameReadyCallback[] = [];
42
+ _options: IFrameWidgetOptions;
43
+
44
+ constructor(options?: IFrameWidgetOptions & Partial<WebKit.WebView.ConstructorProps>) {
45
+ const { enableDeveloperExtras, enableJavascript, ...webViewProps } = options ?? {};
46
+
47
+ const userContentManager = new WebKit.UserContentManager();
48
+ const settings = new WebKit.Settings();
49
+ settings.enable_javascript = enableJavascript ?? true;
50
+ settings.enable_developer_extras = enableDeveloperExtras ?? true;
51
+
52
+ super({
53
+ ...webViewProps,
54
+ user_content_manager: userContentManager,
55
+ settings,
56
+ });
57
+
58
+ this._options = { enableDeveloperExtras, enableJavascript };
59
+
60
+ // Create the DOM element and link it to this widget
61
+ this._iframe = new HTMLIFrameElement();
62
+ this._iframe[PS.iframeWidget] = this as unknown as import('./iframe-widget.js').IFrameWidget;
63
+
64
+ // Set up the message bridge
65
+ this._messageBridge = new MessageBridge(this);
66
+
67
+ // Create the window proxy and connect it
68
+ const windowProxy = new IFrameWindowProxy(this._messageBridge);
69
+ this._iframe[PS.windowProxy] = windowProxy;
70
+ this._messageBridge.setWindowProxy(windowProxy);
71
+
72
+ // Track load state
73
+ this.connect('load-changed', (_webView: WebKit.WebView, event: WebKit.LoadEvent) => {
74
+ switch (event) {
75
+ case WebKit.LoadEvent.COMMITTED: {
76
+ const uri = this.get_uri();
77
+ if (uri) this._messageBridge.updateUri(uri);
78
+ break;
79
+ }
80
+ case WebKit.LoadEvent.FINISHED:
81
+ this._iframe._onLoad();
82
+ for (const cb of this._readyCallbacks) {
83
+ cb(this._iframe as unknown as globalThis.HTMLIFrameElement);
84
+ }
85
+ this._readyCallbacks = [];
86
+ break;
87
+ }
88
+ });
89
+
90
+ this.connect('load-failed', () => {
91
+ this._iframe._onError();
92
+ return false;
93
+ });
94
+
95
+ this.connect('unrealize', () => {
96
+ this._messageBridge.destroy();
97
+ const proxy = this._iframe[PS.windowProxy];
98
+ if (proxy) {
99
+ proxy._close();
100
+ }
101
+ this._iframe[PS.iframeWidget] = null;
102
+ this._iframe[PS.windowProxy] = null;
103
+ });
104
+ }
105
+
106
+ /** The HTMLIFrameElement wrapping this WebView. */
107
+ get iframeElement(): HTMLIFrameElement {
108
+ return this._iframe;
109
+ }
110
+
111
+ /**
112
+ * Register a callback to be invoked when content has loaded.
113
+ * If content is already loaded, the callback fires on next load.
114
+ */
115
+ onReady(cb: IFrameReadyCallback): void {
116
+ this._readyCallbacks.push(cb);
117
+ }
118
+
119
+ /**
120
+ * Load a URI into the WebView.
121
+ * Also updates the iframe element's src attribute.
122
+ */
123
+ loadUri(uri: string): void {
124
+ this._iframe.setAttribute('src', uri);
125
+ this.load_uri(uri);
126
+ }
127
+
128
+ /**
129
+ * Load inline HTML into the WebView.
130
+ * Also updates the iframe element's srcdoc attribute.
131
+ */
132
+ loadHtml(html: string, baseUri?: string): void {
133
+ this._iframe.setAttribute('srcdoc', html);
134
+ this.load_html(html, baseUri ?? 'about:srcdoc');
135
+ }
136
+
137
+ /**
138
+ * Send a message to the WebView content via the standard postMessage API.
139
+ * Equivalent to `this.iframeElement.contentWindow.postMessage(message, targetOrigin)`.
140
+ */
141
+ postMessage(message: unknown, targetOrigin = '*'): void {
142
+ this._messageBridge.sendToWebView(message, targetOrigin);
143
+ }
144
+
145
+ /**
146
+ * Set `globalThis.HTMLIFrameElement` to the gjsify implementation.
147
+ */
148
+ installGlobals(): void {
149
+ Object.defineProperty(globalThis, 'HTMLIFrameElement', {
150
+ value: HTMLIFrameElement,
151
+ writable: true,
152
+ configurable: true,
153
+ });
154
+ }
155
+ },
156
+ );
157
+
158
+ export type IFrameWidget = InstanceType<typeof IFrameWidget>;
@@ -0,0 +1,86 @@
1
+ // IFrameWindowProxy for GJS — lightweight Window proxy for iframe contentWindow
2
+ // Reference: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
3
+ // Reference: refs/happy-dom/packages/happy-dom/src/window/CrossOriginBrowserWindow.ts
4
+
5
+ import { EventTarget } from '@gjsify/dom-events';
6
+
7
+ import type { MessageBridge } from './message-bridge.js';
8
+
9
+ /**
10
+ * Lightweight Window-like proxy returned by `HTMLIFrameElement.contentWindow`.
11
+ *
12
+ * Supports the subset of the Window API needed for cross-origin iframe communication:
13
+ * - `postMessage()` for sending messages to the iframe content
14
+ * - `addEventListener('message', ...)` for receiving messages from the iframe content
15
+ * - `location` (read-only) reflecting the current URI
16
+ * - `parent` reference to the host window
17
+ * - `closed` status
18
+ *
19
+ * This is intentionally NOT a full BrowserWindow — just enough for standard
20
+ * postMessage-based communication patterns.
21
+ */
22
+ export class IFrameWindowProxy extends EventTarget {
23
+ private _bridge: MessageBridge;
24
+ private _closed = false;
25
+
26
+ constructor(bridge: MessageBridge) {
27
+ super();
28
+ this._bridge = bridge;
29
+ }
30
+
31
+ /**
32
+ * Send a message to the iframe content.
33
+ *
34
+ * @param message - Data to send (must be JSON-serializable)
35
+ * @param targetOrigin - Target origin for the message. Default: '*'
36
+ */
37
+ postMessage(message: unknown, targetOrigin = '*'): void {
38
+ if (this._closed) return;
39
+ this._bridge.sendToWebView(message, targetOrigin);
40
+ }
41
+
42
+ /**
43
+ * Read-only location reflecting the current WebView URI.
44
+ */
45
+ get location(): { href: string; origin: string } {
46
+ return this._bridge.getLocation();
47
+ }
48
+
49
+ /**
50
+ * Reference to the host (parent) window — in GJS this is globalThis.
51
+ */
52
+ get parent(): typeof globalThis {
53
+ return globalThis;
54
+ }
55
+
56
+ /**
57
+ * Reference to the top-level window — in GJS this is globalThis.
58
+ */
59
+ get top(): typeof globalThis {
60
+ return globalThis;
61
+ }
62
+
63
+ /**
64
+ * The window itself (self-reference per spec).
65
+ */
66
+ get self(): IFrameWindowProxy {
67
+ return this;
68
+ }
69
+
70
+ get window(): IFrameWindowProxy {
71
+ return this;
72
+ }
73
+
74
+ get closed(): boolean {
75
+ return this._closed;
76
+ }
77
+
78
+ /** @internal Mark as closed when the WebView is destroyed */
79
+ _close(): void {
80
+ this._closed = true;
81
+ }
82
+
83
+ get [Symbol.toStringTag](): string {
84
+ return 'IFrameWindowProxy';
85
+ }
86
+ }