@octane-xplat/macos-renderer 0.0.1 → 0.10.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/src/webview.ts ADDED
@@ -0,0 +1,252 @@
1
+ /** Embedded document surface; deliberately has no app-shell service bridge. */
2
+ // The minimal AppKit host links AppKit, so load WebKit before resolving classes.
3
+ const webKit = dlopen('/System/Library/Frameworks/WebKit.framework/WebKit', 2)
4
+ if (!webKit) {
5
+ throw new Error('Unable to load the system WebKit framework')
6
+ }
7
+
8
+ interface WebViewState {
9
+ props: Record<string, any>
10
+ generation: number
11
+ loaded: boolean
12
+ navigation: any
13
+ delegate: any
14
+ inline?: boolean
15
+ source?: any
16
+ emit(name: string, payload: any): void
17
+ measure(): void
18
+ }
19
+
20
+ const states = new WeakMap<object, WebViewState>()
21
+
22
+ const EmbeddedNavigationDelegate = NSObject.extend(
23
+ {
24
+ 'webView:didStartProvisionalNavigation:'(view: WKWebView, navigation: any) {
25
+ const state = states.get(view)
26
+ if (!state) {
27
+ return
28
+ }
29
+
30
+ state.navigation = navigation
31
+ state.generation++
32
+ state.loaded = false
33
+ },
34
+
35
+ 'webView:didFinishNavigation:'(view: WKWebView, navigation: any) {
36
+ const state = states.get(view)
37
+ if (!state || state.navigation !== navigation) {
38
+ return
39
+ }
40
+
41
+ state.loaded = true
42
+ state.measure()
43
+ state.emit('onLoad', { url: state.inline ? undefined : view.URL?.absoluteString })
44
+ },
45
+
46
+ 'webView:didFailProvisionalNavigation:withError:'(
47
+ view: WKWebView,
48
+ navigation: any,
49
+ error: NSError,
50
+ ) {
51
+ ;(this as any)['webView:didFailNavigation:withError:'](view, navigation, error)
52
+ },
53
+
54
+ 'webView:didFailNavigation:withError:'(view: WKWebView, navigation: any, error: NSError) {
55
+ const state = states.get(view)
56
+ if (!state || state.navigation !== navigation) {
57
+ return
58
+ }
59
+
60
+ state.loaded = false
61
+ state.emit('onError', {
62
+ url: state.inline
63
+ ? undefined
64
+ : (error.userInfo?.objectForKey?.(NSURLErrorFailingURLErrorKey)?.absoluteString ??
65
+ state.props.src ??
66
+ view.URL?.absoluteString),
67
+ error: String(error.localizedDescription ?? error),
68
+ })
69
+ },
70
+
71
+ 'webViewWebContentProcessDidTerminate:'(view: WKWebView) {
72
+ const state = states.get(view)
73
+ if (!state) {
74
+ return
75
+ }
76
+
77
+ state.loaded = false
78
+ state.generation++
79
+ state.emit('onError', {
80
+ url: state.inline ? undefined : view.URL?.absoluteString,
81
+ error: 'Web content process terminated',
82
+ })
83
+ },
84
+ },
85
+ {
86
+ name: 'XplatEmbeddedNavigationDelegate',
87
+ protocols: [WKNavigationDelegate],
88
+ exposedMethods: {
89
+ 'webView:didStartProvisionalNavigation:': {
90
+ params: [WKWebView, NSObject],
91
+ returns: interop.types.void,
92
+ },
93
+ 'webView:didFinishNavigation:': {
94
+ params: [WKWebView, NSObject],
95
+ returns: interop.types.void,
96
+ },
97
+ 'webView:didFailProvisionalNavigation:withError:': {
98
+ params: [WKWebView, NSObject, NSError],
99
+ returns: interop.types.void,
100
+ },
101
+ 'webView:didFailNavigation:withError:': {
102
+ params: [WKWebView, NSObject, NSError],
103
+ returns: interop.types.void,
104
+ },
105
+ 'webViewWebContentProcessDidTerminate:': { params: [WKWebView], returns: interop.types.void },
106
+ },
107
+ },
108
+ )
109
+
110
+ const scrollScript = (enabled: any) => `(() => {
111
+ const id = '__octane_xplat_scroll';
112
+ document.getElementById(id)?.remove();
113
+ if (${enabled === false}) {
114
+ const style = document.createElement('style');
115
+ style.id = id;
116
+ style.textContent = 'html,body { overflow: hidden !important; }';
117
+ (document.head || document.documentElement).appendChild(style);
118
+ }
119
+ })()`
120
+
121
+ const measureScript = `(() => {
122
+ const body = document.body, root = document.documentElement;
123
+ if (!body || !root) return null;
124
+ return JSON.stringify({width: Math.max(body.scrollWidth, root.scrollWidth), height: Math.max(body.scrollHeight, root.scrollHeight)});
125
+ })()`
126
+
127
+ export function makeWebView(): WKWebView {
128
+ const view = WKWebView.alloc().initWithFrameConfiguration(
129
+ { origin: { x: 0, y: 0 }, size: { width: 320, height: 150 } },
130
+ WKWebViewConfiguration.new(),
131
+ )
132
+
133
+ view.translatesAutoresizingMaskIntoConstraints = false
134
+ return view
135
+ }
136
+
137
+ export function updateWebView(
138
+ node: import('./index').ElementNode,
139
+ changed: Record<string, any>,
140
+ resize: (height: number) => void,
141
+ ) {
142
+ const view = node.view as WKWebView
143
+ let state = states.get(view)
144
+ if (!state) {
145
+ // `created` is a const so the closures below keep their declared type.
146
+ const created: WebViewState = {
147
+ props: node.props,
148
+ generation: 0,
149
+ loaded: false,
150
+ navigation: null,
151
+ delegate: EmbeddedNavigationDelegate.alloc().init(),
152
+ emit(name: string, payload: any) {
153
+ const handler = created.props[name]
154
+ if (handler) {
155
+ try {
156
+ node.container.root!.eventScope('discrete', () => handler(payload))
157
+ } catch (error) {
158
+ console.error('[macos-event] WebView handler failed', error)
159
+ }
160
+ }
161
+ },
162
+ measure() {
163
+ const generation = created.generation
164
+ view.evaluateJavaScriptCompletionHandler(scrollScript(created.props.scrollEnabled), null)
165
+ if (!created.props.matchContents && !created.props.onLayoutContent) {
166
+ return
167
+ }
168
+
169
+ view.evaluateJavaScriptCompletionHandler(measureScript, (result: any, error: any) => {
170
+ if (
171
+ states.get(view) !== created ||
172
+ generation !== created.generation ||
173
+ error ||
174
+ !result
175
+ ) {
176
+ return
177
+ }
178
+
179
+ let size: any
180
+ try {
181
+ size = JSON.parse(String(result))
182
+ } catch {
183
+ return
184
+ }
185
+
186
+ if (
187
+ !size ||
188
+ !Number.isFinite(size.width) ||
189
+ !Number.isFinite(size.height) ||
190
+ size.width <= 0 ||
191
+ size.height <= 0
192
+ ) {
193
+ return
194
+ }
195
+
196
+ if (created.props.matchContents) {
197
+ resize(size.height)
198
+ }
199
+
200
+ created.emit('onLayoutContent', size)
201
+ })
202
+ },
203
+ }
204
+
205
+ states.set(view, created) // WKWebView holds its delegate weakly.
206
+ view.navigationDelegate = created.delegate
207
+ state = created
208
+ }
209
+
210
+ state.props = node.props
211
+ const inline = state.props.html !== undefined
212
+ const source = inline ? state.props.html : state.props.src
213
+ if (state.inline !== inline || state.source !== source) {
214
+ state.inline = inline
215
+ state.source = source
216
+ state.loaded = false
217
+ state.generation++
218
+ state.navigation = null
219
+ view.stopLoading()
220
+ if (inline || !source) {
221
+ state.navigation = view.loadHTMLStringBaseURL(String(source ?? ''), null)
222
+ } else {
223
+ const src = String(source)
224
+ const path = src.startsWith('~/')
225
+ ? NSBundle.mainBundle.resourcePath + '/' + src.slice(2)
226
+ : src
227
+
228
+ const url = path.startsWith('/') ? NSURL.fileURLWithPath(path) : NSURL.URLWithString(path)
229
+ if (!url) {
230
+ state.emit('onError', { url: src, error: 'Invalid document URL' })
231
+ } else if (url.isFileURL) {
232
+ state.navigation = view.loadFileURLAllowingReadAccessToURL(
233
+ url,
234
+ url.URLByDeletingLastPathComponent,
235
+ )
236
+ } else {
237
+ state.navigation = view.loadRequest(NSURLRequest.requestWithURL(url))
238
+ }
239
+ }
240
+ } else if (
241
+ state.loaded &&
242
+ ['scrollEnabled', 'matchContents', 'onLayoutContent'].some((name) => name in changed)
243
+ ) {
244
+ state.measure()
245
+ }
246
+ }
247
+
248
+ export function disposeWebView(view: WKWebView) {
249
+ states.delete(view)
250
+ view.navigationDelegate = null
251
+ view.stopLoading()
252
+ }