@react-native-ohos/react-native-webview 13.15.3-1 → 13.15.4-beta.3

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 (59) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +20 -20
  3. package/README.OpenSource +10 -10
  4. package/README.md +395 -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 +18 -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 +95 -95
  15. package/harmony/rn_webview/src/main/cpp/generated/RNOH/generated/components/RNCWebViewJSIBinder.h +127 -126
  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 +241 -241
  22. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/EventEmitters.h +263 -263
  23. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.cpp +109 -108
  24. package/harmony/rn_webview/src/main/cpp/generated/react/renderer/components/react_native_webview/Props.h +512 -511
  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/CutomReference.ts +29 -29
  30. package/harmony/rn_webview/src/main/ets/Logger.ts +43 -43
  31. package/harmony/rn_webview/src/main/ets/Magic.ets +200 -199
  32. package/harmony/rn_webview/src/main/ets/RNCWebView.ets +842 -836
  33. package/harmony/rn_webview/src/main/ets/RNCWebViewPackage.ets +37 -37
  34. package/harmony/rn_webview/src/main/ets/ShouldRequestUrl.ts +47 -47
  35. package/harmony/rn_webview/src/main/ets/WebViewBaseOperate.ets +470 -470
  36. package/harmony/rn_webview/src/main/ets/WebViewTurboModule.ets +62 -62
  37. package/harmony/rn_webview/src/main/ets/generated/components/RNCWebView.ts +530 -526
  38. package/harmony/rn_webview/src/main/ets/generated/components/ts.ts +5 -5
  39. package/harmony/rn_webview/src/main/ets/generated/index.ets +5 -5
  40. package/harmony/rn_webview/src/main/ets/generated/ts.ts +6 -6
  41. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebView.ts +18 -18
  42. package/harmony/rn_webview/src/main/ets/generated/turboModules/RNCWebViewModule.ts +16 -16
  43. package/harmony/rn_webview/src/main/ets/generated/turboModules/ts.ts +6 -6
  44. package/harmony/rn_webview/src/main/module.json5 +10 -10
  45. package/harmony/rn_webview/src/main/resources/base/element/string.json +39 -39
  46. package/harmony/rn_webview/src/main/resources/en_US/element/string.json +39 -39
  47. package/harmony/rn_webview/src/main/resources/zh_CN/element/string.json +39 -39
  48. package/harmony/rn_webview/src/test/List.test.ets +4 -4
  49. package/harmony/rn_webview/src/test/LocalUnit.test.ets +32 -32
  50. package/harmony/rn_webview/ts.ets +8 -8
  51. package/harmony/rn_webview.har +0 -0
  52. package/package.json +100 -99
  53. package/src/NativeRNCWebView.ts +21 -21
  54. package/src/NativeRNCWebViewModule.ts +19 -19
  55. package/src/RNCWebViewNativeComponent.ts +357 -356
  56. package/src/WebView.harmony.tsx +344 -344
  57. package/src/WebView.tsx +10 -10
  58. package/src/codegenUtils.ts +10 -10
  59. package/src/index.ts +10 -10
@@ -1,837 +1,843 @@
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
- originWhitelistToRegex(originWhitelist: string): string {
382
- const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g;
383
- return `^${originWhitelist.replace(matchOperatorsRegex, '\\$&').replace(/\\\*/g, '.*')}`;
384
- }
385
- extractOrigin(url: string): string {
386
- const result = /^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);
387
- return result === null ? '' : result[0];
388
- };
389
- passesWhitelist(compiledWhitelist: string[], url: string) {
390
- const origin = this.extractOrigin(url);
391
- return compiledWhitelist.some((x) => new RegExp(x).test(origin));
392
- };
393
- compileWhitelist(originWhitelist: string[]): string[] {
394
- return ['about:blank', ...(originWhitelist || [])].map(this.originWhitelistToRegex);
395
- }
396
-
397
- onLoadIntercept(event: OnLoadInterceptEvent): boolean {
398
- Logger.debug(TAG, `onLoadIntercept start`)
399
- let originWhitelistArr = this.descriptorWrapper.rawProps.originWhitelist;
400
- let hasOnShouldStartLoadWithRequestEventBool = this.descriptorWrapper.rawProps.hasOnShouldStartLoadWithRequestEvent
401
- let urlString =
402
- this.webViewBaseOperate?.verifyURLFormat(event.data.getRequestUrl(), this.source.html, this.source.baseUrl) || "";
403
-
404
- if (this.passesWhitelist(this.compileWhitelist(originWhitelistArr), urlString) &&
405
- !hasOnShouldStartLoadWithRequestEventBool) {
406
- return false
407
- }
408
- this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
409
-
410
- const params = new ShouldStartParams();
411
- params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
412
- this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
413
- this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
414
- const startTime = Date.now();
415
- // 当lockState没变化并且小于超时时间,阻塞进程
416
- while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
417
- Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
418
- // 空循环等待
419
- }
420
-
421
- Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
422
- return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
423
- }
424
-
425
- onOverrideUrlLoading(event: WebResourceRequest) {
426
- this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
427
- return false
428
- }
429
-
430
- onCreateMenu = (menuItems: Array<TextMenuItem>) => {
431
- let customMenuItems = this.descriptorWrapper.rawProps.menuItems
432
- if (customMenuItems == undefined) {
433
- return menuItems;
434
- } else if (customMenuItems.length == 0) {
435
- return [];
436
- } else {
437
- let menuItemsArray: Array<TextMenuItem> = [];
438
- for (let index = 0; index < customMenuItems.length; index++) {
439
- let element: MenuItem = customMenuItems[index];
440
- let customItem: TextMenuItem = {
441
- content: element.label,
442
- id: TextMenuItemId.of(element.key)
443
- };
444
- menuItemsArray.push(customItem)
445
- }
446
- return menuItemsArray;
447
- }
448
- }
449
-
450
- onMenuItemClick(menuItem: TextMenuItem): boolean {
451
- if (this.descriptorWrapper.rawProps.menuItems != undefined) {
452
- this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
453
- .then((result: string) => {
454
- this.webViewBaseOperate?.customMenuSelection(menuItem, result)
455
- })
456
- .catch((error: string | Error) => {
457
- Logger.info('[RNOH] function onMenuItemClick error: ' + error);
458
- })
459
- // 禁用系统默认行为
460
- return true;
461
- }
462
- return false;
463
- }
464
-
465
- onPermissionRequestAlert(event: OnPermissionRequestEvent){
466
- AlertDialog.show({
467
- title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
468
- message: this.getPermissionDialogMessage(event),
469
- alignment: DialogAlignment.Center,
470
- primaryButton: {
471
- value: $r('app.string.deny'),
472
- action: () => {
473
- event.request.deny();
474
- }
475
- },
476
- secondaryButton: {
477
- value: $r('app.string.on_confirm'),
478
- action: () => {
479
- event.request.grant(event.request.getAccessibleResource());
480
- }
481
- },
482
- cancel: () => {
483
- event.request.deny();
484
- }
485
- })
486
- }
487
-
488
- async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
489
- let permissionList: Array<Permissions> = [];
490
- resources.forEach(resource => {
491
- switch (resource.toString()) {
492
- case 'TYPE_VIDEO_CAPTURE':
493
- permissionList.push('ohos.permission.CAMERA');
494
- break;
495
- case 'TYPE_AUDIO_CAPTURE':
496
- permissionList.push('ohos.permission.MICROPHONE');
497
- break;
498
- }
499
- });
500
-
501
- await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
502
- }
503
-
504
- build() {
505
- Stack() {
506
- if ( deviceInfo.sdkApiVersion >= 20) {
507
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
508
- .mixedMode(this.mode)
509
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
510
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
511
- const resources: string[] = event.request.getAccessibleResource();
512
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
513
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
514
- this.source.baseUrl);
515
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
516
- this.source.baseUrl);
517
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
518
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
519
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
520
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
521
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
522
- }
523
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
524
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
525
- }
526
- switch (mediaCapturePermissionGrantType) {
527
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
528
- if (originDomain != currentDomain) {
529
- this.onPermissionRequestAlert(event)
530
- } else {
531
- event.request.grant(resources);
532
- }
533
- break;
534
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
535
- if (originDomain != currentDomain) {
536
- event.request.deny();
537
- } else {
538
- event.request.grant(resources);
539
- }
540
- break;
541
- case WebMediaCapturePermissionGrantType.DENY:
542
- event.request.deny();
543
- break;
544
- case WebMediaCapturePermissionGrantType.GRANT:
545
- event.request.grant(resources);
546
- break;
547
- case WebMediaCapturePermissionGrantType.PROMPT:
548
- this.onPermissionRequestAlert(event)
549
- break;
550
- }
551
- } else {
552
- this.onPermissionRequestAlert(event)
553
- }
554
- }
555
- })
556
- .width(this.webviewWidth)
557
- .height(this.webviewHeight)
558
- .fileAccess(this.allowFileAccess)
559
- .constraintSize({ minHeight: MINHEIGHT })
560
- .overScrollMode(this.overScrollMode)
561
- .javaScriptAccess(this.javaScriptEnable)
562
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
563
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
564
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
565
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
566
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
567
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
568
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
569
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
570
- .forceDarkAccess(this.forceDark)
571
- .cacheMode(this.cacheMode)
572
- .minFontSize(this.minFontSize)
573
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
574
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
575
- .overScrollMode(this.overScrollMode)
576
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
577
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
578
- .nestedScroll({
579
- scrollForward: this.nestedScroll,
580
- scrollBackward: this.nestedScroll,
581
- })
582
- .onLoadStarted(() => this.onPageBegin())
583
- .onLoadFinished(() => this.onLoadFinished())
584
- .onPageEnd(() => {this.onPageEndExecuteRunInjectedJavaScript()})
585
- .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
586
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
587
- .onControllerAttached(() => this.controllerAttachedInit())
588
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
589
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
590
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
591
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
592
- .onGeolocationShow((event) => {
593
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
594
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
595
- }
596
- })
597
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
598
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
599
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
600
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
601
- .editMenuOptions({
602
- onCreateMenu: this.onCreateMenu.bind(this),
603
- onMenuItemClick: this.onMenuItemClick.bind(this),
604
- })
605
- } else {
606
- Web({ src: "", controller: this.controller, renderMode: this.renderMode })
607
- .mixedMode(this.mode)
608
- .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
609
- if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
610
- const resources: string[] = event.request.getAccessibleResource();
611
- if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
612
- const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
613
- this.source.baseUrl);
614
- const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
615
- this.source.baseUrl);
616
- const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
617
- const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
618
- let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
619
- if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
620
- mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
621
- }
622
- if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
623
- await this.handleMediaCapturePermissionGrantType(resources, originDomain)
624
- }
625
- switch (mediaCapturePermissionGrantType) {
626
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
627
- if (originDomain != currentDomain) {
628
- this.onPermissionRequestAlert(event)
629
- } else {
630
- event.request.grant(resources);
631
- }
632
- break;
633
- case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
634
- if (originDomain != currentDomain) {
635
- event.request.deny();
636
- } else {
637
- event.request.grant(resources);
638
- }
639
- break;
640
- case WebMediaCapturePermissionGrantType.DENY:
641
- event.request.deny();
642
- break;
643
- case WebMediaCapturePermissionGrantType.GRANT:
644
- event.request.grant(resources);
645
- break;
646
- case WebMediaCapturePermissionGrantType.PROMPT:
647
- this.onPermissionRequestAlert(event)
648
- break;
649
- }
650
- } else {
651
- this.onPermissionRequestAlert(event)
652
- }
653
- }
654
- })
655
-
656
- .width(this.webviewWidth)
657
- .height(this.webviewHeight)
658
- .fileAccess(this.allowFileAccess)
659
- .constraintSize({ minHeight: MINHEIGHT })
660
- .overScrollMode(this.overScrollMode)
661
- .javaScriptAccess(this.javaScriptEnable)
662
- .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
663
- .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
664
- .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
665
- .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
666
- .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
667
- .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
668
- this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
669
- .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
670
- .forceDarkAccess(this.forceDark)
671
- .cacheMode(this.cacheMode)
672
- .minFontSize(this.minFontSize)
673
- .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
674
- .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
675
- .overScrollMode(this.overScrollMode)
676
- .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
677
- .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
678
- .nestedScroll({
679
- scrollForward: this.nestedScroll,
680
- scrollBackward: this.nestedScroll,
681
- })
682
- .onPageBegin(() => this.onPageBegin())
683
- .onPageEnd(() => this.onPageEnd())
684
- .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
685
- .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
686
- .onControllerAttached(() => this.controllerAttachedInit())
687
- .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
688
- .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
689
- .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
690
- .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
691
- .onGeolocationShow((event) => {
692
- if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
693
- event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
694
- }
695
- })
696
- .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
697
- .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
698
- .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
699
- .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
700
- .editMenuOptions({
701
- onCreateMenu: this.onCreateMenu.bind(this),
702
- onMenuItemClick: this.onMenuItemClick.bind(this),
703
- })
704
- }
705
- }
706
- .width(this.webviewWidth)
707
- .height(this.webviewHeight)
708
- .position({
709
- x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
710
- y: this.descriptorWrapper.layoutMetrics.frame.origin.y
711
- })
712
- }
713
-
714
- private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
715
- this.initVariable()
716
- this.descriptorWrapper = descriptorWrapper
717
- if (this.source.html != "" && this.html != this.source.html) {
718
- if (this.controllerAttached) {
719
- this.loadHtmlData(this.source.html, this.source.baseUrl)
720
- Logger.debug(TAG, "[RNOH] html is update")
721
- this.html = this.source.html
722
- }
723
- } 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)) {
724
- if (this.controllerAttached) {
725
- let url = this.source.uri as string;
726
- this.url = url;
727
- this.body = this.source.body;
728
- let postData = buffer.from(this.source.body);
729
- this.controller.postUrl(url, postData?.buffer);
730
- }
731
- } else if (this.url != this.source.uri) {
732
- if (this.source.uri != "") {
733
- Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
734
- if (this.controllerAttached) {
735
- this.controller.loadUrl(this.descriptorWrapper.rawProps.newSource.uri, this.headers)
736
- }
737
- } else {
738
- this.loadHtmlData()
739
- }
740
- this.url = this.source.uri as string;
741
- }
742
- if (this.controllerAttached) {
743
- this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
744
- }
745
- }
746
-
747
- private initVariable() {
748
- this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
749
- this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
750
- this.minFontSize =
751
- this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
752
- this.cacheMode =
753
- this.descriptorWrapper.rawProps.cacheEnabled ?
754
- this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
755
- CacheMode.Online;
756
- this.source = {
757
- uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
758
- method: this.descriptorWrapper.rawProps.newSource.method ?? "",
759
- body: this.descriptorWrapper.rawProps.newSource.body ?? "",
760
- html: this.descriptorWrapper.rawProps.newSource.html ?? "",
761
- baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
762
- headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
763
- }
764
- if (this.source.headers) {
765
- this.source.headers.forEach(item => {
766
- if (item.name && item.value) {
767
- this.headers.push({ headerKey: item.name, headerValue: item.value })
768
- }
769
- })
770
- }
771
- // this.html = this.source.html
772
- const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
773
- const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
774
-
775
- if (this.debounceTimer !== -1) {
776
- clearTimeout(this.debounceTimer)
777
- this.debounceTimer = -1
778
- }
779
-
780
- if (this.isFirstDebounce) {
781
- this.webviewWidth = newWidth
782
- this.webviewHeight = newHeight
783
- this.isFirstDebounce = false
784
- } else {
785
- this.debounceTimer = setTimeout(() => {
786
- this.webviewWidth = newWidth
787
- this.webviewHeight = newHeight
788
- }, RNCWebView.DEBOUNCE_TIME)
789
- }
790
- this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
791
- // 当禁止滚动时,使用父组件滚动
792
- this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
793
- if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
794
- this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
795
- }
796
-
797
- if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
798
- this.overScrollMode = OverScrollMode.ALWAYS
799
- } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
800
- this.overScrollMode = OverScrollMode.NEVER
801
- }
802
-
803
- this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
804
-
805
- if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
806
- this.injectedJavaScriptBeforeContentLoaded = [
807
- { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
808
- ];
809
- }
810
- }
811
-
812
- private registerPostMessage() {
813
- if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
814
- return;
815
- }
816
- this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
817
- if (this.messagingEnabled) {
818
- let bridge: RNCWebViewBridge = {
819
- postMessage: (data: string) => {
820
- Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
821
- if (this.controller != null) {
822
- let result: WebViewEventParams = this.createWebViewEvent("onMessage")
823
- if (result) {
824
- result.data = data.toString()
825
- result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
826
- this.eventEmitter!.emit("message", result as ResultType);
827
- }
828
- }
829
- }
830
- };
831
- this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
832
- this.source.uri ?
833
- this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
834
- this.hasRegisterJavaScriptProxy = true
835
- }
836
- }
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
+ @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 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
+ originWhitelistToRegex(originWhitelist: string): string {
382
+ const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g;
383
+ return `^${originWhitelist.replace(matchOperatorsRegex, '\\$&').replace(/\\\*/g, '.*')}`;
384
+ }
385
+ extractOrigin(url: string): string {
386
+ const result = /^[A-Za-z][A-Za-z0-9+\-.]+:(\/\/)?[^/]*/.exec(url);
387
+ return result === null ? '' : result[0];
388
+ };
389
+ passesWhitelist(compiledWhitelist: string[], url: string) {
390
+ const origin = this.extractOrigin(url);
391
+ return compiledWhitelist.some((x) => new RegExp(x).test(origin));
392
+ };
393
+ compileWhitelist(originWhitelist: string[]): string[] {
394
+ return ['about:blank', ...(originWhitelist || [])].map(this.originWhitelistToRegex);
395
+ }
396
+
397
+ onLoadIntercept(event: OnLoadInterceptEvent): boolean {
398
+ Logger.debug(TAG, `onLoadIntercept start`)
399
+ let originWhitelistArr = this.descriptorWrapper.rawProps.originWhitelist;
400
+ let hasOnShouldStartLoadWithRequestEventBool = this.descriptorWrapper.rawProps.hasOnShouldStartLoadWithRequestEvent
401
+ let urlString =
402
+ this.webViewBaseOperate?.verifyURLFormat(event.data.getRequestUrl(), this.source.html, this.source.baseUrl) || "";
403
+
404
+ if (this.passesWhitelist(this.compileWhitelist(originWhitelistArr), urlString) &&
405
+ !hasOnShouldStartLoadWithRequestEventBool) {
406
+ return false
407
+ }
408
+ this.webViewBaseOperate?.setLockIdentifier(BaseOperate.generateLockIdentifier())
409
+
410
+ const params = new ShouldStartParams();
411
+ params.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier() || -1
412
+ this.ctx.runOnWorkerThread(new WorkerRunnable(), params);
413
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequest(event, this.source.html, this.source.baseUrl)
414
+ const startTime = Date.now();
415
+ // 当lockState没变化并且小于超时时间,阻塞进程
416
+ while (params.lockState === ShouldOverrideCallbackState.UNDECIDED &&
417
+ Date.now() - startTime < RNCWebView.SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
418
+ // 空循环等待
419
+ }
420
+
421
+ Logger.debug('params.lockState res:' + params.lockState + `, wait time is ${Date.now() - startTime}`);
422
+ return params.lockState === ShouldOverrideCallbackState.SHOULD_OVERRIDE;
423
+ }
424
+
425
+ onOverrideUrlLoading(event: WebResourceRequest) {
426
+ this.webViewBaseOperate?.emitShouldStartLoadWithRequestOverrideUrlLoading(event)
427
+ return false
428
+ }
429
+
430
+ onCreateMenu = (menuItems: Array<TextMenuItem>) => {
431
+ let customMenuItems = this.descriptorWrapper.rawProps.menuItems
432
+ if (customMenuItems == undefined) {
433
+ return menuItems;
434
+ } else if (customMenuItems.length == 0) {
435
+ return [];
436
+ } else {
437
+ let menuItemsArray: Array<TextMenuItem> = [];
438
+ for (let index = 0; index < customMenuItems.length; index++) {
439
+ let element: MenuItem = customMenuItems[index];
440
+ let customItem: TextMenuItem = {
441
+ content: element.label,
442
+ id: TextMenuItemId.of(element.key)
443
+ };
444
+ menuItemsArray.push(customItem)
445
+ }
446
+ return menuItemsArray;
447
+ }
448
+ }
449
+
450
+ onMenuItemClick(menuItem: TextMenuItem): boolean {
451
+ if (this.descriptorWrapper.rawProps.menuItems != undefined) {
452
+ this.controller.runJavaScript("(function(){return {selection: window.getSelection().toString()} })()")
453
+ .then((result: string) => {
454
+ this.webViewBaseOperate?.customMenuSelection(menuItem, result)
455
+ })
456
+ .catch((error: string | Error) => {
457
+ Logger.info('[RNOH] function onMenuItemClick error: ' + error);
458
+ })
459
+ // 禁用系统默认行为
460
+ return true;
461
+ }
462
+ return false;
463
+ }
464
+
465
+ onPermissionRequestAlert(event: OnPermissionRequestEvent){
466
+ AlertDialog.show({
467
+ title: this.resourceToString($r('app.string.on_confirm')) + event.request.getOrigin(),
468
+ message: this.getPermissionDialogMessage(event),
469
+ alignment: DialogAlignment.Center,
470
+ primaryButton: {
471
+ value: $r('app.string.deny'),
472
+ action: () => {
473
+ event.request.deny();
474
+ }
475
+ },
476
+ secondaryButton: {
477
+ value: $r('app.string.on_confirm'),
478
+ action: () => {
479
+ event.request.grant(event.request.getAccessibleResource());
480
+ }
481
+ },
482
+ cancel: () => {
483
+ event.request.deny();
484
+ }
485
+ })
486
+ }
487
+
488
+ async handleMediaCapturePermissionGrantType(resources: string[], domain: string) {
489
+ let permissionList: Array<Permissions> = [];
490
+ resources.forEach(resource => {
491
+ switch (resource.toString()) {
492
+ case 'TYPE_VIDEO_CAPTURE':
493
+ permissionList.push('ohos.permission.CAMERA');
494
+ break;
495
+ case 'TYPE_AUDIO_CAPTURE':
496
+ permissionList.push('ohos.permission.MICROPHONE');
497
+ break;
498
+ }
499
+ });
500
+
501
+ await atManager.requestPermissionsFromUser(this.ctx.uiAbilityContext, permissionList);
502
+ }
503
+
504
+ build() {
505
+ Stack() {
506
+ if ( deviceInfo.sdkApiVersion >= 20) {
507
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
508
+ .mixedMode(this.mode)
509
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
510
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
511
+ const resources: string[] = event.request.getAccessibleResource();
512
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
513
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
514
+ this.source.baseUrl);
515
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
516
+ this.source.baseUrl);
517
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
518
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
519
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
520
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
521
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
522
+ }
523
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
524
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
525
+ }
526
+ switch (mediaCapturePermissionGrantType) {
527
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
528
+ if (originDomain != currentDomain) {
529
+ this.onPermissionRequestAlert(event)
530
+ } else {
531
+ event.request.grant(resources);
532
+ }
533
+ break;
534
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
535
+ if (originDomain != currentDomain) {
536
+ event.request.deny();
537
+ } else {
538
+ event.request.grant(resources);
539
+ }
540
+ break;
541
+ case WebMediaCapturePermissionGrantType.DENY:
542
+ event.request.deny();
543
+ break;
544
+ case WebMediaCapturePermissionGrantType.GRANT:
545
+ event.request.grant(resources);
546
+ break;
547
+ case WebMediaCapturePermissionGrantType.PROMPT:
548
+ this.onPermissionRequestAlert(event)
549
+ break;
550
+ }
551
+ } else {
552
+ this.onPermissionRequestAlert(event)
553
+ }
554
+ }
555
+ })
556
+ .width(this.webviewWidth)
557
+ .height(this.webviewHeight)
558
+ .fileAccess(this.allowFileAccess)
559
+ .constraintSize({ minHeight: MINHEIGHT })
560
+ .overScrollMode(this.overScrollMode)
561
+ .javaScriptAccess(this.javaScriptEnable)
562
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
563
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
564
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
565
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
566
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
567
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
568
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
569
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
570
+ .forceDarkAccess(this.forceDark)
571
+ .cacheMode(this.cacheMode)
572
+ .minFontSize(this.minFontSize)
573
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
574
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
575
+ .overScrollMode(this.overScrollMode)
576
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
577
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
578
+ .nestedScroll({
579
+ scrollForward: this.nestedScroll,
580
+ scrollBackward: this.nestedScroll,
581
+ })
582
+ .onLoadStarted(() => this.onPageBegin())
583
+ .onLoadFinished(() => this.onLoadFinished())
584
+ .onPageEnd(() => {this.onPageEndExecuteRunInjectedJavaScript()})
585
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
586
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
587
+ .onControllerAttached(() => this.controllerAttachedInit())
588
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
589
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
590
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
591
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
592
+ .onGeolocationShow((event) => {
593
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
594
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
595
+ }
596
+ })
597
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
598
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
599
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
600
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
601
+ .editMenuOptions({
602
+ onCreateMenu: this.onCreateMenu.bind(this),
603
+ onMenuItemClick: this.onMenuItemClick.bind(this),
604
+ })
605
+ } else {
606
+ Web({ src: "", controller: this.controller, renderMode: this.renderMode })
607
+ .mixedMode(this.mode)
608
+ .onPermissionRequest(async (event: OnPermissionRequestEvent) => {
609
+ if (event && (this.getPermissionDialogMessage(event) != "TYPE_SENSOR")) {
610
+ const resources: string[] = event.request.getAccessibleResource();
611
+ if (resources.indexOf('TYPE_AUDIO_CAPTURE') >= 0 || resources.indexOf('TYPE_VIDEO_CAPTURE') >= 0 ) {
612
+ const originUrl = this.webViewBaseOperate?.verifyURLFormat(event.request.getOrigin(), this.source.html,
613
+ this.source.baseUrl);
614
+ const currentUrl = this.webViewBaseOperate?.verifyURLFormat(this.controller.getUrl(), this.source.html,
615
+ this.source.baseUrl);
616
+ const originDomain: string = OSUrl.URL.parseURL(originUrl).hostname;
617
+ const currentDomain: string = OSUrl.URL.parseURL(currentUrl).hostname;
618
+ let mediaCapturePermissionGrantType: string = WebMediaCapturePermissionGrantType.PROMPT
619
+ if (this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType != undefined) {
620
+ mediaCapturePermissionGrantType = this.descriptorWrapper.rawProps.mediaCapturePermissionGrantType;
621
+ }
622
+ if (mediaCapturePermissionGrantType != WebMediaCapturePermissionGrantType.DENY) {
623
+ await this.handleMediaCapturePermissionGrantType(resources, originDomain)
624
+ }
625
+ switch (mediaCapturePermissionGrantType) {
626
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_PROMPT:
627
+ if (originDomain != currentDomain) {
628
+ this.onPermissionRequestAlert(event)
629
+ } else {
630
+ event.request.grant(resources);
631
+ }
632
+ break;
633
+ case WebMediaCapturePermissionGrantType.GRANT_IF_SAME_HOST_ELSE_DENY:
634
+ if (originDomain != currentDomain) {
635
+ event.request.deny();
636
+ } else {
637
+ event.request.grant(resources);
638
+ }
639
+ break;
640
+ case WebMediaCapturePermissionGrantType.DENY:
641
+ event.request.deny();
642
+ break;
643
+ case WebMediaCapturePermissionGrantType.GRANT:
644
+ event.request.grant(resources);
645
+ break;
646
+ case WebMediaCapturePermissionGrantType.PROMPT:
647
+ this.onPermissionRequestAlert(event)
648
+ break;
649
+ }
650
+ } else {
651
+ this.onPermissionRequestAlert(event)
652
+ }
653
+ }
654
+ })
655
+
656
+ .width(this.webviewWidth)
657
+ .height(this.webviewHeight)
658
+ .fileAccess(this.allowFileAccess)
659
+ .constraintSize({ minHeight: MINHEIGHT })
660
+ .overScrollMode(this.overScrollMode)
661
+ .javaScriptAccess(this.javaScriptEnable)
662
+ .javaScriptOnDocumentStart(this.injectedJavaScriptBeforeContentLoaded)
663
+ .horizontalScrollBarAccess(this.descriptorWrapper.rawProps.showsHorizontalScrollIndicator)
664
+ .verticalScrollBarAccess(this.descriptorWrapper.rawProps.showsVerticalScrollIndicator)
665
+ .overviewModeAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
666
+ .textZoomRatio(this.descriptorWrapper.rawProps.textZoom)
667
+ .backgroundColor(BaseOperate.getColorMode(this.ctx.uiAbilityContext,
668
+ this.descriptorWrapper.rawProps.forceDarkOn) === WebDarkMode.On ? Color.White : Color.Transparent)
669
+ .darkMode(BaseOperate.getColorMode(this.ctx.uiAbilityContext, this.descriptorWrapper.rawProps.forceDarkOn))
670
+ .forceDarkAccess(this.forceDark)
671
+ .cacheMode(this.cacheMode)
672
+ .minFontSize(this.minFontSize)
673
+ .domStorageAccess(this.descriptorWrapper.rawProps.domStorageEnabled)
674
+ .zoomAccess(this.descriptorWrapper.rawProps.scalesPageToFit)
675
+ .overScrollMode(this.overScrollMode)
676
+ .onProgressChange((event: OnProgressChangeEvent) => this.onProgressChange(event))
677
+ .onScroll((event: OnScrollEvent) => this.webViewBaseOperate?.emitScroll(event))
678
+ .nestedScroll({
679
+ scrollForward: this.nestedScroll,
680
+ scrollBackward: this.nestedScroll,
681
+ })
682
+ .onPageBegin(() => this.onPageBegin())
683
+ .onPageEnd(() => this.onPageEnd())
684
+ .onErrorReceive((event: OnErrorReceiveEvent) => this.webViewBaseOperate?.emitLoadingError(event))
685
+ .onHttpErrorReceive((event: OnHttpErrorReceiveEvent) => this.webViewBaseOperate?.emitHttpError(event))
686
+ .onControllerAttached(() => this.controllerAttachedInit())
687
+ .onAlert((event: AlertEvent) => this.onJavascriptAlert(event))
688
+ .onConfirm((event: AlertEvent) => this.onJavascriptConfirm(event))
689
+ .onDownloadStart((event: OnDownloadStartEvent) => this.webViewBaseOperate?.onDownloadStart(event))
690
+ .geolocationAccess(this.descriptorWrapper.rawProps.geolocationEnabled)
691
+ .onGeolocationShow((event) => {
692
+ if (event && (this.descriptorWrapper.rawProps.geolocationEnabled != undefined)) {
693
+ event.geolocation.invoke(event.origin, this.descriptorWrapper.rawProps.geolocationEnabled, true);
694
+ }
695
+ })
696
+ .mediaPlayGestureAccess(this.descriptorWrapper.rawProps.mediaPlaybackRequiresUserAction)
697
+ .onRenderExited((event: OnRenderExitedEvent) => this.webViewBaseOperate?.onRenderExited(event))
698
+ .onLoadIntercept((event: OnLoadInterceptEvent) => this.onLoadIntercept(event))
699
+ .onTitleReceive((event: OnTitleReceiveEvent) => this.webViewBaseOperate?.onTitleReceive(event))
700
+ .editMenuOptions({
701
+ onCreateMenu: this.onCreateMenu.bind(this),
702
+ onMenuItemClick: this.onMenuItemClick.bind(this),
703
+ })
704
+ }
705
+ }
706
+ .width(this.webviewWidth)
707
+ .height(this.webviewHeight)
708
+ .position({
709
+ x: this.descriptorWrapper.layoutMetrics.frame.origin.x,
710
+ y: this.descriptorWrapper.layoutMetrics.frame.origin.y
711
+ })
712
+ }
713
+
714
+ private onDescriptorWrapperChange(descriptorWrapper: WebViewDescriptor) {
715
+ this.initVariable()
716
+ this.descriptorWrapper = descriptorWrapper
717
+ if (this.source.html != "" && this.html != this.source.html) {
718
+ if (this.controllerAttached) {
719
+ this.loadHtmlData(this.source.html, this.source.baseUrl)
720
+ Logger.debug(TAG, "[RNOH] html is update")
721
+ this.html = this.source.html
722
+ }
723
+ } 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)) {
724
+ if (this.controllerAttached) {
725
+ let url = this.source.uri as string;
726
+ this.url = url;
727
+ this.body = this.source.body;
728
+ let postData = buffer.from(this.source.body);
729
+ this.controller.postUrl(url, postData?.buffer);
730
+ }
731
+ } else if (typeof this.url !== typeof this.source.uri
732
+ || (typeof this.source.uri === "string" && this.url !== this.source.uri)
733
+ || (typeof this.source.uri === "object" && (this.url as Resource)?.params !== (this.source.uri as Resource)?.params)) {
734
+ if (this.source.uri != "") {
735
+ Logger.debug(TAG, `[RNOH] newDescriptor props update uri: ` + this.source.uri);
736
+ if (this.controllerAttached) {
737
+ this.controller.loadUrl(this.source.uri, this.headers)
738
+ }
739
+ } else {
740
+ this.loadHtmlData()
741
+ }
742
+ this.url = this.source.uri as string;
743
+ }
744
+ if (this.controllerAttached) {
745
+ this.controller?.setScrollable(this.descriptorWrapper.rawProps.scrollEnabled)
746
+ }
747
+ }
748
+
749
+ private initVariable() {
750
+ this.descriptorWrapper = this.ctx.descriptorRegistry.getDescriptor<WebViewDescriptor>(this.tag)
751
+ this.javaScriptEnable = this.descriptorWrapper.rawProps.javaScriptEnabled;
752
+ this.minFontSize =
753
+ this.descriptorWrapper.rawProps.minimumFontSize ? this.descriptorWrapper.rawProps.minimumFontSize : EIGHT;
754
+ this.cacheMode =
755
+ this.descriptorWrapper.rawProps.cacheEnabled ?
756
+ this.webViewBaseOperate?.transCacheMode(this.descriptorWrapper.rawProps.cacheMode as CACHE_MODE) as number :
757
+ CacheMode.Online;
758
+ this.source = {
759
+ uri: this.descriptorWrapper.rawProps.newSource.uri ?? "",
760
+ method: this.descriptorWrapper.rawProps.newSource.method ?? "",
761
+ body: this.descriptorWrapper.rawProps.newSource.body ?? "",
762
+ html: this.descriptorWrapper.rawProps.newSource.html ?? "",
763
+ baseUrl: this.descriptorWrapper.rawProps.newSource.baseUrl ?? "",
764
+ headers: this.descriptorWrapper.rawProps.newSource.headers ?? []
765
+ }
766
+ if (this.source.headers) {
767
+ this.source.headers.forEach(item => {
768
+ if (item.name && item.value) {
769
+ this.headers.push({ headerKey: item.name, headerValue: item.value })
770
+ }
771
+ })
772
+ }
773
+ // this.html = this.source.html
774
+ const newWidth = this.descriptorWrapper.layoutMetrics.frame.size.width
775
+ const newHeight = this.descriptorWrapper.layoutMetrics.frame.size.height
776
+
777
+ if (this.debounceTimer !== -1) {
778
+ clearTimeout(this.debounceTimer)
779
+ this.debounceTimer = -1
780
+ }
781
+
782
+ if (this.isFirstDebounce) {
783
+ this.webviewWidth = newWidth
784
+ this.webviewHeight = newHeight
785
+ this.isFirstDebounce = false
786
+ } else {
787
+ this.debounceTimer = setTimeout(() => {
788
+ this.webviewWidth = newWidth
789
+ this.webviewHeight = newHeight
790
+ }, RNCWebView.DEBOUNCE_TIME)
791
+ }
792
+ this.scrollEnabled = this.descriptorWrapper.rawProps.scrollEnabled;
793
+ // 当禁止滚动时,使用父组件滚动
794
+ this.nestedScroll = this.scrollEnabled ? NestedScrollMode.SELF_FIRST : NestedScrollMode.PARENT_FIRST;
795
+ if (this.source.uri && this.source.uri.toString().startsWith("asset://")) {
796
+ this.source.uri = $rawfile(this.source.uri.toString().replace("asset://", this.ctx.rnInstance.getAssetsDest()));
797
+ }
798
+
799
+ if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.ALWAYS) {
800
+ this.overScrollMode = OverScrollMode.ALWAYS
801
+ } else if (this.descriptorWrapper.rawProps.overScrollMode === WebViewOverScrollMode.NEVER) {
802
+ this.overScrollMode = OverScrollMode.NEVER
803
+ }
804
+
805
+ this.overScrollMode = this.descriptorWrapper.rawProps.bounces ? OverScrollMode.ALWAYS : OverScrollMode.NEVER;
806
+
807
+ if (this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded) {
808
+ this.injectedJavaScriptBeforeContentLoaded = [
809
+ { script: this.descriptorWrapper.rawProps.injectedJavaScriptBeforeContentLoaded, scriptRules: ["*"] }
810
+ ];
811
+ }
812
+
813
+ const renderModeStr = this.descriptorWrapper.rawProps.renderMode || "SYNC_RENDER"
814
+ this.renderMode = (renderModeStr === "ASYNC_RENDER") ? RenderMode.ASYNC_RENDER : RenderMode.SYNC_RENDER
815
+
816
+ }
817
+
818
+ private registerPostMessage() {
819
+ if (this.messagingEnabled == this.descriptorWrapper.rawProps.messagingEnabled) {
820
+ return;
821
+ }
822
+ this.messagingEnabled = this.descriptorWrapper.rawProps.messagingEnabled;
823
+ if (this.messagingEnabled) {
824
+ let bridge: RNCWebViewBridge = {
825
+ postMessage: (data: string) => {
826
+ Logger.debug(TAG, `[RNOH] bridge postMessage, ${JSON.stringify(data)}`);
827
+ if (this.controller != null) {
828
+ let result: WebViewEventParams = this.createWebViewEvent("onMessage")
829
+ if (result) {
830
+ result.data = data.toString()
831
+ result.lockIdentifier = this.webViewBaseOperate?.getLockIdentifier()
832
+ this.eventEmitter!.emit("message", result as ResultType);
833
+ }
834
+ }
835
+ }
836
+ };
837
+ this.controller.registerJavaScriptProxy(bridge, JAVASCRIPT_INTERFACE, ["postMessage"])
838
+ this.source.uri ?
839
+ this.controller.loadUrl(this.source.uri, this.headers) : this.controller.refresh()
840
+ this.hasRegisterJavaScriptProxy = true
841
+ }
842
+ }
837
843
  }