1# SelectionMenu 2 3 4The **SelectionMenu** component is a context menu designed for use with the [RichEditor](ts-basic-components-richeditor.md) component, allowing you to bind a custom context menu on selection through the [bindSelectionMenu](./ts-basic-components-richeditor.md#bindselectionmenu) API. This component is not intended for standalone use, and you are advised to display it by right-clicking or by selecting text with a mouse device. 5 6 7> **NOTE** 8> 9> This component is supported since API version 11. Updates will be marked with a superscript to indicate their earliest API version. 10 11 12## Modules to Import 13 14``` 15import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI' 16``` 17 18## Child Components 19 20Not supported 21 22## SelectionMenu 23 24SelectionMenu(options: SelectionMenuOptions) 25 26Defines a custom context menu on selection. When the input parameter is empty, the sizes of the menu and its content area are 0, making the menu invisible. In this case, for example, if a right-click context menu is bound to the **RichEditor** component, it will not be displayed when the component is right-clicked. 27 28**Decorator**: @Builder 29 30**Atomic service API**: This API can be used in atomic services since API version 12. 31 32**System capability**: SystemCapability.ArkUI.ArkUI.Full 33 34**Parameters** 35 36| Name| Type| Mandatory| Description| 37| -------- | -------- | -------- | -------- | 38| options | [SelectionMenuOptions](#selectionmenuoptions) | Yes| Options of the context menu on selection.| 39 40## SelectionMenuOptions 41 42Defines the options of the context menu on selection. 43 44**Atomic service API**: This API can be used in atomic services since API version 12. 45 46**System capability**: SystemCapability.ArkUI.ArkUI.Full 47 48| Name| Type| Mandatory| Description| 49| -------- | -------- | -------- | -------- | 50| editorMenuOptions | Array<[EditorMenuOptions](#editormenuoptions)> | No| Edit menu.<br>If **editorMenuOptions** is not set, the edit menu is not displayed.<br>When both **action** and **builder** in **EditorMenuOptions** are configured, clicking the edit icon will trigger both.<br>By default, the context menu is not closed when the edit menu icon is clicked. You can configure **closeSelectionMenu** of **RichEditorController** in **action** to enable the menu to be closed.| 51| expandedMenuOptions | Array<[ExpandedMenuOptions](#expandedmenuoptions)> | No| Expanded drop-down menu options.<br>If this parameter is left empty, the expanded drop-down menu is not displayed.<br>The options configured for **ExpandedMenuOptions** are displayed in the **More** menu option, and clicking **More** shows the expanded drop-down menu.| 52| controller | [RichEditorController](ts-basic-components-richeditor.md#richeditorcontroller) | No| Rich text editor controller. If **controller** is set, the default system menu (including the cut, copy, and paste options) is displayed, and the preset menu features are provided.<br>If **controller** is left empty, the **More** menu option is not displayed. If **expandedMenuOptions** is not empty, the expanded drop-down menu is displayed.<br>By default, the copy and paste feature is only available for rich text. To use the feature for content that includes both text and images, define custom **onCopy** and **onPaste** APIs. If a custom **onCopy** \| **onPaste** API is defined, the default copy and paste feature is ineffective, and the custom API is called instead.<br>**NOTE**<br> When the preset copy option is selected, the custom context menu on selection is hidden, while the selected text is still highlighted.<br> When the preset select-all option is selected, the custom context menu on selection is hidden, while all text is highlighted.<br> When the preset paste option is selected, the style of the copied text is retained, whether the text is pasted to a blank area or not.<br> When the **copyOptions** attribute of the [RichEditor](ts-basic-components-richeditor.md) component is set to **CopyOptions.None**, the preset copy and cut features are not restricted.| 53| onCopy | (event?: [EditorEventInfo](#editoreventinfo)) => void | No| Event callback to take the place of the preset copy menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br> **event** indicates the returned information.| 54| onPaste | (event?: [EditorEventInfo](#editoreventinfo)) => void | No| Event callback to take the place of the preset paste menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br> **event** indicates the returned information.| 55| onCut | (event?: [EditorEventInfo](#editoreventinfo)) => void | No| Event callback to take the place of the preset cut menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br>**event** indicates the returned information.| 56| onSelectAll | (event?: [EditorEventInfo](#editoreventinfo)) => void | No| Event callback to take the place of the preset select-all menu option.<br>It is effective only when the **controller** parameter is set and the preset menu is available.<br>**NOTE**<br>**event** indicates the returned information.| 57 58 59## EditorMenuOptions 60 61Describes the edit menu options. 62 63**Atomic service API**: This API can be used in atomic services since API version 12. 64 65**System capability**: SystemCapability.ArkUI.ArkUI.Full 66 67| Name| Type| Mandatory| Description| 68| -------- | -------- | -------- | -------- | 69| icon | [ResourceStr](ts-types.md#resourcestr) | Yes| Icon.| 70| builder | () => void | No| Builder of the custom component displayed upon click. It must be used with @Builder for building the custom component.| 71| action | () => void | No| Action triggered when the menu option is clicked.| 72 73 74## ExpandedMenuOptions 75 76Describes the expanded drop-down menu options. 77 78Inherits from [MenuItemOptions](ts-basic-components-menuitem.md#menuitemoptions). 79 80**Atomic service API**: This API can be used in atomic services since API version 12. 81 82**System capability**: SystemCapability.ArkUI.ArkUI.Full 83 84| Name| Type| Mandatory| Description| 85| -------- | -------- | -------- | -------- | 86| action | () => void | No| Action triggered when the menu option is clicked.| 87 88## EditorEventInfo 89 90Provides the information about the selected content. 91 92**Atomic service API**: This API can be used in atomic services since API version 12. 93 94**System capability**: SystemCapability.ArkUI.ArkUI.Full 95 96| Name| Type| Mandatory| Description| 97| -------- | -------- | -------- | -------- | 98| content | [RichEditorSelection](ts-basic-components-richeditor.md#richeditorselection) | No| Information about the selected content.| 99 100## Attributes 101 102The [universal attributes](ts-universal-attributes-size.md) are not supported. The default width is 256 vp, and the height is adaptive. 103 104## Events 105The [universal events](ts-universal-events-click.md) are not supported. 106 107## Example 108 109```ts 110import { SelectionMenu, EditorMenuOptions, ExpandedMenuOptions, EditorEventInfo, SelectionMenuOptions } from '@kit.ArkUI' 111 112@Entry 113@Component 114struct Index { 115 @State select: boolean = true 116 controller: RichEditorController = new RichEditorController(); 117 options: RichEditorOptions = { controller: this.controller } 118 @State message: string = 'Hello word' 119 @State textSize: number = 30 120 @State fontWeight: FontWeight = FontWeight.Normal 121 @State start: number = -1 122 @State end: number = -1 123 @State visibleValue: Visibility = Visibility.Visible 124 @State colorTransparent: Color = Color.Transparent 125 @State textStyle: RichEditorTextStyle = {} 126 private editorMenuOptions: Array<EditorMenuOptions> = 127 [ 128 { icon: $r("app.media.ic_notepad_textbold"), action: () => { 129 if (this.controller) { 130 let selection = this.controller.getSelection(); 131 let spans = selection.spans 132 spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 133 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 134 let span = item as RichEditorTextSpanResult 135 this.textStyle = span.textStyle 136 let start = span.offsetInSpan[0] 137 let end = span.offsetInSpan[1] 138 let offset = span.spanPosition.spanRange[0] 139 if (this.textStyle.fontWeight != 11) { 140 this.textStyle.fontWeight = FontWeight.Bolder 141 } else { 142 this.textStyle.fontWeight = FontWeight.Normal 143 } 144 this.controller.updateSpanStyle({ 145 start: offset + start, 146 end: offset + end, 147 textStyle: this.textStyle 148 }) 149 } 150 }) 151 } 152 } }, 153 { icon: $r("app.media.ic_notepad_texttilt"), action: () => { 154 if (this.controller) { 155 let selection = this.controller.getSelection(); 156 let spans = selection.spans 157 spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 158 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 159 let span = item as RichEditorTextSpanResult 160 this.textStyle = span.textStyle 161 let start = span.offsetInSpan[0] 162 let end = span.offsetInSpan[1] 163 let offset = span.spanPosition.spanRange[0] 164 if (this.textStyle.fontStyle == FontStyle.Italic) { 165 this.textStyle.fontStyle = FontStyle.Normal 166 } else { 167 this.textStyle.fontStyle = FontStyle.Italic 168 } 169 this.controller.updateSpanStyle({ 170 start: offset + start, 171 end: offset + end, 172 textStyle: this.textStyle 173 }) 174 } 175 }) 176 } 177 } }, 178 { icon: $r("app.media.ic_notepad_underline"), 179 action: () => { 180 if (this.controller) { 181 let selection = this.controller.getSelection(); 182 let spans = selection.spans 183 spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 184 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 185 let span = item as RichEditorTextSpanResult 186 this.textStyle = span.textStyle 187 let start = span.offsetInSpan[0] 188 let end = span.offsetInSpan[1] 189 let offset = span.spanPosition.spanRange[0] 190 if (this.textStyle.decoration) { 191 if (this.textStyle.decoration.type == TextDecorationType.Underline) { 192 this.textStyle.decoration.type = TextDecorationType.None 193 } else { 194 this.textStyle.decoration.type = TextDecorationType.Underline 195 } 196 } else { 197 this.textStyle.decoration = { type: TextDecorationType.Underline, color: Color.Black } 198 } 199 this.controller.updateSpanStyle({ 200 start: offset + start, 201 end: offset + end, 202 textStyle: this.textStyle 203 }) 204 } 205 }) 206 } 207 } 208 }, 209 { icon: $r("app.media.app_icon"), action: () => { 210 }, builder: (): void => this.sliderPanel() }, 211 { icon: $r("app.media.ic_notepad_textcolor"), action: () => { 212 if (this.controller) { 213 let selection = this.controller.getSelection(); 214 let spans = selection.spans 215 spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 216 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 217 let span = item as RichEditorTextSpanResult 218 this.textStyle = span.textStyle 219 let start = span.offsetInSpan[0] 220 let end = span.offsetInSpan[1] 221 let offset = span.spanPosition.spanRange[0] 222 if (this.textStyle.fontColor == Color.Orange || this.textStyle.fontColor == '#FFFFA500') { 223 this.textStyle.fontColor = Color.Black 224 } else { 225 this.textStyle.fontColor = Color.Orange 226 } 227 this.controller.updateSpanStyle({ 228 start: offset + start, 229 end: offset + end, 230 textStyle: this.textStyle 231 }) 232 } 233 }) 234 } 235 } }] 236 private expandedMenuOptions: Array<ExpandedMenuOptions> = 237 [{ startIcon: $r("app.media.icon"), content: 'Dictionary', action: () => { 238 } }, { startIcon: $r("app.media.icon"), content: 'Translate', action: () => { 239 } }, { startIcon: $r("app.media.icon"), content: 'Search', action: () => { 240 } }] 241 private expandedMenuOptions1: Array<ExpandedMenuOptions> = [] 242 private editorMenuOptions1: Array<EditorMenuOptions> = [] 243 private selectionMenuOptions: SelectionMenuOptions = { 244 editorMenuOptions: this.editorMenuOptions, 245 expandedMenuOptions: this.expandedMenuOptions, 246 controller: this.controller, 247 onCut: (event?: EditorEventInfo) => { 248 if (event && event.content) { 249 event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 250 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 251 let span = item as RichEditorTextSpanResult 252 console.info('test cut' + span.value) 253 console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1]) 254 } 255 }) 256 } 257 }, 258 onPaste: (event?: EditorEventInfo) => { 259 if (event && event.content) { 260 event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 261 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 262 let span = item as RichEditorTextSpanResult 263 console.info('test onPaste' + span.value) 264 console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1]) 265 } 266 }) 267 } 268 }, 269 onCopy: (event?: EditorEventInfo) => { 270 if (event && event.content) { 271 event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 272 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 273 let span = item as RichEditorTextSpanResult 274 console.info('test cut' + span.value) 275 console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1]) 276 } 277 }) 278 } 279 }, 280 onSelectAll: (event?: EditorEventInfo) => { 281 if (event && event.content) { 282 event.content.spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 283 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 284 let span = item as RichEditorTextSpanResult 285 console.info('test onPaste' + span.value) 286 console.info('test start ' + span.offsetInSpan[0] + ' end: ' + span.offsetInSpan[1]) 287 } 288 }) 289 } 290 } 291 } 292 293 @Builder sliderPanel() { 294 Column() { 295 Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { 296 Text('A').fontSize(15) 297 Slider({ value: this.textSize, step: 10, style: SliderStyle.InSet }) 298 .width(210) 299 .onChange((value: number, mode: SliderChangeMode) => { 300 if (this.controller) { 301 let selection = this.controller.getSelection(); 302 if (mode == SliderChangeMode.End) { 303 if (this.textSize == undefined) { 304 this.textSize = 0 305 } 306 let spans = selection.spans 307 spans.forEach((item: RichEditorTextSpanResult | RichEditorImageSpanResult, index) => { 308 if (typeof (item as RichEditorTextSpanResult)['textStyle'] != 'undefined') { 309 this.textSize = Math.max(this.textSize, (item as RichEditorTextSpanResult).textStyle.fontSize) 310 } 311 }) 312 } 313 if (mode == SliderChangeMode.Moving || mode == SliderChangeMode.Click) { 314 this.start = selection.selection[0] 315 this.end = selection.selection[1] 316 this.textSize = value 317 this.controller.updateSpanStyle({ 318 start: this.start, 319 end: this.end, 320 textStyle: { fontSize: this.textSize } 321 }) 322 } 323 } 324 }) 325 Text('A').fontSize(20).fontWeight(FontWeight.Medium) 326 }.borderRadius($r('sys.float.ohos_id_corner_radius_card')) 327 } 328 .shadow(ShadowStyle.OUTER_DEFAULT_MD) 329 .backgroundColor(Color.White) 330 .borderRadius($r('sys.float.ohos_id_corner_radius_card')) 331 .padding(15) 332 .height(48) 333 } 334 335 @Builder 336 MyMenu() { 337 Column() { 338 SelectionMenu(this.selectionMenuOptions) 339 } 340 .width(256) 341 .backgroundColor(Color.Transparent) 342 } 343 344 @Builder 345 MyMenu2() { 346 Column() { 347 SelectionMenu({ 348 editorMenuOptions: this.editorMenuOptions, 349 expandedMenuOptions: this.expandedMenuOptions1, 350 controller: this.controller, 351 }) 352 } 353 .width(256) 354 .backgroundColor(Color.Transparent) 355 } 356 357 @Builder 358 MyMenu3() { 359 Column() { 360 SelectionMenu({ 361 editorMenuOptions: this.editorMenuOptions1, 362 expandedMenuOptions: this.expandedMenuOptions, 363 controller: this.controller, 364 }) 365 } 366 .width(256) 367 .backgroundColor(Color.Transparent) 368 } 369 370 build() { 371 Column() { 372 Button("SetSelection") 373 .onClick((event: ClickEvent) => { 374 if (this.controller) { 375 this.controller.setSelection(0, 2) 376 } 377 }) 378 379 RichEditor(this.options) 380 .onReady(() => { 381 this.controller.addTextSpan(this.message, { style: { fontColor: Color.Orange, fontSize: 30 } }) 382 this.controller.addTextSpan(this.message, { style: { fontColor: Color.Black, fontSize: 25 } }) 383 }) 384 .onSelect((value: RichEditorSelection) => { 385 if (value.selection[0] == -1 && value.selection[1] == -1) { 386 return 387 } 388 this.start = value.selection[0] 389 this.end = value.selection[1] 390 }) 391 .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu3(), RichEditorResponseType.RIGHT_CLICK) 392 .bindSelectionMenu(RichEditorSpanType.TEXT, this.MyMenu2(), RichEditorResponseType.SELECT) 393 .borderWidth(1) 394 .borderColor(Color.Red) 395 .width(200) 396 .height(200) 397 } 398 } 399} 400``` 401> **NOTE** 402> 403> Icons in bold and italics are not preset in the system. The sample code uses the default icons. You need to replace the icons in **editorMenuOptions** with the desired icons. 404 405 406