@react-native-ohos/react-native-webview 13.16.2-beta.2 → 13.16.2

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +20 -20
  3. package/README.OpenSource +10 -10
  4. package/README.md +469 -13
  5. package/harmony/rn_webview/.gitignore +6 -0
  6. package/harmony/rn_webview/BuildProfile.ets +17 -0
  7. package/harmony/rn_webview/OAT.xml +44 -44
  8. package/harmony/rn_webview/build-profile.json5 +28 -28
  9. package/harmony/rn_webview/index.ets +12 -12
  10. package/harmony/rn_webview/oh-package-lock.json5 +29 -0
  11. package/harmony/rn_webview/oh-package.json5 +13 -13
  12. package/harmony/rn_webview/src/main/cpp/CMakeLists.txt +9 -9
  13. package/harmony/rn_webview/src/main/cpp/WebViewPackage.h +16 -16
  14. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/BaseReactNativeWebviewPackage.h +96 -96
  15. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/components/RNCWebViewJSIBinder.h +121 -120
  16. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebView.cpp +18 -18
  17. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebView.h +16 -16
  18. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebViewModule.cpp +17 -17
  19. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/turbo_modules/RNCWebViewModule.h +16 -16
  20. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ComponentDescriptors.h +24 -24
  21. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/EventEmitters.cpp +258 -258
  22. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/EventEmitters.h +277 -277
  23. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.cpp +104 -103
  24. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.h +510 -509
  25. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ShadowNodes.cpp +17 -17
  26. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/ShadowNodes.h +32 -32
  27. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/States.cpp +16 -16
  28. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/States.h +28 -28
  29. package/harmony/rn_webview/src/main/ets/Logger.ts +43 -43
  30. package/harmony/rn_webview/src/main/ets/Magic.ets +201 -198
  31. package/harmony/rn_webview/src/main/ets/RNCWebView.ets +1019 -818
  32. package/harmony/rn_webview/src/main/ets/RNCWebViewPackage.ets +37 -37
  33. package/harmony/rn_webview/src/main/ets/WebViewBaseOperate.ets +528 -514
  34. package/harmony/rn_webview/src/main/ets/WebViewTurboModule.ets +67 -63
  35. package/harmony/rn_webview/src/main/ets/generated/components/RNCWebView.ts +529 -525
  36. package/harmony/rn_webview/src/main/ets/generated/components/ts.ts +5 -5
  37. package/harmony/rn_webview/src/main/ets/generated/index.ets +5 -5
  38. package/harmony/rn_webview/src/main/ets/generated/ts.ts +6 -6
  39. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebView.ts +18 -18
  40. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebViewModule.ts +16 -16
  41. package/harmony/rn_webview/src/main/ets/generated/turboModules/ts.ts +6 -6
  42. package/harmony/rn_webview/src/main/module.json5 +10 -10
  43. package/harmony/rn_webview/src/main/resources/base/element/string.json +39 -39
  44. package/harmony/rn_webview/src/main/resources/en_US/element/string.json +39 -39
  45. package/harmony/rn_webview/src/main/resources/zh_CN/element/string.json +39 -39
  46. package/harmony/rn_webview/src/test/List.test.ets +4 -4
  47. package/harmony/rn_webview/src/test/LocalUnit.test.ets +32 -32
  48. package/harmony/rn_webview/ts.ets +8 -8
  49. package/harmony/rn_webview.har +0 -0
  50. package/lib/NativeRNCWebView.d.ts +10 -0
  51. package/lib/NativeRNCWebView.d.ts.map +1 -0
  52. package/lib/NativeRNCWebView.js +1 -0
  53. package/lib/NativeRNCWebViewModule.d.ts +9 -0
  54. package/lib/NativeRNCWebViewModule.d.ts.map +1 -0
  55. package/lib/NativeRNCWebViewModule.js +1 -0
  56. package/lib/RNCWebViewNativeComponent.d.ts +250 -0
  57. package/lib/RNCWebViewNativeComponent.d.ts.map +1 -0
  58. package/lib/RNCWebViewNativeComponent.js +1 -0
  59. package/lib/WebView.d.ts +4 -0
  60. package/lib/WebView.d.ts.map +1 -0
  61. package/lib/WebView.harmony.d.ts +16 -0
  62. package/lib/WebView.harmony.d.ts.map +1 -0
  63. package/lib/WebView.harmony.js +1 -0
  64. package/lib/WebView.js +1 -0
  65. package/lib/codegenUtils.d.ts +3 -0
  66. package/lib/codegenUtils.d.ts.map +1 -0
  67. package/lib/codegenUtils.js +0 -0
  68. package/lib/index.d.ts +4 -0
  69. package/lib/index.d.ts.map +1 -0
  70. package/lib/index.js +1 -0
  71. package/package.json +101 -101
  72. package/src/NativeRNCWebView.ts +21 -21
  73. package/src/NativeRNCWebViewModule.ts +19 -19
  74. package/src/RNCWebViewNativeComponent.ts +356 -355
  75. package/src/WebView.harmony.tsx +348 -346
  76. package/src/WebView.tsx +10 -10
  77. package/src/codegenUtils.ts +10 -10
  78. package/src/index.ts +10 -10
  79. package/harmony/rn_webview/src/main/ets/CutomReference.ts +0 -30
  80. package/harmony/rn_webview/src/main/ets/ShouldRequestUrl.ts +0 -48
@@ -1,819 +1,1020 @@
1
- // Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
2
- // Use of this source code is governed by a Apache-2.0 license that can be
3
- // found in the LICENSE file.
4
-
5
- import { RNComponentContext } from '@rnoh/react-native-openharmony';
6
- import webview from '@ohos.web.webview';
7
- import { url as OSUrl } from '@kit.ArkTS';
8
- import { RNC, TM } from './generated';
9
- import Logger from './Logger';
10
- import { BaseOperate } from './WebViewBaseOperate';
11
- import { deviceInfo ,BusinessError} from '@kit.BasicServicesKit';
12
- import {
13
- AlertEvent,
14
- CACHE_MODE,
15
- JAVASCRIPT_INTERFACE,
16
- ONE_HUNDRED,
17
- EIGHT,
18
- ResultType,
19
- RNCWebViewBridge,
20
- WebViewEventParams,
21
- WebViewNewSource,
22
- WebViewOverScrollMode,
23
- ZERO,
24
- COMMAND_NAME,
25
- WebViewDescriptor,
26
- REFRESH_OFFSET,
27
- MINHEIGHT,
28
- WebMediaCapturePermissionGrantType,
29
- MenuItem
30
- } from './Magic';
31
-
32
- import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
33
- import { WebViewTurboModule, ShouldStartParams, ShouldOverrideCallbackState } from './WebViewTurboModule';
34
- import { AnyThreadRNInstance, WorkerTaskRunnable } from '@rnoh/react-native-openharmony/src/main/ets/RNOH';
35
- import buffer from '@ohos.buffer';
36
-
37
- export const TAG = "WebView"
38
-
39
- export const WEB_VIEW = "RNCWebView"
40
-
41
- @Sendable
42
- class WorkerRunnable implements WorkerTaskRunnable<ShouldStartParams> {
43
- run(rnInstance: AnyThreadRNInstance, params: ShouldStartParams): void {
44
- params.lockState = ShouldOverrideCallbackState.UNDECIDED
45
- rnInstance.getTurboModule<WebViewTurboModule>(TM.RNCWebViewModule.NAME).setShouldStartParams(params)
46
- }
47
- }
48
-
49
- let atManager = abilityAccessCtrl.createAtManager();
50
-
51
- @Component
52
- export struct RNCWebView {
53
- public static readonly NAME = RNC.RNCWebView.NAME
54
- ctx!: RNComponentContext
55
- tag: number = ZERO
56
- source: WebViewNewSource = {
57
- uri: "",
58
- method: "",
59
- body: "",
60
- html: "",
61
- baseUrl: ""
62
- }
63
- html: string | undefined = "";
64
- url: string | Resource = "";
65
- body: string | undefined = "";
66
- controller: webview.WebviewController = new webview.WebviewController();
67
- javaScriptEnable: boolean = true;
68
- allowFileAccess: boolean = true;
69
- forceDark: boolean = true;
70
- minFontSize: number = EIGHT;
71
- overScrollMode: OverScrollMode = OverScrollMode.NEVER;
72
- progress: number = ZERO;
73
- cacheMode: number = CacheMode.Default;
74
- requestUrl: string = "";
75
- bundleName: string = "";
76
- messagingEnabled: boolean = false;
77
- hasRegisterJavaScriptProxy: boolean = false;
78
- controllerAttached: boolean = false;
79
- renderMode: RenderMode = RenderMode.SYNC_RENDER;
80
- scrollEnabled = true;
81
- nestedScroll = NestedScrollMode.SELF_FIRST;
82
- headers: Array<webview.WebHeader> = []
83
- injectedJavaScriptBeforeContentLoaded: Array<ScriptItem> = [
84
- { script: '', scriptRules: ["*"] }
85
- ];
86
- allowPageStartInProgress = true;
87
- @State webviewWidth: number = ZERO
88
- @State webviewHeight: number = ZERO
89
- @State isRefreshing: boolean = false
90
- @State mode: MixedMode = MixedMode.All;
91
- private unregisterDescriptorChangesListener?: () => void = undefined
92
- private cleanupCommandCallback?: () => void = undefined
93
- private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined
94
- private cleanUpCallbacks: (() => void)[] = []
95
- private descriptorWrapper: WebViewDescriptor = Object() as WebViewDescriptor
96
- private webViewBaseOperate: BaseOperate | null = null
97
- private shouldInterceptLoad: boolean = true
98
- private callLoadTimer: number = -1
99
- static readonly SHOULD_OVERRIDE_URL_LOADING_TIMEOUT: number = 250
100
- private debounceTimer: number = -1
101
- static readonly DEBOUNCE_TIME: number = 100
102
- private isFirstDebounce: boolean = true
103
-
104
- aboutToAppear() {
105
- try {
106
- this.initVariable()
107
- this.getBundleName()
108
- this.url = this.source.uri as string;
109
- this.onDescriptorWrapperChange(this.descriptorWrapper)
110
- this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag,
111
- (newDescriptor) => {
112
- this.onDescriptorWrapperChange(newDescriptor as WebViewDescriptor)
113
- }
114
- )
115
- BaseOperate.setThirdPartyCookiesEnabled(this.descriptorWrapper.rawProps.thirdPartyCookiesEnabled)
116
- webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.rawProps.webviewDebuggingEnabled)
117
- this.registerCommandCallback()
118
- if (this.hasIncognito()) {
119
- this.cacheMode = CacheMode.Online;
120
- }
121
- if (this.source.headers) {
122
- this.source.headers.forEach(item => {
123
- if (item.name && item.value) {
124
- this.headers.push({ headerKey: item.name, headerValue: item.value })
125
- }
126
- })
127
- }
128
- if (this.descriptorWrapper.rawProps.geolocationEnabled) {
129
- atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, ["ohos.permission.APPROXIMATELY_LOCATION"]).then((data) => {
130
- }).catch((error: BusinessError) => {
131
- console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
132
- })
133
- }
134
- } catch (error) {
135
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
136
- }
137
-
138
- }
139
-
140
- hasIncognito(): boolean {
141
- return this.descriptorWrapper.rawProps.incognito && this.descriptorWrapper.rawProps.incognito === true
142
- }
143
-
144
- createWebViewEvent(type: string): WebViewEventParams {
145
- return this.webViewBaseOperate?.createWebViewEvent({ type, progress: this.progress }) as WebViewEventParams;
146
- }
147
-
148
- aboutToDisappear() {
149
- this.getUIContext().getFocusController().clearFocus()
150
- this.cleanupCommandCallback?.()
151
- this.unregisterDescriptorChangesListener?.()
152
- this.cleanUpCallbacks.forEach(cb => cb())
153
- try {
154
- if (this.hasIncognito()) {
155
- this.webViewBaseOperate?.setIncognito()
156
- }
157
- this.controller.deleteJavaScriptRegister(JAVASCRIPT_INTERFACE)
158
- this.controller.refresh()
159
- this.hasRegisterJavaScriptProxy = false
160
- this.webViewBaseOperate = null
161
- } catch (error) {
162
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
163
- }
164
- }
165
-
166
- registerCommandCallback() {
167
- this.cleanupCommandCallback = this.ctx.componentCommandReceiver.registerCommandCallback(
168
- this.tag,
169
- (command: COMMAND_NAME, args: string[]) => this.webViewBaseOperate?.registerCommandCallback(command, args));
170
- }
171
-
172
- onLoadingStart() {
173
- this.webViewBaseOperate?.emitLoadingStart({ progress: this.progress })
174
- }
175
-
176
- onProgressChange(event: OnProgressChangeEvent) {
177
- this.progress = event.newProgress
178
- // 修复单页面应用切换路由时 onPageBegin 未触发的问题
179
- if (this.controller.getUrl() !== this.url && this.progress < ONE_HUNDRED && this.allowPageStartInProgress) {
180
- this.allowPageStartInProgress = false;
181
- this.onLoadingStart();
182
- }
183
- this.webViewBaseOperate?.emitProgressChange({ progress: this.progress })
184
- if (this.progress === ONE_HUNDRED) {
185
- this.allowPageStartInProgress = true;
186
- }
187
- }
188
-
189
- onLoadingFinish() {
190
- this.webViewBaseOperate?.emitLoadingFinish({ progress: this.progress })
191
- this.isRefreshing = false
192
- }
193
-
194
- runInjectedJavaScript() {
195
- let injectedJS = this.descriptorWrapper.rawProps.injectedJavaScript
196
- if (this.javaScriptEnable && injectedJS != "" && this.controllerAttached) {
197
- try {
198
- this.controller.runJavaScript("(function() {\n" + injectedJS + ";\n})();")
199
- .then((result) => {
200
- Logger.debug(TAG, '[RNOH] result: ' + result);
201
- })
202
- .catch((error: string | Error) => {
203
- Logger.error(TAG, "[RNOH] error: " + error);
204
- })
205
- } catch (error) {
206
- Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
207
- }
208
- }
209
- }
210
-
211
- getJsDialogTitle() {
212
- const url = this.controller.getUrl()
213
- // 对于 data: URL,只显示 'JavaScript',类似于 Chrome
214
- let title = 'JavaScript';
215
- const isDataUrl = (url !== null) && url.startsWith("data:");
216
-
217
-
218
- if (!isDataUrl) {
219
- try {
220
- const dialogUrl = OSUrl.URL.parseURL(url);
221
- title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示:`;
222
- } catch (ex) {
223
- title = url;
224
- }
225
- }
226
-
227
- return title;
228
- }
229
-
230
- onJavascriptAlert(event?: AlertEvent) {
231
- if (event) {
232
- AlertDialog.show({
233
- title: this.getJsDialogTitle(),
234
- message: event.message,
235
- alignment: DialogAlignment.Center,
236
- secondaryButton: {
237
- value: this.resourceToString($r('app.string.determined')),
238
- action: () => event.result.handleConfirm()
239
- },
240
- cancel: () => event.result.handleCancel(),
241
- })
242
- }
243
- return true
244
- }
245
-
246
- onJavascriptConfirm(event?: AlertEvent) {
247
- if (event) {
248
- AlertDialog.show({
249
- title: this.getJsDialogTitle(),
250
- message: event.message,
251
- alignment: DialogAlignment.Center,
252
- secondaryButton: {
253
- value: this.resourceToString($r('app.string.determined')),
254
- action: () => event.result.handleConfirm()
255
- },
256
- cancel: () => event.result.handleCancel(),
257
- primaryButton: {
258
- value: this.resourceToString($r('app.string.cancel')),
259
- action: () => event.result.handleCancel()
260
- },
261
- })
262
- }
263
- return true
264
- }
265
-
266
- ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch: boolean) {
267
- this.webViewBaseOperate?.ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch)
268
- }
269
-
270
- loadHtmlData(html: string = '', url: string = '') {
271
- try {
272
- this.controller.loadData(
273
- html,
274
- "text/html",
275
- "UTF-8",
276
- url,
277
- " "
278
- );
279
- } catch (error) {
280
- Logger.error(TAG, "error:" + error)
281
- }
282
- }
283
-
284
- controllerAttachedInit(): void {
285
- this.controllerAttached = true;
286
- this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag)
287
- this.webViewBaseOperate = new BaseOperate(this.eventEmitter, this.controller)
288
- this.webViewBaseOperate.setCustomUserAgent(this.descriptorWrapper.rawProps.userAgent,
289
- this.descriptorWrapper.rawProps.applicationNameForUserAgent)
290
- this.webViewBaseOperate.setFraudulentWebsiteWarningEnabled(this.descriptorWrapper.rawProps.fraudulentWebsiteWarningEnabled)
291
-
292
- let baseUrl = this.source.baseUrl
293
- let uri = this.source.uri
294
- if (this.source.html != undefined && this.source.html != "") {
295
- this.loadHtmlData(this.source.html, baseUrl)
296
- } else if (uri != undefined && uri != "") {
297
- if (this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body) {
298
- let postData = buffer.from(this.source.body);
299
- let url = uri as string;
300
- this.controller.postUrl(url, postData?.buffer);
301
- } else {
302
- this.controller.loadUrl(uri, this.headers);
303
- }
304
- } else {
305
- this.loadHtmlData()
306
- }
307
- if (!this.hasRegisterJavaScriptProxy) {
308
- this.registerPostMessage()
309
- }
310
- }
311
-
312
- onPageBegin() {
313
- try {
314
- if (this.controller.getUrl() === this.url) {
315
- this.onLoadingStart();
316
- }
317
- this.controller.setScrollable(this.scrollEnabled)
318
- this.ignoreSilentHardwareSwitchMethods(this.descriptorWrapper.rawProps.ignoreSilentHardwareSwitch)
319
- } catch (error) {
320
- Logger.debug(TAG,
321
- `[RNOH] setignoreSilentHardwareSwitch and setScrollable ErrorCode: ${error.code}, Message: ${error.message}, userAgent: ${this.descriptorWrapper.rawProps.userAgent}`);
322
- }
323
- }
324
-
325
- getBundleName() {
326
- let bundleFlags =
327
- bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_ABILITY |
328
- bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
329
-
330
- try {
331
- bundleManager.getBundleInfoForSelf(bundleFlags, (error, data) => {
332
- if (error) {
333
- Logger.error(TAG,
334
- `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
335
- } else {
336
- this.bundleName = JSON.stringify(data.name)
337
- }
338
- });
339
- } catch (error) {
340
- Logger.error(TAG,
341
- `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
342
- }
343
- }
344
-
345
- resourceToString(resource: Resource): string {
346
- return getContext(this).resourceManager.getStringSync(resource)
347
- }
348
-
349
- onLoadFinished() {
350
- this.onLoadingFinish()
351
- }
352
-
353
- onPageEndExecuteRunInjectedJavaScript(){
354
- this.runInjectedJavaScript()
355
- }
356
-
357
- onPageEnd() {
358
- this.runInjectedJavaScript()
359
- this.onLoadingFinish()
360
- }
361
-
362
- getPermissionDialogMessage(event: OnPermissionRequestEvent) {
363
- let permissionDialogMessage = ''
364
- permissionDialogMessage = event.request.getAccessibleResource().toString()
365
- switch (permissionDialogMessage) {
366
- case ProtectedResourceType.VIDEO_CAPTURE:
367
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera'))
368
- break;
369
- case ProtectedResourceType.AUDIO_CAPTURE:
370
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_microphone'))
371
- break;
372
- case ProtectedResourceType.MidiSysex:
373
- permissionDialogMessage = this.resourceToString($r('app.string.midi_sysex_resources'))
374
- break;
375
- case ProtectedResourceType.VIDEO_CAPTURE + "," + ProtectedResourceType.AUDIO_CAPTURE:
376
- permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera_and_microphone'))
377
- break;
378
- }
379
- return permissionDialogMessage;
380
- }
381
-
382
- onLoadIntercept(event: OnLoadInterceptEvent): boolean {
383
- Logger.debug(TAG, `onLoadIntercept start`)
384
- this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
385
-
386
- const params = new ShouldStartParams();
387
- params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
388
- this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
389
- this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
390
- const startTime = Date.now();
391
- // 当lockState没变化并且小于超时时间,阻塞进程
392
- while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
393
- Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
394
- // 空循环等待
395
- }
396
-
397
- Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
398
- return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
399
- }
400
-
401
- onOverrideUrlLoading(event: WebResourceRequest) {
402
- this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
403
- return false
404
- }
405
-
406
- onSslErrorEventCallback(event: SslErrorEvent){
407
- this.webViewBaseOperate?.emitOnSslErrorEventCallback(event);
408
- }
409
-
410
- onCreateMenu = (menuItems: Array<TextMenuItem>) => {
411
- let customMenuItems = this.descriptorWrapper.rawProps.menuItems
412
- if (customMenuItems == undefined) {
413
- return menuItems;
414
- } else if (customMenuItems.length == 0) {
415
- return [];
416
- } else {
417
- let menuItemsArray: Array<TextMenuItem> = [];
418
- for (let index = 0; index < customMenuItems.length; index++) {
419
- let element: MenuItem = customMenuItems[index];
420
- let customItem: TextMenuItem = {
421
- content: element.label,
422
- id: TextMenuItemId.of(element.key)
423
- };
424
- menuItemsArray.push(customItem)
425
- }
426
- return menuItemsArray;
427
- }
428
- }
429
-
430
- onMenuItemClick(menuItem: TextMenuItem): boolean {
431
- if (this.descriptorWrapper.rawProps.menuItems != undefined) {
432
- this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
433
- .then((result: string) => {
434
- this.webViewBaseOperate?.customMenuSelection(menuItem, result)
435
- })
436
- .catch((error: string | Error) => {
437
- Logger.info('[RNOH] function onMenuItemClick error: ' + error);
438
- })
439
- // 禁用系统默认行为
440
- return true;
441
- }
442
- return false;
443
- }
444
-
445
- onPermissionRequestAlert(event: OnPermissionRequestEvent){
446
- AlertDialog.show({
447
- title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
448
- message: this.getPermissionDialogMessage(event),
449
- alignment: DialogAlignment.Center,
450
- primaryButton: {
451
- value: $r('app.string.deny'),
452
- action: () => {
453
- event.request.deny();
454
- }
455
- },
456
- secondaryButton: {
457
- value: $r('app.string.on_confirm'),
458
- action: () => {
459
- event.request.grant(event.request.getAccessibleResource());
460
- }
461
- },
462
- cancel: () => {
463
- event.request.deny();
464
- }
465
- })
466
- }
467
-
468
- async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
469
- let permissionList: Array<Permissions> = [];
470
- resources.forEach(resource => {
471
- switch (resource.toString()) {
472
- case 'TYPE_VIDEO_CAPTURE':
473
- permissionList.push('ohos.permission.CAMERA');
474
- break;
475
- case 'TYPE_AUDIO_CAPTURE':
476
- permissionList.push('ohos.permission.MICROPHONE');
477
- break;
478
- }
479
- });
480
-
481
- await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
482
- }
483
-
484
- build() {
485
- Stack() {
486
- if ( deviceInfo.sdkApiVersion >= 20) {
487
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
488
- .mixedMode(this.mode)
489
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
490
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
491
- const resources: string[] = event.request.getAccessibleResource();
492
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
493
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
494
- this.source.baseUrl);
495
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
496
- this.source.baseUrl);
497
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
498
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
499
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
500
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
501
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
502
- }
503
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
504
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
505
- }
506
- switch (mediaCapturePermissionGrantType) {
507
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
508
- if (originDomain != currentDomain) {
509
- this.onPermissionRequestAlert(event)
510
- } else {
511
- event.request.grant(resources);
512
- }
513
- break;
514
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
515
- if (originDomain != currentDomain) {
516
- event.request.deny();
517
- } else {
518
- event.request.grant(resources);
519
- }
520
- break;
521
- case WebMediaCapturePermissionGrantType.DENY:
522
- event.request.deny();
523
- break;
524
- case WebMediaCapturePermissionGrantType.GRANT:
525
- event.request.grant(resources);
526
- break;
527
- case WebMediaCapturePermissionGrantType.PROMPT:
528
- this.onPermissionRequestAlert(event)
529
- break;
530
- }
531
- } else {
532
- this.onPermissionRequestAlert(event)
533
- }
534
- }
535
- })
536
- .width(this.webviewWidth)
537
- .height(this.webviewHeight)
538
- .fileAccess(this.allowFileAccess)
539
- .constraintSize({ minHeight: MINHEIGHT })
540
- .overScrollMode(this.overScrollMode)
541
- .javaScriptAccess(this.javaScriptEnable)
542
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
543
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
544
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
545
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
546
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
547
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
548
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
549
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
550
- .forceDarkAccess(this.forceDark)
551
- .cacheMode(this.cacheMode)
552
- .minFontSize(this.minFontSize)
553
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
554
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
555
- .overScrollMode(this.overScrollMode)
556
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
557
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
558
- .nestedScroll({
559
- scrollForward: this.nestedScroll,
560
- scrollBackward: this.nestedScroll,
561
- })
562
- .onLoadStarted(() => this.onPageBegin())
563
- .onLoadFinished(() => this.onLoadFinished())
564
- .onPageEnd(() => {this.onPageEndExecuteRunInjectedJavaScript()})
565
- .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
566
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
567
- .onControllerAttached(() => this.controllerAttachedInit())
568
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
569
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
570
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
571
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
572
- .onGeolocationShow((event) => {
573
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
574
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
575
- }
576
- })
577
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
578
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
579
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
580
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
581
- .onSslErrorEvent((event: SslErrorEvent) => this.onSslErrorEventCallback(event))
582
- .editMenuOptions({
583
- onCreateMenu: this.onCreateMenu.bind(this),
584
- onMenuItemClick: this.onMenuItemClick.bind(this),
585
- })
586
- } else {
587
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
588
- .mixedMode(this.mode)
589
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
590
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
591
- const resources: string[] = event.request.getAccessibleResource();
592
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
593
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
594
- this.source.baseUrl);
595
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
596
- this.source.baseUrl);
597
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
598
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
599
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
600
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
601
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
602
- }
603
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
604
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
605
- }
606
- switch (mediaCapturePermissionGrantType) {
607
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
608
- if (originDomain != currentDomain) {
609
- this.onPermissionRequestAlert(event)
610
- } else {
611
- event.request.grant(resources);
612
- }
613
- break;
614
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
615
- if (originDomain != currentDomain) {
616
- event.request.deny();
617
- } else {
618
- event.request.grant(resources);
619
- }
620
- break;
621
- case WebMediaCapturePermissionGrantType.DENY:
622
- event.request.deny();
623
- break;
624
- case WebMediaCapturePermissionGrantType.GRANT:
625
- event.request.grant(resources);
626
- break;
627
- case WebMediaCapturePermissionGrantType.PROMPT:
628
- this.onPermissionRequestAlert(event)
629
- break;
630
- }
631
- } else {
632
- this.onPermissionRequestAlert(event)
633
- }
634
- }
635
- })
636
-
637
- .width(this.webviewWidth)
638
- .height(this.webviewHeight)
639
- .fileAccess(this.allowFileAccess)
640
- .constraintSize({ minHeight: MINHEIGHT })
641
- .overScrollMode(this.overScrollMode)
642
- .javaScriptAccess(this.javaScriptEnable)
643
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
644
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
645
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
646
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
647
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
648
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
649
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
650
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
651
- .forceDarkAccess(this.forceDark)
652
- .cacheMode(this.cacheMode)
653
- .minFontSize(this.minFontSize)
654
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
655
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
656
- .overScrollMode(this.overScrollMode)
657
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
658
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
659
- .nestedScroll({
660
- scrollForward: this.nestedScroll,
661
- scrollBackward: this.nestedScroll,
662
- })
663
- .onPageBegin(() => this.onPageBegin())
664
- .onPageEnd(() => this.onPageEnd())
665
- .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
666
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
667
- .onControllerAttached(() => this.controllerAttachedInit())
668
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
669
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
670
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
671
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
672
- .onGeolocationShow((event) => {
673
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
674
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
675
- }
676
- })
677
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
678
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
679
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
680
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
681
- .onSslErrorEvent((event: SslErrorEvent) => this.onSslErrorEventCallback(event))
682
- .editMenuOptions({
683
- onCreateMenu: this.onCreateMenu.bind(this),
684
- onMenuItemClick: this.onMenuItemClick.bind(this),
685
- })
686
- }
687
- }
688
- .width(this.webviewWidth)
689
- .height(this.webviewHeight)
690
- .position({
691
- x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
692
- y: this.descriptorWrapper.layoutMetrics.frame.origin.y
693
- })
694
- }
695
-
696
- private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
697
- this.initVariable()
698
- this.descriptorWrapper = descriptorWrapper
699
- if (this.source.html != "" && this.html != this.source.html) {
700
- if (this.controllerAttached) {
701
- this.loadHtmlData(this.source.html, this.source.baseUrl)
702
- Logger.debug(TAG, "[RNOH] html is update")
703
- this.html = this.source.html
704
- }
705
- } else if (this.source.uri && this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body && (this.source.uri != this.url || this.body != this.source.body)) {
706
- if (this.controllerAttached) {
707
- let url = this.source.uri as string;
708
- this.url = url;
709
- this.body = this.source.body;
710
- let postData = buffer.from(this.source.body);
711
- this.controller.postUrl(url, postData?.buffer);
712
- }
713
- } else if (this.url != this.source.uri) {
714
- if (this.source.uri != "") {
715
- Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
716
- if (this.controllerAttached) {
717
- this.controller.loadUrl(this.descriptorWrapper.rawProps.newSource.uri, this.headers)
718
- }
719
- } else {
720
- this.loadHtmlData()
721
- }
722
- this.url = this.source.uri as string;
723
- }
724
- if (this.controllerAttached) {
725
- this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
726
- }
727
- }
728
-
729
- private initVariable() {
730
- this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
731
- this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
732
- this.minFontSize =
733
- this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
734
- this.cacheMode =
735
- this.descriptorWrapper.rawProps.cacheEnabled ?
736
- this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
737
- CacheMode.Online;
738
- this.source = {
739
- uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
740
- method: this.descriptorWrapper.rawProps.newSource.method ?? "",
741
- body: this.descriptorWrapper.rawProps.newSource.body ?? "",
742
- html: this.descriptorWrapper.rawProps.newSource.html ?? "",
743
- baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
744
- headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
745
- }
746
- if (this.source.headers) {
747
- this.source.headers.forEach(item => {
748
- if (item.name && item.value) {
749
- this.headers.push({ headerKey: item.name, headerValue: item.value })
750
- }
751
- })
752
- }
753
- // this.html = this.source.html
754
- const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
755
- const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
756
-
757
- if (this.debounceTimer !== -1) {
758
- clearTimeout(this.debounceTimer)
759
- this.debounceTimer = -1
760
- }
761
-
762
- if (this.isFirstDebounce) {
763
- this.webviewWidth = newWidth
764
- this.webviewHeight = newHeight
765
- this.isFirstDebounce = false
766
- } else {
767
- this.debounceTimer = setTimeout(() => {
768
- this.webviewWidth = newWidth
769
- this.webviewHeight = newHeight
770
- }, RNCWebView.DEBOUNCE_TIME)
771
- }
772
- this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
773
- // 当禁止滚动时,使用父组件滚动
774
- this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
775
- if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
776
- this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
777
- }
778
-
779
- if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
780
- this.overScrollMode = OverScrollMode.ALWAYS
781
- } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
782
- this.overScrollMode = OverScrollMode.NEVER
783
- }
784
-
785
- this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
786
-
787
- if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
788
- this.injectedJavaScriptBeforeContentLoaded = [
789
- { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
790
- ];
791
- }
792
- }
793
-
794
- private registerPostMessage() {
795
- if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
796
- return;
797
- }
798
- this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
799
- if (this.messagingEnabled) {
800
- let bridge: RNCWebViewBridge = {
801
- postMessage: (data: string) => {
802
- Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
803
- if (this.controller != null) {
804
- let result: WebViewEventParams = this.createWebViewEvent("onMessage")
805
- if (result) {
806
- result.data = data.toString()
807
- result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
808
- this.eventEmitter!.emit("message", result as ResultType);
809
- }
810
- }
811
- }
812
- };
813
- this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
814
- this.source.uri ?
815
- this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
816
- this.hasRegisterJavaScriptProxy = true
817
- }
818
- }
1
+ // Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved
2
+ // Use of this source code is governed by a Apache-2.0 license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import { RNComponentContext } from '@rnoh/react-native-openharmony';
6
+ import webview from '@ohos.web.webview';
7
+ import { url as OSUrl } from '@kit.ArkTS';
8
+ import { RNC, TM } from './generated';
9
+ import Logger from './Logger';
10
+ import { BaseOperate } from './WebViewBaseOperate';
11
+ import { deviceInfo ,BusinessError} from '@kit.BasicServicesKit';
12
+ import {
13
+ AlertEvent,
14
+ CACHE_MODE,
15
+ JAVASCRIPT_INTERFACE,
16
+ ONE_HUNDRED,
17
+ EIGHT,
18
+ ResultType,
19
+ RNCWebViewBridge,
20
+ WebViewEventParams,
21
+ WebViewNewSource,
22
+ WebViewOverScrollMode,
23
+ ZERO,
24
+ COMMAND_NAME,
25
+ WebViewDescriptor,
26
+ REFRESH_OFFSET,
27
+ MINHEIGHT,
28
+ WebMediaCapturePermissionGrantType,
29
+ MenuItem
30
+ } from './Magic';
31
+
32
+ import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
33
+ import { WebViewTurboModule, ShouldStartParams, ShouldOverrideCallbackState } from './WebViewTurboModule';
34
+ import { AnyThreadRNInstance, WorkerTaskRunnable } from '@rnoh/react-native-openharmony/src/main/ets/RNOH';
35
+ import buffer from '@ohos.buffer';
36
+ import { window } from '@kit.ArkUI';
37
+
38
+ export const TAG = "WebView"
39
+
40
+ export const WEB_VIEW = "RNCWebView"
41
+
42
+ @Sendable
43
+ class WorkerRunnable implements WorkerTaskRunnable<ShouldStartParams> {
44
+ run(rnInstance: AnyThreadRNInstance, params: ShouldStartParams): void {
45
+ params.lockState = ShouldOverrideCallbackState.UNDECIDED
46
+ rnInstance.getTurboModule<WebViewTurboModule>(TM.RNCWebViewModule.NAME).setShouldStartParams(params)
47
+ }
48
+ }
49
+
50
+ let atManager = abilityAccessCtrl.createAtManager();
51
+
52
+ @Component
53
+ export struct RNCWebView {
54
+ public static readonly NAME = RNC.RNCWebView.NAME
55
+ ctx!: RNComponentContext
56
+ tag: number = ZERO
57
+ source: WebViewNewSource = {
58
+ uri: "",
59
+ method: "",
60
+ body: "",
61
+ html: "",
62
+ baseUrl: ""
63
+ }
64
+ html: string | undefined = "";
65
+ url: string | Resource = "";
66
+ body: string | undefined = "";
67
+ controller: webview.WebviewController = new webview.WebviewController();
68
+ javaScriptEnable: boolean = true;
69
+ allowFileAccess: boolean = true;
70
+ minFontSize: number = EIGHT;
71
+ overScrollMode: OverScrollMode = OverScrollMode.NEVER;
72
+ progress: number = ZERO;
73
+ cacheMode: number = CacheMode.Default;
74
+ requestUrl: string = "";
75
+ bundleName: string = "";
76
+ messagingEnabled: boolean = false;
77
+ hasRegisterJavaScriptProxy: boolean = false;
78
+ controllerAttached: boolean = false;
79
+ renderMode: RenderMode = RenderMode.SYNC_RENDER;
80
+ scrollEnabled = true;
81
+ @State nestedScroll: NestedScrollMode = NestedScrollMode.SELF_FIRST;
82
+ headers: Array<webview.WebHeader> = []
83
+ injectedJavaScriptBeforeContentLoaded: Array<ScriptItem> = [
84
+ { script: '', scriptRules: ["*"] }
85
+ ];
86
+ allowPageStartInProgress = true;
87
+ @State webviewWidth: number = ZERO
88
+ @State webviewHeight: number = ZERO
89
+ @State isRefreshing: boolean = false
90
+ @State isFullScreen: boolean = false
91
+ @State mode: MixedMode = MixedMode.None;
92
+ private unregisterDescriptorChangesListener?: () => void = undefined
93
+ private cleanupCommandCallback?: () => void = undefined
94
+ private eventEmitter: RNC.RNCWebView.EventEmitter | undefined = undefined
95
+ private descriptorWrapper: WebViewDescriptor = Object() as WebViewDescriptor
96
+ private webViewBaseOperate: BaseOperate | null = null
97
+ static readonly SHOULD_OVERRIDE_URL_LOADING_TIMEOUT: number = 250
98
+ private debounceTimer: number = -1
99
+ static readonly DEBOUNCE_TIME: number = 100
100
+ private isFirstDebounce: boolean = true
101
+ private initialOrientation: window.Orientation = window.Orientation.PORTRAIT
102
+ private isOrientationSaved: boolean = false
103
+ private cachedWindow: window.Window | null = null
104
+ private allowsFullscreenVideo: boolean = false
105
+ private isProgrammaticLoad = false
106
+ private isComponentAlive: boolean = true
107
+
108
+ async aboutToAppear() {
109
+ this.isComponentAlive = true
110
+ try {
111
+ this.initVariable()
112
+ await this.saveInitialOrientation()
113
+ this.getBundleName()
114
+ this.url = this.source.uri as string;
115
+ this.onDescriptorWrapperChange(this.descriptorWrapper)
116
+ this.unregisterDescriptorChangesListener = this.ctx.descriptorRegistry.subscribeToDescriptorChanges(this.tag,
117
+ (newDescriptor) => {
118
+ this.onDescriptorWrapperChange(newDescriptor as WebViewDescriptor)
119
+ }
120
+ )
121
+ BaseOperate.setThirdPartyCookiesEnabled(this.descriptorWrapper.rawProps.thirdPartyCookiesEnabled)
122
+ webview.WebviewController.setWebDebuggingAccess(this.descriptorWrapper.rawProps.webviewDebuggingEnabled)
123
+ this.registerCommandCallback()
124
+ if (this.hasIncognito()) {
125
+ this.cacheMode = CacheMode.Online;
126
+ }
127
+ if (this.descriptorWrapper.rawProps.geolocationEnabled) {
128
+ atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, ["ohos.permission.APPROXIMATELY_LOCATION"]).then((data) => {
129
+ }).catch((error: BusinessError) => {
130
+ console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
131
+ })
132
+ }
133
+ } catch (error) {
134
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
135
+ }
136
+
137
+ }
138
+
139
+ hasIncognito(): boolean {
140
+ return this.descriptorWrapper.rawProps.incognito && this.descriptorWrapper.rawProps.incognito === true
141
+ }
142
+
143
+ createWebViewEvent(type: string): WebViewEventParams {
144
+ return this.webViewBaseOperate?.createWebViewEvent({ type, progress: this.progress }) as WebViewEventParams;
145
+ }
146
+
147
+ aboutToDisappear() {
148
+ this.isComponentAlive = false
149
+ this.unregisterDescriptorChangesListener?.()
150
+ this.cleanupCommandCallback?.()
151
+ if (this.debounceTimer !== -1) {
152
+ clearTimeout(this.debounceTimer)
153
+ this.debounceTimer = -1
154
+ }
155
+ // 恢复初始屏幕方向和沉浸式状态,防止全屏状态下组件销毁后状态残留
156
+ // 无论是否在全屏状态,都尝试恢复窗口状态(handleFullScreenEnter 异步操作可能在组件销毁后才完成)
157
+ if (this.cachedWindow) {
158
+ try {
159
+ this.cachedWindow.setPreferredOrientation(this.initialOrientation);
160
+ this.cachedWindow.setWindowLayoutFullScreen(false);
161
+ this.cachedWindow.setWindowSystemBarEnable(['status', 'navigation']);
162
+ Logger.debug(TAG, '[WebViewFullScreen] Restored window state in aboutToDisappear');
163
+ } catch (error) {
164
+ Logger.error(TAG, `[WebViewFullScreen] Failed to restore in aboutToDisappear: ${error.code}`);
165
+ }
166
+ }
167
+ this.eventEmitter = undefined
168
+ this.headers = []
169
+ this.isFullScreen = false;
170
+ this.cachedWindow = null;
171
+ this.getUIContext().getFocusController().clearFocus()
172
+
173
+ try {
174
+ if (this.hasIncognito()) {
175
+ this.webViewBaseOperate?.setIncognito()
176
+ }
177
+ } catch (error) {
178
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
179
+ } finally {
180
+ try {
181
+ this.controller.deleteJavaScriptRegister(JAVASCRIPT_INTERFACE)
182
+ this.controller.refresh()
183
+ } catch (e) {
184
+ Logger.error(TAG, `[RNOH] deleteJavaScriptRegister Errorcode: ${e.code}, Message: ${e.message}`);
185
+ }
186
+ this.hasRegisterJavaScriptProxy = false
187
+ this.webViewBaseOperate = null
188
+ }
189
+ }
190
+
191
+ registerCommandCallback() {
192
+ this.cleanupCommandCallback = this.ctx.componentCommandReceiver.registerCommandCallback(
193
+ this.tag,
194
+ (command: string, args: string[]) => {
195
+ if (command === COMMAND_NAME.LOADURL) {
196
+ this.isProgrammaticLoad = true
197
+ }
198
+ this.webViewBaseOperate?.registerCommandCallback(command as COMMAND_NAME, args)
199
+ });
200
+ }
201
+
202
+ onLoadingStart() {
203
+ this.webViewBaseOperate?.emitLoadingStart({ progress: this.progress })
204
+ }
205
+
206
+ onProgressChange(event: OnProgressChangeEvent) {
207
+ this.progress = event.newProgress
208
+ // 修复单页面应用切换路由时 onPageBegin 未触发的问题
209
+ if (this.controller.getUrl() !== this.url && this.progress < ONE_HUNDRED && this.allowPageStartInProgress) {
210
+ this.allowPageStartInProgress = false;
211
+ this.onLoadingStart();
212
+ }
213
+ this.webViewBaseOperate?.emitProgressChange({ progress: this.progress })
214
+ if (this.progress === ONE_HUNDRED) {
215
+ this.allowPageStartInProgress = true;
216
+ }
217
+ }
218
+
219
+ onLoadingFinish() {
220
+ this.webViewBaseOperate?.emitLoadingFinish({ progress: this.progress })
221
+ this.isRefreshing = false
222
+ }
223
+
224
+ runInjectedJavaScript() {
225
+ let injectedJS = this.descriptorWrapper.rawProps.injectedJavaScript
226
+ if (this.javaScriptEnable && injectedJS != "" && this.controllerAttached) {
227
+ try {
228
+ this.controller.runJavaScript("(function() {\n" + injectedJS + ";\n})();")
229
+ .then((result) => {
230
+ Logger.debug(TAG, '[RNOH] result: ' + result);
231
+ })
232
+ .catch((error: string | Error) => {
233
+ Logger.error(TAG, "[RNOH] error: " + error);
234
+ })
235
+ } catch (error) {
236
+ Logger.error(TAG, `[RNOH]Errorcode: ${error.code}, Message: ${error.message}`);
237
+ }
238
+ }
239
+ }
240
+
241
+ getJsDialogTitle() {
242
+ const url = this.controller.getUrl()
243
+ // 对于 data: URL,只显示 'JavaScript',类似于 Chrome
244
+ let title = 'JavaScript';
245
+ const isDataUrl = (url !== null) && url.startsWith("data:");
246
+
247
+
248
+ if (!isDataUrl) {
249
+ try {
250
+ const dialogUrl = OSUrl.URL.parseURL(url);
251
+ title = `网址为”${dialogUrl.protocol}//${dialogUrl.host}“的网页显示:`;
252
+ } catch (ex) {
253
+ title = url;
254
+ }
255
+ }
256
+
257
+ return title;
258
+ }
259
+
260
+ onJavascriptAlert(event?: AlertEvent) {
261
+ if (event) {
262
+ AlertDialog.show({
263
+ title: this.getJsDialogTitle(),
264
+ message: event.message,
265
+ alignment: DialogAlignment.Center,
266
+ secondaryButton: {
267
+ value: this.resourceToString($r('app.string.determined')),
268
+ action: () => event.result.handleConfirm()
269
+ },
270
+ cancel: () => event.result.handleCancel(),
271
+ })
272
+ }
273
+ return true
274
+ }
275
+
276
+ onJavascriptConfirm(event?: AlertEvent) {
277
+ if (event) {
278
+ AlertDialog.show({
279
+ title: this.getJsDialogTitle(),
280
+ message: event.message,
281
+ alignment: DialogAlignment.Center,
282
+ secondaryButton: {
283
+ value: this.resourceToString($r('app.string.determined')),
284
+ action: () => event.result.handleConfirm()
285
+ },
286
+ cancel: () => event.result.handleCancel(),
287
+ primaryButton: {
288
+ value: this.resourceToString($r('app.string.cancel')),
289
+ action: () => event.result.handleCancel()
290
+ },
291
+ })
292
+ }
293
+ return true
294
+ }
295
+
296
+ ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch: boolean) {
297
+ this.webViewBaseOperate?.ignoreSilentHardwareSwitchMethods(ignoreSilentHardwareSwitch)
298
+ }
299
+
300
+ loadHtmlData(html: string = '', url: string = '') {
301
+ try {
302
+ this.controller.loadData(
303
+ html,
304
+ "text/html",
305
+ "UTF-8",
306
+ url,
307
+ " "
308
+ );
309
+ } catch (error) {
310
+ Logger.error(TAG, "error:" + error)
311
+ }
312
+ }
313
+
314
+ controllerAttachedInit(): void {
315
+ this.controllerAttached = true;
316
+ this.eventEmitter = new RNC.RNCWebView.EventEmitter(this.ctx.rnInstance, this.tag)
317
+ this.webViewBaseOperate = new BaseOperate(this.eventEmitter, this.controller)
318
+ this.webViewBaseOperate.setCustomUserAgent(this.descriptorWrapper.rawProps.userAgent,
319
+ this.descriptorWrapper.rawProps.applicationNameForUserAgent)
320
+ this.webViewBaseOperate.setFraudulentWebsiteWarningEnabled(this.descriptorWrapper.rawProps.fraudulentWebsiteWarningEnabled)
321
+
322
+ let baseUrl = this.source.baseUrl
323
+ let uri = this.source.uri
324
+ if (this.source.html != undefined && this.source.html != "") {
325
+ this.loadHtmlData(this.source.html, baseUrl)
326
+ } else if (uri != undefined && uri != "") {
327
+ if (this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body) {
328
+ let postData = buffer.from(this.source.body);
329
+ let url = uri as string;
330
+ this.controller.postUrl(url, postData?.buffer);
331
+ } else {
332
+ this.controller.loadUrl(uri, this.headers);
333
+ }
334
+ } else {
335
+ this.loadHtmlData()
336
+ }
337
+ if (!this.hasRegisterJavaScriptProxy) {
338
+ this.registerPostMessage()
339
+ }
340
+ }
341
+
342
+ onPageBegin() {
343
+ try {
344
+ if (this.controller.getUrl() === this.url) {
345
+ this.onLoadingStart();
346
+ }
347
+ this.controller.setScrollable(this.scrollEnabled)
348
+ this.ignoreSilentHardwareSwitchMethods(this.descriptorWrapper.rawProps.ignoreSilentHardwareSwitch)
349
+ } catch (error) {
350
+ Logger.debug(TAG,
351
+ `[RNOH] setignoreSilentHardwareSwitch and setScrollable ErrorCode: ${error.code}, Message: ${error.message}, userAgent: ${this.descriptorWrapper.rawProps.userAgent}`);
352
+ }
353
+ }
354
+
355
+ getBundleName() {
356
+ let bundleFlags =
357
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_HAP_MODULE | bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_ABILITY |
358
+ bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
359
+
360
+ try {
361
+ bundleManager.getBundleInfoForSelf(bundleFlags, (error, data) => {
362
+ if (error) {
363
+ Logger.error(TAG,
364
+ `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
365
+ } else {
366
+ if (!this.isComponentAlive) return
367
+ this.bundleName = JSON.stringify(data.name)
368
+ }
369
+ });
370
+ } catch (error) {
371
+ Logger.error(TAG,
372
+ `[RNOH] getBundleInfoForSelf failed ErrorCode: ${error.code}, Message: ${error.message}`);
373
+ }
374
+ }
375
+
376
+ resourceToString(resource: Resource): string {
377
+ return getContext(this).resourceManager.getStringSync(resource)
378
+ }
379
+
380
+ onLoadFinished() {
381
+ this.onLoadingFinish()
382
+ }
383
+
384
+ onPageEndExecuteRunInjectedJavaScript(){
385
+ this.runInjectedJavaScript()
386
+ }
387
+
388
+ onPageEnd() {
389
+ this.runInjectedJavaScript()
390
+ this.onLoadingFinish()
391
+ }
392
+
393
+ getPermissionDialogMessage(event: OnPermissionRequestEvent) {
394
+ let permissionDialogMessage = ''
395
+ permissionDialogMessage = event.request.getAccessibleResource().toString()
396
+ switch (permissionDialogMessage) {
397
+ case ProtectedResourceType.VIDEO_CAPTURE:
398
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera'))
399
+ break;
400
+ case ProtectedResourceType.AUDIO_CAPTURE:
401
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_microphone'))
402
+ break;
403
+ case ProtectedResourceType.MidiSysex:
404
+ permissionDialogMessage = this.resourceToString($r('app.string.midi_sysex_resources'))
405
+ break;
406
+ case ProtectedResourceType.VIDEO_CAPTURE + "," + ProtectedResourceType.AUDIO_CAPTURE:
407
+ permissionDialogMessage = this.resourceToString($r('app.string.use_your_camera_and_microphone'))
408
+ break;
409
+ }
410
+ return permissionDialogMessage;
411
+ }
412
+
413
+ onLoadIntercept(event: OnLoadInterceptEvent): boolean {
414
+ Logger.debug(TAG, `onLoadIntercept start`)
415
+ this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
416
+
417
+ const params = new ShouldStartParams();
418
+ params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
419
+ this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
420
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
421
+ const startTime = Date.now();
422
+ // 当lockState没变化并且小于超时时间,阻塞进程
423
+ while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
424
+ Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
425
+ // 空循环等待
426
+ }
427
+
428
+ Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
429
+ return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
430
+ }
431
+ onOverrideUrlLoading(event: WebResourceRequest) {
432
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
433
+ return false
434
+ }
435
+
436
+ onSslErrorEventCallback(event: SslErrorEvent){
437
+ this.webViewBaseOperate?.emitOnSslErrorEventCallback(event);
438
+ }
439
+
440
+ onCreateMenu = (menuItems: Array<TextMenuItem>) => {
441
+ let customMenuItems = this.descriptorWrapper.rawProps.menuItems
442
+ if (customMenuItems == undefined) {
443
+ return menuItems;
444
+ } else if (customMenuItems.length == 0) {
445
+ return [];
446
+ } else {
447
+ let menuItemsArray: Array<TextMenuItem> = [];
448
+ for (let index = 0; index < customMenuItems.length; index++) {
449
+ let element: MenuItem = customMenuItems[index];
450
+ let customItem: TextMenuItem = {
451
+ content: element.label,
452
+ id: TextMenuItemId.of(element.key)
453
+ };
454
+ menuItemsArray.push(customItem)
455
+ }
456
+ return menuItemsArray;
457
+ }
458
+ }
459
+
460
+ onMenuItemClick(menuItem: TextMenuItem): boolean {
461
+ if (this.descriptorWrapper.rawProps.menuItems != undefined) {
462
+ this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
463
+ .then((result: string) => {
464
+ this.webViewBaseOperate?.customMenuSelection(menuItem, result)
465
+ })
466
+ .catch((error: string | Error) => {
467
+ Logger.info('[RNOH] function onMenuItemClick error: ' + error);
468
+ })
469
+ // 禁用系统默认行为
470
+ return true;
471
+ }
472
+ return false;
473
+ }
474
+
475
+ onPermissionRequestAlert(event: OnPermissionRequestEvent){
476
+ AlertDialog.show({
477
+ title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
478
+ message: this.getPermissionDialogMessage(event),
479
+ alignment: DialogAlignment.Center,
480
+ primaryButton: {
481
+ value: $r('app.string.deny'),
482
+ action: () => {
483
+ event.request.deny();
484
+ }
485
+ },
486
+ secondaryButton: {
487
+ value: $r('app.string.on_confirm'),
488
+ action: () => {
489
+ event.request.grant(event.request.getAccessibleResource());
490
+ }
491
+ },
492
+ cancel: () => {
493
+ event.request.deny();
494
+ }
495
+ })
496
+ }
497
+
498
+ async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
499
+ let permissionList: Array<Permissions> = [];
500
+ resources.forEach(resource => {
501
+ switch (resource.toString()) {
502
+ case 'TYPE_VIDEO_CAPTURE':
503
+ permissionList.push('ohos.permission.CAMERA');
504
+ break;
505
+ case 'TYPE_AUDIO_CAPTURE':
506
+ permissionList.push('ohos.permission.MICROPHONE');
507
+ break;
508
+ }
509
+ });
510
+
511
+ await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
512
+ }
513
+
514
+ private async getWindow(): Promise<window.Window | null> {
515
+ try {
516
+ const win = await window.getLastWindow(getContext(this));
517
+ if (win) {
518
+ this.cachedWindow = win;
519
+ }
520
+ return win;
521
+ } catch (error) {
522
+ console.error(TAG, `[WebViewFullScreenTest] getWindow Errorcode: ${error.code}`);
523
+ return null;
524
+ }
525
+ }
526
+
527
+ private async saveInitialOrientation() {
528
+ const win = await this.getWindow();
529
+ if (win) {
530
+ try {
531
+ const orientation = win.getPreferredOrientation();
532
+ this.initialOrientation = orientation;
533
+ this.isOrientationSaved = true;
534
+ console.debug(TAG, `[WebViewFullScreenTest] Saved initial orientation: ${orientation}`);
535
+ } catch (error) {
536
+ console.error(TAG, `[WebViewFullScreenTest] saveInitialOrientation Errorcode: ${error.code}`);
537
+ }
538
+ }
539
+ }
540
+
541
+ private async setOrientation(orientation: window.Orientation): Promise<boolean> {
542
+ const win = await this.getWindow();
543
+ if (win) {
544
+ try {
545
+ await win.setPreferredOrientation(orientation);
546
+ console.debug(TAG, `[WebViewFullScreenTest] Set orientation to: ${orientation}`);
547
+ return true;
548
+ } catch (error) {
549
+ console.error(TAG, `[WebViewFullScreenTest] setOrientation Errorcode: ${error.code}`);
550
+ return false;
551
+ }
552
+ }
553
+ return false;
554
+ }
555
+
556
+ // 处理全屏进入(带状态保护和异步串行)
557
+ private async handleFullScreenEnter() {
558
+ if (!this.isComponentAlive || this.isFullScreen) {
559
+ Logger.debug(TAG, '[WebViewFullScreen] Already in full screen, skipping');
560
+ return;
561
+ }
562
+ if (!this.isOrientationSaved) {
563
+ Logger.warn(TAG, '[WebViewFullScreen] Initial orientation not saved yet, skipping full screen enter');
564
+ return;
565
+ }
566
+ try {
567
+ // 阶段1:设置方向
568
+ const orientationResult = await this.setOrientation(window.Orientation.AUTO_ROTATION);
569
+ if (!orientationResult) {
570
+ Logger.error(TAG, '[WebViewFullScreen] Failed to set orientation, aborting full screen enter');
571
+ return;
572
+ }
573
+ // 阶段2:设置沉浸式模式
574
+ const immersiveResult = await this.enterImmersiveMode();
575
+ if (!immersiveResult) {
576
+ Logger.error(TAG, '[WebViewFullScreen] Failed to enter immersive mode, rolling back');
577
+ // 回滚沉浸式模式(即使部分失败也要尝试恢复)
578
+ await this.exitImmersiveMode();
579
+ // 回滚方向设置
580
+ await this.setOrientation(this.initialOrientation);
581
+ return;
582
+ }
583
+ // 阶段3:所有操作成功,设置状态
584
+ this.isFullScreen = true;
585
+ } catch (error) {
586
+ Logger.error(TAG, `[WebViewFullScreen] Enter full screen failed: ${error}`);
587
+ // 异常时回滚所有操作
588
+ await this.exitImmersiveMode();
589
+ await this.setOrientation(this.initialOrientation);
590
+ }
591
+ }
592
+
593
+ // 处理全屏退出(带状态保护和异步串行)
594
+ private async handleFullScreenExit() {
595
+ if (!this.isComponentAlive ||!this.isFullScreen) {
596
+ Logger.debug(TAG, '[WebViewFullScreen] Not in full screen, skipping');
597
+ return;
598
+ }
599
+ try {
600
+ // 阶段1:恢复方向
601
+ const orientationResult = await this.setOrientation(this.initialOrientation);
602
+ if (!orientationResult) {
603
+ Logger.error(TAG, '[WebViewFullScreen] Failed to restore orientation, keeping full screen state');
604
+ return;
605
+ }
606
+ // 方向已恢复,清除 isFullScreen 让 UI 恢复原始布局
607
+ this.isFullScreen = false;
608
+ // 阶段2:退出沉浸式模式
609
+ const immersiveResult = await this.exitImmersiveMode();
610
+ if (!immersiveResult) {
611
+ Logger.error(TAG, '[WebViewFullScreen] Failed to exit immersive mode after restoring orientation');
612
+ // 沉浸式模式退出失败,但方向已恢复,isFullScreen 已清除,避免重复触发循环
613
+ }
614
+ } catch (error) {
615
+ Logger.error(TAG, `[WebViewFullScreen] Exit full screen failed: ${error}`);
616
+ // 异常时也应清除状态,避免卡死
617
+ this.isFullScreen = false;
618
+ }
619
+ }
620
+
621
+ // 进入沉浸式全屏模式(隐藏系统栏)
622
+ private async enterImmersiveMode(): Promise<boolean> {
623
+ const win = await this.getWindow();
624
+ if (win) {
625
+ try {
626
+ // 设置窗口为全屏布局模式,内容延伸到系统栏下方
627
+ await win.setWindowLayoutFullScreen(true);
628
+ // 隐藏系统导航栏和状态栏
629
+ await win.setWindowSystemBarEnable([]);
630
+ Logger.debug(TAG, '[WebViewFullScreen] Entered immersive mode');
631
+ return true;
632
+ } catch (error) {
633
+ console.error(TAG, `[WebViewFullScreen] enterImmersiveMode Errorcode: ${error.code}`);
634
+ return false;
635
+ }
636
+ }
637
+ return false;
638
+ }
639
+
640
+ // 退出沉浸式全屏模式(恢复系统栏)
641
+ private async exitImmersiveMode(): Promise<boolean> {
642
+ const win = await this.getWindow();
643
+ if (win) {
644
+ try {
645
+ // 关闭全屏布局模式
646
+ await win.setWindowLayoutFullScreen(false);
647
+ // 恢复显示系统导航栏和状态栏(默认显示所有系统栏)
648
+ await win.setWindowSystemBarEnable(['status', 'navigation']);
649
+ Logger.debug(TAG, '[WebViewFullScreen] Exited immersive mode');
650
+ return true;
651
+ } catch (error) {
652
+ console.error(TAG, `[WebViewFullScreen] exitImmersiveMode Errorcode: ${error.code}`);
653
+ return false;
654
+ }
655
+ }
656
+ return false;
657
+ }
658
+
659
+ build() {
660
+ Stack() {
661
+ if ( deviceInfo.sdkApiVersion >= 20) {
662
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
663
+ .mixedMode(this.mode)
664
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
665
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
666
+ const resources: string[] = event.request.getAccessibleResource();
667
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
668
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
669
+ this.source.baseUrl);
670
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
671
+ this.source.baseUrl);
672
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
673
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
674
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
675
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
676
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
677
+ }
678
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
679
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
680
+ }
681
+ switch (mediaCapturePermissionGrantType) {
682
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
683
+ if (originDomain != currentDomain) {
684
+ this.onPermissionRequestAlert(event)
685
+ } else {
686
+ event.request.grant(resources);
687
+ }
688
+ break;
689
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
690
+ if (originDomain != currentDomain) {
691
+ event.request.deny();
692
+ } else {
693
+ event.request.grant(resources);
694
+ }
695
+ break;
696
+ case WebMediaCapturePermissionGrantType.DENY:
697
+ event.request.deny();
698
+ break;
699
+ case WebMediaCapturePermissionGrantType.GRANT:
700
+ event.request.grant(resources);
701
+ break;
702
+ case WebMediaCapturePermissionGrantType.PROMPT:
703
+ this.onPermissionRequestAlert(event)
704
+ break;
705
+ }
706
+ } else {
707
+ this.onPermissionRequestAlert(event)
708
+ }
709
+ }
710
+ })
711
+ .width(this.isFullScreen ? "100%" : this.webviewWidth)
712
+ .height(this.isFullScreen ? "100%" : this.webviewHeight)
713
+ .fileAccess(this.allowFileAccess)
714
+ .constraintSize({ minHeight: MINHEIGHT })
715
+ .overScrollMode(this.overScrollMode)
716
+ .javaScriptAccess(this.javaScriptEnable)
717
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
718
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
719
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
720
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
721
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
722
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
723
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.Black : Color.Transparent)
724
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
725
+ .forceDarkAccess(this.descriptorWrapper.rawProps.forceDarkOn)
726
+ .cacheMode(this.cacheMode)
727
+ .minFontSize(this.minFontSize)
728
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
729
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
730
+ .overScrollMode(this.overScrollMode)
731
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
732
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
733
+ .nestedScroll({
734
+ scrollForward: this.nestedScroll,
735
+ scrollBackward: this.nestedScroll,
736
+ })
737
+ .onLoadStarted(() => this.onPageBegin())
738
+ .onLoadFinished(() => this.onLoadFinished())
739
+ .onPageEnd(() => {this.onPageEndExecuteRunInjectedJavaScript()})
740
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
741
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
742
+ .onControllerAttached(() => this.controllerAttachedInit())
743
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
744
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
745
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
746
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
747
+ .onGeolocationShow((event) => {
748
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
749
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
750
+ }
751
+ })
752
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
753
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
754
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
755
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
756
+ .onSslErrorEvent((event: SslErrorEvent) => this.onSslErrorEventCallback(event))
757
+ .onFullScreenEnter(async () => {
758
+ Logger.debug(TAG, '[WebViewFullScreen] onFullScreenEnter called');
759
+ if (this.allowsFullscreenVideo) {
760
+ await this.handleFullScreenEnter();
761
+ } else {
762
+ Logger.debug(TAG, '[WebViewFullScreen] Fullscreen video disabled by allowsFullscreenVideo');
763
+ }
764
+ })
765
+ .onFullScreenExit(async () => {
766
+ Logger.debug(TAG, '[WebViewFullScreen] onFullScreenExit called');
767
+ await this.handleFullScreenExit();
768
+ })
769
+ .editMenuOptions({
770
+ onCreateMenu: this.onCreateMenu.bind(this),
771
+ onMenuItemClick: this.onMenuItemClick.bind(this),
772
+ })
773
+ } else {
774
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
775
+ .mixedMode(this.mode)
776
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
777
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
778
+ const resources: string[] = event.request.getAccessibleResource();
779
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
780
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
781
+ this.source.baseUrl);
782
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
783
+ this.source.baseUrl);
784
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
785
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
786
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
787
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
788
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
789
+ }
790
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
791
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
792
+ }
793
+ switch (mediaCapturePermissionGrantType) {
794
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
795
+ if (originDomain != currentDomain) {
796
+ this.onPermissionRequestAlert(event)
797
+ } else {
798
+ event.request.grant(resources);
799
+ }
800
+ break;
801
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
802
+ if (originDomain != currentDomain) {
803
+ event.request.deny();
804
+ } else {
805
+ event.request.grant(resources);
806
+ }
807
+ break;
808
+ case WebMediaCapturePermissionGrantType.DENY:
809
+ event.request.deny();
810
+ break;
811
+ case WebMediaCapturePermissionGrantType.GRANT:
812
+ event.request.grant(resources);
813
+ break;
814
+ case WebMediaCapturePermissionGrantType.PROMPT:
815
+ this.onPermissionRequestAlert(event)
816
+ break;
817
+ }
818
+ } else {
819
+ this.onPermissionRequestAlert(event)
820
+ }
821
+ }
822
+ })
823
+
824
+ .width(this.isFullScreen ? "100%" : this.webviewWidth)
825
+ .height(this.isFullScreen ? "100%" : this.webviewHeight)
826
+ .fileAccess(this.allowFileAccess)
827
+ .constraintSize({ minHeight: MINHEIGHT })
828
+ .overScrollMode(this.overScrollMode)
829
+ .javaScriptAccess(this.javaScriptEnable)
830
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
831
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
832
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
833
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
834
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
835
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
836
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.Black : Color.Transparent)
837
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
838
+ .forceDarkAccess(this.descriptorWrapper.rawProps.forceDarkOn)
839
+ .cacheMode(this.cacheMode)
840
+ .minFontSize(this.minFontSize)
841
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
842
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
843
+ .overScrollMode(this.overScrollMode)
844
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
845
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
846
+ .nestedScroll({
847
+ scrollForward: this.nestedScroll,
848
+ scrollBackward: this.nestedScroll,
849
+ })
850
+ .onPageBegin(() => this.onPageBegin())
851
+ .onPageEnd(() => this.onPageEnd())
852
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
853
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
854
+ .onControllerAttached(() => this.controllerAttachedInit())
855
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
856
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
857
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
858
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
859
+ .onGeolocationShow((event) => {
860
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
861
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
862
+ }
863
+ })
864
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
865
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
866
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
867
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
868
+ .onSslErrorEvent((event: SslErrorEvent) => this.onSslErrorEventCallback(event))
869
+ .onFullScreenEnter(async () => {
870
+ Logger.debug(TAG, '[WebViewFullScreen] onFullScreenEnter called');
871
+ if (this.allowsFullscreenVideo) {
872
+ await this.handleFullScreenEnter();
873
+ } else {
874
+ Logger.debug(TAG, '[WebViewFullScreen] Fullscreen video disabled by allowsFullscreenVideo');
875
+ }
876
+ })
877
+ .onFullScreenExit(async () => {
878
+ Logger.debug(TAG, '[WebViewFullScreen] onFullScreenExit called');
879
+ await this.handleFullScreenExit();
880
+ })
881
+ .editMenuOptions({
882
+ onCreateMenu: this.onCreateMenu.bind(this),
883
+ onMenuItemClick: this.onMenuItemClick.bind(this),
884
+ })
885
+ }
886
+ }
887
+ .width(this.isFullScreen ? "100%" : this.webviewWidth)
888
+ .height(this.isFullScreen ? "100%" : this.webviewHeight)
889
+ .position(this.isFullScreen ? undefined : {
890
+ x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
891
+ y: this.descriptorWrapper.layoutMetrics.frame.origin.y
892
+ })
893
+ }
894
+
895
+ private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
896
+ this.initVariable()
897
+ this.descriptorWrapper = descriptorWrapper
898
+ if (this.source.html != "" && this.html != this.source.html) {
899
+ if (this.controllerAttached) {
900
+ this.loadHtmlData(this.source.html, this.source.baseUrl)
901
+ Logger.debug(TAG, "[RNOH] html is update")
902
+ this.html = this.source.html
903
+ }
904
+ } else if (this.source.uri && this.source.method && this.source.method.toUpperCase() === 'POST' && this.source.body && (this.source.uri != this.url || this.body != this.source.body)) {
905
+ if (this.controllerAttached) {
906
+ let url = this.source.uri as string;
907
+ this.url = url;
908
+ this.body = this.source.body;
909
+ let postData = buffer.from(this.source.body);
910
+ this.controller.postUrl(url, postData?.buffer);
911
+ }
912
+ } else if (typeof this.url !== typeof this.source.uri
913
+ || (typeof this.source.uri === "string" && this.url !== this.source.uri)
914
+ || (typeof this.source.uri === "object" && (this.url as Resource)?.params !== (this.source.uri as Resource)?.params)) {
915
+ if (this.source.uri != "") {
916
+ Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
917
+ if (this.controllerAttached) {
918
+ this.controller.loadUrl(this.source.uri, this.headers)
919
+ }
920
+ } else {
921
+ this.loadHtmlData()
922
+ }
923
+ this.url = this.source.uri as string;
924
+ }
925
+ if (this.controllerAttached) {
926
+ this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
927
+ }
928
+ }
929
+
930
+ private initVariable() {
931
+ this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
932
+ this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
933
+ this.minFontSize =
934
+ this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
935
+ this.cacheMode =
936
+ this.descriptorWrapper.rawProps.cacheEnabled ?
937
+ this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
938
+ CacheMode.Online;
939
+ this.source = {
940
+ uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
941
+ method: this.descriptorWrapper.rawProps.newSource.method ?? "",
942
+ body: this.descriptorWrapper.rawProps.newSource.body ?? "",
943
+ html: this.descriptorWrapper.rawProps.newSource.html ?? "",
944
+ baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
945
+ headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
946
+ }
947
+ if (this.source.headers) {
948
+ this.source.headers.forEach(item => {
949
+ if (item.name && item.value) {
950
+ this.headers.push({ headerKey: item.name, headerValue: item.value })
951
+ }
952
+ })
953
+ }
954
+ // this.html = this.source.html
955
+ const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
956
+ const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
957
+
958
+ if (this.debounceTimer !== -1) {
959
+ clearTimeout(this.debounceTimer)
960
+ this.debounceTimer = -1
961
+ }
962
+
963
+ if (this.isFirstDebounce) {
964
+ this.webviewWidth = newWidth
965
+ this.webviewHeight = newHeight
966
+ this.isFirstDebounce = false
967
+ } else {
968
+ this.debounceTimer = setTimeout(() => {
969
+ this.webviewWidth = newWidth
970
+ this.webviewHeight = newHeight
971
+ }, RNCWebView.DEBOUNCE_TIME)
972
+ }
973
+ this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
974
+ // 当禁止滚动时,使用父组件滚动
975
+ this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
976
+ if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
977
+ this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
978
+ }
979
+
980
+ this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
981
+
982
+ if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
983
+ this.injectedJavaScriptBeforeContentLoaded = [
984
+ { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
985
+ ];
986
+ }
987
+
988
+ const renderModeStr = this.descriptorWrapper.rawProps.renderMode || "SYNC_RENDER"
989
+ this.renderMode = (renderModeStr === "ASYNC_RENDER") ? RenderMode.ASYNC_RENDER : RenderMode.SYNC_RENDER
990
+ this.allowsFullscreenVideo = this.descriptorWrapper.rawProps.allowsFullscreenVideo ?? false;
991
+ this.allowFileAccess = this.descriptorWrapper.rawProps.allowFileAccess ?? true;
992
+ }
993
+
994
+ private registerPostMessage() {
995
+ if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
996
+ return;
997
+ }
998
+ this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
999
+ if (this.messagingEnabled) {
1000
+ const self = this;
1001
+ let bridge: RNCWebViewBridge = {
1002
+ postMessage: (data: string) => {
1003
+ Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
1004
+ if (self.controller != null) {
1005
+ let result: WebViewEventParams = self.createWebViewEvent("onMessage")
1006
+ if (result) {
1007
+ result.data = data.toString()
1008
+ result.lockIdentifier = self.webViewBaseOperate?.getLockIdentifier()
1009
+ self.eventEmitter!.emit("message", result as ResultType);
1010
+ }
1011
+ }
1012
+ }
1013
+ };
1014
+ this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
1015
+ this.source.uri ?
1016
+ this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
1017
+ this.hasRegisterJavaScriptProxy = true
1018
+ }
1019
+ }
819
1020
  }