@react-native-ohos/react-native-webview 13.10.6-beta.3 → 13.10.6

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