1# 使用Web组件菜单处理网页内容 2<!--Kit: ArkWeb--> 3<!--Subsystem: Web--> 4<!--Owner: @zourongchun--> 5<!--Designer: @zhufenghao--> 6<!--Tester: @ghiker--> 7<!--Adviser: @HelloCrease--> 8菜单作为用户交互的关键组件,其作用是构建清晰的导航体系,通过结构化布局展示功能入口,使用户能够迅速找到目标内容或执行操作。作为人机交互的重要枢纽,它显著提升了Web组件的可访问性和用户体验,是应用设计中必不可少的部分。Web组件菜单类型包括[文本选中菜单](./web_menu.md#文本选中菜单)、[上下文菜单](./web_menu.md#上下文菜单)和[自定义菜单](./web_menu.md#自定义菜单),应用可根据具体需求灵活选择。 9|菜单类型|目标元素|响应类型|是否支持自定义| 10|----|----|----|----| 11|[文本选中菜单](./web_menu.md#文本选中菜单)|文本|手势长按|可增减菜单项,菜单样式不可自定义| 12|[上下文菜单](./web_menu.md#上下文菜单)|超链接、图片、文字|手势长按、鼠标右键|支持通过菜单组件自定义| 13|[自定义菜单](./web_menu.md#自定义菜单)|图片|手势长按|支持通过菜单组件自定义| 14## 文本选中菜单 15Web组件的文本选中菜单是一种通过自定义元素实现的上下文交互组件,当用户选中文本时会动态显示,提供复制、分享、标注等语义化操作,具备标准化功能与可扩展性,是移动端文本操作的核心功能。文本选中菜单在用户长按选中文本或编辑状态下长按出现单手柄时弹出,菜单项横向排列。系统提供默认的菜单实现。应用可通过[editMenuOptions](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#editmenuoptions12)接口对文本选中菜单进行自定义操作。 161. 通过onCreateMenu方法自定义菜单项,通过操作Array<[TextMenuItem](../reference/apis-arkui/arkui-ts/ts-text-common.md#textmenuitem12对象说明)>数组可对显示菜单项进行增减操作,在[TextMenuItem](../reference/apis-arkui/arkui-ts/ts-text-common.md#textmenuitem12对象说明)中定义菜单项名称、图标、ID等内容。 172. 通过onMenuItemClick方法处理菜单项点击事件,当返回false时会执行系统默认逻辑。 183. 创建一个[EditMenuOptions](../reference/apis-arkui/arkui-ts/ts-text-common.md#editmenuoptions)对象,包含onCreateMenu和onMenuItemClick两个方法,通过Web组件的[editMenuOptions](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#editmenuoptions12)方法与Web组件绑定。 19 20 ```ts 21 // xxx.ets 22 import { webview } from '@kit.ArkWeb'; 23 @Entry 24 @Component 25 struct WebComponent { 26 controller: webview.WebviewController = new webview.WebviewController(); 27 28 onCreateMenu(menuItems: Array<TextMenuItem>): Array<TextMenuItem> { 29 let items = menuItems.filter((menuItem) => { 30 // 过滤用户需要的系统按键 31 return ( 32 menuItem.id.equals(TextMenuItemId.CUT) || 33 menuItem.id.equals(TextMenuItemId.COPY) || 34 menuItem.id.equals(TextMenuItemId.PASTE) 35 ); 36 }); 37 let customItem1: TextMenuItem = { 38 content: 'customItem1', 39 id: TextMenuItemId.of('customItem1'), 40 icon: $r('app.media.startIcon') 41 }; 42 let customItem2: TextMenuItem = { 43 content: $r('app.string.EntryAbility_label'), 44 id: TextMenuItemId.of('customItem2'), 45 icon: $r('app.media.startIcon') 46 }; 47 items.push(customItem1);// 在选项列表后添加新选项 48 items.unshift(customItem2);// 在选项列表前添加选项 49 items.push(customItem1); 50 items.push(customItem1); 51 items.push(customItem1); 52 items.push(customItem1); 53 items.push(customItem1); 54 return items; 55 } 56 57 onMenuItemClick(menuItem: TextMenuItem, textRange: TextRange): boolean { 58 if (menuItem.id.equals(TextMenuItemId.CUT)) { 59 // 用户自定义行为 60 console.log("拦截 id:CUT") 61 return true; // 返回true不执行系统回调 62 } else if (menuItem.id.equals(TextMenuItemId.COPY)) { 63 // 用户自定义行为 64 console.log("不拦截 id:COPY") 65 return false; // 返回false执行系统回调 66 } else if (menuItem.id.equals(TextMenuItemId.of('customItem1'))) { 67 // 用户自定义行为 68 console.log("拦截 id:customItem1") 69 return true;// 用户自定义菜单选项返回true时点击后不关闭菜单,返回false时关闭菜单 70 } else if (menuItem.id.equals(TextMenuItemId.of('customItem2'))){ 71 // 用户自定义行为 72 console.log("拦截 id:customItem2") 73 return true; 74 } 75 return false;// 返回默认值false 76 } 77 78 @State EditMenuOptions: EditMenuOptions = { onCreateMenu: this.onCreateMenu, onMenuItemClick: this.onMenuItemClick } 79 80 build() { 81 Column() { 82 Web({ src: $rawfile("index.html"), controller: this.controller }) 83 .editMenuOptions(this.EditMenuOptions) 84 } 85 } 86 } 87 ``` 88 89 ```html 90 <!--index.html--> 91 <!DOCTYPE html> 92 <html> 93 <head> 94 <title>测试网页</title> 95 </head> 96 <body> 97 <h1>editMenuOptions Demo</h1> 98 <span>edit menu options</span> 99 </body> 100 </html> 101 ``` 102  103## 上下文菜单 104上下文菜单是用户通过特定操作(如右键点击或长按富文本)触发的快捷菜单,用于提供与当前操作对象或界面元素相关的功能选项。菜单项纵向排列。系统未提供默认实现,若应用未实现,则不显示上下文菜单。应用需要创建一个[Menu](../reference/apis-arkui/arkui-ts/ts-basic-components-menu.md)组件并与Web绑定,在菜单弹出时可通过Web组件的[onContextMenuShow](../reference/apis-arkweb/arkts-basic-components-web-events.md#oncontextmenushow9)回调接口获取上下文菜单的详细信息,包括点击位置的HTML元素信息及点击位置信息。 105 1061. [Menu](../reference/apis-arkui/arkui-ts/ts-basic-components-menu.md)组件作为弹出的菜单,包含所有菜单项行为与样式。 1072. 使用bindPopup方法将Menu组件与Web组件绑定。当上下文菜单弹出时,将显示创建的Menu组件。 1083. 在onContextMenuShow回调中获取上下文菜单事件信息[onContextMenuShowEvent](../reference/apis-arkweb/arkts-basic-components-web-i.md#oncontextmenushowevent12)。其中param为[WebContextMenuParam](../reference/apis-arkweb/arkts-basic-components-web-WebContextMenuParam.md)类型,包含点击位置对应HTML元素信息和位置信息,result为[WebContextMenuResult](../reference/apis-arkweb/arkts-basic-components-web-WebContextMenuResult.md)类型,提供常见的菜单能力。 109 110```ts 111// xxx.ets 112import { webview } from '@kit.ArkWeb'; 113import { pasteboard } from '@kit.BasicServicesKit'; 114 115const TAG = 'ContextMenu'; 116 117@Entry 118@Component 119struct WebComponent { 120 controller: webview.WebviewController = new webview.WebviewController(); 121 private result: WebContextMenuResult | undefined = undefined; 122 @State linkUrl: string = ''; 123 @State offsetX: number = 0; 124 @State offsetY: number = 0; 125 @State showMenu: boolean = false; 126 uiContext: UIContext = this.getUIContext(); 127 128 @Builder 129 // 构建自定义菜单及触发功能接口 130 MenuBuilder() { 131 // 以垂直列表形式显示的菜单。 132 Menu() { 133 // 展示菜单Menu中具体的item菜单项。 134 MenuItem({ 135 content: '复制图片', 136 }) 137 .width(100) 138 .height(50) 139 .onClick(() => { 140 this.result?.copyImage(); 141 this.showMenu = false; 142 }) 143 MenuItem({ 144 content: '剪切', 145 }) 146 .width(100) 147 .height(50) 148 .onClick(() => { 149 this.result?.cut(); 150 this.showMenu = false; 151 }) 152 MenuItem({ 153 content: '复制', 154 }) 155 .width(100) 156 .height(50) 157 .onClick(() => { 158 this.result?.copy(); 159 this.showMenu = false; 160 }) 161 MenuItem({ 162 content: '粘贴', 163 }) 164 .width(100) 165 .height(50) 166 .onClick(() => { 167 this.result?.paste(); 168 this.showMenu = false; 169 }) 170 MenuItem({ 171 content: '复制链接', 172 }) 173 .width(100) 174 .height(50) 175 .onClick(() => { 176 let pasteData = pasteboard.createData('text/plain', this.linkUrl); 177 pasteboard.getSystemPasteboard().setData(pasteData, (error) => { 178 if (error) { 179 return; 180 } 181 }) 182 this.showMenu = false; 183 }) 184 MenuItem({ 185 content: '全选', 186 }) 187 .width(100) 188 .height(50) 189 .onClick(() => { 190 this.result?.selectAll(); 191 this.showMenu = false; 192 }) 193 } 194 .width(150) 195 .height(300) 196 } 197 198 build() { 199 Column() { 200 Web({ src: $rawfile("index.html"), controller: this.controller }) 201 // 触发自定义弹窗 202 .onContextMenuShow((event) => { 203 if (event) { 204 this.result = event.result 205 console.info("x coord = " + event.param.x()); 206 console.info("link url = " + event.param.getLinkUrl()); 207 this.linkUrl = event.param.getLinkUrl(); 208 } 209 console.info(TAG, `x: ${this.offsetX}, y: ${this.offsetY}`); 210 this.showMenu = true; 211 this.offsetX = 0; 212 this.offsetY = Math.max(this.uiContext!.px2vp(event?.param.y() ?? 0) - 0, 0); 213 return true; 214 }) 215 .bindPopup(this.showMenu, 216 { 217 builder: this.MenuBuilder(), 218 enableArrow: false, 219 placement: Placement.LeftTop, 220 offset: { x: this.offsetX, y: this.offsetY }, 221 mask: false, 222 onStateChange: (e) => { 223 if (!e.isVisible) { 224 this.showMenu = false; 225 this.result!.closeContextMenu(); 226 } 227 } 228 }) 229 } 230 } 231} 232``` 233```html 234<!-- index.html --> 235<!DOCTYPE html> 236<html lang="en"> 237<body> 238 <h1>onContextMenuShow</h1> 239 <a href="http://www.example.com" style="font-size:27px">超链接www.example.com</a> 240 <!--example.png为html同目录下图片--> 241 <div><img src="example.png"></div> 242 <p>选中文字鼠标右键弹出菜单</p> 243</body> 244</html> 245``` 246 247## 自定义菜单 248自定义菜单赋予开发者调整菜单触发时机与视觉展现的能力,使应用能够依据用户操作场景动态匹配功能入口,简化开发流程中的界面适配工作,同时使应用交互更符合用户直觉。应用可通过[bindSelectionMenu](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#bindselectionmenu13)接口,实现自定义菜单。目前已额外支持通过长按图片和链接响应自定义菜单。 2491. 创建[Menu](../reference/apis-arkui/arkui-ts/ts-basic-components-menu.md)组件作为菜单弹窗。 2502. 通过Web组件的[bindSelectionMenu](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#bindselectionmenu13)方法绑定MenuBuilder菜单弹窗。将[WebElementType](../reference/apis-arkweb/arkts-basic-components-web-e.md#webelementtype13)设置为WebElementType.IMAGE,[responseType](../reference/apis-arkweb/arkts-basic-components-web-e.md#webresponsetype13)设置为WebResponseType.LONG_PRESS,表示长按图片时弹出菜单。在[options](../reference/apis-arkweb/arkts-basic-components-web-i.md#selectionmenuoptionsext13)中定义菜单显示回调onAppear、菜单消失回调onDisappear、预览窗口preview和菜单类型menuType。 251```ts 252// xxx.ets 253import { webview } from '@kit.ArkWeb'; 254 255interface PreviewBuilderParam { 256 previewImage: Resource | string | undefined; 257 width: number; 258 height: number; 259} 260 261@Builder function PreviewBuilderGlobal($$: PreviewBuilderParam) { 262 Column() { 263 Image($$.previewImage) 264 .objectFit(ImageFit.Fill) 265 .autoResize(true) 266 }.width($$.width).height($$.height) 267} 268 269@Entry 270@Component 271struct WebComponent { 272 controller: webview.WebviewController = new webview.WebviewController(); 273 274 private result: WebContextMenuResult | undefined = undefined; 275 @State previewImage: Resource | string | undefined = undefined; 276 @State previewWidth: number = 0; 277 @State previewHeight: number = 0; 278 uiContext: UIContext = this.getUIContext(); 279 280 @Builder 281 MenuBuilder() { 282 Menu() { 283 MenuItem({ content: '复制', }) 284 .onClick(() => { 285 this.result?.copy(); 286 this.result?.closeContextMenu(); 287 }) 288 MenuItem({ content: '全选', }) 289 .onClick(() => { 290 this.result?.selectAll(); 291 this.result?.closeContextMenu(); 292 }) 293 } 294 } 295 build() { 296 Column() { 297 Web({ src: $rawfile("index.html"), controller: this.controller }) 298 .bindSelectionMenu(WebElementType.IMAGE, this.MenuBuilder, WebResponseType.LONG_PRESS, 299 { 300 onAppear: () => {}, 301 onDisappear: () => { 302 this.result?.closeContextMenu(); 303 }, 304 preview: PreviewBuilderGlobal({ 305 previewImage: this.previewImage, 306 width: this.previewWidth, 307 height: this.previewHeight 308 }), 309 menuType: MenuType.PREVIEW_MENU 310 }) 311 .onContextMenuShow((event) => { 312 if (event) { 313 this.result = event.result; 314 if (event.param.getLinkUrl()) { 315 return false; 316 } 317 this.previewWidth = this.uiContext!.px2vp(event.param.getPreviewWidth()); 318 this.previewHeight = this.uiContext!.px2vp(event.param.getPreviewHeight()); 319 if (event.param.getSourceUrl().indexOf("resource://rawfile/") == 0) { 320 this.previewImage = $rawfile(event.param.getSourceUrl().substr(19)); 321 } else { 322 this.previewImage = event.param.getSourceUrl(); 323 } 324 return true; 325 } 326 return false; 327 }) 328 } 329 } 330} 331``` 332```html 333<!--index.html--> 334<!DOCTYPE html> 335<html> 336 <head> 337 <title>测试网页</title> 338 </head> 339 <body> 340 <h1>bindSelectionMenu Demo</h1> 341 <!--img.png为html同目录下图片--> 342 <img src="./img.png" > 343 </body> 344</html> 345``` 346 347 348自API version 20起,支持绑定长按超链接菜单。可以为图片和链接绑定不同的自定义菜单。 349 350以下示例中,PreviewBuilder定义了超链接对应菜单的弹出内容,用Web组件加载了超链接内容,使用[Progress组件](../ui/arkts-common-components-progress-indicator.md)展示了加载进度。 351 352```ts 353import { webview } from '@kit.ArkWeb'; 354import { pasteboard } from '@kit.BasicServicesKit'; 355 356interface PreviewBuilderParam { 357 width: number; 358 height: number; 359 url:Resource | string | undefined; 360} 361 362interface PreviewBuilderParamForImage { 363 previewImage: Resource | string | undefined; 364 width: number; 365 height: number; 366} 367 368 369@Builder function PreviewBuilderGlobalForImage($$: PreviewBuilderParamForImage) { 370 Column() { 371 Image($$.previewImage) 372 .objectFit(ImageFit.Fill) 373 .autoResize(true) 374 }.width($$.width).height($$.height) 375} 376 377@Entry 378@Component 379struct SelectionMenuLongPress { 380 controller: webview.WebviewController = new webview.WebviewController(); 381 previewController: webview.WebviewController = new webview.WebviewController(); 382 @Builder PreviewBuilder($$: PreviewBuilderParam){ 383 Column() { 384 Stack(){ 385 Text("") // 可选择是否展示url 386 .padding(5) 387 .width('100%') 388 .textAlign(TextAlign.Start) 389 .backgroundColor(Color.White) 390 .copyOption(CopyOptions.LocalDevice) 391 .maxLines(1) 392 .textOverflow({overflow:TextOverflow.Ellipsis}) 393 Progress({ value: this.progressValue, total: 100, type: ProgressType.Linear }) // 展示进度条 394 .style({ strokeWidth: 3, enableSmoothEffect: true }) 395 .backgroundColor(Color.White) 396 .opacity(this.progressVisible?1:0) 397 .backgroundColor(Color.White) 398 }.alignContent(Alignment.Bottom) 399 Web({src:$$.url,controller: new webview.WebviewController()}) 400 .javaScriptAccess(true) 401 .fileAccess(true) 402 .onlineImageAccess(true) 403 .imageAccess(true) 404 .domStorageAccess(true) 405 .onPageBegin(()=>{ 406 this.progressValue = 0; 407 this.progressVisible = true; 408 }) 409 .onProgressChange((event)=>{ 410 this.progressValue = event.newProgress; 411 }) 412 .onPageEnd(()=>{ 413 this.progressVisible = false; 414 }) 415 .hitTestBehavior(HitTestMode.None) // 使预览Web不响应手势 416 }.width($$.width).height($$.height) // 设置预览宽高 417 } 418 419 private result: WebContextMenuResult | undefined = undefined; 420 @State previewImage: Resource | string | undefined = undefined; 421 @State previewWidth: number = 1; 422 @State previewHeight: number = 1; 423 @State previewWidthImage: number = 1; 424 @State previewHeightImage: number = 1; 425 @State linkURL:string = ""; 426 @State progressValue:number = 0; 427 @State progressVisible:boolean = true; 428 uiContext: UIContext = this.getUIContext(); 429 430 @Builder 431 LinkMenuBuilder() { 432 Menu() { 433 MenuItem({ content: '复制链接', }) 434 .onClick(() => { 435 const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.linkURL); 436 const systemPasteboard = pasteboard.getSystemPasteboard(); 437 systemPasteboard.setData(pasteboardData); 438 }) 439 MenuItem({content:'打开链接'}) 440 .onClick(()=>{ 441 this.controller.loadUrl(this.linkURL); 442 }) 443 } 444 } 445 @Builder 446 ImageMenuBuilder() { 447 Menu() { 448 MenuItem({ content: '复制图片', }) 449 .onClick(() => { 450 this.result?.copyImage(); 451 this.result?.closeContextMenu(); 452 }) 453 } 454 } 455 build() { 456 Column() { 457 Web({ src: $rawfile("index.html"), controller: this.controller }) 458 .javaScriptAccess(true) 459 .fileAccess(true) 460 .onlineImageAccess(true) 461 .imageAccess(true) 462 .domStorageAccess(true) 463 .bindSelectionMenu(WebElementType.LINK, this.LinkMenuBuilder, WebResponseType.LONG_PRESS, 464 { 465 onAppear: () => {}, 466 onDisappear: () => { 467 this.result?.closeContextMenu(); 468 }, 469 preview: this.PreviewBuilder({ 470 width: 500, 471 height: 400, 472 url:this.linkURL 473 }), 474 menuType: MenuType.PREVIEW_MENU, 475 }) 476 .bindSelectionMenu(WebElementType.IMAGE, this.ImageMenuBuilder, WebResponseType.LONG_PRESS, 477 { 478 onAppear: () => {}, 479 onDisappear: () => { 480 this.result?.closeContextMenu(); 481 }, 482 preview: PreviewBuilderGlobalForImage({ 483 previewImage: this.previewImage, 484 width: this.previewWidthImage, 485 height: this.previewHeightImage, 486 }), 487 menuType: MenuType.PREVIEW_MENU, 488 }) 489 .zoomAccess(true) 490 .onContextMenuShow((event) => { 491 if (event) { 492 this.result = event.result; 493 this.previewWidthImage = this.uiContext!.px2vp(event.param.getPreviewWidth()); 494 this.previewHeightImage = this.uiContext!.px2vp(event.param.getPreviewHeight()); 495 if (event.param.getSourceUrl().indexOf("resource://rawfile/") == 0) { 496 this.previewImage = $rawfile(event.param.getSourceUrl().substring(19)); 497 } else { 498 this.previewImage = event.param.getSourceUrl(); 499 } 500 this.linkURL = event.param.getLinkUrl() 501 return true; 502 } 503 return false; 504 }) 505 } 506 507 } 508 // 侧滑返回 509 onBackPress(): boolean | void { 510 if (this.controller.accessStep(-1)) { 511 this.controller.backward(); 512 return true; 513 } else { 514 return false; 515 } 516 } 517} 518``` 519html示例 520```html 521<html lang="zh-CN"><head> 522 <meta charset="UTF-8"> 523 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 524 <title>综合信息页面</title> 525</head> 526<body> 527<div> 528 <h1>综合信息与联系详情</h1> 529 <section> 530 <a href="https://www.example.com">EXAMPLE</a> 531 <br> 532 <a href="https://www.example1.com/">EXAMPLE1</a> 533 </section> 534</div> 535<footer> 536 <p>请注意,以上提供的所有网址仅供演示之用。</p> 537</footer> 538</body> 539</html> 540``` 541 542 543## Web菜单保存图片 5441. 创建MenuBuilder组件作为菜单弹窗,使用[SaveButton](../reference/apis-arkui/arkui-ts/ts-security-components-savebutton.md)组件实现图片保存,通过bindContextMenu将MenuBuilder与Web绑定。 5452. 在onContextMenuShow中获取图片url,通过copyLocalPicToDir或copyUrlPicToDir将图片保存至应用沙箱。 5463. 通过photoAccessHelper将应用沙箱中的图片保存至图库。 547 548 ```ts 549import { webview } from '@kit.ArkWeb'; 550import { common } from '@kit.AbilityKit'; 551import { fileIo as fs} from '@kit.CoreFileKit'; 552import { systemDateTime } from '@kit.BasicServicesKit'; 553import { http } from '@kit.NetworkKit'; 554import { photoAccessHelper } from '@kit.MediaLibraryKit'; 555 556@Entry 557@Component 558struct WebComponent { 559 saveButtonOptions: SaveButtonOptions = { 560 icon: SaveIconStyle.FULL_FILLED, 561 text: SaveDescription.SAVE_IMAGE, 562 buttonType: ButtonType.Capsule 563 } 564 controller: webview.WebviewController = new webview.WebviewController(); 565 @State showMenu: boolean = false; 566 @State imgUrl: string = ''; 567 context = this.getUIContext().getHostContext() as common.UIAbilityContext; 568 569 copyLocalPicToDir(rawfilePath: string, newFileName: string): string { 570 let srcFileDes = this.context.resourceManager.getRawFdSync(rawfilePath); 571 let dstPath = this.context.filesDir + "/" +newFileName; 572 let dest: fs.File = fs.openSync(dstPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 573 let bufsize = 4096; 574 let buf = new ArrayBuffer(bufsize); 575 let off = 0, len = 0, readedLen = 0; 576 while (len = fs.readSync(srcFileDes.fd, buf, { offset: srcFileDes.offset + off, length: bufsize })) { 577 readedLen += len; 578 fs.writeSync(dest.fd, buf, { offset: off, length: len }); 579 off = off + len; 580 if ((srcFileDes.length - readedLen) < bufsize) { 581 bufsize = srcFileDes.length - readedLen; 582 } 583 } 584 fs.close(dest.fd); 585 return dest.path; 586 } 587 588 async copyUrlPicToDir(picUrl: string, newFileName: string): Promise<string> { 589 let uri = ''; 590 let httpRequest = http.createHttp(); 591 let data: http.HttpResponse = await(httpRequest.request(picUrl) as Promise<http.HttpResponse>); 592 if (data?.responseCode == http.ResponseCode.OK) { 593 let dstPath = this.context.filesDir + "/" + newFileName; 594 let dest: fs.File = fs.openSync(dstPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 595 let writeLen: number = fs.writeSync(dest.fd, data.result as ArrayBuffer); 596 uri = dest.path; 597 } 598 return uri; 599 } 600 601 @Builder 602 MenuBuilder() { 603 Column() { 604 Row() { 605 SaveButton(this.saveButtonOptions) 606 .onClick(async (event, result: SaveButtonOnClickResult) => { 607 if (result == SaveButtonOnClickResult.SUCCESS) { 608 try { 609 let context = this.context; 610 let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); 611 let uri = ''; 612 if (this.imgUrl?.includes('rawfile')) { 613 let rawFileName: string = this.imgUrl.substring(this.imgUrl.lastIndexOf('/') + 1); 614 uri = this.copyLocalPicToDir(rawFileName, 'copyFile.png'); 615 } else if (this.imgUrl?.includes('http') || this.imgUrl?.includes('https')) { 616 uri = await this.copyUrlPicToDir(this.imgUrl, `onlinePic${systemDateTime.getTime()}.png`); 617 } 618 let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, uri); 619 await phAccessHelper.applyChanges(assetChangeRequest); 620 } 621 catch (err) { 622 console.error(`create asset failed with error: ${err.code}, ${err.message}`); 623 } 624 } else { 625 console.error(`SaveButtonOnClickResult create asset failed`); 626 } 627 this.showMenu = false; 628 }) 629 } 630 .margin({ top: 20, bottom: 20 }) 631 .justifyContent(FlexAlign.Center) 632 } 633 .width('80') 634 .backgroundColor(Color.White) 635 .borderRadius(10) 636 } 637 638 build() { 639 Column() { 640 Web({src: $rawfile("index.html"), controller: this.controller}) 641 .onContextMenuShow((event) => { 642 if (event) { 643 let hitValue = this.controller.getLastHitTest(); 644 this.imgUrl = hitValue.extra; 645 } 646 this.showMenu = true; 647 return true; 648 }) 649 .bindContextMenu(this.MenuBuilder, ResponseType.LongPress) 650 .fileAccess(true) 651 .javaScriptAccess(true) 652 .domStorageAccess(true) 653 } 654 } 655} 656 ``` 657 ```html 658<!--index.html--> 659<!DOCTYPE html> 660<html> 661<head> 662 <title>SavePicture</title> 663</head> 664<body> 665<h1>SavePicture</h1> 666<br> 667<br> 668<br> 669<br> 670<br> 671<!--startIcon.png为html同目录下图片--> 672<img src="./startIcon.png"> 673</body> 674</html> 675 ``` 676 677 678## Web菜单获取选中文本 679Web组件的[editMenuOptions](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#editmenuoptions12)接口中没有提供获取选中文本的方式。开发者可通过[javaScriptProxy](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#javascriptproxy)获取到JavaScript的选中文本,实现自定义菜单的逻辑。 6801. 创建SelectClass类,通过[javaScriptProxy](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#javascriptproxy)将SelectClass对象注册到Web组件中。 6812. 在Html侧注册选区变更监听器,在选区变更时通过SelectClass对象将选区设置到ArkTS侧。 682 ```ts 683import { webview } from '@kit.ArkWeb'; 684let selectText = ''; 685 686class SelectClass { 687 constructor() { 688 } 689 690 setSelectText(param: String) { 691 selectText = param.toString(); 692 } 693} 694 695@Entry 696@Component 697struct WebComponent { 698 webController: webview.WebviewController = new webview.WebviewController(); 699 @State selectObj: SelectClass = new SelectClass(); 700 @State textStr: string = ''; 701 702 build() { 703 Column() { 704 Web({ src: $rawfile('index.html'), controller: this.webController}) 705 .javaScriptProxy({ 706 object: this.selectObj, 707 name: 'selectObjName', 708 methodList: ['setSelectText'], 709 controller: this.webController 710 }) 711 .height('40%') 712 Text('Click here to get the selected text.') 713 .fontSize(20) 714 .onClick(() => { 715 this.textStr = selectText; 716 }) 717 .height('10%') 718 Text('Selected text is ' + this.textStr) 719 .fontSize(20) 720 .height('10%') 721 } 722 } 723} 724 ``` 725 ```html 726<!DOCTYPE html> 727<html> 728<head> 729 <title>Test Get Select</title> 730 <style> 731 body { 732 margin: 40px; 733 background-color: #f4f4f4; 734 } 735 .edit-container { 736 padding: 20px; 737 background-color: #fff; 738 border-radius: 8px; 739 box-shadow: 0 0 10px rgba(0,0,0,0.1); 740 margin: auto; 741 } 742 textarea { 743 width: 100%; 744 height: 400px; 745 font-size: 16px; 746 padding: 10px; 747 border: 1px solid #ccc; 748 border-radius: 4px; 749 } 750 </style> 751</head> 752<body> 753<div class="edit-container"> 754 <textarea placeholder="Enter the text here and select it by long pressing."></textarea> 755</div> 756<script> 757 document.addEventListener('selectionchange', () => { 758 var selection = window.getSelection(); 759 if(selection.rangeCount > 0) { 760 var selectedText = selection.toString(); 761 selectObjName.setSelectText(selectedText); 762 } 763 }) 764</script> 765</body> 766</html> 767 ``` 768 769 770## Web菜单识别图片二维码 771在二维码跳转页面或者付款场景中,开发者可通过实现上下文菜单,获取到[onContextMenuShow](../reference/apis-arkweb/arkts-basic-components-web-events.md#oncontextmenushow9)接口中的二维码图片信息进行处理,提供给用户扫描二维码入口。 7721. 创建MenuBuilder组件作为菜单弹窗,通过bindContextMenu将MenuBuilder与Web绑定。 7732. 在onContextMenuShow中获取图片url,通过copyLocalPicToDir或copyUrlPicToDir将图片保存至应用沙箱。 7743. 通过detectBarcode.decode解析保存在沙箱中的图片,获取到结果。 775 ```ts 776import { webview } from '@kit.ArkWeb'; 777import { common } from '@kit.AbilityKit'; 778import { fileIo as fs } from '@kit.CoreFileKit'; 779import { systemDateTime } from '@kit.BasicServicesKit'; 780import { http } from '@kit.NetworkKit'; 781import { scanCore, scanBarcode, detectBarcode } from '@kit.ScanKit'; 782import { BusinessError } from '@kit.BasicServicesKit'; 783 784@Entry 785@Component 786struct WebComponent { 787 saveButtonOptions: SaveButtonOptions = { 788 icon: SaveIconStyle.FULL_FILLED, 789 text: SaveDescription.SAVE_IMAGE, 790 buttonType: ButtonType.Capsule 791 } 792 controller: webview.WebviewController = new webview.WebviewController(); 793 private result: WebContextMenuResult | undefined = undefined; 794 @State showMenu: boolean = false; 795 @State imgUrl: string = ''; 796 @State decodeResult: string = ''; 797 context = this.getUIContext().getHostContext() as common.UIAbilityContext; 798 799 copyLocalPicToDir(rawfilePath: string, newFileName: string): string { 800 let srcFileDes = this.context.resourceManager.getRawFdSync(rawfilePath); 801 let dstPath = this.context.filesDir + "/" +newFileName; 802 let dest: fs.File = fs.openSync(dstPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 803 let bufsize = 4096; 804 let buf = new ArrayBuffer(bufsize); 805 let off = 0, len = 0, readedLen = 0; 806 while (len = fs.readSync(srcFileDes.fd, buf, { offset: srcFileDes.offset + off, length: bufsize })) { 807 readedLen += len; 808 fs.writeSync(dest.fd, buf, { offset: off, length: len }); 809 off = off + len; 810 if ((srcFileDes.length - readedLen) < bufsize) { 811 bufsize = srcFileDes.length - readedLen; 812 } 813 } 814 fs.close(dest.fd); 815 return dest.path; 816 } 817 818 async copyUrlPicToDir(picUrl: string, newFileName: string): Promise<string> { 819 let uri = ''; 820 let httpRequest = http.createHttp(); 821 let data: http.HttpResponse = await(httpRequest.request(picUrl) as Promise<http.HttpResponse>); 822 if (data?.responseCode == http.ResponseCode.OK) { 823 let dstPath = this.context.filesDir + "/" + newFileName; 824 let dest: fs.File = fs.openSync(dstPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE); 825 let writeLen: number = fs.writeSync(dest.fd, data.result as ArrayBuffer); 826 uri = dest.path; 827 } 828 return uri; 829 } 830 831 @Builder 832 MenuBuilder() { 833 Menu() { 834 MenuItem({ 835 content: "Scan QR Code", 836 }) 837 .width(200) 838 .height(50) 839 .onClick(async () => { 840 try { 841 let uri = ''; 842 if (this.imgUrl?.includes('rawfile')) { 843 let rawFileName: string = this.imgUrl.substring(this.imgUrl.lastIndexOf('/') + 1); 844 uri = this.copyLocalPicToDir(rawFileName, 'copyFile.png'); 845 } else if (this.imgUrl?.includes('http') || this.imgUrl?.includes('https')) { 846 uri = await this.copyUrlPicToDir(this.imgUrl, `onlinePic${systemDateTime.getTime()}.png`); 847 } 848 let options: scanBarcode.ScanOptions = { scanTypes: [scanCore.ScanType.ALL], enableMultiMode: true, enableAlbum: true } 849 let inputImage: detectBarcode.InputImage = { uri: uri }; 850 try { 851 // 调用图片识码接口 852 detectBarcode.decode(inputImage, options, (error: BusinessError, result: Array<scanBarcode.ScanResult>) => { 853 if (error && error.code) { 854 console.error(`create asset failed with error: ${error.code}, ${error.message}`); 855 return; 856 } 857 this.decodeResult = JSON.stringify(result); 858 }); 859 } catch (err) { 860 console.error(`Failed to detect Barcode. Code: ${err.code}, ${err.message}`); 861 } 862 } 863 catch (err) { 864 console.error(`create asset failed with error: ${err.code}, ${err.message}`); 865 } 866 }) 867 } 868 } 869 870 build() { 871 Column() { 872 Web({src: $rawfile("index.html"), controller: this.controller}) 873 .onContextMenuShow((event) => { 874 if (event) { 875 let hitValue = this.controller.getLastHitTest(); 876 this.imgUrl = hitValue.extra; 877 } 878 this.showMenu = true; 879 return true; 880 }) 881 .bindContextMenu(this.MenuBuilder, ResponseType.LongPress) 882 .fileAccess(true) 883 .javaScriptAccess(true) 884 .domStorageAccess(true) 885 .height('40%') 886 Text('Decode result is ' + this.decodeResult) 887 .fontSize(20) 888 .height('10%') 889 } 890 } 891} 892 ``` 893 ```html 894<!--index.html--> 895<!DOCTYPE html> 896<html> 897<head> 898 <title>test QR code</title> 899</head> 900<body> 901<h1>Long press and click to scan the QR code</h1> 902<!--img.png为二维码图片--> 903<img src="img.png" > 904</body> 905</html> 906 ``` 907 908 909## 常见问题 910### 如何禁用长按选择时弹出菜单 911可通过[editMenuOptions](../reference/apis-arkweb/arkts-basic-components-web-attributes.md#editmenuoptions12)接口将系统默认菜单全部过滤,此时无菜单项,则不会显示菜单。 912 ```ts 913// xxx.ets 914import { webview } from '@kit.ArkWeb'; 915@Entry 916@Component 917struct WebComponent { 918 controller: webview.WebviewController = new webview.WebviewController(); 919 920 onCreateMenu(menuItems: Array<TextMenuItem>): Array<TextMenuItem> { 921 let items = menuItems.filter((menuItem) => { 922 // 过滤用户需要的系统按键 923 return false; 924 }); 925 return items; 926 } 927 928 onMenuItemClick(menuItem: TextMenuItem, textRange: TextRange): boolean { 929 return false;// 返回默认值false 930 } 931 932 @State EditMenuOptions: EditMenuOptions = { onCreateMenu: this.onCreateMenu, onMenuItemClick: this.onMenuItemClick } 933 934 build() { 935 Column() { 936 Web({ src: $rawfile("index.html"), controller: this.controller }) 937 .editMenuOptions(this.EditMenuOptions) 938 } 939 } 940} 941 ``` 942 ```html 943<!--index.html--> 944<!DOCTYPE html> 945<html> 946 <head> 947 <title>测试网页</title> 948 </head> 949 <body> 950 <h1>editMenuOptions Demo</h1> 951 <span>edit menu options</span> 952 </body> 953</html> 954 ``` 955 956 957### 出现选区时手柄菜单不显示 958可排查是否通过JS的[selection-api](https://www.w3.org/TR/selection-api/)对选区进行了操作,目前通过这种方式改变选区会导致手柄菜单不显示。 959 960### 如何修改文本选中菜单的样式 961目前暂不支持修改文本选中菜单的具体样式。